PS:以下题目均在BUUCTF
1、极客大挑战 2019 EasySQL(单引号闭合,万能账号密码,SQL注入,HackBar)
题目

做法
启动靶机,打开给出的网址



输入1、1’、1″判断SQL语句闭合方式





注:以下sql语句如’1″ ‘ 实际上是’1″‘ ,这里只是为了快速区分
(1)一般情况下,SQL语句闭合方式为单引号
当用户名为1时,形成的sql语句是 select * from table_name where username=’1′ and password=’123′;
当用户名为1″时,形成的sql语句是 select * from table_name where username=’1″ ‘ and password=’123’;
当字符串内需要包含双引号时,除了使用转义字符外,也可以使用一对单引号来包括字符串。此时字符串内的双引号被视为普通字符,无需特殊处理
同理,当字符串内需要包含单引号时,除了使用转义字符外,也可以使用一对双引号来包括字符串。此时字符串内的单引号被视为普通字符,无需特殊处理
补充
使用转义字符
\判断 SQL 注入的闭合方式
原理:
当参数后面的闭合符遇到转义字符 \ 时,闭合符会被转义,从而失去原本闭合字符串的作用
这样 SQL 语句中的字符串就无法正常结束,语句结构被破坏,数据库就会报错
通过观察被转义的字符,可以推断当前参数使用的闭合符
例如,假设后端 SQL 语句大致如下:
select * from table_name where username='$username' and password='$password';
当 username 输入 1\ 时,形成的 SQL 语句为:
select * from table_name where username='1\' and password='123';
此时,反斜杠 \ 会把它后面的单引号 ' 转义掉,导致原本用于闭合 username 字符串的单引号失效。这样 username 字符串没有正常闭合,后面的 SQL 语句结构就会被破坏,从而产生报错
也就是说,原本应该在 1 后面结束的字符串没有结束,而是继续向后匹配,直到遇到后面 password 部分中的下一个单引号 ' 才尝试闭合。
可以理解成类似下面这样:
username='1\' and password='
此时前面的字符串内容变成了:
1' and password=
而后面的 123' 就脱离了原本的 SQL 结构,导致整条 SQL 语句语法错误,从而产生报错
因此,如果报错信息中可以看到类似 1\' 的结构,说明反斜杠后面被转义的是单引号 ',可以推断 username 参数原本使用单引号闭合
也可以通过输入单引号进行验证。当 username 输入 1' 时,形成的 SQL 语句为:
select * from table_name where username='1'' and password='123';
其中,前两个单引号组成了一个完整的字符串:
'1'
但后面又多出了一个单引号 ',导致 SQL 语句结构不完整,从而报错
综上,可以判断本题中 username 参数使用的是单引号闭合
需要注意的是,本题中同时存在 username 和 password 两个变量,使用反斜杠判断时可能会影响后面 password 部分的引号结构,因此这种方法不一定非常直观,通常需要结合单引号、双引号、注释符等方式一起判断
(2)假设MySQL语句为双引号闭合
username输入1时,形成的sql语句是正确的
select * from table_name where username="1"and password="123";
username输入1″时,形成的sql语句是错误的
select * from table_name where username="1" "and password="123";
正确的SQL语句不可以出现一对双引号包含双引号的。所以上面这条应该出现SQL报错,但实际没有报错,因此我们假设的双引号闭合方式是不成立的
username输入的是1’,形成的sql语句是正确的,不会报错
select * from table_name where username="1' "and password="123";
而然实际上这条语句报错了,因此我们假设的双引号闭合方式是不成立的
综上,我们可以推出SQL语句闭合方式是单引号
进行SQL注入
万能账号密码获取入口: 万能账号密码使用详解,渗透测试常用的入门级操作 – 知乎
由上得:该数据库的闭合方式为单引号
因此,在理解完以上网页内容后,我们回归该题
当我们不知道用户的账号并且不知道用户的密码时,可以使用万能账号 我们随便挑选一个——a or true #
但是它上面的万能账号都没有加单引号’或是双引号”
因此,综上,我们需要在a后面加上本题的闭合符号,“ ‘ ” 得出我们所需的账号为a’ or true #
密码随便输即可


补充做法(使用HackBar,不过跟上面的大差不差)
自行下载HackBar V2
前提: 先看测试后的网址(与为刚从靶机点进来的网址进行对比),可以看到我们输入的账号密码都显示在url中,可知此处是get传参
补充:get传参——参数和 URI 之间用问号?隔开, 参数键值用等号=连接,然后参数之间用连接符&拼接起来




username=后面跟的是账号
password=后面跟的是密码
因此,我们根据上面说的思路,把万能账号填进去,密码随便填,构造一下,复制
check.php?username=a' or true %23&password=1
然后填进Load URL得出的网址后面,点击Execute执行
解释:%23是#的 URL 编码形式,表示普通字符#, 在 URL 中,某些字符有特殊含义,需要使用URL 编码(即百分号编码)来表示, 类似于进制统一一样,为了避免不必要的麻烦,我们通常都把格式统一一下,
以下字符在 URL 中可直接使用,无需编码:
字母:a-z、A-Z
数字:0-9
部分符号:-、_、.、~
但是遇到特殊字符的时候,最好去查一查看下要不要转变成URL
得出flag

2、极客大挑战 2019 Havefun(HTML注释源码泄露,GET参数控制,HackBar)
题目

做法
启动靶机,点进去


<!--
$cat=$_GET['cat'];
echo $cat;
if($cat=='dog'){
echo 'Syc{cat_cat_cat_cat}';
}
-->
简要分析一波
前提小知识
$cat
$:表示一个自定义的普通变量。
$ + 字符串:表示一个变量名 / 对象名。
双 $,也就是 $$:表示可变变量,用于通过一个变量的值去访问另一个变量。
$_GET
$_GET 是 PHP 中预定义的超全局变量(Superglobal Variable),用于获取通过 HTTP GET 请求发送的参数。
说人话:
如果 URL 是:
?cat=dog
那么 PHP 可以通过 $_GET['cat'] 获取到参数 cat 的值,也就是 dog。
例如题目中的代码:
$cat = $_GET['cat'];
意思是:定义一个叫 $cat 的变量,然后通过 $_GET 从 URL 参数中获取 cat 的值,并赋值给 $cat。
echo $cat
echo 是 PHP 中常用的输出方式,用于把内容输出到页面上。
例如:
echo $cat;
意思是:把变量 $cat 的值直接输出到页面。
PHP 中常见的两个输出方式是 echo 和 print。
区别:
echo可以输出一个或多个字符串。
例如输出一个字符串:
echo "hello";
输出结果:
hello
例如输出多个字符串:
echo "hello", " ", "world";
输出结果:
hello world
也可以输出变量:
$name = "cat";
echo "hello ", $name;
输出结果:
hello cat
print只能输出一个字符串,并且返回值总是1。
例如:
print "hello";
输出结果:
hello
print 有返回值,所以可以这样写:
$result = print "hello";
输出结果:
hello
此时 $result 的值是:
1
但是 print 不能像 echo 那样一次输出多个字符串:
print "hello", " ", "world";
这种写法是错误的。
如果想用 print 输出多个内容,可以先拼接成一个字符串:
print "hello" . " " . "world";
输出结果:
hello world
补充:
echo 是 PHP 的语言结构,不是普通函数,所以使用时可以不加括号,也可以加括号:
echo $cat;
echo($cat);
题目代码分析
题目中的关键代码大概是:
$cat = $_GET['cat']; // 从 URL 参数中获取 cat 的值
echo $cat; // 直接输出 cat 变量的值
if ($cat == 'dog') { // 判断 cat 的值是否等于字符串 'dog'
echo 'Syc{cat_cat_cat_cat}'; // 如果条件成立,输出类似 flag 的内容
}
解题思路
因为代码中有判断:
if ($cat == 'dog')
所以我们只需要让 URL 参数 cat 的值等于 dog 即可。
构造 URL 参数:
?cat=dog
这样:
$cat = $_GET['cat'];
就相当于:
$cat = 'dog';
条件判断成立后,页面就会输出:
Syc{cat_cat_cat_cat}
Syc{cat_cat_cat_cat} 的格式类似于常见的 flag 格式 flag{},所以可以猜测它就是本题要找的 flag。
注意点
这里的判断使用的是:
$cat == 'dog'
== 是弱比较,只判断值是否相等。
如果是严格比较,应该写成:
$cat === 'dog'
=== 不仅比较值,还会比较数据类型。
方法一
直接在URL上编写payload 根据上面的分析,我们直接在地址后面加上
/?cat=dog
然后回车,我们就可以得出flag

解释:
(1)关于 / 的作用
URL 的基本结构是:
协议://域名或IP/路径?参数名=参数值
例如:
http://example.com/?cat=dog
这里的 / 表示访问网站的根路径,也就是网站首页所在的位置。
也就是说:
http://example.com/
表示访问:
网站根目录 /
服务器通常会在根目录下自动寻找默认首页文件,例如:
index.php
index.html
所以在 CTF 题目里,如果题目的代码在首页逻辑中,比如:
$cat = $_GET['cat'];
那么我们访问:
http://example.com/?cat=dog
意思就是:
访问网站首页,并传入 GET 参数 cat=dog
而:
http://example.com?cat=dog
很多情况下也能访问,因为浏览器或服务器通常会自动把它理解成:
http://example.com/?cat=dog
也就是默认访问根路径 /。
但是为了规范和清晰,建议写成:
http://example.com/?cat=dog
原因是:
/明确表示访问网站根路径。?cat=dog明确表示给这个路径传入 GET 参数。- 写成
/?cat=dog可以清楚区分“访问路径”和“传入参数”。 - 在写题解、payload、笔记时更标准,不容易产生歧义。
简单理解:
http://example.com/?cat=dog
可以拆成:
http://example.com 网站地址
/ 访问根路径,也就是首页
?cat=dog 传入 GET 参数 cat=dog
所以 / 的作用就是:
告诉服务器:我要访问网站根路径,也就是首页;然后再把
cat=dog这个参数传进去。
再深入一点,拿我们这个现在访问的网页来举例,一样的道理
https://snk0xheart.top/buuctf-web-wp/
也要按同样的逻辑理解。
它可以拆成:
https://snk0xheart.top 网站域名
/buuctf-web-wp/ 访问路径
这里的 /buuctf-web-wp/ 不是网站根目录本身,而是根目录下面的一个路径 / 目录。
也就是说:
/ 网站根路径
/buuctf-web-wp/ 根路径下面的 buuctf-web-wp 路径
如果要给这个页面传 GET 参数,就要把参数加在完整路径后面:
https://snk0xheart.top/buuctf-web-wp/?cat=dog
意思是:
访问 /buuctf-web-wp/ 这个路径,并传入 GET 参数 cat=dog
但是注意:不要乱测,否则容易被屏蔽甚至是更严重后果
(2)关于 GET 传参
GET 传参需要使用 ?参数名=值 的格式
参数和路径之间用问号 ? 隔开
PS:
? 在 URL 里表示:
路径结束,后面开始传 GET 参数
参数名和参数值之间用等号 = 连接
多个参数之间用 & 连接。
例如:
url/?cat=dog
表示传入一个参数:
cat = dog
如果有多个参数:
/?cat=dog&id=1&name=test
表示传入三个参数:
cat = dog
id = 1
name = test
在 PHP 中,可以通过 $_GET 获取这些参数:
$cat = $_GET['cat'];
$id = $_GET['id'];
$name = $_GET['name'];
方法二(思路大差不差,不过利用了一下HackBar)

然后点击左边的Load URL即可把当前的URL放进框中
然后就像上面的思路一般,直接在后面加入
/?cat=dog
点击Execute,即可得出flag

最后,回到题目提交flag即可

3、ACTF2020 新生赛 Include(文件包含,PHP filter伪协议,任意文件读取)
题目

做法
启动靶机,点进去


文件包含漏洞:php://filter 读取源码
查看 URL,发现有参数:
?file=flag.php
说明页面可能通过 file 参数控制要访问或包含的文件。
例如后端代码可能类似于:
include($_GET['file']);
或者:
require($_GET['file']);
这种情况下,用户可以控制被包含的文件名,就可能存在文件包含漏洞。
为什么不能直接访问 flag.php
如果直接传入:
?file=flag.php
服务器会正常包含并执行 flag.php。
如果 flag.php 里面是 PHP 代码,例如:
<?php
$flag = "flag{xxxxxx}";
?>
那么 PHP 代码会被服务器执行,而不是直接显示源码。
也就是说,页面不会把:
$flag = "flag{xxxxxx}";
原样显示出来。
所以我们需要想办法让 PHP 读取文件源码,而不是执行文件代码。
php://filter 的作用
php://filter 是 PHP 内置的伪协议,也叫数据流过滤器协议。
它的作用是:
在 PHP 读取文件内容时,对文件内容进行一层处理。
这里我们使用:
convert.base64-encode
这个过滤器,把目标文件内容转换成 Base64 编码后再返回。
这样做的好处是:
原本 flag.php 会被当成 PHP 代码执行。
但是经过 Base64 编码后,文件内容变成了一串普通文本。
这样 PHP 就不会把它当成 PHP 代码执行,而是把编码后的内容显示出来。
然后我们再把 Base64 解码,就可以得到 flag.php 的源码。
构造 Payload
构造如下 payload:
?file=php://filter/read=convert.base64-encode/resource=flag.php
完整含义是:
通过 file 参数,让服务器使用 php://filter 读取 flag.php,
并在读取时把 flag.php 的内容进行 Base64 编码后返回。
Payload 结构解释
php://filter/read=convert.base64-encode/resource=flag.php
可以拆成三部分:
php://filter
php://filter
这是 PHP 的数据流过滤器协议。
它可以在读取或写入文件时,对文件内容进行过滤处理。
在文件包含题里,常用它来读取 PHP 文件源码。
read=convert.base64-encode
read=convert.base64-encode
这里表示在读取文件时,使用 convert.base64-encode 过滤器。
convert.base64-encode 的作用是:
把读取到的文件内容转换成 Base64 编码
比如原本文件内容是:
<?php
$flag = "flag{test}";
?>
经过 Base64 编码后,页面返回的就是一串类似这样的内容:
PD9waHAKJGZsYWcgPSAiZmxhZ3t0ZXN0fSI7Cj8+
拿到这串 Base64 后,再解码,就可以看到原来的 PHP 源码。
resource=flag.php
resource=flag.php
resource 是必选参数。
它表示要读取和处理的目标文件。
这里的意思是:
读取 flag.php 这个文件
所以整体 payload 的意思就是:
读取 flag.php,并把它的内容进行 Base64 编码后返回
为什么要加 Base64 编码
因为如果直接包含:
?file=flag.php
PHP 会执行 flag.php 里的代码,我们不一定能看到源码。
但是使用:
?file=php://filter/read=convert.base64-encode/resource=flag.php
PHP 读取到的是经过 Base64 编码后的内容。
编码后的内容已经不是 PHP 代码格式了,所以不会被正常执行,而是会作为普通文本显示出来。
然后我们再对页面返回的 Base64 内容进行解码,就能拿到 flag.php 的源码。
关于 read、write 和过滤器
read 表示读取文件时使用过滤器。
write 表示写入文件时使用过滤器。
在读取源码的场景下,一般使用:
read=convert.base64-encode
如果不写 read 或 write,PHP 有时会根据场景自动选择合适的过滤方向。
但是为了清晰和稳定,CTF 中通常写完整:
php://filter/read=convert.base64-encode/resource=flag.php
过滤器补充
convert.base64-encode 是一种转换过滤器。
常见过滤器大致可以分为:
- 字符串过滤器
- 转换过滤器
- 压缩过滤器
- 加密过滤器
php://filter 中可以使用一个或多个过滤器。
多个过滤器之间可以用 | 连接。
例如:
php://filter/read=过滤器1|过滤器2/resource=目标文件
在 CTF 文件包含题中,灵活使用过滤器经常是解题关键。
最终流程总结
- URL 中出现
?file=flag.php,说明可能通过参数控制文件名。 - 猜测后端可能存在文件包含逻辑。
- 直接包含
flag.php时,PHP 代码会被执行,源码不会直接显示。 - 使用
php://filter伪协议读取文件。 - 使用
convert.base64-encode把文件源码转成 Base64 文本。 - 页面返回 Base64 内容。
- 对 Base64 内容解码,得到
flag.php源码。 - 从源码中找到 flag。
一句话理解
php://filter 的作用就是:
不让 PHP 直接执行目标文件,而是先把目标文件内容转换成 Base64 文本显示出来,从而读取 PHP 源码。
这个 payload 里为什么用 /:
?file=php://filter/read=convert.base64-encode/resource=flag.php
原因是:这是 php://filter 伪协议规定的写法,/ 在这里用来分隔不同的参数段。
它不是随便选的符号。
URL修改后得出结果

复制下来,去解码,这里推荐一个(信息源于互联网,风险自担) Base64 编码/解码 – 锤子在线工具


4、HCTF 2018 WarmUp(白名单校验绕过 + 文件包含漏洞 + 路径穿越)
题目

做法
启动靶机,点进去



代码分析
访问 source.php 后,可以看到页面直接展示了 PHP 源码:
<?php
highlight_file(__FILE__);
这里用到了:
highlight_file():PHP 内置函数,用于高亮显示指定文件的源码。__FILE__:PHP 魔术常量,表示当前文件的完整路径。
所以这行代码的作用是:
把当前 PHP 文件的源码高亮显示在页面上。
- 定义类和检查函数
class emmm
{
public static function checkFile(&$page)
{
这里定义了一个类 emmm,并在类里面定义了一个静态方法 checkFile()。
解释:
class emmm:定义一个名为emmm的类。public:表示该方法可以被外部访问。static:表示这是一个静态方法。静态方法不需要先创建对象,可以直接通过类名调用。
普通方法:需要先 new 一个对象,再用 -> 调用
静态方法:不需要 new 对象,直接用 类名::方法名() 调用
例如:
emmm::checkFile("hint.php");
意思是:调用 emmm 类里的 checkFile() 方法,用来检查 "hint.php" 是否合法。
checkFile():用于检查传入的文件名是否合法。&$page:表示引用传参。
普通传参时,函数里拿到的是变量的复制品,函数内部修改它,不会影响外面的原变量。
引用传参时,函数里拿到的是原变量本身的别名,函数内部如果修改 $page,外面的原变量也会被修改。
不过这道题里,函数内部并没有直接修改 $page,而是使用了新的变量 $_page,所以这里的 &$page 对实际逻辑影响不大。
- 白名单设置
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
这里定义了一个白名单数组,只允许访问两个文件:
source.php
hint.php
注意:虽然数组的键是 source 和 hint,但后面用的是 in_array(),检查的是数组里的值,也就是:
source.php
hint.php
- 检查参数是否存在且为字符串
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
这里做了基础检查:
isset($page):判断$page是否存在,并且不是NULL。is_string($page):判断$page是否为字符串。
如果 $page 不存在,或者不是字符串,就输出:
you can't see it
然后返回 false,表示检查失败。
- 第一次白名单检查
if (in_array($page, $whitelist)) {
return true;
}
这里直接检查 $page 是否在白名单中。
也就是说,如果传入的是:
source.php
或者:
hint.php
就会返回 true,允许包含该文件。
例如:
?file=hint.php
这时 $_REQUEST['file'] 的值就是:
hint.php
符合白名单,所以可以通过检查。
- 截取
?前面的内容再检查
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
先看这一部分:
mb_strpos($page . '?', '?')
. 是字符串拼接符
在 PHP 里,. 表示拼接字符串。
例如:
$page = "source.php";
echo $page . "?";
结果是:
source.php?
所以:
$page . '?'
意思是:
在 $page 后面再拼接一个 ?
mb_strpos() 是找位置
mb_strpos($page . '?', '?')
意思是:
在
$page . '?'这个字符串里,查找第一个?出现的位置。
位置是从 0 开始数的。
比如:
source.php?aaa=bbb
每个字符的位置大概是:
s o u r c e . p h p ? a a a = b b b
0 1 2 3 4 5 6 7 8 9 10
所以第一个 ? 的位置是:
10
也就是说:
mb_strpos("source.php?aaa=bbb?", "?")
结果是:
10
注意这里虽然代码里又拼了一个 ?,但字符串本来已经有 ?,所以找到的是第一个 ?,也就是原本那个。
mb_substr($page, 0, 位置)
再看外层:
mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
mb_substr() 是截取字符串。
格式可以理解成:
mb_substr(要截取的字符串, 从哪里开始, 截取多长)
所以:
mb_substr($page, 0, 10)
意思是:
从
$page的第 0 个位置开始,截取10个字符。
如果:
$page = "source.php?aaa=bbb";
那么:
mb_substr($page, 0, 10)
得到:
source.php
因为 source.php 正好是 10 个字符。
这里的逻辑是:
- 找到
$page中第一个?出现的位置。 - 截取
?前面的内容。 - 再判断截取后的内容是否在白名单中。
例如:
source.php?aaa=bbb
截取 ? 前面的内容后,得到:
source.php
而 source.php 在白名单中,所以检查通过。
这里的:
$page . '?'
是为了防止 $page 中没有 ? 时,mb_strpos() 找不到位置。
如果原字符串里没有 ?,拼接一个 ? 后,就一定能找到 ?。
- URL 解码后再次检查
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
这里会先对 $page 进行 URL 解码。
例如:
%3F
解码后是:
?
所以如果传入:
source.php%3Faaa=bbb
经过 urldecode() 解码后变成:
source.php?aaa=bbb
然后再截取 ? 前面的内容,得到:
source.php
如果截取后的结果在白名单中,也会通过检查。
- 检查失败
echo "you can't see it";
return false;
如果前面的所有检查都没有通过,就会输出:
you can't see it
然后返回 false,不允许包含文件。
- 主程序逻辑
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
这里是程序的主要逻辑。
$_REQUEST['file']
$_REQUEST['file']
表示接收用户传入的 file 参数。
$_REQUEST 可以接收 GET、POST、Cookie 中的参数。
例如访问:
?file=hint.php
那么:
$_REQUEST['file']
拿到的值就是:
hint.php
判断条件
! empty($_REQUEST['file'])
表示 file 参数不能为空。
is_string($_REQUEST['file'])
表示 file 参数必须是字符串。
emmm::checkFile($_REQUEST['file'])
表示调用 checkFile() 方法,检查传入的文件名是否符合白名单规则。
只有这三个条件都满足,才会执行文件包含。
- 文件包含点
include $_REQUEST['file'];
这里是核心危险点。
include 会把指定文件包含进当前 PHP 脚本中。
如果被包含的文件是 PHP 代码,那么 PHP 会执行它。
如果被包含的文件是 HTML、图片报错信息、纯文本等内容,可能会直接显示在页面中。
这类代码的风险在于:
如果用户可以控制
include后面的文件名,并且校验不严格,就可能造成文件包含漏洞。
- 初步利用思路
代码中白名单允许访问:
source.php
hint.php
所以可以先访问:
?file=hint.php
因为 hint.php 在白名单中,可以通过检查。
也就是说:
$_REQUEST['file'] = "hint.php";
然后:
emmm::checkFile("hint.php")
返回 true。
接着执行:
include "hint.php";
页面就会包含并显示 hint.php 的内容。
这里用了?,即通过GET方式传入参数 file=hint.php
为什么可以?
因为这道题代码用的是:
$_REQUEST['file']
$_REQUEST 可以接收 GET、POST、COOKIE 里的参数。
(直接把根目录下的source.php换成hint.php也行,e.g.http://4751184b-cd59-4060-ad37-b25a06768ecc.node5.buuoj.cn:81/hint.php)
总结
这段代码的核心逻辑是:
- 页面通过
highlight_file(__FILE__)显示当前源码。 - 程序从
$_REQUEST['file']接收用户传入的文件名。 checkFile()会检查文件名是否符合白名单。- 白名单中只允许
source.php和hint.php。 - 如果检查通过,就执行:
include $_REQUEST['file'];
- 因为
hint.php在白名单中,所以可以先构造:
?file=hint.php
- 通过包含
hint.php查看提示,继续寻找 flag 位置。

至此,我们编写payload
source.php?file=hint.php?/../../../../ffffllllaaaagggg
or
source.php?file=source.php?/../../../../ffffllllaaaagggg
解释:
- 为什么要在
hint.php后面加?
构造的 payload 类似:
?file=hint.php?/../../../../ffffllllaaaagggg
传入 file 参数的值是:
hint.php?/../../../../ffffllllaaaagggg
代码中会先截取 ? 前面的内容:
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
所以:
hint.php?/../../../../ffffllllaaaagggg
截取 ? 前面的部分后,得到:
hint.php
而 hint.php 在白名单中,所以检查通过。
因此,? 的作用是:
让白名单检查时只看到
hint.php,从而通过校验。
include实际包含的是什么?
虽然白名单检查时只检查了 hint.php,但是后面真正执行的是:
include $_REQUEST['file'];
也就是说,include 接收到的仍然是完整内容:
include "hint.php?/../../../../ffffllllaaaagggg";
这里不要理解成:
include "hint.php";
这不严谨。
更准确的理解是:
白名单检查和
include处理的是同一个字符串,但是检查函数只看?前面的部分,而include会尝试按照完整路径去解析。
- 为什么
?/../可以绕过?
payload:
hint.php?/../../../../ffffllllaaaagggg
可以拆成:
hint.php? / .. / .. / .. / .. / ffffllllaaaagggg
这里的 hint.php? 可以理解成一个“假的路径段”。
后面的:
..
表示返回上一级目录。
所以路径大致会被规范化成类似:
../../../../ffffllllaaaagggg
也就是通过 ../ 不断向上级目录跳转,最后去包含目标文件:
ffffllllaaaagggg
这就是路径穿越。
- 为什么
?后面最好接/
推荐写法:
?file=hint.php?/../../../../ffffllllaaaagggg
而不是:
?file=hint.php?../../../../ffffllllaaaagggg
原因是:
hint.php?/../../../../ffffllllaaaagggg
路径结构更清楚:
hint.php? / .. / .. / .. / .. / ffffllllaaaagggg
也就是:
hint.php?:可以理解成一个路径段/:路径分隔符..:表示返回上一级目录hint.php?/../:可以理解成先出现一个hint.php?路径段,再通过..返回上一级,从而把前面的hint.php?抵消掉- 后面的多个
../:继续向上跳转目录,最终寻找目标文件 /的解释:
开头的 / :从根目录开始,绝对路径
中间的 / :路径分隔符
结尾的 / :表示这是目录
没有开头 / :从当前目录开始,相对路径
可能会说的有点懵,具体来说就是,自己类比一下:
/www/html/.. -> /www
/www/html/../ -> /www/
如果写成:
hint.php?../../../../ffffllllaaaagggg
那么 ? 后面的 .. 不一定会被清晰地当成单独的路径段,解析效果可能不稳定。
所以在这题里,更推荐写:
?file=hint.php?/../../../../ffffllllaaaagggg
简单记:
?用来骗过白名单检查,../用来进行目录穿越。
- 为什么不是直接访问
ffffllllaaaagggg
如果直接传:
?file=ffffllllaaaagggg
检查函数会判断:
ffffllllaaaagggg
是否在白名单中。
但白名单只有:
source.php
hint.php
所以会失败。
因此需要构造:
?file=hint.php?/../../../../ffffllllaaaagggg
让检查函数看到的是:
hint.php
但真正包含时,又能通过路径穿越访问到:
ffffllllaaaagggg
- 为什么不直接写
hint.php?/ffffllllaaaagggg
看起来:
?file=hint.php?/ffffllllaaaagggg
好像是直接从根目录访问:
/ffffllllaaaagggg
但实际上不是。
因为完整字符串是:
hint.php?/ffffllllaaaagggg
它并不是以 / 开头的绝对路径,而是以:
hint.php?
开头的相对路径。
也就是说,它更像是在找:
hint.php? 这个目录下面的 ffffllllaaaagggg
但是正常情况下并不存在一个叫 hint.php? 的目录,所以可能找不到目标文件。
而:
?file=hint.php?/../../../../ffffllllaaaagggg
里面的第一个:
/..
可以把前面的 hint.php? 这个假路径段抵消掉,然后继续向上跳目录,最终找到目标文件。
../的作用(跟上面有点重合,主要再补充一下)
../ 表示返回上一级目录。
假设当前文件在:(/var/www/html:常见的网站根目录,很多 Apache 环境默认使用它)
/var/www/html/source.php
那么:
../
表示从:
/var/www/html/
返回到:
/var/www/
再来一个:
../../
就是返回到:
/var/
继续:
../../../
就是返回到:
/
所以:
../../../../ffffllllaaaagggg
大致意思是:
不断向上返回目录,最后访问 ffffllllaaaagggg 文件
在 Linux 中,根目录 / 已经是最顶层目录了,再继续 ../ 一般还是停留在 /,所以有时多写几个 ../ 也能成功。
但是实际做题时,../ 的数量需要根据题目环境尝试。
可能会看/在不同的地方会有点懵,主要是这样的,可以结合上面的知识点深入理解一下:
.. 的核心含义是返回上一级目录。
/ 只是路径分隔符,但它出现在不同位置时含义会不同:
..返回上一级目录../返回上一级目录,后面的/表示还可以继续接路径/..前面的/是路径分隔符,..表示返回上一级目录/../前面的/是路径分隔符,..表示返回上一级目录,后面的/表示还可以继续接路径
在目录穿越 payload 中,真正起作用的是 ..,/ 主要负责分隔路径。
- 最终 payload
最终构造:
?file=hint.php?/../../../../ffffllllaaaagggg
完整访问形式类似:
/source.php?file=hint.php?/../../../../ffffllllaaaagggg
含义是:
file参数传入hint.php?/../../../../ffffllllaaaagggg- 检查函数截取
?前面的hint.php hint.php在白名单中,检查通过include使用完整路径进行包含- 通过
../进行路径穿越 - 最终包含并读取
ffffllllaaaagggg
- 一句话总结
这题的关键不是 hint.php 自己再次 include 了什么,而是:
检查函数只检查
?前面的白名单文件名,但真正include时使用完整字符串,导致可以在白名单文件名后拼接?/../../../../目标文件,通过路径穿越包含目标文件。
得出flag

复制,回去提交




