Apache Struts2 文件上传漏洞分析(CVE-2023-50164)
2023-12-13 12:2:47 Author: 白帽子左一(查看原文) 阅读量:23 收藏

 扫码领资料

获网安教程

免费&进群

简述

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类的单元测试类分两个

  1. shouldGetBeCaseInsensitive 方法**
    该测试方法的主要目的是验证 HttpParameters 类的参数获取方法是否是大小写不敏感的。
    创建一个 HttpParameters 对象,包含一个键值对:"param1" -> "value1"。
    通过不同大小写形式的键来获取参数值,如 "Param1"、"paraM1"、"pAraM1"。
    使用断言验证上述不同形式的键获取的参数值是否都是 "value1"。

  2. shouldAppendSameParamsIgnoringCase** 方法
    测试了 HttpParameters 类的参数追加(append)功能是否忽略了大小写。
    创建一个 HttpParameters 对象,包含一个键值对:"param1" -> "value1"。
    然后通过不同大小写形式的键来获取参数值,验证初始状态下是否正常。
    然后创建另一个 HttpParameters 对象,包含一个键值对:"Param1" -> "Value1"。
    再将第二个 HttpParameters 对象的参数追加到第一个对象上。
    使用断言验证追加后,原始对象是否包含新的参数键 "Param1" 和 "param1",以及它们的值是否是 "Value1"。
    主要是确保 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,因为如果相应的在文件上传处进行严格的拦截和检查,那么也很难绕过。

修复方案

  1. https://struts.apache.org/download.cgi官方下载最新补丁

  2. 避免暴露无授权的文件上传路径

  3. 对文件上传处增加拦截器进行检验

来源:https://xz.aliyun.com/t/13172

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247603835&idx=1&sn=1380ed5daad3573970b1dd26ae5ffe3f&chksm=ea0e58c054ad6d5a26ae01600a715bdf9c787c360d7a4177b968923677ff2b1a0dd95a9dbb55&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh