You are given a search tool which finds jokes which contain your search term.
The form submission returns a 302 redirect to another page with an encrypted get param already attached. This encrypted param must contain your input in some way.
They look something like this:
The data is url encoded and base 64 encoded. Undoing those will give you the raw encrypted bytes.
If you try out several different inputs you will notice a pattern. The prefix of the encrypted param does not change as your input gets longer. The first 32 bytes are always the same. This means that some text is consistently getting prepended to our text. As well, changing the first character of your input does not change the end of the encrypted text.
This indicates that the crypto mode used to encrypt this text is not using a chain block cipher, so this means that every block is independent (ECB).
Originally, I did not quite catch that this. If you mess with the encrypted text and send it to the server it will tell you that the padding is incorrect. This led me to believe that a padding oracle attack was necessary. This isn’t the case.
The block size is 16 bytes. We can observe this by adding 1 character at a time and we can see that the third set of 16 characters stops changing once we add enough characters, it gets locked in.
We need 10 characters to fill the third block. (Adding an additional character past 10 does not change bytes in index [32:48).
Brute Forcing to Find The Appended Text
Text is being appended to the input we pass. We can tell because even if we pass exactly 10 characters to fill the third block, there’s still more blocks after that. What is the decrypted text in those blocks?
If we submit an input with only 9 characters then the last character in the third block will contain the first character of the text that is being appended to our input!
Let’s say you submit an input of 9 letter ‘a’, and you see that the value of the third block is X. Now if you try to submit an input starting with 9 letter ‘a’ and a random last character, you can compare it with X to see if you guessed correctly. Using this you can decrypt this character.
The character is a %.
Normally we would be able to extend this procedure and decrypt the entire text that is being appended. Unfortunately you will find that because the input you enter is being escaped (” turns into \”), that it is impossible to do guess and check if any of the appended characters contain characters that would be escaped and that turns out to be the case for the next character.
We can only decrypt this single % character from this procedure.
This % shows us that our text is probably being matched using a SQL LIKE. We could have inferred this instead of doing this decryption, but whatever.
SQL Injection by inserting encrypted text
We know that the structure looks like this:
SELECT text from jokes where text LIKE ‘%user_input%’;
The reason we can’t simply type in sql injection into the input box is because the app is doing sanitation of the input on the server side, all quotes are being escaped so you can not break out of the single quotes.
The escaping is being done before the encryption. This means that if we can get an encrypted block for the part of the sql query we want to inject, then we can simply insert our block and it will decrypt to the query we want.
The trick is crafting exactly the blocks we want. This is just a matter of making our input exactly the right size with padding before.
Let’s say we want to inject this (This is actually what will give us the password. I found the users table by doing another similar command):
‘ UNION ALL SELECT password FROM users;#
We want to find the exact encrypted blocks which could contain this data.
First we need to fill the third block since we know that it’s always going to already be partially filled. We know that it takes 10 characters to fill the remainder of the third block. We know that the single quote that we want in our query is going to get escaped and that’s no good. This means that we want the backslash to not be in our encrypted block.
To accomplish this, we will prepend 9 spaces to our query string so that the third block will be filled entirely before we start a fresh block for our sql injection string. Why 9? Because the single quote in our string will get escaped with a backslash and it will end up being the 10th and final character of the third block, which makes the single quote in our fourth block unescaped 🙂
This means that if we send the following string:
” ‘ UNION ALL SELECT password FROM users;#”
We can get the encrypted blocks we want.
Ignoring the data that ends up in the third block which has our spaces and the backslash, we need the 4th, 5th, and 6th blocks which will contain our sql injection. We extract those out of the query param.
Then we do an ordinary query with a 10 character input (to fill the third block again) and just paste our encrypted blocks in right after the third block and the password is revealed.
# For a given query string find the encrypted query param
s = urllib.quote_plus(s)
result = subprocess.check_output(‘curl -I -u natas28:JWwR438wkgTsNKBbcJoowyysdM82YjeF http://natas28.natas.labs.overthewire.org?query=’ + s + ‘ 2>/dev/null’, shell=True)
key = “Location: search.php/?query=”
pos = result.find(key)
pos += len(key)
start = pos
while result[pos] != ‘\n’:
pos += 1
encoded = result[start:pos]
decoded = urllib.unquote(encoded).decode(‘utf8’)
# Prepend 9 spaces to fill third block. The third block
# actually has room for 10 characters but the backslash
# escape character from the single quote will
# fill in the 10th remaining character in the third block!
apos = (” ” * 9) + “‘ UNION ALL SELECT password FROM users;#”
# Just a calculation to find how many blocks our encrypted
# sql injection stirng occupies
blocks = (len(apos) – 10)
while blocks % 16 != 0:
blocks = blocks + 1
blocks = blocks / 16
inject = q(apos)
# Create an ordinary query that ends the third block
# cleanly with the sql query’s single quote still open
spaces = ” ” * 10
base = q(spaces)
# Patch in our encrypted blocks that contain our sql injection
b64 = base64.b64encode(base[0:48] + inject[48:(48 + 16*blocks)] + base[48:])
url = urllib.quote_plus(b64)
# Prints a query param which gives us the password