BlackArrow blog header

Lateral movement via MSSQL: a tale of CLR and socket reuse

Recently, our Red Team had to deal with a restricted scenario, where all traffic from the DMZ to the main network was blocked, except for connections to specific services like databases and some web applications. In this article, we will explain how we overcame the situation, covering the technical details. We also introduce mssqlproxy, a tool for turning a Microsoft SQL Server into a socks proxy.

Introduction and context

At some point, sysadmin access to a Microsoft SQL Server within the main network is gained, but only traffic to the 1433 port is allowed. After trying to launch a reverse shell via xp_cmdshell, the team realizes that connections back to the DMZ were also blocked.

Restricted environment
Restricted environment

In order to turn the SQL Server into a useful pivot, we decided to apply some of the old socket reuse techniques [1] usually related to one-way shellcodes. The idea is to reuse the connection itself between the client and the database to pass data through, turning it into a working socks proxy.

Tunnel created via SQL socket reuse
Tunnel created via socket reuse

The first step is to execute code in the SQL Server process context. As extended stored procedures are going to be deprecated in future versions of MSSQL, we pay attention to Microsoft recommendations and thus, use CLR assemblies instead.

CLR Assemblies

The usage of CLR assemblies in post-exploitation is another well-known technique. This article from NetSPI [2] is a good starting point to understand how to implement your own assemblies.

In our case, we decided to develop an assembly which will be responsible of loading a DLL (written in C) and executing the whole networking logic. In the future, we may do all the work from the CLR itself.

To dynamically invoke a function of our DLL, we make use of LoadLibrary (to load the DLL) and GetProcAddress (to get a pointer to the function). However, we cannot call this function directly as we can do in C, because C# doesn’t support function pointers [3]. Here we first have to convert the pointer to a Delegate object, using Marshal.GetDelegateForFunctionPointer.

Server: socket’s hunt and proxyfication

Let’s move on to the DLL part, where all the logic resides. To reuse the client’s socket, the first step is to find it. Different approaches can be used to perform this task, each with its own advantages and drawbacks. In this case, we choose the less intrusive way: findport. It consists of socket handler bruteforcing (since it’s identified by an integer), iterating until the target socket is found. The same technique has been used in the past by our Red Team in MySQL scenarios related with persistence [4]. In this case, we can match the source address and port if the connection is made directly. If there are intermediaries (e.g. proxies, NAT/PAT) we can check the source address only if we are sure that there are no more connections from that host.

Since the whole process is triggered by a SQL query, we can obtain the source address of the request by querying CONNECTIONPROPERTY('client_net_address'). This way, if the connection is made via an intermediary, we can still get the right IP address without knowing it beforehand. If the connection is direct, we also provide the source port, obtained on the client side.

The findport bruteforce can be summarized as:

...
    // Iterate over all sockets
    for (i = 1; i < max_socket; i++) { 
        len = sizeof(sockaddr);

        // Check if it is a socket
        if (getpeername((SOCKET)i, (SOCKADDR*)&sockaddr, &len) == 0) {
            // Check if it is our target
			if (strcmp(inet_ntoa(sockaddr.sin_addr), client_addr) == 0 && (client_port == 0 || sockaddr.sin_port == htons(client_port))) {
...

We noticed that one connection opened multiple socket handlers, and other threads were interfering. To make sure we fully control the socket, we can duplicate it (WSADuplicateSocket) and close the original one (closesocket). After that, a initialization message is sent to the client, to announce that the proxy socks is ready.

SOCKS5 is a simple protocol that can be implemented in a few lines of code. The server code was simplified and adapted from this repo. We don’t want to reinvent the wheel, so the client stuff was developed on top of impacket, expanding the mssqlclient.py capabilities.

Client: mssqlclient.py 2.0

File upload/download

First of all, we need to somehow upload files to the server, since we need to host the DLL. This can be done by using Ole Automation Stored Procedures to instanciate an ADODB.Stream object [5].

DECLARE @ob INT;
EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;
EXEC sp_OASetProperty @ob, 'Type', 1;
EXEC sp_OAMethod @ob, 'Open';
EXEC sp_OAMethod @ob, 'Write', NULL, 'C:\path\file.dll';
EXEC sp_OAMethod @ob, 'SaveToFile', NULL, 0x[HEXDATA], 2;
EXEC sp_OAMethod @ob, 'Close';
EXEC sp_OADestroy @ob;

Although not necessary for this scenario to work, we also added a download command. Files can be easily read from SQL using OPENROWSET with the BULK option [6].

SELECT * FROM OPENROWSET(BULK N'C:\path\file.ext', SINGLE_BLOB) rs

Proxy mode

Relating to the proxy stuff, four commands have been implemented:

  • install: Creates the CLR assembly and links it to a stored procedure. You need to provide the -clr param to read the generated CLR from a local DLL file.
  • uninstall: Removes what install creates.
  • check: Checks if everything is ready to start the proxy. Requires to provide the server DLL location (-reciclador), which can be uploaded using the upload command.
  • start: Starts the proxy. If not -local-port is specified, it will start listening on port 1337.

Once the proxy is started, you can plug in your proxychains ;)

Note: if your connection is not direct (e.g. proxies in between), you have to use the -no-check-src-port flag, so the server will only check the source address.

Conclusions

In this article we saw how to overcome a situation where the network restrictions only allow us to use a legitimate MSSQL service. This kind of isolation is extremely fragile and can be bypassed as long as the attacker can execute arbitrary code in the compromised service. The same approach can be translated to any other service that meets the conditions of loading custom code to hijack a client socket.

References