### 漏洞分析 此漏洞出现在jsrpc.php中,180行 ``` case 'screen.get': $options = [ 'pageFile' => !empty($data['pageFile']) ? $data['pageFile'] : null, 'mode' => !empty($data['mode']) ? $data['mode'] : null, 'timestamp' => !empty($data['timestamp']) ? $data['timestamp'] : time(), 'resourcetype' => !empty($data['resourcetype']) ? $data['resourcetype'] : null, 'screenid' => (isset($data['screenid']) && $data['screenid'] != 0) ? $data['screenid'] : null, 'screenitemid' => !empty($data['screenitemid']) ? $data['screenitemid'] : null, 'groupid' => !empty($data['groupid']) ? $data['groupid'] : null, 'hostid' => !empty($data['hostid']) ? $data['hostid'] : null, 'period' => !empty($data['period']) ? $data['period'] : null, 'stime' => !empty($data['stime']) ? $data['stime'] : null, 'profileIdx' => !empty($data['profileIdx']) ? $data['profileIdx'] : null, 'profileIdx2' => !empty($data['profileIdx2']) ? $data['profileIdx2'] : null, 'updateProfile' => isset($data['updateProfile']) ? $data['updateProfile'] :...
### 漏洞分析 此漏洞出现在jsrpc.php中,180行 ``` case 'screen.get': $options = [ 'pageFile' => !empty($data['pageFile']) ? $data['pageFile'] : null, 'mode' => !empty($data['mode']) ? $data['mode'] : null, 'timestamp' => !empty($data['timestamp']) ? $data['timestamp'] : time(), 'resourcetype' => !empty($data['resourcetype']) ? $data['resourcetype'] : null, 'screenid' => (isset($data['screenid']) && $data['screenid'] != 0) ? $data['screenid'] : null, 'screenitemid' => !empty($data['screenitemid']) ? $data['screenitemid'] : null, 'groupid' => !empty($data['groupid']) ? $data['groupid'] : null, 'hostid' => !empty($data['hostid']) ? $data['hostid'] : null, 'period' => !empty($data['period']) ? $data['period'] : null, 'stime' => !empty($data['stime']) ? $data['stime'] : null, 'profileIdx' => !empty($data['profileIdx']) ? $data['profileIdx'] : null, 'profileIdx2' => !empty($data['profileIdx2']) ? $data['profileIdx2'] : null, 'updateProfile' => isset($data['updateProfile']) ? $data['updateProfile'] : null ]; if ($options['resourcetype'] == SCREEN_RESOURCE_HISTORY) { $options['itemids'] = !empty($data['itemids']) ? $data['itemids'] : null; $options['action'] = !empty($data['action']) ? $data['action'] : null; $options['filter'] = !empty($data['filter']) ? $data['filter'] : null; $options['filter_task'] = !empty($data['filter_task']) ? $data['filter_task'] : null; $options['mark_color'] = !empty($data['mark_color']) ? $data['mark_color'] : null; } elseif ($options['resourcetype'] == SCREEN_RESOURCE_CHART) { $options['graphid'] = !empty($data['graphid']) ? $data['graphid'] : null; $options['profileIdx2'] = $options['graphid']; } $screenBase = CScreenBuilder::getScreen($options); if (!empty($screenBase)) { $screen = $screenBase->get(); } if (!empty($screen)) { if ($options['mode'] == SCREEN_MODE_JS) { $result = $screen; } else { if (is_object($screen)) { $result = $screen->toString(); } } } else { $result = ''; } break; ``` 当`method`赋值为`screen.get`,调用`CScreenBuilder::getScreen($data)`,跟进到`CScreenBuilder.php`中171行: ``` public static function getScreen(array $options = []) { ...... if ($options['resourcetype'] === null) { return null; } switch ($options['resourcetype']) { case SCREEN_RESOURCE_GRAPH: return new CScreenGraph($options); ...... case SCREEN_RESOURCE_DISCOVERY: return new CScreenDiscovery($options); default: return null; } } ``` 在初始结构体的时候,最后位置会调用CScreenBase类中的calculateTime的方法,其中涉及到了profileIdx2变量,继续跟入CScreenBase,第332行 ``` public static function calculateTime(array $options = []) { if (!array_key_exists('updateProfile', $options)) { $options['updateProfile'] = true; } if (empty($options['profileIdx2'])) { $options['profileIdx2'] = 0; } // show only latest data without update is set only period if (!empty($options['period']) && empty($options['stime'])) { $options['updateProfile'] = false; $options['profileIdx'] = ''; } // period if (empty($options['period'])) { $options['period'] = !empty($options['profileIdx']) ? CProfile::get($options['profileIdx'].'.period', ZBX_PERIOD_DEFAULT, $options['profileIdx2']) : ZBX_PERIOD_DEFAULT; } else { if ($options['period'] < ZBX_MIN_PERIOD) { show_error_message(_n('Minimum time period to display is %1$s minute.', 'Minimum time period to display is %1$s minutes.', (int) ZBX_MIN_PERIOD / SEC_PER_MIN )); $options['period'] = ZBX_MIN_PERIOD; } elseif ($options['period'] > ZBX_MAX_PERIOD) { show_error_message(_n('Maximum time period to display is %1$s day.', 'Maximum time period to display is %1$s days.', (int) ZBX_MAX_PERIOD / SEC_PER_DAY )); $options['period'] = ZBX_MAX_PERIOD; } } if ($options['updateProfile'] && !empty($options['profileIdx'])) { CProfile::update($options['profileIdx'].'.period', $options['period'], PROFILE_TYPE_INT, $options['profileIdx2']); } // stime $time = time(); $usertime = null; $stimeNow = null; $isNow = 0; if (!empty($options['stime'])) { $stimeUnix = zbxDateToTime($options['stime']); if ($stimeUnix > $time || zbxAddSecondsToUnixtime($options['period'], $stimeUnix) > $time) { $stimeNow = $options['stime']; $options['stime'] = date(TIMESTAMP_FORMAT, $time - $options['period']); $usertime = date(TIMESTAMP_FORMAT, $time); $isNow = 1; } else { $usertime = date(TIMESTAMP_FORMAT, zbxAddSecondsToUnixtime($options['period'], $stimeUnix)); $isNow = 0; } if ($options['updateProfile'] && !empty($options['profileIdx'])) { CProfile::update($options['profileIdx'].'.stime', $options['stime'], PROFILE_TYPE_STR, $options['profileIdx2']); CProfile::update($options['profileIdx'].'.isnow', $isNow, PROFILE_TYPE_INT, $options['profileIdx2']); } } else { if (!empty($options['profileIdx'])) { $isNow = CProfile::get($options['profileIdx'].'.isnow', null, $options['profileIdx2']); if ($isNow) { $options['stime'] = date(TIMESTAMP_FORMAT, $time - $options['period']); $usertime = date(TIMESTAMP_FORMAT, $time); $stimeNow = date(TIMESTAMP_FORMAT, zbxAddSecondsToUnixtime(SEC_PER_YEAR, $options['stime'])); if ($options['updateProfile']) { CProfile::update($options['profileIdx'].'.stime', $options['stime'], PROFILE_TYPE_STR, $options['profileIdx2']); } } else { $options['stime'] = CProfile::get($options['profileIdx'].'.stime', null, $options['profileIdx2']); $usertime = date(TIMESTAMP_FORMAT, zbxAddSecondsToUnixtime($options['period'], $options['stime'])); } } if (empty($options['stime'])) { $options['stime'] = date(TIMESTAMP_FORMAT, $time - $options['period']); $usertime = date(TIMESTAMP_FORMAT, $time); $stimeNow = date(TIMESTAMP_FORMAT, zbxAddSecondsToUnixtime(SEC_PER_YEAR, $options['stime'])); $isNow = 1; if ($options['updateProfile'] && !empty($options['profileIdx'])) { CProfile::update($options['profileIdx'].'.stime', $options['stime'], PROFILE_TYPE_STR, $options['profileIdx2']); CProfile::update($options['profileIdx'].'.isnow', $isNow, PROFILE_TYPE_INT, $options['profileIdx2']); } } } return [ 'period' => $options['period'], 'stime' => $options['stime'], 'stimeNow' => !empty($stimeNow) ? $stimeNow : $options['stime'], 'starttime' => date(TIMESTAMP_FORMAT, $time - ZBX_MAX_PERIOD), 'usertime' => $usertime, 'isNow' => $isNow ]; } } ``` 这里会调用CProfile的update进行更新,继续跟入CProfile ``` public static function update($idx, $value, $type, $idx2 = 0) { if (is_null(self::$profiles)) { self::init(); } if (!self::checkValueType($value, $type)) { return; } $profile = [ 'idx' => $idx, 'value' => $value, 'type' => $type, 'idx2' => $idx2 ]; $current = self::get($idx, null, $idx2); if (is_null($current)) { if (!isset(self::$insert[$idx])) { self::$insert[$idx] = []; } self::$insert[$idx][$idx2] = $profile; } else { if ($current != $value) { if (!isset(self::$update[$idx])) { self::$update[$idx] = []; } self::$update[$idx][$idx2] = $profile; } } if (!isset(self::$profiles[$idx])) { self::$profiles[$idx] = []; } self::$profiles[$idx][$idx2] = $value; } ``` 可以看到profileIdx2会作为$idx2变量进行更新,接下来回到最外层jsrpt.php,在结尾会引用page_footer.php,跟入这个php,38行 ``` // last page if (!defined('ZBX_PAGE_NO_MENU') && $page['file'] != 'profile.php') { CProfile::update('web.paging.lastpage', $page['file'], PROFILE_TYPE_STR); } if (CProfile::isModified()) { DBstart(); $result = CProfile::flush(); DBend($result); } ``` 这里涉及到一个getScreen操作,直接跟入这个函数,这个函数内设置resourcetype后返回一个继承自CScreenBase的实例,父类的构造方法将被执行.CScreenBase.php中,161行 ``` public function __construct(array $options = []) { ...... // Get resourcetype. if ($this->resourcetype === null && array_key_exists('resourcetype',$this->screenitem)) { $this->resourcetype = $this->screenitem['resourcetype']; } foreach ($this->parameters as $pname => $default_value) { if ($this->required_parameters[$pname]) { $this->$pname = array_key_exists($pname, $options) ? $options[$pname] : $default_value; } } // Get page file. if ($this->required_parameters['pageFile'] && $this->pageFile === null) { global $page; $this->pageFile = $page['file']; } // Calculate timeline. if ($this->required_parameters['timeline'] && $this->timeline === null) { //关键函数调用calculateTime() $this->timeline = $this->calculateTime([ 'profileIdx' => $this->profileIdx, //关键参数 'profileIdx2' => $this->profileIdx2, 'updateProfile' => $this->updateProfile, 'period' => array_key_exists('period', $options) ? $options['period'] : null, 'stime' => array_key_exists('stime', $options) ? $options['stime'] : null ]); } } ``` flush函数中调用了insertDB,并且会将idx2也就是注入参数传入,这个过程没有进行控制。 ``` private static function insertDB($idx, $value, $type, $idx2) { $value_type = self::getFieldByType($type); $values = [ 'profileid' => get_dbid('profiles', 'profileid'), 'userid' => self::$userDetails['userid'], 'idx' => zbx_dbstr($idx), $value_type => zbx_dbstr($value), 'type' => $type, 'idx2' => zbx_dbstr($idx2) ]; return DBexecute('INSERT INTO profiles ('.implode(', ', array_keys($values)).') VALUES ('.implode(', ', $values).')'); } ``` 最后在insertDB中,直接执行了SQL语句,造成了漏洞的发生 ### 补丁对比 在3.0.4版本中,修复了这个漏洞,其中最外层screen.get做了控制。 ``` case 'screen.get': $result = ''; $screenBase = CScreenBuilder::getScreen($data); if ($screenBase !== null) { $screen = $screenBase->get(); if ($data['mode'] == SCREEN_MODE_JS) { $result = $screen; } else { if (is_object($screen)) { $result = $screen->toString(); } } } break; ``` 其次还是进入insertDB函数 ``` private static function insertDB($idx, $value, $type, $idx2) { $value_type = self::getFieldByType($type); $values = [ 'profileid' => get_dbid('profiles', 'profileid'), 'userid' => self::$userDetails['userid'], 'idx' => zbx_dbstr($idx), $value_type => zbx_dbstr($value), 'type' => $type, 'idx2' => zbx_dbstr($idx2) ]; return DBexecute('INSERT INTO profiles ('.implode(', ', array_keys($values)).') VALUES ('.implode(', ', $values).')'); } ``` 这里对参数做了一个控制,调用了zbx_dbstr函数,跟一下这个函数,在db.inc.php中。 ``` function zbx_dbstr($var) { global $DB; if (!isset($DB['TYPE'])) { return false; } switch ($DB['TYPE']) { case ZBX_DB_DB2: if (is_array($var)) { foreach ($var as $vnum => $value) { $var[$vnum] = "'".db2_escape_string($value)."'"; } return $var; } return "'".db2_escape_string($var)."'"; case ZBX_DB_MYSQL: if (is_array($var)) { foreach ($var as $vnum => $value) { $var[$vnum] = "'".mysqli_real_escape_string($DB['DB'], $value)."'"; } return $var; } return "'".mysqli_real_escape_string($DB['DB'], $var)."'"; ``` 可见,对每一种数据库情况都做了过滤,这里是Mysql数据库,做了转义,防止sql注入的发生。