Apache Struts2 是一个开源的 Java Web 应用程序开发框架,旨在帮助开发人员构建灵活、可维护和可扩展的企业级Web应用程序。
根据最新推送,检测到Apache Struts文件上传漏洞(CVE-2023-50164)。 经过分析和研判,攻击者可利用该漏洞,在特定的条件下,通过污染(越界,特殊符号,等等)相关上传参数导致任意文件上传,执行任意代码,建议及时修复。
简单的来说就是有一个文件上传,然后有一个特定条件,然后还有相关参数
Struts 2.0.0-2.3.37
Strust 2.5.0-2.5.32
Strust 6.0.0-6.3.0
由于涉及范围太广,我就拿一个6.3.0的来测试
ActionSupport是什么?
表示它是一个Struts2的Action。该Action包含了三个属性:upload表示上传的文件,fileFileName表示上传的文件名,fileContentType表示上传的文件类型。
关于Struts的文件上传,分为两种方式
1.如果没有对配置表进行配置,默认就会去获得javax.servlet.context.tempdir默认配置的参数
2.如果配置了,就按照所给的路径进行。
以下是根据6.3.0和6.3.2进行的一个对比
其次!!!!!!!!!!!!!!!!!!!!!!需要有一个上传接口,这就是在特定的条件下
有很多种搭建方式,我采取的是最麻烦的,但方便修改源码进行debug查看
https://github.com/apache/struts
下载6.3.0
官方比较贴心,提供了两个测试项目,可以拿其一个进行修改,创建一个Aciton
模仿它官方的:
package org.apache.struts2.showcase.fileupload; import com.opensymphony.xwork2.ActionSupport; import org.apache.struts2.ServletActionContext; import java.io.*; public class UploadAction extends ActionSupport { private static final long serialVersionUID = 5156288255337069381L; private File upload; private String uploadContentType; private String uploadFileName; private String caption; 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 getCaption(){ return caption; } public void setCaption(String caption){ this.caption=caption; } public String upload() { String path = "D:\\uploads\\a\\"; String realPath = path + File.separator +uploadFileName; System.out.println(realPath); try { //做部分做文件上传 } catch (Exception e) { e.printStackTrace(); } return SUCCESS; } }
然后去struts-fileupload添加
<action class="org.apache.struts2.showcase.fileupload.UploadAction" method="upload" name="upFile"> <result>/WEB-INF/fileupload/demoupload.jsp</result> </action>
正常情况下,上传文件,缓存文件只会存在于0点几秒就会消失,以下是具体分析流程
我们采取的是默认状态,这里进入了后会判断存储路径,默认情况下是会走到setMultipartSaveDir这个方法去执行,给路径赋值,全局路径就确定了。
这边的getSaveDir返回的就是确定后的路径,生成tmp临时文件
最后,Struts就会对其进行文件的数据的删除,也就是删除刚刚的tmp临时文件。
这部分如果能够控制tmp的文件名那是可以利用的,再有一个补丁
我们先不看补丁修改了一些什么,我们先看它添加补丁后,然后写了一个测试类,那个测试类的目的是什么间接分析出补充了一部分内容是做什么?
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.struts2.dispatcher; import org.junit.Test; import java.util.HashMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class HttpParametersTest { @Test public void shouldGetBeCaseInsensitive() { // given HttpParameters params = HttpParameters.create(new HashMap<String, Object>() {{ put("param1", "value1"); }}).build(); // then assertEquals("value1", params.get("Param1").getValue()); assertEquals("value1", params.get("paraM1").getValue()); assertEquals("value1", params.get("pAraM1").getValue()); } @Test public void shouldAppendSameParamsIgnoringCase() { // given HttpParameters params = HttpParameters.create(new HashMap<String, Object>() {{ put("param1", "value1"); }}).build(); // when assertEquals("value1", params.get("param1").getValue()); params = params.appendAll(HttpParameters.create(new HashMap<String, String>() {{ put("Param1", "Value1"); }}).build()); // then assertTrue(params.contains("param1")); assertTrue(params.contains("Param1")); assertEquals("Value1", params.get("param1").getValue()); assertEquals("Value1", params.get("Param1").getValue()); } }
这是一个针对org.apache.struts2.dispatcher.HttpParameters类的单元测试类分两个
在回到上面对这部分处理的地方,这一部分是主要是处理大小写的问题,那么,看它的类名,HttpParameters证明是处理Http请求的Parameters部分的。
String keyString = String.valueOf(key).toLowerCase();:将键转换为其字符串表,并将其转换为小写。这可能是为了执行不区分大小写的比较。
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(keyString)) {:检查并且是否等于keyString(不区分大小写比较)。如果条件为真,表示找到了匹配。
进行第一组正常数据尝试:
查看HashMap里面的值:
{uploadContentType=File{name='uploadContentType'},
uploadFileName=File{name='uploadFileName'},
upload=File{name='upload'}}
改变其大小写
第二组不正常数据:
{Upload=File{name='Upload'},
UploadFileName=File{name='UploadFileName'},
UploadContentType=File{name='UploadContentType'}}
其实发现了,顺序变了,排列在前面的跑后面去了,由此可见,这会影响数据的顺序,按顺序来说的话,小写的可以覆盖大写的内容,我们还可以控制参数的顺序了,这一步的主要想的是绕过
对文件名的一个限制,这个会造成传递过去的携带/ \之间的值全部删掉。
而通过数据的覆盖,来实现目录的绕过就可以采用上面的方法
主要是注意一下TmpFile的参数变化,不难发现,它是进入了
tracker.track(result.getTempFile(), result);
```当中但它第一次是不会进入的,在第二次循环的时候,进入判断
processNormalFormField:144, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)
processUpload:105, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)
parse:71, JakartaMultiPartRequest (org.apache.struts2.dispatcher.multipart)
<init>:80, MultiPartRequestWrapper (org.apache.struts2.dispatcher.multipart)
wrapRequest:948, Dispatcher (org.apache.struts2.dispatcher)
wrapRequest:143, PrepareOperations (org.apache.struts2.dispatcher)
doFilter:112, StrutsPrepareFilter (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:893, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1789, 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:833, Thread (java.lang)</init>
这边就是判断我们的污染参数的地方,根据官方补丁进行的判断
![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20231211172658-6ee36342-9807-1.png)
![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20231211175626-8c6f5b1a-980b-1.png)
不难发现,没修改前是进行判断大小
```java
String errorKey = "struts.messages.upload.error.parameter.too.long";
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(), errorKey, null,
new Object[] { item.getFieldName(), maxStringLength, size });
if (!errors.contains(localizedMessage)) {
errors.add(localizedMessage);
}
return;
大于的话走的这部分代码
如果大小超过,既会爆出String errorKey = "struts.messages.upload.error.parameter.too.long";这个错误
最后也是去执行了 prepare.cleanupWrappedRequest(request);
这个方法,但是,生成的时候是变成了两个文件,在上面的Item.add的时候分成了两个文件添加。当你的大小超过的时候就会进行异常的处理
补的最佳处就是
params.put(item.getFieldName(), values); } finally { item.delete(); }
删除上传的文件
具体第二张图具体做的就是对Caption参数进行限制,避免长度超出
构造方式:
先Upload在最前面,优先被处理后,再通过uploadFileName来设置路径和文件名,最后会变成的部分是
{Upload=File{name='Upload'},
UploadFileName=File{name='UploadFileName'},
uploadFileName=../../shell.jsp,
UploadContentType=File{name='UploadContentType'}}
回到被创建的地方是:D:\uploads\a\../../shell.jsp
该漏洞得根据不同的场景来制作不同的poc,因为如果相应的在文件上传处进行严格的拦截和检查,那么也很难绕过。