The 2fapache service was a python webservice running on apache using FastCGI that implemented some sort of remote filestorage.
Like most CTF services it allowed to register user accounts for login.
Unlike most CTF services, it used two-factor authentication using the TOTP standard during login.
For this, the user is presented with a QR code after registration, which can be scanned with any 2FA app.
The login mask then queries the username, password, and the current 2FA token.
After successfull login, a user is redirected to /~username/, which displays the (initially empty) contents of their home folder.
Behind the scenes, every user is backed by an actual linux user:
which turned out to be another filename located in the home folder of that user.
This file then contained the flag:
To get to that point, all we had to do was create a user, login, and then access /~62GP3QgOXfOoXiHI/public/reminder.
Clicking through the login form and using FreeOTP to handle the 2FA code allowed us to score our first flag for this service at only 14:07 UTC.
However, with over 200 Teams and new flags every 3 minutes it became quite clear that using an Android app to scan a qr code and then hastily type an eight digit 2FA token wouldn’t get us far.
To automate the 2FA token generation, we needed to “read” the QR code automatically.
Looking at the registration code we can see that the QR code displayed during registration is created using the qrencode utility.
Specifically, it is using the -t UTF8 option to generate the code not as an image, but rather as a sequence of UTF-8 characters.
Since we couldn’t find a python library that would take such an UTF-8 representation as an input, we resorted to converting the UTF-8 encoding into an image using Pillow and passing that on to pyzbar:
Furthermore, the qrcode contains a string with the following structure:
This tells us, that we are using TOTP tokens with 8 digits that are rotated every 30 seconds.
Naturally, there is also a python library for these: pyotp.
Plugging both of these together allowed us to register and login users automatically, and thus steal other user’s flags:
Fixing this flaw proved to be a little bit more tricky.
Our intial patch looked like this:
However, with this patch applied our service turned to Flag not found state.
After lots of trial and error we decided to do the user check at the latest possible stage: only when accessing a file that didn’t contain public in its name we compared the assumed user (from the URI) with the actual user (from env).
Another problem we had to deal with during the CTF was other teams deleting our precious flags.
This was sadly neither prevent through the service design (DELETE suffered from the same access vulnerabilities as GET) nor forbidden by the rules.
As a quick fix we simply disabled the delete-functionality alltogether.
However, it turns out teams also resorted to simply overwriting our files (PUTalso suffered from the same flaw).
While deleting flags was easily preventable by fixing the access checks in all cases, it also meant that stealing other teams flags essentially became a race:
With other teams deleting flags we could only score a flag if our exploit ran first.
This caused us to schedule our exploits much more aggressively than usual, trying to steal a flag every 15 seconds instead of just once or twice per round.
Looking at final scores one may take an educated guess at which teams did delete flags and which teams didn’t…
Alltogether this was a fun challenge, and we were very proud to have scored not only first blood of this service but first blood of FAUST CTF overall ;)