利用Tai-e探测反序列化漏洞(其一)
字数 1526 2025-08-22 18:37:27
利用Tai-e静态分析框架探测Java反序列化漏洞教学文档
1. Tai-e框架简介
Tai-e是由南京大学李樾樾、谭添老师开发的针对Java的静态程序分析框架,主要功能是提供精准快速的指针分析框架。该框架具有以下特点:
- 采用插件化开发模式,允许开发者扩展具体功能
- 内置强大的反射分析技术Solar
- 2023年发表在ISSTA会议上
- 支持污点分析等安全分析功能
2. 环境配置
2.1 下载Tai-e
git clone https://github.com/pascal-lab/Tai-e.git
git submodule update --init --recursive
2.2 Gradle配置问题解决
如果网络环境导致Gradle下载卡住:
- 从官网下载对应版本的Gradle(complete版)
- 修改
gradle-wrapper.properties中的distributionUrl,改为本地路径:distributionUrl=file:///path/to/your/gradle-x.x.x-bin.zip
2.3 测试样本准备
创建URLDNS测试类:
public class URLDNS {
public URLDNS() {}
public static void main(String[] args) throws Exception {
byte[] evil_payload = getpayload();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(evil_payload));
Object o = ois.readObject();
}
public static byte[] getpayload() throws Exception {
HashMap<URL, Integer> hashmap = new HashMap();
URL url = new URL("http://7u2atlxr1e2fd1nv40jyr60m0d66uv.oastify.com");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 1234);
hashmap.put(url, 1);
hashCodeField.set(url, -1);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashmap);
oos.close();
return barr.toByteArray();
}
}
编译后放置在java-benchmarks/urldns目录下。
3. 配置文件编写
3.1 options.yml
optionsFile: null
printHelp: false
classPath: []
appClassPath:
- java-benchmarks/urldns
mainClass: URLDNS
inputClasses: []
javaVersion: 8
prependJVM: false
allowPhantom: true
worldBuilderClass: pascal.taie.frontend.soot.SootWorldBuilder
outputDir: output
preBuildIR: true
worldCacheMode: false
scope: ALL
nativeModel: true
planFile: null
analyses:
pta:
cs: ci
implicit-entries: false
distinguish-string-constants: null
reflection-inference: solar
merge-string-objects: false
merge-string-builders: false
merge-exception-objects: false
taint-config: java-benchmarks/urldns/taint-config.yml
onlyGenPlan: false
keepResult:
- $KEEP-ALL
3.2 taint-config.yml
sources:
- { kind: param, method: "<java.util.HashMap: void readObject(java.io.ObjectInputStream)>", index: 0 }
sinks:
- { method: "<java.net.InetAddress: java.net.InetAddress getByName(java.lang.String)>", index: 0 }
transfers:
- { method: "<java.io.ByteArrayInputStream: void <init>(byte[])>", from: 0, to: base }
- { method: "<java.io.ObjectInputStream: void <init>(java.io.InputStream)>", from: 0, to: base }
- { method: "<java.io.ObjectInputStream: java.lang.Object readObject()>", from: base, to: result, type: "java.net.URL"}
- { method: "<java.net.URL: java.lang.String getHost()>", from: base, to: result, type: "java.lang.String"}
4. 插件开发
4.1 插件功能设计
需要实现两个主要功能:
- 入口方法识别:将HashMap.readObject()设为入口方法
- 抽象类处理:处理URLStreamHandler等抽象类的指针指向问题
4.2 完整插件代码
public class UnserializeEntryPointHandler implements Plugin {
private Solver solver;
private String findclass = "java.util.HashMap";
private String exceptionclass = "java.net.URLStreamHandler";
@Override
public void setSolver(Solver solver) {
this.solver = solver;
}
@Override
public void onStart() {
// 添加HashMap readObject到入口点
List<JClass> list = solver.getHierarchy().allClasses().toList();
List<Type> paramType = new ArrayList<>();
for (JClass jClass : list) {
if (jClass.getName().equals(findclass)) {
JMethod jMethod = jClass.getDeclaredMethod("readObject");
if (jMethod != null) {
solver.addEntryPoint(new EntryPoint(
jMethod,
new DeclaredParamProvider(jMethod, solver.getHeapModel())
));
}
}
}
}
@Override
public void onPhaseFinish() {
solver.getCallGraph().reachableMethods().forEach(csMethod -> {
if (csMethod.getMethod().getDeclaringClass().getName().equals("java.net.URL")) {
csMethod.getMethod().getIR().getStmts().forEach(stmt1 -> {
if (stmt1 instanceof Invoke invoke &&
(invoke.isVirtual() || invoke.isInterface()) &&
invoke.getRValue() instanceof InvokeInstanceExp invokeInstanceExp) {
Var var = invokeInstanceExp.getBase();
Context context = csMethod.getContext();
if (solver.getCSManager().getCSVar(context, var).getPointsToSet() == null ||
solver.getCSManager().getCSVar(context, var).getPointsToSet().isEmpty()) {
JClass jclass = World.get().getClassHierarchy().getClass(var.getType().getName());
Collection<JClass> implementors = new ArrayList<>();
if (invoke.isInterface()) {
implementors.addAll(
World.get().getClassHierarchy().getDirectImplementorsOf(jclass)
);
} else {
implementors.add(jclass);
implementors.addAll(
World.get().getClassHierarchy().getDirectSubclassesOf(jclass)
);
}
implementors.forEach(implementor -> {
solver.addPointsTo(
solver.getCSManager().getCSVar(csMethod.getContext(), var),
csMethod.getContext(),
solver.getHeapModel().getMockObj(
() -> "Unserialzie",
implementor.getName(),
implementor.getType()
)
);
});
}
}
});
}
});
}
}
4.3 插件注册
在pascal.taie.analysis.pta.PointerAnalysis类的setPlugin方法中加入该插件。
5. 关键问题解析
5.1 为什么选择HashMap.readObject作为source
反序列化漏洞的调用栈通常为:
ObjectInputStream.readObject -> 反射调用 -> 目标类.readObject
由于反射调用的目标类取决于攻击者控制的输入,静态分析难以确定具体类型,因此直接从已知的载体类(如HashMap)的readObject方法开始分析。
5.2 污点转移的类型指定
在taint-config.yml中,特别指定了:
- { method: "<java.io.ObjectInputStream: java.lang.Object readObject()>", from: base, to: result, type: "java.net.URL"}
这是因为:
- 如果不指定类型,返回的污点类型默认为java.lang.Object
- 当Object进入hash()方法时,会调用Object.hashCode()而非预期的URL.hashCode()
- 需要人为干预污点类型以保证分析准确性
5.3 抽象类处理问题
URLStreamHandler是抽象类,在代码中没有实例化语句,Tai-e不会为其生成PointerToSet。解决方案:
- 在
onPhaseFinish阶段检查所有可达方法 - 对java.net.URL类中的方法调用特别处理
- 为抽象类变量添加所有子类的mock对象到指向集
6. 运行与结果
配置Tai-e启动参数后运行,成功检测到从HashMap.readObject到InetAddress.getByName的污点传播路径。
7. 优化方向
- 更优雅的污点转移处理:寻找不依赖硬编码类型指定的方法,可能结合CHA(类层次分析)
- 灵活的入口方法配置:支持通过配置文件批量指定入口方法,不限于HashMap
- 更通用的抽象类处理:考虑对所有类应用类似处理,而不仅限于特定类
- 完整的反序列化分析:包含ObjectInputStream.readObject的完整分析流程
8. 参考资源
- Tai-e官方文档:https://tai-e.pascal-lab.net/docs/0.2.2/reference/en/index-single.html
- Tai-e GitHub仓库:https://github.com/pascal-lab/Tai-e
- 相关技术文章:https://xz.aliyun.com/t/13775, https://xz.aliyun.com/t/14058