长亭百川云 - 文章详情

Struts2 系列漏洞 - S2-057

Medi0cr1ty

34

2024-07-13

01

概述

当项目配置了 struts.mapper.alwaysSelectFullNamespace 为 true 时(默认为 false ),且 namespace 未配置或使用通配符匹配,则会将 url 中最后一个 / 前面的内容当成 namespace ,当 result type 为 chain 、 redirectAction 或 postback 时,在 result 对象返回时,会触发对 namespace 内容的 ognl 表达式解析。

官方链接:

https://cwiki.apache.org/confluence/display/WW/S2-057

影响版本:

Struts 2.0.4 - Struts 2.3.34, Struts 2.5.0 - Struts 2.5.16

02

复现

环境:

apache-tomcat-6.0.10 、 jdk1.8.0_261 、 struts 2.5.16

一个正常启动的 struts2 项目,将 struts.xml 修改:

或者下载官方示例  http://archive.apache.org/dist/struts/2.5.16/struts-2.5.16-all.zip ,IDEA 打开  struts-2.5.16\src\apps\showcase\pom.xml 以项目形式引入。

去掉 namespace ,加入 result type 。(不需要修改 struts.mapper.alwaysSelectFullNamespace 为 true 是因为在该项目中其值是动态注入为 true 的)

Struts2.5.10.1(含)之前可用 S2-045 中的 payload :

Struts2.5.13 - Struts 2.5.16 可用的 payload :

发两个数据包:

  1. /${(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}/login.action

  2. /${(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}/login

Struts2.5.12 可用的 payload :

和 Struts2.5.13 - Struts 2.5.16 一样,只是少了从 attr 中获取 context 。

03

分析

首先看一下漏洞触发的过程。

在 org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter#doFilter 中寻找请求对应的 action 时,也即是对 ActionMapping 对象进行填充时,通过 ActionMapper 的 getMapping 解析获得请求的 namespace 、 name 、 method 、 extension 、 params 、 result 信息。在其解析 name 和 namespace 时,判断了 alwaysSelectFullNamespace 参数。

为 true 则将最后 / 斜杠之前的内容赋值给 namespace ,后边的内容赋值给 name 。解析完后,(讲启动过程,可跳过)来到 org.apache.struts2.dispatcher.ExecuteOperations#executeAction 具体到 org.apache.struts2.dispatcher.Dispatcher#serviceAction 创建 com.opensymphony.xwork2.ActionProxy 实例,在创建过程中在 com.opensymphony.xwork2.config.impl.DefaultConfiguration.RuntimeConfigurationImpl#findActionConfigInNamespace 中获得了请求的 action 信息,同时初始化了 com.opensymphony.xwork2.ActionInvocation ,接下来由 proxy 调用 ActionInvocation 实例的 invoke 方法进行 action 调用。执行完拦截器栈,就来到 com.opensymphony.xwork2.DefaultActionInvocation#invokeAction 准备进入 action 具体方法中处理了。根据之前初始化的 ActionInvocation 信息进入 execute 默认方法中返回 success 。来到 com.opensymphony.xwork2.DefaultActionInvocation#executeResult 中,在 createResult 时(最终在 buildResult 中)初始化了 result 信息。

由 org.apache.struts2.result.ServletActionRedirectResult#execute 处理 result 信息。

下图中 namespace 设置为空,所以去 proxy 中取之前解析出来的 namespace 。接下来将新的 uri 存入 org.apache.struts2.result.StrutsResultSupport#location 中。

来到 org.apache.struts2.result.StrutsResultSupport#execute ,后面就和前面众多利用链一样的了。

接下来重点看一下 struts2 版本变化及对应 payload 变化

在 struts 2.5.12 后,excludedClasses 等一系列黑名单集合不再可变,无法通过 clear 来消除。(抛出异常 java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableCollection.clear )

https://github.com/apache/struts/compare/STRUTS\_2\_5\_10\_1...STRUTS\_2\_5\_12

core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java :

同时 struts 2.5.13 后也无法直接通过 #context 获取 context map 了。

首先解决后者 #context 的问题。由于 context map 中很多 key 中都包含了 context map 。比如 payload 中的 attr ,找 key 的顺序是 page -> request -> session -> application 。故 payload 中 #attr['struts.valueStack'].context 可以获得当前的 context 。

找到 context 后,还是用 S2-045 中的方法,获取 OgnlUtil 实例,通过 OgnlUtil 实例将其 excluded 集合清空。但是这里直接用 clear() 会报错,因为 excluded 集合设置为了不可变集合。

但是我们可以通过 setter 方法将他们值重新置为空。也即是:#ognlUtil.setExcludedClasses('') 、 #ognlUtil.setExcludedPackageNames('') 。

但是这样你会发现还是不行,为什么呢,因为当 setExcludedClasses 时,会分配一个新的空 set 给 ognlUtil ,而不是直接去修改 _memberAccess 和 ognlUtil 共同引用的那个地址的集合。

意思就是 OgnlUtil 中的集合确实为空了,但是 _memberAccess 与 OgnlUtil 引用的是不同的地址了,那么 OgnlUtil 中集合改变也就不能影响 _memberAccess 了。

同样一个简单的例子:

`package com.mediocrity.action;``   ``import java.util.Collections;``import java.util.HashSet;``import java.util.Set;``   ``public class Test {`    `public static void main(String[] args){`        `Set<String> excludedClassesOgnlUtil = Collections.emptySet();`        `Set<String> excludedClassesSecurityMemberAccess = Collections.emptySet();``   `        `Set<String> classes1 = new HashSet();`        `Set<String> classes2 = new HashSet();`        `classes1.add("test");`        `classes2.add("");``   `        `excludedClassesOgnlUtil = Collections.unmodifiableSet(classes1);`        `System.out.println(excludedClassesOgnlUtil);``   `        `// excludedClassesSecurityMemberAccess 与 excludeClassesOgnlUtil 引用的地址相同`        `excludedClassesSecurityMemberAccess = excludedClassesOgnlUtil;`        `System.out.println(excludedClassesSecurityMemberAccess);``   `        `try {`            `excludedClassesOgnlUtil.clear();            //抛出异常`            `System.out.println(excludedClassesOgnlUtil);`        `}catch (Exception e){`            `System.out.println(e.fillInStackTrace());`        `}`        `//重新set,excludedClassesOgnlUtil 变成空集合`        `excludedClassesOgnlUtil = Collections.unmodifiableSet(classes2);`        `System.out.println(excludedClassesOgnlUtil);`        `System.out.println(excludedClassesSecurityMemberAccess);`    `}``}`

运行:

回到 payload 分析中,其实会发现发送了两个请求。在 S2-045 中有提到 OgnlUtil 是一个单例模式,所以应用从始至终都是用的同一个 OgnlUtil ,而 _memberAccess 也即是 SecurityMemberAccess 的作用域是一次请求范围内的,那么如果重新发一次请求,初始化 _memberAccess 时会重新进行赋值操作,而此时的 OgnlUtil 中的 excludedClasses 等集合已经置为空了。也就是下一次请求 _memberAccess 中的 excludedClasses 、excludedPackageNames 是为空的,这时候就可以和之前一样的操作,将 _memberAccess 覆盖为 DefaultMemberAccess 。(为什么这么做可重新回顾 Struts2 系列漏洞 - S2-032 )

04

修复

https://github.com/apache/struts/compare/STRUTS\_2\_5\_16...STRUTS\_2\_5\_17

  • 在 ActionMapper 中对 namespace 进行过滤

  • 在 org.apache.struts2.result.StrutsResultSupport#execute 函数中判断了 parseLocation ,而 ServletActionRedirectResult 和 PostBack 中都设为了 false 。

  • com.opensymphony.xwork2.ActionChainResult 中 namespace 设置不为空才进行解析。

  • 将 setExcludedClasses 等函数完善,不直接将其赋值,而是把原本的 excludedClasses 也加入进来。

  • 完善 excludedPackageNames

春天了!

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2