54

[Java unserialization] fastjson <= 1.2.24 反序列化漏洞分析

 5 years ago
source link: https://lightless.me/archives/java-unserialization-fastjson.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

0x00 fastjson

fastjson 是一个非常流行的库,可以将数据在JSON和Java Object之间互相转换,但是在2017年官方主动爆出了fastjson的反序列化漏洞以及 升级公告 ,这次我们就学习一下这个漏洞。

最终的 payload 会放到我的 GitHub 上。

这次使用的 fastjson 是1.2.23版本:

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.23</version>
</dependency>

在分析漏洞之前,我们先看下这个库都有什么样的功能,我们先创建一个 User 对象:

class User {
    private int age;
    public String username;
    private String secret;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getSecret() {
        return secret;
    }
}

我们主要关注一下从JSON还原回Object的方法,主要的API有两个,分别是 JSON.parseObjectJSON.parse ,最主要的区别就是前者返回的是 JSONObject 而后者返回的是实际类型的对象,当在没有对应类的定义的情况下,通常情况下都会使用 JSON.parseObject 来获取数据。

fastjson 接受的JSON可以通过 @type 字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。

String myJSON = "{\"@type\":\"me.lightless.fastjsonvuln.User\",\"age\":99,\"username\":\"lightless\",\"secret\":\"2333\"}";
JSONObject u3 = JSON.parseObject(myJSON);
System.out.println("u3 => " + u3.get("secret"));

如果需要还原出 private 成员的话,还需要加上 Feature.SupportNonPublicField

User u3 = (User) JSON.parseObject(myJSON, User.class, Feature.SupportNonPublicField);

0x01 跟踪分析

根据官方的公告中的WAF检测方法来看,问题很有可能是因为反序列化了任意类型的class从而导致的RCE。

r636nyq.png!web

从网上找到的payload中也能看出利用的是 TemplatesImpl 来执行的命令,这个在之前的JDK7u21中已经分析过了,可能还存在其他的执行命令方法,这些我们暂且不谈,主要来看fastjson的部分。

我们搭建一个简单的Web应用来接受用户POST过来的JSON并且进行反序列化:

@RestController
public class IndexController {
    @RequestMapping(value = "/fastjson", method = RequestMethod.GET)
    public String fastjson() {
        return "Hello World!";
    }

    @RequestMapping(value = "/fastjson", method = RequestMethod.POST)
    public JSONObject testVuln(@RequestBody String data) {

        JSONObject obj = JSON.parseObject(data, Feature.SupportNonPublicField);

        JSONObject ret = new JSONObject();
        ret.put("code", 1001);
        ret.put("data", "Hello " + obj.get("name"));

        return ret;
    }

}

传入我们带有 @type 类型的 JSON字符串 并且开始调试。在 JSON.parseObject 处下断并开始向下跟。

bYjqUbF.png!web

一开始会跟进到JSON.parse方法,并且调用了parse()方法继续进行JSON格式的匹配。继续跟进 parser.parse() 方法。

yYR7jmn.png!web

到了这里之后,开始依次进行JSON的解析,我们传入的第一个字符是 { ,所以进入LBRACE这个分支中,并继续进入 parseObject(object, fieldName) 方法来解析对象。

6BNZJrj.png!web

这个时候 lexer 所在的字符为 " ,会进入下面这个分支继续解析JSON字符串,通过 scanSymbol 方法获取到双引号之间的字符串也就是 @type

JZ3QFnQ.png!web

之后会获取 @type 字段的值,并且尝试获取这个类的Class,经过一系列的判断后,调用了 deserializer.deserialize(this, clazz, fieldName) 方法进行反序列化。

Qv67ZfJ.png!web

一开始没找到sortedFieldDeserializers是在什么地方生成的,想仔细跟一下代码,于是就从头到尾的看了下 getDeserializer(clazz) 的部分,发现是在这里生成的。这个函数的目的是获取一个可以反序列化我们通过 @type 指定的类的 deserializer ,由于预定于的列表中没有,于是会继续调用 createJavaBeanDeserializer() 来生成一个,实际上是调用了 ParseConfig.build() 方法,其中会通过反射机制获取我们指定类的一些信息,通过对 method 进行一些过滤,猜测出 gettersetter 并推出一些可能存在的 field

具体的猜测规则这里不展开说明了,感兴趣的话可以自行跟一下

紧接着就依次处理JSON字符串中的各个字段,当匹配到 payload 中的 _tfactory 字段的时候,由于我们传入的JSON字符串中是一个空的对象,进入 parseField 方法后,继续向下跟就会调用到 JavaBeanDeserializer.deserialize() 方法,在这里会为 _factory 创建一个 TransformerFactoryImpl 对象并赋值。

同样的, _outputProperties 字段我们也是传入了一个空对象,会进入和上面相同的流程,仔细分析一下这段代码,这里是触发命令执行的关键部分。

nEr6rqr.png!web

首先依然会进入 parseField 方法

NnQVBrN.png!web

进入之后会调用 smartMatch(key) 方法,这个方法的主要作用是进行一些『智能匹配』,方便后续获取对应变量的 gettersetter 。调用后这个方法会去掉字符串中的 - 、删除开头的下划线等,所以当我们传入了 _outputProperties 的时候,实际上就给处理成了 outputProperties ,并返回对应的 FieldDeserializer 对象,之后就会调用该对象的 parseField 方法。进入该方法后,就会调用 setValue(object, value) 方法,继续跟进。

fqEbi2I.png!web

跟进之后显而易见, getOutputProperties 被调用了:

vy6RR32.png!web

然后就会执行我们在 _bytecodes 构造的恶意字节码,造成命令执行。

0x02 一些疑问

根据上面的流程,我们应该已经可以写出PoC了,这里就不占用篇幅展示了,直接放到了我的 GitHub 上。

一开始在构造的时候发现字节码中存在许多的不可显字符,网上公开的PoC中使用了base64来编码,感觉非常神奇,为什么 fastjson 会帮我们解码呢?于是构造了数组传入之后,发现 fastjson 在处理 [B 类型的数组时,会调用 lexer.bytesValue() ,其中的 lexer 就是 JSONScanner ,这个 bytesValue() 方法会自动帮我们执行一次 base64 解码,所以我们构造payload的时候只需要传入base64编码后的内容即可。

MRFbI3a.png!web

ea6r2mA.png!web

至此,其实整个流程已经走完了,但是还有一点令我们非常难受,就是我们的demo中,在接收JSON的时候设置了 Feature.SupportNonPublicField 。默认情况下fastjson只会反序列化 public 的方法和属性,而我们构造的PoC中有 private 的成员变量 _bytecodes_name ,为了给这些变量赋值,则必须要假设服务端开启了 SupportNonPublicField 功能。

而现实情况下,大部分都是 parse(json)parseObject(json) 一把梭,使用这个功能的情况不是很多,这样一来就导致我们的PoC没有了良好的通用性,那么有没有解决方案呢?答案当然是有的,那就是不使用 TemplatesImpl ,换一种RCE的触发方式即可。在网上经过一番搜索,发现了一些可以利用的方法

主要是利用JNDI+RMI方法,这个可以参考之前的 spring-tx.jar 的反序列化问题,当时就是采用这种方式来触发的。但是调用链不太好找, 这个PPT 中给出了多个利用链,这里的调用链后续再进行分析,这里暂且不做验证。

这样一来,只要是开发同学在编写代码时,直接反序列化了用户传入的JSON,就有可能造成RCE,而攻击者也无需关心 SupportNonPublicField 是否开启了,危害提高了许多。

0x03 修复措施

在fastjson的 官方补丁 中,将 loadClass(typeName, config.getDefaultClassLoader()) 替换为了 config.checkAutoType(typeName) ,并且扩充了黑名单列表,将传入的类名与黑名单一一比较,如果发现了相同开头的类就停止反序列化。

// 新增的黑名单
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

可以看到绝大部分常用的类都已经被加进来了,但是如果不经常维护此名单,一旦后面出现了新的可以利用的类,很容易就绕过这个限制。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK