Home » Getting the sysadmin role via trustworthy database

Getting the sysadmin role via trustworthy database

by Vlad Drumea
2 comments 4 minutes read

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.

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.

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.

SSMS query result: LoginName DBUserName PrivEscLogin PrivEscLogin IsSysAdmin DatabaseName 0 PrivEscDB SEO: getting sysadmin via trustworthy database

In order to achieve privilege escalation I just need to create and execute the following procedure.

Now, I execute the stored procedure.

I run the above T-SQL in one go so that the changes are noticeable in the output.

SSMS query result 1: LoginName DBUserName PrivEscLogin PrivEscLogin IsSysAdmin DatabaseName 0 PrivEscDB SSMS query result 2: LoginName DBUserName PrivEscLogin PrivEscLogin IsSysAdmin DatabaseName 1 PrivEscDB SEO: getting sysadmin via trustworthy database

Cleanup:


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:

  1. Create a dedicated role that would be used instead of db_owner.
  2. Add the role to the db_datareader, db_datawriter, and db_ddladmin (only if needed) roles.
  3. Grant EXECUTE to that role (this will cover all existing and future stored procedures).
  4. Grant EXECUTE tot the role an all scalar-valued functions.
  5. Grant SELECT to the role on all table-valued functions.
  6. Remove user(s) from the db_owner role.
  7. 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.

You may also like

2 comments

Emad Al-Mousa November 22, 2024 - 14:58

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

Reply
Vlad Drumea November 22, 2024 - 21:48

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).

Reply

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

This site uses Akismet to reduce spam. Learn how your comment data is processed.