Pwny IDE is a good-looking advanced IDE that lets you write HTML/CSS code and watch the changes in real-time! Pretty cool right?
JS is not supported :)
This challenge was one of the unsolved challenges in UIUCTF 2021, and it was also pretty fun! The author decided to not release the solution after the contest and he also ran a bounty program ($50 to the first solver). Luckily we could solve it after the CTF and we won both the contest and the bounty!
Features of this IDE:
This IDE is beautiful!!!
When we register on the website, it assigns us a random uid, after that it creates a folder for us in file:///files
folder. It will be used to save our code.
We can save and share our code! It will be saved in a file named file:///files/${uid}/file
and anybody can access it at http://website/workspace/${uid}
.
Some facts about this endpoint:
text/html
The FTP server is written in js and uses ftpd. It's listening on 127.0.0.1:21
so we can't access it from outside. It uses HTTP server's users for authentication and the authenticated user can view and edit files in his folder (file:///files/${user.uid
). Also, The PASV
command is disabled.
Only requests from localhost with Sec-Pro-Hacker: 1
header can access the flag.
We can report our codes on the website to the admin, and he ( headlessChrome/93 ) will check it out. Also, there is a check that checks if the URL matches the following regexp.
An app called tcpslow is running on the server that simply forwards connections from 127.0.0.1:8021
to FTP server with a 500ms delay.
When I started to work on this challenge, a hint was already released.
HINT: The first step is to be able to execute arbitrary FTP commands
That made me think that it should be easy to send FTP commands because they have given that hint about it and also thought that it shouldn't be hard to do that with a browser. Well, I was wrong :) it took me about 8 hours ( or more? ) to figure out how to do that.
Eventually, I started to look for things I can do with arbitrary FTP commands.
FTP servers are nice targets when we have SSRF vulnerability, Some challenges have used this concept before, like this or this and this. We have a browser, so the attack should be like chrome attacks FTPServer and FTPServer attacks HTTPServer
. When I was looking at the source code for the first time and then I saw the FTP server, I thought that it's gonna be the same PORT/PASV trick again, but the /ssrf
endpoint blew my mind. flag was in the response lol. All writeups that I have had seen have used FTP servers to launch attacks against another service. I had no idea if it was possible to read the response using FTP.
Read this.
TL;DR: Using the PORT command, we can make the FTP Server read a file and send its content to a host:port.
The following script will login and send our code ( which we have full control over it ) to HTTP Server. It first saves the payload, then it gets the uid ( name of the folder which our payload is inside it ), Then it connects to the FTP socket ( pretending SSRF ), and then it connects to 127.0.0.1:1337
and will send our payload to it.
I was wondering what happens if I send a STOR command right after RETR, Maybe some magics happen and the response will be written to the file 🤔
So i just added STOR /files/{SESS_ID}/file\r\n
right after the RETR
command.
and it didn't work.
Then I tried again with sending many requests instead of one.
It worked this time!
Both RETR
and STOR
commands use the following function to get a socket to read/write.
When we enter this function for the first time ( after sending the RETR
command ), Both self.dataListener
and self.dataSocket
are undefined, so a new socket will be created with calling self._initiateData
function. The second time we enter this function ( after sending the STOR
command ), self.dataSocket
is defined, because the RETR
command is still writing that huge data, so the STOR
command will use the same socket! Now that we know why it happens, we can replace that requests with newlines and it will still work!
As you can see in the above scripts, after sending each command, we wait for the server's response, and then we send the next command. We can't do that in chrome afaik 😂. According to writeups and rfc354 section IV paragraph one, CRLF can be used to terminate each command, so practically it should work.
FTP commands are ASCII terminated by the ASCII
character sequence CRLF (Carriage Return follow by Line Feed).
So Let's try to login
What? let's see FTP logs.
So apparently ftpd doesn't give a sh*t about rfc354 section IV paragraph one. It thinks that CRLF is part of the argument. Let's see how they handle each TCP packet's data. You can find the following function here.
So what if we send each command in a separate packet? The maximum TCP packet size is around 64K and localhost's MTU is also around that number nowadays.
To do that, we can prefix our commands with around 65k spaces. It will work because of that trim()
in the above function.
Let's try it
Worked!
This was my favorite part of
The headlessChrome only visits URLs that match the following regexp.
Good news is that admin visits a page in which we can put arbitrary HTML inside it, Bad news is that the page is protected by a strict CSP.
You can read more about it here. Basically, these two limitations make things complicated:
We have to send a very long text with whitespaces and alphanumeric chars inside it to the FTP server without using JS. What an awesome challenge!
I first tried the official implementation and It was not even working with short payloads lol. Then i found this blog. Since I didn't know anything about TLS stuff, I just ran the commands in readme 😊 and it didn't work either with large(>100k) payloads.
My friend renwa found this bug, Basically the bug is that the dots are not escaped.
So we can just buy a .tf domain with $6.49 and earn $50 bounty from admin😛
It was unintended btw
This was my most promising failed attempt. You can read more about report-uri here. Basically Mr.chrome puts the whole CSP in the report HTTP request, so we can put some strings after the CSP and it will be sent along with the report request to the target.
But we are limited to 65k again Because of these limitations 😢
While I was testing my failed attempts again, this attribute came to my mind. You can read more about it here. Basically, it will be placed in a request header called Sec-Required-CSP
.
And the mind-blowing thing is that there is no strict length/syntax check🤯!
Although it has some problems with some characters like commas in the policy But we can easily send long payloads inside it.
Finally, we can execute arbitrary FTP commands! I replaced the PORT
command with EPRT since it was acting weird in CSP.
Cleaned Final exploit.
Solved!
uiuctf{i_h0p3_th4t_waS_a_fUn_ch4In_75d997b}
Shout-out to my teammates, organizers, and especially arxenix for creating this challenge.