静态分析流水线:reverse → parse(调用链入 Neo4j)→ analyze(sink + 污点检测)。
sast/
├── reverse/ # 逆向:下载源码 / 反编译
├── parse/ # JavaParseIr → parse_ir → Neo4j 调用图
├── analyze/ # 分析检测:sink 识别 + 简单污点
├── target/ # 默认输入 JAR
├── tmpwork/ # 纯源码输出
└── docker-compose.yml # 不起服务;Neo4j 用本机已有容器
项目不会再起 Neo4j,只连 bolt://127.0.0.1:7687(免密)。搭建命令:
docker run -d \
--name sast-neo4j \
--publish=7474:7474 \
--publish=7687:7687 \
-m 6G \
-e NEO4J_server_memory_heap_initial__size=512m \
-e NEO4J_server_memory_heap_max__size=8G \
-e NEO4J_server_memory_pagecache_size=4G \
-e NEO4J_server_memory_transaction_total__max=8G \
-e NEO4J_server_config_strict__validation_enabled=false \
-e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
-e NEO4J_PLUGINS='["apoc"]' \
-e NEO4J_AUTH=none \
neo4j:2026.05.0-enterpriseDocker Desktop 内存约 8G 时不要用 heap 4G + pagecache 4G(会直接起不来)。 容器停了用
docker start <容器名>恢复即可。Browser: http://localhost:7474
# 1. 确认本机 Neo4j 已在跑(见上一节),不要为本项目再起容器
# 2. 安装依赖
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# 3. reverse:默认读 target/,结果写到 tmpwork/
# 胖 JAR 拆 BOOT-INF/lib → 优先下源码,失败则 CFR;业务 class 始终 CFR
# 源码缓存默认:tmpwork/source_cache/(按 GAV,避免重复下载)
python run_reverse.py
# 依赖源码失败时跳过 CFR(更快,但缺源码)
python run_reverse.py --no-decompile-libs
# 指定输入 / 仅当前层
python run_reverse.py -i /path/to/app.jar
python run_reverse.py -i /path/to/libs --no-recursive
# 4. parse:JavaParseIr 产出 parse_ir.json → Python objects → Neo4j
# 首次需 JDK 17+:
bash parse/tools/build_java_parse_ir.sh
python run_parse.py -p JavaTarget
# 默认自动合并 JDK21 源码(输入已有 java/lang/Object.java 则跳过)
# 默认挂 teamctiy_lib 作 JarTypeSolver;大树 = 单 JVM 先索引 jar 再 --threads 解析源码
# python run_parse.py -p JavaTarget --no-jdk # 关闭 JDK 合并
# python run_parse.py -p JavaTarget --no-jars # 关闭默认 jar-dir
# python run_parse.py -p JavaTarget --jdk-home /path # 指定带 lib/src.zip 的 JDK
# python run_parse.py -p JavaTarget --jar-dir /other # 覆盖默认 jar 目录
# 可选:保留中间 IR
# python run_parse.py -p JavaTarget --no-import \
# --dump-parse-ir tmpwork/ir/parse_ir.json \
# --dump-json tmpwork/ir/parse_ir_objects.json
# 5. analyze:两种污点模式
# vuln = 找漏洞(source=方法参数)
python run_analyze.py -p JavaTarget --mode vuln --dump-json tmpwork/analyze_vuln.json --report
# gadget = 找 gadget(source=类字段 + readObject 入口)
python run_analyze.py -p JavaTarget --mode gadget --dump-json tmpwork/analyze_report.json --report
open tmpwork/analyze_report.html| 模块 | 职责 |
|---|---|
reverse |
JAR 中心拿源码 / CFR;输出纯源码树 app/ + lib/ |
parse |
JavaParseIr → parse_ir objects → Neo4j 调用链 |
analyze |
sink 检测 + 简单污点(参数/字段经赋值传播到 exec/readObject) |
对 Spring Boot / 胖 JAR 输出纯源码树(不含 jar/class):
tmpwork/<app>/
app/ # 业务 .java(包路径)
resources/ # 配置等
lib/<dependency>/ # 依赖源码(下载或反编译)
- 解包到
tmpwork/.unpack/(完成后删除) - 依赖并行拉源码 →
lib/<name>/ - 业务 class CFR →
app/ - 下载失败的依赖再 CFR(
--no-decompile-libs可关)
对每个普通 JAR,识别顺序:
- 同目录旁路
foo-1.0-sources.jar META-INF/MANIFEST.MF(Implementation-* / Bundle-*)META-INF/maven/**/pom.properties- JAR SHA1 / 文件名搜索 Maven Central
*-SNAPSHOT跳过远程下载;都没有再 CFR
目录输入默认 --recursive。
| 概念 | 是否通用 | 说明 |
|---|---|---|
| Call site | ✅ 通用 | 调用发生的位置(那一行 .foo(...) / new Foo()) |
| Caller / Callee | ✅ 通用 | 调用方方法 / 被调方方法 |
| Call graph / call edge | ✅ 通用 | 方法→方法的调用关系(概念) |
关系类型 CALLS |
Method→Method 的 call edge;也有人写 INVOKES |
|
CallSite 节点 |
✅ 常见 | 把 call site 建成独立节点 |
HAS_CALL_SITE / RESOLVED_TO |
❌ 本项目命名 | 刻意避开和 CALLS 撞名:拥有调用点 / 解析到目标方法 |
一句话:Call site / call graph 是通用术语;CALLS(方法→方法)、HAS_CALL_SITE(方法→调用点)、RESOLVED_TO(调用点→方法)是本仓库关系名。
例如在 GadgetVulController#upper 里:
new TestUpVul().test(vul); // 第 50 行upper(Method) --HAS_CALL_SITE--> CallSite(line=50, receiver=new TestUpVul(),
callee_name=test, arguments=[vul],
caller_qn=...#upper, resolved_qn=...#test)
| |
| +--RESOLVED_TO--> test(Method)
|
+--CALLS--> test(Method) // 方法→方法简图,便于走链
| 关系 | 从 → 到 | 干什么 |
|---|---|---|
HAS_CALL_SITE |
Method → CallSite | 这个方法里有一次调用记录(细节) |
RESOLVED_TO |
CallSite → Method | 这次调用解析到哪个方法 |
CALLS |
Method → Method | 调用图简边(不经 CallSite,方便 CALLS*1..n) |
三者描述同一调用的不同侧面,不是互相矛盾的两套数据。
| 属性 | 含义 |
|---|---|
caller_qn |
所在方法(谁发起的调用) |
callee_name |
语法上的被调名(如 test) |
receiver |
. 左边(如 new TestUpVul() / vul) |
arguments |
实参文本列表 |
line |
源码行号 |
resolved_qn |
SymbolSolver 解析出的目标方法全名 |
target_qn |
导入时选中的主目标(通常同 resolved_qn) |
is_constructor |
是否 new Xxx(...) |
is_sink |
Method:该方法是否调用了 Tabby sink;CallSite:该调用点是否命中 Tabby sink |
(:Project)-[:HAS_FILE]->(:File)-[:DECLARES]->(:Type)
(:Type)-[:HAS_METHOD]->(:Method)-[:HAS_PARAM]->(:Parameter)
(:Type)-[:HAS_FIELD]->(:Field)
(:Type)-[:EXTENDS|IMPLEMENTS]->(:Type) # 继承 / 实现(已落库)
(:Field)-[:DECLARED_TYPE]->(:Type) # 字段声明类型
(:Field)-[:POINTS_TO]->(:Type) # 声明类型 + CHA 子类型
(:Type)-[:MAY_REF {field, serializable_write}]->(:Type) # 对象图快捷边
(:Method)-[:CALLS]->(:Method)
(:Method)-[:HAS_CALL_SITE]->(:CallSite)-[:RESOLVED_TO]->(:Method)
(:Finding)-[:IN_METHOD]->(:Method)
# 方法级调用链(走 CALLS)
MATCH (a:Method)-[:CALLS*1..5]->(b:Method)
WHERE a.project = 'JavaTarget'
RETURN a.qualified_name, b.qualified_name LIMIT 50
# 某方法里的调用点(走 HAS_CALL_SITE → CallSite)
MATCH (m:Method {name:'upper'})-[:HAS_CALL_SITE]->(cs:CallSite)
RETURN cs.caller_qn, cs.line, cs.receiver, cs.callee_name, cs.resolved_qn
ORDER BY cs.line
# 对象图:字段可能指向(MAY_REF)
MATCH (a:Type)-[r:MAY_REF]->(b:Type)
WHERE a.project = 'CC_JDK8' AND a.name = 'LazyMap'
RETURN a.name, r.field, b.name, r.serializable_write LIMIT 30
# 污点发现
MATCH (f:Finding {project:'JavaTarget'})
RETURN f.sink_name, f.method_qn, f.sink_line, f.sink_arg, f.source_kind完整说明(gadget source / 赋值传播):→ [docs/taint.md](docs/taint.md)
| 模式 | CLI | Source | 额外规则 |
|---|---|---|---|
| vuln(找漏洞) | --mode vuln |
方法参数 | 不把类字段默认当污点;readObject() 调用仅当 receiver 已被参数污染才报 |
| gadget(找 gadget) | --mode gadget |
类字段 + 方法参数 | readObject/readExternal 为反序列化入口;字段默认攻击者可控 |
- Sink:对齐 Tabby
rules/sinks.json(本地rules/sinks.json);按 类 + 方法 匹配,优先CallSite.resolved_qn - 传播:赋值 RHS 出现污点标识符即污染 LHS(如
x1 = xxx + x2) - 分析流程(调用关系串起来):
- A 找调用了 Tabby sink 的方法(gadget 下
readObject/readExternal入口也算) - B 对这些方法做过程内污点 → 得到确认可利用的方法
- C 只对确认方法查调用链:Entry → sink(动态 CHA + stitch_mid 双向拼接,见下节文档)
- C2 查字段对象图路径(
MAY_REF,见下方 FAQ) - D 对链上额外方法再做一轮污点(仍过程内)
- A 找调用了 Tabby sink 的方法(gadget 下
- 默认模式见
analyze/config.py的TAINT_MODE(当前vuln) --no-import时只做 A+B,没有调用链 / 对象图
Gadget 寻找专项(符号解析 / 污点经验 / 场景 playbook):
→ [docs/gadget/](docs/gadget/README.md)
调用链算法说明(动态 CHA、stitch_mid、为何不做「全世界构造器」):
→ [docs/stitch/](docs/stitch/README.md)(查链总目录:overview / stitch_mid / 动态 CHA / frontier)
→ [docs/taint.md](docs/taint.md)(过程内污点)
→ [docs/method_white_filter.md](docs/method_white_filter.md)(方法白过滤 / 高扇出 CHA)
→ [docs/parse_sharding.md](docs/parse_sharding.md)(大树分片 / 跨包 CALLS 错绑与 TEAMCITY_XS 经验)
指定两个方法 QN,在 Neo4j 图上做 CALLS + OVERRIDES(CHA) 双向 BFS,适合「从 A 到 B 有没有路」。
(批量 entry→sink / stitch 用 analyze/dynamic_cha_chains.py,见 [docs/stitch/](docs/stitch/README.md)。)
# 例:TiedMapEntry#getValue → Method#invoke
python3 tools/find_call_path.py -p TEAMCITY_XS \
--from 'org.apache.commons.collections.keyvalue.TiedMapEntry#getValue()' \
--to 'java.lang.reflect.Method#invoke(Object,)' \
--path-num 10 --max-depth 12 \
-o tmpwork/getvalue_to_invoke.txt
# 例:BeanMap#get → Method#invoke
python3 tools/find_call_path.py -p TEAMCITY_XS \
--from 'org.apache.commons.beanutils.BeanMap#get(Object)' \
--to 'java.lang.reflect.Method#invoke(Object,)' \
--path-num 5| 参数 | 含义 | 默认 |
|---|---|---|
-p / --project |
Neo4j project |
必填 |
--from |
起点方法 qualified_name | 必填 |
--to |
终点方法 qualified_name | 必填 |
--path-num |
过滤后最多返回几条路径 | 5 |
--max-depth |
最大跳数 | 12 |
--cha-max |
每个虚调用 CHA 扇出上限(0=不截断) |
CHA_MAX_CALLEES(100) |
--prefer |
CHA 截断时优先保留的子串(逗号分隔) | 空 |
--sink-caller-tag |
invoke 直接 caller 须为 true 的 Method 布尔属性;空串关闭 | is_sink |
-o |
写入文本文件 | 仅打印 |
-v |
DEBUG 日志 | off |
QN 必须与图里一致(含签名括号)。不确定时:
MATCH (m:Method {project:'TEAMCITY_XS'})
WHERE m.qualified_name CONTAINS 'BeanMap#get'
RETURN m.qualified_name LIMIT 10高扇出槽(如 Map#get)会按 Method.is_method_white 过滤(见 [docs/method_white_filter.md](docs/method_white_filter.md))。更多 XStream 工具见 [tools/xstream_teamcity/README.md](tools/xstream_teamcity/README.md)。
封装 analyze.dynamic_cha_chains.DynamicChaChainFinder(与 analyze 同一套:CALLS + 按需 CHA + stitch)。
适合 Map#get → Method#invoke 这类会 CHA 展开的查询:自动把 frontier 的 OVERRIDES 当 entry。
# Map#get → Method#invoke(推荐写法)
python3 tools/find_cha_chains.py -p TEAMCITY_XS \
--from 'java.util.Map#get(Object)' \
--to 'java.lang.reflect.Method#invoke(Object,)' \
--path-num 30 -o tmpwork/map_get_to_invoke.txt
# 简写别名
python3 tools/find_cha_chains.py -p TEAMCITY_XS \
--from 'Map#get' --to 'Method#invoke' --path-num 20
# 多个入口
python3 tools/find_cha_chains.py -p TEAMCITY_XS \
--from 'TiedMapEntry#getValue()' --from 'LazyMap#get' \
--to 'Method#invoke' --path-num 20| 参数 | 含义 | 默认 |
|---|---|---|
-p |
Neo4j project | 必填 |
--from |
入口(可重复;Map#get 会 CHA 展开) |
必填 |
--to |
sink(可重复;别名 Method#invoke) |
必填 |
--path-num |
过滤后最多保留路径数 | 30 |
--max-depth |
正向深度 | 8 |
--backward-depth |
sink 反向深度 | 5 |
--stitch-open-invoke |
打开 invoke→任意 sink 拼接(默认关) | off |
--bfs-only / --no-bfs |
只用 successors BFS / 只用 find_chains | 两者都跑 |
--no-expand-frontier |
不对 Map#get 等做 CHA 展开 | off |
--sink-caller-tag |
invoke 直接 caller 须为 true 的 Method 布尔属性;空串关闭 | is_sink |
-o |
输出文件 | 仅打印 |
与 find_call_path.py:后者是点对点双向 BFS(更快、无 stitch);本工具走完整 DynamicCha 引擎。
全量布局(推荐挖新链):CC3 + CC4 + 全量 JDK8 都进 app/ 并写入 Neo4j。
# 重建全量源码树(~1.2 万+ .java)
python3 tmpwork/cc_full/rebuild_full_layout.py
# app/ = JDK8 + CC3 + CC4(全部进图)
# lib/ = 空(不再把 JDK 藏成 solver-only)
# 首次解析会分片并行 JavaParseIr,并缓存到 tmpwork/cc_full/.cache/parse_ir.json
# 之后默认走缓存;改源码或 --force-reparse 才重解析
python3 run_analyze.py \
-i tmpwork/cc_full -p CC_FULL --mode gadget \
--app-root tmpwork/cc_full/app \
--dump-json tmpwork/cc_full_analyze_report.json \
--report tmpwork/cc_full_analyze_report.html
# 强制重解析
python3 run_analyze.py -i tmpwork/cc_full -p CC_FULL --mode gadget --force-reparse ...速度:PARSE_SHARD_WORKERS / JAVA_PARSE_XMX / BATCH_SIZE 在 parse/config.py;
分析阶段 CHA 按需展开,链查询见 [docs/stitch/](docs/stitch/README.md)。
CHA_MAX_CALLEES = 100(强烈注意):每个虚调用点做 CHA 时,最多只保留 100 个子类型/实现类上的同名方法(排序后截断)。
不是找全所有 CHA 类;候选多于 100 时后面的 override 仍会被裁掉。详见链文档第 2 节。
CHA / 反射策略:
- import:只存精确边——
CALLS→解析目标(如Map#get),MAY_REF→声明类型;不做 Map→所有实现类扇出,不物化全图CHA_CALLS - analyze 查链:
CALLS+ 按需子类型 CHA(受CHA_MAX_CALLEES截断);Method#invoke/Constructor#newInstance在 stitch_mid 上 A/B/C 拼接(危险构造器由 sink 反向 + 逆 CHA 得到),避免「全世界构造器」爆炸 - 配置
parse/config.py;实现analyze/dynamic_cha_chains.py、analyze/reflective.py、analyze/cha_expand.py;JavaParseIr 默认-Xmx6g(分片)
较小联跑(仅 AIH 片段 + CC3):tmpwork/cc_jdk8/。
报告 Tab:Call Chains / Object Graph / Findings · by sink group(按危险 API 聚合) / Findings · all(逐条)。
文本答案键(对照分析用):[rules/cc_gadget_answer_chains.md](rules/cc_gadget_answer_chains.md)
来源:Squirt1e — CC利用链总结
查链概念:[docs/stitch/](docs/stitch/README.md) · invoke mid:[docs/stitch/invoke_mid.md](docs/stitch/invoke_mid.md) · 动态 CHA:[docs/stitch/dynamic_cha.md](docs/stitch/dynamic_cha.md)
两者都是 Finding(污点确认过的危险调用),只是展示粒度不同:
| Findings · by sink group | Findings · all | |
|---|---|---|
| 是什么 | 按危险 API(vul + owner + name)聚合 | 逐条列出每一次命中 |
| 关注点 | 哪种 sink、有哪些 caller | 哪次调用、哪行、参数、证据 |
| 例子 | Method#invoke 下挂 N 个 caller |
BeanMap#get L333,invoke 参数被污染 |
规则里的 Tabby sink 仍指危险 API 本身;报告这两个 Tab 都是 finding 视图。
Call chain ≠ gadget chain。
- 当前
CALLS链回答:谁调用了谁,最终落到确认的 sink 方法 - 经典 gadget(如 CC1)还要证明:反序列化入口 + 字段拼装 能把控制流/数据配到
Method.invoke
常见缺口:
- 缺 JDK 入口(只扫 CC 库时没有
AnnotationInvocationHandler)→ 合成cc_jdk8项目可补 - 污点是过程内的(当前刻意保持简单,靠人工审查降误报)
- 接口/
Object虚调:不是“猜一个类”,而是 CHA 展开所有子类型(边会变多,再按能否到 sink 筛)
CHA = Class Hierarchy Analysis(类层次分析)。
遇到接口/父类上的虚调用或字段声明类型时,按 EXTENDS / IMPLEMENTS 把工程内子类/实现类当成候选目标。
- import:一般不把 CHA 扇出写进
CALLS(只存精确解析边) - analyze 查链:在 BFS 时按需展开 override(动态 CHA,见
[docs/stitch/dynamic_cha.md](docs/stitch/dynamic_cha.md))CHA_MAX_CALLEES = 100:每个虚调用点最多只跟 100 个 CHA 目标(排序截断,不是找全)
- 反射:
invoke/newInstance用 stitch_mid 双向拼接;详见[docs/stitch/](docs/stitch/README.md)
有。 import 写入:
(子)-[:EXTENDS]->(父)(类)-[:IMPLEMENTS]->(接口)
没有继承/实现边,就无法可靠判断「是否 implements Serializable、能否被反序列化写入」。
当前还会:对可序列化类型打 Type.is_serializable;字段非 static/transient 时标 serializable_write(另有 readObject 等启发式)。
MAY_REF:类型 A 的某个字段可能引用类型 B(对象图快捷边)。
(AnnotationInvocationHandler)-[:MAY_REF {field:'memberValues'}]->(LazyMap)
(LazyMap)-[:MAY_REF {field:'factory'}]->(InvokerTransformer)
| 边 | 含义 |
|---|---|
HAS_FIELD |
类上有这个字段 |
DECLARED_TYPE / POINTS_TO |
挂在 Field 节点上的类型边 |
MAY_REF |
Type→Type 快捷边(带字段名),方便查对象链 |
CALLS |
方法调用方法 |
实现(目前):字段声明类型 + CHA,不是精确指针分析。
- 做了:擦除泛型/数组、跳过
Object/String、声明类型 ∪ 子类型(有上限)、标serializable_write - 没做:不看
new Xxx赋值、不做 points-to / 堆抽象
所以是 MAY(可能),不是 MUST。
可以挖候选 + 人工确认,不能当自动“证明可利用”引擎。
已具备:Sink / 过程内污点、CALLS 链、字段对象图(MAY_REF+CHA)、Serializable 继承、HTML 报告。
对 CC1 能同时给出例如:
- 调用:
AnnotationInvocationHandler.invoke → LazyMap.get → InvokerTransformer.transform - 字段:
AIH.memberValues → LazyMap.factory → InvokerTransformer(及 Chained 变体)
仍缺:字段值约束(数组里具体 Constant+Invoker)、触发条件细节、跨方法精确污点 / 利用可行性证明。
- Neo4j:
bolt://127.0.0.1:7687,免密(与上节NEO4J_AUTH=none一致) - parse / analyze 默认读
tmpwork/下 reverse 的app/源码