DEV Community

Cover image for CTFs are dead, long live CTFs!

CTFs are dead, long live CTFs!

MLH Fellow '20 | GSoC '20 @ Wikimedia | ex-Data Science Intern @ Sensus Labs | Python, Rust, anything ML/AI
・9 min read

Not gonna lie - I was not expecting to learn this much when I started off with my first-ever CTF with Major League Hacking (huge props to Karan and Gabe from MLH 💯 for making it happen). My first interaction was CTFs were earlier this year when I came across the omnipresent Live Overflow channel (if you haven't watched any of their content yet, you're missing out!) but it seemed all too vague and complicated for me. The first real CTF "problem" I solved was one that Karan sent us (his underlings, so to say?) - involving an HTML page with the flag directly in the source code. While solving that gives you a sort of a high, you have no idea what you're getting into, with each problem (d)evolving into an intricate mess of problems, with each having an increasingly gratifying solution. And that's just the beginning of the story.

Anyone new to CTFs probably gets to know of Hack the Box, and makes an account using their super-secure system, and does a few challenges and lets it be - well, same. But, let's talk about this MLH CTF, the CTF for the Batch 1 of MLH Fellows. I'm not gonna lie but when we solved the first few, we were pretty confident about getting through it easy, until of course, later.

So, for today, I'll talk about the challenges that I felt were the most unique (not necessarily ones that I liked). 🥱

The Misc.

Silent Night, Holy Night, All is calm all is well. 
Well, jingle bells are all around the town and Karan is
reminiscing the time he spent with Santa last year.
Going through images and reviews he left around town,
he wants you to visit and have a glimpse of his fun times.
The ticket that leads you to the place is within the
registration email. 
Category: Misc.
Enter fullscreen mode Exit fullscreen mode


The "...ticket that leads you to the place is within the
registration email" is a pretty blatant hint, I can just search for the email using filters.

Once you click on reply to the email and hover over the email ID, you should get a dynamically generated hovercard, which contains the UID of the user. Then, you need to make an educated guess about where to go next but the fact that we have a video of the Santa Village and you needed the Gmail address should tell you where to go next - of course, Google Maps. In fact, you could've just looked up the reviews of the village in the course of the investigation (as a lot of people did) and even found Karan's reviews (which also a lot of people did).

The approach was to basically access the user's map contributions by going to<user-id> and finding the reviews (well, not exactly).

But as has been said, it's a miscellaneous challenge, not an OSINT one.
The second part is a steganography challenge and it's not immediately clear sadly. But hey, sometimes you gotta try everything. The flag was actually (very well) hidden in one of the images (out of two) uploaded with the reviews (out of two). I was eventually able to extract the flag with PCA with relative ease, just took a while because it can be literally anywhere.

And there, we find the flag - mlh{M3rry_Chr1stmas} 🆙

The AWS exploit

Will has been tasked with collecting feedbacks for the CTF. Instead of using online form he made this app as he wanted to get his hands dirty with App dev and DevOps.
[Link to download app]
Category: Reversing
Enter fullscreen mode Exit fullscreen mode

This one took a while because no one in our team had resources for APK decompilation. One of our team members had ADB logcat and that is where we discovered the first chunk of information, as to insight into what the challenge is all about. To be quite honest, this problem had one of the least intuitive and beginner-friendly solutions and required quite a lot of intricate AWS knowledge. If you have worked with certain AWS SDK features or something like, this might be easy, but otherwise, I felt this was extremely restrictive.

Using logcat, we can see what the app is doing once we press "Send feedback":

2020-12-19 01:38:44.558 8550-9744/? D/AmazonS3Client: Key: feedback/.json; Request: PUT https://<...> /feedback/.json 
2020-12-19 01:38:44.559 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.559 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.561 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.561 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.563 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.563 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.577 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.578 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.578 8550-9744/? D/AWSMobileClient: Inspecting user state details
2020-12-19 01:38:44.580 8550-9744/? D/AWSMobileClient: waitForSignIn: userState:GUEST
2020-12-19 01:38:44.580 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.580 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.593 8550-9744/? D/KeyProvider23: AndroidKeyStore contains keyAlias
2020-12-19 01:38:44.593 8550-9744/? D/KeyProvider23: Loading the encryption key from Android KeyStore.
2020-12-19 01:38:44.606 8550-9744/? D/AWS4Signer: AWS4 Canonical Request: '"PUT

Enter fullscreen mode Exit fullscreen mode

Simply put, the app is PUTing our feedback as a feedback.json file to S3. Any requests except POST gave us AccessDenied so it was clear that the ACL was strict, making a POST however kept having weird validation, and even after we met the requirements, the server gave the same validation error. Eventually, I abandoned this attack surface.

Reversing the app

This was very annoying because we didn't have the resources, so I started by installing mobSF and Genymotion, the app looks great and all and also does a lot of security analysis, such as searching for trackers and malware - but in this case, it was (almost) pointless. There was nothing in the Java files and Genymotion just didn't work.

I then proceeded to use an online decompiler at which is a typical JADX compiler and I was able to access the resources folder (as well as the Java files) which had what I believed to be was necessary for the exploit to work. The funny bit was that despite having opened the same file the previous day, I never realized that they were usable credentials. In the resources folder, we see a res folder, with a raw/awsconfiguration.json file:

"CredentialsProvider": {
  "CognitoIdentity": {
    "Default": {
      "PoolId": "ap-south-1:<...>",
      "Region": "ap-south-1"
  "S3TransferUtility": {
    "Default": {
      "Bucket": "<...>",
      "Region": "ap-south-1"
Enter fullscreen mode Exit fullscreen mode

After reading a bit up on AWS theory (yes, really...), we figured that the Cognito Identification Pool has to be unauthenticated because there were no other credentials being exchanged, so the key was to get an ID from our ID Pool, exchange it for STS tokens and use those credentials to access the S3. While I was exactly right in this case, I still wonder what would happen if I wasn't, but anyway. 🤷‍♂️

I started working on a Node script and built something from sample code to generate the required credentials, and as expected, Cognito was giving us proper access tokens, where it failed was retrieving resources from S3 because of CORS. And while I was going bonkers, a teammate wrote a script with boto3 to do this. Oh also, I don't know how to write Node.js, so probably that was the problem.

from boto3.session import Session
from io import BytesIO


session = Session(aws_access_key_id=ACCESS_KEY,
s3 = session.resource('s3')
your_bucket = s3.Bucket('<...>')

for obj in your_bucket.objects.all():
    key = obj.key
    body = obj.get()['Body'].read()
Enter fullscreen mode Exit fullscreen mode

We can then see the very first item in the S3 is the flag:

And we retrieve the flag - mlh{th1s_i5_why_i_d0nt_l1k3_devops}. 🚀

The theory

Cryptography is beautiful, isn't it? You just have to use it right.
Enter fullscreen mode Exit fullscreen mode

secret-flag.png.crypt encrypt.c

This challenge was on a different level, not gonna lie. I mean, to veteran CTF-ers, this would be medium (?) at best, but it was an absolute wreck for our team, as all of us were beginners. But the best part, we eventually solved it, right as the CTF was ending, tying in points but 4 minutes later than another team. Quite dramatic, yes.

I had a serious love-hate relationship with this relationship but I probably learnt the most from this one. The first step to solving this challenge was to take apart the source code with a fine toothpick and secondly, understand how PNGs work. Since I am how I am, I did neither and immediately dug into the different kinds of possible attacks, known-plaintext, chosen-plaintext, forbidden whatnot (well, it was somewhat like a forbidden known-plaintext attack).

So, the first step! Research. I read up on how block ciphers work and more importantly, the counter mode - really what that means is that I read a bunch of pages on Wikipedia. 🥱 But seriously, it was kind of worth it. In particular, I noticed the paragraph about known-plaintext attack but didn't understand anything because, yeah, cryptography, amirite.

But I read this and that, one-time pad attack, two-time pad, all of that (yes, I was basking in my ignorance). All of that until the first hint was released:

the CTR implementation is buggy

Despite knowing this, I had never actually figured out what was the bug - because I assumed that the bug was actually the code itself, which itself was only buggy because it was written with OpenBSD libraries not available on standard Linux distros. Ugh. (And yeah, those took hours to fix and script).

Eventually, I got around to seeing the inc_counter() function in the script and had a very vague suspicion if it was actually written correctly because of they way it was written, I was very sure it was not changing the counter at all (that was not the actual problem apparently). The issue was with the ctr_block_encrypt() function parameter which receives a copy of the struct instead of a pointer to it:

ctr_block_encrypt(struct ctr_state state, uint8_t *in, uint8_t *out)`
Enter fullscreen mode Exit fullscreen mode

So, I set to debugging with my favourite debugger, fprintf. I wrapped them around the function invocation, and lo and behold, no increment! The nonce was being reused. This opens us up to the world of attacks based on nonce-reuse. The part after this was actually more painful - figuring how to guess the plaintext to actually get the keystream - and then the decrypted file.

PNGs say what?

I was quite sure that this would have to be the way to decrypt the file because we know how they all have a unique file signature. I retrieved the hex values from the W3C PNG spec site and got to work.
Using a crib dragging attack (a "crib" is known or guessed plaintext, making this a variant of a known-plaintext attack), I figured out the first half of the key, I got 5c 92 d5 10 d0 63 20 9d 00 00 00 00 00 00 00. But I was truly stuck on the rest of the key, or so I thought - but I discovered that each PNG file at least has two other critical chunks - IHDR and IEND.
Using the IEND (+ CRC) bytes, I used crib dragging to get a bit more out of the key, 5c 92 d5 10 d0 63 20 9d 34 cf 24 95 00 00 00 00 - finally, all that was left was to fit in IHDR into an appropriate block allowing us to guess the whole key.

We then dump the decrypted bytes into a .txt file, clean up the file to meet PNG specs (and remove the counter bytes), and open it to retrieve the flag!

And we can finally get the flag with our own eyes - mlh{n0nc3_m34ns_numb3r_u53d_ONCE}. Truly a thrilling end to this journey (I remember screaming after getting this flag). 🏁

The outcome

While this started off as a CTF for beginners, it got really interesting and complex as it progressed, and not to mention, the reason we did well was because of my amazing teammates, Yash, Rohan, and Shivay. We got all of the challenges and in the end, that's what matters.

Discussion (2)

amitagarw profile image

Can you help me in a CTF, I am trying so far but not able to solve it ? CTF CHALLENGE

qedk profile image
qedk Author

Hmm, is the challenge ongoing? (and again to emphasize, I am new to this xD)