之前的工作中我处理过一些洞态iast[1]的漏报误报案例,也逐渐了解这个项目。
本文记录我对洞态iast基本原理的理解,内容包括:
洞态做漏洞检测的原理
洞态中的污点是什么
源码分析java-agent的业务逻辑
举个例子:洞态怎么检测mybatis写的sql是否存在sql注入
如上,用户可以在server端配置四类规则:
污点源方法:是获取api、rpc请求信息的接口或者类签名,比如javax.servlet.ServletRequest.getParameter(java.lang.String)
传播方法:是字符串拼接、编码等接口或类签名,比如java.lang.String.<init>(java.lang.String)
危险方法:是高危函数,比如javax.naming.Context.lookup(java.lang.String)
源码中有三个重要的数据结构,TAINT_POOL
存放污点对象,TAINT_HASH_CODES
存放污点对象的hashCode值,TRACK_MAP
存放调用关系
当代码执行到被hook的传播方法时,会根据用户配置的"污点来源"规则,拿到对象(一般是函数的某个参数)去TAINT_POOL
和TAINT_HASH_CODES
搜索匹配。如果能匹配上,就会根据用户配置的"污点去向"规则,生成污点对象并放到TAINT_POOL
中,并将污点对象的hashCodes存放到TAINT_HASH_CODES
中,最后将传播方法的调用关系存放到TRACK_MAP
。
当代码执行到被hook的危险方法时,和传播方法的逻辑比较类似,不过没有"污点去向"。
这里的"污点"是什么呢?
最重要的概念是对象的hashcode/identifyHashCode,hashcode/identifyHashCode作为数据的唯一跟踪方法会被加入到污点池中,也会被用来判断是否在污点池中。
下面我带你通过一个我遇到过的误报案例来理解这个概念。
因为Java中相同字符串对象的hashcode/identifyHashCode是不变的,如下
String a = "123";String b = "123";System.out.println(System.identityHashCode(a)); // 1289696681System.out.println(System.identityHashCode(b)); // 1289696681
所以有时候即使危险函数的参数完全不可控,也会报警。如下代码中的iast17接口之前会误报(现已修复),因为iast会认为f.getName()
返回的字符串对象123
是污点。
@ResponseBody@RequestMapping("/iast17")public String iast17(@RequestParam("name") String name) { ArrayList<String> a = new ArrayList<>(); a.add("123"); a.add(name); // a对象会被标记成污点 Iterator<String> b = a.iterator(); System.out.println(b.next()); System.out.println(b.next()); // "123"会被标记成污点 File f = new File("123"); return f.getName(); // 返回值"123"被认为是可控的,会产生误报}
iast为什么会认为"123"是污点呢?
因为执行a.add(name)
时,下面的传播规则会使得a
对象变成污点
在执行b.next()
时,iterator.next()
传播规则会让123
字符串变成污点
collectMethodPool方法串联了"最重要"的业务流程。当java-agent启动时,会拉取server端规则,然后根据规则hook类,确保在被hook的方法执行前或者执行后能调用到collectMethodPool方法。在处理http请求时,collectMethodPool方法会判断当前是属于哪一类规则,并做对应的动作。
你可以从java-agent启动时和请求过来时两个场景来看业务逻辑。
java-agent启动时会找到所有jvm已经加载的类并重写字节码,如下
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/IastClassFileTransformer.java#L250public void reTransform() { ... Class<?>[] waitingReTransformClasses = findForRetransform(); // 找到所有待重写的类 ... for (Class<?> clazz : waitingReTransformClasses) { ... inst.retransformClasses(clazz); // 用asm重新生成字节码 ... }}
因此实现了对污点源方法、传播方法、危险方法的hook,并且使得执行方法前或者执行方法后,调用captureMethodState方法。
// 污点源方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/SourceAdviceAdapter.java#L26public class SourceAdviceAdapter extends AbstractAdviceAdapter { ... @Override protected void after(int opcode) { ... captureMethodState(opcode, HookType.SOURCE.getValue(), true); ... }// 传播方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/PropagateAdviceAdapter.java#L31public class PropagateAdviceAdapter extends AbstractAdviceAdapter { ... @Override protected void after(final int opcode) { ... captureMethodState(opcode, HookType.PROPAGATOR.getValue(), true); ... }// 危险方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/SinkAdviceAdapter.java#L31public class SinkAdviceAdapter extends AbstractAdviceAdapter { ... @Override protected void before() { ... captureMethodState(-1, HookType.SINK.getValue(), false); ... }
captureMethodState 最终会调用collectMethodPool方法
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/AbstractAdviceAdapter.java#L103protected void captureMethodState( final int opcode, final int hookValue, final boolean captureRet) { ... invokeInterface(ASM_TYPE_SPY_DISPATCHER, SPY$collectMethodPool); pop();}// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/asm/AsmMethods.java#L131Method SPY$collectMethodPool = InnerHelper.getAsmMethod( SpyDispatcher.class, "collectMethodPool", ...);
请求过来时,就会执行到collectMethodPool方法,方法中根据hookType处理。
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/handler/hookpoint/SpyDispatcherImpl.java#L462@Overridepublic boolean collectMethodPool(Object instance, Object[] argumentArray, Object retValue, String framework, String className, String matchClassName, String methodName, String methodSign, boolean isStatic, int hookType) { // hook点降级判断 ... // 尝试获取hook限速令牌,耗尽时降级 ... ... MethodEvent event = new MethodEvent(0, -1, className, matchClassName, methodName, methodSign, methodSign, instance, argumentArray, retValue, framework, isStatic, null); if (HookType.HTTP.equals(hookType)) { HttpImpl.solveHttp(event); } else if (HookType.RPC.equals(hookType)) { solveRPC(framework, event); } else if (HookType.PROPAGATOR.equals(hookType) && !EngineManager.TAINT_POOL.isEmpty()) { // 处理传播方法 PropagatorImpl.solvePropagator(event, INVOKE_ID_SEQUENCER); } else if (HookType.SOURCE.equals(hookType)) { // 处理污点源方法 SourceImpl.solveSource(event, INVOKE_ID_SEQUENCER); } else if (HookType.SINK.equals(hookType)) { // 处理危险方法 SinkImpl.solveSink(event); } ...}
后端服务用mybatis时,${变量}
的sql写法容易造成sql注入,而#{变量}
底层会使用预编译通常不会产生sql注入问题,如下
// 第一个sql:存在sql注入select * from user where name=${name}// 第二个sql:不存在sql注入select * from user where name=#{name}
当用户请求/user?name=admin
时,iast是怎么检查出第一种接口存在SQL注入风险,而不会对第二种接口误报呢?
实际上如果我们调试一下,就知道#
和$
的写法调用的sql接口是有区别的,如下
// 使用 ${name}时conn.prepareStatement("select * from user where name="admin")// 使用#{name}时pstmt=conn.prepareStatement("select * from user where name=?)pstmt.setString(1, "admin")
洞态iast默认有一个危险方法规则是java.sql.Connection.prepareStatement(java.lang.String)
,当第一个参数是污点时,就会告警,规则如下。
所以使用 ${name}时,admin
字符串对象是污点,"select * from user where name="admin"
字符串对象也会被标记成污点,于是命中危险方法规则,产生告警。
学习iast时阅读官方文档和代码调试很有用,java-agent调试可以看 https://doc.dongtai.io/docs/development/dongtai-java-agent-doc/agent-debug
[1]
洞态iast: https://doc.dongtai.io/