Skip to content

Repository files navigation

SAST Pipeline

静态分析流水线:reverseparse(调用链入 Neo4j)→ analyze(sink + 污点检测)。

sast/
├── reverse/                 # 逆向:下载源码 / 反编译
├── parse/                   # JavaParseIr → parse_ir → Neo4j 调用图
├── analyze/                 # 分析检测:sink 识别 + 简单污点
├── target/                  # 默认输入 JAR
├── tmpwork/                 # 纯源码输出
└── docker-compose.yml       # 不起服务;Neo4j 用本机已有容器

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-enterprise

Docker 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 JavaParseIrparse_ir objects → Neo4j 调用链
analyze sink 检测 + 简单污点(参数/字段经赋值传播到 exec/readObject

reverse:源码怎么来(不是解析项目 Maven 依赖)

Spring Boot / 胖 JAR 输出纯源码树(不含 jar/class):

tmpwork/<app>/
  app/                 # 业务 .java(包路径)
  resources/           # 配置等
  lib/<dependency>/    # 依赖源码(下载或反编译)
  1. 解包到 tmpwork/.unpack/(完成后删除)
  2. 依赖并行拉源码 → lib/<name>/
  3. 业务 class CFR → app/
  4. 下载失败的依赖再 CFR(--no-decompile-libs 可关)

对每个普通 JAR,识别顺序:

  1. 同目录旁路 foo-1.0-sources.jar
  2. META-INF/MANIFEST.MF(Implementation-* / Bundle-*)
  3. META-INF/maven/**/pom.properties
  4. JAR SHA1 / 文件名搜索 Maven Central
  5. *-SNAPSHOT 跳过远程下载;都没有再 CFR

目录输入默认 --recursive

Neo4j 图模型与调用相关概念

哪些名称是业界通用的?

概念 是否通用 说明
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

三者描述同一调用的不同侧面,不是互相矛盾的两套数据。

CallSite 主要属性

属性 含义
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)
# 方法级调用链(走 CALLSMATCH (a:Method)-[:CALLS*1..5]->(b:Method)
WHERE a.project = 'JavaTarget'
RETURN a.qualified_name, b.qualified_name LIMIT 50

# 某方法里的调用点(走 HAS_CALL_SITECallSiteMATCH (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_REFMATCH (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

analyze 污点规则(简单,分模式)

完整说明(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
  • 分析流程(调用关系串起来)
    1. A 找调用了 Tabby sink 的方法(gadget 下 readObject/readExternal 入口也算)
    2. B 对这些方法做过程内污点 → 得到确认可利用的方法
    3. C 只对确认方法查调用链:Entry → sink(动态 CHA + stitch_mid 双向拼接,见下节文档)
    4. C2 查字段对象图路径(MAY_REF,见下方 FAQ)
    5. D 对链上额外方法再做一轮污点(仍过程内)
  • 默认模式见 analyze/config.pyTAINT_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 经验)

点对点查链:tools/find_call_path.py

指定两个方法 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)

动态 CHA 查链:tools/find_cha_chains.py

封装 analyze.dynamic_cha_chains.DynamicChaChainFinder(与 analyze 同一套:CALLS + 按需 CHA + stitch)。
适合 Map#getMethod#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 引擎。

CommonsCollections + JDK8 联跑(gadget 挖掘)

全量布局(推荐挖新链):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_SIZEparse/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#newInstancestitch_mid 上 A/B/C 拼接(危险构造器由 sink 反向 + 逆 CHA 得到),避免「全世界构造器」爆炸
  • 配置 parse/config.py;实现 analyze/dynamic_cha_chains.pyanalyze/reflective.pyanalyze/cha_expand.py;JavaParseIr 默认 -Xmx6g(分片)

较小联跑(仅 AIH 片段 + CC3):tmpwork/cc_jdk8/

报告 Tab:Call Chains / Object Graph / Findings · by sink group(按危险 API 聚合) / Findings · all(逐条)。

CommonsCollections 标准答案链

文本答案键(对照分析用):[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)

FAQ(设计问答摘要)

报告里「Findings · by sink group」和「Findings · all」有什么区别?

两者都是 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?

Call chain ≠ gadget chain。

  • 当前 CALLS 链回答:谁调用了谁,最终落到确认的 sink 方法
  • 经典 gadget(如 CC1)还要证明:反序列化入口 + 字段拼装 能把控制流/数据配到 Method.invoke

常见缺口:

  1. 缺 JDK 入口(只扫 CC 库时没有 AnnotationInvocationHandler)→ 合成 cc_jdk8 项目可补
  2. 污点是过程内的(当前刻意保持简单,靠人工审查降误报)
  3. 接口/Object 虚调:不是“猜一个类”,而是 CHA 展开所有子类型(边会变多,再按能否到 sink 筛)

CHA 是什么?在哪做的?

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 / newInstancestitch_mid 双向拼接;详见 [docs/stitch/](docs/stitch/README.md)

继承关系有没有保存?和 Serializable 有什么关系?

有。 import 写入:

  • (子)-[:EXTENDS]->(父)
  • (类)-[:IMPLEMENTS]->(接口)

没有继承/实现边,就无法可靠判断「是否 implements Serializable、能否被反序列化写入」。
当前还会:对可序列化类型打 Type.is_serializable;字段非 static/transient 时标 serializable_write(另有 readObject 等启发式)。

MAY_REF 是什么?怎么实现的?

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。

目前可以用来找 gadget 吗?

可以挖候选 + 人工确认,不能当自动“证明可利用”引擎。

已具备: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/ 源码

About

SAST

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages