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.

The procedure is set to EXECUTE AS OWNER, which is specifically what makes this work.
This specifies the statements inside the module executes 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.

Now I just have to 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:

Mitigation

Microsoft recommends avoiding setting a database as trustworthy, but if you absolutely have to, there are a few steps you should take to mitigate this:

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