phpunserializechain_tools:链挖掘与 PoC 生成(深度复盘)
June 26, 2026 · View on GitHub
本文档描述插件 php_unserialize_chain_tools 的完整工作流:如何从 PHP 代码构建 Codedb(DataFlow DB),如何以“可触发魔术方法”为入口挖掘反序列化利用链,以及如何把链信息渲染为可执行的 PoC。
相关代码入口:
- 插件入口与参数:core/plugins/phpunserializechain/main.py
- Codedb 生成:core/plugins/phpunserializechain/dataflowgenerate.py
- DataFlow 动态表:web/index/models.py
1. 使用方式与前置条件
1.1 前置条件
- 插件依赖 Django ORM,会动态创建数据表;首次运行前需完成数据库初始化:
python kunlun.py init initialize
1.2 运行方式
python kunlun.py plugin php_unserialize_chain_tools -t <target_path>
常用参数:
-t/--target:扫描目标(建议传目录;若传入文件路径,会自动使用其所在目录作为目标与默认输出目录)-r/--renew:重建 DataFlow DB(目标第一次跑或代码变化较大时建议使用)-o/--output:自定义输出目录(默认<target>/.kunlunm_unserialize_poc/)
2. 总体流程(从目标到 PoC)
插件主流程非常直接:
- 构建或加载 DataFlow DB(Codedb)
- 从“可触发的魔术方法”出发挖掘调用链,得到
available_chains - 将每条链渲染成
chain_XX.php,并生成summary.json与批量执行器
代码对应:
- 主入口:PhpUnSerChain.main
- 链挖掘入口:get_unserialize_magic_method
- PoC 生成入口:generate_poc_files
3. Codedb(DataFlow DB)是如何生成的
3.1 DataFlow DB 的本质
插件不会直接在 AST 上做复杂的跨文件解析,而是先把“类/方法/调用/赋值/控制结构”等关键节点,按一定规则写入一张 DataFlow 表,后续链挖掘只查询这张表完成递归搜索。
DataFlow 表字段(动态 Model):
node_locate:节点定位字符串(“文件/类/方法”层级路径)node_sort:同一 locate 下的顺序编号(用于筛选方法体)source_node:源表达式(例如Method-__destruct、MethodCall-$this->a->b->foo等)node_type:节点类型(例如newMethod、MethodCall、Assignment等)sink_node:汇表达式(例如调用参数、赋值右侧、控制结构条件等)
3.2 DataFlow 表名如何确定
当你传入目标目录 <target_path> 时,插件取“目标路径最后一段名称”作为表名后缀:
DataFlow_<targetName>
实现:
- 目标名解析:DataflowGenerate.main
- 创建/重建逻辑:_get_dataflow_model
-r/--renew 会删除并重建该表(仅针对对应目标名的 DataFlow 表)。
3.3 代码解析与写表步骤
当 DataFlow 表为空(首次扫描)时,插件会执行 new_dataflow():
- 解析 target:复用 Kunlun 的
ParseArgs来确定目录与 zip 解压逻辑(此插件固定按 PHP 扫描) - 枚举文件:
Directory.collect_files() - AST 预处理:
ast_object.init_pre(...)+ast_object.pre_ast_all(['php']) - 遍历 PHP 文件:对每个文件
ast_object.get_nodes(filename)得到节点列表 - 构造
base_locate:将文件路径里的/\\.替换为#/_,形成文件级定位前缀 - 调用
base_dataflow_generate(all_nodes, base_locate)将 AST 节点转成 DataFlow 行(保存在self.dataflows) - 批量写入 DB(并对地址引用
&123做全局偏移,避免不同文件的引用 id 冲突)
核心代码:
4. 链挖掘:从魔术方法入口开始
4.1 为什么从魔术方法开始
反序列化利用链往往不要求代码中存在显式 unserialize() 调用点;更常见的触发点是对象反序列化后的生命周期与魔术方法(如 __wakeup/__destruct/__toString/__call 等)。
插件的策略是:
- 不找
unserialize的调用 - 直接枚举“可能在反序列化场景中被触发”的魔术方法作为入口
入口集合:get_unserialize_magic_method
4.2 入口节点如何定位
对每一个入口魔术方法名 __xxx:
- 在 DataFlow 表中查询
node_type='newMethod'且source_node前缀为Method-__xxx - 每个命中的
newMethod节点都作为一条链的起点 - 以
node.node_locate + '.' + node.source_node为前缀,再取出该方法体内的所有子节点作为搜索空间
实现:get_unserialize_magic_method
4.3 deep_search_chain:递归搜索骨架
deep_search_chain 是链挖掘的核心递归函数,输入是“某个方法体中的 DataFlow 节点集合”,输出是“是否找到可用链”:
- 每处理一个节点,优先检查是否命中危险 sink(命中即成功)
- 若遇到“可控的 MethodCall”,则将该调用加入链并跟入目标方法继续深搜
- 在分支失败时,会回滚链与关系快照,继续搜索同一方法体的其他节点
入口与签名:deep_search_chain
硬阈值:
deepth > 40直接终止(防止无限递归)deep_search_chain
5. “危险 sink”判定与“参数可控性”判定
5.1 危险 sink 判定(check_danger_sink)
插件内置两类 sink:
- 典型 RCE/写文件类 sink(需要参数可控)
call_user_funccall_user_func_arrayevalsystemfile_put_contentscreate_function
每个 sink 会指定“必须可控”的参数下标(例如 eval 的第 0 个参数),逐一调用 check_param_controllable(...)。
- CTF 风格输出 sink(弱判定)
echo/printf/print_r/var_dump/die/exit- 输出内容若命中
flag|ctf\{|$flag|key|secret也会视为 sink
5.2 参数可控性判定(check_param_controllable)
这个函数决定“某个调用/赋值的表达式是否可由反序列化 payload 控制”。主要判定路径:
- 若表达式含
Variable-$this->...:直接认为可控(对象属性可控) - 若表达式是局部变量:尝试在同一 locate 中向上找
Assignment回溯来源,直到找到$this->...或形参来源 - 若表达式匹配当前方法的
newMethodparams:认为形参可控(由上层调用传入) - 内部用
trace_stack做去重防环(避免重复回溯同一参数/节点)
6. 方法跟入、动态调用与隐式触发
6.1 只在“调用目标可控”时跟入 MethodCall
链扩展的关键分支是:
- 当前节点是
MethodCall - 且
source_node可控(通常意味着$this->a->b->foo(...)的$this->a->b可控) 满足则:
- 把该节点 append 到链
unserchain - 抽取属性路径与属性名(用于后续 PoC 对象图渲染)
- 尝试在“当前类”中找到对应方法定义并递归进入其方法体
- 若找不到该方法,则尝试
__call或去继承链中找原型类实现 - 失败则回滚关系快照并
unserchain.pop(),继续扫描其他节点
主逻辑位置:deep_search_chain MethodCall 分支
6.2 动态属性/动态方法名(b / $this->a->b)
插件额外支持一部分“动态链”:
- 当检测到
source_node出现多段->,并且$this->a可控时,会进入动态分支 - 如果方法名本身是变量且可控,会尝试“任意类任意方法”搜索(风险更高,但能覆盖更多链)
- 否则会尝试 “任意方法名匹配” 或通过
__call兜底
实现:
- 动态判定:check_dynamic_class_var_exist
- 动态分支处理:deep_search_chain 动态分支
6.3 赋值与控制结构触发 __get/__set 等
插件并不只看“显式 MethodCall”:
- 赋值左值为动态属性时可能触发
__set(prop, value),会尝试跟入__set - 右值读取动态属性时可能触发
__get或__call,会尝试从sink_node继续跟踪 - 控制结构(if/while/try 等)里出现动态属性访问,也会触发相同的跟踪逻辑
实现:deep_search_chain Assignment/switch 分支
7. 关系与属性:PoC 渲染的“数据燃料”
7.1 analysis_properties:可控属性名集合
链挖掘过程中会持续从表达式里提取 ->prop 形式的属性名,写入 current_chain_properties,最终固化到链结果的 analysis_properties 字段。
实现:
7.2 recursive_relations:对象关系路径(把下一跳对象挂在哪里)
当挖链命中一个“可控 MethodCall”并准备跟入时,插件会从 source_node/sink_node 里抽取属性访问路径(例如 ['a','b']),并记录一条 relation:
from_class:当前类to_method:将要跟入的方法property_path:用于对象图连边的路径(会对长路径进行“去掉末段”的规整)deepth:递归深度(用于调试/理解链结构)
8. 链结果结构与去重策略
8.1 链结果(available_chains)结构
每条链最终会被固化成一个 dict,核心字段包括:
chain_id:入口类/方法与 sink 的简要标识entry_class/entry_methodclass_sequence:链上的类序列(去重后)method_sequence:链上的方法序列(去重后,主要为newMethod*)chain_items:链节点详细列表(每个节点包含 type/locate/source/sink 等)recursive_relations:对象连边路径列表(见上一节)analysis_properties:可控属性名列表(见上一节)
实现:parse_chain_nodes、record_available_chain
8.2 去重(fingerprint)
为了避免同一条链被重复输出,插件对 chain_items 做 JSON 指纹:
fingerprint = json.dumps(chain_items, sort_keys=True, ensure_ascii=False)- 若 fingerprint 已存在则跳过记录
9. PoC 生成:从链数据到可执行 PHP
9.1 输出目录与生成文件
输出根目录:
-o指定则使用-o- 否则
<target>/.kunlunm_unserialize_poc/
生成文件:
php_unserialize_chain_summary.json:所有链的汇总chain_XX.php:每条链一个 PoCpoc_all_chains.php:批量执行器(按文件名顺序执行所有 chain)
9.2 单链 PoC 的结构(chain_XX.php)
单链文件的主要内容:
- class stub:为链上的每个类生成“若不存在则定义”的空类(避免运行时报类缺失)
build_payload_chain_XX():构造对象图、设置可控属性、输出serialize($root)结果- trigger code:根据入口魔术方法补充触发语句(可选)
渲染入口:render_chain_php
9.3 对象图构造(关系路径优先 + 回退)
对象图连边的路径选择顺序:
- 优先使用
recursive_relations[].property_path - 若为空,则从链节点里临时抽取路径并补
['next']兜底 - 若仍无路径,则输出注释提示手动补充
实现:
- 关系路径抽取:build_relation_paths_from_recursive
- 回退路径构造:build_recursive_relation_paths
- 生成对象图函数:render_chain_function
9.4 可控属性赋值(analysis_properties 优先 + 回退)
PoC 会尽量把链中出现的属性名设为字符串 payload(例如 PAYLOAD_prop)以触发后续方法调用/危险 sink。
属性名来源顺序:
- 优先使用链结果的
analysis_properties - 若为空,则从链节点
source_node/sink_node再抽一遍 - 若仍为空,PoC 中会提示“未找到属性名,需要手动补充”
提取函数:extract_controllable_properties
9.5 触发语法(按入口魔术方法生成)
插件会按入口方法生成可读的“触发提示”(hint),默认不在 PoC 内主动触发魔术方法:
__toString:提示(string)$root;__call:提示$root->undefinedMethod('PAYLOAD_CALL');__invoke:提示$root();__wakeup:提示“目标侧 unserialize() 会自动触发 __wakeup”__destruct/default:提示“由对象生命周期触发”
10. 输出文件说明(summary.json)
php_unserialize_chain_summary.json 用于辅助人工复核与二次处理,包含:
- 生成时间
generated_at - 目标
target - 链数量
chain_count - 链数组
chains[](每个元素是第 8 节描述的链结构)
11. 局限性与常见坑
11.1 结果可执行性不等价于真实可利用
- 插件是“链挖掘与 PoC 生成辅助工具”,其成功条件是“命中 sink + 判定参数可控”,并不保证真实环境下可利用(例如依赖运行时类型、魔术方法触发条件、框架上下文等)。
11.2 可控性判定是启发式的
- 以
$this->...为强可控信号 - 以形参为可控信号(来自上层调用)
- 以赋值回溯做局部传播
这会带来误报与漏报的可能,需要结合
summary.json与生成的chain_XX.php人工确认。
11.3 DataFlow 表名与目标选择
- DataFlow 表名来自目标目录名;若目标传文件路径,表名可能含
.等字符导致建表问题,因此建议传目录(插件已对文件 target 做了自动降级到目录)。