FlagsintheAir
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

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.
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
.

And we are in!
We have command line access:

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.

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.
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!
Tags: