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