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 :
发两个数据包:
/${(#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
/${(#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
春天了!