Apache Struts2 文件上传逻辑绕过(CVE-2024-53677)(S2-067)
前言​ Apache官方公告又更新了一个Struts2的漏洞,考虑到很久没有发无密码的博客了,再加上漏洞的影响并不严重,因此公开分享利用的思路。分析影响版本Struts 2.0.0 - Struts 2024-12-16 12:3:43 Author: y4tacker.github.io(查看原文) 阅读量:29 收藏

前言

Apache官方公告又更新了一个Struts2的漏洞,考虑到很久没有发无密码的博客了,再加上漏洞的影响并不严重,因此公开分享利用的思路。

分析

影响版本

Struts 2.0.0 - Struts 2.3.37 (EOL), Struts 2.5.0 - Struts 2.5.33, Struts 6.0.0 - Struts 6.3.0.2

环境搭建

Struts2的环境搭建比较简单,分析时使用了两种不同漏洞场景的代码

UploadsAction对应多文件上传的场景,也是最简单的场景,不需要任何其他背景知识方便理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.struts2;

import com.opensymphony.xwork2.ActionSupport;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class UploadsAction extends ActionSupport {

private static final long serialVersionUID = 1L;
private List<File> upload;
private List<String> uploadContentType;
private List<String> uploadFileName;
private List<String> uploadedFileNames = new ArrayList<String>();

public List<File> getUpload() {
return upload;
}

public void setUpload(List<File> upload) {
this.upload = upload;
}

public List<String> getUploadContentType() {
return uploadContentType;
}

public void setUploadContentType(List<String> uploadContentType) {
this.uploadContentType = uploadContentType;
}

public List<String> getUploadFileName() {
return uploadFileName;
}

public void setUploadFileName(List<String> uploadFileName) {
this.uploadFileName = uploadFileName;
}

public List<String> getUploadedFileNames() {
return uploadedFileNames;
}

public String doUpload() {
for (int i = 0; i < uploadFileName.size(); i++) {
uploadedFileNames.add(uploadFileName.get(i));
}
return SUCCESS;
}

}

UploadAction对应单文件上传的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.struts2;

import com.opensymphony.xwork2.ActionSupport;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class UploadAction extends ActionSupport {

private static final long serialVersionUID = 1L;


private File upload;
private String uploadContentType;
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() {
return SUCCESS;
}

}

struts.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="upload" extends="struts-default">
<action name="upload" class="com.struts2.UploadAction" method="doUpload">
<result name="success" type="">/file.jsp</result>
</action>
</package>
<package name="uploads" extends="struts-default">
<action name="uploads" class="com.struts2.UploadsAction" method="doUpload">
<result name="success" type="">/files.jsp</result>
</action>
</package>
</struts>

file.jsp

1
2
3
<%@page contentType="text/html; charset=UTF-8" language="java" %>
<%@ taglib prefix="y4tacker" uri="/struts-tags"%>
上传的文件名是:<y4tacker:property value="uploadFileName" />

files.jsp

1
2
3
4
5
6
7
8
9
10
11
<%@page contentType="text/html; charset=UTF-8" language="java" %>
<%@ taglib prefix="y4tacker" uri="/struts-tags"%>
<y4tacker:if test="uploadedFileNames.size() > 0">
文件上传成功:
<y4tacker:iterator value="uploadedFileNames">
<li><y4tacker:property /></li>
</y4tacker:iterator>
</y4tacker:if>
<y4tacker:else>
no files.
</y4tacker:else>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
















<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">

<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>

</web-app>

目录结构如下

image-20241216204105946

前置知识

由于是S2-066的绕过,所以需要对上一个漏洞的原理有所了解,在我上一篇文章中Apache Struts2 文件上传分析(S2-066)对此有详细的介绍,这里就不详细描述了,对于上一个漏洞,官方的修复也很暴力,在FileUploadInterceptor中设置参数时,忽略大小写遍历删除同名参数再做添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public HttpParameters appendAll(Map<String, Parameter> newParams) {
this.remove(newParams.keySet());
this.parameters.putAll(newParams);
return this;
}

public HttpParameters remove(Set<String> paramsToRemove) {
Iterator var2 = paramsToRemove.iterator();

while(var2.hasNext()) {
String paramName = (String)var2.next();
this.parameters.entrySet().removeIf((p) -> {
return ((String)p.getKey()).equalsIgnoreCase(paramName);
});
}

return this;
}

S2-067,不同于以往的漏洞分析,这一次不能通过官方的commits对比快速定位漏洞原因

image-20241216205557836

原因是官方直接使用了一个新的类,在官方文档中,告诉我们在处理上传时推荐使用新的拦截器org.apache.struts2.interceptor.ActionFileUploadInterceptor

简单分析不难看到,其与之前的org.apache.struts2.interceptor.FileUploadInterceptor最大的区别在于,这一次并没有参数存储的过程,因此也不存在变量覆盖的问题

image-20241216210259192

失败的尝试

在一开始,没有其他背景知识的情况下,我的第一个思路是java.lang.String#equalsIgnoreCase是否安全?

查看Java的实现可以看到,在regionMatches中对于每个字符的比较过程中都是同时转小写以及大写做比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}

public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;

if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
if (ignoreCase) {




char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}




if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}

在这个时候,突然想到phithon曾写过一篇关于:Fuzz中的javascript大小写特性的文章

同样的,有个天马行空的思路就是,有没有可能存在一些字符它的大写等于另一个字符的小写呢?如果存在这种情况,在后面参数绑定过程中ognl.OgnlRuntime#capitalizeBeanPropertyName做参数处理时又通过对其转大写还原成正常的字母

image-20241216204904759

很可惜,跑了很久的代码并没有发现存在这样的情况🤪那么

(Ps: 当然这其中不止失败了一次,期间也想过很多不同的思路,当然都是以失败告终🥱)

Struts2的参数绑定

​ 在上文中提到了,新版的Struts2文件上传拦截器没有参数存储的过程,那么很容易联想到漏洞的利用还是与参数相关,Struts2中对于参数绑定通过Ognl表达式实现,具体实现在com.opensymphony.xwork2.interceptor.ParametersInterceptor拦截器中

image-20241216211033904

简单发一个上传的包Debug做验证

image-20241216211429703

com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters中,有着对参数字符的限制函数,只有isAcceptableParameter条件为true才能做接下来的参数绑定

image-20241216211708349

这部分限制还是满死的,毕竟历史上Struts2被爆出无数RCE漏洞,其中修修补补无数(没学过的自己去补补课),因此想要绕过各种个样限制直接完成RCE是极为困难的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

protected boolean isAcceptableParameter(String name, Object action) {
ParameterNameAware parameterNameAware = action instanceof ParameterNameAware ? (ParameterNameAware)action : null;
return this.acceptableName(name) && (parameterNameAware == null || parameterNameAware.acceptableParameterName(name));
}


protected boolean isAcceptableParameterValue(Parameter param, Object action) {
ParameterValueAware parameterValueAware = action instanceof ParameterValueAware ? (ParameterValueAware)action : null;
boolean acceptableParamValue = parameterValueAware == null || parameterValueAware.acceptableParameterValue(param.getValue());
if (this.hasParamValuesToExclude() || this.hasParamValuesToAccept()) {
acceptableParamValue &= this.acceptableValue(param.getName(), param.getValue());
}

return acceptableParamValue;
}

在这里,RCE的宏伟目标就暂不考虑了,我们只需要知道既然Struts2使用了Ognl做参数绑定的实现,那么便可以尝试通过参数绑定的过程去实现对上传文件名的修改,从而绕过系统对于目录穿越的限制

S2-067之多文件上传场景绕过

回到本身,简单整理下漏洞绕过的思路,用一句话来概括就是:

在参数名与文件上传参数不一致的前提下,能通过Ognl参数绑定过程对文件名做修改

在多文件上传情景下,为方便调试,首先简单构造一个上传多文件的数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /uploads.action HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryq0PW93h6lyBzjZNZ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 138


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

y4tacker

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

1


在这个场景下如何使用简单的Ognl表达式对文件名做赋值呢?

由于在这里我们的uploadFileName是列表的格式

image-20241216215633895

我们很容易想到使用中括号写法uploadFileName[0]的形式对其中的文件名做修改,简单在控制台尝试,在这里成功对我们的文件名做了修改

image-20241216214230468

在这个场景下,很容易验证得到绕过的Poc,在自己尝试时同样别忘了参数保存是在Map中的,错误的大小写会影响其排列顺序,导致文件名无法覆盖(S2-066的时候讲过,这里不再赘述)

image-20241216215221263

S2-067之单文件上传场景绕过

同样的Payload放在单文件上传的场景自然而然就失效了,毕竟我们的uploadFileName在这里只是一个String类型的变量

image-20241216215916972

同样的为了完成文件名的修改,我们依旧需要在参数名与文件上传参数不一致的前提下,通过Ognl参数绑定过程对文件名做修改

在讲解之前我们需要知道一个概念,在Ognl中有个重要的概念叫做值栈,值栈主要目的是为了让能方便的访问Action的属性

在Struts2中默认的实现为OgnlValueStack,Struts2在执行一次请求的过程中会把当前的Action对象自动存入值栈中,

因此我们只要能获取到这个对象就能完成对文件名的修改

为了方便调试Ognl语句,我们首先构造一个正常的Http流量包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /upload.action HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryq0PW93h6lyBzjZNZ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 138


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

y4tacker


在Struts2中我们可以使用[0]获取整个栈对象,为方便显示转换为String对象,调用其 toString()方法输出对象信息,可以看到栈顶元素即为我们的Action对象

image-20241216222946380

因此我们可以使用top关键词直接获取到栈顶的Action对象,从而获取到FileName参数

image-20241216223426658

因此我们可以尝试使用[0].top.UploadFilename来对文件名做修改,但显然从返回结果来看并没有成功

image-20241216223556486

经过调试发现,这里的isAcceptableParameter返回了false

image-20241216223739866

没通过的条件是com.opensymphony.xwork2.interceptor.ParametersInterceptor#isAccepted

对应的表达式为\w+((\.\w+)|(\[\d+])|(\(\d+\))|(\['(\w-?|[\u4e00-\u9fa5]-?)+'])|(\('(\w-?|[\u4e00-\u9fa5]-?)+'\)))*

image-20241216224001694

没通过的原因很简单[0]前面不能为空

image-20241216224332972

这个条件Bypass也很简单,在表达式中[0].top等价于top

image-20241216224605814

最终我们成功实现了在单文件上传场景下的绕过

image-20241216224806731

参考文章

https://developer.aliyun.com/article/330800

https://paper.seebug.org/794


文章来源: https://y4tacker.github.io/2024/12/16/year/2024/12/Apache-Struts2-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E9%80%BB%E8%BE%91%E7%BB%95%E8%BF%87-CVE-2024-53677-S2-067/
如有侵权请联系:admin#unsafe.sh