Hi, my fellow hackers. This is Rayofhope. I have over 5 years of experience and am currently working as a consultant with a Big 4 firm.
It’s Day 12 of posting all the PortSwigger labs, not just the solutions. I’ll break down why we take each step, because once the ‘why’ is clear, the ‘how’ becomes easy.
Let’s Start:
Before you go for this blog, make sure to read the Previous one
Link to First Blog: https://arayofhope7.medium.com/blind-sql-injection-with-conditional-responses-zero-to-hero-blind-injection-portswigger-dad0cab48d57
Video Walkthrough — You can watch the video or read the blog, totally up to you. But if you ask me, start with the video, then read the blog to connect all the dots.
Deadliest Virus in the world.
This is how the application looks:
As we already know, the cookies
parameter is vulnerable to Blind SQL Injection. So, let's intercept the request and explore what we can do with it.
Tried injecting a single quote ('
), and it returned a 200 OK response. However, there's no clear indication, such as a true or false statement, to confirm the vulnerability. It could be that the Pets
parameter is the one being affected.
Replaced the Tracking ID with rayofhope
, and it still returned a 200 OK response. However, rayofhope
doesn't appear anywhere in the response, and we aren't seeing any indicators that would confirm a true or false condition.
Used a single quote ('
), and it returned a 500 Internal Server Error. This indicates that the single quote might be breaking the SQL query, which suggests that the query could be part of custom developer-written code.
There we go, this time, we didn’t get an error. We used ‘ and 1=1, which is a universal true condition, confirming that the single quote is now properly balancing the SQL query. This suggests that the parameter could potentially be vulnerable to Blind SQL Injection.
But wait a minute, we tried
' and 1=2
, which is clearly a false condition (because, yeah, 1 is never equal to 2, not even in developer dreams). Yet, we still got a 200 OK response! No error, no change, no sign of rejection.This is where things get tricky. When both true (
' and 1=1
) and false (' and 1=2
) conditions return the same response, we can't rely on visual cues or error messages. Welcome to the world of Blind SQL Injection — where the injection is happening, but the app acts like nothing’s wrong. It's like trying to read a poker face... in the dark!
Inputs like
'--
or''
are valid parts of an SQL query, so the database doesn’t throw an error, the query still runs, and we get a 200 OK in response.But waiiih — we just saw that ‘’ is accepted, right? That might mean the input is being treated as part of a valid SQL string. Sooo… what if we try to concatenate something? Like ‘ || (SELECT ‘’)’
Oh nooo, we still got an error with '||(SELECT '')||'
! Where are we messing up? Wait a sec… maybe it's not using a string concatenation operator like ||
. Could it be an Oracle database? 🤔 Either way, getting an error now is actually a good thing — it means we’re poking the query in the right place!
We replaced dual
with rayofhope,
a table we know doesn’t exist, and boom, we got an error! But withdual
, we get a clean 200 OK. That’s the confirmation we needed: the input is being executed, the equation is balanced, and yes, it's vulnerable to Blind SQL Injection.
What’s Next? — See Me Doin’ It! 🎥🔥
Why does '||(select '' from users)||'
give an error even though the users
table exists?
Let’s understand this way: —
If the users
table has no rows (i.e., it's empty), the query SELECT '' FROM users
will return no results. This means a 200 OK response, as the query completes successfully without returning any rows.
But wait, '||(select '' from users where rownum = 1)||'
With this command, it returned 200 OK. Why? Why? Why? It is because ROWNUM
is a special keyword in Oracle SQL that returns the first row from a query result. WHERE rownum = 1
ensures that only the first row from the users
table is returned, and therefore, it returns 200 OK.
Since we know the users
table exists, we tried to confirm whether the administrator
user exists in the username
column.
We tried using the username rayofhope
, and in both cases, the application returned a 200 OK response. This suggests there's something off with our query — we're unable to verify true or false conditions, which is the foundation of Blind SQL Injection testing.
Used '||(SELECT CASE WHEN (1=0) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
, got 200 OK, which means the condition was false, so no error happened.
Used '||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
, got an error because the condition was true, causing a divide-by-zero error, which confirms that the SQL query is being executed and that the application is vulnerable to error-based Blind SQL Injection.
But what is CASE , TO_CHAR: —
Note: —
CASE WHEN (1=1) is a logical condition. Always true. If you replace this with
1=0
, the behaviour will change.
THEN TO_CHAR(1/0) forces a divide by zero error (crash) to confirm the condition is true.
ELSE ‘’ If statement is false, just return empty string quietly — no error, no noise.
END — If we use case when we use end to close it
Used '||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
. As per the above theory, you may now understand that if it is giving an error, it means the administrator
account exists.
Used '||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='rayofhope')||'
as the username, and it returned a 200 OK response. This means the condition was false (as the rayofhope
username doesn't exist), so the error was avoided and the query executed successfully without triggering any error.
- Now, we are sure that the admin user exists.
I used the following syntax.
'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' AND length(password)>1)||'
, and it returned an error. This means the condition was true, indicating that the length of the administrator
password is greater than 1.
I used the following syntax.
'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' AND length(password)>25)||'
, and it gave a 200 OK response. This means the length of the administrator
password is less than 25 characters. We can either manually try different values like 24, 23, 22, and so on to determine the password length, or simply use a tool like Burp Suite Intruder to automate the process.
Send the request to Intruder, select the parameter for the password length, and set the payload range from 1 to 25. Start the attack to automate the process of discovering the correct password length.
You can see that at payload number 20, the response behaviour changed. Remember one rule: brute-force attacks often rely on changes in response length or behaviour. This indicates that the password is 20 characters long.
Since this is an Oracle database, I’m using SUBSTR
instead of SUBSTRING
to manually extract the password. I used the following payload:
'||(SELECT CASE WHEN (SUBSTR(password,1,1)='r') THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
This helps confirm whether the first character of the password is 'r'
by triggering a divide-by-zero error. Manually finding all 20 characters is possible, but it could take hours, so let's automate the process!
Send the request to Intruder, and configure the payload positions:
- For the first payload position (to represent the character position in the password), select “Numbers” as the payload type and set the range from 1 to 20.
- For the second payload position (to guess the password character), select “Simple list” or “Brute forcer”, and include lowercase letters and numbers.
Once configured, start the attack to brute-force each character of the password, one position at a time.
We have the correct password now, let's collect it
This is the password: -7atmswkujntemnlzmxno