Grey Cat The Flag 2022
Organized by NUS Greyhats in collaboration with National Cybersecurity R&D Labs from Singapore.
Last updated
Organized by NUS Greyhats in collaboration with National Cybersecurity R&D Labs from Singapore.
Last updated
I played the qualifiers while on holiday, so I was only able to join in for 1-2 hours every night. Nonetheless, my team did pretty well, finishing 3rd among Singapore teams and 4th overall.
Here are the challenges I solved.
Challenge | Category | Value |
---|---|---|
Data Degeneration | Misc | 394 |
Logical Computers | Misc | 467 |
Web | 485 | |
SelNode | Web | 467 |
Grapache | Web | 493 |
Web | 495 |
Feeling lost? Why don't you come and get quotes from the wise?
MD5 (quotes.tar.gz) = 3ba36e72cb0ee2186745673475de8cf7
复读机
This was a simple client-side web exploitation challenge. From the /share
endpoint we can submit a URL for the admin bot to visit.
Let's take a look at the actual functionality of the web app! The flag can be found in the /quote
WebSockets endpoint - as long as we satisfy the following conditions:
The WebSocket client's origin must start with http://localhost
The client must have the correct auth
cookie
The correct auth
cookie is set at the /auth
endpoint when the request is made locally by the admin bot.
It is trivial to perform a GET-based CSRF through a top-level navigation to set the authentication cookie for the victim. We subsequently "sleep" for 1 second before continuing with the rest of the exploit to ensure that the nagivation was completed and the cookie was set.
Although the WebSockets library used (flask_sockets) is pretty old, there is no vulnerability in the ws.origin
provided - afterall, gevent
is the one providing the necessary information in the WSGI environment.
The ws.origin
value corresponds to that of the Origin
request header, which is one of the forbidden header names that cannot be modified progammatically by JavaScript. This is a special request header that comprises of only the following three parts of the current webpage URL:
Unless we find a browser zero-day that allows a malicious webpage to spoof Origin
headers (this would be quite interesting), there is no way around our exploit page's origin needing to start with http://localhost
.
But is that sufficient validation to ensure the WebSocket connection came from a page hosted on the localhost? Nope! We could simply use a domain starting with localhost
, e.g. localhost.zeyu2001.com
.
Because there is no CSRF token being checked and because WebSockets are not restricted by the Same-Origin Policy, we could use "cross-site WebSocket hijacking" to obtain and exfiltrate the flag.
The following page needs to be hosted on a domain starting with localhost
and submitted to /share
.
We like cat, so don't abuse it please =(
复读机
The premise of this challenge was quite simple. We are given the following source code, with the goal of finding the flag somewhere on the server.
By supplying a ?f=
GET request parameter, we can run commands on the server. One problem though - the regex filter is more than a little restrictive.
This is the part where the challenge turns from a web challenge to a command injection filter bypass challenge 😭
The list of allowed characters are as follows:
.
c
a
t
!
?
/
|
-
[
]
(
)
$
One trick to bypass the character filter and run commands other than cat
is to use wildcards. In particular, the ?
wildcard character is used to match any single character.
For example, using cat /?tc/???t?
, we could read the /etc/hosts
file.
Using cat /????????
yielded this very interesting-looking binary. At first glance, it contained the string readflag.c
, so we could guess that this binary is probably called readflag
and it runs with elevated permissions to read a flag file somewhere (so that we need RCE instead of simple file reading)
If we download the binary and open it up in a decompiler, we would see that we need to pass the string sRPd45w_0
as an argument (argv[1]
) in order to read the flag. This was the result of rearranging the letters in the string P4s5_w0Rd
.
Since the |
character is allowed, we are able to use piping to terminate the cat
command and start a new command. For example, using ?f=| /??a???a?
will translate to cat | /??a???a?
, which runs the /readflag
binary.
Now comes the torturous part. How do we get arbitrary characters to use as the password?
One thing that might help is that $()
is allowed, so we could use command substitution to get the strings we need.
When reading the binary previously, we could see that the string P4s5_w0Rd
is in the binary. If we could run strings
on the binary, somehow extract only the password string, and rearrange the letters, we could use command substitution to pass the correct password as an argument.
We could run /usr/bin/strings /readflag
using /???/???/?t????? /??a???a?
Now we need some way of filtering out the rest of the strings and only keeping the relevant P4s5_w0Rd
string. I came across this writeup of a similar command injection challenge where the author used /etc/alternatives/nawk
to filter output using regex, so I decided to try something similar.
Luckily enough, many useful regex characters are allowed - in particular, .
, [
and ]
are very useful. This allowed me to construct a regex that leaves only the password string.
Using /???/???/?t????? /???????? | /???/a?t???a?????/?a?? /[.-t][.-a][.-t][.-a][!-a].[.-a][.-t][c-t]/
, we can get the P4s5_w0Rd
string!
At this point, we could try passing in the string as an argument to /readflag
using $()
, but this will yield "Wrong Password!".
We needed a way to rearrange P4s5_w0Rd
into sRPd45w_0
. It would be great if we could get characters of the string at specified indices - it sure is nice that a cut
command exists for this very purpose!
By using /???/???/c?t -cX
, we will get the character of the string at index X.
But how do we get numbers? It turns out that $?
is one of the special parameters in bash, containing the exit status code of the previous command. If the exit code is non-zero, then $? / $?
will yield 1
, $? / $? -- $? / $?
will yield 2
, and so on. If the exit code is zero, this method will lead to a division by zero error.
But how do we make the exit code non-zero? We just need to place an extra bogus command in front of it: (a || /???/???/c?t -c$(($? / $?)))
.
Here's the script to generate the payload required to reconstruct the password string.
And here's the payload...
All we need to do now is to use the output from the previous script and put it behind /readflag
.
and we get the flag: grey{r35p3c7_70_b45h_m4573r_0dd14e9bc3172d16}
.