文件包含(LFI)两题 题解

文件包含(LFI)两题 题解

本文字数:3645  阅读时长:8分钟

访问量 0

文件包含(LFI)两题 Writeup

项目题一题二
地址937ba0b47ea4bd937506d8b9.http-ctf2.dasctf.com82471751e7fcfde408bd3123.http-ctf2.dasctf.com
环境openresty + PHP/7.3.13openresty + PHP/5.6.36
入口?file=?file=
防护无过滤白名单 + 截断检查
核心障碍php 文件被解析执行任意路径被白名单拒绝
解法php://filter + base64? 截断 + 路径穿越
考点流包装器检查与使用不一致 / 路径规范化
FlagCTF2{fe966466-067b-441b-ab9e-967f78c27225}CTF2{6d8e40f8-f3eb-4c12-b1a9-2296c4d86acb}
解题日期2026-11-062026-11-06

第一部分 · 知识点

1.1 include 的本质:解析执行,不是读文件

这是整个 LFI 方向的地基。

include / require 把目标文件当 PHP 代码解析执行,不是把内容读出来给你看。

被包含的文件实际发生什么页面上能看到什么
x.txt(纯文本,无 <?php)原样输出全部内容
x.php(<?php $a='flag'; ?>)当代码执行,标签内不输出空白
flag.php(<?php echo "hi"; //flag{...})执行,只输出 hihi,flag 不见

所以「包含 flag.php 后一片空白 / 只有一句提示」不是失败,是 include 的正常语义。

由此推出 LFI 的第一分水岭:我要的是「文件里的字符」,还是「让文件里的代码跑起来」?

本题两题都属于「要字符」,所以核心都是让内容在交给 PHP 解释器之前,先变成它不认识的东西。

1.2 LFI 的三条能力方向

方向目的依赖
读文件内容拿到 .php 的源码能把内容当「数据」而非「代码」交出来
执行代码让服务器跑我给的 PHP/命令输入能变成可执行内容
写文件再包含先落地文件,再 include有可控写入点(日志 / session / 上传 / 临时文件)

1.3 PHP 流包装器表

include 的参数不一定是路径,PHP 会按协议前缀匹配包装器。

包装器能力限制
php://filter对数据流做过滤器链处理后再交给 include通常不受 allow_url_include 限制,CTF 最常用
php://input读 POST body 当文件内容受 allow_url_include,默认 Off
data://内联数据当文件(可塞 PHP 代码)受 allow_url_include,默认 Off
phar://访问 phar 归档内文件需能上传 phar;可能触发反序列化
zip:// / compress.zlib://读压缩包内成员前提是能上传压缩包
file://本地文件等价于直接写路径
expect://直接执行命令需 expect 扩展,现实中几乎没有

php://filter 的语法结构:

text
php://filter/<过滤器链>/resource=<目标文件>
①协议              ②可替换可叠加        ③固定写法

真正要记的只有两个词:php://filter 和 resource=,中间那截按规律现场拼。

1.4 过滤器命名规律(不用背)

规律例子
成对出现,后缀 -encode / -decodeconvert.base64-encode / convert.base64-decode
convert. 是转换类命名空间convert.quoted-printable-encode
string. 字符串类,看名字就懂string.toupper / string.strip_tags
iconv. 编码转换,格式 iconv.<源>.<目标>convert.iconv.utf-8.utf-16
类别内容
值得背(3 个)php://filter、php://input、data://
值得记结构php://filter/<过滤器>/resource=<文件>
现场查手册具体过滤器名、iconv 组合
绝不要背filter chain(几十个 convert.iconv 串联)—— 那是工具生成的

1.5 为什么 base64 能绕过「被解析」

include 会把文件当代码解析。base64 编码后,内容变成 PD9waHAg... 这种纯字母数字:

  • 里面不再有 <?php 标记,解释器认不出来,只能原样吐出
  • 纯 ASCII,二进制内容也不会把响应搞乱
  • 可逆,解码即得原文

同理,convert.iconv、quoted-printable 这些过滤器目的一样:把内容变得不像代码。


第二部分 · 题一:php://filter 读源码

2.1 侦察

首页只给了一个链接:

html
<a href="?file=flag.php">tips</a>

参数名 file,值直接是 flag.php —— 参数形态 + PHP 后端,锁定 include($_GET['file'])。

本题未暴露源码,以下结论由行为推断:

请求返回推出
?file=flag.phpCan you find out the flag?文件被当代码执行,只 echo 了这一句
?file=/etc/passwd完整文件内容无路径过滤,支持绝对路径

两个现象放在一起,结论非常干净:

text
/etc/passwd → 能看(纯文本,没有 <?php)
flag.php    → 只有一句提示(它是 PHP,被解析执行了)

同样的 include,唯一差别就是文件里有没有 <?php。

2.2 原理

环节发生什么
include "flag.php"PHP 读取文件 → 交给 Zend 解析 → 执行 → 只有 echo 的内容进响应
文件里的 //CTF2{...} 注释被解释器丢弃,永远不会出现在页面上
include "php://filter/convert.base64-encode/resource=flag.php"先编码成 Base64,再作为「输出」交出来,不进入解析阶段

关键点:php://filter 让 include 不再是「执行文件」而是「输出数据流」,这是同一个函数的两种用法。

2.3 Payload

text
?file=php://filter/convert.base64-encode/resource=flag.php

响应(混在页面自带 HTML 里):

text
<meta charset="utf8">
PD9waHAKZWNobyAiQ2FuIHlvdSBmaW5kIG91dCB0aGUgZmxhZz8iOwovL0NURjJ7ZmU5NjY0NjYtMDY3Yi00NDFiLWFiOWUtOTY3Zjc4YzI3MjI1fQo=

Base64 解码后拿到 flag.php 的真实源码:

php
<?php
echo "Can you find out the flag?";
//CTF2{fe966466-067b-441b-ab9e-967f78c27225}

2.4 踩坑记录

坑现象处理
响应开头有 <meta charset="utf8">直接全选 Base64 解码失败 / 乱码只取中间那段纯字母行再解码
flag 在注释里页面永远不显示必须读源码,不能指望执行结果
用浏览器地址栏正常,本题 payload 无 ?/# 问题但含 ? # 的 payload 必须走 Burp/curl

第三部分 · 题二:白名单绕过 + 路径穿越

3.1 侦察

首页只有一个 HTML 注释:

html
<!--source.php-->

访问 ?file=source.php 拿到判卷规则:

php
$whitelist = ["source"=>"source.php", "hint"=>"hint.php"];

if (in_array($page, $whitelist)) return true;

$_page = mb_substr($page, 0, mb_strpos($page . '?', '?'));
if (in_array($_page, $whitelist)) return true;

$_page = urldecode($page);
$_page = mb_substr($_page, 0, mb_strpos($_page . '?', '?'));
if (in_array($_page, $whitelist)) return true;

echo "you can't see it";
return false;

调用处:

php
if (!empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file'])) {
    include $_REQUEST['file'];   // ← 用的是原值
    exit;
}

?file=hint.php 给出目标:

text
flag not here, and flag in ffffllllaaaagggg

3.2 三段检查翻译成人话

检查看的是哪个值通过条件
①原始 $page 整体完全等于 source.php / hint.php
②$page 中第一个 ? 之前等于白名单名
③先 urldecode,再取第一个 ? 之前等于白名单名

3.3 漏洞本质:检查与使用不一致

php
emmm::checkFile($_REQUEST['file']);  // 检查的是「截断后的值」
include $_REQUEST['file'];           // 执行的是「原始完整字符串」

检查的对象 ≠ 执行的对象,白名单在逻辑上被架空。

? 成了分界线:

位置谁在看内容要求
? 左边checkFile必须是 hint.php 或 source.php
? 右边include → 文件系统随便写,检查函数不看

一个字符串,两副面孔。

3.4 路径穿越原理

? 后面必须紧跟 /,让 hint.php? 成为一个目录段:

写法结果
hint.php?/../../../../etc/passwd✅ 能读到
hint.php?../../../../etc/passwd❌ 空白(hint.php?.. 粘成一个文件名,.. 失效)

路径是纯字符串计算的,.. 的回退不检查目录是否真实存在。

text
/var/www/html/hint.php?     ← 假设 "hint.php?" 是个目录(其实不存在)
                    ↓ ..
/var/www/html
                    ↓ ..
/var/www
                    ↓ ..
/var
                    ↓ ..
/
                    ↓ 接上目标名
/ffffllllaaaagggg

a/.. 在计算上等于「空」,所以 hint.php? 被干净抵消 —— 它只负责骗检查,不参与真实文件访问。

层数:脚本在 /var/www/html/,回到 / 需 4 层(html → www → var → /)。多写无害,到根后 .. 还是 /。

3.5 Payload

text
?file=hint.php?/../../../../ffffllllaaaagggg
        └─骗检查─┘└──── 路径穿越 ────┘└─目标─┘

响应末尾(317 字节):

text
<!--source.php-->

CTF2{6d8e40f8-f3eb-4c12-b1a9-2296c4d86acb}

3.6 三态判读表(这题全靠它)

页面返回卡在哪检查是否通过
you can't see itcheckFile❌ 字符串构造错了
空白include✅ 通过,但路径指不到文件
有内容成功✅

注意本题 display_errors=Off,include 失败不会打印 failed to open stream,只能靠这个三态表判断。

3.7 踩坑记录

坑现象原因 / 处理
.. 打成 ...一直空白路径只认恰好两个点;... 是个普通目录名,无回退语义
浏览器地址栏发送空白浏览器可能把第二个 ? 编码成 %3F;checkFile 第三段 urldecode 仍放行,但 include 拿到字面 %3F → 文件不存在
Burp 发送274 字节(失败)请求行显示与实发不一致,改用 curl.exe 复核才拿到 317 字节
层数判断—用 hint.php?/../../../../etc/passwd 先验证路径形态是否通,4 层即到达根

第四部分 · 两题对比

维度题一题二
PHP 版本7.3.135.6.36
输入过滤完全无白名单 + ? 截断 + urldecode
卡点位置「怎么读」「怎么过检查」
核心手法流包装器(数据流编码)逻辑缺陷(检查≠使用)+ 路径规范化
payload 特征长、含 php://filter短、含 ? 与 ../
依赖条件无需过滤、可绝对路径需白名单函数存在截断逻辑
判读信号有无 Base64 串you can't see it / 空白 / 有内容
共同点都是 include($_GET['file']),都在 / 或网站目录读文件同左

一句话区分:

  • 题一是**「函数用法」问题** —— include 会执行,我换个方式让它不执行。
  • 题二是**「代码逻辑」问题** —— 检查函数看的是 A,执行时用的是 B。

第五部分 · 方法论

5.1 拿到 LFI 题的四问

#问题决定什么
1能否读绝对路径(/etc/passwd)?无过滤?还是被拼了前缀?
2目标文件是「纯文本」还是「PHP」?要读源码,还是要执行结果
3allow_url_include 开没开?「执行代码」那条路能不能走
4有没有可控写入点?「写文件再包含」是否可行

5.2 命令/代码能执行但没输出时,按顺序想

顺序思路手法
1让包装器处理输出php://filter + 编码
2断开拼接的后缀;# 注释、%0A 换行(题一之前的 RCE 题)
3写文件再读日志 / session / 临时文件
4带外传出curl / DNSLog

5.3 工具层坑(真实渗透高频)

现象排查
payload 正确但一直失败用 curl.exe 复核,别信浏览器和 Repeater 的显示
? # % 在 URL 里浏览器会编码,Burp 版本差异也会改字节
编码后反而「检查过但文件找不到」说明某处对原值做了操作,而对解码值做检查(题二的第三段就是活例子)
报错被吞display_errors=Off 时用「三态判读」推断

5.4 通用公式:白名单 + 路径穿越

text
<白名单文件名> ? / (../ × N) <目标文件相对根的路径>
      ↑骗检查        ↑变目录  ↑回退层数    ↑真正要读的

变化点只有三个:白名单名、层数 N、目标路径,剩下都固定。


第六部分 · 汇总表

项目内容
题一考点include 把 php 当代码执行 → php://filter 把内容变数据
题一 payload?file=php://filter/convert.base64-encode/resource=flag.php
题一 flagCTF2{fe966466-067b-441b-ab9e-967f78c27225}(藏在注释里)
题二考点checkFile 检查截断值、include 用原值 → 检查与使用不一致
题二 payload?file=hint.php?/../../../../ffffllllaaaagggg
题二 flagCTF2{6d8e40f8-f3eb-4c12-b1a9-2296c4d86acb}
题二三态you can't see it=检查拒 / 空白=路径错 / 有内容=成功
必背三词php://filter、resource=、php://input
必记结构php://filter/<过滤器>/resource=<文件>
关键认知a/.. 是字符串运算,假目录可被抵消
关键认知检查的值 ≠ 使用的值,是白名单类漏洞的通用突破口
工具教训.. 不能写成 ...;用 curl 复核;浏览器/Burp 可能改字节