0x00 原理
變量覆蓋漏洞可以讓用戶定義的變量值覆蓋原有程序變量,可控制原程序邏輯。
0x01 代碼
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_Ginclude($_GET['file']){
echo 'flag={你猜猜}';
}
}
?>
0x02 代碼審計
首先對一下代碼進行分析
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
我們知道foreach是php遍歷數組的一種方式,這里waf函數遍歷$a里面的變量名和變量值,如果變量名中含有flag,則報錯
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
將_POST,_GET,_COOKIE中的值作為 $__R 值判斷,是否存在以 $__R 值為變量名的變量,如果有 繼續遍歷這個變量里的變量名和變量值,如果有里面的變量名存在而且變量名等於變量值就銷毀變量__k
。。這樣說確實有點亂,但是后面這段foreach 和 熟悉的$$ 應該能想到是通過GET 進行傳參。也就是說 $__k==$_GET[flag] ,因為后面要判斷flag傳參,所以這里只能是flag。 $$__k 是取 $_GET[flag]中的值作為變量名,
之后結合實戰可能更好理解點吧
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['daiker'])){
echo 'flag={你猜猜}';
}
}
通過 if 判斷 是否存在POST,GET,COOKIE 變量傳參,如果存在 用waf去檢測
然后再使用extract($_POST, EXTR_SKIP),extract($_GET, EXTR_SKIP)將變量寫入符號表保存起來,而且使用到EXTR_SKIP關鍵字后不會覆蓋之前的變量。
最后判斷是否通過flag進行了傳參,傳了后,將$flag中的值和$daiker進行比較,相同則結束, 不相同 則 通過md5加密 進行比較,這里存在個php若類型,
我們可以通過兩種md5加密后為0e開頭的字符串進行繞過,比如QNKCDZO和s878926199a
0x03 實戰
雖然waf函數是最先定義的,但是它是在消除變量后使用的,也就是說題目意思其實是讓我們先進行變量清除-》unset函數,不然無法繞過waf函數。因為waf函數匹配了'_POST', '_GET', '_COOKIE',而且后面有要求是通過flag進行傳參,所以只能先通過flag和daiker傳入對應的值。
簡單測試一下吧
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
echo '成功消除函數';
}
}
}
var_dump($_POST);
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_Ginclude($_GET['file']){
echo 'flag={你猜猜}';
}
}
?>
其實如果理清了思路,多測測就能得到flag了,但是想要深入了解 foreach 是不行的,這里我們構造GET:?_POST[flag]&_POST[daiker] POST: flag=&daike= 來做個小實驗
可以說明當我們傳入?_POST[flag]&_POST[daiker]時 ,$_GET 保存了 以 _POST為鍵 值為 flag 和daiker 的數組 的數組。然后通過foreach遍歷因為有'_POST', '_GET', '_COOKIE' 所以總共遍歷了3次, 遍歷到_GET取出數組賦值到$__R保存,$$__R其實就是$_POST 判定是否存在POST,這里我們是POST提交了參數的,所以存在,繼續執行,取出里面的鍵和值 其實就是 flag:NULL 和 daiker:NULL 判斷是否存在 $flag 由於我們POST提交了 flag和daiker,所以它遍歷時可判斷存在$flag,而且post提交的flag的值正好和GET里flag值一樣都為空,通過unset 消去了匹配到的$flag和$daiker,使得$_POST里面沒了內容。
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
echo '成功消除函數';
}
}
}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
var_dump($_POST);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_Ginclude($_GET['file']){
echo 'flag={你猜猜}';
}
}
?>
根據上面的運行結果我們可以看到之前被消去的$_POST突然回來了,賦值后應該會明顯點。
也就是說知道了這個原理,我們可以通過GET: ?flag=aa&daiker=bb POST: _GET[flag]=aa&_POST[daiker]=bb , 之前故意調換順序的就是為了讓這里變得更加清楚點。
這里正好和前面反過來,GET傳進去了后 遍歷時就存在 $flag 變量 而$_GET[flag] 通過 foreach 進行鍵值得配對,最后刪除了$_GET中的$flag和$daiker,然后通過extract函數,
對extract進行測試
<?php
if($_POST){
extract($_POST, EXTR_SKIP);
echo '成功';
}
var_dump($_GET);
?>
測試失敗,說明 extract 是不能直接進行變量的覆蓋的,那只能說明 unset 沒有刪除干凈了,可能涉及到指針吧,如果unset是使用引用置NULL,而extract是通過指針訪問數組內存的話就有可能,因為內存地址保存的值還是原來的GET,通過extract可以獲取到之前GET中的值,從而繞過了unset。
因為_GET雖然沒了但是POST_存在,會通過指針去訪問原來保存了_GET變量的內存地址,直接取出原來的值。(感覺設置不能覆蓋之前變量的關鍵字也和內存地址有干系)。
知道了這個原理,后面的弱類型也就簡單了,只要把aa和bb改成QNKCDZO和s878926199a,嘗試一下。
成功爆出flag,但是我想看看 extract 是如何覆蓋的。