sqlmap couldn't detect it because we tricked it

Posted on Sun 06 December 2015 in tools

Nothing works before demo time

Back in the spring of the year 2015, Justin Whitehead and I decided it was time to defy the demo gods, and made it to a few conferences to talk about Spanking the Monkey, or How Pentesters Can do It Better. We traveled far and beyond to the land of Texas for BSides Austin where knight Justin got slashdotted and I lost a few days of life under the stress of having the demo failing minutes before the presentation. We then rode East to Kansas City for SecKC Apr 2015. The road to Kansas was dangerous, with car chases and DoS-inducing barbecues, but thanks to our persistence and desire to taste the liquid gold of KC we succeeded in our quest. Unfortunately, I had to travel to foreign lands after this, and Chester Bishop took my place to ride with Justin once again and conquer DerbyCon 5.0 and GrrCON 2015 together. After this mighty victory, the quest had reached a mature state, tales and songs were written about it, and it was framed and placed up on the wall to be shared with others.

Enough middle age crap. Let's talk about SQL Injection.

The talk in question included a part where sqlmap was unsuccessful detecting an instance of SQL Injection we had coded in a web application. We did code the application with some protections against tools, in part not make it too easy in part to force the tools to make a lot of noise and prove our point on how necessary it is to know the ways of manual testing -which was the whole motivation for the talk-.

Something we said during the talk was that thanks to sqlmap being open-source, we could investigate what was preventing it from finding the SQLi. That's what I'll do here.

If you want to follow along, install the vulnerable app following the instructions from the repo.

This is the sqlmap commands we used in the demo:

sqlmap -u 'http://www.angry_monkey.com/profiles/42*/antonio' --dbms=mysql --level=5 --risk=3

It's using a * custom injection marker that tells sqlmap where to inject payloads, ideal for rewritten URLs such as that one. This command generated about 20K requests to the web server.

Let's start by looking at the web app's vulnerable code in profile.php:

<?php
function anti_sqli_filter($input) {
    $result = $input;
    $result = str_replace('\'', '\\\'', $result); // filter with replacement, payload will have to avoid single quotes
    $result = str_replace(' ', '', $result); // Filter 'space'. Bypass with ASCII chars %09 (tab), %0a (new line), %0b, %0c or %a0.
    return $result;
}

if( isset($_GET['id']) ) {

    $id = anti_sqli_filter($_GET['id']); // bad escaping
    $username = anti_sqli_filter($_GET['username']); // bad escaping

    $sql = "SELECT * FROM users WHERE id='$id' AND us3rn4m3='$username'";
    $result = mysqli_query($conn, $sql);
    if (!$result) {
        echo "<!--" . $sql . "-->\n"; // info leak
        echo mysqli_error($conn);
    }

  // [...]
?>

The classic example of SQL Injection. It can be triggered with something like curl "http://www.angry_monkey.com/profiles/42%5C/antonio" because the first str_replace() won't let us use the classic single quote payload:

"SQL Injection finding with cURL"

How come sqlmap didn't find it then? I tried by modifying the command like so:

sqlmap -u 'http://www.angry_monkey.com/profiles/42*/antonio' --dbms=mysql --level=5 --risk=3 --suffix="\\" --tamper=space2randomblank.py

This command was geared towards circumventing the filtering done by anti_sqli_filter() by adding a \ suffix to trigger the SQLi and replacing spaces with something else (I hardcoded space2randomblank.py to be tabs).

This last command uses heuristics that return a potential positive. The heuristic test in question contains the payload )(,'")((.)", which does indeed trigger the error, as seen below.

Heuristics payload

As pointed in the image, the single quote is escaped by anti_sqli_filter(), and the trailing \ triggers the SQLi.

Positive heuristics but no SQL Injection found after thousands of requests. I thought I was getting closer to the solution when in fact, I was far from it. I was going to start digging into sqlmap when I decided to try something else first. What if the fact that there are two parameters in the SQL query is driving sqlmap off? Let's remember what the query looks like when the SQLi is triggered with a \:

"Triggered SQLi"

As it is this results in a 500 with the MySQL error in the body. The logic in the code is such that a MySQL error is required to exploit this, specifically mysqli_query() has to return FALSE, otherwise proper sanitation is carried out with mysqli_real_escape_string(). This last part can be better observed in profile.php.

We have to have an error, and we cannot use single quotes. Therefore we have to use a \ to trigger the error. And then? We have to complete the query by injecting into the value of us3rn4m3 at the same time we trigger the error. This is the key. sqlmap cannot figure out that it needs to interact with two parameters simultaneously.

With that into account, the answer to what command sqlmap needs to exploit the SQL Injection is this:

sqlmap -u "http://www.angry_monkey.com/profiles/42\/*antonio" --tamper=space2randomblank.py

Mystery uncovered. Not so exciting anymore. I was kind of hoping for a bug in sqlmap and a contribution to fix it. Oh well. What about implementing the feature of considering two parameters simultaneously? Maybe something like that exists in sqlmap that I'm not aware of?

Let me know if you have any comments.