Party Cat0%

FlagsintheAir

Omar Mohamed
Thanks for sharing!

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Flags in the Air
Welcome back to another writeup. The official writeup for "Flags in the air" challenge in Connectors CTF Quals 2025.
This challenge combines source code logic flaws with a touch of Linux internals.

You are given a source code (Download from here)
In app.py in /login route:
This endpoint checks if the username exists in the TWO_FA_CODES dictionary. If it does, it sets the corresponding value to None. Then, it verifies if the provided password matches the one stored in the USERS dictionary. If they match, it generates a 2FA code, stores it in the TWO_FA_CODES dictionary, and generates a JWT token with a '2fa' role.

In /verify-2fa route:
This endpoint retrieves the 2FA code from the request data and compares it with the code stored in the TWO_FA_CODES dictionary for the current user. If they match, it deletes the 2FA code from the dictionary and generates a new JWT token with a 'full' role.

In auth.py there are standard JWT functions, and USERS and TWO_FA_CODES dictionaries:
We notice the USERS dictionary is empty, and there is no registration in this code.

Getting back to the /login route, we see it doesn't check if the username exists in the USERS dictionary before comparing the passwords, which mean if the username doesn't exist, USERS.get(username) will return None.
So what about if we pass a non-existent user with a password set to None? The condition if USERS.get(username) == password: will evaluate to True, allowing us to successfully log in and go to the 2FA step.
login bypass
Here I removed the password field from the request body, which makes it None.

So far, there is a random 2FA code generated for the user mush and stored in TWO_FA_CODES dictionary.
Looking back in the /login route, we see this:
With each login attempt with a specific username, the 2FA code for that username is set to None, and a new one is generated when the password matches. But when passwords don't match, it does nothing. The 2FA code remains None, and the JWT token with '2fa' role is still valid.
So all we need to do is to login for the second time with the same username and any wrong password (which is anything since the user doesn't exist), and the 2FA code will be None.
wrong login
2fa none
And we are in!

We have command line access:
cmd
Nothing special in the corresponding endpoint, just a simple command execution.
In the Dockerfile, we see the following command:
From this, it’s clear the flag is being echoed every 10 seconds to standard output. The challenge is: how can we actually capture it?
On Linux, whenever text is written to the terminal, it goes through a file descriptor (FD). Standard output is always FD 1. Inside a container, we can access this stream via /proc/1/fd/1, where 1 refers to the main process PID.
However, since the flag only appears briefly every 10 seconds, we need to keep reading continuously in order to catch it. A one-shot cat won’t work reliably. Instead, we can use timeout to stream the output for a window long enough to capture the flag:
This keeps reading FD 1 for 15 seconds, which is guaranteed to catch at least one instance of the flag being printed.
flag
This was the intended solution. Some players used some other methods like cat /proc/1/cmdline which prints the command line of the main process which has the flag.
cmdline
Other player used pspy, all lead to the same result.
And that’s it for this challenge. Sometimes flags are flying right past you in stdout. Hope you enjoyed the writeup and learned something new. Till the next one!

You might also like