Skip to content

Second-order SQLi

The injection point and the trigger are different requests. You submit a payload through one form, the application stores it (safely-quoted), and later uses the stored value in a different query that doesn’t re-quote it. The second query executes the injection.

  1. Find an input that gets stored in the database (registration, profile update, comment, filename).
  2. Submit a payload that survives the initial insert as a literal string.
  3. Trigger the second query that reads the stored value (login, profile view, file lookup, search).
  4. Injection fires.

Common payload shapes:

admin'-- -
admin' UNION SELECT password FROM users-- -
admin'+(SELECT 1 FROM users WHERE pg_sleep(5))+'

The first-order test (inject ', look for an error) only checks the immediate response. Second-order vulnerabilities have:

  1. Input request: POST /register with username=admin'-- - - succeeds with HTTP 200, no error visible.
  2. Storage: Application uses parameterised insert, so the literal string admin'-- - lands in the database safely.
  3. Trigger request: POST /login with the same username. The login flow does SELECT * FROM logs WHERE created_by = '<retrieved_username>' - concatenating the stored value without re-quoting.
  4. Injection fires at step 3, not step 1.
# Step 1: Register
POST /register
username=admin'-- -&password=test123
# Server: INSERT INTO users (username, password) VALUES (?, ?) -- safe
# Stored username: admin'-- -
# Step 2: Login
POST /login
username=admin'-- -&password=test123
# Server: SELECT * FROM users WHERE username='admin'-- -' AND password='...'
# Comment removes password check → logged in as admin (or whichever account already exists)
Input requestStorageTrigger requestAffected query
Update profile nameusers.nameView dashboardActivity-log query joins on name
Upload filefiles.original_nameDownloadLookup by original_name
Set password (yes, sometimes)users.passwordLogin or password changeAudit log
Add commentcomments.bodySearchLIKE clause on body
Set biousers.bioPublic profileSometimes used in templating queries

Manual:

  1. Enumerate every field that gets stored.
  2. For each, register/submit with a benign-but-detectable payload like ' OR pg_sleep(0)-- - - no behavioural impact.
  3. Walk through the application using the account/data that contains the payload.
  4. Look for any page where response time changes, errors appear, or behaviour shifts. That page is the trigger.
  5. Replace the benign payload with pg_sleep(5) or full extraction.

Automated:

sqlmap has limited support - --second-url lets you specify a separate URL to fetch after each injection request:

Terminal window
sqlmap -u "https://<TARGET>/register" \
--data="username=test*&password=test" \
--second-url="https://<TARGET>/login" \
--technique=T --dbms=mysql

This is fragile - it works for simple register-then-login flows but not for multi-step workflows.

  • Second-order is harder to detect but often easier to exploit once found, because the trigger query is frequently the most privileged one (auth, admin actions).
  • Look for second-order in any application that does post-processing of stored user input: audit logs, search indexers, batch jobs, report generation.
  • Stored procedures and triggers can also fire second-order injection from data inserted by an unrelated path.