Appointment is the SQL-injection box in the Starting Point: a single login form, no password,
flag on success. The payload is the canonical ' or '1'='1, which almost everyone can recite. The
part worth slowing down on is why it works - what the application does with the two text fields,
and the precise shape of the query before and after the injection. Get that right and SQLi stops
being a memorised string and becomes something you can reason about on a form you have never seen.
Path to root, at a glance:
nmap-> port 80, Apache, a login page.- A single quote in the username breaks the query -> injectable.
- A tautology plus a comment bypasses auth -> flag.
Recon - nmap
┌──(r3vpwnx㉿r3vpwnx)-[~/CTF/HTB]
└─$ sudo nmap -Pn -A 10.129.124.108
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: Login
One service, and nmap even grabs the page title: Login. A login form with no other attack
surface is a near-explicit invitation to test the authentication logic itself.
Enumeration - what the form probably does
Before touching it, reason about the back end. A naive PHP login handler builds its query by gluing the two POST fields straight into a string:
$q = "SELECT * FROM users WHERE username='$user' AND password='$pass'";
“Logged in” means that query returned at least one row. There is no separate password check in
code - the database is the authority, and the password comparison lives inside the WHERE. That is
the whole weakness: if I can make the WHERE clause true without knowing a password, I am in.
Foothold - SQL injection auth bypass
First, confirm the field is injectable by breaking the string with a lone single quote:
Username: admin'
An error (rather than a plain “wrong credentials”) means my ' reached the SQL parser and left it
with an unterminated string - the input is not being escaped. Now supply a username that closes the
string, adds an always-true condition, and comments out everything after it:
Username: admin' or '1'='1'-- -
Conceptually the query becomes:
SELECT * FROM users WHERE username='admin' AND password='test' OR 1=1#'
password='test'is falseOR 1=1is always true#/-- -comments out the remainder
Operator precedence does the rest: AND binds tighter than OR, so the row test collapses to
(username='admin' AND password='test') OR 1=1, which is false OR true = true for every row.
The query returns the whole users table, the handler sees “at least one row,” and treats me as
authenticated. (-- - is the MySQL comment token - it needs the trailing space, which the extra -
guarantees.)
Loot - the flag
Congratulations!
Your flag is: [flag redacted]
Detection and hardening
- Parameterise the query. Prepared statements (
WHERE username=? AND password=?) send the SQL and the data on separate channels, so input can never become syntax. This is the fix - not escaping, not a blacklist. - Do not let authentication be a row count. Fetch the user by name, then verify a hashed
password (
password_verify) in application code, so a tautology that returns rows still fails the credential check. - Fail closed and quiet. A single quote should not surface a database error; generic failures deny the attacker the oracle that confirmed injectability here.
- Blue team:
' or '1'='1,-- -,#,UNION SELECTin POST bodies are high-signal WAF/log patterns - but treat the WAF as telemetry, not as the fix.
Lessons
- SQLi is a reasoning exercise, not a payload. Model the query the server builds, then craft input that changes its meaning rather than its data.
- The database is doing exactly what it was told. The injection is valid SQL; the bug is that untrusted text was allowed to be SQL at all.
- Prepared statements end the class. Every mitigation short of separating code from data is a speed bump.