phpMyAdmin4.8.1 文件包含漏洞

起因

一个很简单的漏洞, 分析的文章也都出来了很多, 但是看了那些文章, 我一直搞不懂为啥问号要双重url编码, 我自己看那些文章感觉不编码应该也能成功利用的。 并且好像大家对问号编码的说法各有不同。 还是自己下载了份源码看了一下。

4.8.1下载地址:https://files.phpmyadmin.net/phpMyAdmin/4.8.1/phpMyAdmin-4.8.1-all-languages.zip


分析

在index.php中

1
2
3
4
5
6
7
8
9
10
// If we have a valid target, let's load that script instead
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;
}

可以加载一些脚本。
直接看最后一个检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 . '?', '?')
); // $page 是我们传递进来的要包含的文件
// 这里是截取到$page里?之前的文件名 并存入$_page中,
// 如果$_page, 在白名单之类的话, 直接通过, 所以这里我们不用双重编码也会return true.
// public static $goto_whitelist = array(
// 'db_datadict.php',
// 'db_sql.php',
// 'db_events.php',
// 随便从白名单的列表中选一个文件就行了。
if (in_array($_page, $whitelist)) {
return true;
}
// 如果双重编码, 在下面这个流程里因为解码了, 也会return true
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
return false;
}

return true了, 就直接include了。

直接传 db_sql.php?/../robots.txt 就能成功包含到了。
不需要编码, 因为这时候 include ‘db_sql.php?/../robots.txt’
在include中, ?号后面的字符 并不会当成query,
问号也就只是路径的一部分而已。 所以不需要编码, 利用/../ 也能跳出目录。


Getshell的话, 根据p师傅的思路,执行sql语句后, 直接包含session文件就行。

1
2
3
4
5
6
7
$session_name = 'phpMyAdmin';
@session_name($session_name);
// Restore correct sesion ID (it might have been reset by auto started session
if (isset($_COOKIE['phpMyAdmin'])) {
session_id($_COOKIE['phpMyAdmin']);
}

phpmyadmin的session_name 是phpMyAdmin

执行的sql语句存入session的有两个位置。 一个就是sql_history。
/libraries/classes/Relation.php中

1
2
3
4
5
$_SESSION['sql_history'][] = array(
'db' => $db,
'table' => $table,
'sqlquery' => $sqlquery,
);

这里的\$sqlquery 就是所执行的sql语句, 然后存入到了\$_SESSION当中。


修复

4.8.2中, 修复了这个漏洞。

1
2
3
4
5
6
7
8
9
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'], [], true)
) {
include $_REQUEST['target'];
exit;
}

在调用checkPageValidity方法的时候, 第三个参数设置为了true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static function checkPageValidity(&$page, array $whitelist = [], $include = false)
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
if ($include) {
return false;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

当第三个参数为true的时候, 在mb_substr之前, 就return false了。
所以, 现在真的只能包含白名单内的文件了。