SQL Server 暴力攻击检测:第 1 部分






4.97/5 (22投票s)
使用 T-SQL 防止对可远程访问的 SQL Server 数据库进行暴力登录攻击
系列文章
引言
当部署一个可供 Internet 客户端访问的 SQL Server 数据库时,最好始终使用 VPN 让客户端连接到数据库的 LAN。还可以使用 Windows Active Directory 登录进行访问,以便利用 AD 安全性(例如,登录失败后账户锁定)。大多数安全专家和 DBA 都强烈建议不要将 SQL Server 监听的端口(默认 1433)开放到 Internet。这是因为自动化 端口扫描器 在 扫描 Internet 查找开放的 SQL Server 端口,并尝试对 sa 账户进行暴力破解或字典攻击,这非常普遍。
然而,由于应用程序的设计规范或无法修改的遗留代码,有时将 SQL Server 开放到 Internet 是不可避免的。也许一个移动应用程序连接到数据库,使得仅限于在网络或服务器防火墙中列入白名单的 IP 地址列表进行访问变得不可行或不可能。外部访问与 SQL Server 登录账户访问相结合,会使数据库特别容易受到暴力破解攻击,因为 SQL Server 没有内置功能来检测连续的登录失败并随后禁用账户或阻止恶意客户端。最糟糕的是,当 sa 账户被启用并允许远程访问服务器时;如果遗留应用程序采用这种数据库访问方式,系统管理员将很难维护服务器的安全性,因为攻击者可以随意尝试任意次数的登录以猜测密码。最好能做的是定期检查 SQL Server 事件日志,并将恶意 IP 地址手动添加到 Windows 防火墙或网络的边缘防火墙。即便如此,这也会给攻击者充足的时间尝试成千上万次的登录。即使 sa 账户被禁用,攻击者也必须猜测登录名,这些登录失败的尝试会不必要地浪费目标服务器上的资源。
我们可以使用 T-SQL 在任何版本的 SQL Server 2005+ 上检测并自动阻止试图暴力破解我们数据库登录的客户端。SQL Server 能够使用 sp_readerrorlog
在 T-SQL 中读取事件日志,并使用 xp_cmdshell
执行命令 shell 命令。可以读取事件日志以提取登录失败的尝试,并获取远程客户端 IP 地址,而 xp_cmdshell
可用于调用 netsh advfirewall
以自动添加 Windows 防火墙的阻止规则。通过一些额外的处理代码,我们可以为在阻止恶意客户端 IP 地址之前检测到的失败尝试次数进行配置,以及在一段时间后清除被阻止的条目。
背景
SQL Server 有几千条应用程序日志条目;这是来自少数 IP 地址的数千次登录失败尝试!我在防火墙中阻止了它们,但很快就出现了来自其他 IP 地址的更多失败尝试。我移除了防火墙中的端口转发,并决定只允许通过我的 VPN 连接访问,但开始思考其他方法来加固 SQL Server,以防万一外部访问通过端口转发是开发人员或系统管理员的唯一选择。我知道有 用 PowerShell 编写的脚本,它们会扫描事件日志以查找失败的 RDP 连接并在 Windows 防火墙中阻止 IP。经过一番研究,我确定 SQL Server 具备了扫描事件日志和运行 netsh
以创建 Windows 防火墙规则所需的功能。然后,可以使用 T-SQL 轻松处理用于存储配置和管理阻止列表的额外逻辑。
Using the Code
配置服务器
首先,我们需要启用对 xp_cmdshell
的访问(默认禁用)并打开 SQL Server 实例上登录失败尝试的日志记录。从 sa 或管理员账户运行以下代码
USE [master]
GO
/*
Enable auditing of failed logins and use of command shell on database.
*/
EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', _
N'Software\Microsoft\MSSQLServer\MSSQLServer', N'AuditLevel', REG_DWORD, 2
GO
EXEC sp_configure 'show advanced options', 1;
GO
RECONFIGURE;
GO
EXEC sp_configure'xp_cmdshell', 1
GO
RECONFIGURE
GO
重启 MSSQL 服务以使更改生效。
创建表
接下来,我们创建一个新的数据库 BruteForceAttackMonitor
和三个表来存储被阻止的 IP 地址、一些配置和一组白名单 IP。
CREATE DATABASE [BruteForceAttackMonitor]
GO
ALTER DATABASE [BruteForceAttackMonitor] SET RECOVERY SIMPLE
GO
USE [BruteForceAttackMonitor]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/*
Create tables
*/
CREATE TABLE BlockedClient
(
IPAddress VARCHAR(15) NOT NULL PRIMARY KEY,
LastFailedLogin DATETIME,
FailedLogins INT,
Firewallrule VARCHAR(255)
);
CREATE INDEX IX_BlockedClient_LastFailedLogin ON BlockedClient(LastFailedLogin);
CREATE TABLE Config
(
ConfigID INT NOT NULL PRIMARY KEY,
ConfigDesc VARCHAR(255),
ConfigValue INT,
);
CREATE TABLE Whitelist
(
IPAddress VARCHAR(15) NOT NULL PRIMARY KEY,
Description VARCHAR(255)
);
GO
配置
我们将在 Config
表中添加三个配置,以提供一些用户可配置性。我相信可以实现许多其他配置,但我们将专注于对系统管理员最重要的配置。
1) 回溯时间
首先,我们需要一个时间段,从当前时间戳回溯到事件日志中的登录失败尝试。这应该相当短,但也要比运行阻止脚本的计划间隔时间长。我将其设置为秒为单位的时长,并设置为 15 分钟前。
其背后的逻辑很简单。我们希望扫描足够长的时间以捕获事件日志中的足够条目,但如果必须手动删除一个因意外输入多次错误密码而被阻止的客户端,我们也希望时间短一些。假设连接的应用程序是一个移动应用程序,用户输入用户名和密码连接到数据库。他们多次输入错误密码而被阻止,然后可能联系支持部门重置。支持部门知道要通过从我们的 BlockedClient
表中删除条目来解除对 IP 的阻止,并且当引起他们注意时,事件日志中过去的登录失败事件将超出我们的扫描时间窗口,因此该客户端不会被重新阻止。在这种情况下,我们不希望将其 IP 列入白名单;他们从移动提供商那里获得动态 IP,将来某个恶意客户端可能会获得此 IP 并绕过我们的暴力破解检测。
INSERT INTO Config(ConfigID, ConfigDesc, ConfigValue)
VALUES(1, 'Time in seconds to look back for failed logins', 900)
2) 登录失败尝试次数
接下来,我们定义在 IP 被阻止之前的登录失败尝试阈值。通常,您希望此值大于 1,这样意外输入的错误密码就不会触发客户端被阻止。3 到 10 之间的值可能最好,任何更大的值很可能是恶意客户端。我们将此设置为 3 次尝试(即在第 3 次失败的尝试时,阻止客户端 IP)。
INSERT INTO Config(ConfigID, ConfigDesc, ConfigValue)
VALUES(2, 'Number of failed logins before client is blocked', 3)
3) 解锁客户端的时间
我们不希望永远阻止某个 IP 地址。客户端通常使用其 ISP 或移动提供商的动态 IP 地址进行连接,因此很有可能被恶意客户端使用的 IP 地址将来会被受信任的客户端使用。这可能是一个不太可能的情况,但我们也不需要让 Windows 防火墙充斥着不断增长的被阻止 IP 列表。只要恶意 IP 被阻止足够长的时间,针对我们服务器的黑客或脚本小子就会在几分钟的连接被拒绝后放弃并移走。如果他们从同一 IP 地址持续攻击我们的服务器一段时间,他们将只有很短的时间窗口(阻止脚本运行的频率)进行几次登录尝试,然后再次被阻止,从而大大降低其攻击的有效性。
此配置以小时为单位,我们将设置为 24
;如果需要,我们仍然可以通过将此值设置为 <=0
来永远阻止客户端。
INSERT INTO Config(ConfigID, ConfigDesc, ConfigValue)
VALUES(3, 'Hours before client is unblocked (<=0 for never)', 24)
管理防火墙规则
被阻止 IP 地址列表将与 Windows 高级防火墙保持同步,通过在 BlockedClient
表上插入和删除操作的触发器。插入触发器使用 xp_cmdshell
和 netsh
为每个被阻止的 IP 自动添加防火墙规则。然后,BlockedClient
中的记录将使用防火墙规则的名称进行更新。
CREATE TRIGGER trg_BlockedClient_I
ON BlockedClient
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @IPAddress VARCHAR(15);
DECLARE @FirewallRule VARCHAR(255);
DECLARE @FirewallCmd VARCHAR(1000);
DECLARE vINSERT_CURSOR CURSOR LOCAL FOR
SELECT IPAddress FROM INSERTED
OPEN vINSERT_CURSOR;
FETCH NEXT FROM vINSERT_CURSOR INTO @IPAddress
WHILE @@FETCH_STATUS = 0
BEGIN
SET @FirewallCmd = 'netsh advfirewall firewall add rule name="';
SET @FirewallRule = 'SQL Server Failed Login Block for ' + @IPAddress;
/*
Alternative firewall rule that just blocks IP on SQL Server TCP port
SET @FirewallCmd = @FirewallCmd + @FirewallRule + _
'" dir=in interface=any protocol=TCP action=block remoteip=' + @IPAddress + ' LocalPort=1433'
*/
SET @FirewallCmd = @FirewallCmd + @FirewallRule + _
'" dir=in interface=any action=block remoteip=' + @IPAddress
EXEC xp_cmdshell @FirewallCmd --Create firewall rule
UPDATE BlockedClient --Update blocked client entry with firewall rule
--so we can reference for delete
SET FirewallRule = @FirewallRule
WHERE IPAddress = @IPAddress;
FETCH NEXT FROM vINSERT_CURSOR INTO @IPAddress;
END
CLOSE vINSERT_CURSOR;
DEALLOCATE vINSERT_CURSOR;
END
GO
delete
触发器引用存储在 BlockedClient
中的防火墙规则名称,再次使用 netsh
按名称从防火墙中删除该规则。
CREATE TRIGGER trg_BlockedClient_D
ON BlockedClient
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @FirewallRule VARCHAR(255);
DECLARE @FirewallCmd VARCHAR(1000);
DECLARE vDELETE_CURSOR CURSOR LOCAL FOR
SELECT FirewallRule FROM DELETED
WHERE FirewallRule IS NOT NULL
OPEN vDELETE_CURSOR;
FETCH NEXT FROM vDELETE_CURSOR INTO @FirewallRule
WHILE @@FETCH_STATUS = 0
BEGIN
SET @FirewallCmd = 'netsh advfirewall firewall delete rule name="' + @FirewallRule + '" dir=in';
EXEC xp_cmdshell @FirewallCmd --Delete firewall rule
FETCH NEXT FROM vDELETE_CURSOR INTO @FirewallRule;
END
CLOSE vDELETE_CURSOR;
DEALLOCATE vDELETE_CURSOR;
END
GO
接下来,我们在 Whitelist
上添加一个简单的 insert
触发器,以便在 IP 被列入白名单时自动删除 BlockedClient
中的记录,以防万一需要。
CREATE TRIGGER trg_WhiteList_I
ON Whitelist
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON; -- Automatically delete any blocked clients after they are whitelisted
DELETE FROM BlockedClient
WHERE EXISTS (SELECT * FROM INSERTED
WHERE INSERTED.IPAddress = BlockedClient.IPAddress);
END
GO
白名单 IP
在编写实际检测登录失败尝试的代码之前,让我们先编写一些代码,以便使用 CIDR 范围(例如,白名单我们的 LAN 子网)更轻松地白名单 IP。我借用了一些 代码,以便在 T-SQL 中更轻松地处理 CIDR 表示法。使用这两个函数,我们可以创建一个简单的存储过程,该过程可以枚举 CIDR 范围内的所有 IP 并将其插入 Whitelist
。
首先是我们的 IP 辅助函数
CREATE FUNCTION ConvertIPToLong(@IP VARCHAR(15))
RETURNS BIGINT
AS
BEGIN
DECLARE @Long bigint
SET @Long = CONVERT(bigint, PARSENAME(@IP, 4)) * 256 * 256 * 256 +
CONVERT(bigint, PARSENAME(@IP, 3)) * 256 * 256 +
CONVERT(bigint, PARSENAME(@IP, 2)) * 256 +
CONVERT(bigint, PARSENAME(@IP, 1))
RETURN (@Long)
END
GO
CREATE FUNCTION ConvertLongToIp(@IpLong bigint)
RETURNS VARCHAR(15)
AS
BEGIN
DECLARE @IpHex varchar(8), @IpDotted varchar(15)
SELECT @IpHex = substring(convert(varchar(30), master.dbo.fn_varbintohexstr(@IpLong)), 11, 8)
SELECT @IpDotted = convert(varchar(3), convert(int, _
(convert(varbinary, substring(@IpHex, 1, 2), 2)))) + '.' +
convert(varchar(3), convert(int, _
(convert(varbinary, substring(@IpHex, 3, 2), 2)))) + '.' +
convert(varchar(3), convert(int, _
(convert(varbinary, substring(@IpHex, 5, 2), 2)))) + '.' +
convert(varchar(3), convert(int, _
(convert(varbinary, substring(@IpHex, 7, 2), 2))))
RETURN @IpDotted
END
接下来,我们创建一个单一的 SP 来插入白名单 IP,这些 IP 将被我们的登录失败检测代码完全忽略。我们将子网掩码设为可选参数,默认为 /32,以便更轻松地白名单单个 IP。
CREATE PROCEDURE WhitelistIP
(
@IpAddress varchar(15),
@Mask int = 32,
@Description text = null
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Base bigint = cast(4294967295 as bigint);
DECLARE @Power bigint = Power(2.0, 32.0 - @Mask) - 1;
DECLARE @LowRange bigint = dbo.ConvertIPToLong(@IpAddress) & (@Base ^ @Power);
DECLARE @HighRange bigint = @LowRange + @Power;
DECLARE @CurrentIP VARCHAR(15)
IF @Description IS NULL
SET @Description = 'Whitelist for ' + @IPAddress + '/' + CONVERT(varchar(2), @Mask);
WHILE @LowRange <= @HighRange
BEGIN
SET @CurrentIP = dbo.ConvertLongToIp(@LowRange);
INSERT INTO Whitelist(IPAddress, Description)
SELECT @CurrentIP, @Description
WHERE NOT EXISTS (SELECT * FROM Whitelist WHERE IPAddress = @CurrentIP);
SET @LowRange = @LowRange + 1;
END;
END
GO
立即白名单 LAN 上的 IP 范围非常重要,这样我们才不会将自己锁在外面,尤其是如果我们决定让创建的防火墙规则阻止所有端口上的连接。trg_BlockedClient_I
触发器的代码包含阻止所有端口或仅阻止 SQL Server 监听的 TCP 端口上的连接的命令。如果您的 LAN 子网是 192.168.1.x,子网掩码为 255.255.255.0,请使用以下参数运行 SP
EXEC [dbo].[WhitelistIP] @IpAddress = N'192.168.1.0', @Mask = 24
白名单 LAN 以外的 IP 时,请确保仅为具有 ISP 已知静态 IP 的客户端执行此操作。对于移动设备或典型的家庭互联网连接,公共 IP 是动态的,因此白名单它没有意义。通常,仅白名单您知道将始终由受信任客户端使用的外部 IP。
阻止恶意客户端
现在让我们来处理实际检测恶意登录尝试并管理我们的阻止列表和防火墙规则的代码。我已将此代码放入一个单一的存储过程中,可以定期运行。这可以通过 SQL Server Agent 或 SQL Server CLI 和 Windows 任务计划程序(如果使用的是不支持 SQL Server Agent 的 Express 版本)来完成。我建议每 30 秒到一分钟运行一次。确保 Config
中的“回溯以查找登录失败的时间(秒)”参数比此值长,否则将存在一个未监控的时间窗口,在此期间的登录失败将不会被检测到。
确保运行脚本的用户帐户是 SQL Server 运行的计算机上的本地管理员(如果使用 SQL Server Agent,则服务必须以本地计算机管理员帐户运行)。运行 netsh
命令以添加/删除防火墙规则需要计算机管理员权限。
CREATE PROCEDURE CheckFailedLogins
AS
BEGIN
SET NOCOUNT ON;
DECLARE @UnblockDate DATETIME
DECLARE @LookbackDate DATETIME
DECLARE @MaxFailedLogins INT
DECLARE @FailedLogins TABLE
(
LogDate datetime,
ProcessInfo varchar(50),
Message text
);
SELECT @LookbackDate = dateadd(second, -ConfigValue, getdate())
FROM Config
WHERE ConfigID = 1
SELECT @MaxFailedLogins = ConfigValue
FROM Config
WHERE ConfigID = 2
SELECT @UnblockDate = CASE WHEN ConfigValue > 0 THEN DATEADD(hour, -ConfigValue, getdate()) END
FROM Config
WHERE ConfigID = 3
INSERT INTO @FailedLogins -- Read current log
exec sp_readerrorlog 0, 1, 'Login failed';
INSERT INTO BlockedClient(IPAddress, LastFailedLogin, FailedLogins)
SELECT IPAddress,
MAX(LogDate) AS LastFailedLogin,
COUNT(*) AS FailedLogins
FROM
(
SELECT LogDate,
ProcessInfo,
Message,
ltrim(rtrim(substring(CONVERT(varchar(1000), Message), -- Extract client IP
charindex('[CLIENT: ', CONVERT(varchar(1000), Message)) + 9,
charindex(']', CONVERT(varchar(1000), Message)) - 9 - _
charindex('[CLIENT: ', CONVERT(varchar(1000), Message))))) as IPAddress
FROM @FailedLogins
WHERE (Message like '%Reason: An error occurred while _
evaluating the password.%' -- Some filter criteria
OR Message like '%Reason: Could not find a login matching the name provided.%'
OR Message like '%Reason: Password did not match that for the login provided.%'
OR Message LIKE '%Login failed. The login is from an untrusted domain _
and cannot be used with Windows authentication.%')
AND LogDate >= @LookbackDate
) AS t
WHERE NOT EXISTS (SELECT * FROM Whitelist l -- Check against whitelist
WHERE l.IPAddress = t.IPAddress)
AND NOT EXISTS (SELECT * FROM BlockedClient c -- ignore already blocked clients
WHERE c.IPAddress = t.IPAddress)
AND IPAddress <> '<local machine>' -- ignore failed logins from local machine
GROUP BY IPAddress
HAVING COUNT(*) >= @MaxFailedLogins -- Check against number of failed logins config
AND MAX(LogDate) >= COALESCE(@UnblockDate, MAX(LogDate)) -- Check that new entries
-- meet delete config criteria so we don't unnecessarily
-- add a rule that would then get deleted.
DELETE FROM BlockedClient -- Delete entries older than the delete config
WHERE LastFailedLogin < @UnblockDate
END
GO
该过程的第二部分查找被阻止时间超过我们设置的 24 小时时段的客户端,并将其删除,以免我们的列表失控。
最后,查看 Windows 防火墙,我们可以看到自动创建的阻止规则!
备注
远程访问 SQL Server 数据库的最佳方式始终是使用 VPN 让客户端连接到数据库的专用 LAN(并可选择使用 AD 帐户访问),以获得最佳安全性。此设置始终能提供最高的安全性和最大的客户端访问管理灵活性。
然而,有时开发人员或系统管理员可能被迫使用安全性较弱的实现,该实现采用从 LAN 外部直接访问监听端口的方式,甚至更糟的是使用 sa 账户。使用此设置,服务器通常会使用常用端口扫描工具在 SA 账户上受到成千上万次登录尝试的轰炸。此代码提供了一种免费且易于部署的入侵防御方案,适用于 Windows 环境中 2005 年后的所有 SQL Server 版本,无论是 Express 版还是付费的企业版。
请随时评论代码,指出我可能遗漏的任何错误或可改进之处!