作者:Longofo@知道创宇404实验室 时间:2019年9月4日
接上篇
Java 反序列化工具 gadgetinspector 初窥 (上)
样例分析
现在根据作者的样例写个具体的demo实例来测试下上面这些步骤。
demo如下:
IFn.java:
package com.demo.ifn;
import java.io.IOException;
public interface IFn {
public Object invokeCall(Object arg) throws IOException;
}
FnEval.java
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
public class FnEval implements IFn, Serializable {
public FnEval() {
}
public Object invokeCall(Object arg) throws IOException {
return Runtime.getRuntime().exec((String) arg);
}
}
FnConstant.java:
package com.demo.ifn;
import java.io.Serializable;
public class FnConstant implements IFn , Serializable {
private Object value;
public FnConstant(Object value) {
this.value = value;
}
public Object invokeCall(Object arg) {
return value;
}
}
FnCompose.java:
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
public class FnCompose implements IFn, Serializable {
private IFn f1, f2;
public FnCompose(IFn f1, IFn f2) {
this.f1 = f1;
this.f2 = f2;
}
public Object invokeCall(Object arg) throws IOException {
return f2.invokeCall(f1.invokeCall(arg));
}
}
TestDemo.java:
package com.demo.ifn;
public class TestDemo {
//测试拓扑排序的正确性
private String test;
public String pMethod(String arg){
String vul = cMethod(arg);
return vul;
}
public String cMethod(String arg){
return arg.toUpperCase();
}
}
AbstractTableModel.java:
package com.demo.model;
import com.demo.ifn.IFn;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
public class AbstractTableModel implements Serializable {
private HashMap<String, IFn> __clojureFnMap;
public AbstractTableModel(HashMap<String, IFn> clojureFnMap) {
this.__clojureFnMap = clojureFnMap;
}
public int hashCode() {
IFn f = __clojureFnMap.get("hashCode");
try {
f.invokeCall(this);
} catch (IOException e) {
e.printStackTrace();
}
return this.__clojureFnMap.hashCode() + 1;
}
}
注:下面截图中数据的顺序做了调换,同时数据也只给出com/demo中的数据
Step1 枚举全部类及每个类所有方法
classes.dat:
methods.dat:
Step2 生成passthrough数据流
passthrough.dat:
可以看到IFn的子类中只有FnConstant的invokeCall在passthrough数据流中,因为其他几个在静态分析中无法判断返回值与参数的关系。同时TestDemo的cMethod与pMethod都在passthrough数据流中,这也说明了拓扑排序那一步的必要性和正确性。
Step3 枚举passthrough调用图
callgraph.dat:
Step4 搜索可用的source
sources.dat:
Step5 搜索生成调用链
在gadget-chains.txt中找到了如下链:
com/demo/model/AbstractTableModel.hashCode()I (0)
com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (1)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)
可以看到选择的确实是找了一条最短的路径,并没有经过FnCompose、FnConstant路径。
上面流程分析第五步中说到,如果去掉已访问过节点的判断会怎么样呢,能不能生成经过FnCompose、FnConstant的调用链呢?
陷入了爆炸状态,Search space无限增加,其中必定存在环路。作者使用的策略是访问过的节点就不再访问了,这样解决的环路问题,但是丢失了其他链。
比如上面的FnCompose类:
public class Fncompose implements IFn{
private IFn f1,f2;
public Object invoke(Object arg){
return f2.invoke(f1.invoke(arg));
}
}
由于IFn是接口,所以在调用链生成中会查找是它的子类,假如f1,f2都是FnCompose类的对象,这样形成了环路。
测试隐式调用看工具能否发现,将FnEval.java做一些修改:
FnEval.java
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
public class FnEval implements IFn, Serializable {
private String cmd;
public FnEval() {
}
@Override
public String toString() {
try {
Runtime.getRuntime().exec(this.cmd);
} catch (IOException e) {
e.printStackTrace();
}
return "FnEval{}";
}
public Object invokeCall(Object arg) throws IOException {
this.cmd = (String) arg;
return this + " test";
}
}
结果:
com/demo/model/AbstractTableModel.hashCode()I (0)
com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (0)
java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lang/StringBuilder; (1)
java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; (0)
com/demo/ifn/FnEval.toString()Ljava/lang/String; (0)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)
隐式调用了tostring方法,说明在字节码分析中做了查找隐式调用这一步。
在github的工具说明中,作者也说到了在静态分析中这个工具的盲点,像下面这中FnEval.class.getMethod("exec", String.class).invoke(null, arg)
写法是不遵循反射调用的,将FnEval.java修改:
FnEval.java
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
public class FnEval implements IFn, Serializable {
public FnEval() {
}
public static void exec(String arg) throws IOException {
Runtime.getRuntime().exec(arg);
}
public Object invokeCall(Object arg) throws IOException {
try {
return FnEval.class.getMethod("exec", String.class).invoke(null, arg);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
经过测试,确实没有发现。但是将FnEval.class.getMethod("exec", String.class).invoke(null, arg)
改为this.getClass().getMethod("exec", String.class).invoke(null, arg)
这种写法却是可以发现的。
测试一下比较特殊的语法呢,比如lambda语法?将FnEval.java做一些修改:
FnEval.java:
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
public class FnEval implements IFn, Serializable {
public FnEval() {
}
interface ExecCmd {
public Object exec(String cmd) throws IOException;
}
public Object invokeCall(Object arg) throws IOException {
ExecCmd execCmd = cmd -> {
return Runtime.getRuntime().exec(cmd);
};
return execCmd.exec((String) arg);
}
}
经过测试,没有检测到这条利用链。说明目前语法分析那一块还没有对特殊语法分析。
测试匿名内部类,将FnEval.java做一些修改:
FnEval.java:
package com.demo.ifn;
import java.io.IOException;
import java.io.Serializable;
public class FnEval implements IFn, Serializable {
public FnEval() {
}
interface ExecCmd {
public Object exec(String cmd) throws IOException;
}
public Object callExec(ExecCmd execCmd, String cmd) throws IOException {
return execCmd.exec(cmd);
}
public Object invokeCall(Object arg) throws IOException {
return callExec(new ExecCmd() {
@Override
public Object exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}, (String) arg);
}
}
经过测试,没有检测到这条利用链。说明目前语法分析那一块还没有对匿名内部类的分析。
sink->source?
既然能source->sink,那么能不能sink->source呢?因为搜索source->sink时,source和sink都是已知的,如果搜索sink->source时,sink与soure也是已知的,那么source->sink与sink->source好像没有什么区别?如果能将source总结为参数可控的一类特征,那么sink->source这种方式是一种非常好的方式,不仅能用在反序列化漏洞中,还能用在其他漏洞中(例如模板注入)。但是这里也还有一些问题,比如反序列化是将this以及类的属性都当作了0参,因为反序列化时这些都是可控的,但是在其他漏洞中这些就不一定可控了。
目前还不知道具体如何实现以及会有哪些问题,暂时先不写。
缺 陷
目前还没有做过大量测试,只是从宏观层面分析了这个工具的大致原理。结合平安集团分析文章以及上面的测试目前可以总结出一下几个缺点(不止这些缺陷):
• callgraph生成不完整
• 调用链搜索结果不完整,这是由于查找策略导致的
• 一些特殊语法、匿名内部类还不支持
• ...
设想与改进
•对以上几个缺陷进行改进
•结合已知的利用链(如ysoserial等)不断测试
•尽可能列出所有链并结合人工筛选判断,而作者使用的策略是只要经过这个节点有一条链,其他链经过这个节点时就不再继续寻找下去。主要解决的就是最后那个调用链环路问题,目前看到几种方式:
•DFS+最大深度限制
•继续使用BFS,人工检查生成的调用链,把无效的callgraph去掉,重复运行
•调用链缓存(这一个暂时还没明白具体怎么解决环路的,只是看到了这个方法)
我的想法是在每条链中维持一个黑名单,每次都检查是否出现了环路,如果在这条链中出现了环路,将造成环路的节点加入黑名单,继续使其走下去。当然虽然没有了环,也能会出现路径无限增长的情况,所以还是需要加入路径长度限制。
•尝试sink->source的实现
•多线程同时搜索多条利用链加快速度
•...
最 后
在原理分析的时候,忽略了字节码分析的细节,有的地方只是暂时猜测与测试得出的结果,所以可能存在一些错误。字节码分析那一块是很重要的一环,它对污点的判断、污点的传递调用等起着很重要的作用,如果这些部分出现了问题,整个搜索过程就会出现问题。由于ASM框架对使用人员要求较高,所以需要要掌握JVM相关的知识才能较好使用ASM框架,所以接下来的就是开始学习JVM相关的东西。这篇文章只是从宏观层面分析这个工具的原理,也算是给自己增加些信心,至少明白这个工具不是无法理解和无法改进的,同时后面再接触这个工具进行改进时也会间隔一段时间,回顾起来也方便,其他人如果对这个工具感兴趣也可以参考。等以后熟悉并能操纵Java字节码了,在回头来更新这篇文章并改正可能有错误的地方。
如果这些设想与改进真的实现并且进行了验证,那么这个工具真的是一个得力帮手。但是这些东西要实现还有较长的一段路要走,还没开始实现就预想到了那么多问题,在实现的时候会遇到更多问题。不过好在有一个大致的方向了,接下来就是对各个环节逐一解决了。