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
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
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 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.