Northsec CTF Writeup: Failing revenge
Two weeks ago, I participated in the 2020 Northsec CTF. Due to the ongoing pandemic, the event was held online but we still manage to have a lot of fun and I certainly learned a lot. One challenge that I particularly enjoyed was the “failing revenge”. Since it could theoretically happen in real life, I wanted to do a small write-up to explain the attack (and how to prevent it).
Setup
Let’s start with the challenge description:
This dude Wilton Trojan from Stuyvesant High has been wrecking havoc on our EFnet channel for weeks now. He’s learned how to use the script feature in mIRC and he thinks he can just spam our channels with dynamic IPs off of TOR. We tried to play nice but that’s done.
We found some of the source code for the grading system while dumpster diving the school’s hardware dumpster. Can you try and see if it’s exploitable somehow? You have to find a way to make him fail Computer Science this year. He will be utterly humiliated and revenge will be swift. Can’t spam IRC if you’re stuck in summer school!
Nothing too fancy there, we then download the source code and we are given an architecture diagram and two flask projects.
The frontend
project contains some Jinja templates that will fetch the data from the grade server using the forwarder.
We cannot access directly the forwarder nor the grade servers so we must exploit them through the frontend
.
Flag 1
Lets dive into the challenge by first examining the grade server source code. We quickly find a registered route called Flag
with the following code:
From this we know that we have to make an SSRF attack to do a GET on this route and retrieve the flag. In real life this would a be private API that contains some sensitive information that should not be public. Obviously, the frontend
won’t just let us query this API like we want (or will it? 😨)
Here is one route from the frontend that we will attack:
First we see the decorator @valid_school
which verifies that the <school_name>
path parameter is valid with a whitelist. We can’t bypass that one so that leaves us with the <course_id>
. At the bottom of the same file, we also see:
This is very convenient, because if we manage to get an API error (which will be the case with the flag route), we will see a nice error page with the message. This is the reason why you have to return as little information as possible when you encounter an error in your code because you might unintentionally leak information, even a message that you can’t 100% predict is risky.
So how do we force the frontend
to make that call? Let’s dive into the _api
method:
So here comes the interesting part. In this method the endpoint
is directly appended to the high school uri
. which is in turn a combination of the forwarder URL and the grade server url:
From that we deduce that the forwarder will take anything in the path and forward the request to it. In real life, that would be some kind of reverse proxy or API Gateway to route traffic to the right service. So in theory, if a user makes a request like: GET http://grades.ctf/Stuyvesant%20High/course/1
, the frontend code will call GET https://gradeservers.ctf/[::1]:6001/course/1
(the infrastructure is using IPV6).
This all good and well, but the developer forgot one thing… The path parameter course_id
is not typed nor sanitized. That mean that by default it will be a string that we control. So if we decide to send the following request: GET http://grades.ctf/Stuyvesant%20High/course/..%252fflag
(notice the /
URL encoded twice), it will we sent like this to the forwarder GET http://gradeservers.ctf/[::1]:6001/course/../flag
which will fetch the flag!
You might be wondering why the /
needs to be URL encoded twice. When the frontend
receives the request, it will try to decode it once which will yield %2F
. If we had only encoded it once, it would have decoded a /
and try to pattern match it directly. This would have resulted in a 404 since the route http://grades.ctf/Stuyvesant%20High/course/flag
does not exist on the frontend
.
Great job 😊 We found the first flag! in real life, this attack could have been prevented easily by changing the path parameter to <int:course_id>
which is a built-in URL converter in flask. Never trust user input!
Flag 2
For the second flag, we see in the grades.html
template the following snippet:
So we must somehow manage to change the grade of the student to get the flag. The grades API is another private API just like flag, but with a twist: it requires a PATCH and HTTPS.
Fortunately for us, the frontend server configuration allows all method for all paths and even forwards that method to the forwarder. This is why in real life you have to be very careful which methods are allowed on which path and you should never do modifications on a GET.
So this should be easily to exploit, right? From the configuration, we know that the HTTPS server is running on the port 6002 for the school we are targeting. We can use the same exploit that we found previously, but we do one more return to override the host too (it is just another path parameter sent to the forwarder after all) 😈 We then put the path parameter we want to modify the grade. So it gives us something like:
Which then gets forwarded as PATCH http://[::1]:6002/grade/15/1/F
. Unfortunately, there is a catch. The grade API is only available via HTTPS and we are using HTTP. So we get a nice error: The plain HTTP request was sent to HTTPS port
😤😤😤😤
But we did learn something important, we can override the host and the forwarder will happy forward the request to it without asking any question. This is extremely dangerous and that is why in real life you should never allow a host to be specified to your reverse proxy / API Gateway. But if you really have to, you should at the very least have a strict whitelist of allowed targets. For this challenge, this was not the case 🎉
During the CTF, each team had a small VM that it could use for their exploits (available at shell.ctf
). We quickly head over to it and write a small script to launch a small redirect server:
So what will that do? Basically we want the forwarder
to do a PATCH on our server, get served a direct to itself BUT using HTTPS and which will then forward the request to the grades server using HTTPS. This is somewhat similar to an Open Redirect attack but server side.
Note that we are using a HTTP code 307 instead of a typical 301 or 302. This is required so the forwarder uses the same method (and body but we don’t care here) as the original request when following the redirect. That way we still keep our PATCH
. We finally fire off our attack with: PATCH http://grades.ctf/Stuyvesant%20High/course/..%252f..%252fshell.ctf:3000
.
When we fetch the grades we get the flag:
So that's it! I hoped you enjoyed reading this small write-up and learned one or two best practices of modern web development. I will post the full source code here when the challenge owner releases its challenge. Until next time, stay safe 👋