前言
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对应多文件上传的场景,也是最简单的场景,不需要任何其他背景知识方便理解
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 |
package com.struts2;import com.opensymphony.xwork2.ActionSupport;import java.io.*;import java.util.ArrayList;import java.util.List;publicclassUploadsActionextendsActionSupport{privatestaticfinallong 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;}publicvoidsetUpload(List<File> upload){this.upload = upload;}public List<String>getUploadContentType(){return uploadContentType;}publicvoidsetUploadContentType(List<String> uploadContentType){this.uploadContentType = uploadContentType;}public List<String>getUploadFileName(){return uploadFileName;}publicvoidsetUploadFileName(List<String> uploadFileName){this.uploadFileName = uploadFileName;}public List<String>getUploadedFileNames(){return uploadedFileNames;}public StringdoUpload(){for (int i =0; i < uploadFileName.size(); i++) {uploadedFileNames.add(uploadFileName.get(i));}return SUCCESS;}} |
UploadAction对应单文件上传的场景
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 |
package com.struts2;import com.opensymphony.xwork2.ActionSupport;import java.io.*;import java.util.ArrayList;import java.util.List;publicclassUploadActionextendsActionSupport{privatestaticfinallong serialVersionUID =1L;private File upload;private String uploadContentType;private String uploadFileName;public FilegetUpload(){return upload;}publicvoidsetUpload(File upload){this.upload = upload;}public StringgetUploadContentType(){return uploadContentType;}publicvoidsetUploadContentType(String uploadContentType){this.uploadContentType = uploadContentType;}public StringgetUploadFileName(){return uploadFileName;}publicvoidsetUploadFileName(String uploadFileName){this.uploadFileName = uploadFileName;}public StringdoUpload(){return SUCCESS;}} |
struts.xml
12345678910111213141516 |
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEstrutsPUBLIC"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN""http://struts.apache.org/dtds/struts-2.0.dtd"><struts><packagename="upload"extends="struts-default"><actionname="upload"class="com.struts2.UploadAction"method="doUpload"><resultname="success"type="">/file.jsp</result></action></package><packagename="uploads"extends="struts-default"><actionname="uploads"class="com.struts2.UploadsAction"method="doUpload"><resultname="success"type="">/files.jsp</result></action></package></struts> |
file.jsp
123 |
<%@page contentType="text/html; charset=UTF-8" language="java" %><%@ taglib prefix="y4tacker" uri="/struts-tags"%>上传的文件名是:<y4tacker:property value="uploadFileName" /> |
files.jsp
1234567891011 |
<%@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
12345678910111213141516171819202122232425262728293031323334 |
<?xml version="1.0" encoding="UTF-8"?><!--Licensed to the Apache Software Foundation (ASF) under one or morecontributor license agreements. See the NOTICE file distributed withthis 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 withthe License. You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License.--><web-appxmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://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> |
目录结构如下
前置知识
由于是S2-066的绕过,所以需要对上一个漏洞的原理有所了解,在我上一篇文章中Apache Struts2 文件上传分析(S2-066)对此有详细的介绍,这里就不详细描述了,对于上一个漏洞,官方的修复也很暴力,在FileUploadInterceptor
中设置参数时,忽略大小写遍历删除同名参数再做添加
123456789101112131415161718 |
public HttpParametersappendAll(Map<String, Parameter> newParams){this.remove(newParams.keySet());this.parameters.putAll(newParams);returnthis;}public HttpParametersremove(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);});}returnthis;} |
S2-067,不同于以往的漏洞分析,这一次不能通过官方的commits对比快速定位漏洞原因
原因是官方直接使用了一个新的类,在官方文档中,告诉我们在处理上传时推荐使用新的拦截器org.apache.struts2.interceptor.ActionFileUploadInterceptor
简单分析不难看到,其与之前的org.apache.struts2.interceptor.FileUploadInterceptor
最大的区别在于,这一次并没有参数存储的过程,因此也不存在变量覆盖的问题
失败的尝试
在一开始,没有其他背景知识的情况下,我的第一个思路是java.lang.String#equalsIgnoreCase
是否安全?
查看Java的实现可以看到,在regionMatches
中对于每个字符的比较过程中都是同时转小写以及大写做比较
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 |
publicbooleanequalsIgnoreCase(String anotherString){return (this == anotherString) ?true: (anotherString !=null)&& (anotherString.value.length == value.length)&& regionMatches(true,0, anotherString,0, value.length);}publicbooleanregionMatches(boolean ignoreCase,int toffset,String other,int ooffset,int len){char ta[] = value;int to = toffset;char pa[] = other.value;int po = ooffset;// Note: toffset, ooffset, or len might be near -1>>>1.if ((ooffset <0) || (toffset <0)|| (toffset > (long)value.length - len)|| (ooffset > (long)other.value.length - len)) {returnfalse;}while (len-- >0) {char c1 = ta[to++];char c2 = pa[po++];if (c1 == c2) {continue;}if (ignoreCase) {// If characters don't match but case may be ignored,// try converting both characters to uppercase.// If the results match, then the comparison scan should// continue.char u1 = Character.toUpperCase(c1);char u2 = Character.toUpperCase(c2);if (u1 == u2) {continue;}// Unfortunately, conversion to uppercase does not work properly// for the Georgian alphabet, which has strange rules about case// conversion. So we need to make one last check before// exiting.if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {continue;}}returnfalse;}returntrue;} |
在这个时候,突然想到phithon曾写过一篇关于:Fuzz中的javascript大小写特性的文章
同样的,有个天马行空的思路就是,有没有可能存在一些字符它的大写等于另一个字符的小写呢?如果存在这种情况,在后面参数绑定过程中ognl.OgnlRuntime#capitalizeBeanPropertyName
做参数处理时又通过对其转大写还原成正常的字母
很可惜,跑了很久的代码并没有发现存在这样的情况🤪那么
(Ps: 当然这其中不止失败了一次,期间也想过很多不同的思路,当然都是以失败告终🥱)
Struts2的参数绑定
在上文中提到了,新版的Struts2文件上传拦截器没有参数存储的过程,那么很容易联想到漏洞的利用还是与参数相关,Struts2中对于参数绑定通过Ognl表达式实现,具体实现在com.opensymphony.xwork2.interceptor.ParametersInterceptor
拦截器中
简单发一个上传的包Debug做验证
在com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters
中,有着对参数字符的限制函数,只有isAcceptableParameter
条件为true
才能做接下来的参数绑定
这部分限制还是满死的,毕竟历史上Struts2被爆出无数RCE漏洞,其中修修补补无数(没学过的自己去补补课),因此想要绕过各种个样限制直接完成RCE是极为困难的。另外在这里,我也不会把所有的参数限制条件列举出来,哪里卡住绕哪里即可,这里就浪费时间讲解一些不重要的过程,当然有兴趣也可以具体看看各个限制条件。
12345678910111213141516 |
// com.opensymphony.xwork2.interceptor.ParametersInterceptor#isAcceptableParameterprotectedbooleanisAcceptableParameter(String name, Object action){ParameterNameAware parameterNameAware = actioninstanceof ParameterNameAware ? (ParameterNameAware)action :null;returnthis.acceptableName(name) && (parameterNameAware ==null || parameterNameAware.acceptableParameterName(name));}// com.opensymphony.xwork2.interceptor.ParametersInterceptor#isAcceptableParameterValueprotectedbooleanisAcceptableParameterValue(Parameter param, Object action){ParameterValueAware parameterValueAware = actioninstanceof 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参数绑定过程对文件名做修改
在多文件上传情景下,为方便调试,首先简单构造一个上传多文件的数据包
12345678910111213141516171819 |
POST/uploads.actionHTTP/1.1Host:127.0.0.1:8080Connection:keep-aliveContent-Type:multipart/form-data; boundary=----WebKitFormBoundaryq0PW93h6lyBzjZNZUser-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36Content-Length:138------WebKitFormBoundaryq0PW93h6lyBzjZNZContent-Disposition: form-data;name="Upload";filename="1.txt"Content-Type:text/plainy4tacker------WebKitFormBoundaryq0PW93h6lyBzjZNZContent-Disposition: form-data;name="Upload";filename="2.txt"Content-Type:text/plain1------WebKitFormBoundaryq0PW93h6lyBzjZNZ-- |
在这个场景下如何使用简单的Ognl表达式对文件名做赋值呢?
由于在这里我们的uploadFileName是列表的格式
我们很容易想到使用中括号写法uploadFileName[0]
的形式对其中的文件名做修改,简单在控制台尝试,在这里成功对我们的文件名做了修改
在这个场景下,很容易验证得到绕过的Poc,在自己尝试时同样别忘了参数保存是在TreeMap
中,这是个有序列表,简单解释下尽管在FileUploadInterceptor
中参数保存在无序的HashMap
中了,但是在com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters
完成参数绑定的过程中重新用了有序的Treemap
做包装
因此错误的大小写以及参数名会影响其排列顺序,导致文件名无法覆盖(S2-066的时候也讲过)
S2-067之单文件上传场景绕过
同样的Payload放在单文件上传的场景自然而然就失效了,毕竟我们的uploadFileName
在这里只是一个String
类型的变量
同样的为了完成文件名的修改,我们依旧需要在参数名与文件上传参数不一致的前提下,通过Ognl参数绑定过程对文件名做修改
在讲解之前我们需要知道一个概念,在Ognl中有个重要的概念叫做值栈
,值栈主要目的是为了让能方便的访问Action的属性
在Struts2中默认的实现为OgnlValueStack
,Struts2在执行一次请求的过程中会把当前的Action对象自动存入值栈中,
因此我们只要能获取到这个对象就能完成对文件名的修改
为了方便调试Ognl语句,我们首先构造一个正常的Http流量包
1234567891011121314 |
POST/upload.actionHTTP/1.1Host:127.0.0.1:8080Connection:keep-aliveContent-Type:multipart/form-data; boundary=----WebKitFormBoundaryq0PW93h6lyBzjZNZUser-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36Content-Length:138------WebKitFormBoundaryq0PW93h6lyBzjZNZContent-Disposition: form-data;name="Upload";filename="1.txt"Content-Type:text/plainy4tacker------WebKitFormBoundaryq0PW93h6lyBzjZNZ-- |
在Struts2中我们可以使用[0]
获取整个栈对象,为方便显示转换为String对象,调用其 toString()方法输出对象信息,可以看到栈顶元素即为我们的Action对象
因此我们可以使用top
关键词直接获取到栈顶的Action
对象,从而获取到FileName
参数
因此我们可以尝试使用[0].top.UploadFilename
来对文件名做修改,但显然从返回结果来看并没有成功
经过调试发现,这里的isAcceptableParameter
返回了false
没通过的条件是com.opensymphony.xwork2.interceptor.ParametersInterceptor#isAccepted
对应的表达式为w+((.w+)|([d+])|((d+))|(['(w-?|[u4e00-u9fa5]-?)+'])|(('(w-?|[u4e00-u9fa5]-?)+')))*
没通过的原因很简单[0]
前面不能为空
这个条件Bypass也很简单,在表达式中[0].top
等价于top
最终我们成功实现了在单文件上传场景下的绕过
参考文章
https://developer.aliyun.com/article/330800
https://paper.seebug.org/794
https://cwiki.apache.org/confluence/display/WW/S2-067
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/
关 注 有 礼
本文内容来自网络,如有侵权请联系删除
原文始发于微信公众号(网络安全者):Apache Struts2 文件上传逻辑绕过(CVE-2024-53677)(S2-067)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论