Bas Groothedde

Infosec Institute n00bs CTF Labs

Capturing Flags - Infosec Institute CTF


I've never participated in any capture the flag challenges that are so popular these days, so I decided I should try one. I searched for a CTF challenge that I consider to be easy, just to get the hang of things. This brought me to the Infosec Institute n00bs CTF - this post is the writeup describing how I got to all the flags. I don't expect that this will contain extremely difficult challenges, however that's exactly what I need. I want to slowly roll into the CTF world!

Let's just get into the solutions, shall we?

Level 1

This one is meant to start easily; one thing I almost always do when looking at something that is not obvious, is to look at the source code of that very thing I am looking at. In this case, I found the flag as the first line of the source code.

flag: infosec_flagis_welcome

Level 2

This one is also quite easy to solve, as the page gives you all the hints you need.

It seems like the image is broken.. Can you check the file?

Right above it you see an image element with an icon that indicates that it could not be properly loaded. I open the image URL in a new tab and can see that it does exist at the specified URL. The next step is to download the image and inspect it further; when you open it in any editor you are presented the flag, nothing more.

flag: infosec_flagis_wearejuststarting

Level 3

There is a QR code image on the page, when decoded you'll notice that the resulting text has a Morse code pattern. If you use an online tool (or write one yourself) to decode that Morse code, you get the flag.

When you scan the QR code, you'll be presented with the next Morse: .. -. ..-. --- ... . -.-. ..-. .-.. .- --. .. ... -- --- .-. ... .. -. --.

When parsed in an online Morse code parser, you will be presented with the flag. text: INFOSECFLAGISMORSING flag: infosec_flagis_morsing

Level 4

This one mentions HTTP, so one then assumes to check the requests and try intercept useful headers or response/request data. We do not need special tools for this, I'm merely using the Network tab in the Developer Console in Google Chrome. According to this Network tab, this challenge sends a cookie, which seems to be rotated with Caesar / rot13:

cookie: fusrodah=vasbfrp_syntvf_jrybirpbbxvrf

I figured I would try simply rotating the characters back with a rot13 translation, it presents the flag. I did this using Python.

# The content from the cookie we found 
content = "vasbfrp_syntvf_jrybirpbbxvrf"

# Define a translation dictionary from one rot definition to another
def maketrans_compat(source, destination):
    assert len(source) == len(destination), "length of data not equal"
    return dict(zip(list(source), list(destination)))

# Translate input to a new string based on the contents of the trans dictionary
def translate(input, trans):
    return ''.join(map(lambda c: trans[c] if c in trans else c, input))

rot13 = maketrans_compat(

# Translate and display
print(translate(content, rot13))

flag: infosec_flagis_welovecookies

Level 5

At first the site keeps spamming alerts, so viewing anything becomes difficult. After disabling JavaScript on that page I couldn't find any other clue, so the image remains. First I tried many things, such as using exiftool and strings, but that yielded nothing. I then tried using steghide to see if any data was hidden in the image, and it appears to be so: all.txt

  • steghide.exe extract -sf aliens.jpg
  • Password: [leave empty]
  • wrote extracted data to "all.txt".

all.txt contains ones and zeros, and the number of characters in all.txt is divisible by 8. I could only assume that it was a binary string representation of the original string (the flag). I used the following code to automate the whole challenge:

import subprocess

# Convert a binary representation of a string back to a string
def bin2str(input):
    return ''.join(chr(int(input[i:i+8], 2)) for i in range(0, len(input), 8))

    # run seghide with the extract command, set an empty passphrase,
    # force overwrite and use aliens.jpg as input
    command = ["steghide", "extract", "-p", "", "-f", "-sf", "aliens.jpg"]
    pipe    = subprocess.PIPE

    with subprocess.Popen(command, stdin=pipe, stdout=pipe, stderr=pipe) as p:
        output, err = p.communicate()

        # convert binary object to string and find all.txt, if not present the
        # process failed
        if str(err.strip(), "utf-8").find("all.txt") == -1:
            print("all.txt not extracted from aliens.jpg")

# catch any exception and display error
except Exception as e:
    print("could not execute steghide command")

# Read all the data from all.txt
with open("all.txt", "r") as all:
    print("flag: " + bin2str(

flag: infosec_flagis_stegaliens

Level 6

This challenge asks you if you want to download a pcap file, so this is something we can analyse in Wireshark. After opening the file in Wireshark, one thing immediately seems odd to me; the very first packet is an UDP packet from localhost to localhost. I already noticed something when I opened the pcap file in a text editor just to have a look at it; in the first visible data line in my text editor I see a string of characters that resemble ascii-characters represented in hexadecimal, is this one that easy?

The characters I found in my text editor match the data in the first UDP packet, so I decided to check the contents using python.

# The data in the UDP packet ( ->
# This also happened to be the first packet in the pcap
content = "696e666f7365635f666c616769735f736e6966666564"

# Convert a binary representation of a string back to a string
def hex2str(input):
    return ''.join(chr(int(input[i:i+2], 16)) for i in range(0, len(input), 2))

# Display result 

This yields the flag; flag: infosec_flagis_sniffed

Level 7

The first thing you see is an obvious 404 page, however the link to Level 7 in the menu actually refers to 404.php. I thought that this was weird, so I tried loading the logical next level by manually visiting levelseven.php. levelseven.php exists, however it is a blank page. This made me think about viewing the network information of the request. I notice something weird in the status code, it’s something out of the ordinary for sure;

Status: 200 aW5mb3NlY19mbGFnaXNfeW91Zm91bmRpdA==

I bet the flag is in there…

import base64

# The message from the HTTP Status Code 200
content = "aW5mb3NlY19mbGFnaXNfeW91Zm91bmRpdA=="

# Decode the base64 string and display result as utf-8 string
print(str(base64.standard_b64decode(content), "utf-8"))

flag: infosec_flagis_youfoundit

Level 8

The page asks you to download app.exe, so I do. The first thing that always comes to my mind with binary data is to look at it visually first, in notepad++ (on Windows) or any other capable editor. From that alone, I already found the flag residing in the binary data; but the Unix command strings app.exe | grep flagis yields the result even easier.

strings app.exe | grep flagis

flag: infosec_flagis_0x1a


I also wondered what the application would do with the flag, but this I cannot quite figure out. After opening its main function in IDA, I can see it does access the offset to the value and also stores the lower part of the offset in a variable, but then after that never accesses the variable again.

App.exe Disassembled

I believe they did this to keep a reference to it, to ensure people could find it through disassembly as well, but to me it was only an additional curiosity; it wasn't required at all to open app.exe in any disassembler.

Level 9

This challenge presents a Cisco IDS login page, with a username and password input. Clicking on "Log in" seems to do nothing (the form action is "#"). When you press return in an input field, the form does submit. A number of options came to my mind; SQL-i, source code and default passwords. Before I wanted to try SQL-i, I checked the page source; nothing. I then thought it would be a small bit of work to google “cisco ids default password” and Google presented me with a nice ‘root’ and ‘attack’ as result table. After testing, I was glad SQL-i wasn’t required.

Cisco IDS Default Passwords

Upon entering these credentials the flag is presented in reverse format, so python will help us out there:


Or when fully automated:

import requests # http requests
import re       # regular expression

url = ""

# Use the default Cisco IDS password
r =, data = {"username": "root", "password": "attack"})

# We consider it a fail if the response code is not 200: HTTP OK
assert r.status_code == 200, "error: HTTP {:d}".format(r.status_code)

# Prepare a regular expression to extract the flag and search
p = re.compile('<script>alert\\(\'([a-z0-9_]+)\'\\)</script>').search(r.text)

# Ensure we have a match
assert p, "error: could not find the flag in output"

# Reverse what we find and display

flag: infosec_flagis_defaultpass

Level 10

This challenge was interesting to me, because it introduced something to me in Python (which for the purpose of these challenges I'm using) which I have not done before, play audio. The level asks you to listen to a piece of audio. When you click on the link you'll be referred to 'Flag.wav' - that should be an indication that we can find the flag in that file one way or another.

When the audio is played, I notice a very high pitch and it seems to play quite fast. This is where my new experience in Python comes in; I want to download and play the audio slowed down completely in a script.

The python code first downloads the 'Flag.wav' file from the infosec website. It then writes the downloaded raw data to a temporary named file. Using PyDub we can load this wav file, slow it down by using _spawn and overwrite the frame rate. After normalizing the frame rate we have a new sound we can play, one that plays at about 10% speed of the original. Here we can clearly here the flag.

import requests                     # get request to retrieve Flag.wav
import tempfile                     # store the .wav file in temp
from pydub import AudioSegment      # from_wav
from pydub.playback import play     # actually play loaded sounds 

def speed_change(sound, speed=1.0):
    # Manually override the frame_rate. This tells the computer how many
    # samples to play per second
    altered = sound._spawn(sound.raw_data, overrides={
        "frame_rate": int(sound.frame_rate * speed)

    # convert the sound with altered frame rate to a standard frame rate
    # so that regular playback programs will work right. They often only
    # know how to play audio at standard frame rate (like 44.1k)
    return altered.set_frame_rate(sound.frame_rate)

# We can find the wav file here
url = ""

# Download the wav file, which contains the flag. We need to play it
# slowed down drastically
raw = requests.get(url, stream=True)
assert raw.status_code, "could not retrieve Flag.wav"

# Write raw data to temporary file
with tempfile.NamedTemporaryFile() as wav:
    # copy from http request response
    for chunk in raw.iter_content(chunk_size=128):

    # Now load the downloaded wave file
    sound = AudioSegment.from_wav(

    # slow down to 10% of original speed, understandable!
    paced = speed_change(sound, 0.10)

    # play our modified sound chunk

audio: infosec, flag is, s, o, u, n, d

flag: infosec_flagis_sound

Level 11

At first I thought that the hints implied that there was another sound challenge in Level 11, however it presents a meme and a PHP image file named 'php-logo-virus.jpg'. At first I decided I didn't want to pursue the audio hunch and I downloaded the image. We already know that this image is valid, as it is properly displaying on the Level 11 page, so it can be one of the following:

  • The flag is hidden in plain site (strings php-logo-virus.jpg)
  • The flag is an EXIF tag (It's a JPEG file, exiftool php-logo-virus.jpg)
  • More steganography, I mean it's an image
  • Some sort of stub, data appended to the end of the file which would be ignored by the image decoder

Upon checking the file with strings php-logo-virus.jpg | grep flagis we immediately see results: infosec_flagis_aHR0cDovL3d3dy5yb2xsZXJza2kuY28udWsvaW1hZ2VzYi9wb3dlcnNsaWRlX2xvZ29fbGFyZ2UuZ2lm

To verify, I wanted to see if the flag is in the EXIF tags of the image with exiftool php-logo-virus.jpg, which confirms what I initially thought as the Document Name is set to the same flag we found with strings. You can verify this with exiftool -DocumentName php-logo-virus.jpg.

Now this obviously does not look like the final solution, as it seems there is encoded data in the last part of the flag. It looks like base64 encoding, so I re-use the code I used earlier to quickly decode that string of base64 data:

import base64

# The message from the HTTP Status Code 200
content = "aHR0cDovL3d3dy5yb2xsZXJza2kuY28udWsvaW1hZ2VzYi9wb3dlcnNsaWRlX2xvZ29fbGFyZ2UuZ2lm"

# Decode the base64 string and display result as utf-8 string
print(str(base64.standard_b64decode(content), "utf-8"))

which outputs an URL to a gif image: The image contains the text powerslide, so I conclude that the flag results in what we found prepended to that word.

flag: infosec_flagis_powerslide

Level 12

We are presented another page with little to no hints, like in the very first challenge. Now, what I did for the first challenge was to look at the source code and there it was, the flag. For this challenge it was slightly different though, the flag was not in the HTML source code, but a new 'design.css' was referenced I have not seen before.

When I opened 'design.css', I see an extremely invalid color code, here's the contents of that css file:

    color: #696e666f7365635f666c616769735f686579696d6e6f7461636f6c6f72;

This looks like another hex encoded flag, so we can reuse some code from earlier to convert that to readable text:

# The very invalid color value from 'design.css'
content = "696e666f7365635f666c616769735f686579696d6e6f7461636f6c6f72"

# Convert a binary representation of a string back to a string
def hex2str(input):
    return ''.join(chr(int(input[i:i+2], 16)) for i in range(0, len(input), 2))

# Display result

flag: infosec_flagis_heyimnotacolor

Level 13


Well, it seems we won't have to solve this problem... No, we obviously do, and the text on the page is mentioning finding a backup. Often backups are stored with a .old or .bak extension on Linux systems, at least I store them like that, so I tried those extensions and found that levelthirteen.php.old exists.

When we download the .old file, we can open it in any text editor. Inside a comment in a PHP tag I can see an interesting reference:

<p>Do you want to download this mysterious file?</p>

<a href="misc/imadecoy">
    <button class="btn">Yes</button>

Naturally, I download imadecoy, it's a downloadable file. I want to discover what type of file this is, so I use the file command to see if it can solve that for me. It outputs imadecoy: tcpdump capture file (little-endian) - version 2.4 (Linux "cooked", capture length 65535).

That sounds like something we can open in Wireshark, however I first ran strings imadecoy | grep flagis on the file. This easy search did not yield any results, so we have to do a bit more digging.

In Wireshark I've been looking at the data for a few minutes and noticed that most packets are DNS or raw TCP/UDP packets, but some chunks are HTTP packets from localhost to localhost. In the earlier challenge with a packet capture file sharkfin.pcap we noticed peculiar> traffic which contained our flag.

Through the Wireshark option File -> Export Objects -> HTTP I export all HTTP data to a directory. Upon viewing the dictionary of results, one thumbnail immediately shows me it merely contains a bit of text. The file, HoneyPY.PNG, contains the flag in text. A fun challenge!

flag: infosec_flagis_morepackets

Level 14

Level 14 presents us with a link to an SQL file, which contains a phpMyAdmin dump of a database. The database name is level14 and it contains the following tables:

  • administrator
  • ads_links
  • am_commentmeta
  • app_options
  • articles
  • flag? (it is actually named 'flag?')
  • friends
  • money_transfer
  • wp_comments
  • wp_postmeta
  • wp_posts
  • wp_terms
  • wp_term_relationships
  • wp_term_taxonomy
  • wp_usermeta

This collection of tables indicates that this is a Wordpress database with some additional tables. When I was listing all the tables names, I scrolled through the script and noticed the flag? and friends tables. The flag? table seemed a bit too obvious for me, but when I looked at it I saw a funny entry in the friends table;

-- Dumping data for table `friends`

INSERT INTO `friends` (`id`, `name`, `address`, `status`) VALUES
-- ...
(104, '\\u0069\\u006e\\u0066\\u006f\\u0073\\u0065\\u0063\\u005f\\u0066\\u006c\\u0061\\u0067\\u0069\\u0073\\u005f\\u0077\\u0068\\u0061\\u0074\\u0073\\u006f\\u0072\\u0063\\u0065\\u0072\\u0079\\u0069\\u0073\\u0074\\u0068\\u0069\\u0073', 'annoying', '0x0a');

This seemed different from the other entries, which were visible text. This entry seems to be a string of escaped Unicode characters. Judging from the character codes, I'd say utf-8. We can simply print this chunk of escaped characters in bash, so copy-paste and execute!

printf \\u0069\\u006e\\u0066\\u006f\\u0073\\u0065\\u0063\\u005f\\u0066\\u006c\\u0061\\u0067\\u0069\\u0073\\u005f\\u0077\\u0068\\u0061\\u0074\\u0073\\u006f\\u0072\\u0063\\u0065\\u0072\\u0079\\u0069\\u0073\\u0074\\u0068\\u0069\\u0073\\n

flag: infosec_flagis_whatsorceryisthis

Level 15

Here we go, the final level. The link in the menu refers to /levelfifteen/index.php - which seems to be a dead link. I tried many different paths (like in the earlier challenge) and checked out network traffic in the developer console and Wireshark, but unfortunately this level seems to be down completely. One Google search yields:

NOTE: This challenge was taken down shortly after the conclusion of the CTF due to vulnerabilities in the web server. You can read about the challenge and solution below, but you won’t be able to perform it. This write-up will be longer than most as a result.

Which is a shame, because when I read further I see that this was another interesting challenge. It displayed a page wich an input that allowed you to do a DNS lookup. In the back-end, the Unix dig command was used to resolve your input to the result delivered to you in the web page.

This means you could end the dig command with a semicolon and execute other commands, for example you could enter; ls -al and get the DNS lookup + a directory listing of all files. This is where I stopped reading the writeup, because in that output in this writeup we see a file named .hey. We can access that file at and get the contents.

This does not yet give us the flag, so we still have something to do. The contents of .hey look like an encoded string: Miux+mT6Kkcx+IhyMjTFnxT6KjAa+i6ZLibC

I first thought this looked like base64, but not quite. It also didn't decode using base64. After a quite long session of Google searching I came across many articles on padding in base64 and it took me a while before I figured the last 'C' in the encoded string might be a padding for another encoder. After even more searching I found that Atom128 often ends in a 'C' or 'CC', especially after testing a few samples. For example, Hello World!! encodes to a string ending with CC in Atom-128. There is not much information about this encoding, but it looks like it is just a translation / mapping of the base64 characters.

I assume we found the right encoding to use for this data, and tried decoding the chunk in Python, we can use the translation code from earlier to define a translation from Atom-128 to Base-64 and decode the result.

import base64

# Define a translation dictionary from one rot definition to another
def maketrans_compat(source, destination):
    assert len(source) == len(destination), "length of data not equal"
    return dict(zip(list(source), list(destination)))

# Translate input to a new string based on the contents of the trans dictionary
def translate(input, trans):
    return ''.join(map(lambda c: trans[c] if c in trans else c, input))

# The atom-128 encoded data we found in .hey
content = "Miux+mT6Kkcx+IhyMjTFnxT6KjAa+i6ZLibC"

# Define a translation from atom-128 to base-64
atom128_base64 = maketrans_compat(

# Translate to base64 and decode result as base64
translation = translate(content, atom128_base64)
decoded     = str(base64.standard_b64decode(translation), "utf-8")

# Display steps
print("atom-128: {:s}".format(content))
print("base-64:  {:s}".format(translation))
print("decoded:  {:s}".format(decoded))
atom-128: Miux+mT6Kkcx+IhyMjTFnxT6KjAa+i6ZLibC
base-64:  aW5mb3NlY19mbGFnaXNfcmNlYXRvbWl6ZWQ=
decoded:  infosec_flagis_rceatomized

flag: infosec_flagis_rceatomized


This was a fun and small CTF to do, and perfect to get into doing more CTF challenges. I think that this one was still slightly too easy for me, so my challenge is now to find one that increases the difficulty a bit. I was slightly disappointed that I couldn't do level 15 completely, because it was taken down. Thankfully I only read the writeup until the part that tells you to find .hey, it helped us learn a bit more about atom-128.

If you have never done a CTF challenge before, I suggest you start doing one; they teach you about things you've never considered. I personally knew of Wireshark, but I didn't use it much; this CTF taught me a few things about Wireshark. It also introduced me to Atom-128, a variant of Base-64 I have never heard of before. I now know how to recognize those strings! It also reminded me about Cisco's habit of using funny default passwords ('attack').

I was also introduced to something completely new to me; processing and playing audio files in Python, which was something I had never done or needed before. I am glad I now know how easy that is and I am glad I used Python for this CTF.

The rest wasn't completely new to me, but it was fun to revise some of these things and get reminded of certain tools and methods that can be useful in day to day life as a software engineer or hobbyist hacker. The steganography challenge wasn't something new either, I once even wrote a steganography tool myself for lossless image formats. This Steganographer tool is ancient though, and it uses an internal format for storing data - so I could not use it in this CTF.

Conclusion: Fun!

Related articles