漏洞说明
由于Struts框架在处理参数名称大小写方面的不一致性,导致攻击者能够通过修改参数名称的大小写来利用目录遍历技术(如使用../路径)上传文件到服务器的非预期位置。
漏洞复现
环境介绍 2.5.32
漏洞分析
struts2在处理http请求的时候,会经过很多的自身实现的拦截器等,这里下个断点,调用栈如下
uploadFile:39, UploadAction (org.test)
invoke:-1, GeneratedMethodAccessor55 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethodInsideSandbox:1266, OgnlRuntime (ognl)
invokeMethod:1251, OgnlRuntime (ognl)
callAppropriateMethod:1969, OgnlRuntime (ognl)
callMethod:68, ObjectMethodAccessor (ognl)
callMethodWithDebugInfo:98, XWorkMethodAccessor (com.opensymphony.xwork2.ognl.accessor)
callMethod:90, XWorkMethodAccessor (com.opensymphony.xwork2.ognl.accessor)
callMethod:2045, OgnlRuntime (ognl)
getValueBody:97, ASTMethod (ognl)
evaluateGetValueBody:212, SimpleNode (ognl)
getValue:258, SimpleNode (ognl)
getValue:537, Ognl (ognl)
getValue:501, Ognl (ognl)
execute:492, OgnlUtil$3 (com.opensymphony.xwork2.ognl)
compileAndExecuteMethod:544, OgnlUtil (com.opensymphony.xwork2.ognl)
callMethod:490, OgnlUtil (com.opensymphony.xwork2.ognl)
invokeAction:438, DefaultActionInvocation (com.opensymphony.xwork2)
invokeActionOnly:293, DefaultActionInvocation (com.opensymphony.xwork2)
invoke:254, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:250, DebuggingInterceptor (org.apache.struts2.interceptor.debugging)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:179, DefaultWorkflowInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:263, ValidationInterceptor (com.opensymphony.xwork2.validator)
doIntercept:49, AnnotationValidationInterceptor (org.apache.struts2.interceptor.validation)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:142, ConversionErrorInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:140, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:140, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:201, StaticParametersInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:67, MultiselectInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:133, DateTextFieldInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:89, CheckboxInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:321, FileUploadInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:101, ModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:142, ScopedModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:160, ChainingInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:175, PrepareInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:121, I18nInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:167, ServletConfigInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:228, AliasInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:196, ExceptionMappingInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
execute:48, StrutsActionProxy (org.apache.struts2.factory)
serviceAction:574, Dispatcher (org.apache.struts2.dispatcher)
executeAction:79, ExecuteOperations (org.apache.struts2.dispatcher)
doFilter:141, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:889, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1743, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
struts2本身存在的漏洞问题,所以我们只需要关注struts2相关的拦截器包,找到了如下拦截器
(1)FileUploadInterceptor
public String intercept(ActionInvocation invocation) throws Exception {
ActionContext ac = invocation.getInvocationContext();
HttpServletRequest request = (HttpServletRequest)ac.get("com.opensymphony.xwork2.dispatcher.HttpServletRequest");
if (!(request instanceof MultiPartRequestWrapper)) {
if (LOG.isDebugEnabled()) {
ActionProxy proxy = invocation.getProxy();
LOG.debug(this.getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()}));
}
return invocation.invoke();
} else {
ValidationAware validation = null;
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
validation = (ValidationAware)action;
}
MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper)request;
if (multiWrapper.hasErrors() && validation != null) {
TextProvider textProvider = this.getTextProvider(action);
String errorMessage;
for(Iterator var8 = multiWrapper.getErrors().iterator(); var8.hasNext(); validation.addActionError(errorMessage)) {
LocalizedMessage error = (LocalizedMessage)var8.next();
if (textProvider.hasKey(error.getTextKey())) {
errorMessage = textProvider.getText(error.getTextKey(), Arrays.asList(error.getArgs()));
} else {
errorMessage = textProvider.getText("struts.messages.error.uploading", error.getDefaultMessage());
}
}
}
Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
while(fileParameterNames != null && fileParameterNames.hasMoreElements()) {
String inputName = (String)fileParameterNames.nextElement();
String[] contentType = multiWrapper.getContentTypes(inputName);
if (!this.isNonEmpty(contentType)) {
if (LOG.isWarnEnabled()) {
LOG.warn(this.getTextMessage(action, "struts.messages.invalid.content.type", new String[]{inputName}));
}
} else {
String[] fileName = multiWrapper.getFileNames(inputName);
if (!this.isNonEmpty(fileName)) {
if (LOG.isWarnEnabled()) {
LOG.warn(this.getTextMessage(action, "struts.messages.invalid.file", new String[]{inputName}));
}
} else {
UploadedFile[] files = multiWrapper.getFiles(inputName);
if (files != null && files.length > 0) {
List<UploadedFile> acceptedFiles = new ArrayList(files.length);
List<String> acceptedContentTypes = new ArrayList(files.length);
List<String> acceptedFileNames = new ArrayList(files.length);
String contentTypeName = inputName + "ContentType";
String fileNameName = inputName + "FileName";
for(int index = 0; index < files.length; ++index) {
if (this.acceptFile(action, files[index], fileName[index], contentType[index], inputName, validation)) {
acceptedFiles.add(files[index]);
acceptedContentTypes.add(contentType[index]);
acceptedFileNames.add(fileName[index]);
}
}
if (!acceptedFiles.isEmpty()) {
Map<String, Parameter> newParams = new HashMap();
newParams.put(inputName, new Parameter.File(inputName, acceptedFiles.toArray(new UploadedFile[acceptedFiles.size()])));
newParams.put(contentTypeName, new Parameter.File(contentTypeName, acceptedContentTypes.toArray(new String[acceptedContentTypes.size()])));
newParams.put(fileNameName, new Parameter.File(fileNameName, acceptedFileNames.toArray(new String[acceptedFileNames.size()])));
ac.getParameters().appendAll(newParams);
}
}
}
}
}
return invocation.invoke();
}
}
流程大致处理如下
(1)从context获取HttpServletRequest,判断是否为MultiPartRequestWrapper类型
(2)从文件上传请求中获取Content-Type,filename等请求参数
(3)经过一系列的处理以后,将文件名保存到HttpParameters.parameters中
#ac.getParameters().appendAll(newParams);
代码中发现这一行
String contentTypeName = inputName + "ContentType";
String fileNameName = inputName + "FileName";
其中inputName获取如下,也就是fileNameName这里是可控的
Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
while(fileParameterNames != null && fileParameterNames.hasMoreElements()) {
String inputName = (String)fileParameterNames.nextElement();
(2)ParametersInterceptor
这里通过treemap.put将参数put到acceptableParameters变量中
参数限制参考https://struts.apache.org/security/#accepted–excluded-patterns
调用newStack.setParameter(name, value.getObject());传递参数
看如下调用栈
isAccessible:157, DefaultMemberAccess (ognl)
isAccessible:118, SecurityMemberAccess (com.opensymphony.xwork2.ognl)
isMethodAccessible:2850, OgnlRuntime (ognl)
hasSetMethod:2952, OgnlRuntime (ognl)
hasSetProperty:2970, OgnlRuntime (ognl)
setProperty:83, CompoundRootAccessor (com.opensymphony.xwork2.ognl.accessor)
setProperty:3356, OgnlRuntime (ognl)
setValueBody:134, ASTProperty (ognl)
evaluateSetValueBody:220, SimpleNode (ognl)
setValue:308, SimpleNode (ognl)
setValue:780, Ognl (ognl)
execute:436, OgnlUtil$1 (com.opensymphony.xwork2.ognl)
execute:428, OgnlUtil$1 (com.opensymphony.xwork2.ognl)
compileAndExecute:523, OgnlUtil (com.opensymphony.xwork2.ognl)
setValue:428, OgnlUtil (com.opensymphony.xwork2.ognl)
trySetValue:186, OgnlValueStack (com.opensymphony.xwork2.ognl)
setValue:173, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameter:157, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameters:214, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
doIntercept:132, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:140, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
这里是反射调用set方法并赋值。
我们观察我们的struts2的上传代码,结果呼之欲出,如果我们可以调用setUploadFileName,那么就能完成文件上传文件名控制的操作!
当第一次访问文件上传的action
addIfAccessor:2728, OgnlRuntime (ognl)
collectAccessors:2715, OgnlRuntime (ognl)
getDeclaredMethods:2682, OgnlRuntime (ognl)
_getSetMethod:2912, OgnlRuntime (ognl)
getSetMethod:2881, OgnlRuntime (ognl)
hasSetMethod:2952, OgnlRuntime (ognl)
hasSetProperty:2970, OgnlRuntime (ognl)
setProperty:83, CompoundRootAccessor (com.opensymphony.xwork2.ognl.accessor)
setProperty:3356, OgnlRuntime (ognl)
setValueBody:134, ASTProperty (ognl)
evaluateSetValueBody:220, SimpleNode (ognl)
setValue:308, SimpleNode (ognl)
setValue:780, Ognl (ognl)
execute:436, OgnlUtil$1 (com.opensymphony.xwork2.ognl)
execute:428, OgnlUtil$1 (com.opensymphony.xwork2.ognl)
compileAndExecute:523, OgnlUtil (com.opensymphony.xwork2.ognl)
setValue:428, OgnlUtil (com.opensymphony.xwork2.ognl)
trySetValue:186, OgnlValueStack (com.opensymphony.xwork2.ognl)
setValue:173, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameter:157, OgnlValueStack (com.opensymphony.xwork2.ognl)
setParameters:214, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
doIntercept:132, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:140, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:201, StaticParametersInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:67, MultiselectInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:133, DateTextFieldInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:89, CheckboxInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:321, FileUploadInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:101, ModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:142, ScopedModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:160, ChainingInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:175, PrepareInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:121, I18nInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:167, ServletConfigInterceptor (org.apache.struts2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:228, AliasInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:196, ExceptionMappingInterceptor (com.opensymphony.xwork2.interceptor)
invoke:249, DefaultActionInvocation (com.opensymphony.xwork2)
execute:48, StrutsActionProxy (org.apache.struts2.factory)
serviceAction:574, Dispatcher (org.apache.struts2.dispatcher)
executeAction:79, ExecuteOperations (org.apache.struts2.dispatcher)
doFilter:141, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:889, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1743, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
这里如果第一个字母小写,第二个大写 则直接返回propertyName。否则就将第一个字母变为大写,然后返回propertyName。这里导致我们有以下的写法返回Upload
Upload
upload
这里判断是否为baseName结尾,也就是我们可控的setxxx,getxxx方法的xxx结尾。
获取到方法以后,返回OgnlRuntime,这里会将方法push到缓存中,第二次调用就直接从缓存中获取了
注意这里的本质是我们对map中的数值进行进一步的覆盖,所以我们传入的先后顺序有影响。这里赋值的主要关系在于treemap
大写在上,小写在下。所以我们取值的时候,需要将小写的uploadFileName设置为我们的恶意文件名,达到文件名覆盖的目的。
分析发现,是因为struts2通过传入的参数进行get,set filename的方法调用覆盖导致的问题。所以不一定所有的问题参数都是upload,需要根据上传的name去做判断
原文始发于微信公众号(e0m安全屋):struts2任意文件上传分析