01
前言
还记得 S2-016 的利用吗?好,大概是不记得的。在 S2-016 中利用了 DefaultActionMapper 中对 url 中 redirect 、 redirectAction 、 action 参数的处理,对 ActionMapping 中 result 属性赋值,之后转发时判断了 result 是否为空,为空则正常的转发去 action 中处理,不为空则直接根据 result 跳过 action 业务逻辑直接返回,而在 StrutsResultSupport 处理时,会对 location 也就是 result 进行 ognl 解析。防御的方法是将 redirect 、 redirectAction 两个特殊参数的处理给去掉了,而对 action 特殊参数的处理也添加了 cleanupActionName 函数对 action 名进行过滤。然而在高版本中,method 这个参数的处理又出了幺蛾子。(为什么低版本没出,因为在低版本的 org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor#getActionMethod 中没找到对应的方法抛出了异常,不会去调用 action 处理)
高版本中判断了一下 devMode ,其值默认为 false ,(低版本中不会判断)故虽然没找到也不会抛出异常,会继续执行下面的逻辑。
02
概述
正如前言所说中 url 中含有特殊参数名 method 时,会对其进行 ognl 表达式解析了。
官方链接:
https://cwiki.apache.org/confluence/display/WW/S2-032
影响版本:
Struts 2.3.20 - Struts 2.3.28 ( 除去 2.3.20.3 、 2.3.24.3)
03
复现
环境:
apache-tomcat-6.0.10 、 jdk1.8.0_261 、 struts 2.3.28
从 struts 2.3.15.2 开始 struts.enable.DynamicMethodInvocation 就默认为 false 了。所以为了能够动态执行方法,我们需要在 struts.xml 中将其值设为 true 。
其他没有必要配置,可正常运行即可。
payload:
解码:
?method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,#res=@org.apache.struts2.ServletActionContext@getResponse(),#res.setCharacterEncoding(#parameters.encoding[0]),#w=#res.getWriter(),#s=new+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream()).useDelimiter(#parameters.pp[0]),#str=#s.hasNext()?#s.next():123,#w.print(#str),#w.close(),#request.toString&pp=\\A&ppp= &encoding=UTF-8&cmd=whoami
04
分析
首先分享一下,纠结了比较久的点吧, payload 中你发现没有对 xwork.MethodAccessor.denyMethodExecution 参数的设置嘛?也怪我一直没仔细去看这个参数,其实前面有的 payload 中对于这个参数的设置也是没必要的,因为在 com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept 的 finally 中会重新将该值设为 false 。所以只要不是走之前 Params 拦截器中 setParameters 中触发漏洞,理论上都不需要设置这个参数,为什么说理论上,因为我没全试,我只试了 S2-016 ,确实是不需要的。
payload 中还有一点,就是出现了三目运算符,也就是 判断?表达式:表达式 ,刚开始我看着这是啥呢,后来发现 ognl 中也是可以使用三目运算符的。
好了,那我们接下来具体来看。前面的和 S2-016 一样,从 StrutsPrepareAndExecuteFilter 步入,在 org.apache.struts2.dispatcher.mapper.DefaultActionMapper#handleSpecialParameters 中设置了 mapping 中的 method 。
接下来和 S2-016 不同的是,这时候,我们的 result 为 null ,故不会直接返回,而是正常通过 ActionProxy 执行拦截器链,进入调用 action 逻辑。
来到 com.opensymphony.xwork2.DefaultActionInvocation#invokeAction ,在 364 行会进入 com.opensymphony.xwork2.ognl.OgnlUtil#getValue(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>, java.lang.Object) 在此处会对 methodName 拼上 () 后进行 ognl 表达式解析。所以在 payload 中 method: 最后有一个 #request.toString 就是为了匹配上 () 。
接下来重点解读一下 payload 。
首先 #parameters.pp[0] 就是从 context 的 parameters 中找到 pp 的值。
所以我们的 payload 其实简化一下就是:
s = new java.util.Scanner(@java.lang.Runtime@getRuntime().exec(whoami).getInputStream()).useDelimiter("\\A")
str = s.hasNext()?s.next:123
@org.apache.struts2.ServletActionContext@getResponse().print(str)
其中 scanner.useDelimiter("\\A") 表示以文本的开头作为分隔符分割文本,也就是获取整段文本内容。
还有一个问题:为什么可以使用 #_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 来绕过 struts2 不允许执行静态方法的设置?
先看看 @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 是什么
是一个 DefaultMemberAccess 的实例。
struts 是在方法解析执行前去判断该方法是否可以访问。我们跟到 ognl.OgnlRuntime#isMethodAccessible 这里 context.getMemberAccess() 默认是返回 SecurityMemberAccess 实例。
为什么默认会返回 SecurityMemeberAccess 实例呢?
在最开始org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter#doFilter 中初始化 ActionContext 的 ValueStack ,com.opensymphony.xwork2.ognl.OgnlValueStack#setRoot 中:
70 行 createDefaultContext 将 securityMemberAccess 传入,最终赋值给 ognl.OgnlContext#_memberAccess 。所以默认会返回 securityMemberAccess 。
那么在 payload 中 #_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS 将 _memberAccess 覆盖为 DefaultMemberAccess ,故 context.getMemberAccess() 返回 DefaultMemberAccess 实例。然后走 DefaultMemberAccess 的 inAccessible 方法的逻辑。
实现了 MemberAccess 接口的两个类:
ognl.DefaultMemberAccess#isAccessible 这里只判断了方法是否为 public ,是则返回 true ,然后方法执行。
com.opensymphony.xwork2.ognl.SecurityMemberAccess#isAccessible 判断了 allowStaticMethodAccess ,默认为 false 。
05
修复
不允许动态方法调用;
对 ActionMapping 中的 method 赋值时,进行 cleanupActionName 方法的过滤。