0x00 背景
前两天看到一篇老外写的用codeql挖掘Java反序列化gadget的文章 https://www.synacktiv.com/en/publications/finding-gadgets-like-its-2022.html,虽然显示的效果还可以,但用了种比较土味的半自动化半人工的方式。直觉上感觉codeql用法应该不会这么呆,于是研究了下,有了这篇文章。
0x01 分析
翻了翻网上的帖子,发现好像除了上面那篇也没有什么挖掘Java原生反序列化的利用链的文章。那么先分析下挖掘原生反序列化利用链的条件是什么:
1. source,入口点,一般就是一个readObject方法
2. sink,执行点,一般是动态方法执行、Jndi注入、写文件之类的
3. gadget,连接source和sink的多个类。有几个条件:
1. 类之间的方法调用是链式的。
2. 类实例之间的关系是嵌套的,调用链上后一个类实例是前一个类实例的属性。
3. 调用链上的类都需要是可以序列化的。
所以发现原生反序列化的污点不好定义,每个类的每个属性都可能是污点。自己水平也不够,索性就放弃了污点分析的方式,直接从函数调用关系入手,误报其实没有想象的那么多。
0x02 实现
首先找source,这里只定义了readObject。对codeql不太熟,没找到系统内置的readObject,自己定义了个
class ROMethod extends Method{
ROMethod(){
this.hasName("readObject")
and this.isPrivate()
and this.getReturnType() instanceof VoidType
}
}
class Source extends Callable {
Source(){
getDeclaringType().getASupertype*() instanceof TypeSerializable and (
this instanceof ROMethod
)
}
}
然后是sink,就定义了一个Method.invoke,可以自行加别的执行点。这里定义了一个CallsDangerousMethod,是因为后面分析调用关系时候的限制条件是存在调用关系的两个类都需要是可序列化的,但sink点其实是不需要可序列化的。
class InvokeMethod extends Method {
InvokeMethod(){
this.hasName("invoke") and
this.getDeclaringType().hasQualifiedName("java.lang.reflect","Method")
}
}
class DangerousMethod extends Callable {
DangerousMethod(){
this instanceof InvokeMethod
}
}
class CallsDangerousMethod extends Callable {
CallsDangerousMethod() {
exists(Callable a| this.polyCalls(a) and
a instanceof DangerousMethod )
}
}
最后是函数调用,在codeql里就是a.polyCall(b),代表a方法里调用了b方法。原生反序列化的话,两个类都需要是可以序列化的,或者调用的是静态方法(其实还有可能是链式调用的静态方法,这么写就会漏报)。查询时用edges解决了土味自动化的问题。
query predicate edges(Method a, Method b) {
a.polyCalls(b) and
(a.getDeclaringType().getASupertype*() instanceof TypeSerializable or a.isStatic()) and
(b.getDeclaringType().getASupertype*() instanceof TypeSerializable or b.isStatic())
}
最终一个简易的完整实现是这样:
/**
@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
class ROMethod extends Method{
ROMethod(){
this.hasName("readObject")
and this.isPrivate()
and this.getReturnType() instanceof VoidType
}
}
class Source extends Callable {
Source(){
getDeclaringType().getASupertype*() instanceof TypeSerializable and (
this instanceof ROMethod
)
}
}
class InvokeMethod extends Method {
InvokeMethod(){
this.hasName("invoke") and
this.getDeclaringType().hasQualifiedName("java.lang.reflect","Method")
}
}
class DangerousMethod extends Callable {
DangerousMethod(){
this instanceof InvokeMethod
}
}
class CallsDangerousMethod extends Callable {
CallsDangerousMethod() {
exists(Callable a| this.polyCalls(a) and
a instanceof DangerousMethod )
}
}
query predicate edges(Method a, Method b) {
a.polyCalls(b) and
(a.getDeclaringType().getASupertype*() instanceof TypeSerializable or a.isStatic()) and
(b.getDeclaringType().getASupertype*() instanceof TypeSerializable or b.isStatic())
}
from Source source, CallsDangerousMethod sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName()
0x03 结果
导入的数据库是cc3,尝试找一条不依赖JDK的完整反序列化利用链,没啥意义,就是试试好不好使。
大概分析下发现Flat3Map#readObject下面的几条链是可用的,误报也还算可以接受,可能因为cc代码量比较小。
其实大部分已知的链都是从JDK内部类开始的,但codeql没有办法同时跑两个数据库,折中的解决方法就是找半条链,比如把hashCode、equals之类的当作source,自己补充就好了,能跑出一大堆。
在yso里面已知的库上跑了跑,发现误报主要集中在entry这种transient变量,漏报主要是一些不可序列化类的静态方法。