Adapted from SEED Labs: A Hands-on Lab for Security Education.
SQL injection is a code injection technique that exploits vulnerabilities in the interface between web applications (web apps) and databases. The vulnerability is present when user inputs are not properly handled within the web app before being sent to a back-end database.
Many web apps take inputs from users, and then use these inputs to construct SQL queries, which retrieve information from a database. Web apps also use SQL queries to store information in the database. These are common practices in the development of web apps. When SQL queries are not carefully constructed, SQL injection vulnerabilities can occur. SQL injection is one of the most common attacks on web apps.
In this lab, we have created a web app that is vulnerable to SQL injection attacks. Our web app includes examples of common mistakes made by many web developers. Your goal is to find ways to exploit these SQL injection vulnerabilities, demonstrate the damage that can be done by these attacks, and master the techniques that can help defend against these types of attacks.
This lab covers the following topics:
SELECT
, UPDATE
, etc.04_sqli/
of our class’s GitHub repository.We have developed a web app for this lab, which runs inside docker containers. There are two containers in the lab setup, one for hosting the web app, and the other for hosting the database.
The IP address for the web app container is 10.9.0.5
,
and the URL for the web app is: http://www.seedlabsqlinjection.com
Your VM should already be configured to have this hostname/IP address mapping in the /etc/hosts
file:
10.9.0.5 www.seedlabsqlinjection.com
If you experience issues trying to address the web app by its human-readable hostname, please verify your settings.
If you need to update this information, add the above entry to the /etc/hosts
file.
(You need root privileges to modify this file.)
Please ensure that you have the class repo cloned locally.
Once this is done, navigate to the 04_sqli/
directory.
For example:
$ cd ~
$ git clone https://github.com/reesep/csci476-code.git code # name the local clone 'code'
$ cd /home/seed/code/04_sqli
We will make use of Docker and Compose to make working with containers easy.
# First, build the container
$ docker-compose build # Build the container image
# Next, start/stop the container(s) as needed
$ docker-compose up -d # Start the container (-d runs container in the background; i.e., detached)
$ docker-compose down # Shut down the container
In general for our labs, we will create and start containers that will run in the background
(i.e., use the -d
flag when bringing your container up).
At times we may need to run commands on a container — docker makes it pretty easy to attach to a container running in the background and get a shell on that container.
To run commands on a specific container, we first need to use the docker ps
command to find out the ID of the container,
and then we can use docker exec
to start a shell on that container.
(I told you this would be easy!)
$ docker ps -a # Show all containers (default shows just running)
$ dockps # Show active containers using custom formatting for docker ps
$ docksh <id> # Connect to container with <id>
### Examples ###
# The following example shows how to get a shell inside hostC
$ dockps
b1004832e275 hostA-10.9.0.5
0af4ea7a3e2e hostB-10.9.0.6
9652715c8e0a hostC-10.9.0.7
# Attach to the container with an ID that starts with "96"
$ docksh 96
root@9652715c8e0a:/#
# NOTE: If a docker command requires a container ID, you do not need to type the entire ID string.
# Typing the first few characters will be sufficient so long as it can uniquely identify a container.
Docker/Compose Aliases. For convenience we provide a number of aliases for the commands above. Feel free to use them (or don’t).
### see docker aliases ###
$ grep docker ~/.bashrc
Troubleshooting. If you encounter problems when setting up the lab environment, please read the “Common Problems” section of the SEED Manual for Containers for potential solutions. If you still can’t get things figured out, please connect a member of the course staff.
Containers are usually meant to be disposable, so once they are destroyed, all the data inside the containers is lost.
For this lab, we want to keep the data in the MySQL database (i.e., so that we do not lose our work when we shutdown our container).
To achieve this, we have mounted the mysql_data
folder on the host machine to the /var/lib/mysql
folder inside the MySQL container.
The folder is created inside of
04_sqli/
automatically once the MySQL container runs once.
This folder is where MySQL stores its database. Thus, even if the container is destroyed, data in the database will persist since it actually resides on the host. If you do want to start from a clean database, you can remove this folder:
$ sudo rm -rf mysql_data
We have created a web app, which is a simple employee management application.
Employees can view and update their personal information in the database through this web app.
There are two main roles in this web app:
Administrator
is a privileged role and can manage each individual employees’ profile information;
Employee
is a normal role and can view or update only their own profile information.
All employee information is described in a single table stored within the database.
(You will get a chance to explore the format of this table in Task 1.)
At various times throughout this lab it may be helpful to know the credentials (username / password) of certain employees:
In real-world applications, it may be hard to check whether your SQL injection attack contains syntax errors (servers typically will not return any meaningful error messages if your injected payload raises errors). To validate your payload, you can copy SQL statements from PHP source code to the MySQL console and test them there.
Assume you have the following SQL statement, and the injection string is ' or 1=1;#
.
SELECT * from credential
WHERE name='$name' and password='$pwd';
You can replace the value of $name
with your payload and test it using the MySQL console.
This approach can help you construct a valid payload before launching the real attack.
This lab has been tested on the pre-built SEED VM (Ubuntu 20.04 VM).
The objective of this task is to get familiar with SQL commands by experimenting within the provided database.
The data used by our web app is stored in a MySQL database, which is hosted on our MySQL container.
We have created a database called sqllab_users
, which contains a table called credential
.
The table stores the personal information (e.g., eid, password, salary, ssn, etc.) of every employee.
In this task, you will interact with this database from the mysql
command line interface (CLI) to get familiar with SQL queries.
First, make sure your containers are running (see the “Environment Setup” section above).
Next, get a shell on the MySQL container:
$ dockps
# ...ids of containers...
$ docksh <id-of-mysql-container>
Then start the mysql
client program to interact with the database.
The username is root
and password is dees
.
# Inside the MySQL container
$ mysql --user=root --password=dees
After you log in, you can create a new database or use an existing one.
You can see which databases currently exist using the show databases
command.
We have already created the sqllab_users
database for you, so for this task you just need to load this existing database using the use
command.
To show what tables exist within the sqllab_users
database,
you can use the show tables
command to print out all the tables of the currently selected database.
After running the commands above, you need to use a SQL command to print all the information for the employee Alice.
Please provide proof of your results (e.g., command line output, screenshot).
A SQL injection is basically a technique through which attackers can execute their own (malicious) SQL statements; the input, which is specially crafted to inject attacker-chosen SQL statements, is generally referred to as the payload. Through the injected SQL statements, attackers can steal information from the victim database, or even worse, they may be able to make changes to the database (e.g., update or delete information). Our employee management web application has SQL injection vulnerabilities that mimic the mistakes frequently made by web app developers.
To explore SQL injection vulnerabilities in this task, we will use the login page found at http://www.seedlabsqlinjection.com/. The login page is shown in the figure below. As is typical in authentication, the web app prompts users to provide a username and a password. The web app then authenticates users based on these two pieces of information. Presumably, only employees who know their own username and password should be able to log in. Your objective throughout this task is to exploit a SQL injection vulnerability in the login page to successfully log into the web app without knowing any valid employee credentials.
To help you get started with this task, we explain at a high level how authentication is implemented in the web app.
Snippets and pseudocode from the the actual PHP source code that is used to conduct user authentication are shown below.
You will likely want to review the complete source code more carefully:
04_sqli/image_www/code/unsafe_home.php
.
The user’s credentials are sent via an HTTP GET request when the form is submitted. Upon receiving the request, the server code extracts the provided username and password.
$input_uname = $_GET['username'];
$input_pwd = $_GET['password'];
$hashed_pwd = sha1($input_pwd);
The SQL statement (middle section) selects personal employee information such as id, name, salary, ssn, etc. from the credential
table.
Notice that the SQL statement is partially constructed from two variables in the source code, input_uname
and hashed_pwd
,
where input_uname
holds the string typed by users in the username field of the login page,
and hashed_pwd
holds the SHA1 hash of the password typed by the user.
$sql = "SELECT id, name, eid, salary, birth, ssn, address, email, nickname, password
FROM credential
WHERE name= '$input_uname' and password='$hashed_pwd'";
$result = $conn -> query($sql);
The program then checks whether any record matches the provided username and (hashed) password; if there is a match, the user is successfully authenticated, and is given the corresponding employee information. If there is no match, the authentication fails.
// The following is primarily pseudocode. You won't find most of these exact lines in the
// actual source code linked above, but the idea is what is important.
if (id != NULL) {
if (name == 'admin') {
return All employees information;
} else if (name != NULL) {
return employee information;
}
} else {
Authentication fails;
}
In this task you need to log into the web app as the administrator from the login page, which will enable you to see the information of all employees.
We assume that you do know the administrator’s account name, which is admin
, but you do not know the password.
You need to decide what payload to enter into the “Username” and/or “Password” fields to succeed in your attack. Your lab report should include the payload you used for the username and password, as well as a screenshot that show you were able to login successfully.
In this task you need to repeat Task 2.1, but you need to do it without using the login webpage.
You can use command line tools, such as curl
, which can be used to send HTTP requests.
Using curl
is probably the quickest and easiest way to go, but there are also other great libraries that are fun to learn; e.g., requests
in Python, httpparty
in Ruby.
One thing that is worth mentioning is that if you want to include multiple parameters in HTTP requests,
you need to put the URL and the parameters between a pair of single quotes;
otherwise, the special characters used to separate parameters (such as &
) will be interpreted by the shell program, changing the meaning of the command.
The following example shows how to send an HTTP GET request to our web app via curl
, with two parameters (username
and password
):
$ curl 'www.seedlabsqlinjection.com/unsafe_home.php?username=alice&password=mypass123'
If you need to include special characters in the username
or password
fields, you need to encode them properly, otherwise the unencoded characters can change the meaning of your requests.
If you want to include a single quote in any of those fields, you should use %27
;
if you want to include a space, you should use %20
.
In this task, you do need to handle HTTP encoding while sending requests using curl
.
There are lots of great resources on the Internet to learn more about encoding characters (e.g., URL Encoded Characters cheatsheet, Online URL Encode/Decode tool).
In the above two attacks, we only view arbitrary information from the database;
it could be interesting to explore whether it is possible modify the database using the same vulnerability in the login page.
One idea is to use the SQL injection attack to turn one SQL statement into two, with the second one being an update or delete statement.
In SQL, a semicolon (;
) is used to separate two SQL statements.
With this idea in mind, now try to craft your input so that you can run two SQL statements via the login page.
(Spoiler alert!) It turns out that there is a countermeasure to prevent this specific issue known as a prepared statement. Feel free to do some independent research on this topic and describe your discovery in the lab report.
If a SQL injection vulnerability happens to an UPDATE statement, the damage could be quite severe, because attackers can use such SQL injection vulnerabilities to modify databases. In our employee management web app, there is an “edit profile” page (see figure below) that allows employees to update their profile information, including nickname, email, address, phone number, and password. To access this page, employees need to log in first.
When employees update their information through the edit profile page, the following SQL UPDATE query will be executed.
The PHP code implemented in /var/www/SQLInjection/unsafe_edit_backend.php
file is used to update employee’s profile information.
$hashed_pwd = sha1($input_pwd);
$sql = "UPDATE credential SET
nickname='$input_nickname',
email='$input_email',
address='$input_address',
password='$hashed_pwd',
PhoneNumber='$input_phonenumber'
WHERE ID=$id;";
$conn->query($sql);
As you can see in the edit profile page, employees can only update their nicknames, emails, addresses, phone numbers, and passwords; they are not authorized to change their salaries.
Assume that you (Alice) are a disgruntled employee, and your boss did not increase your salary this year.
You want to increase your own salary by exploiting the SQL injection vulnerability in the “edit profile” page.
Please demonstrate how you can achieve this kind of update.
We assume that you do know Alice’s credentials and that salary information is stored in a column named salary
. Your lab report should include the payload you used for the nickname field, as well as a screenshot that show you were able to edit your information successfully.
After increasing your own salary, you decide to punish your boss Samy. You want to reduce their salary to 1 dollar. (Dont worry, Samy is a total jerk and they deserve this!)
Please demonstrate how you can achieve this update. Your lab report should include the payload you used for the nickname field, as well as a screenshot that show you were able to edit Samy's information successfully.
After changing Samy’s salary, you are still disgruntled, so you want to change Samy’s password to something that you know, and then you can log into their account and do further damage. Please demonstrate how you can achieve this. Your lab report should include the payload you used for the nickname field, as well as a screenshot that show you were able to change Samy's password successfully.
You need to demonstrate that you can successfully log into Samy’s account using the new password.
One thing worth mentioning here is that the database stores the SHA1 hash value of passwords instead of the plaintext password string.
You can look at the unsafe_edit_backend.php
code to see exactly how the password is being stored.
You are welcome to generate a sha1 hash of the new password however is most convenient for you. For example, you can do this directly in an SQL statement in MySQL or at the MySQL CLI, using a normal command line tool such as
openssl
, or using an online sha1 calculator.
The fundamental problem of the SQL injection vulnerability is the failure to clearly separate code from data. When constructing a SQL statement, the program (e.g., PHP program) knows which part is data and which part is code. Unfortunately, when the SQL statement is sent to the database, the distinction is lost; the boundaries that the SQL interpreter sees may be different from the original boundaries that were intended by the developers. To solve this problem, it is important to ensure that the view of the boundaries between code and data are consistent in the server-side code and in the database.
The folks at PortSwigger (the makers of Burp Suite) do a nice job of summarizing why parameterized queries are an effective countermeasure:
The most effective way to prevent SQL injection attacks is to use parameterized queries (also known as prepared statements) for all database access. This method uses two steps to incorporate potentially tainted data into SQL queries: first, the application specifies the structure of the query, leaving placeholders for each item of user input; second, the application specifies the contents of each placeholder. Because the structure of the query has already been defined in the first step, it is not possible for malformed data in the second step to interfere with the query structure. You should review the documentation for your database and application platform to determine the appropriate APIs which you can use to perform parameterized queries. It is strongly recommended that you parameterize every variable data item that is incorporated into database queries, even if it is not obviously tainted, to prevent oversights occurring and avoid vulnerabilities being introduced by changes elsewhere within the code base of the application.
Below, we provide an example of how to write a prepared statement in PHP.
The first code snippet shows code that is vulnerable to SQL injection attacks:
$sql = "SELECT name, local, gender
FROM USER_TABLE
WHERE id = $id AND password ='$pwd' ";
$result = $conn->query($sql))
This code can be rewritten as follows:
// Prepare the query
$stmt = $conn->prepare("SELECT name, local, gender
FROM USER_TABLE
WHERE id = ? and password = ? ");
// Bind parameters to the query
$stmt->bind_param("is", $id, $pwd);
$stmt->execute();
$stmt->bind_result($bind_name, $bind_local, $bind_gender);
$stmt->fetch();
Using the prepared statement mechanism, we divide the process of sending a SQL statement to the database into two steps.
The first step is to only send the code part,
i.e., a SQL statement without the actual the data.
This is the prepare step.
As we can see from the above code snippet, the actual data are replaced by question marks (?
).
After this step, we then send the data to the database using bind_param()
.
The database will treat everything sent in this step only as data, not as code anymore.
It binds the data to the corresponding question marks of the prepared statement.
In the bind_param()
method, the first argument "is"
indicates the types of the parameters:
"i"
means that the data in $id
has type integer
, and "s"
means that the data in $pwd
has type string
.
In this task, we will use the prepared statement mechanism to fix SQL injection vulnerabilities. For the sake of simplicity, we have the patched version that uses prepare statements: You will need to take this code, and paste it into the 04_sqli/image_www/code/defense/unsafe.php file. If you point your browser to the following URL — http://www.seedlabsqlinjection.com/defense/ — you will see a page similar to the original login page for the web app. This page allows you to query an employee’s information, but you need to provide the correct user name and password.
The data submitted via this page will be sent to the server program getinfo.php
, which invokes a program called unsafe.php
.
The SQL query inside this PHP program is vulnerable to SQL injection attacks.
Your objective for this task is to update unsafe.php with the correct code that uses prepare statements (given above), and verify that your SQL injection attack no longer works.
There are two ways that you can make your changes and validate that your fixes work.
You can modify the program source code on the host (i.e., within your SEED VM). After you are done making changes to the code on the host, you need to rebuild and restart the container, or the changes will not take effect.
You can modify the file within the container while the container is running.
On the running container, the unsafe.php
program is inside /var/www/sql_injection/defense/
.
Both approaches have advantages and disadvantages.
In the first approach, you potentially need to rebuild/restart your container multiple times as you write and validate your code.
In the second approach, the container environment is quite minimal;
we have installed a very simple text editor called nano
,
but you’ll have to use apt install ...
to install other programs if you want them (e.g., vim).
Another drawback of the second approach is that any changes you make within the running container will be discarded after the container is shutdown and destroyed.
Thus, you need to ensure that you copy any work that you want to save from the container to your host before the container is shutdown and destroyed.
Submit your assignment as a single PDF to the appropriate D2L dropbox