Explorando Vulnerabilidades de Subida de Archivos sin Romper la Ley
Como saltar validaciones de upload en tu propio lab, mapear clases de fallos y endurecer webservers contra RCE via archivo malicioso.

En este artículo
Un endpoint de subida mal configurado sigue siendo una de las formas mas baratas de convertir un formulario humilde en ejecucion remota de codigo. En un lab armado con DVWA, OWASP Juice Shop y un reverse proxy Nginx 1.25, pasamos de 'subir avatar' a una shell www-data en menos de 40 minutos. Antes de cualquier payload, el contrato importa: el objetivo es nuestra propia VM, aislada en una red 10.10.0.0/24 sin ruta a internet, y la meta es escribir reglas endurecidas, no coleccionar cabelleras. Este recorrido cubre toda la superficie de ataque de la subida de archivos, cada clase de bug con vector, oraculo y el fix que la cierra de verdad.
El contrato: solo lab autorizado#
Cada tecnica aqui se demuestra contra infraestructura que poseemos y estamos explicitamente autorizados a romper. Replicarlas contra un SaaS de produccion 'solo para chequear' no es bug bounty, es un delito bajo la mayoria de las leyes de abuso informatico, y la diferencia entre investigador y acusado suele ser un scope escrito y un snapshot de VM. Arma el lab como lo describe pentest web desde cero: Docker en un bridge aislado, sin ruta al host, Burp Suite como proxy interceptor. Toma snapshot antes de cada corrida para probar exactamente que tocaste y revertir limpio.
Bypass del lado cliente y de blacklist de extension#
La primera clase de bug es la validacion de extension del lado cliente. El PHP legacy suele traer una blacklist (.php, .phtml, .php5) sin verificacion real de Content-Type. Renombrar shell.php a shell.pHp.jpg, interceptar con Burp y forzar el Content-Type a image/jpeg vence cerca del 80% de los filtros amateur que se ven en CTFs corporativos. Mezclar mayusculas vence blacklists ingenuas en minusculas; puntos y espacios finales (shell.php.) vencen las orientadas a Windows; y handlers alternos como .phtml, .phar o .pht a menudo ejecutan donde .php esta bloqueado. El oraculo es simple: pide la ruta subida y ve si el servidor devuelve tu cadena marcador o el codigo fuente crudo. Si ejecuta, la blacklist era el unico control y fallo.
Confusion de Content-Type y MIME#
La segunda capa es confiar en el tipo MIME declarado por el cliente. Un backend que revisa solo el header Content-Type del multipart se evade trivialmente, porque ese header lo controla el atacante; ponlo en image/png y un payload PHP pasa. Backends algo mejores llaman getimagesize() o husmean los primeros bytes, lo que sube la vara pero no la libra. El oraculo correcto en pruebas es separar tipo declarado de tipo real: envia una extension de imagen genuina con contenido ejecutable, luego un header de imagen falso frente a codigo, y observa que combinacion el servidor almacena y sirve como ejecutable. Cualquier ruta donde el tipo declarado por si solo decide la aceptacion es un hallazgo que vale reportar.
Parseo de rutas: dobles extensiones y null bytes#
Una superficie clasica es el parseo de rutas del servidor. El viejo doble-parseo de Nginx (CVE-2013-4547 y descendientes) dejaba que un archivo llamado shell.jpg seguido de un null byte y .php enganara stacks que combinan Nginx y PHP-FPM con un pathinfo descuidado. En el lab lo reconstrui con nginx:1.14-alpine y php:7.2-fpm solo como referencia historica; con Nginx 1.25 y cgi.fix_pathinfo=0 el mismo payload muere. La leccion: la seguridad de subida no la decide la aplicacion sola; el servidor web, el handler FastCGI y el ajuste fix_pathinfo deciden juntos si un directorio de imagenes puede ejecutar codigo. Este ejercicio combina con la mentalidad de parseo de entrada de SQL injection en la practica.
Magic bytes, polyglots y el pipeline de imagen#
Cuando el backend valida magic bytes, cambia el juego. Los polyglots GIF/PHP vencen getimagesize() pero fallan ante finfo mas Imagick con reprocesamiento, asi que el truco es atacar el pipeline de transformacion en vez del validador. ImageMagick con policies laxas aun parsea MVG y SVG, y la familia ImageTragick resurge en forks de LMS y CMS. En un cliente fintech encontre una subida de recibos que llamaba convert sin limites de memoria ni de delegates, lo que convirtio un SVG en un SSRF y luego en una lectura de archivo. Si el pipeline trae recursos remotos, la subida se vuelve una primitiva de request forgery, por lo que esta clase se liga directo a SSRF y cloud metadata. Reprocesa cada imagen, desactiva coders peligrosos y nunca dejes que convert siga una URL.
Donde aterriza el archivo: almacenamiento, traversal, servido#
Una superficie subestimada es donde aterriza el archivo. Almacenar en /var/www/uploads servido directo por Apache es el clasico, pero el mismo problema se esconde en buckets S3 con Content-Disposition equivocada y en CDNs que reprocesan HTML. El path traversal en el nombre de archivo (secuencias punto-punto-barra apuntando a /tmp/cron.d/ o a un webroot) aun funciona contra middleware Node que confia en el originalname de Multer. El oraculo: sube un marcador benigno con nombre de traversal y revisa donde se materializa en disco; si escapa del directorio previsto, el atacante elige el lugar de escritura. Para un plan de prueba mas formal por endpoint, sobre todo en APIs, sigue la checklist de pentest REST y GraphQL.
Fuzzing metodico del endpoint#
Los bypass manuales encuentran el primer bug; el fuzzing metodico encuentra el resto. Apunta Burp Intruder o un pequeno script al campo de subida e itera tres dimensiones a la vez: la extension (una wordlist de .php, .phtml, .phar, .pht, .php7, .inc, mezcla de mayusculas, punto y espacio finales), el Content-Type declarado y el prefijo de magic bytes. Para cada combinacion, el oraculo es una sonda de dos pasos: acepta el servidor el archivo, y ejecuta el marcador al pedirlo de vuelta. No olvides la race condition de subida, donde una webshell es brevemente alcanzable entre la escritura y el paso de antivirus o de move; un bucle apretado de subir-y-pedir puede ganar esa ventana en un objetivo real y debe probarse explicitamente para justificar el fix (validar antes de que el archivo sea alcanzable).
Encadenar una subida hasta el compromiso total#
Un solo marcador ejecutado es un hallazgo, pero el reporte debe mostrar impacto. En el lab, el polyglot GIF/PHP dio una webshell minima de comandos; desde ahi una reverse shell estandar (un callback bash TCP a la VM atacante en la red aislada) elevo el punto de apoyo a una sesion interactiva www-data en segundos. La escalada realista desde ahi no es un exploit de Hollywood sino persistencia aburrida: un directorio escribible servido por el servidor web, una ruta de cron world-writable alcanzable por el traversal anterior, o una credencial de base de datos filtrada en un archivo de config que la shell ahora puede leer. Documenta la cadena hasta el impacto probado y detente; el objetivo es justificar la calificacion de severidad, no saquear una maquina, y el snapshot te deja re-ejecutar toda la cadena para el cliente sin dejar dano.
Reglas defensivas que de verdad funcionan#
Del lado defensivo, las reglas que se ganan su lugar en un reporte son aburridas y efectivas. Genera un nombre de archivo aleatorio (UUIDv7) y descarta del todo el nombre del cliente; almacena fuera del document root; sirve por un handler que fuerce Content-Disposition: attachment y Content-Type: application/octet-stream; valida el MIME con libmagic del lado servidor, no el header declarado; reprocesa imagenes y quita metadatos; y bloquea dobles extensiones en el propio servidor web, por ejemplo negando toda peticion cuya ruta contenga .php, .phtml o .phar antes de otro punto. Suma un pase de antivirus y un tope de tamano, y pon todo tras los gates shift-left de AppSec shift-left para que una regresion caiga en CI, no en produccion.
Checklist#
Antes de aprobar una funcion de subida: nombre de archivo generado por el servidor y que nunca refleje entrada del usuario; extension en allowlist, no blacklist; MIME verificado por contenido, no por header; almacenamiento fuera del webroot y servido con disposicion attachment; imagenes reprocesadas y metadatos quitados; el servidor web no ejecuta nada en la ruta de subida; path traversal en el nombre neutralizado; existen limites de tamano y de tasa; y un antivirus o escaner de contenido corre en asincrono. Cada 'no' de esa lista es un hallazgo, y cada hallazgo recibe una linea concreta de remediacion, no un vago 'sanitiza la entrada'.
FAQ#
Alcanza con bloquear .php para estar seguro? No; una blacklist es el control mas debil posible, vencido por mezcla de mayusculas, handlers alternos (.phtml, .phar, .pht), puntos finales y rarezas del parseo de rutas del servidor. El control duradero es una allowlist de extensiones combinada con validacion MIME por contenido y un servidor web que jamas ejecuta codigo en el directorio de subida. Puedo permitir subidas SVG con seguridad? Solo con mucha cautela: SVG es XML y puede portar script y referencias externas, asi que sirvelo con una Content-Security-Policy restrictiva y disposicion attachment, o rasterizalo a PNG al recibirlo y descarta el original. Tratar SVG como imagen inofensiva es como ImageTragick y el XSS almacenado siguen aterrizando.
Deberian vivir las subidas en el mismo host que sirve la app? Prefiere un dominio de almacenamiento separado sin ejecucion de codigo y un handler de servido cerrado, para que hasta una escritura exitosa no se vuelva RCE en el host de la app. Y como pruebo sin arriesgar dano? Toma snapshot de la VM, trabaja solo dentro del scope autorizado, usa payloads marcadores benignos que prueben el bug (una cadena unica, el propio id de la VM) en vez de exfiltrar datos reales, y revierte tras cada corrida; probar la escritura basta, leer /etc/passwd en la maquina de otro es innecesario e ilegal.
Conclusion#
La subida de archivos sigue siendo peligrosa porque se ubica en la costura de tres sistemas, la aplicacion, el servidor web y la capa de almacenamiento, y una brecha en cualquiera basta. Atacala como este lab, clase por clase, con un oraculo claro para cada una, y las reglas defensivas se escriben solas: nombres generados por el servidor, extensiones en allowlist, chequeos MIME por contenido, imagenes reprocesadas, sin ejecucion en la ruta de subida y traversal neutralizado. Manten cada experimento dentro del scope autorizado con un snapshot que lo pruebe, envia las reglas endurecidas por CI, y el humilde formulario de avatar deja de ser un camino de 40 minutos a una shell.


