關於WAF的那些事
首先WAF(Web Application Firewall),俗稱Web應用防火牆,主要的目的實際上是用來過濾不正常或者惡意請求包,以及為伺服器打上臨時補丁的作用。
1、雲waf:
在配置雲waf時(通常是CDN包含的waf),DNS需要解析到CDN的ip上去,在請求uri時,資料包就會先經過雲waf進行檢測,如果通過再將資料包流給主機。常見產品:阿里雲防護,騰訊雲防護,創宇雲之類等。
2、主機防護軟體:
在主機上預先安裝了這種防護軟體,和監聽web埠的流量是否有惡意的,所以這種從功能上講較為全面。這裡再插一嘴,mod_security、ngx-lua-waf這類開源waf雖然看起來不錯,但是有個弱點就是升級的成本會高一些。常見產品:雲鎖,安全狗之類產品。
3、硬體WAF:
硬體WAF可以理解為流量代理,一般部署方式都需要流量經過它,針對資料包進行拆包->清洗->規則命中->放行/丟棄,當然現在更有甚者,給WAF上了一個頭腦,採用深度學習,語義分析等操作,來減少本身WAF因為單調的規則導致可能被繞過的問題。常見產品:各產品鐵盒子waf
為什麼WAF可被繞過
- 業務與安全存在一定的衝突。
- WAF無法100%覆蓋語言,中介軟體,資料庫的特性。
- WAF本身漏洞。
0x02 waf繞過方式
1、Web容器的特性
特殊的百分號
在 IIS+ASP 的環境中,對於URL請求的引數值中的 %
,如果和後面的字元構成的字串在 URL編碼表 之外,ASP指令碼處理時會將其忽略。
假設現在有個url是:
http://www.test.com/test.asp?id=1 union se%lect 1,2,3,4 fro%m adm%in
再經過 IIS+ASP 中處理之後。
到達伺服器上的實際是
http://www.test.com/test.asp?id=1 union all select 1,2,3,4 from admin
原理是因為在WAF層,獲取到的id引數值為 1 union all se%lect 1,2,3,4 fro%m adm%in
,此時waf因為 % 的分隔,無法檢測出關鍵字 select from 等。
但是因為IIS的特性,最後在伺服器上解析的時候, id 獲取的實際引數就變為 1 union all select 1,2,3,4 from admin
,從而繞過了waf。
PS:這個特性僅在iis+asp上 asp.net並不存在
unicode編碼
IIS支援Unicode編碼字元的解析,但是某些WAF卻不一定具備這種能力。(已知 s 的unicode編碼為: %u0053 , f 的unicode編碼為 %u0066 )。
假設現在有個連結
http://www.test.com/test.asp?id=1 union %u0053elect 1,2,3,4 %u0066rom admin
當該請求到達WAF之後,WAF獲取的數值是
1 union %u0053elect 1,2,3,4 %u0066rom admin
由於資料清洗等其他原因,WAF取出髒資料,進行規則匹配的時候,可能理解只是一個 union ,不會將其阻斷,那麼經過到達 IIS中介軟體 的時候, IIS 會做如下處理:
最後伺服器和資料庫最終獲取到的引數會是:
1 union select 1,2,3,4 from admin
此方法還存在另外一種情況,多個不同的widechar可能會被轉換為同一個字元。例如: 3609647549905" target="_blank" rel="nofollow,noindex">WideChar和MultiByte字元轉換問題
s%u0065lect->select s%u00f0lect->select
PS:
其實不止這個,還有很多類似的:
字母a: %u0000 %u0041 %u0061 %u00aa %u00e2 單引號: %u0027 %u02b9 %u02bc %u02c8 %u2032 %uff07 %c0%27 %c0%a7 %e0%80%a7 空白: %u0020 %uff00 %c0%20 %c0%a0 %e0%80%a0 左括號(: %u0028 %uff08 %c0%28 %c0%a8 %e0%80%a8 右括號): %u0029 %uff09 %c0%29 %c0%a9 %e0%80%a9
HPP(HTTP Parameter Pollution): HTTP引數汙染
在HTTP協議中是允許同樣名稱的引數出現多次的。
例如:
http://www.test.com/test.asp?id=123&id=456
假設提交的引數即為:
id=1&id=2&id=3
Asp.net + iis:id=1,2,3 Asp + iis:id=1,2,3 Php + apache/nginx:id=3
所以對於這類過濾規則,假設利用以下payload:
id=union+select+password/*&id=*/from+admin
來逃避對 select * from 的檢測。因為HPP特性,id的引數值最終會變為:
union select password/*,*/from admin
上面的例子是一個ASP的,再舉例一個PHP的例子,程式碼如下:
1、對於傳入的非法的 $_GET 陣列引數名,會將他們轉換成 下劃線 。經過fuzz,有以下這些字元。
2、php在遇到相同引數時接受的是第二個引數。
3、通過 $_SERVER[‘REQUEST_URI’] 方式獲得的引數並不會對引數中的某些特殊字元進行轉換。
這裡的程式碼中有兩個waf。
第一個WAF在程式碼 第29行-第30行 ,這裡面採用了 dowith_sql() 函式,跟進一下 dowith_sql() 函式,該函式主要功能程式碼在 第19-第26行 ,如果 $_REQUEST 陣列中的資料存在 select|insert|update|delete 等敏感關鍵字或者是字元,則直接 exit() 。如果不存在,則原字串返回。
而第二個WAF在程式碼 第33行-第39行 ,這部分程式碼通過 $_SERVER[‘REQUEST_URI’] 的方式獲取引數,然後使用 explode 函式針對 & 進行分割,獲取到每個引數的引數名和引數值。然後針對每個引數值呼叫 dhtmlspecialchars() 函式進行過濾,跟進一下 dhtmlspecialchars() 函式,發現其相關功能程式碼在 第3行-第14行 ,這個函式主要功能是針對 ‘&’, ‘“‘, ‘<’, ‘>’, ‘(‘, ‘)’ 等特殊字元進行過濾替換,最後返回替換後的內容。
由於 第44行和第45行 程式碼我們可以看到,這題的引數獲取都是通過 REQUEST 方式,因此我們來看個例子。
第一次 $_REQUEST 僅僅只會輸出 i_d=2 的原因是因為php自動將 i.d 替換成了 i_d 。而根據我們前面說的第二個特性,出現了兩個 i_d ,php會自動使用第二個變數覆蓋第一個,因此第一次 $_REQUEST 輸出的是2。
第二次 $_REQUEST 會輸出 i_d=select&i.d=2 是因為 \$_SERVER[‘REQUEST_URI’] 並不會對特殊的符號進行替換,因此結果會原封不動的輸出。
所以這題payload如何構造,我們可以先來看個思維導圖。
- 我們通過頁面請求 i_d=padyload&i.d=123 。
- 當資料流到達第一個WAF時,由於我們開始的第一個知識點已經介紹過了,php會將引數中的某些特殊符號替換為下劃線。因此便得到了兩個 i_d ,所以此時的payload變成了 i_d=payload&i.d=123 。
- 前面我們介紹了,如果引數相同的情況下,預設 第二個引數傳入的值 會覆蓋 第一個引數傳入的值 。因此此時在第一個WAF中 i_d=123 ,不存在其他特殊的字元,因此繞過了第一個WAF。
- 當資料流到達進入到第二個WAF時,由於程式碼是通過 $_SERVER[‘REQUEST_URI’] 取引數,而我們前面開頭的第三個知識點已經介紹過了 $_SERVER[‘REQUEST_URI’] 是不會將引數中的特殊符號進行轉換,因此這裡的 i.d 引數並不會被替換為 i_d ,所以此時正常來說 i.d 和 i_d 都能經過第二個WAF。
- 第二個WAF中有一個 dhtmlspecialchars() 函式,作用前面我們已經介紹過了,這裡需要繞過它,其實很好繞過。繞過之後 i_d=payload&i.d=123 便會進入到業務層程式碼中,執行SQL語句,由於這裡的SQL語句採用拼接的方式,因此存在SQL注入。
因此最後payload如下:
http://127.0.0.1/index.php?submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123
畸形HTTP請求
當前的HTTP服務依據的是RFC2616標準的HTTP請求,但是當我們向Web伺服器傳送畸形的,也就是並非這個標準的HTTP資料包的時候,由於Web伺服器的一些相容性的特性,會盡力解析這些畸形的資料包。但是如果Web伺服器和WAF針對畸形資料包解析的差距,就可能會出現一些Bypass的情況。以下是正常的資料包:
POST /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/adminHTTP/1.1 Host: 127.0.0.1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: ECS[visit_times]=2 Connection: close ....
如果我們將資料包修改為這樣:
L1nk3r /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/admin Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: ECS[visit_times]=2 Connection: close ....
由於 L1nk3r 並不是標準HTTP協議中的請求方法,也沒有協議欄位 HTTP/1.1 ,也沒有 host 欄位。如果是在 HTTP/1.1 協議中,缺少 host 欄位一般會返回 400 bad request 。但是某些版本的Apache在處理這個請求時,預設會設定協議為 HTTP/0.9 , host 則預設使用 apache 預設的 servername ,這種畸形的請求仍然能夠被處理。
如果某些WAF在處理資料的時候嚴格按照GET、POST等標準HTTP方法來獲取資料,或者採用正則匹配的方式來處理資料,那麼這個時候就有可能因為WAF和WEB服務解析的前後不對等,繞過了這個WAF。
2、 Web應用層的問題
編碼繞過
通過一般WAF會針對傳來的資料包中帶有的編碼進行一次解碼工作,如果WAF不能進行有效解碼還原攻擊向量,可能導致繞過, 常見編碼如URL編碼、unicode編碼(IIS)、寬位元組編碼等。 例如我們用 url 的二次編碼,而經過一次 url 解碼的之後,WAF可能無法識別出它是惡意的資料,當把該惡意資料放行到Web伺服器上時,Web伺服器會再一次解碼,最後導致了WAF繞過的結果。
看個實際例子:
多資料來源的問題
一般來說Web伺服器從三個位置來獲取使用者傳入的資料:
- 從GET中獲取
- 如果GET中沒有,嘗試從POST中查詢需要的值
- 若GET和POST中都沒有,嘗試從Cookie中獲取想要的值。
典型的例子就如 ASP 和 ASP.NET ,這兩種語言中的 Request 物件對於請求資料包的解析並沒有按照RFC的標準來,一般開發者如果按照下面方式獲取資料:
ID=Request('ID'); ID=Request.Params(ID);
會出現以下這種情況:
在PHP情況下,我們看到是通過 request 方式傳入資料,而php中 REQUEST 變數預設情況下包含了 GET , POST 和 COOKIE 的陣列。在 php.ini 配置檔案中,有一個引數 variables_order ,這引數有以下可選專案
; variables_order ;Default Value: "EGPCS" ;Development Value: "GPCS" ;Production Value: "GPCS"
這些字母分別對應的是 E: Environment , G:Get , P:Post , C:Cookie , S:Server 。這些字母的出現順序,表明了資料的載入順序。而 php.ini 中這個引數預設的配置是 GPCS ,也就是說如果有 POST 方式傳入相同的陣列,就覆蓋掉 GET 方式傳入的。
我們看個簡單的例子
那個利用這個特性呢,實際上也可以bypasswaf,我們看下圖中的例子:
3、WAF自身的問題
白名單機制
WAF存在某些機制,不處理和攔截白名單中的請求資料:
1、指定IP或IP段的資料。
2、來自於搜尋引擎爬蟲的訪問資料。
3、其他特徵的資料。
以前某些WAF為了不影響站點的SEO優化,將User-Agent為某些搜尋引擎(如谷歌)的請求當作白名單處理,不檢測和攔截。偽造HTTP請求的User-Agent非常容易,只需要將HTTP請求包中的User-Agent修改為谷歌搜尋引擎的User-Agent即可暢通無阻。
資料獲取方式存在缺陷
1、某些WAF無法全面支援GET、POST、Cookie等各類請求包的檢測,當GET請求的攻擊資料包無法繞過時,轉換成POST可能就繞過去了。或者,若POST以 Content-Type: application/x-www-form-urlencoded 無法繞過時,轉換成上傳包格式的 Content-Type: multipart/form-data 也許就可以繞過了。
當然關於檔案上傳的話,可以試試將關鍵字換行分離試試看。下面看個例子:
對於網站來說,這樣寫是可以解析的,但是站在WAF的設計者的立場,他們可能並不知道,這個是可以這樣寫的。當用正則表示式去獲取上傳的檔名時,正則表示式就匹配不到了,所以上傳就被繞過了。
2、某些WAF從資料包中提取檢測特徵的方式存在缺陷,如正則表示式不完善,某些攻擊資料因為某些干擾字元的存在而無法被提取,常見的如%0a、%0b、%0c、%0d、%09、%0a等。之前某論壇流傳的一個fuzz過某狗的指令碼,程式碼主要如下:
主要上面程式碼中有兩個關鍵fuzz的內容, fuzz_zs 和 fuzz_ch ,其實主要來說還是利用mysql的一些特性,bypass一些正則表示式針對關鍵字的檢查,例如 union 、 select 之類的。
資料處理不恰當
1、%00截斷
對於 %00 進行URL解碼,實際上解碼出來的就是C語言中的NULL字元,如果WAF對獲取到的資料儲存和處理不當,那麼 %00 解碼後會將後面的資料截斷,造成後面的資料沒有經過檢測。
WAF在獲取到引數id的值並解碼後,引數值將被截斷成 1/*
,因此沒有命中規則,從而放過了。
2、&字元處理
某些WAF在對HTTP請求資料包中的引數進行檢測時,使用 & 字元對多個引數進行分割,然後分別進行檢測,如:
http://www.test.com/1.php?p1=1&p2=2&p3=3
這些WAF會使用&符號分割p1、p2和p3,然後對其引數值進行檢測。
p1=1 p2=2 p3=3
但是,如果遇到這種構造:
http://www.test.com/1.php?p1=1+union/*%26x1=1*/+select/*%26x2=1*/1,2,3+from+admin
WAF會將以上引數分割成如下3部分:
p1=1+union/* x1=1*/+select/* x2=1*/1,2,3+from+admin
由於目標伺服器就只有一個引數 p1 ,然後 x1 和 x2 是不存在的, %26 是 & 符號的 url編碼 ,如果WAF針對上述的三個引數進行分別的檢測,是不會報注入的。這裡巧妙就巧妙在利用兩個引數拼接註釋符號將不存在的 x1 和 x2 註釋了。所以最後實際上進入資料庫查詢的語句也就只有。
p1=1+union+select1,2,3+from+admin
資料清洗不恰當
當攻擊者提交的引數值中存在大量干擾資料時,如大量空格、TAB、換行、%0c、註釋等,一般waf都是清洗之後再進行規則匹配,因為如果幹擾字串過多的話檢測需要消耗大量的資源和效能,所以清洗後可以提升效能降低匹配規則的複雜度,篩選出真實的攻擊資料進行檢測,以提高檢查效能,節省資源。如果WAF對資料的清洗不恰當,會導致真實的攻擊資料被清洗,剩餘的資料無法被檢測出攻擊行為。
例如:
http://localhost/test/Article.php?id=9999-"/*" union all select 1,2,3,4,5 as "*/" from mysql.user
由於waf會結合一些資料庫的特性來清洗資料,對於 /*
來說,它只是一個字串,對於 */
來說,它也是一個字串,更是一個別名,但是對於WAF來說,它會認為這是多行註釋符,當waf把上面的payload清洗為
9999-"" from mysql.user
針對規則庫進行匹配,如果沒有命中規則,執行原始語句:
9999-"/*" union all select 1,2,3,4,5 as "*/" from mysql.user
資料庫的註釋一般是 # – /**/等。
規則通用性問題
通用型的WAF,一般無法獲知後端使用的是哪些WEB容器、什麼資料庫、以及使用的什麼指令碼語言。每一種WEB容器、資料庫以及程式語言,它們都有自己的特性,想使用通用的WAF規則去匹配和攔截,是非常難的。通用型WAF在考慮到它們一些共性的同時,也必須兼顧它們的特性,否則就很容易被一些特性給Bypass!
比如對SQL注入資料進行清洗時,WAF一般不能知道後端資料庫是MySQL還是SQL Server。那麼對於MySQL的 /*!50001Select*/
來說,這是一個Select的命令,但是對於SQL Server來說,這只不過是一個註釋而已,註釋的內容為 !50001Select
。看個例子:
9999' and 1=(select top 1 name as # from master.sysdatabases)--
經過waf之後會被當成,waf將後端資料庫認為是Mysql,由於在Mysql中 # 是註釋,經過資料清洗,無法命中規則
9999' and 1=(select top 1 name as
但是實際上,這裡的 # 只是一個字元,充當一個別名的角色而已。如果後端資料庫是SQL Server,這樣的語句是沒問題的。但是通用型WAF怎麼能知道後端是SQL Server呢?
為效能和業務妥協
要全面相容各類Web Server及各類資料庫的WAF是非常難的,為了普適性,需要放寬一些檢查條件,暴力的過濾方式會影響業務。對於通用性較強的軟WAF來說,不得不考慮到各種機器和系統的效能,故對於一些超大資料包、超長資料可能會跳過不檢測。
例如下圖中的例子:
從容師傅分享的例子中有個非常暴力的方法,直接用大資料包打掛WAF。。。用這個註釋中包含超長查詢字串,導致安全狗在識別的過程中掛掉了,連帶著整個機器 Service Unavailable:
/*666666666666666666666666666666666666666666666666666666666666666 66666666666666666666666666666666666666666666666666666666666666666 66666666666666666666666666666666666666666666666666666666666666666 66666666666666666666666666666666666666666666666666666666666666666 66666666666666666666666666666666666666666666666666666666666666666 66666666666666666666666666666666666666666666666666666666666666666 6666666666666666666666666666666666666666*/
本身缺陷
比如某WAF,預設情況下只能獲取前100個引數進行檢測,當提交第101個引數時,那麼,將無法對攻擊者提交的第100個以後的引數進行有效安全檢測,從而繞過安全防禦。( CVE-2018-9230 )