Herramientas web contra una aplicación HTTP controlada
Este laboratorio comprueba el comportamiento de seis clientes contra el mismo sistema. La aplicación sirve recursos válidos, respuestas negativas con estado 200, una redirección, un recurso autenticado, una respuesta lenta y un 429. Nginx añade selección por Host, TLS y Server Name Indication (SNI).
Las afirmaciones comprobadas son:
- una respuesta de referencia permite separar recursos de un soft 404 estático
- un soft 404 dinámico exige filtrar contenido, no un único tamaño
- Gobuster 3.8.2 requiere
--exclude-lengthal activar--exclude-hostname-length - ffuf 2.2.1 evalúa
-frsobre cabeceras y cuerpo concatenados - los límites de concurrencia y frecuencia no sustituyen el timeout por petición
- HTTPX conserva
200y429como observaciones distintas - Katana respeta un scope regular y descubre enlaces internos sin seguir el nombre externo
- SNI,
Hosty cookie cambian la respuesta sin cambiar la dirección de destino
Topología y estado inicial
Sección titulada «Topología y estado inicial»El fixture vive en labs/web-tooling/. Docker Compose crea una aplicación Node.js 22.17.0 y un frontal Nginx 1.29.5. Las imágenes están fijadas por tag y digest. Los únicos puertos publicados son 127.0.0.1:18080 y 127.0.0.1:18443.
La aplicación se ejecuta como el usuario node. Ambos servicios usan un sistema de archivos raíz de solo lectura, no-new-privileges y retirada de capacidades Linux (capabilities). Nginx recupera únicamente CHOWN, SETUID y SETGID para preparar sus directorios temporales y reducir el usuario de sus procesos. La primera variante retiraba todas las capacidades del frontal. Nginx terminó antes de servir porque no podía cambiar el propietario de client_temp. Devolver el conjunto completo habría ocultado qué permiso necesitaba realmente.
start.sh genera un certificado desechable para app.example.test, arranca los contenedores y comprueba /exists. La clave queda bajo .state/, excluida de Git:
./labs/web-tooling/start.shmkdir -p labs/web-tooling/evidencedocker compose \ --project-name offsec-kb-web-tooling \ --file labs/web-tooling/compose.yml \ psEn mkdir, -p crea el directorio cuando falta y no falla si ya existe. En docker compose, --project-name fija el namespace de los recursos y --file selecciona el manifiesto. El estado esperado muestra app y edge en ejecución. Si el script no alcanza HTTPS en veinte intentos, devuelve un error en lugar de dejar el fallo como un timeout posterior de las herramientas.
Las releases de Gobuster, ffuf, HTTPX y Katana se descargaron desde sus repositorios y se verificaron contra sus manifiestos oficiales de checksums. El digest publicado por GitHub se utilizó para Feroxbuster. El laboratorio registró estas versiones:
curl 8.7.1gobuster 3.8.2ffuf 2.2.1feroxbuster 2.13.1httpx 1.10.0katana 1.6.1nginx 1.29.5node 22.17.0La arquitectura del sistema que ejecutó los clientes no modifica las afirmaciones y, por tanto, no forma parte del identificador publicado.
Comprobar SNI, Host, sesión y timeout con cURL
Sección titulada «Comprobar SNI, Host, sesión y timeout con cURL»--resolve asocia FQDN, puerto y dirección solo para esa ejecución. También hace que cURL envíe el SNI y la autoridad HTTP correctos. El certificado es local y autofirmado, por lo que --insecure se utiliza únicamente dentro de este laboratorio:
curl --silent --show-error --insecure \ --resolve app.example.test:18443:127.0.0.1 \ https://app.example.test:18443/existsLa respuesta fue known-resource. La petición directa a https://127.0.0.1:18443/exists llegó al servidor TLS predeterminado y devolvió default-tls-host.
--cookie envía una cookie concreta. Sin ella, /private responde 401. Con la sesión esperada devuelve 200 y el recurso:
curl --silent --show-error --include \ --resolve app.example.test:18080:127.0.0.1 \ http://app.example.test:18080/private
curl --silent --show-error --include \ --cookie 'session=valid' \ --resolve app.example.test:18080:127.0.0.1 \ http://app.example.test:18080/private--max-time limita toda la operación. Con 0.5 segundos, /slow terminó con el código de cURL 28. Con dos segundos devolvió slow-resource. El timeout corto demuestra el límite del cliente, no una caída del servicio:
curl --silent --show-error --max-time 0.5 \ --resolve app.example.test:18080:127.0.0.1 \ http://app.example.test:18080/slow
curl --silent --show-error --max-time 2 \ --resolve app.example.test:18080:127.0.0.1 \ http://app.example.test:18080/slowSoft 404 estático y virtual hosts con Gobuster
Sección titulada «Soft 404 estático y virtual hosts con Gobuster»La rama /static/ devuelve un cuerpo de 15 bytes para cualquier nombre ausente y 22 bytes para /static/exists. --exclude-length 15 elimina la referencia negativa. --threads, --delay y --timeout controlan, respectivamente, concurrencia, espera por hilo y tiempo máximo de cada petición:
gobuster dir \ --url http://127.0.0.1:18080/static/ \ --wordlist labs/web-tooling/content-wordlist.txt \ --headers 'Host: app.example.test' \ --threads 4 \ --delay 50ms \ --timeout 2s \ --exclude-length 15 \ --output labs/web-tooling/evidence/gobuster-static.txtEl único candidato fue exists (Status: 200) [Size: 22]. En la raíz, el cuerpo negativo incorpora la ruta y cambia de longitud. Gobuster detectó el wildcard durante el precheck y detuvo la ejecución. Forzarlo habría ocultado la razón del fallo.
Para vhosts, --domain aporta el sufijo y --append-domain construye palabra.example.test. La respuesta predeterminada tiene 18 bytes. --exclude-hostname-length compensa que el nombre probado cambie de longitud, pero Gobuster 3.8.2 exige que se declare también --exclude-length:
gobuster vhost \ --url http://127.0.0.1:18080/ \ --domain example.test \ --append-domain \ --wordlist labs/web-tooling/vhost-wordlist.txt \ --threads 2 \ --delay 100ms \ --timeout 2s \ --exclude-length 18 \ --exclude-hostname-length \ --output labs/web-tooling/evidence/gobuster-vhost.txtEl resultado fue app.example.test, con estado 200 y 10 bytes. Omitir --exclude-length produjo un error de argumentos antes de enviar la wordlist.
Soft 404 dinámico, estados y latencia con ffuf
Sección titulada «Soft 404 dinámico, estados y latencia con ffuf»-fr filtra una expresión regular. En ffuf 2.2.1, el motor antepone las cabeceras al cuerpo antes de evaluarla. Por eso ^missing: no filtró los soft 404, mientras que missing: sí lo hizo. -mc all conserva todos los estados antes del filtro. -p 0.05, -rate 10, -t 4, -timeout 2 y -maxtime 30 fijan pausa, frecuencia global, tareas concurrentes, timeout por petición y duración total:
ffuf -w labs/web-tooling/content-wordlist.txt \ -u http://127.0.0.1:18080/FUZZ \ -H 'Host: app.example.test' \ -mc all \ -fr 'missing:' \ -t 4 \ -rate 10 \ -p 0.05 \ -timeout 2 \ -maxtime 30 \ -sa \ -json > labs/web-tooling/evidence/ffuf.jsonlLos resultados conservados fueron 200 /exists, 403 /admin, 302 /redirect, 401 /private, 429 /limited y 200 /slow. Las dos rutas ausentes quedaron filtradas. /slow tardó aproximadamente 1,5 segundos. Reducir -timeout a un segundo la convierte en error sin cambiar el endpoint.
Comparar la recursión de Feroxbuster
Sección titulada «Comparar la recursión de Feroxbuster»Feroxbuster recibió el mismo soft 404 estático. --threads 4 limita peticiones por scan, --scan-limit 1 evita procesar varios directorios en paralelo, --rate-limit 10 fija la frecuencia global y --filter-size 15 elimina el cuerpo negativo:
feroxbuster \ --url http://127.0.0.1:18080/static/ \ --wordlist labs/web-tooling/content-wordlist.txt \ --headers 'Host: app.example.test' \ --depth 1 \ --threads 4 \ --scan-limit 1 \ --rate-limit 10 \ --timeout 2 \ --filter-size 15 \ --json \ --output labs/web-tooling/evidence/feroxbuster.jsonFeroxbuster registró su configuración, la raíz y /static/exists. También ejecutó peticiones de autofiltrado. El contador total superó las entradas de la wordlist. Por ello, el tamaño de la lista no equivale al número final de solicitudes.
Conservar estados con HTTPX
Sección titulada «Conservar estados con HTTPX»La entrada contiene /exists y /limited. Créala de forma explícita para que ninguna URL proceda del historial de otra herramienta:
printf '%s\n' \ 'http://127.0.0.1:18080/exists' \ 'http://127.0.0.1:18080/limited' \ > labs/web-tooling/evidence/http-input.txtEl formato %s\n imprime cada argumento como una línea. Después, -threads 2, -rate-limit 5, -timeout 2 y -retries 0 fijan concurrencia, frecuencia, espera y reintentos. -sc guarda el estado, -include-chain conserva redirecciones y -json produce JSONL:
httpx -l labs/web-tooling/evidence/http-input.txt \ -H 'Host: app.example.test' \ -threads 2 \ -rate-limit 5 \ -timeout 2 \ -retries 0 \ -sc \ -include-chain \ -json \ -o labs/web-tooling/evidence/httpx.jsonlHTTPX conservó un 200 de 15 bytes y un 429 de 13 bytes. En la primera ejecución de la versión 1.10.0, el output JSON inicializó un clasificador de páginas y descargó su modelo a la caché del usuario. La caché se retiró después de la prueba. Este acceso externo debe prepararse o bloquearse explícitamente antes de usar JSON en una estación aislada.
Limitar el crawling con Katana
Sección titulada «Limitar el crawling con Katana»-crawl-scope recibe una expresión regular aplicada a las URL que el crawler puede seguir. -concurrency 2 limita fetchers para la entrada y -parallelism 1 procesa una sola semilla. Los límites global y por host son cinco peticiones por segundo:
katana -u http://127.0.0.1:18080/crawl \ -H 'Host: app.example.test' \ -crawl-scope '^http://127\.0\.0\.1:18080/' \ -d 2 \ -concurrency 2 \ -parallelism 1 \ -rate-limit 5 \ -host-rate-limit 5 \ -timeout 2 \ -retry 0 \ -jsonl \ -o labs/web-tooling/evidence/katana.jsonlEl JSONL registró /crawl, /exists y /redirect. El enlace outside.example.invalid se descubrió en el HTML, pero no se solicitó. Una variante con dos barras invertidas antes del punto no coincidió con la URL y produjo un archivo vacío. El fallo pertenecía a la expresión regular, no al crawler.
Cleanup y control final
Sección titulada «Cleanup y control final»stop.sh ejecuta docker compose down sobre el nombre y el manifiesto exactos, elimina volúmenes, retira contenedores huérfanos y borra el certificado generado:
./labs/web-tooling/stop.shcurl --silent --show-error --max-time 1 \ http://127.0.0.1:18080/existsEl control final debe terminar con conexión rechazada. El directorio labs/web-tooling/evidence/ está excluido de Git. Conserva solo resultados saneados o sus hashes cuando la práctica lo requiera.
Límites
Sección titulada «Límites»La prueba se ejecutó sobre loopback y no cubre pérdida de red, HTTP/2, proxy autenticado, WAF, CDN, crawling headless, JavaScript dinámico, formularios ni reanudación tras caída. Los perfiles validados no deben copiarse como rates universales. Demuestran la relación entre cada control y la señal observada.
Relación canónica
Sección titulada «Relación canónica»- Interacción manual con HTTP y TLS
- Hosts virtuales
- Descubrimiento de contenido
- Automatización del reconocimiento web
- Checklist de reconocimiento web