突破open_basedir限制列举目录
PHP open_basedir
open_basedir是php.ini中的一个设置选项。它可将用户访问文件的活动范围限制在指定的区域。假设open_basedir=/var/www/html/:/tmp/,那么通过Web访问服务器的用户就无法获取服务器上除了/var/www/html/和/tmp/这两个目录以外的文件。
另外,使用open_basedir可以限制程序可操作的目录和文件,提高系统安全性。但会影响I/O性能导致系统执行变慢,因此需要根据具体需求,在安全与性能上做平衡。
另外用open_basedir指定的限制就是目录名,而不是网上传的所谓的前缀,起码我测试下来是这样的。
我们在/var/www/目录下创建一个tmp文件夹与tmp1文件夹,再创建一个1.txt文件
其中1.txt内容为www

再在tmp目录与tmp1目录下分别创建一个1.txt文件,tmp/1.txt的内容为tmp,tmp1/1.txt的内容为tmp1。
最后将/var/www/目录的所属用户和组改为apache,确保正常情况下网站可以访问。

html为网站根目录,下有一个test.php。
其中test.php就是我们用来测试的php文件,如下
1 |
|
在未设置open_basedir的情况下访问结果

我们设置open_basedir,如下

重启web服务,刷新页面,发现访问受到限制

可见,无论是file_get_contents函数还是highlight_file函数都无效。而且也可以证实open_basedir指定的限制就是目录名。
利用 glob://伪协议 直接获取目录结构
glob://协议是php5.3.0以后一种查找匹配的文件路径模式。

glob伪协议在筛选目录时不受open_basedir制约。
经测试,Windows系统也是可以直接通过glob伪协议获取目录结构的,如获取C:\下的目录结构的glob协议:
glob://c:\\*
DirectoryIterator类 + glob://伪协议
DirectoryIterator是php5中增加的一个类,为用户提供一个简单的查看目录的接口,利用此方法可以绕过open_basedir限制。
1 |
|

当然,不光可以打印文件名,也可以打印文件的其它信息。DirectoryIterator类的具体使用方法可以查找PHP官方手册:https://www.php.net/manual/en/class.directoryiterator.php
FilesystemIterator类 + glob://伪协议
FilesystemIterator继承自DirectoryIterator,在显示上与父类略微有区别
1 |
|

Filesystemterator类的具体使用方法可以查找PHP官方手册:https://www.php.net/manual/en/class.filesystemiterator.php
scandir()函数 + glob://伪协议
scandir() 函数返回指定目录中的文件和目录的数组。
1 |
|

opendir()函数 + readdir()函数 + glob://伪协议
opendir()函数打开目录句柄。readdir()函数返回目录中下一个文件的文件名。
1 |
|

利用glob伪协议直接获取目录结构无疑是最好的选择,但是如果无法使用glob伪协议或者PHP版本低于5.3.0,那么就只好通过以下方法对目录结构进行枚举。
利用readpath()函数 枚举目录结构
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来列举一下:
1 |
|
首先设置open_basedir为当前目录,并枚举d:/test/目录下的所有文件。将错误处理交给isexists函数,在isexists函数中匹配出目录名称,并打印出来。
执行可以看到:

open_basedir为c:\wamp\www,但我们列举出了d:/test/目录下的文件:

当然,这是个很粗糙的POC,因为我并没有考虑到首字母相同的文件,所以这个POC只能列举首字母不同的文件。
如果首字母相同,我们只需要再枚举第二个字符、第三个字符依次类推,即可列举出目录中所有文件。
这个方法好处是windows下php所有版本通用,当然坏处就是只有windows下才能使用通配符,如果是linux下就只能暴力猜解了。
利用SplFileInfo::getRealPath方法 枚举目录结构
受到上一个方法的启发,我开始在php中寻找类似的方法。一旦realpath不能使用的情况下,也能找到替代方式。
SplFileInfo类是PHP5.1.2之后引入的一个类,提供一个对文件进行操作的接口。其中有一个和realpath名字很像的方法叫getRealPath。
这个方法功能和realpath类似,都是获取绝对路径用的。我们在SplFileInfo的构造函数中传入文件相对路径,并且调用getRealPath即可获取文件的绝对路径。
这个方法有个特点:完全没有考虑open_basedir。在传入的路径为一个不存在的路径时,会返回false;在传入的路径为一个存在的路径时,会正常返回绝对路径。
我们的realpath函数还是考虑了open_basedir,只是在报错上没有考虑周全导致我们能够判断某个文件是否存在。但我们可爱的SplFileInfo::getRealPath方法是直接没有考虑open_basedir,就能够判断一个文件是否存在。
那么,我给出一个POC:
1 |
|
只是把之前的POC稍作修改,同样列出了D:/test下的文件:

这个方法有个特点,不管是否开启open_basedir都是可以枚举任意目录的。而上一个方法(realpath)只有在开启open_basedir且在open_basedir外的时候才会报错,才能列举目录。当然,没有开启open_basedir的时候也不需要这样列举目录了。
GD库imageftbbox/imagefttext 枚举目录结构
GD库一般是PHP必备的扩展库之一,所以我在寻找open_basedir的时候也会看看这些有用的扩展库。
我拿imageftbbox举个例子,这个函数第三个参数是字体的路径。我发现当这个参数在open_basedir外的时候,当文件存在,则php会抛出“File(xxxxx) is not within the allowed path(s)”错误。但当文件不存在的时候会抛出“Invalid font filename”错误。
也就是说,我们可以通过抛出错误的具体内容来判断一个文件是否存在。这个方法和realpath有相似性,都会抛出open_basedir的错误。
1 |
|
同样列举一下d:/test

如上图,我们发现虽然“通配符”在判断是否存在的时候奏效了,但我们真正的文件名并没有显示出来,而是还是以通配符“<><”代替。
所以,这个方法报错的时候并不会把真正的路径爆出来,这也是其与realpath的最大不同之处。所以,我们只能一位一位地猜测,但总体来说,还是能够猜测出来的,只不过可能比realpath更麻烦一些罢了。
bindtextdomain()函数 猜解目录
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。
写个简单的测试代码:
1 |
|
当/etc/passwd存在的时候输出之:

当/etc/hello不存在的时候返回false:

并没有考虑到open_basedir。所以,我们也可以通过返回值的不同来猜解、列举某个目录。
但很大的鸡肋点在,windows下默认是没有这个函数的,而在linux下不能使用通配符进行目录的猜解,所以显得很鸡肋。
当然,在万无退路的时候进行暴力猜解目录,也不失为一个还算行的方法。
总结:
open_basedir本来作为php限制跨目录读写文件的最基础的方式,应该需要进行完好的设计。但可能php在当初编写代码的时候并没有进行一个统一的设计,导致每当新增加功能或遇到一些偏僻的函数的时候,都会出现类似“open_basedir绕过”等悲剧。估计又会有人质疑了,光绕过open_basedir列目录有什么用?
诚然,列目录相比于读、写具体文件,都鸡肋了很多。但很多时候,就是这些看似“鸡肋”的漏洞组合技完成了绝杀。
下一节我会带大家讲解如何突破open_basedir实现文件读写。
参考资料
- php文件包含目录配置openbasedir的使用与性能分析傲雪星枫-CSDN博客_open_basedir php
- Open_basedir绕过 - LLeaves - 博客园 (cnblogs.com)
- open_basedir绕过 - Von的博客 | Von Blog (v0n.top)
- 『CTF Tricks』PHP-绕过openbasedir东京没有下雨天-CSDN博客
- bypass open_basedir的新方法 - 先知社区 (aliyun.com)
- PHP绕过open_basedir列目录的研究 - 掘金 (juejin.cn)
- 从PHP底层看open_basedir bypass - 知乎 (zhihu.com)
- 利用PHP-FPM实现open_basedir绕过 - 知乎 (zhihu.com)