Story time! A few weeks ago I was at the AREA41 conference and there were A LOT of CTFs there - it was almost like every village and every sponsor's booth had one. I had some time before my talk so I decided to play one of these and I ended up selecting the InfoGuard one (shoutout to super friendly organizers).
One of the tasks had MQTT in the name, so I expected to have to play with the standard IoT messaging protocol, but that's not what ended up happening at all - I ended up accidentally cheesing the challenge with an unintended solution instead!
From the player's perspective the task started with a website - or, to be more accurate - a web-based login panel asking for a username and password. Trying a couple of typical credentials (admin/admin and the like) didn't work, so I decided to look at the actual HTTP request sent and play a bit with it using command-line curl.
The actual POST request was a pretty typical JSON and contained only two string fields: "username" and "password". I started by sending just the "username", though instead of sending it with a string value, I sent it with a number instead:
{ "username": 123 }
...and I received an "OK" with a session id.
This isn't really what I expected. What I did expect was some form of error saying that "hey, the field has the wrong type" or "where's the password?" - these error messages are pretty useful in establishing what libraries or frameworks are used. Instead, it seems I was logged in?
But was the session id I got really a "logged in" session? Why, yes, it was. And after copy-pasting the cookie to the browser, I could see the full admin panel including THE FLAG!
Pretty obviously this was an unintended solution, but hey - a flag is a flag 🤷
I've chatted on the next day with the organizers about the task and apparently the code worked like this (pseudo-code from my memory):
USERS = {
"admin": "some long and complex plaintext password"
}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
if USERS.get(data["username"]) != data.get("password"):
return { "error": "wrong password" }
... # Continue as a logged in user.
What's going on here is pretty simple and boils down to the difference between the dictionary[key] and dictionary.get(key) calls. In the first case a non-existent key leads to a KeyError exception being raised, while in case of the .get() call the second argument - None by default - gets returned.
As such, USERS.get(data["username"]) for a provided but ultimately non-existent username evaluates to None, and the password field is missing in the request altogether, so we get None there as well. And hey, None is equal to None in Python, so that's a correct password, right? 😅
By the way, if you like Python stories that include surprising quirks, starting end of July I'm running a Python Cyber Summer Camp that will feature some more hardcore examples as well - check it out at hackArcana (my educational / CTF website).
Eventually I finished the CTF first (tied in first place with two other participants per this CTF's "it's a conference so we allow ties" rule) and won a USB Rubber Ducky! And I've heard that at least one other CTF player cheesed this challenge the same way.
Anyway, it was a fun CTF and overall I greatly enjoyed this edition of the AREA41 conference - looking forward to the next event in the series!
