浅析php中open_basedir存在安全隐患

0x00 预备知识

漏洞很久之前被提出来了,但并不是php代码上的问题,所以问题一直存在,直到现在。我一直没留意,后来yaseng告诉我的,他测试了好像5.5都可以。

如下是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指定的限制实际上是前缀,而不是目录名。
举例来说: 若”open_basedir = /dir/user”, 那么目录 “/dir/user” 和
“/dir/user1″都是
可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。例如设置成:
“open_basedir = /dir/user/” open_basedir也可以同时设置多个目录,
在Windows中用分号分隔目录,在任何其它系统中用
冒号分隔目录。当其作用于Apache模块时,父目录中的open_basedir路径自动被继承。
有三种方法可以在Apache中为指定的用户做独立的设置:
在Apache的httpd.conf中Directory的相应设置方法: php_admin_value
open_basedir /usr/local/apache/htdocs/ #设置多个目录可以参考如下:
php_admin_value open_basedir /usr/local/apache/htdocs/:/tmp/
在Apache的httpd.conf中VirtualHost的相应设置方法: php_admin_value
open_basedir /usr/local/apache/htdocs/ #设置多个目录可以参考如下:
php_admin_value open_basedir /var/www/html/:/var/tmp/
因为VirtualHost中设置了open_basedir之后,
这个虚拟用户就不会再自动继承php.ini
中的open_basedir设置值了,这就难以达到灵活的配置措施,
所以建议您不要在VirtualHost 中设置此项限制.
例如,可以在php.ini中设置open_basedir = .:/tmp/, 这个设置表示允许
访问当前目录和/tmp/目录. 请注意:
若在php.ini所设置的上传文件临时目录为/tmp/,
那么设置open_basedir时就必须 包含/tmp/,否则会导致上传失败.
新版php则会提示”open_basedir restriction in effect” 警告信息,
但move_uploaded_file()函数仍然可以成功取出/tmp/目录下的上传文件,不知道
这是漏洞还是新功能. 针对ShopEx472版本的配置: open_basedir =
“D:/Server;../catalog;../include;../../home;../syssite;../templates;../language;../../language;../../../language;../../../../language”

先看一段我们不考虑open_basedir安全问题代码

关于open_basedir

漏洞详情在这里

在php写了句require_once ‘../Zend/Loader.php’; 报错:
Warning: require_once() [function.require-once]: open_basedir
restriction in effect. File(../Zend/Loader.php) is not within the
allowed path(s): (D:/phpnow/vhosts/zf.com;C:/Windows/Temp;) in
D:/phpnow/vhosts/zf.com/index.php on line 6

open_basedir是php.ini中的一个配置选项

它可将用户访问文件的活动范围限制在指定的区域,

假设open_basedir=/home/wwwroot/home/web1/:/tmp/,那么通过web1访问服务器的用户就无法获取服务器上除了/home/wwwroot/home/web1/和/tmp/这两个目录以外的文件。

注意用open_basedir指定的限制实际上是前缀,而不是目录名。

举例来说: 若”open_basedir = /dir/user”, 那么目录 “/dir/user” 和
“/dir/user1″都是可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。

符号链接又叫软链接,是一类特殊的文件,这个文件包含了另一个文件的路径名。

路径可以是任意文件或目录,可以链接不同文件系统的文件。在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。

复制代码 代码如下: 0; $i–) {
chdir;}$paths = explode;$j = 0;for ($i = 0; $paths[$i] == ‘..’; $i++)
{ mkdir; $j++;}for ($i = 0; $i <= $j; $i++) { chdir;}$tmp =
array_fill;symlink, ‘tmplink’);$tmp = array_fill;symlink(‘tmplink/’ .
implode . $file, $exp);unlink;mkdir;delfile;$exp =
dirname($_SERVER[‘SCRIPT_NAME’]) . “/{$exp}”;$exp =

“n—————–content—————nn”;echo
file_get_contents;delfile;

Warning: require_once(../Zend/Loader.php) [function.require-once]:
failed to open stream: Operation not permitted in
D:/phpnow/vhosts/zf.com/index.php on line 6

0x01 命令执行函数

function getRelativePath { // some compatibility fixes for Windows paths
$from = rtrim . ‘/’; $from = str_replace; $to = str_replace;

Fatal error: require_once() [function.require]: Failed opening
required ‘../Zend/Loader.php’
(include_path=’D:/phpnow/vhosts/zf.comZend;.;C:/php5/pear’)
in D:/phpnow/vhosts/zf.com/index.php on line
6字面分析是受到了open_basedir的限制,造成Operation not
permitted(操作不被允许)。

由于open_basedir的设置对system等命令执行函数是无效的,所以我们可以使用命令执行函数来访问限制目录。

$from = explode; $to = explode; $relPath = $to;

打开php.ini跳转到open_basedir相关设置段落:

/home/puret/test/

foreach($from as $depth => $dir) { // find first non-matching dir if
{ // ignore this directory array_shift; } else { // get number of
remaining dirs to $from $remaining = count – $depth; if { // add
traversals up to first matching dir $padLength = + $remaining – 1) *
-1; $relPath = array_pad($relPath, $padLength, ‘..’); break; } else {
$relPath[0] = ‘./’ . $relPath[0]; } } } return implode;}

; 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.

且在该目录下新建一个1.txt 内容为abc

function delfile{ if { @chmod; return @unlink; }else if{ if(($mydir =
@opendir return false; while(false !== ($file = @readdir { $name =
File_Str; if && {delfile;} } @closedir; @chmod; return @rmdir ? true :
false; }}

;open_basedir
=如果设置了open_basedir,那么所有能被操作的文件就只能限制在open_basedir指定的目录里面。
这个在虚拟主机里面这个指令相当有用。不管安全模式是否打开,这个指令都不受影响。
看来php.ini没有设置open_basedir。
打开apache虚拟主机配置文件:

再在该目录下创建一个目录命名为b

function File_Str{ return str_replace(‘//’,’/’,str_replace;}

 代码如下

并且在该目录下创建一个1.php文件内容为

function getRandStr { $chars =
‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’;
$randStr = ”; for ($i = 0; $i < $length; $i++) { $randStr .=
substr($chars, mt_rand – 1), 1); } return $randStr;}

<virtualhost *>
    <directory “../vhosts/zf.com”>
        Options -Indexes FollowSymLinks
    </directory>
    ServerAdmin admin@zf.com
    DocumentRoot “../vhosts/zf.com”
    ServerName zf.com:80
    ServerAlias *.zf.com
    ErrorLog logs/zf.com-error_log
    php_admin_value open_basedir
“D:/phpnow/vhosts/zf.com;C:/Windows/Temp;”
</virtualhost>

且在php.ini中设置好我们的open_basedir

如我们欲读取/etc/passwd。其实原理就是创建一个链接文件x,用相对路径指向a/a/a/a,再创建一个链接文件exp指向x/../../../etc/passwd。

里面的php_admin_value
open_basedir就限定了操作目录。我这里是本地测试,安全因素不考虑,直接将
php_admin_value open_basedir
“D:/phpnow/vhosts/zf.com;C:/Windows/Temp;” 删除掉,重新启动apache,

open_basedir = /home/puret/test/b/

其实指向的就是a/a/a/a/../../../etc/passwd,其实就是./etc/passwd。

上面如果给利用完可以可随意删除服务器文件了,但是比较幸运的是目前php站点的安全配置基本是open_basedir+safemode,确实很无敌、很安全,即使在权限没有很好设置的环境中,这样配置都是相当安全的,当然了,不考虑某些可以绕过的情况。本文讨论两点开启open_basedir后可能导致的安全隐患(现实遇到的),一个也许属于php的一个小bug,另外一个可能是由于配置不当产生的。

我们尝试执行1.php看看open_basedir是否会限制我们的访问

这时候删除x,再创建一个x目录,但exp还是指向x/../../../etc/passwd,所以就成功跨到/etc/passwd了。

一、open_basedir中处理文件路径时没有严格考虑目录的存在,这将导致本地包含或者本地文件读取的绕过。

很明显我们无法直接读取open_basedir所规定以外的目录文件。

精华就是这四句:复制代码
代码如下:symlink(“abc/abc/abc/abc”,”tmplink”);
symlink(“tmplink/../../../etc/passwd”, “exploit”); unlink; mkdir;

看一个本地文件任意读取的例子:

接下来我们用system函数尝试绕open_basedir的限制来删除1.txt

我们访问

 代码如下

先来看看执行1.php之前的文件情况

其中并没有任何操作触发open_basedir,但达到的
效果就是绕过了open_basedir读取任意文件 。

<?php
$file = $_GET[‘file’];
preg_match(“/^img/”,
$file) or die(‘error_file’);
$file=’/home/www/upload/’.$file;
file_exists($file) or die(‘no_such_file’);
$f = fopen(“$file”, ‘r’);
$jpeg = fread($f, filesize(“$file”));
fclose($f);
Header(“Content-type: image/jpeg”);
Header(“Content-disposition: inline; filename=test.jpg”);
echo $jpeg;
?>

成功通过命令执行函数绕过open_basedir来删除文件。由于命令执行函数一般都会被限制在disable_function当中,所以我们需要寻找其他的途径来绕过限制。

错误不在php,但又不知道把错误归结到谁头上,所以php一直未管这个问题。

虽然file是任意提交的,但是限制了前缀必须为img,我们如果想跳出目录读文件,比如,读取网站根目录下的config.php,我们得提交?file=img/../../config.php,但是此处有个限制条件,就是upload目录下不存在img文件夹,在windows文件系统里,系统不会去考虑目录存在不存在,会直接跳转目录从而导致漏洞;但linux文件系统非常严谨,它会仔细判断每一层目录是否存在,比如这里由于不存在img,则跳出去读取文件的时候直接报错。看如下一个示意图:

0x02 symlink()函数

open_basedir

再看一个类似的本地包含的例子:

我们先来了解一下symlink函数

将 PHP 所能打开的文件限制在指定的目录树,包括文件本身。本指令 不受
安全模式打开或者关闭的影响。

 代码如下

bool symlink ( string $target , string $link )

当一个脚本试图用例如 fopen
打开一个文件时,该文件的位置将被检查。当文件在指定的目录树之外时 PHP
将拒绝打开它。所有的符号连接都会被解析,所以不可能通过符号连接来避开此限制。

<?php
include “aaa”.$_GET[‘lang’].”.php”;
?>

symlink函数将建立一个指向target的名为link的符号链接,当然一般情况下这个target是受限于open_basedir的。由于早期的symlink不支持windows,我的测试环境就放在Linux下了。

特殊值 .
指明脚本的工作目录将被作为基准目录。但这有些危险,因为脚本的工作目录可以轻易被
chdir() 而改变。

由于linux文件系统的限制,我们无法利用旁注去包含tmp下的文件。

测试的PHP版本是5.3.0,其他的版本大家自测吧。

在 httpd.conf 文件中中,open_basedir
可以像其它任何配置选项一样用“php_admin_value open_basedir none”的
方法 关闭。

linux严谨的考虑在php那里显然没有得到深刻的体会。在开启了open_basedir的时候,php对传入的文件路径进行了取真实路径的处理,然后跟open_basedir中设置的路径进行比较:

在Linux环境下我们可以通过symlink完成一些逻辑上的绕过导致可以跨目录操作文件。

在 Windows 中,用分号分隔目录。在任何其它系统中用冒号分隔目录。作为
Apache 模块时,父目录中的 open_basedir 路径自动被继承。

 代码如下

我们首先在/var/www/html/1.php中 编辑1.php的内容为

用 open_basedir
指定的限制实际上是前缀,不是目录名。也就是说“open_basedir =
/dir/incl”也会允许访问“/dir/include”和“/dir/incls”,如果它们存在的话。如果要将访问限制在仅为指定的目录,用斜
线结束路径名。例如:“open_basedir = /dir/incl/”。

……
/* normalize and expand path */
if (expand_filepath(path, resolved_name TSRMLS_CC) == NULL) {
return -1;
}

接着在/var/www/中新建一个1.txt文件内容为

www.9778.com,支持多个目录是 3.0.7 加入的。

path_len = strlen(resolved_name);
memcpy(path_tmp, resolved_name, path_len + 1); /* safe */
……

再来设置一下我们的open_basedir

默认是允许打开所有文件。

但php在处理的时候忽略了检查路径是否存在,于是在开启了open_basedir时,上面那个文件读取的例子,我们可以使用?file=img/../../config.php来直接读取了,此时提交的路径已经被处理成/home/www/config.php了,所以不存在任何读取问题了。

open_basedir = /var/www/html/

我在我的VPS和树莓派上都测试过,成功读取。

问题由渗透测试的时候遇到绕过的情况从而导致疑问,经分析环境差异,然后xi4oyu牛指点有可能是open_basedir的问题后测试总结出这是php的一个小的bug,但是很有可能导致安全隐患。

在html目录下编辑一个php脚本检验一下open_basedir

相比于5.3
XML那个洞,这个成功率还是比较稳的,很多文件都能读。而且版本没要求,危害比较大。

二、open_basedir的值配置不当,有可能导致目录跨越。

意料之中,文件无法访问。

前几天成信的CTF,试了下这个脚本,apache也可以读取,当时读了读kali机子的/etc/httpd/conf/httpd.conf,没啥收获。

很多管理员都知道设置open_basedir,但在配置不当的时候可能发生目录跨越的问题。

我们执行刚才写好的脚本,1.php

发现没旁站,流量是通过网关转发的。

错误的配置:/tmp:/home/www,正确的配置:/tmp/:/home/www/

可以看到成功读取到了1.txt的文件内容,逃脱了open_basedir的限制

 代码如下

symlink(“tmplink/../../1.txt”,”exploit”);

……
/* Resolve open_basedir to resolved_basedir */
if (expand_filepath(local_open_basedir, resolved_basedir TSRMLS_CC)
!= NULL) {
/* Handler for basedirs that end with a / */
resolved_basedir_len = strlen(resolved_basedir);
if (basedir[strlen(basedir) – 1] == PHP_DIR_SEPARATOR) {
if (resolved_basedir[resolved_basedir_len – 1] !=
PHP_DIR_SEPARATOR) {
resolved_basedir[resolved_basedir_len] = PHP_DIR_SEPARATOR;
resolved_basedir[++resolved_basedir_len] = ‘/0’;
}
} else {
resolved_basedir[resolved_basedir_len++] = PHP_DIR_SEPARATOR;
resolved_basedir[resolved_basedir_len] = ‘/0’;
}
……

此时tmplink还是一个符号链接文件,它指向的路径是c/d,因此exploit指向的路径就变成了

php考虑了以/结束的路径,但是如果没有/,就直接带入下文比较了。

c/d/../../1.txt

于是,当新建一个网站为
/home/wwwoldjun/(均已经分别设置open_basedir),如果配置错误,则可以从/home/www/目录跳转到/home/wwwoldjun/目录。

由于这个路径在open_basedir的范围之内所以exploit成功建立了。

举个渗透实例,某idc商在租用虚拟主机的时候如此分配空间/home/wwwroot/userxxx/、/home/wwwroot/useryyy/…,而open_basedir是这样错误配置的:/tmp:/home/wwwroot/userxxx、/tmp:/home/wwwroot/useryyy。如果我们想通过配置的错误轻易渗透下userxxx站点,我们该怎么做?

之后我们删除tmplink符号链接文件再新建一个同名为tmplink的文件夹,这时exploit所指向的路径为

特殊值 .
指明脚本的工作目录将被作为基准目录。但这有些危险,因为脚本的工作目录可以轻易被
chdir() 而改变。

tmplink/../../

在 httpd.conf 文件中中,open_basedir
可以像其它任何配置选项一样用“php_admin_value open_basedir
none”的方法关闭(例如某些虚拟主机中)。

由于这时候tmplink变成了一个真实存在的文件夹所以tmplink/../../变成了1.txt所在的目录即/var/www/

在 Windows 中,用分号分隔目录。在任何其它系统中用冒号分隔目录。作为
Apache 模块时,父目录中的 open_basedir 路径自动被继承。

然后再通过访问符号链接文件exploit即可直接读取到1.txt的文件内容

用 open_basedir
指定的限制实际上是前缀,不是目录名。也就是说“open_basedir =
/dir/incl”也会允许访问“/dir/include”和“/dir/incls”,如果它们存在的话。如果要将访问限制在仅为指定的目录,用斜线结束路径名。例如:“open_basedir
= /dir/incl/”。

当然,针对symlink()只需要将它放入disable_function即可解决问题,所以我们需要寻求更多的方法。

0x03 glob伪协议

glob是php自5.3.0版本起开始生效的一个用来筛选目录的伪协议,由于它在筛选目录时是不受open_basedir的制约的,所以我们可以利用它来绕过限制,我们新建一个目录在/var/www/下命名为test

并且在/var/www/html/下新建t.php内容为

成功躲过open_basedir的限制读取到了文件。