software 2025 · varios meses de ajuste fino · 0 €

Firewall HTTP para IIS en VB.NET

Un WordPress en un servidor Windows con IIS expuesto a internet tarda pocas horas en recibir las primeras peticiones a xmlrpc.php, los primeros intentos de login en serie y los primeros escáneres de vulnerabilidades. La mayoría de soluciones que encuentras están pensadas para Linux. Este tutorial explica cómo construir un módulo HTTP para IIS en VB.NET que intercepta cada petición antes de que llegue a la aplicación, con doble capa de bloqueo: aplicación y Windows Firewall.

01 — El problema y la idea

El escenario es habitual en entornos de empresa: servidor Windows con IIS, varios sitios WordPress o aplicaciones PHP, sin presupuesto para un WAF comercial ni justificación técnica para migrar a Linux. El servidor funciona bien, pero los logs de IIS crecen a base de peticiones automatizadas que nadie está mirando.

Los tres vectores de ataque más frecuentes contra WordPress son los mismos de siempre. Primero, xmlrpc.php: lleva años sin ser necesario en la mayoría de instalaciones pero sigue abierto, y los bots lo usan para amplificación y autenticaciones masivas sin que aparezca en los logs de login estándar. Segundo, fuerza bruta contra wp-login.php: scripts que prueban miles de combinaciones de usuario y contraseña. Tercero, escáneres de reconocimiento: herramientas automáticas que prueban cientos de rutas conocidas buscando archivos de configuración expuestos, webshells, rutas de administración, inyecciones SQL.

La solución que planteamos aquí actúa antes de que la petición llegue a PHP. No es un plugin de WordPress, no es un proxy inverso externo. Es código que vive dentro del propio pipeline de IIS.

02 — Qué es un módulo HTTP de IIS

IIS procesa cada petición HTTP pasándola por una cadena de módulos. Cada módulo puede inspeccionar la petición, modificarla, responder directamente o simplemente dejarla pasar. Los hay nativos (autenticación, compresión, logs) y se pueden añadir los propios.

Un módulo HTTP personalizado es una DLL .NET que implementa la interfaz IHttpModule. Esta interfaz define dos métodos: Init(), que se llama una vez cuando IIS carga el módulo, y Dispose(), que se llama al descargarlo. En Init() se registra un handler para el evento que nos interesa. En nuestro caso, BeginRequest: el primer evento del pipeline, que se dispara antes de que IIS determine qué aplicación va a manejar la petición.

Esto significa que si el módulo decide devolver un 403 Forbidden, la petición muere ahí. WordPress nunca se entera, PHP nunca arranca, no hay acceso a base de datos, no hay carga. La DLL se registra en el GAC (Global Assembly Cache) de Windows y se declara en el applicationHost.config de IIS, con lo que protege automáticamente todos los sitios del servidor.

03 — Arquitectura: tres capas

Petición HTTP entrante IIS + módulo HTTP IHttpModule / BeginRequest (DLL en GAC) OK → Aplicación WordPress / PHP ataque SQL Server SGPFirewall 403 Forbidden hackers.txt IPs para bloquear PowerShell tarea programada Windows Firewall bloqueo a nivel de red (async) ──────────────────────────
Las tres capas: el módulo HTTP intercepta en tiempo real, SQL Server guarda el estado, y un script PowerShell convierte las IPs más peligrosas en reglas de Windows Firewall.

04 — El módulo: Init y BeginRequest

El proyecto en Visual Studio es una Class Library (.NET Framework 4.8), firmada con certificado de nombre fuerte (necesario para el GAC). Las referencias que hay que añadir son System.Web y System.Configuration. El archivo principal es una clase que implementa IHttpModule.

La estructura básica del módulo:

Option Explicit On
Option Strict On

Imports System.Web
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient
Imports System.IO

Public Class manejadorHttp
    Implements IHttpModule

    Private Const RUTA_FICHERO_HACKERS As String = "C:\SGPFirewall\hackers.txt"
    Private Shared ReadOnly LockFichero As New Object()

    Public Sub Init(ByVal application As HttpApplication) _
        Implements IHttpModule.Init
        AddHandler application.BeginRequest, AddressOf Me.Application_BeginRequest
    End Sub

    Public Sub Dispose() Implements IHttpModule.Dispose
    End Sub

End Class

Init() recibe la instancia de la aplicación IIS y registra nuestro método como handler del evento BeginRequest. A partir de ahí, Application_BeginRequest se ejecuta en cada petición que llegue al servidor, en el hilo correspondiente. El LockFichero es un objeto de sincronización para cuando varios hilos intenten escribir en hackers.txt al mismo tiempo.

El método principal, con la lógica completa:

Private Sub Application_BeginRequest(ByVal source As Object, ByVal e As EventArgs)
    Dim application As HttpApplication = DirectCast(source, HttpApplication)
    Dim context As HttpContext = application.Context
    Dim filePath As String = context.Request.FilePath.ToLower()

    ' 1. Archivos estáticos: pasar sin analizar
    If EsArchivoEstatico(filePath) Then Exit Sub

    Dim ip_actual As String = nz(pilla_IP())

    ' 2. Whitelist: IPs de confianza, paso libre
    If EsIPAmiga(ip_actual) Then Exit Sub

    ' 3. IP ya baneada previamente
    Dim infracciones As Integer = compruebaIP(ip_actual)
    If infracciones >= 3 Then
        BloquearYTerminar(application, "IP Banned Permanently")
        Exit Sub
    End If

    ' 4. REGLA 1: XMLRPC — tolerancia cero
    If filePath.Contains("xmlrpc.php") Then
        Logea(5, "XMLRPC Attack Detected")
        set_ips(ip_actual, 1, 100)
        GuardarIPParaFirewall(ip_actual)
        BloquearYTerminar(application, "XMLRPC Blocked")
        Exit Sub
    End If

    ' 5. REGLA 2: Fuerza bruta
    If filePath.Contains("wp-login.php") OrElse filePath.Contains("login") Then
        Logea(1, "Login Attempt")
        Dim intentosRecientes As Integer = contarIntentosRecientes(ip_actual, 60)
        If intentosRecientes >= 5 Then
            Logea(9, "Brute Force Detected (>5 attempts/min)")
            set_ips(ip_actual, 1, 100)
            GuardarIPParaFirewall(ip_actual)
            BloquearYTerminar(application, "IP baneada por exceso de intentos de login.")
            Exit Sub
        End If
    End If

    ' 6. REGLA 3: Patrones de ataque (SQLi, XSS, path traversal...)
    Dim offe As Integer = buscaOfensa(filePath)
    If offe <> 0 Then
        Logea(5, "Attack Detected: " & filePath)
        If infracciones = 0 Then
            set_ips(ip_actual, 0, 1)
            infracciones = 1
        Else
            incrementa_Infracciones(ip_actual)
            infracciones += 1
        End If
        If infracciones >= 1 Then
            set_ips(ip_actual, 1, infracciones)
            GuardarIPParaFirewall(ip_actual)
            BloquearYTerminar(application, "IP baneada")
            Exit Sub
        End If
    End If

    ' Si llega aquí: petición legítima, no logueamos nada
End Sub

05 — El flujo de decisión y las tres reglas

Petición HTTP ¿Archivo estático? .jpg .css .js .png… PASAR No ¿IP en whitelist? red local, admin… PASAR No REGLA 1 — XMLRPC ¿URL contiene xmlrpc.php? → 403 inmediato + BD(100) + firewall REGLA 2 — Fuerza bruta ¿wp-login.php / login? → contar intentos / 60s Si ≥ 5 → 403 + BD(100) + firewall REGLA 3 — Patrones (SQLi / XSS / shells) ¿URL en lista de patrones? → +1 infracción en BD Si ≥ 3 infracciones → 403 + firewall Petición pasa a la aplicación
El flujo completo de decisión para cada petición. Las reglas se aplican en orden; si alguna dispara, la petición muere ahí.

Algunas decisiones de diseño que conviene entender. Los archivos estáticos se filtran primero porque suponen la mayoría del tráfico en un sitio normal: no tiene sentido hacer una consulta a SQL Server por cada .jpg. La whitelist va después: las IPs de confianza pasan sin ningún registro ni análisis. Esto es importante porque la propia IP del servidor (localhost) necesita estar en whitelist o el módulo se pelearía consigo mismo.

La Regla 1 (XMLRPC) es de tolerancia cero porque es imposible llegar a xmlrpc.php de forma legítima en una instalación WordPress estándar. Un solo intento es suficiente para un baneo permanente: estado 1 (Blacklisted) y 100 infracciones en la base de datos, más la IP en el fichero para el Windows Firewall.

La Regla 2 (fuerza bruta) usa una ventana temporal de 60 segundos. Consulta la tabla log y cuenta cuántos eventos de tipo "Login Attempt" hay de esa IP en el último minuto. Si son 5 o más, bloqueo. Esto permite que un usuario legítimo que se equivoca con la contraseña no sea baneado, pero detecta los scripts que disparan decenas de intentos por segundo.

La Regla 3 (patrones) usa un sistema de strikes. El primer patrón detectado da una infracción, el segundo otra, el tercero bloquea. Esto reduce los falsos positivos: una URL rara de por sí no es necesariamente un ataque, pero la misma IP con tres URLs raras distintas sí lo es con mucha más probabilidad.

06 — Los helpers de base de datos

Cada función auxiliar abre su propia conexión, la usa y la cierra. El Using de VB.NET garantiza que la conexión se cierra aunque ocurra un error. Todas las funciones tienen Try/Catch que en caso de fallo devuelve un valor seguro: falso en booleanos, cero en enteros. El módulo no puede permitir que un error de base de datos tire el servidor web.

' Connection string desde la configuración global de .NET
Private Shared Function Cadena_Aplicacion() As String
    If ConfigurationManager.ConnectionStrings("SGPFirewallConn") IsNot Nothing Then
        Return ConfigurationManager.ConnectionStrings("SGPFirewallConn").ConnectionString
    End If
    Return ""
End Function

' Extrae la IP del cliente, respetando proxies
Private Function pilla_IP() As String
    Dim ip As String = HttpContext.Current.Request.ServerVariables("HTTP_X_FORWARDED_FOR")
    If String.IsNullOrEmpty(ip) Then ip = HttpContext.Current.Request.ServerVariables("REMOTE_ADDR")
    If ip.Contains(",") Then ip = ip.Split(","c)(0)
    Return ip.Trim()
End Function

' Null-safe: si el objeto es Nothing o DBNull, devuelve "0"
Private Function nz(ByVal obj As Object) As String
    If obj Is Nothing OrElse IsDBNull(obj) Then Return "0"
    Return obj.ToString()
End Function

' ¿Está la IP en la whitelist?
Private Function EsIPAmiga(ip As String) As Boolean
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("esWhitelisted", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@IP", SqlDbType.NVarChar, 20).Value = ip
                conex.Open()
                Return CInt(cmd.ExecuteScalar()) > 0
            End Using
        End Using
    Catch
        Return False
    End Try
End Function

' ¿Cuántas infracciones tiene la IP?
Private Function compruebaIP(ip As String) As Integer
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand(
                "SELECT TOP 1 contador_infracciones FROM ips WHERE ip = @ip", conex)
                cmd.Parameters.AddWithValue("@ip", ip)
                conex.Open()
                Return CInt(nz(cmd.ExecuteScalar()))
            End Using
        End Using
    Catch
        Return 0
    End Try
End Function

' Cuenta intentos de login de una IP en los últimos N segundos
Private Function contarIntentosRecientes(ip As String, segundos As Integer) As Integer
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("check_intentos_recientes", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@IP", SqlDbType.NVarChar, 20).Value = ip
                cmd.Parameters.Add("@Segundos", SqlDbType.Int).Value = segundos
                conex.Open()
                Return CInt(nz(cmd.ExecuteScalar()))
            End Using
        End Using
    Catch
        Return 0
    End Try
End Function

' Registra un evento de seguridad en la tabla log
Private Sub Logea(ByVal codigo_evento As Integer, ByVal descripcion As String)
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("logea", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@codigo_evento", SqlDbType.Int).Value = codigo_evento
                cmd.Parameters.Add("@descripcion", SqlDbType.NVarChar, 255).Value =
                    If(descripcion.Length > 254, descripcion.Substring(0, 254), descripcion)
                cmd.Parameters.Add("@fecha_hora", SqlDbType.DateTime).Value = DateTime.Now
                cmd.Parameters.Add("@IP", SqlDbType.NVarChar, 20).Value = nz(pilla_IP())
                conex.Open()
                cmd.ExecuteNonQuery()
            End Using
        End Using
    Catch
    End Try
End Sub

' Inserta o actualiza el estado de una IP en la tabla ips
Private Sub set_ips(ByVal ip As String, estado As Byte,
                    Optional num_infracciones As Integer = 1)
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("set_ips", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@estado", SqlDbType.SmallInt).Value = estado
                cmd.Parameters.Add("@ip", SqlDbType.NVarChar, 15).Value = nz(ip)
                cmd.Parameters.Add("@num_infracciones", SqlDbType.Int).Value = num_infracciones
                conex.Open()
                cmd.ExecuteNonQuery()
            End Using
        End Using
    Catch
    End Try
End Sub

' Incrementa el contador de infracciones de una IP
Private Sub incrementa_Infracciones(ByVal ip As String)
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("incrementaInfracciones", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@ip", SqlDbType.NVarChar, 15).Value = nz(ip)
                conex.Open()
                cmd.ExecuteNonQuery()
            End Using
        End Using
    Catch
    End Try
End Sub

' Busca la URL contra la lista de patrones prohibidos en BD
Private Function buscaOfensa(ByVal responseUrl As String) As Integer
    Try
        Using conex As New SqlConnection(Cadena_Aplicacion())
            Using cmd As New SqlCommand("comparaOfending", conex)
                cmd.CommandType = CommandType.StoredProcedure
                cmd.Parameters.Add("@responseUrl", SqlDbType.NVarChar, -1).Value = responseUrl
                conex.Open()
                Return Convert.ToInt32(cmd.ExecuteScalar())
            End Using
        End Using
    Catch
        Return 0
    End Try
End Function

' Responde 403 y corta la petición
Private Sub BloquearYTerminar(app As HttpApplication, razon As String)
    app.Response.Clear()
    app.Response.StatusCode = 403
    app.Response.StatusDescription = "Forbidden"
    app.Response.Write("<h1>Access Denied</h1><p>" & razon & "</p>")
    app.Response.Flush()
    app.Response.End()
End Sub

' Escribe la IP en el fichero para el script del Windows Firewall
' SyncLock porque múltiples hilos pueden llegar aquí a la vez
Private Sub GuardarIPParaFirewall(ip As String)
    Try
        Dim dir As String = Path.GetDirectoryName(RUTA_FICHERO_HACKERS)
        If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
        SyncLock LockFichero
            File.AppendAllText(RUTA_FICHERO_HACKERS, ip & Environment.NewLine)
        End SyncLock
    Catch
    End Try
End Sub

' Archivos estáticos: no analizar
Private Function EsArchivoEstatico(ByVal ruta As String) As Boolean
    Select Case Path.GetExtension(ruta).ToLower()
        Case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".svg", ".webp",
             ".css", ".js", ".map", ".woff", ".woff2", ".ttf", ".eot", ".txt", ".xml"
            Return True
        Case Else
            Return False
    End Select
End Function

07 — La base de datos SQL Server

ips id int PK ip nvarchar(15) estado smallint FK contador_ infracciones int log id int PK codigo_evento descripcion fecha_hora IP nvarchar(20) whitelist id int PK ip nvarchar(15) descripcion cadenasProhibidas id int PK cadena nvarchar(255) estados_ips id smallint PK descripcion nvarchar(50) 0=Watching 1=Blacklisted
Las cinco tablas de SGPFirewall. La tabla ips guarda el estado de cada IP vista; log registra cada evento; whitelist excluye IPs de confianza; cadenasProhibidas contiene los patrones de ataque; y estados_ips es la lookup table de estados.

El script de creación de la base de datos, limpiado para uso general:

-- Crear la base de datos (SQL Server usará la ruta por defecto)
CREATE DATABASE [SGPFirewall]
GO

USE [SGPFirewall]
GO

-- Tabla de IPs vistas y su estado
CREATE TABLE [dbo].[ips] (
    [id]                   INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [ip]                   NVARCHAR(15)      NULL,
    [estado]               SMALLINT          NULL DEFAULT (1),
    [contador_infracciones] INT               NULL
)
GO

-- Log de eventos de seguridad
CREATE TABLE [dbo].[log] (
    [id]           INT IDENTITY(1,1) NOT NULL,
    [codigo_evento] INT              NULL,
    [descripcion]  NVARCHAR(255)    NULL,
    [fecha_hora]   DATETIME         NULL,
    [IP]           NVARCHAR(20)     NULL,
    [timest]       TIMESTAMP        NOT NULL
)
GO

-- IPs de confianza que nunca serán bloqueadas
CREATE TABLE [dbo].[whitelist] (
    [id]          INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [ip]          NVARCHAR(15)      NULL,
    [descripcion] NVARCHAR(100)     NULL
)
GO

-- Patrones de URL asociados a ataques conocidos
CREATE TABLE [dbo].[cadenasProhibidas] (
    [id]     INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [cadena] NVARCHAR(255)     NULL
)
GO

-- Lookup table de estados de IP
CREATE TABLE [dbo].[estados_ips] (
    [id]          SMALLINT      NOT NULL PRIMARY KEY,
    [descripcion] NVARCHAR(50)  NULL
)
GO

INSERT INTO estados_ips (id, descripcion) VALUES
    (0, 'Watching'),
    (1, 'Blacklisted'),
    (2, 'Whitelisted'),
    (3, 'For firewalling'),
    (4, 'Firewalled')
GO

-- Whitelist mínima: siempre incluir localhost
INSERT INTO whitelist (ip, descripcion) VALUES ('127.0.0.1', 'Localhost')
GO

Los stored procedures son la clave de que todo funcione de forma concurrente sin problemas. SQL Server gestiona los bloqueos internamente, algo que sería muy difícil de hacer correctamente en el código del módulo con ficheros de texto o SQLite.

-- Busca si la URL coincide con algún patrón de ataque
CREATE PROCEDURE [dbo].[comparaOfending] (@responseUrl NTEXT) AS
BEGIN
    SELECT ISNULL(
        (SELECT TOP 1 id FROM cadenasProhibidas
         WHERE @responseUrl LIKE '%' + cadena + '%'),
        0)
END
GO

-- Cuenta intentos de login de una IP en los últimos N segundos
CREATE PROCEDURE [dbo].[check_intentos_recientes]
    (@IP NVARCHAR(20), @Segundos INT) AS
BEGIN
    SELECT COUNT(*) FROM [log]
    WHERE IP = @IP
      AND fecha_hora > DATEADD(SECOND, -@Segundos, GETDATE())
END
GO

-- Comprueba si una IP está en whitelist
CREATE PROCEDURE [dbo].[esWhitelisted] (@IP NVARCHAR(20)) AS
BEGIN
    SELECT COUNT(*) FROM whitelist WHERE ip = @IP
END
GO

-- Registra un evento de seguridad
CREATE PROCEDURE [dbo].[logea]
    (@codigo_evento INT, @descripcion NVARCHAR(255),
     @ip NVARCHAR(20), @fecha_hora DATETIME) AS
BEGIN
    INSERT INTO [log] (codigo_evento, descripcion, ip, fecha_hora)
    VALUES (@codigo_evento, @descripcion, @ip, @fecha_hora)
END
GO

-- Inserta o actualiza el estado de una IP (upsert)
CREATE PROCEDURE [dbo].[set_ips]
    (@ip NVARCHAR(15), @estado SMALLINT, @num_infracciones INT) AS
BEGIN
    IF EXISTS (SELECT TOP 1 1 FROM ips WHERE ip = @ip)
        UPDATE ips
        SET estado = @estado, contador_infracciones = @num_infracciones
        WHERE ip = @ip
    ELSE
        INSERT INTO ips (ip, estado, contador_infracciones)
        VALUES (@ip, @estado, @num_infracciones)
END
GO

-- Incrementa el contador de infracciones de una IP
CREATE PROCEDURE [dbo].[incrementaInfracciones] (@ip NVARCHAR(15)) AS
BEGIN
    UPDATE dbo.ips
    SET contador_infracciones = 1 + ISNULL(contador_infracciones, 0)
    WHERE ip = @ip
END
GO

La lista de patrones prohibidos va en la tabla cadenasProhibidas. Lo útil de tenerla en base de datos es que se puede ampliar sin tocar el código ni reiniciar IIS. Algunas categorías representativas:

-- Webshells conocidas
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('shell.php'), ('cmd.php'), ('alfa.php'), ('bypass.php'),
    ('r57.php'), ('c99.php'), ('alfashell.php')
GO

-- Archivos de configuración que no deben ser accesibles
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('wp-config.php'), ('wp-config.php.bak'), ('.env'),
    ('/.aws/credentials'), ('.git/config'), ('web.config'), ('php.ini')
GO

-- Path traversal y archivos del sistema
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('/etc/passwd'), ('/etc/shadow'), ('/proc/self/environ'),
    ('c:\boot.ini'), ('c:\windows'), ('/bin/bash')
GO

-- SQL injection
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('union select'), ('information_schema'), ('insert into'),
    ('xp_cmdshell'), ('concat('), (';--')
GO

-- Wrappers PHP y ejecución remota
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('php://filter'), ('php://input'), ('cmd.exe'), ('powershell.exe')
GO

-- XMLRPC (también cubierto por la Regla 1, por si acaso)
INSERT INTO cadenasProhibidas (cadena) VALUES
    ('/xmlrpc.php'), ('/xmrlpc.php')
GO

08 — El instalador PowerShell

Instalar un módulo HTTP en IIS implica varios pasos que hay que hacer en orden correcto. El instalador automatiza todo. Requiere PowerShell 7 o superior y ejecutarse como Administrador.

# instala.ps1 — Instalador del módulo HTTP SGPFirewall
# Requiere: PowerShell 7+, permisos de Administrador

$dllOrigen  = Resolve-Path ".\firewallSGP.dll"
$targetDir  = "C:\SGPFirewall"
$dllDestino = "$targetDir\firewallSGP.dll"
$moduleName = "sgpFirewall"
$dbName     = "SGPFirewall"
$appCmd     = "$env:SystemRoot\System32\inetsrv\appcmd.exe"
$globalWebConfig = "$env:windir\Microsoft.NET\Framework64\v4.0.30319\Config\web.config"

# Usuario de aplicación con contraseña aleatoria
$appUser = "SGPUser"
$appPass = "SGP" + [Guid]::NewGuid().ToString().Substring(0,8) + "!" + (Get-Random -Min 100 -Max 999)

# ── PASO 1: Detección de SQL Server ──────────────────────────────
$sqlServices = Get-Service | Where-Object {
    ($_.Name -eq "MSSQLSERVER" -or $_.Name -like "MSSQL$*") -and $_.Status -eq "Running"
}
if ($sqlServices) {
    $svc = $sqlServices | Select-Object -First 1
    $sqlServer = if ($svc.Name -eq "MSSQLSERVER") { "localhost" }
                 else { ".\$($svc.Name.Split('$')[1])" }
} else {
    $sqlServer = Read-Host "SQL Server no detectado. Servidor (ej: localhost)"
    if ([string]::IsNullOrWhiteSpace($sqlServer)) { $sqlServer = "localhost" }
}

$sqlUser = Read-Host "Usuario Admin SQL (Enter para 'sa')"
if ([string]::IsNullOrWhiteSpace($sqlUser)) { $sqlUser = "sa" }
$sqlPassSec = Read-Host "Password de $sqlUser" -AsSecureString
$sqlPassPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sqlPassSec))

# ── PASO 2: Parar IIS ────────────────────────────────────────────
Stop-Service W3SVC -Force -ErrorAction SilentlyContinue
Stop-Service WAS   -Force -ErrorAction SilentlyContinue

# ── PASO 3: Copiar DLL ───────────────────────────────────────────
if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir | Out-Null }
Copy-Item -Path $dllOrigen -Destination $dllDestino -Force

# ── PASO 4: Verificar que la DLL contiene IHttpModule ────────────
$assembly    = [Reflection.Assembly]::Load([IO.File]::ReadAllBytes($dllDestino))
$tipoModulo  = $assembly.GetTypes() |
               Where-Object { $_.GetInterface("System.Web.IHttpModule") -ne $null } |
               Select-Object -First 1
if (-not $tipoModulo) { Write-Error "La DLL no implementa IHttpModule."; exit }
$fullTypeString = "$($tipoModulo.FullName), $($assembly.FullName)"

# ── PASO 5: Registrar en el GAC ──────────────────────────────────
[Reflection.Assembly]::Load("System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") | Out-Null
$pub = New-Object System.EnterpriseServices.Internal.Publish
$pub.GacInstall($dllDestino)

# ── PASO 6: Crear BD, tablas, procedimientos y usuario ───────────
# (aquí van los scripts SQL del apartado anterior, ejecutados via SqlConnection)
$conn = New-Object System.Data.SqlClient.SqlConnection
$conn.ConnectionString = "Server=$sqlServer;Database=master;User Id=$sqlUser;Password=$sqlPassPlain;"
$conn.Open()
# ... ejecutar scripts SQL ...
$conn.Close()

# ── PASO 7: Registrar el módulo en IIS ───────────────────────────
$configFile = "$env:SystemRoot\System32\inetsrv\config\applicationHost.config"
$backup = "$configFile.$((Get-Date).ToString('yyyyMMddHHmmss')).bak"
Copy-Item -Path $configFile -Destination $backup   # Backup antes de tocar

# Eliminar si ya existía (para reinstalaciones)
Start-Process $appCmd -ArgumentList "set config /section:system.webServer/modules /-[name='$moduleName']" -NoNewWindow -Wait
# Añadir el módulo
Start-Process $appCmd -ArgumentList "set config /section:system.webServer/modules /+""[name='$moduleName',type='$fullTypeString']""" -NoNewWindow -Wait

# ── PASO 8: Inyectar connection string en web.config global ──────
$finalConnString = "Server=$sqlServer;Database=$dbName;User Id=$appUser;Password=$appPass;"
$xml = New-Object System.Xml.XmlDocument
$xml.Load($globalWebConfig)
$backupWeb = "$globalWebConfig.$((Get-Date).ToString('yyyyMMddHHmmss')).bak"
Copy-Item -Path $globalWebConfig -Destination $backupWeb   # Backup

$root   = $xml.configuration
$csNode = $root.SelectSingleNode("connectionStrings")
if ($null -eq $csNode) {
    $csNode = $xml.CreateElement("connectionStrings")
    $root.AppendChild($csNode) | Out-Null
}
$existing = $csNode.SelectSingleNode("add[@name='SGPFirewallConn']")
if ($null -ne $existing) { $csNode.RemoveChild($existing) | Out-Null }

$entry = $xml.CreateElement("add")
$entry.SetAttribute("name", "SGPFirewallConn")
$entry.SetAttribute("connectionString", $finalConnString)
$entry.SetAttribute("providerName", "System.Data.SqlClient")
$csNode.AppendChild($entry) | Out-Null
$xml.Save($globalWebConfig)

# ── PASO 9: Arrancar IIS ─────────────────────────────────────────
try {
    Start-Service WAS  -ErrorAction Stop
    Start-Service W3SVC -ErrorAction Stop
} catch {
    # Si IIS no arranca, ofrecemos restaurar los backups
    $resp = Read-Host "IIS no arranca. ¿Restaurar backup? (S/N)"
    if ($resp -eq "S") {
        Copy-Item -Path $backup    -Destination $configFile    -Force
        Copy-Item -Path $backupWeb -Destination $globalWebConfig -Force
        Start-Service WAS; Start-Service W3SVC
    }
}

Tres puntos importantes del instalador. Primero, la contraseña del usuario de SQL ($appUser) se genera aleatoriamente en cada instalación y nunca aparece en ningún fichero fuente. Segundo, el instalador hace backup de applicationHost.config y del web.config global antes de tocarlos, y ofrece restaurarlos si IIS no arranca tras la instalación. Tercero, el connection string va al web.config global de .NET Framework (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\web.config), no al web.config de ningún sitio web individual, lo que evita que sea accesible desde el exterior.

Para que el módulo pueda registrarse en el GAC, el proyecto debe estar firmado con un certificado de nombre fuerte. En las propiedades del proyecto de Visual Studio, en la pestaña Firma, se activa "Firmar el ensamblado" y se genera o importa un fichero .pfx. Sin eso, el GAC rechazará la DLL.

09 — El script del Windows Firewall

Este script es intencionadamente corto. Su única responsabilidad es leer el fichero hackers.txt que va llenando el módulo HTTP y convertir cada IP en una regla de bloqueo de entrada en Windows Firewall. Se ejecuta como tarea programada con permisos de administrador, cada pocos minutos.

# mete_ips_de_hackers_txt_a_firewall.ps1
# Ejecutar como tarea programada, con permisos de Administrador

$LogFile   = "C:\SGPFirewall\hackers.txt"
$Procesados = "C:\SGPFirewall\procesados.txt"

if (Test-Path $LogFile) {
    $ips = Get-Content $LogFile

    foreach ($ip in $ips) {
        if (-not [string]::IsNullOrWhiteSpace($ip)) {
            New-NetFirewallRule `
                -DisplayName  "Bloqueo_Automatico_$ip" `
                -Direction    Inbound `
                -LocalPort    Any `
                -Protocol     TCP `
                -Action       Block `
                -RemoteAddress $ip `
                -ErrorAction  SilentlyContinue

            Add-Content $Procesados $ip
        }
    }

    # Vaciar el fichero para no procesar dos veces lo mismo
    Clear-Content $LogFile
}

La separación entre el módulo HTTP y este script es deliberada. El proceso de IIS no tiene permisos de administrador (ni debería tenerlos). Añadir reglas al Windows Firewall requiere privilegios elevados. Con esta arquitectura, el módulo solo escribe en un fichero de texto — algo que puede hacer sin privilegios — y el script con permisos de administrador lee ese fichero desde fuera de IIS y actúa.

Para configurarlo como tarea programada:

# Registrar la tarea programada (ejecutar como Administrador)
$action  = New-ScheduledTaskAction -Execute "pwsh.exe" `
           -Argument "-NonInteractive -File C:\SGPFirewall\mete_ips_de_hackers_txt_a_firewall.ps1"
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) -Once `
           -At (Get-Date)
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2)

Register-ScheduledTask `
    -TaskName "SGPFirewall - Actualizar Windows Firewall" `
    -Action   $action `
    -Trigger  $trigger `
    -Settings $settings `
    -RunLevel Highest `
    -Force

Con esto el sistema queda completo. El módulo HTTP bloquea en tiempo real a nivel de aplicación. Las IPs más peligrosas van al fichero cada vez que disparan la Regla 1 (XMLRPC) o la Regla 2 (fuerza bruta con más de 5 intentos). La tarea programada las convierte en reglas de Windows Firewall cada cinco minutos, pasando esas IPs al bloqueo de red. A partir de ese momento, sus paquetes se descartan antes de llegar a IIS.

Si quieres usar una versión lista para instalar sin montar el proyecto desde cero, de momento la manera de pedirlo es escribirnos directamente.