Movimiento lateral via MSSQL: CLR y reutilización de sockets

Recientemente nuestro Red Team tuvo que operar en un escenario restringido, donde todo el tráfico desde la DMZ a la red principal se encontraba bloqueado salvo conexiones a servicios específicos como bases de datos y alguna aplicación web. En este artículo se describirán los detalles técnicos de cómo se solucionó esta situación, además de presentar mssqlproxy: una herramienta para convertir servicios Microsoft SQL en proxies sobre los que tunelizar tráfico.

Introducción y contexto

En un punto de la operación se consigue acceso como sysadmin a un Microsoft SQL de un servidor de la red principal al que, desde la DMZ, solo se permite tráfico al puerto 1433. Después de intentar infructuosamente obtener una shell inversa a través de xp_cmdshell, se identifica que las conexiones salientes de este servidor hacia la DMZ también se encuentran bloqueadas.

Entorno restringido

Para poder convertir el SQL Server en un pivote útil se decide optar por emplear las mismas técnicas de reutilización de sockets que se utilizan en exploiting [1]. La idea es reutilizar la propia conexión entre el cliente y la base de datos para tunelizar el tráfico, convirtiendo el servicio en un proxy socks funcional.

Reutilización del socket para tunelizar el tráfico hacia la red principal

El primer paso es ejecutar código en el contexto del proceso del SQL Server. Como los procedimientos almacenados extendidos van a quedar obsoletos en futuras versiones de MSSQL, se decide poner atención a las recomendaciones de Microsoft y utilizar CLR assemblies.

CLR Assemblies

El uso de CLR assemblies en la fase de post-explotación es, al igual que la reutilización de sockets, una técnica conocida. Este artículo de NetSPI [2] es un buen punto de partida para entender cómo implementar tus propios assemblies.

En este caso, se optó por desarrollar un assembly que fuera responsable de cargar una DLL (escrita en C) y de ejecutar toda la lógica del proxy. En el futuro, la siguiente versión hará todo el trabajo directamente desde el propio CLR, sin necesidad de una DLL extra.

Para invocar las funciones de nuestra DLL se hace uso de LoadLibrary (para cargar la DLL) y GetProcAddress (para obtener el puntero a la función). Sin embargo, no es posible llamar directamente a las funciones desde el assembly como se haría en C debido a que C# no soporta punteros a funciones [3]. Para poder hacer esta operación se debe de convertir el puntero en un objeto Delegate usando Marshal.GetDelegateForFunctionPointer.

Servidor: identificación del socket y proxyficación

Para reutilizar el socket del cliente, el primer paso es encontrarlo. Esta tarea se puede realizar desde diferentes enfoques, cada uno con sus ventajas y sus inconvenientes. En este caso se eligió un método poco intrusivo como es findport. Esta técnica consiste en hacer fuerza bruta al handler del socket (se trata de un integer), incrementando su valor hasta encontrar el socket objetivo. Esta misma técnica ha sido puesta en practica anteriormente por el Red Team en escenarios relacionados con MySQL y persistencia [4]. Si la conexión es directa es posible identificar el socket correcto comprobando la dirección IP y el puerto de origen; en el caso de que existan intermediarios (por ejemplo proxies, NAT/PAT, etc.) se puede comprobar la dirección de origen del intermediario siempre y cuando no existan más conexiones hacia la máquina objetivo.

Como todo el proceso es desencadenado por una setencia SQL, podemos obtener la dirección de origen de la petición con una consulta CONNECTIONPROPERTY('client_net_address'). De esta forma si la conexión es establecida desde un intermediario (un proxy, por ejemplo), es posible identificar la dirección IP correcta sin conocerla de antemano. Si la conexión es directa podemos suministrar el puerto de origen (el cual si podemos conocer).

La fuerza bruta para identificar el socket se puede resumir en el siguiente fragmento de código:

...
    // 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))) {
...

Durante la implementación de la DLL se observó que otros hilos del servidor interferían con el proceso, por lo que se optó por tomar el control total del socket. Para ello se duplica (WSADuplicateSocket) y se cierra el original (closesocket). Una vez se ha «secuestrado» el socket se procede a enviar un mensaje de bienvenida al cliente para anunciar que el proxy socks se encuentra preparado.

SOCKS5 es un protocolo simple que puede ser implementado en unas pocas líneas de código. El servidor socks utilizado es una versión simplificada y adaptada de este repo. No pretendemos reinventar la rueda, por lo que el cliente ha sido desarrollado utilizando impacket, aprovechando las capacidades de mssqlclient.py.

Cliente: mssqlclient.py 2.0

Subida y bajada de ficheros

En primer lugar, es necesario un método para subir ficheros al servidor para poder cargar posteriormente la DLL que contiene la lógica del proxy. Esto puede hacerse utilizando Ole Automation Stored Procedures para instanciar un objeto ADODB.Stream [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;

Aunque para este escenario no fuera necesario, se ha aprovechado también para añadir la funcionalidad de descarga de ficheros. Los archivos son leídos desde SQL utilizando OPENROWSET con la opción BULK [6].

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

Modo proxy

Relacionados con el modo proxy, se implementan cuatro comandos:

  • install: Crea el assembly CLR y lo enlaza al procedimiento almacenado. Es necesario utilizar el parámetro -clr para leer el CLR generado desde un fichero DLL local.
  • uninstall: Elimina lo que install crea.
  • check: Comprueba si todo está preparado para iniciar el proxy. Necesita conocer la localización de la DLL en el servidor (-reciclador). La DLL se puede subir utilizando el comando upload.
  • start: Inicia el proxy. Si no se especifica la opción -local-port se pondrá a la escucha en el puerto 1337.

Una vez el proxy se ha iniciado, sólo resta conectar a él con proxychains.

Nota: si la conexión entre el cliente y el servidor SQL Server no es directa (por ejemplo hay proxies entre ellos), se puede utilizar el parámetro -no-check-src-port para que el servidor sólo compruebe la dirección IP de origen en base a la consulta SQL.

Conclusiones

En este artículo se ha visto cómo se solventó una situación donde el movimiento lateral se veía limitado debido a restricciones de red que únicamente permitían conexiones legítimas a un servicio MSSQL. Este tipo de aislamiento es extremadamente frágil cuando se permite el acceso a servicios que de manera innata permiten de una forma u otra la ejecución de código arbitrario. El mismo enfoque utilizado para MSSQL puede ser trasladado a cualquier otro servicio que cumpla la condición de cargar código arbitrario que permita el secuestro del socket del cliente.

Referencias

[1] https://www.blackhat.com/presentations/bh-asia-03/bh-asia-03-chong.pdf
[2] https://blog.netspi.com/attacking-sql-server-clr-assemblies/
[3] https://www.codeproject.com/Articles/27298/Dynamic-Invoke-C-DLL-function
[4] https://x-c3ll.github.io/posts/Pivoting-MySQL-Proxy/
[5] https://docs.microsoft.com/en-us/sql/ado/reference/ado-api/stream-object-ado
[6] https://docs.microsoft.com/es-es/sql/t-sql/functions/openrowset-transact-sql