### 漏洞分析
此漏洞出现在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注入的发生。
                       
                       
        
          
暂无评论