PostHole
Compose Login
You are browsing eu.zone1 in read-only mode. Log in to participate.
rss-bridge 2025-07-25T12:03:56+00:00

A journey implementing Channel Binding on MSSQLClient.py

A few weeks ago my friend Zblurx pushed a PR to Impacket in which he implemented the Channel Binding Token computation based on code that was developed by @lowercase_drm for the ldap3 library. This PR allowed any tool relying on the ldap3 library to be able to connect to LDAP servers even if LDAP signing and LDAPS channel binding are enabled. Looking at the code I thought it would be easy to implement the same mechanism on other protocols such as MSSQL which I was already working on pushing as PRs on NetExec.


A few weeks ago my friend Zblurx pushed a PR to Impacket in which he implemented the Channel Binding Token computation based on code that was developed by @lowercase_drm for the ldap3 library. This PR allowed any tool relying on the ldap3 library to be able to connect to LDAP servers even if LDAP signing and LDAPS channel binding are enabled. Looking at the code I thought it would be easy to implement the same mechanism on other protocols such as MSSQL which I was already working on pushing as PRs on NetExec.

So I started implementing CBT for MSSQL, thinking it would only take a couple of hours… but it ended up taking two whole weeks! Along the way, I learned a bunch of interesting things, so I figured I’d write a short blog post to share the journey.

1/ About MSSQL Servers installation and configuration

First things first, how do you install an MSSQL server? Well Microsoft proposes multiple installers that you can get from the following page:

Some installers are for enterprises (and thus require a licence), some are for testing purposes such as the “Express” one that is free. Once the installer runs, you’ll have a fully working MSSQL database along with the SQLCMD.exe binary used to manage the database. Yup, by default you won’t have a GUI tool so if you need one you can either install your favourite GUI tool (hello there DBeaver) or the official tool from Microsot, SSMS.exe.

MSSQL, being a service, has a dedicated MMC console available also:

Multiple panels will let you configure pretty much whatever you want. For example, you can chose which account will run the database

This account can be a local account (in my lab it’s the MSSQLSERVER virtual account), or a domain account (if and only if your database needs to query information over your Active Directory). Keep in mind that whether it is a local or a domain account, you must restrict the account’s privileges as much as you can to prevent lateral/horizontal movement if the MSSQL server is compromised.

Others options will let you configure whether you want your database to be exposed on a TCP/IP port or via a named pipe (or both):

If you want your server to support TLS encryption:

And if it should also support Extended Protection for Authentication (EPA) that, under the hood, means supporting Channel Binding Token:

By default, a freshly installed MSSQL server is vulnerable to NTLM relay attacks, as you can see below:

You probably already know that there are two mechanisms that can prevent these attacks:

  • NTLM signing (on unencrypted communications) which will sign (compute a hash from the packet itself) each NTLM packet and add the signature in the Message Integrity Code (MIC) field;
  • Channel Binding (on encrypted communications) which will compute a binding token based on the TLS session established between the client and the server and store that value in the Channel Binding value of the NTLM repsonse.

That being said, MSSQL doesn’t support signing on unencrypted communications. As such, it is recommended to enforce both encryption and Extended Protection. These will effectively prevent any NTLM relay attacks:

But keep in mind that if you do, you’ll have to use tooling that supports Channel Binding as well… And that’s not the case for the MSSQLClient.py script from the Impacket toolkit. Using it while having EPA enabled will get you this message:

Connection error. The connection comes from a non approuved domain and cannot be used with integrated authentication

That is a generic error that basically says, your authentication failed and it does not actually tell us what’s wrong with it… Is it the username? The password? Is the channel binding token missing? Helpfully, MSSQL server’s logs are much more detailed and can be found at the following path (adapt the <VERSION> depending on your MSSQL instance):

C:\\Program Files\\Microsoft SQL Server\\MSSQL<VERSION>.MSSQLSERVER\\MSSQL\\Log

You’ll see the following line:

Which translated says something like:

The binding token for that client is missing or does not match the established TLS link.
It is possible that the MSSQL service is under attack or that the client does not support extended protection.
Closing the connection. The SSPI binding token sent by the client does not match.

So we know that the Channel Binding token is missing or is invalid and we’ll have to compute it. The question is, how?

2/ Integrating CBT into TDS.py

In the Impacket library, the MSSQL communication is handled via the TDS.py script. This one is huge, but looking at it we understand that MSSQL communications is not just about sending some credentials / commands to a server. Instead it relies on structured packets that are part of an application layer protocol called TDS. This protocol was initially designed and developed by Sybase for their own SQL database engine but was later bought by Microsoft for MSSQL. Looking at the code, we can see that two functions are used to:

  • Send TDS packets (sendTDS):
def sendTDS(self, packetType, data, packetID = 1):
if (len(data)-8) > self.packetSize:
remaining = data[self.packetSize-8:]
tds = TDSPacket()
tds['Type'] = packetType
tds['Status'] = TDS_STATUS_NORMAL
tds['PacketID'] = packetID
tds['Data'] = data[:self.packetSize-8]
self.socketSendall(tds.getData())

while len(remaining) > (self.packetSize-8):
packetID += 1
tds['PacketID'] = packetID
tds['Data'] = remaining[:self.packetSize-8]
self.socketSendall(tds.getData())
remaining = remaining[self.packetSize-8:]
data = remaining
packetID+=1

tds = TDSPacket()
tds['Type'] = packetType
tds['Status'] = TDS_STATUS_EOM
tds['PacketID'] = packetID
tds['Data'] = data
self.socketSendall(tds.getData())
  • Receive TDS packets sent by the MSSQL server:
def recvTDS(self, packetSize = None):
if packetSize is None:
packetSize = self.packetSize

data = b""
while data == b"":
data = self.socketRecv(packetSize)

packet = TDSPacket(data)

status = packet["Status"]
packetLen = packet["Length"]-8
while packetLen > len(packet["Data"]):
data = self.socketRecv(packetSize)
packet["Data"] += data

remaining = None
if packetLen <  len(packet["Data"]):
remaining = packet["Data"][packetLen:]
packet["Data"] = packet["Data"][:packetLen]

while status != TDS_STATUS_EOM:
if remaining is not None:
tmpPacket = TDSPacket(remaining)
else:
tmpPacket = TDSPacket(self.socketRecv(packetSize))

packetLen = tmpPacket["Length"] - 8
while packetLen > len(tmpPacket["Data"]):
data = self.socketRecv(packetSize)
tmpPacket["Data"] += data

remaining = None
if packetLen <  len(tmpPacket["Data"]):
remaining = tmpPacket["Data"][packetLen:]
tmpPacket["Data"] = tmpPacket["Data"][:packetLen]

status = tmpPacket["Status"]
packet["Data"] += tmpPacket["Data"]
packet["Length"] += tmpPacket["Length"] - 8

return packet

Both these functions rely on the TDSPacket structure defined as:

[...]


Original source

Reply