In this post I cover SQL Server roles and permissions that can act as viable avenues for privilege escalation to sysadmin.
A while ago, I wrote a blog post about gaining sysadmin role membership by using a trustworthy database owned by sa.
While that scenario is more prevalent, and people tend to be on the lookout for, there are other scenarios that aren’t generally so well covered.
What is privilege escalation?
Privilege escalation is a method in which an attacker gains unauthorized privileged access into a system.
This is done by exploiting misconfigurations, design flaws or unpatched vulnerabilities.
The most straightforward example in SQL Server land being someone “promoting themselves” from members of the public fixed server role, to members of the sysadmin role.
Disclaimer
Everything in this post is for educational purposes only and meant to show folks why they should be extra careful when granting some permissions.
If you want to reproduce any of this, make sure it’s on an environment that you own or where you have explicit permission to do so.
Demo environment
I’m doing all the demos in this post on SQL Server 2022 CU25.
Which, as of yesterday (May 20th 2026) is the latest patch level available for SQL Server 2022.
The hardware and OS aren’t relevant in this case.
Granted, if this is a real-life scenario and the OS is Windows, an attacker could then leverage xp_cmdshell to execute commands at the OS level.
SECURITYADMIN
securityadmin is a fixed server role, it allows its members to manage logins and their properties, as well as database users (as long as the role member has access to said database). Besides this it can also reset login passwords.
It’s the second most powerful role after sysadmin.
Due to its powerful privileges, Microsoft makes the following note:
The securityadmin role should be treated as equivalent to the sysadmin role.
Privilege escalation via securityadmin
First, I create the login that will be a member of the securityadmin role.
| 1 2 3 4 5 6 7 | CREATE LOGIN [test_securityadmin] WITH PASSWORD = 'StrongPassword!123', CHECK_POLICY = ON, CHECK_EXPIRATION = OFF; GO ALTER SERVER ROLE [securityadmin] ADD MEMBER [test_securityadmin]; GO |
Now, I log in as test_securityadmin and do a quick check of the server level permissions that come with this role.
| 1 2 3 | SELECT SUSER_SNAME() AS login_name, permission_name FROM fn_my_permissions(NULL,'SERVER'); |

The useful permissions in this case are CREATE LOGIN and ALTER ANY LOGIN.
So, if an attacker somehow gained access to a login that’s a member of the securityadmin role, or a login that has those two permissions explicitly granted, the next logical step would be one of the following:
- Create a new login.
- Change the password of an existing low privilege* login.
*low privilege because, as powerful as the securityadmin role is, its members are still not allowed to add members to the sysadmin role or alter logins that are already members of sysadmin.
Note: Since this requires an additional login, securityadmin isn’t a direct path to privilege escalation, but it is an intermediary step.
So, I create the following login to act as our attacker’s next step in their SQL Server privilege escalation attack.
| 1 | CREATE LOGIN [test_user] WITH PASSWORD = 'StrongPassword!123'; |
The second most useful thing that the securityadmin role can do is allow its members to grant some very interesting permissions.
So, after this, the next step is for our fictitious attacker to grant one of 3 permissions in order to achieve their goal.
And, at this point, it’s going to look like one of those “choose your own adventure” books.
CONTROL SERVER
The CONTROL SERVER permission is the most powerful server-level (duh) permission.
It’s as close as possible to sysadmin role membership without actually being sysadmin.
So, let’s say our attacker opts to grant their newly create login the CONTROL SERVER permission.
| 1 2 | GRANT CONTROL SERVER TO [test_user]; GO |
Privilege escalation via CONTROL SERVER
I’m not going to list all the server-level permissions that CONTROL SERVER comes with.
What matters in this case is the fact that it comes with the IMPERSONATE ANY LOGIN permission.
This means that any login that has been granted CONTROL SERVER can gain privilege escalation to sysadmin with one simple command:
| 1 | EXECUTE AS LOGIN ='sa'; |
So, logged in as test_user, I run the following:
| 1 2 3 4 5 6 | SELECT SUSER_SNAME() AS whoami_before; GO EXECUTE AS LOGIN ='sa'; GO SELECT SUSER_SNAME() AS whoami_after; GO |

IMPERSONATE ANY LOGIN
As the name implies, IMPERSONATE ANY LOGIN PERMISSION allows logins to impersonate any other login on the instance.
Including members of the sysadmin fixed server role.
This makes the privilege escalation path the same as with CONTROL SERVER.
But, just for example’s sake:
| 1 2 | GRANT IMPERSONATE ANY LOGIN TO [test_user]; GO |
Privilege escalation via IMPERSONATE ANY LOGIN
Logged in as test_user, now with the IMPERSONATE ANY LOGIN permission, I re-run the same T-SQL as the previous round:
| 1 2 3 4 5 6 | SELECT SUSER_SNAME() AS whoami_before; GO EXECUTE AS LOGIN ='sa'; GO SELECT SUSER_SNAME() AS whoami_after; GO |

IMPERSONATE LOGIN where the login impersonated already has elevated privileges
In this case, instead of the IMPERSONATE ANY LOGIN permission, the test_user login is granted permission to impersonate a specific, high privileged, login.
| 1 2 | GRANT IMPERSONATE ON LOGIN::[sa] TO [test_user]; GO |
Here, the privilege escalation through IMPERSONATE LOGIN::[sa] is straightforward.
The attacker logged in as test_user just has to run the same command as in previous examples to interact with the instance as sa.
| 1 2 | EXECUTE AS LOGIN = 'sa'; GO |
Note: to exit the impersonated context just run the REVERT command.
IMPERSONATE LOGIN and nesting impersonation
What about situations where the compromised login has IMPERSONATE LOGIN permissions on a login with one of the above permissions instead of directly on a member of the sysadmin server role?
For this part of the demo I’ll first revoke test_user’s permission to impersonate sa.
| 1 2 | REVOKE IMPERSONATE ON LOGIN::[sa] FROM [test_user]; GO |
I’ll only use the CONTROL SERVER permission as an example, to keep things short.
| 1 2 3 4 5 | CREATE LOGIN [test_control_server] WITH PASSWORD = N'StrongPassword!123'; GO GRANT CONTROL SERVER TO [test_control_server]; GO |
Finally, I grant test_user the IMPERSONATE LOGIN permission for the test_control_server login.
| 1 2 | GRANT IMPERSONATE ON LOGIN::[test_control_server] TO [test_user]; GO |
Now, logged in as test_user, I run the following:
| 1 2 3 4 5 6 7 8 9 10 | SELECT SUSER_SNAME() AS whoami_originally; GO EXECUTE AS LOGIN ='test_control_server'; GO SELECT SUSER_SNAME() AS whoami_intermediary; GO EXECUTE AS LOGIN ='sa'; GO SELECT SUSER_SNAME() AS whoami_finally; GO |

This way, the privilege escalation path is clear, test_user can only impersonate test_control_server which, in turn, can impersonate any login (including sa) as part of the CONTROL SERVER permission.
The same logic applies with all the previous permissions and securityadmin role as well.
It just needs an additional level of impersonation.

Cleanup
| 1 2 3 4 5 6 7 8 | DROP LOGIN [test_securityadmin]; GO REVOKE CONTROL SERVER FROM [test_control_server]; GO DROP LOGIN [test_control_server]; GO DROP LOGIN [test_user]; GO |
Finding logins that can be targeted in a privilege escalation attempt
I’m currently working on adding a security check to PSBlitz, and a few of the checks target SQL Server logins that have permissions which can be used for privilege escalation.
But, until that’s ready, here are a few queries to help in identifying potential targets.
SQL Server logins with permissions that can be used for privilege escalation
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | SELECT [name] AS [login_name], 'securityadmin' AS [permission_or_role] FROM sys.[syslogins] WHERE [securityadmin] = 1 AND [denylogin] = 0 AND [name] NOT LIKE N'NT SERVICE\%' UNION ALL SELECT [pri].[name] AS [login_name], [perm].[permission_name] AS [permission_or_role] FROM sys.[server_principals] AS [pri] INNER JOIN sys.[server_permissions] AS [perm] ON [perm].[grantee_principal_id] = [pri].[principal_id] WHERE [perm].[state] IN ( 'G', 'W' ) AND [perm].[class] = 100 AND [perm].[type] IN ( 'IAL', 'CL' ) AND [pri].[type] IN ( 'R', 'S', 'U', 'G' ); |
This will return any login that is either a member of securityadmin or has either the IMPERSONATE ANY LOGIN or CONTROL SERVER permissions

Note: I’m aware that sys.syslogins is a legacy view, but it still works in both old and current versions of SQL Server, so I’ll continue to use it.
SQL Server logins that can impersonate other logins that can be used for privilege escalation
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | SELECT [pri].name AS [login_name], [target].name AS [impersonation_target], CASE WHEN [perm2].[permission_name] = N'CONNECT SQL' AND [l].[sysadmin] = 1 THEN N'sysadmin' WHEN [perm2].[permission_name] = N'CONNECT SQL' AND [l].[securityadmin] = 1 THEN N'securityadmin' ELSE [perm2].[permission_name] END AS [target_permission_or_role] FROM sys.[server_principals] AS [pri] INNER JOIN sys.[server_permissions] AS [perm] ON [perm].[grantee_principal_id] = [pri].[principal_id] INNER JOIN sys.[server_principals] AS [target] ON [perm].[major_id] = [target].[principal_id] INNER JOIN sys.[syslogins] AS [l] ON [target].[sid] = [l].[sid] LEFT JOIN sys.[server_permissions] AS [perm2] ON [perm2].[grantee_principal_id] = [target].[principal_id] WHERE [perm].[state] IN ( 'G', 'W' ) AND [perm].[class] = 101 AND [perm].[type] = 'IM' AND [pri].[type] IN ( 'R', 'S', 'U', 'G' ) AND (([l].[sysadmin] = 1 OR [l].[securityadmin] = 1) OR ( [perm2].[type] IN ( 'IAL', 'CL' ) AND [perm2].[state] IN ( 'G', 'W' ))); |
This query will return any login that can impersonate logins that are either members of the sysadmin or securityadmin roles, or have CONTROL SERVER, IMPERSONATE ANY or ALTER ANY LOGIN permissions.

Mitigation
Ideally, you would avoid adding users to the securityadmin role.
Especially since, starting with SQL Server 2022, Microsoft provides ##MS_LoginManager##, an alternative, less powerful, role.
And you’d also avoid granting the CONTROL SERVER, IMPERSONATE ANY or ALTER ANY LOGIN permissions.
As well as avoiding permission to impersonate privileged logins.
So, in short, adhering to the principle of least privilege.
But, if you absolutely need to use the securityadmin role or grant one of the aforementioned permissions, just remember that SQL Server has a maximum password length of 128 characters.
Use the full width of the password for those accounts.
The same goes for any AD logins that might be used as service or application accounts.
Ensure they have strong passwords and that they adhere to the principle of least privilege both at the OS level as well as in SQL Server.
Also, configure audit for server principal impersonation events and make sure you know if one of those events is caught by the audit.
Have security assessments of your instances at least once per quarter.
For the CONTROL SERVER permission specifically, you can DENY the IMPERSONATE ANY LOGIN permission.
Example:
| 1 2 3 4 | USE [master] GO DENY IMPERSONATE ANY LOGIN TO [test_control_server]; GO |
And, if you want to do an audit of how strong your SQL Server login passwords are, there are ways of doing that for both 2025 and pre-2025.
Conclusion
Check your logins and their permissions regularly, folks.
Make sure you’re not leaving a path exposed for privilege escalation to sysadmin.
2 comments
Interesting read, but I think this type of argument suffers from a fallacy: “So, if an attacker somehow gained access to a login that’s a member of the securityadmin role…”
The same reasoning can be applied to literally any login, including members of sysadmin.
While privilege escalation is certainly an issue, the root problem is always how they gained initial access in the first place.
In 20 years of working with SQL Server, I have never seen securityadmin role being used.
After this article, probably will never use it.
Thanks Vlad.
Hi Art,
Thanks for taking the time to read the post!
Fair point about the usage of securityadmin in the wild, I’ve included it mainly for completeness’ sake.
Instead, I have ran into an in-house monitoring dashboard that had its login set up with CONTROL SERVER permission (the reasoning being that “it’s not sysadmin”), and the password was stored in plain-text in an easily accessible config file.