PicoCTF 2021
Writeups for PicoCTF 2021 Challenges
Web Exploit
Here are the web challenges that I completed in PicoCTF 2021
Get aHEAD
Description: Find the flag being held on this server to get ahead of the competition
Points: 20
Solution
The title of the challenge is interesting, the first instinct is that there is something hidden in the headers but let's look at Hints
Hint 1: Maybe you have more than 2 choices
Let's look at the HTML code for this
You can see there are two different methods used. "GET" and "POST" so the hint is probably referring to a third method and we can see "HEAD" popping out in the title. Let's try a "HEAD" request.
The above curl request returns the flag as expected.
Flag: picoCTF{r3j3ct_th3_du4l1ty_2e5ba39f}
Cookies
Description: Who doesn't love cookies? Try to figure out the best one.
Points: 40
Solution
The challenge name is "Cookies" so let's look at cookies.
The cookie set is name=-1
, let's try changing it to 1
There is a change in the page, So we just need to find the right cookie. Using Brute force manually at I name=18
found the flag.
Flag: picoCTF{3v3ry1_l0v3s_c00k135_88acab36}
Scavenger Hunt
Description: There is some interesting information hidden around this site. Can you find it?
Points: 50
Solution
By inspecting the source code I noticed this
Inspecting the CSS I noticed
Assuming I will find something in the JS file too, I found /* How can I keep Google from indexing my website? */
After a quick google search, I figured it's robots.txt where I found 3rd part and a hint
Working a lot with apache servers in the good 'ol days, I know it has to be .htaccess. Surpise, Surprise
I know Mac creates hidden files like .DS_Store
so let's look for that
Awesome!!
Flag: picoCTF{th4ts_4_l0t_0f_pl4c3s_2_lO0k_a69684fd}
Some Assembly Required 1
Points: 70
Solution
Judging by the title it probably has something to do with Assembly language, Since it's a web challenge. It's probably Web Assembly, Let's see if there is a WASM file imported.
There is and it also has the flag.
Flag: picoCTF{a2843c6ba4157dc1bc052818a6242c3f}
More Cookies
Description: I forgot Cookies can Be modified Client-side, so now I decided to encrypt them!
Points: 90
Solution
Looking at the website, This is a continuation of the "Cookies" challenge. So let's have a look
This time the page reads "Welcome to my cookie search page. Only the admin can use it!" and the cookie is
It's a base64 but when I decode it, it's still in gibberish, So it's encrypted. Let's see the Hint
Hint 1: https://en.wikipedia.org/wiki/Homomorphic_encryption
It's a Wikipedia page for a very interesting encryption method, It's more like an algorithm than an encryption formula. I found this to be the hardest challenge in the web, Reading articles about Homomorphic encryption and looking at other writeups I understand that we do not have to decrypt it to solve it, Homomorphic encryption allows you to perform operations on encrypted text. Also, I noticed that the letters "CBC" are oddly capitalized in the challenge description. So, It's a CBC bitflip. Meaning the encrypted text contains a bit that determines if it's admin or not, so probably something like admin=0
but I don't know it's position so I brute forced it, Here's the code
Flag: picoCTF{cO0ki3s_yum_82f39377}
Who are you?
Description: Let me in. Let me iiiiiiinnnnnnnnnnnnnnnnnnnn
Points: 100
Solution
Stage 1:
This is the only thing I see on the website, Let's look at the hints
Hint 1: It ain't much, but it's an RFC https://tools.ietf.org/html/rfc2616
So, I have to set special headers for this, After looking through Mozilla HTTP Header Docs I found a header User-Agent
which contains information regarding the browser, So I set it to "picobrowser"
Stage 2:
After changing the header related to the browser, This is what I see. After looking through headers that relate to information regarding the previous site. I found Referer
I changed it to the same URL.
Referer: http://mercury.picoctf.net:39114/
Stage 3:
After passing the previous stage, Now I see this. It is asking us to visit it from 2018. I know that there is a header for "Date", so I changed it to a date back in 2018.
Date: Date: Fri, 1 Dec 2018
Stage 4:
This time I need to set a header for "Do Not Track" which is DNT
DNT: 1
Stage 5:
This time, We need to access this from Sweden, My first instinct was VPN but so far we only used Headers to reach here. So I looked for another header that may reveal about location, I found X-Forwarded-For
I changed it to the first IP I found on googling "Sweden IP address"
X-Forwarded-For: 83.254.0.167
Stage 6:
This is an easy one, I need to change the Accept-Language
header, I set it to sv-en
Accept-Language: sv-en
Result:
Finally, we have our flag!
Flag: picoCTF{http_h34d3rs_v3ry_c0Ol_much_w0w_20ace0e4}
Some Assembly Required 2
Points: 110
Solution
Like the "Some Assembly Required 1" challenge this is also related to Web Assembly. I opened the imported WASM file in Chrome Developer Tools and here's what I found towards the end (Place where I found the flag for 1st part)
xakgK\5cNsmn;j8j<9;<?=l?k88mm1n9i1j>:8k?l0u\00\00
This looks like it's encrypted, To figure out the encryption, I used this website. It tries all standard XOR decryptions at once. Using XOR({'option':'Hex','string':'8'},'Standard',false)
the encrypted text outputs: picoCTF{ef3b0b413475d7c00ee9f1a9b620c7d8}T88T88
, Great!
Flag: picoCTF{ef3b0b413475d7c00ee9f1a9b620c7d8}
Super Serial
Description: Try to recover the flag stored on this website
Points: 130
Solution
As soon as you open the website you're prompted with a login screen. There isn't much to see on the login page. Let's have a look at the Hint
Hint 1: The flag is at ../flag
So we need to figure out a way to read that file, I tried to brute force for other pages on the URL and found /robots.txt
Notice "admin.phps". That is interesting, I tried the same extension for index.phps and it revealed the source code. Here's the PHP code for index.php
As expected from the name it's a deserialization exploit (see line 10). Here we can also see two more files. cookie.php
and authentication.php
let's look at authentication.phps first.
Here you can see that class access_log
contains an interesting function called __toString()
this is a well known exploit. If you are able to echo the class access_log
with a file, you can read its contents. So we need to pass the ../flag
into the access_log()
and find a place where it's echoed. Let's look at our last file cookie.php
Here's what I found towards the end. It was what we wanted. $perm
is unserializing the cookie and is echoed (die("Deserialization error. ".$perm);
) when the functions is_guest()
and is_admin()
are not found. So we need to pass access_log("../flag")
to it.
First, let's serialize and encode it in base64 so it gives the expected output on unserialize(base64_decode(urldecode()))
Here's our exploit
The cookie.php
reads from the cookie login
so we will set our exploit as
Now let's go to authentication.php
where the access_log
class exists.
There you go!
Flag: picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_66832978}
Most Cookies
Description: Alright, enough of using my own encryption. Flask session cookies should be plenty secure!
Points: 150
Solution
This is a similar problem to the first two "Cookie" problems. You'll need to set the right cookie to get the flag. This time we are provided with a source code file. Let's have a look
You can see the flag()
function for the display endpoint. It reads the very_auth
cookie and checks if it is "admin" if yes, then it will show us the flag. Flask cookies use JWT to created a signed token we need to find the secret. From the above source code we can see from lines 6 and 7 that the secret is a random word from cookie_names
list. If we can figure out which one is it then we can create a signed token for very_auth=admin
I found a python library called flask-unsign which has useful tools to decrypt the cookie and also brute-force for the secret. First, let us decrypt the cookie session
set by the server using
We can use this token to brute force for secret from the word list provided using a flask-unsign
and a text file (cookies.txt) consisting of the words from the list cookie_name
Then we can use the following command to find the secret from the list.
kiss
is our secret, Now I can just use the flask-unsign
module to create a signed token for {'very_auth': 'admin'}
Let's change the cookie value of session
to our generated token.
Flag: picoCTF{pwn_4ll_th3_cook1E5_478da04c}
Web Gauntlet 2
Description: This website looks familiar... Log in as admin Site: ... Filter: ...
Points: 170
Solution
This challenge is apparently a continuation of the "Web Gauntlet" challenge. Here, we are provided with two URLs one for login and another is for filters. Login is the page you'll need to exploit and bypass the form and gain access. "Filters" shows the list of keywords that are not allowed in the injection. Let's have a look at filters:
Filters: or and true false union like = > < ; -- /* */ admin
We can not use the above keywords in our injection. As mentioned on the login page. The server uses SQLite for the database. Here our username should be "admin" but we cannot use it due to filters so we need to find a way to enter the username=admin
without spelling it.
The query for username:
We can use the "CONCAT Operator" which is ||
using this operator we can concatenate strings, For example: 'Pico'||'CTF'
will give us 'PicoCTF'
We can use this for "admin": 'a'||'dmin'
The query for the password:
This is a tricky one because the filters pretty much block all mainstream operators like =><
and we can't even comment out the rest of the query because ;/**/
are blocked too. But SQLite has several interesting operators apart from these that can be used for boolean expressions. One such operator is "GLOB" which is similar to the "LIKE" operator in MySQL. The query password GLOB '*'
means that "a password with at least length one, which should return true, But in our case, we can't change the characters before query as in the query already has password = '
we can not get rid of the = '
Here's where we'll use a dirty trick. We can use an invalid expression that always returns true for everything: column='' GLOB '*'
this statement isn't technically invalid but returns true. Let's understand it with a better example.
Consider this, the query SELECT * FROM table WHERE name = "Jake" = False
is same as SELECT * FROM table WHERE name != "Jake"
which means the second =
actually returns True/False value of the first one.
The query SELECT * FROM table WHERE name = "Jake"
has two objects one where the rows meet the condition (True) and the other where the rows do not meet the condition (False). So our query password = '' GLOB '*'
basically translates to 'return True and False rows for password=''
which is always True
Final Injection
Username: a'||'dmin
Password: 'GLOB'*
Final Query
The character limit is 35 but our password is just 7 characters long! Let's enter it into the login page
Flag: picoCTF{0n3_m0r3_t1m3_fc0f841ee8e0d3e1f479f1a01a617ebb}
X marks the spot
Description: Another login you have to bypass. Maybe you can find an injection that works?
Points: 250
Solution
The link takes you to a login screen that reads "Only I know the password, and I don't use any of those regular old unsafe query languages!" Let's look at the hint.
Hint 1: XPATH
So, this is an XPath injection. This is not like any of the injections before. I tried bypassing the login with always true queries but they do not work, However, There is an interesting message that pops up when I enter an always true booleans like ' or 1=1 or 'a
It says "You're on the right path." I tried to change things a little bit and used a false query like ' and 1=2 and 'a'='a
Now it says "Login failure." This means that it's a Blind XPATH injection which means we have to figure out the username and password using queries. Here we can use "starts-wth()" operator. Which returns true if the passed characters are at the beginning of a document. We do not know the column names yet so we can use //*
which basically means check for all documents (columns). I tried testing it with ' or //*[starts-with(text(),'a')] or 'a'='b
which interestingly enough returned true. I tried again with ' or //*[starts-with(text(),'ab')] or 'a'='b
which returned false.
We can actually write a script that runs through all the combinations and stacks the successful characters upon success. Here's the Python script that I made
I tried running the above script several times before which returned with values like admin, bob, thisisnottheflag then I figured that we are supposed to look for the flag itself, not some password. so I started with the standard starting format picoCTF{
after running for a while, I finally got the full flag.
Flag: picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016ef}
Web Gauntlet 3
Description: Last time, I promise! Only 25 characters this time. Log in as admin
Points: 300
Solution
This is the same challenge as the "Web Gauntlet 2" but this time we are only allowed to use 25 characters for our injection. We can solve this using the exact same injection because last time our query was only 7 characters.
Username: a'||'dmin
Password: 'GLOB'*
Flag: picoCTF{k3ep_1t_sh0rt_30593712914d76105748604617f4006a}
Bithug
Description: Code management software is way too bloated. Try our new lightweight solution, BitHug.
Points: 500
Solution
This is the last challenge of Web Exploit and also has the highest points in the web section. Here we are also given source code.
This is basically a clone of GitHub it has features like webhooks, collaborators, etc. The flag is hidden at _/<username>.git
but we do not have access to read it. So we need to figure out a way to gain read access to the repo.
Let's go through the source code, Here's an interesting thing I found in auth-api.ts
You notice that requests from localhost (127.0.0.1) are given admin access and all endpoints from git-api.ts
can be freely accessed by admins.
The server has a webhook feature that we can use to send a request from the server to the server itself (SSRF). But this way we can't really read any data because there is no way to echo the data back from the endpoint that was accessed by web-hook.
However there is one thing we could do here, We can add ourselves as a collaborator to _/<username>.git
since admin has rights to all the endpoints we can send a request to git upload endpoint /:user/:repo.git/git-upload-pack
which is responsible for updating the repo on git.
The Plan
Create a payload for adding a collaborator to a repository
Create a webhook that sends a POST request to
_127.0.0.1:1823/<username>.git/git-upload-pack
Use this payload to push to
_/<username>.git
using webhook
Notice, we need to send this to that 1823 port because that's where the server is actually running locally. You can find this from the Dockerfile provided.
Step 1
Let us create a user with username: "abbas" and password: "abbas" on bithug. Now, create a repository named "abbas". We will clone this repository locally using
Now, we'll need to add a collaborator, we can find instructions on the repository page
Let us add a collaborator using
Do not push it yet, we need to capture this request. I am going to use Wireshark for this. After starting "capture" on Wireshark, git push origin @:refs/meta/config
Here is the http
stream from Wireshark, You'll notice that there is a POST
request. That's what we need.
Highlighted is the data we need, let us copy it in the Hex string format which should look like this
Now we need to get this to the webhook body, If you look at this part of the source code:
You'll notice that the body of the webhook is encoded in base64. So let's do that using a simple python script.
This will give us the body of our webhook, it should look like this
Step 2
Now we need to create a webhook, let us create a sample webhook first in our abbas
repository and see what requests are made.
This is the format we need to use for creating our webhook. We already have the body and the contentType
is application/x-git-receive-pack-request
which you can find on your Wireshark request.
The URL is supposed to be 127.0.0.1:1823
but it's tricky due to this
There are filters in place that prevent us from adding 127.0.0.1 as host and a port that's not 80. I solved this by hosting a flask app that redirects all the traffic to http://127.0.0.1:1823/_/abbas.git/git-receive-pack
here is the code for that
Note, it has to be a 307
redirect or else the incoming requests will be ignored.
Let's prepare our payload for creating a webhook
I am going to use curl so here's my request. Note that you'll need to grab the authentication cookie from your browser
Great, We are ready without webhook
Step 3:
Now we just need to trigger our webhook by doing a git push to our abbas/abbas
repository.
Perfect! Now let us try to open _/abbas
on Bithug.
Flag: picoCTF{good_job_at_gitting_good}
Last updated