Warning: preg_match(): Unknown modifier 'b' in /mnt/datadisk/www/src/Controllers/PostController.php on line 206 SQL Server: cómo evitar ataques de fuerza bruta a su base de datos — Dirceu ResendeSaltar al contenido
¡Hola, chicos!
En el artículo de hoy demostraré cómo se producen los ataques de fuerza bruta en SQL Server y cómo intentar defendernos de este tipo de ataques.
¿Qué es un ataque de fuerza bruta?
Haga clic para ver este contenido
El ataque de fuerza bruta es la técnica más sencilla y que requiere más tiempo para acceder a sistemas y bases de datos. Consiste en utilizar bases de datos de contraseñas para probar cada una de estas contraseñas o realizar una comprobación sistemática de todas las claves y contraseñas posibles hasta que una de ellas pueda iniciar sesión con éxito en el destino.
Este tipo de ataque se puede utilizar cuando no es posible aprovechar otras debilidades de un sistema criptográfico (si las hubiera) que facilitarían la tarea, ya que el tiempo necesario para probar todas las contraseñas posibles puede ir desde unos pocos segundos (3 caracteres) hasta miles de años, dependiendo del número de caracteres de la contraseña y de la complejidad de los caracteres utilizados.
Ataque de fuerza bruta en SQL Server
Haga clic para ver este contenido
Debido a que almacenan prácticamente todos los datos de los clientes y de la empresa en su conjunto, las bases de datos son potencialmente uno de los objetivos más populares para los atacantes que intentan robar información o simplemente obtener acceso privilegiado a esta base de datos para cualquier otro propósito. Un ejemplo clásico de esto es intentar acceder al banco con un usuario con poderes administrativos para detener el servicio SQL Server y así ejecutar un comando malicioso, como el ransomware WannaCry, por ejemplo, que requiere que los bancos no estén en uso para poder cifrar los datos.
Para poder identificar si tu instancia de SQL Server está siendo atacada, lo primero que debes hacer es activar la auditoría de fallos de conexión (ya viene activada por defecto), como se muestra en la captura de pantalla a continuación:
Esto también se puede cambiar usando comandos T-SQL:
EXEC sys.xp_instance_regwrite
@rootkey = 'HKEY_LOCAL_MACHINE',
@key = 'SOFTWARE\Microsoft\MSSQLServer\MSSQLServer',
@value_name = 'AuditLevel',
@type = 'REG_DWORD',
@value = 2 -- 0 = Nenhum / 1 = Apenas sucesso / 2 = Apenas falha / 3 = Sucesso e Falha
Una vez que se haya asegurado de que los inicios de sesión fallidos ya estén siendo auditados, cualquier intento de conexión que no tenga éxito, debido a una contraseña incorrecta o a un usuario que no existe, se registrará en el ERRORLOG de SQL Server, al que puede acceder de esta manera:
Y luego ver los registros:
Además, también puedes utilizar el Procedimientos extendidos sp_readerrorlog y xp_readerrorlog para poder ver el contenido del registro de SQL Server usando la línea de comando.
Para obtener más información sobre cómo funcionan estos dos procedimientos, le sugiero que estudie el código que mostraré en este artículo y también lea el excelente artículo. ERRORLOG – Conceptos básicos, que explica todo sobre el ERRORLOG de SQL Server.
Cómo identificar un ataque de fuerza bruta en SQL Server
Haga clic para ver este contenido
Ahora que he mostrado cómo activar la auditoría de fallas de conexión y también dónde y cómo podemos consultar esta información, comencemos a trabajar con estos datos para lograr nuestro objetivo: Cómo identificar ataques de fuerza bruta en SQL Server.
En el uso básico, usaremos xp_readerrorlog para leer el archivo de registro actual (0) y filtrar los errores de inicio de sesión en el registro:
Ahora ejecutemos este comando nuevamente, pero almacenando el retorno del SP en una tabla temporal e identificando la IP y el usuario de este mensaje:
IF (OBJECT_ID('tempdb..#Login_Failed') IS NOT NULL) DROP TABLE #Login_Failed
CREATE TABLE #Login_Failed (
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Text] NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Username] AS LTRIM(RTRIM(REPLACE(REPLACE(SUBSTRING(REPLACE([Text], 'Login failed for user ''', ''), 1, CHARINDEX('. Reason:', REPLACE([Text], 'Login failed for user ''', '')) - 2), CHAR(10), ''), CHAR(13), ''))),
[IP] AS LTRIM(RTRIM(REPLACE(REPLACE(REPLACE((SUBSTRING([Text], CHARINDEX('[CLIENT: ', [Text]) + 9, LEN([Text]))), ']', ''), CHAR(10), ''), CHAR(13), '')))
)
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.xp_readerrorlog 0, 1, N'Login failed'
SELECT * FROM #Login_Failed
Resultado:
Si desea analizar no solo el último archivo de registro, sino todos ellos, necesitará sp_enumerrorlogs, para enumerar los registros existentes y así configurar su bucle entre los archivos. Aproveché para refinar aún más la búsqueda y mostrar solo intentos fallidos debido a contraseña incorrecta e inicio de sesión inexistente (hay otros tipos, como usuario sin permiso en la base de datos predeterminada):
--------------------------------------------------------------
-- Cria as tabelas temporárias
--------------------------------------------------------------
IF (OBJECT_ID('tempdb..#Arquivos_Log') IS NOT NULL) DROP TABLE #Arquivos_Log
CREATE TABLE #Arquivos_Log (
[idLog] INT,
[dtLog] NVARCHAR(30) COLLATE SQL_Latin1_General_CP1_CI_AI,
[tamanhoLog] INT
)
IF (OBJECT_ID('tempdb..#Login_Failed') IS NOT NULL) DROP TABLE #Login_Failed
CREATE TABLE #Login_Failed (
[LogNumber] TINYINT,
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Text] NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Username] AS LTRIM(RTRIM(REPLACE(REPLACE(SUBSTRING(REPLACE([Text], 'Login failed for user ''', ''), 1, CHARINDEX('. Reason:', REPLACE([Text], 'Login failed for user ''', '')) - 2), CHAR(10), ''), CHAR(13), ''))),
[IP] AS LTRIM(RTRIM(REPLACE(REPLACE(REPLACE((SUBSTRING([Text], CHARINDEX('[CLIENT: ', [Text]) + 9, LEN([Text]))), ']', ''), CHAR(10), ''), CHAR(13), '')))
)
--------------------------------------------------------------
-- Importa os arquivos do ERRORLOG
--------------------------------------------------------------
INSERT INTO #Arquivos_Log
EXEC sys.sp_enumerrorlogs
--------------------------------------------------------------
-- Loop para procurar por falhas de login nos arquivos
--------------------------------------------------------------
DECLARE
@Contador INT = 0,
@Total INT = (SELECT COUNT(*) FROM #Arquivos_Log)
WHILE(@Contador < @Total)
BEGIN
-- Pesquisa por senha incorreta
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.sp_readerrorlog @Contador, 1, N'Password did not match that for the login provided'
-- Pesquisa por tentar conectar com usuário que não existe
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.sp_readerrorlog @Contador, 1, N'Could not find a login matching the name provided.'
-- Atualiza o número do arquivo de log
UPDATE #Login_Failed
SET LogNumber = @Contador
WHERE LogNumber IS NULL
SET @Contador += 1
END
SELECT * FROM #Login_Failed
Resultado:
Con estos datos ya podemos empezar a identificar qué usuarios y el origen de los ataques a los usuarios:
SELECT [IP], COUNT(*) AS Quantidade
FROM #Login_Failed
GROUP BY [IP]
ORDER BY 2 DESC
SELECT [Username], COUNT(*) AS Quantidade
FROM #Login_Failed
GROUP BY [Username]
ORDER BY 2 DESC
Resultado:
Importante: Un punto interesante que podemos tener en cuenta es crear una lista de excepciones con IP que no se pueden bloquear, incluso en los casos en los que hay muchas conexiones fallidas, como por ejemplo IP fija de la propia empresa, IP del servidor de aplicaciones, etc.
Cómo monitorear posibles intrusiones de fuerza bruta
Haga clic para ver este contenido
Como este tema es sumamente importante, no puedes contar con tener la suerte de acordarte de estar atento a este tipo de eventos en tu entorno. Si comienza a sufrir un posible ataque debes actuar lo antes posible y nada mejor que una alerta para avisarte cuando este tipo de conductas empiecen a aparecer en las instancias que manejas.
Para lograr este objetivo utilizaré SQL Server Database Mail, que nos permitirá enviar correos electrónicos a través de la base de datos cuando exista una determinada cantidad de fallas de conexión y el Procedimiento Almacenado. stpExporta_Table_HTML_Output, que permite almacenar el contenido de una tabla en la base de datos en una variable en formato de tabla HTML, ideal para enviar por correo electrónico.
El siguiente script buscará eventos de falla de inicio de sesión de la última hora y enviará estos registros por correo electrónico (si exceden el límite definido para evitar enviar demasiados correos electrónicos y convertirse en SPAM).
-- Configurações
DECLARE
@Qt_Tentativas_Para_Alertar INT = 10,
@Fl_Envia_Email BIT = 1
--------------------------------------------------------------
-- Cria as tabelas temporárias
--------------------------------------------------------------
IF (OBJECT_ID('tempdb..#Arquivos_Log') IS NOT NULL) DROP TABLE #Arquivos_Log
CREATE TABLE #Arquivos_Log (
[idLog] INT,
[dtLog] NVARCHAR(30) COLLATE SQL_Latin1_General_CP1_CI_AI,
[tamanhoLog] INT
)
IF (OBJECT_ID('tempdb..#Login_Failed') IS NOT NULL) DROP TABLE #Login_Failed
CREATE TABLE #Login_Failed (
[LogNumber] TINYINT,
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Text] NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Username] AS LTRIM(RTRIM(REPLACE(REPLACE(SUBSTRING(REPLACE([Text], 'Login failed for user ''', ''), 1, CHARINDEX('. Reason:', REPLACE([Text], 'Login failed for user ''', '')) - 2), CHAR(10), ''), CHAR(13), ''))),
[IP] AS LTRIM(RTRIM(REPLACE(REPLACE(REPLACE((SUBSTRING([Text], CHARINDEX('[CLIENT: ', [Text]) + 9, LEN([Text]))), ']', ''), CHAR(10), ''), CHAR(13), '')))
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao') IS NOT NULL) DROP TABLE ##Tentativas_Conexao
CREATE TABLE ##Tentativas_Conexao (
[LogNumber] TINYINT,
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50),
[Text] NVARCHAR(MAX),
[Username] NVARCHAR(256),
[IP] NVARCHAR(50)
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao_Por_IP') IS NOT NULL) DROP TABLE ##Tentativas_Conexao_Por_IP
CREATE TABLE ##Tentativas_Conexao_Por_IP (
[IP] NVARCHAR(256),
Qt_Tentativas INT
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao_Por_Usuario') IS NOT NULL) DROP TABLE ##Tentativas_Conexao_Por_Usuario
CREATE TABLE ##Tentativas_Conexao_Por_Usuario (
[Username] NVARCHAR(256),
Qt_Tentativas INT
)
IF (OBJECT_ID('tempdb..##Lista_IPs_Bloquear') IS NOT NULL) DROP TABLE ##Lista_IPs_Bloquear
CREATE TABLE ##Lista_IPs_Bloquear (
[Lista_IPs] VARCHAR(MAX)
)
--------------------------------------------------------------
-- Importa os arquivos do ERRORLOG
--------------------------------------------------------------
INSERT INTO #Arquivos_Log
EXEC sys.sp_enumerrorlogs
--------------------------------------------------------------
-- Loop para procurar por falhas de login nos arquivos
--------------------------------------------------------------
DECLARE
@Contador INT = 0,
@Total INT = (SELECT COUNT(*) FROM #Arquivos_Log),
@Ultima_Hora VARCHAR(19) = FORMAT(DATEADD(HOUR, -1, GETDATE()), 'yyyy-MM-dd HH:mm:00'),
@Agora VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121)
WHILE(@Contador < @Total)
BEGIN
-- Pesquisa por senha incorreta
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.xp_readerrorlog @Contador, 1, N'Password did not match that for the login provided', NULL, @Ultima_Hora, @Agora
-- Pesquisa por tentar conectar com usuário que não existe
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.xp_readerrorlog @Contador, 1, N'Could not find a login matching the name provided.', NULL, @Ultima_Hora, @Agora
-- Atualiza o número do arquivo de log
UPDATE #Login_Failed
SET LogNumber = @Contador
WHERE LogNumber IS NULL
SET @Contador += 1
END
--------------------------------------------------------------
-- Salva as tentativas realizadas, já excluindo a lista de exceções
--------------------------------------------------------------
INSERT INTO ##Tentativas_Conexao
SELECT
A.*
FROM
#Login_Failed A
WHERE
A.[IP] NOT LIKE '%local machine%'
ORDER BY
A.LogDate
INSERT INTO ##Tentativas_Conexao_Por_IP
SELECT
[IP],
COUNT(*) AS Quantidade
FROM
##Tentativas_Conexao
GROUP BY
[IP]
ORDER BY
2 DESC
INSERT INTO ##Tentativas_Conexao_Por_Usuario
SELECT
[Username],
COUNT(*) AS Quantidade
FROM
##Tentativas_Conexao
GROUP BY
[Username]
ORDER BY
2 DESC
INSERT INTO ##Lista_IPs_Bloquear
SELECT
STUFF((
SELECT
',' + [IP]
FROM
##Tentativas_Conexao_Por_IP
ORDER BY
[IP]
FOR XML PATH('')
), 1, 1, '') AS listaIps
IF ((SELECT COUNT(*) FROM ##Tentativas_Conexao) > 0)
BEGIN
IF (@Fl_Envia_Email = 1 AND (SELECT COUNT(*) FROM ##Tentativas_Conexao) > @Qt_Tentativas_Para_Alertar)
BEGIN
DECLARE
@Assunto VARCHAR(200) = '[' + @@SERVERNAME + '] - Tentativas de conexão sem sucesso',
@Mensagem VARCHAR(MAX) = 'Olá,<br/>Seguem logs de tentativas de conexão sem sucesso na instância ' + @@SERVERNAME + ':',
@HTML VARCHAR(MAX)
--------------------------------------------------------------
-- Gera o código HTML para enviar por e-mail
-- https://dirceuresende.com/blog/como-exportar-dados-de-uma-tabela-do-sql-server-para-html/
--------------------------------------------------------------
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Histórico do Log</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao_Por_IP', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Acessos por IP</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao_Por_Usuario', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Acessos por Usuário</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Lista_IPs_Bloquear', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Lista de IPs para Bloquear</h2>' + @HTML
--------------------------------------------------------------
-- Envia o e-mail
-- https://dirceuresende.com/blog/como-habilitar-enviar-monitorar-emails-pelo-sql-server-sp_send_dbmail/
--------------------------------------------------------------
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'Profile DirceuResende',
@recipients = 'email@gmail.com',
@subject = @Assunto,
@body = @Mensagem,
@body_format = 'html'
END
END
Resultado:
Cómo evitar posibles intrusiones de fuerza bruta
Haga clic para ver este contenido
Hemos llegado a la parte final de este artículo, donde discutiré posibles soluciones a este problema tan común, que son los ataques de fuerza bruta. La alternativa que se puede implementar en SQL Server es crear una lista de IP para bloquear y ejecutar esta lista a través de un script externo, como PowerShell o incluso el programador de tareas de Windows, para crear reglas en el Firewall de Windows para bloquear estas IP cuando alcancen una cierta cantidad de intentos de conexión fallidos.
Para exportar la lista de IP a un archivo en el servidor, podemos usar diferentes métodos como xp_cmdshell, OLE Automation o SQLCLR, pero elegiré xp_cmdshell en este ejemplo, ya que es el más sencillo de implementar. Una vez exportado el archivo en formato bat con los comandos a ejecutar, basta con programar la ejecución de este script en el programador de tareas de Windows y configurar la ejecución para utilizar un usuario con permiso de Administrador.
Otra alternativa viable es dejar que SQL Server ejecute los comandos para agregar IP al Firewall en lugar de exportar un archivo bat con estos comandos. Para que esto sea posible, el usuario que inicia el servicio SQL Server debe ser Administrador local de la máquina, lo que termina no siendo tan recomendable desde el punto de vista de la seguridad.
Un punto importante a demostrar nuevamente es la lista de excepciones, para evitar que se bloqueen IP importantes durante cualquier prueba o error temporal.
Para finalizar este tema, pondré a disposición el script completo, el cual:
identifica los registros
Categorizar por IP y usuario
almacena un historial de intentos
busca ocurrencias de fallas de conexión desde la última ejecución del procedimiento
lista de excepciones implementada
enviar correo electrónico de alerta
genera un archivo .bat en el disco con comandos para bloquear IP con más de N fallas de conexión en el Firewall de Windows
Código fuente del script:
USE [dirceuresende]
GO
IF (OBJECT_ID('dbo.stpVerifica_Falha_Conexao') IS NULL) EXEC('CREATE PROCEDURE dbo.stpVerifica_Falha_Conexao AS SELECT 1')
GO
ALTER PROCEDURE dbo.stpVerifica_Falha_Conexao (
@Fl_Envia_Email BIT = 1,
@Qt_Tentativas_Para_Alertar INT = 100,
@Fl_Gera_Arquivo_Firewall BIT = 1,
@Qt_Tentativas_para_Bloquear INT = 5
)
AS
BEGIN
SET NOCOUNT ON
-- DECLARE @Qt_Tentativas_Para_Alertar INT = 100, @Fl_Envia_Email BIT = 1, @Fl_Gera_Arquivo_Firewall BIT = 1, @Qt_Tentativas_para_Bloquear INT = 5
--------------------------------------------------------------
-- Cria as tabelas temporárias
--------------------------------------------------------------
IF (OBJECT_ID('tempdb..#Arquivos_Log') IS NOT NULL) DROP TABLE #Arquivos_Log
CREATE TABLE #Arquivos_Log (
[idLog] INT,
[dtLog] NVARCHAR(30) COLLATE SQL_Latin1_General_CP1_CI_AI,
[tamanhoLog] INT
)
IF (OBJECT_ID('tempdb..#Login_Failed') IS NOT NULL) DROP TABLE #Login_Failed
CREATE TABLE #Login_Failed (
[LogNumber] TINYINT,
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Text] NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Username] AS LTRIM(RTRIM(REPLACE(REPLACE(SUBSTRING(REPLACE([Text], 'Login failed for user ''', ''), 1, CHARINDEX('. Reason:', REPLACE([Text], 'Login failed for user ''', '')) - 2), CHAR(10), ''), CHAR(13), ''))),
[IP] AS LTRIM(RTRIM(REPLACE(REPLACE(REPLACE((SUBSTRING([Text], CHARINDEX('[CLIENT: ', [Text]) + 9, LEN([Text]))), ']', ''), CHAR(10), ''), CHAR(13), '')))
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao') IS NOT NULL) DROP TABLE ##Tentativas_Conexao
CREATE TABLE ##Tentativas_Conexao (
[LogNumber] TINYINT,
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50),
[Text] NVARCHAR(MAX),
[Username] NVARCHAR(256),
[IP] NVARCHAR(50)
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao_Por_IP') IS NOT NULL) DROP TABLE ##Tentativas_Conexao_Por_IP
CREATE TABLE ##Tentativas_Conexao_Por_IP (
[IP] NVARCHAR(256),
Qt_Tentativas INT
)
IF (OBJECT_ID('tempdb..##Tentativas_Conexao_Por_Usuario') IS NOT NULL) DROP TABLE ##Tentativas_Conexao_Por_Usuario
CREATE TABLE ##Tentativas_Conexao_Por_Usuario (
[Username] NVARCHAR(256),
Qt_Tentativas INT
)
IF (OBJECT_ID('tempdb..##Lista_IPs_Bloquear') IS NOT NULL) DROP TABLE ##Lista_IPs_Bloquear
CREATE TABLE ##Lista_IPs_Bloquear (
[Lista_IPs] VARCHAR(MAX)
)
IF (OBJECT_ID('tempdb..#Bloquear_IP') IS NOT NULL) DROP TABLE #Bloquear_IP
CREATE TABLE #Bloquear_IP (
Contador INT IDENTITY(1,1) NOT NULL,
[IP] NVARCHAR(256),
Qt_Tentativas INT
)
--------------------------------------------------------------
-- Lista com IP's permitidos que não podem ser bloqueados
--------------------------------------------------------------
IF (OBJECT_ID('dbo.Excecoes') IS NULL)
BEGIN
-- DROP TABLE dbo.Excecoes
CREATE TABLE dbo.Excecoes (
[IP] VARCHAR(15) COLLATE SQL_Latin1_General_CP1_CI_AI NOT NULL UNIQUE
) WITH(DATA_COMPRESSION=PAGE)
INSERT INTO dbo.Excecoes
VALUES
('192.168.31.108'),
('127.0.0.1')
END
--------------------------------------------------------------
-- Histórico das tentativas de conexão
--------------------------------------------------------------
IF (OBJECT_ID('dbo.Tentativas_Conexao') IS NULL)
BEGIN
-- TRUNCATE TABLE dbo.Tentativas_Conexao
CREATE TABLE dbo.Tentativas_Conexao (
[LogDate] DATETIME,
[ProcessInfo] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI,
[Username] NVARCHAR(256) COLLATE SQL_Latin1_General_CP1_CI_AI,
[IP] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AI
) WITH(DATA_COMPRESSION=PAGE)
CREATE CLUSTERED INDEX SK01_Tentativas_Conexao ON dbo.Tentativas_Conexao(LogDate) WITH(DATA_COMPRESSION=PAGE, FILLFACTOR=100)
END
--------------------------------------------------------------
-- Importa os arquivos do ERRORLOG
--------------------------------------------------------------
INSERT INTO #Arquivos_Log
EXEC sys.sp_enumerrorlogs
--------------------------------------------------------------
-- Loop para procurar por falhas de login nos arquivos
--------------------------------------------------------------
DECLARE
@Contador INT = 0,
@Total INT = (SELECT COUNT(*) FROM #Arquivos_Log),
@Ultima_Coleta VARCHAR(19) = CONVERT(VARCHAR(19), ISNULL(DATEADD(SECOND, 1, (SELECT MAX(LogDate) FROM dbo.Tentativas_Conexao)), '1900-01-01'), 121),
@Agora VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121),
@IP VARCHAR(20),
@Query VARCHAR(4000)
WHILE(@Contador < @Total)
BEGIN
-- Pesquisa por senha incorreta
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.xp_readerrorlog @Contador, 1, N'Password did not match that for the login provided', NULL, @Ultima_Coleta, @Agora
-- Pesquisa por tentar conectar com usuário que não existe
INSERT INTO #Login_Failed (LogDate, ProcessInfo, [Text])
EXEC master.dbo.xp_readerrorlog @Contador, 1, N'Could not find a login matching the name provided.', NULL, @Ultima_Coleta, @Agora
-- Atualiza o número do arquivo de log
UPDATE #Login_Failed
SET LogNumber = @Contador
WHERE LogNumber IS NULL
SET @Contador += 1
END
--------------------------------------------------------------
-- Salva as tentativas realizadas, já excluindo a lista de exceções
--------------------------------------------------------------
INSERT INTO ##Tentativas_Conexao
SELECT
A.*
FROM
#Login_Failed A
LEFT JOIN dbo.Excecoes B ON B.[IP] = A.[IP] COLLATE SQL_Latin1_General_CP1_CI_AI
WHERE
(B.[IP] IS NULL AND A.[IP] NOT LIKE '%local machine%')
ORDER BY
A.LogDate
INSERT INTO ##Tentativas_Conexao_Por_IP
SELECT
[IP],
COUNT(*) AS Quantidade
FROM
##Tentativas_Conexao
GROUP BY
[IP]
ORDER BY
2 DESC
INSERT INTO ##Tentativas_Conexao_Por_Usuario
SELECT
[Username],
COUNT(*) AS Quantidade
FROM
##Tentativas_Conexao
GROUP BY
[Username]
ORDER BY
2 DESC
INSERT INTO #Bloquear_IP
SELECT
A.[IP],
COUNT(*) AS Quantidade
FROM
##Tentativas_Conexao A
LEFT JOIN dbo.Tentativas_Conexao B ON B.[IP] = A.[IP] COLLATE SQL_Latin1_General_CP1_CI_AI
WHERE
B.[IP] IS NULL
GROUP BY
A.[IP]
HAVING
COUNT(*) >= @Qt_Tentativas_para_Bloquear
ORDER BY
2 DESC
INSERT INTO ##Lista_IPs_Bloquear
SELECT
STUFF((
SELECT
',' + [IP]
FROM
#Bloquear_IP
ORDER BY
[IP]
FOR XML PATH('')
), 1, 1, '') AS listaIps
--------------------------------------------------------------
-- Armazena o histórico
--------------------------------------------------------------
INSERT INTO dbo.Tentativas_Conexao
(
LogDate,
ProcessInfo,
Username,
[IP]
)
SELECT
LogDate,
(CASE
WHEN [Text] LIKE '%password%' THEN 'Password failed'
WHEN [Text] LIKE '%Could not find a login matching the name provided%' THEN 'Login does not exists'
END) AS ProcessInfo,
Username,
[IP]
FROM
##Tentativas_Conexao
IF ((SELECT COUNT(*) FROM ##Tentativas_Conexao) > 0)
BEGIN
IF (@Fl_Envia_Email = 1 AND (SELECT COUNT(*) FROM ##Tentativas_Conexao) > @Qt_Tentativas_Para_Alertar)
BEGIN
DECLARE
@Assunto VARCHAR(200) = '[' + @@SERVERNAME + '] - Tentativas de conexão sem sucesso',
@Mensagem VARCHAR(MAX) = 'Olá,<br/>Seguem logs de tentativas de conexão sem sucesso na instância ' + @@SERVERNAME + ':',
@HTML VARCHAR(MAX)
--------------------------------------------------------------
-- Gera o código HTML para enviar por e-mail
-- https://dirceuresende.com/blog/como-exportar-dados-de-uma-tabela-do-sql-server-para-html/
--------------------------------------------------------------
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Histórico do Log</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao_Por_IP', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Acessos por IP</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Tentativas_Conexao_Por_Usuario', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Acessos por Usuário</h2>' + @HTML
EXEC dbo.stpExporta_Tabela_HTML_Output
@Ds_Tabela = '##Lista_IPs_Bloquear', -- varchar(max)
@Ds_Saida = @HTML OUT -- varchar(max)
SET @Mensagem += '<br/><br/><h2>Lista de IPs para Bloquear</h2>' + @HTML
--------------------------------------------------------------
-- Envia o e-mail
-- https://dirceuresende.com/blog/como-habilitar-enviar-monitorar-emails-pelo-sql-server-sp_send_dbmail/
--------------------------------------------------------------
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'Profile DirceuResende',
@recipients = 'email@gmail.com',
@subject = @Assunto,
@body = @Mensagem,
@body_format = 'html'
END
--------------------------------------------------------------
-- Bloqueia os IP's no Firewall do Windows
-- https://dirceuresende.com/blog/como-instalar-e-configurar-o-microsoft-sql-server-2016-no-windows-server-2016/
--------------------------------------------------------------
IF (@Fl_Gera_Arquivo_Firewall = 1)
BEGIN
DECLARE @Fl_Xp_CmdShell_Ativado BIT = (SELECT (CASE WHEN CAST([value] AS VARCHAR(MAX)) = '1' THEN 1 ELSE 0 END) FROM sys.configurations WHERE [name] = 'xp_cmdshell')
IF (@Fl_Xp_CmdShell_Ativado = 0)
BEGIN
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE WITH OVERRIDE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE WITH OVERRIDE;
END
SET @Contador = 1
SET @Total = (SELECT COUNT(*) FROM #Bloquear_IP)
-- Apaga o arquivo
EXEC master.dbo.xp_cmdshell 'type nul > "C:\Temporario\Firewall.bat"'
WHILE(@Contador <= @Total)
BEGIN
SELECT TOP(1) @IP = [IP]
FROM #Bloquear_IP
WHERE Contador = @Contador
SET @Query = 'ECHO netsh advfirewall firewall add rule name="SQL Server - IP Block - ' + @IP + '" dir=in interface=any action=block remoteip=' + @IP + '/32 >> "C:\Temporario\Firewall.bat'
EXEC master.dbo.xp_cmdshell @Query
--PRINT @Query
SET @Contador += 1
END
IF (@Fl_Xp_CmdShell_Ativado = 0)
BEGIN
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE WITH OVERRIDE;
EXECUTE sp_configure 'show advanced options', 0;
RECONFIGURE WITH OVERRIDE;
END
END
END
END
Script de bloqueo de IP generado (ejecutar como administrador):
Seguridad en SQL Server – ¡Asunto SERIO!
Después de demostrar algunas formas de mitigar el problema de la invasión por fuerza bruta identificando y bloqueando las IP que intentan invadir el entorno en el Firewall de Windows, Necesitamos hablar seriamente sobre Seguridad. Lamentablemente es muy común ver entornos completamente descuidados en cuanto a seguridad de bases de datos, donde los usuarios de las aplicaciones tienen permiso de sysadmin, los usuarios que acceden a la base de datos también como sysadmin, usuarios con privilegios elevados y contraseñas débiles, xp_cmdshell y la consulta dinámica son prácticas comunes en entornos y muchos otros escenarios muy preocupantes.
Respecto al escenario de este post que es el ataque de fuerza bruta, la gran mayoría de los casos ocurren debido a que la base de datos está publicada para Internet, lo que facilita mucho el ingreso de posibles atacantes y la posibilidad de que alguien intente atacar tu instancia. En muchos casos esto ocurre cuando el servidor del banco es el mismo que el de la aplicación, lo que no es una buena práctica ni para el banco ni para la aplicación.
El banco acaba compitiendo por los recursos del servidor (CPU, Memoria, Disco, Red) con la aplicación y aún así tiene que estar visible y expuesto a Internet, sin posibilidad de crear una Lista Blanca (solo las direcciones de la lista tienen acceso al servidor), ya que esto impediría que los usuarios accedan a la aplicación. Sin mencionar que si el servidor de aplicaciones se ve comprometido, también se puede acceder a los datos del banco, lo que es un escenario catastrófico.
Otro punto que también hay que plantear es que incluso cuando el ataque de fuerza bruta se lleva a cabo en el entorno, incluso si no tiene éxito, igualmente logra dañar a la empresa, porque mientras sigue intentando miles de contraseñas para ingresar al banco, este proceso termina consumiendo una gran cantidad de recursos del servidor innecesariamente.
Además, muchos ransomware, como WannaCry (que devastó Internet el año pasado) Generalmente van precedidos de ataques de fuerza bruta. para obtener acceso a la base de datos para que el atacante pueda desconectar las bases de datos para que el Ransonware pueda luego cifrar los archivos de datos (mdf) y de registro (ldf).
Entonces, ¿qué hacer para resolver esto?
Bueno, el PRIMER paso hacia esto es hacer que el banco sea invisible para Internet. Si tienes que separar la base de datos de la aplicación y configurar 1 servidor para cada uno, entonces hazlo, creando una VPN para realizar la comunicación entre los 2 servidores (si están en redes diferentes, como Azure). Si la empresa no puede permitirse un servidor para cada persona y se accede a la aplicación a través de Internet, a través de varias IP y no hay posibilidad de crear una lista blanca para limitar las IP que accederán al servidor, la solución es implementar la solución que proporcioné anteriormente y esperar que no suceda lo peor, ya que el script de este artículo solo bloqueará los intentos de acceso a la base de datos, pero existen N otros tipos de ataques para acceder al servidor, como ataques RDP, SSH, etc.
El SEGUNDO paso es deshabilitar y cambiar el nombre del usuario SA y cualquier otro usuario estándar de SQL Server, ya que estos usuarios son los principales objetivos de los ataques de fuerza bruta, ya que el atacante ya conoce el inicio de sesión y sólo necesita la contraseña.
El TERCER paso es evitar en la medida de lo posible el uso de inicios de sesión con autenticación de SQL Server, ya que el banco tiene que controlar y gestionar estas contraseñas. Además, el hash de estas contraseñas se almacena en la base de datos y se puede descifrar y validar fácilmente sin generar ni siquiera una excepción en el registro de SQL Server, como lo demostré en el artículo. SQL Server: cómo identificar contraseñas débiles, vacías o iguales al nombre de usuario. Siempre que sea posible, utilice la autenticación de Windows AD, eliminando la necesidad de ingresar contraseñas y SQL Server no tiene información de hash o contraseña.
El CUARTO paso es implementar el seguimiento de este artículo y estar siempre atento a este informe, bloqueando las IP que tengan muchas fallas de conexión en el Firewall de Windows (o Azure). Este trabajo debe ser CONSTANTE y no sólo puntual. Dejó la base de datos disponible para Internet, por lo que ahora puede manejarla. Si es posible, utiliza esta lista de IP para bloquear estas direcciones a nivel organizacional, en el firewall general de la red y no solo en el servidor del banco. También puede obtener listas de rangos de IP por país e intentar publicar solo IP de Brasil (lista blanca más completa).
El QUINTO paso es revisar periódicamente la contraseña de los inicios de sesión de SQL Server de su entorno. Es muy importante que las contraseñas de estos usuarios se cambien AL MENOS 1 VEZ AL AÑO. Además, las contraseñas de estos usuarios deben ser largas y complejas (yo siempre uso contraseñas de 50 caracteres o más, incluyendo letras, números y símbolos). Además, es importante contar con una política de revisión de acceso (al menos) anual. sysadmin es solo para DBA y db_owner ni siquiera debería usarse (salvo excepciones muy raras).
Bueno chicos, este fue el artículo sobre Brute Force Attack en SQL Server. Espero que hayan disfrutado de esta línea de Seguridad, que es un área en la que debo invertir mucho tiempo estudiando y escribiendo artículos este año y es la tema de mi charla en MVPConf Latam 2019.
Un fuerte abrazo y nos vemos en el próximo post.
Dirceu Resende
Arquitecto de Bases de Datos y BI · Microsoft MVP · MCSE, MCSA, MCT, MTA, MCP.
Comentários (0)
Carregando comentários…