Intro
Between finishing three different writing assignments for college, taking a peek at the release of the Havoc C2 Framework (blog on that soon!), and the beginning of FLARE-On, I somehow managed to make time to do Project Sekai’s first CTF event, and I had a lot of fun! Some of the challenges here were the most creative I’ve seen in a while, from a terminal version of Keep Talking and Nobody Explodes, to an entire category dedicated to competitive programming, to a crypto category that didn’t have any RSA in it as far as I know!
In the interim between main blog posts, I’ll be doing writeups of three of the challenges that I solved. Time Capsule was a cryptography challenge that featured the classic time-based seeding trick, but then featured an interesting problem of having to undo a simple scrambling algorithm. Bottle Poem was a web challenge that featured an easy-to-spot directory traversal vulnerability to find another endpoint, but then a deserialization attack to get code execution. And finally, one of the most time-consuming funniest challenges was Perfect Match X-treme, an intro game hacking challenge with a very uncanny valley version of Fall Guys.
Time Capsule
Crypto, 178 Solves | Author: sahuang
Description
I have encrypted a secret message with this super secure algorithm and put it into a Time Capsule. Maybe nobody can reveal my secret without a time machine...
Challenge
We’re given an encrypted flag.enc
and how it was encrypted.
Solution
This encryption algorithm is a little more involved than the typical encoding challenge that you find in most events. Since encryption is in two parts, we’ll look at encrypt_stage_two
first, and then encrypt_stage_one
.
encrypt_stage_two
starts by getting the current time, including the decimal part, and then pads that number out to 18 characters with 0’s. This value is used to seed Python’s random
module, and then generates a list of random bytes as long as the original message (i.e. a one-time pad key). The return value is the XOR of the message concatenated with the time and the key concatenated with 0x42
bytes to meet the length of the timestamp.
We know the message and the key are the same length, so all we have to do to recover the time is to XOR the last 18 bytes of the ciphertext with 0x42
. We can then use the result of this to seed the randomness and recover the key. Once we have the key, we can XOR with the first part of the ciphertext to recover the message. Easy! 😎
Ah, not as easy. We can see that we get the timestamp, but the message looks like a flag with all of the characters shifted around randomly. It’s probably possible to reconstruct the flag from here by guessing the message they wanted to send in l33t t3xt, but we have source code.
Prior to encrypt_stage_two
getting called, the source code generates 8 random numbers using the operating system’s urandom
, meaning we can’t use the seed to get that. Then, encrypt_stage_one
gets called 42 times using the random numbers as a key. The code for encrypt_stage_one
is a little harder to understand just from reading it, so I decided to test it dynamically. You can run a Python script and enter an interactive shell with all of the functions and variables defined using the -i
flag, so I’ll try that with a modified version of the challenge code (remember to edit the file to make sure it doesn’t overwrite the flag!).
We can see what gets stored in u
by running that line in the shell.
What happened? Notice that the content of each tuple are (random_num, index)
, where index
is the number’s position in rand_nums
. We then sort the tuples by key. Looking back at the source code, we then iterate over each tuple, and then for each tuple, starting at the index
in the message, we continue through the message jumping len(key)
characters and adding that to res
. If that didn’t make sense, let’s look at a toy example.
If this was only being done once, it would be very easy to reverse. If this was only being done twice, it still wouldn’t be that bad to reverse. But we do this 42 times, and that doesn’t seem immediately easy to reverse. There’s something more easy to exploit though, and that’s the size of the key. The size of the keyspace will be , i.e. the number of permutations of a list of 8, or in other words, the number of ways you can put 8 numbers in order without repeating a number. This value computes to be 40320, which, in the context of computers and cryptography, is not very large. Since we know the ending positions of all of the characters, and we have a flag format that we know, we can shuffle the characters on a fake flag 42 times, and if all of characters that we know line up with the garbled message that we recovered, that key is highly likely to be the permutation we need.
We eventually find the key to be (6, 3, 7, 4, 2, 1, 0, 5)
. We can then use this key to reverse the operation done in encrypt_stage_one
and repeat that 42 times to find the original flag. The final solve script (ctf quality, my apologies) is below.
flag: SEKAI{T1m3_15_pr3C10u5_s0_Enj0y_ur_L1F5!!!}
what a nice message after i spent at least 3 hours trying to figure out how to reverse stage one without bruteforcing…
Bottle Poem
Web, 146 Solves | Author: bwjy
Description
Come and read poems in the bottle.
Solution
There was no source provided for this challenge, so we just have to navigate to http://bottle-poem.ctf.sekai.team/
and see what we find. The URL brings us to this very simple webpage.
If I click on one of these links, I’m brought to a plain text file.
spring.txt
looks like it’s the name of the file as it’s stored on the webserver, which could mean some kind of file inclusion or just a regular directory traversal vulnerability. I’ll try and find /etc/passwd
since these challenges are usually deployed in Linux docker containers, and I’ll also do this using cURL so we don’t flood this page with images.
Very nice! Now we need to figure out exactly what files we might need to leak to find the flag. After trying to just guess the location of flag.txt
(and then realizing that the organizers said that the flag is an executable), I decided to enumerate some more. On Linux, you can actually leak information about the current process with directory traversal vulnerabilities. We don’t know the backend framework, so we don’t have an immediate idea of what the server-side program’s name is, but we can leak that by reading /proc/self/cmdline
, the file that stores the command line parameters for the current running process.
I also tried some cheese by reading environment variables, as many times the docker container might store the flag as an environment variable when being built, but that didn’t seem to be the case.
At least we know where the app.py
file is, so that might give us some more insight. Our file read lets us see this
A few things to note here:
- We’re not actually using the Flask or Django framework (still don’t fully understand the difference) like you would see in most Python backends. We’re using bottle, which is a “lightweight, WSGI micro web-framework for Python”
- There is another endpoint called
/sign
that’s checking a cookie, which might be the next place to look - We can also see why directory traversal works. The regex
^../app
isn’t stopping anyone with more than two braincells.
We can see what’s up with the /sign
endpoint, again using cURL to save me some disk space :)
We can see that the cookie we’re assigned looks like some weird version of base64, and decoding it just gives us junk. It appears that the cookies are signed, and using the app.py
file, we know the secret is stored in config/secret.py
. We can use our directory traversal to get that file too.
From here, we can just host a slightly modified version of the webapp locally, signing our cookies with this new secret, and copying and pasting the cookie from our local instance into the remote instance. Maybe the admin portal has something for us.
Credit to another teammate Kreshnik for actually putting the pieces together here, mostly just catching the minor errors we made.
We run this locally and grab the cookie from it.
Copying and pasting into the browser, we realize we’re not done yet.
We can keep enumerating for files or try and see if there’s more to the webapp, but ultimately, we won’t find anything. The secret is actually in the sauce.
If we go to the bottle-py docs, we can see the source code for how the cookie is made.
Turns out we’re sticking pickles in our cookies, and that’s not a good thing. In most cases, an attacker likely doesn’t possess the server secret (unless it’s default), and likely doesn’t have a novel, field-shaking technique to break HMAC, so this is usually fine. But, we have the secret, and the Python pickle
library is a form of serialization, which always has the looming threat of a deserialization attack.
I won’t go into detail about how a deserialzation attack works because I think other people have done that well enough already. For specifics about the attack that we’re going to implement, please reference this great blog by David Hamann, or just find the ippsec/0xdf writeup of a box that has pickle deserialization.
We can revise our local server to create the cookie using the serialized payload instead of just the “admin” value, and hope this works for code execution. Rather than go directly for a reverse shell (which might be stopped by a firewall, assuming there is one), I’ll have it call back to a local HTTP server using ngrok
to port forward. We do the same process of copying and pasting cookies.
Aaand it did not work. Looking back at the source code, if an exception is thrown at any point of the cookie manipulation, we’ll get “pls no hax” returned to us. This isn’t really that significant of a mitigation. The error is thrown when trying to parse the JSON encoded into the cookie, so if we just put our payload in valid JSON, it should deserialize and be parsed properly. We should also notice that we’re actually double pickling here, the RCE()
class should stand as is. Our final solve script can look like this:
And using this will get us the flag. At the time of me writing this, the server keeps timing out when I try to do it, probably because infra is downgraded after the event, but trust me, this works.
flag: SEKAI{W3lcome_To_Our_Bottle}
Perfect Match X-treme
Rev (but actually game hacking), 111 Solves | sahuang & enscribe
Description
Can you qualify Fall Guy’s Perfect Match and get the flag?
Challenge
Unlike the usual “crackme” rev challenge, this one is a video game. Opening the zip, the files are laid out like so:
The files in all of these folders are all necessary to run the game built in the Unity engine. Obviously, as the great gamer that I am, there will be no need to do any hacking, and I will just be able to win the game as is.
That Fall Guy looks like my sleep paralysis demon (why is he bent like that?). Regardless, the game, just like the source material, is very easy and for babies and is the worst minigame that they should have removed a long time ago (yes I’m this passionate about this), but something happens on round 3.
It appears that the organizers were too fearful of my gaming prowess and decided to cheat on round 3, the Sekai tile never shows up. Time to do the hacking.
Solution
The nice thing about this game being done in Unity is that it was likely built using C#, part of the .NET framework of languages. .NET was originally created to be a single set of standard machine code instructions between a wide array of languages via the Common Language Runtime (CLR). However, as with any kind of attempts to standardize…
The short of it is that languages in the .NET framework compile to an intermediate language that gets loaded into the CLR, which translates that to machine code instructions. For reverse engineers, the benefit of this is that those intermediate language commands are very easily reversible, and we can basically get back to original source code. Java, another language that’s easy to reverse to source, works similarly, by compiling to bytecode that gets passed to the Java Virtual Machine (JVM).
The best tool to decompile C# is dnSpy, and we’ve already used it before during HTB Cyber Apocalypse CTF. For Unity projects, the source code for the game is located in Assembly-CSharp.dll
, and we can pop that into dnSpy to see what we can work with.
Once I get more familiar with game hacking, I plan on making a larger blog post about it, but for now, we can just do whatever we want.
- We can go into
Grid.RemoveIncorrectTiles()
and set everything to true so that tiles are never removed. - We can mess with the gravity by removing the
this.RemoveVerticalVelocity()
inMoveBehaviour.MovementManagement()
- We can change what round we start and end at by manipulating
GameManager.CheckRound()
If you’re new to game hacking like me, I highly encourage you to try and play around with whatever you want. I’ll go ahead and do the first option and third options I suggested. Right click the method you want to edit, then select “Edit Method”. Make then changes you want, hit “Compile”. Then make sure to do a File > Save Module
to save the changes to disk (check mixed mode so you don’t run into errors later), and then reload the game. We can then play it out, see that the tiles are never removed, aaaaaaand
*sigh*
I was unable to locate the code that creates the display objects, so I couldn’t exactly get rid of that. But by messing with the MoveBehaviour.JumpManagement()
function, and changing the if statement to be if (Input.GetButtonDown(this.jumpButton))
, I was able to give myself buggy but infinite jumps. At that point, it was just a matter of positioning.
Apparently this one was strings-able according to another writeup, but I didn’t bother checking. The game hacking was more interesting anyways.
flag: SEKAI{F4LL_GUY5_H3CK_15_1LL3G4L}