目标: 10.129.231.176
信息: 我知道目标是zabbix服务器。我收到了一个标准用户账户用于登录zabbix:用户matthew 密码96qzn0h2e1k3。这个账户是标准用户,没有任何组或额外权限。
按照惯例,我们从枚举开始,使用nmap进行端口扫描。

nmap输出显示标准ssh端口和apache2也在标准端口。还有端口10051和10050运行着zabbix的某个服务。
我们通过浏览器URL输入IP地址和默认HTTP端口(80端口)来访问zabbix。

这是zabbix的登录界面,我将用收到的用户登录。


在页脚,我发现了zabbix的版本:

用谷歌搜索(万能工具),我查了是否已有这个zabbix版本的CVE。

经过一段时间的搜索,我发现这个版本容易受到CVE-2024-42327的影响,该漏洞涉及SQL注入以获取数据库数据并提升权限,以及CVE-2024-36467,该漏洞允许通过滥用缺失的访问控制将用户角色更改为超级用户。
https://nvd.nist.gov/vuln/detail/CVE-2024-36467
https://nvd.nist.gov/vuln/detail/CVE-2024-42327
在zabbix文档中有如何发送HTTP请求调用API的说明。

https://www.zabbix.com/documentation/current/en/manual/api
我发送了一个请求调用apiinfo.version,文档中教了我们这个。

返回结果如下:
{"jsonrpc":"2.0","result":"7.0.0","id":1}
为了下一个测试,我修改了这个请求中的一些参数再次发送。

在method中,我从apiinfo.version改为user.login,并添加了username和password参数。这也是在zabbix文档中看到的。

返回了一个token:
{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}
经过更多研究,我决定去github上的zabbix仓库。
https://github.com/zabbix/zabbix
我搜索了CUser并找到了一个CUser.php文件。

我们找到了user.update函数:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
我没有找到任何授权检查,所以我决定将我的函数改为超级用户函数,返回请求并调整了payload。

返回了一个错误,提示invalid params。
经过更长时间的代码分析,我们找到了这个函数:
/**
* Additional check to exclude an opportunity to deactivate himself.
*
* @param array $users
* @param array $users[]['usrgrps'] (optional)
*
From this snippet, we understand that we cannot change our roles because our role is checked
from extracting our data from the API token, and verifying against the database if we are that user.
But following the code we see that usrgrps has no validation at all, and therefore can be abused
to add ourselves into multiple groups at once. As long as the group is not disabled and the group
allows GUI access we can abuse this to change our current role with the following command:
User ID 3 is matthew , User group 7 is the Zabbix administrators group and user group 13 is the
Internal group which both hold unrestrictive privileges. The response indicates that the change
was successful:
* @throws APIException
*/
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;
}
}
}
根据这段代码,我们不能更改自己的角色,因为我们的角色是通过从API token中提取数据并与数据库验证是否是当前用户来检查的。但跟踪代码后,我们看到usrgrps完全没有验证,因此可以被滥用来一次性将我们自己添加到多个组中。只要该组未被禁用且允许GUI访问,我们就可以利用这一点来更改当前角色,使用以下命令: 用户ID 3是matthew,用户组7是Zabbix管理员组,用户组13是内部组,这两个组都拥有不受限制的权限。响应表明更改成功:
现在我们可以提取当前用户的用户组。我们修改请求并再次发送。

查看响应,我们看到ID为3的用户已经在内部管理员和Zabbix管理员组中。
{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps":
[{"usrgrpid":"8","name":"Guests"}]},{"userid":"3","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]}],"id":1}
在一个有效的Host Group被分配给Zabbix管理员组的情况下,他们可以利用创建项来触发远程代码执行,这将在下一个CVE中讨论。
再次分析CUser类中的源代码,我们调查了第68行的user.get函数。第108行包含一个检查,代码如下:
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
从这段代码中,如果在API请求中提供了editable选项,则不会验证用户组,而是只检查当前用户ID是否匹配,这在使用user.get时绕过了权限检查。在第234行,调用了addRelatedObjects,该函数存在漏洞,容易受到SQL注入。分析第2969行的addRelatedObject函数,我们可以看到大多数SQL语句看起来是安全的,直到我们到达第3041行。
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !==
API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.',
$options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
在这个块中,如果指定了selectRole选项,则会不安全地调用DBSelect函数,而没有对用户输入进行清理。这导致了基于时间的和布尔盲注SQL注入。
为了测试这一点,我们从以下链接获取了一个payload,并验证了我们在selectRole参数上有一个成功的注入点。

我们成功命中,目标睡眠了5秒。
{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","role":
{"roleid":"1",""r.name and (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real 5.12s
user 0.00s
sys 0.01s
cpu 0%
使用Charles Proxy拦截请求并保存到文件中,请求如下:

现在,使用SQLMap,我们尝试识别潜在的漏洞并提取数据库数据:

一段时间后,我们得到了以下结果:
available databases [2]:
[*] information_schema
[*] zabbix
根据输出,我们通过利用基于时间的SQL注入成功获取了数据库名称。
现在让我们尝试RCE(远程代码执行)。
我们可以利用配置不当的agent来获得远程代码执行。为了通过基于时间的SQL注入实现这一点,我们需要泄露数据库中的会话表,以查看Admin用户是否已通过身份验证。不幸的是,由于是基于时间的攻击,这可能需要一些时间,所以我包含了一个多线程脚本,可以更快地提取管理员会话以供后续使用。
payload如下:

这是一个嵌套的基于时间的SQL注入,我们将payload注入到name参数中,通过追加AND来链式条件。
SELECT * FROM (SELECT(SLEEP(...)))BEEF
我们使用一个外部SELECT条件,将SLEEP条件包装在一个名为BEEF的子查询中。
SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE
userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0,
{TRUE_TIME})))
SLEEP条件在此脚本中获取1秒的TRUE_TIME值,并检索已通过网站或API进行身份验证的活动管理员账户的sessionid。上面的SELECT条件检索索引(ROW)为0的第一个结果,该结果被封装在MID条件中。我们使用MID条件提取sessionid中特定位置的字符,该位置是递增的并封装在ORD条件中。ORD条件将提取的字符转换为ASCII值以进行比较,并封装在IF条件中。IF条件检查提取的字符是否与预期的ASCII字符(ord(char))匹配。如果条件满足并且触发了SLEEP条件,那么我们就识别出了正确的字符,并可以泄露32字符的sessionid。
我写了一个Python脚本并运行了它。


运行脚本后,我们看到我们在仅30秒内成功获取了管理员会话。

使用Admin用户的API token,我们可以继续创建一个项,然后通过任务触发该项。首先,我们需要创建项,但我们需要获取当前主机ID及其接口ID。

我们得到了响应:
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}
现在我们可以用以下payload创建一个项:

在输入payload之前,我们在端口4448上设置了一个nc监听器并等待了几秒。

现在是时候输入payload了。

成功了,任务已创建并包含我们的恶意payload,我们获得了RCE(远程代码执行),现在可以访问服务器了。

现在我们有了服务器的访问权限,接下来进行权限提升,尝试获取服务器的root访问权限。
由于我们是zabbix用户,让我们检查是否可以用sudo权限执行某个守护进程(程序):

我们看到我们可以无限制地执行/usr/bin/nmap。经过一段时间的互联网搜索,我找到了GTFOBins项目。GTFOBins是一个仓库,列出了在Unix/Linux系统中可用于权限提升、绕过受限环境(如chroot或容器)以及执行恶意命令的二进制文件。

https://gtfobins.github.io/gtfobins/nmap/#sudo
我们尝试使用GTFOBins的sudo逃逸。

看起来nmap被一个wrapper脚本保护了,这是一个额外的保护层,用于限制nmap中可能被利用的选项。让我们尝试读取/usr/bin/nmap文件。我们使用nano文本编辑器打开并分析这个文件。

经过大量研究,我看到所有GTFOBins逃逸在此场景下都无用。他们实现了一个wrapper来保护nmap免受常见的权限提升方法。我没有其他选择了,于是去阅读nmap的库文件。
经过一段时间的阅读,我发现了一个有趣的选项--datadir。
https://nmap.org/book/data-files-replacing-data-files.html
--datadir <dirname>: Specify custom Nmap data file location
这个选项允许你指定一个数据目录,其中存储了默认脚本和其他nmap必要项,此场景下的默认目录是/usr/share/nmap。让我们看看该文件的权限:

搜索这些文件后,我发现nse_main.lua文件是默认脚本文件,可以通过-sC参数触发,它是Nmap脚本引擎(NSE)的主要脚本文件。它包含当nmap使用-sC选项(使用默认脚本扫描)时执行的函数。通过创建一个同名的恶意脚本,可以让nmap自动执行它。为了利用这一点,我们在/tmp/nse_main.lua中创建一个新文件,内容为os.execute("chmod 4755 /bin/bash")。
我创建了nse_main.lua文件,其中包含命令os.execute("chmod 4755 /bin/bash")。
4755:为/bin/bash二进制文件设置SUID(设置用户ID)。这样任何用户执行/bin/bash都会拥有文件所有者(root)的权限。


当我们使用-sC选项扫描localhost时,我们将/bin/bash设置为SUID,并生成一个有效用户ID为root的shell。

--datadir=/tmp:让nmap在/tmp目录中查找其配置文件和脚本。这包括我们刚刚创建的恶意脚本nse_main.lua。
-sC:激活默认脚本的执行,包括我们刚刚创建的恶意脚本。
localhost:让nmap扫描本机。
nse_main.lua脚本以root权限由nmap执行(因为命令是用sudo执行的)。

SUID激活后,我们可以执行:/bin/bash -p

-p:保留SUID位,并以文件所有者(root)的权限执行bash。

uid=114:zabbix用户的身份。 euid=0:实际以root身份运行。
现在我们已经获得了root权限。