1. 前言
官方公告**:**
https://cwiki.apache.org/confluence/display/WW/S2-066
漏洞描述:
攻击者可以操纵文件上传参数以启用路径遍历,在某些情况下,这可能导致上传可用于执行远程代码执行的恶意文件。
影响版本:
Struts 2.0.0 - Struts 2.3.37 (EOL)
Struts 2.5.0 - Struts 2.5.32
Struts 6.0.0 - Struts 6.3.0
新建一个项目,Archetype 选择org.apache.maven.archetypes:maven-archetype-webapp;
pom.xml 中添加 Struts2 依赖,以 6.3.0 版本为例,tomcat 也添加上,以便获取当前路径;
`<dependency>` `<groupId>org.apache.struts</groupId>` `<artifactId>struts2-core</artifactId>` `<version>6.3.0</version>``</dependency>``<dependency>` `<groupId>org.apache.tomcat</groupId>` `<artifactId>tomcat-catalina</artifactId>` `<version>8.5.81</version>``</dependency>`
web.xml 中配置过滤器:
`<!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>`
文件上传的 action:
`package blckder02.struts2.action;``import com.opensymphony.xwork2.ActionSupport;``import org.apache.commons.io.FileUtils;``import java.io.File;``import java.io.IOException;``publicclass UploadAction extends ActionSupport {` `private File myfile;` `private String myfileContentType;` `private String myfileFileName;` `private String destpath;` `public String execute() {` `destpath = ServletActionContext.getServletContext().getRealPath("/")+"uploads\\upload\\";` `try{` `System.out.println("Src File name: " + myfile);` `System.out.println("Dst File name: " + myfileFileName);` `File destFile = new File(destpath, myfileFileName);` `FileUtils.copyFile(myfile, destFile);` `return SUCCESS;` `} catch (IOException | NullPointerException e) {` `e.printStackTrace();` `return ERROR;` `}` `}` `public File getMyfile() {` `return myfile;` `}` `public void setMyfile(File myfile) {` `this.myfile = myfile;` `}` `public String getMyfileContentType() {` `return myfileContentType;` `}` `public void setMyfileContentType(String myfileContentType) {` `this.myfileContentType = myfileContentType;` `}` `public String getMyfileFileName() {` `return myfileFileName;` `}` `public void setMyfileFileName(String myfileFileName) {` `this.myfileFileName = myfileFileName;` `}``}`
uopload.jsp:
`<%@ page contentType="text/html;charset=UTF-8" language="java" %>``<%@ taglib prefix="s" uri="/struts-tags"%>``<html>``<head>` `<title>Upload</title>``</head>``<body>``<h2>Upload</h2><br/>``<form action="upload.action" method="post" enctype="multipart/form-data">` `<s:label for="myfile">Please upload your file</s:label><br/>` `<input type="file" name="myfile"/>` `<input type="submit" value="Upload"/>``</form>``</body>``</html>`
success.jsp:
`<%@ page contentType="text/html;charset=UTF-8" language="java" %>``<%@ taglib prefix="s" uri="/struts-tags"%>``<html>``<head>` `<title>Success</title>``</head>``<body>``You have successfully uploaded <s:property value="myfileFileName"/>``</body>``</html>`
struts.xml 中配置 action 的解析,继承了 struts-default 包;
`<?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="default" namespace="/" extends="struts-default">` `<action name="upload" class="blckder02.struts2.action.UploadAction" method="execute">` `<result name="success">/success.jsp</result>` `<result name="error">/upload.jsp</result>` `</action>` `</package>``</struts>`
struts-default 包中使用了FileUploadInterceptor和ParametersInterceptor,在这里也是继承使用的,会对上传的文件以及参数进行拦截校验;
最后配置上 Tomcat 就可以运行了。
准备一个含有简单 jsp 木马的文件;
选择文件,上传抓包;
发送到 Repeater,修改请求包,将myfile参数名的第一个字母改为大写,再构造一个myfileFileName参数,参数值为想要文件保存后所在的路径及文件名。构造的参数名必须是第一个参数名+"FileName",不能随便构造。
上传成功后可以看到文件名为自定义的参数值;
访问 jsp,能成功执行命令,文件保存到了 /uploads 目录下,且文件名保存为构造传入的exec.jsp。
断点跟踪一下文件上传的流程,可以从org.apache.struts2.dispatcher.Dispatcher#serviceAction()断点,开始处理文件上传的 action;
可以看到 request 中含有一个文件类型参数files和一个字符串类型的参数params;
跟进,将myfileFileName参数封装成了 HttpParameters 对象,添加到了context中;
回到serviceAction()中,extraContext是前面createContextMap()创建的 context 对象,这里生成了一个 action 的代理对象,开始执行 action 流程。
接着进入 FileUploadInterceptor,从上传的请求中提取了文件、Content-Type 和文件名;可以看到保存文件名的键名是inputName + "FileName",所以请求包中构造的参数名也得是这种形式,才能完成覆盖。
在调用multiWrapper.getFileNames()的时候,对文件名中/、\符号前的字符串进行了过滤,所以直接在文件名中进行路径穿越是不行的;
把这三个值添加到 HttpParameters 对象中后,actioncontext 里的参数就有四个了。
进入 ParametersInterceptor,在setParameters()中,将 HttpParameters 对象中的参数依次放入 TreeMap 对象中;
可以看到 TreeMap 对象中保存的参数顺序变了,是因为 TreeMap 对象会按参数名的大小顺序从小到大保存值;
在TreeMap.put()方法中,会将新添入的参数名与已保存的参数名做比较;
比较逻辑就是逐字符比较,遇到不相同的字符就返回新增参数名与已保存参数名 ASCII 的差值;
差值为负,则说明新增参数名比已保存参数名的 ASCII 值小,将新增参数插入到已保存参数的前面,所以要保证构造的文件名比inputName + "FileName"生成的文件名的 ASCII 值大。
newStack.setParameter()进行参数绑定,遍历后myfileFileName的值已经被赋为S2-66.txt;
跟进,调用task.execute()执行表达式;
一直跟进,会再次调用 setter 方法为myfileFileName赋值,所以第一次赋值的S2-66.txt就被覆盖为../exec.jsp了。
最后在 UploadAction 中保存文件,路径中拼接的文件名包含路径穿越符,便会解析保存到上一目录。
在 6.3.0.2版本中,HttpParameters 类的appendAll()中添加调用了remove()方法;遍历检查参数是否需要删除,remove()方法中添加使用了equalsIgnoreCase()方法来忽略大小写进行比较。
断点进入,将MyfileFileName与myfileFileName带入校验;
跟进,在StringLatin1.regionMatchesCI()中,将两个参数名进行了统一的大小写转换再进行比较,这里的比较结果相同,返回 true;
于是将已经存在的同名参数(忽略大小写)删除了,后面就不存在二次调用 setter 方法来覆盖了。
参考链接:
https://y4tacker.github.io/2023/12/09/year/2023/12/Apache-Struts2-文件上传分析-S2-066/
https://xz.aliyun.com/t/13172