Signum Collective

EasyCTF IV Writeups


Zachary Taylor

Zachary Taylor

Founder and lead visionary behind Signum. I've been an entrepreneur for as long as I can remember, and a tinkerer long before that.

tutorial writeup capture-the-flag hacking

EasyCTF IV Writeups

Posted by Zachary Taylor on .

tutorial writeup capture-the-flag hacking

EasyCTF IV Writeups

Posted by Zachary Taylor on .

Here are the writeups for the problems I wrote for EasyCTF IV. I explain the solutions to each problem in great detail, and for some problems add a little reflection after the solution. This is to share my thoughts on how the problem turned out, and interesting caveats that I identified over the course of the competition. I am intrigued by observing how people learn, and tried to write problems this year that forced an unorthodox approach to a solution.

Shortcut Links:

The Oldest Trick In The Book

This problem might as well have been named 'Obligatory', as it's usually the first problem in any beginner (and sometimes intermediate) CTF. It's a Caesarian shift - a common cipher that works by 'rotating' each letter of a message by a certain 'shift' value n. For example, the Ceasarian shift of abcd would be bcde for n = 1.

There are myriad tools online to solve these types of ciphers, my personal favorite being Rumkin also has other userful cipher tools, so go check it out!

My encrypted flag was: jfxdhyk{b3qh0r3_70_345dh7k_569210}, but each team's was different as this problem was auto-generated. For my ciphertext, I found that n = 21 did the trick!

Here's the flag: easyctf{w3lc0m3_70_345yc7f_569210}

Markov's Bees

This problem required you to head over to the shell server and search through 30+ nested directories that held 1000 3kb files. What was contained in these files is also the inspiration for the title: random text generated using a Markov chain trained on the Bee Movie script.

No, the problem didn't have anything to do with bees or the Bee Movie; I just thought that would be fun. Plus, @GenericNickname had a script in place to generate the files, so it didn't require too much work. You can find the script on GitHub here.

We'll solve with by using the command-line tool grep to search for the flag in the files on the server. To search through the directories automatically, we use the option -R to tell grep to perform a recursive search. Knowing that the flag starts with easyctf{, we can solve the challenge as follows:

[email protected]:~# grep -R "easyctf{" /problems/markovs_bees/

And there you have it!

Markov's Bees was originally valued at 80 points, but was reduced to 50 after some of the problems in the same point range proved to be much more difficult for competitors. As such, it felt overvalued and a reduction became necessary. I never intended this to be a difficult challenge, but something interesting did come out of it.

One competitor - no names, if you're reading this you know who you are - left me pleasantly surprised with his solution to this problem. He wrote the following code to search for the flag:

import os
def readFile(path):
    with open(path, "rt") as f:
def flagChecker(content):
    for line in content.splitlines():
        if "easyctf{" in line:
def markovBees(path):
    if (os.path.isdir(path) == False):
        for filename in os.listdir(path):
            markovBees(path + "/" + filename)

Many of you reading this might think it to be "reinventing the wheel," and you'd be right - but I believe there's something really interesting about this. First, having only taken an introductory Python course, that is a really clean recursive search. Props to you, unnamed competitor.

Additionally, with a tool as ubiquitous as grep, it's natural to think that someone should know to use it. But because he'd only ever used Windows, he didn't. And rather than searching for "search for string in file linux" he chose to solve the problem with the tools he knew. Sure, he ended up reinventing the wheel, but he didn't know there was a wheel in the first place. And I think that's cool.

In Plain Sight

This challenge prompted you to find the flag hidden at Trouble is, the link is dead! However, rather than being a 400 or 500 type error, we get ERR_NAME_NOT_RESOLVED. What exactly does this mean? To start, if we got a 400 or 500 type response, we'd know there was an actual web server tied to this domain, since it would be the server responding with those error codes. ERR_NAME_NOT_RESOLVED means that this domain doesn't point to a server. Or, more specifically, the name server associated with this domain name does not contain an A, CNAME, or any other record that causes it to resolve to a web server.

Since the domain doesn't resolve to a website, it's worthwhile to check if it's even registered. A quick search on Namecheap shows that is is in fact registered:

Domain name:
Registrant Name: WhoisGuard Protected
Registrant Organization: WhoisGuard, Inc.
Registrant Street: P.O. Box 0823-03411

Well, this is certainly a registered domain. That means it must have a name server tied to it - so let's investigate that with the linux command line utility dig. We'll use the ANY flag to ask the server for as many records as it's willing to disclose, and +short to remove extra verbosity. Here's the result:

[email protected]:~$ dig +short ANY
"ANY obsoleted" "See draft-ietf-dnsop-refuse-any"

Hmm. Turns out that ANY isn't synonymous with 'all'... in fact, the name server chooses how to respond to the ANY query based on its configuration. Let's find out what name servers this domain uses with the NS flag:

[email protected]:~$ dig +short NS

That explains it! Cloudflare deprecated the ANY query in 2015 due to its widespread abuse. They go into further detail as to why here. To get any records from Cloudflare, we are going to need to explicitly query them by type. The most likely record to contain a flag would be a TXT record, so let's check for that:

[email protected]:~$ dig +short TXT

Great! We got our flag.

To be perfectly honest, I was shocked at the amount of headaches this problem caused for many competitors. Approximately 70% of the private messages I received on Discord during the competition were about this problem. I did not anticipate the flood of messages on the chat about In Plain Sight being "down" and "offline," asking us admins to "fix it." As it turns out, understanding of the Domain Name System (DNS) isn't as widespread as I assumed. When competitors were faced with a 'broken' link, they just assumed the problem was broken rather than trying to investigate why. Had they looked into what ERR_NAME_NOT_RESOLVED meant, they'd have known that it was impossible for the web server to be down, because there wasn't a web server at all. I ended up posting a disclaimer in the problem's description explaining this, and that cleared up most of the confusion.

I think problems like this one draw attention to one of the difficuties of being a problem writer for a subject with which you are intimately familiar. It's easy for me to say "Of course they'll think to check the DNS records," but I am actually assuming too much about the competitors' skill. Sure, the teams composed of high school seniors who've been doing CTFs for years will solve this problem with zero effort, but as a problem writer I also must cater to the beginners who won't have a clue where to begin.

If I were to do this problem over again, I'd probably directly reference the Wikipedia article for the domain name system. The goal of EasyCTF is for people to learn and have fun, so when writing a problem I need to ask myself "What do I want them to learn?" and design the problem accordingly. This problem and the next one were designed with a "What methods do I want them to use?" mentality, and I think competitors would be better served with problems whose difficulty stemmed from learning those methods, instead of finding the method to begin with.

Digging For Soup

This challenge claims to have hidden things a little better than previously, and gives the domain name Well, let's see if there's a website! Hmm. Same as with In Plain Sight, there doesn't seem to be a website associated with the domain name provided. Given the description, we can assume that this should be similar to the other DNS problem, but perhaps a bit more difficult.

Let's go ahead and check the DNS records for this domain with dig. We don't know if this domain is on Cloudflare like the other, so trying an ANY query is worth a shot:

[email protected]:~$ dig +short ANY 2018021205 28800 7200 604800 86400
"Close, but no cigar... where else could it be? hint: the nameserver's IP is"

Look's like we've found something! First, we know this domain is on a different name server, as it responded to the ANY request. The TXT record we found gives as the IP of the name server for this domain. We can probably trust this, but let's go ahead and make sure. But how?

Well, the domain name system works by having several top-level domain (TLD) servers which contain glue records pointing to the name servers of the domains they handle. Once again, dig will come in handy here. We can query the top-level domain of like so:

[email protected]:~$ dig +short com NS

This gives us the top-level name servers that we can query for's name servers. Note that we can use any of the servers. Here's how we dig a specific name server (I've trimmed the output a bit):

[email protected]:~$ dig NS
;; AUTHORITY SECTION:     172800  IN      NS     172800  IN      NS

;; ADDITIONAL SECTION: 172800  IN      A 172800  IN      A

Looks like is in fact the IP for the name server, so nothing interesting here. So where could it be? The TXT record we found suggests that we're close, so it's likely that the flag resides on a subdomain of What makes this difficult is that subdomains aren't disclosed by the name server... you have to explicitly request them by name. So unless we brute force requests, we likely won't find it. However, the hint on the problem description does seem interesting:

How do slave zones know when updates are made to the master?

A quick Google search for "dns slave master updates" leads us this post on "DNS BIND Zone Transfers and Updates." Looking around, we find this exerpt on provide-ixfr that seems interesting:

Applies to slave zones only. The request-ixfr option defines whether a server will request an incremental zone transfer (IXFR) (option = yes) or will request a full zone transfer (AXFR) (option = no).

Turns out that a zone transfer is how slave DNS servers get updated records from the master DNS server. As a security precaution, DNS servers should never respond to a zone transfer request from an IP that isn't a slave server, but perhaps the server at has been misconfigured? Let's try it (output trimmed):

[email protected]:~$ dig @ AXFR     86400   IN      SOA 2018021205 28800 7200 604800 86400 10  IN      TXT     "easyctf{why_do_i_even_have_this_domain}"     100     IN      TXT     "Close, but no cigar... where else could it be? hint: the nameserver's IP is"     86400   IN      SOA 2018021205 28800 7200 604800 86400

Aweseome! Turns out this name server was misconfigured, and is likely responding to AXFR requests on The flag is easyctf{why_do_i_even_have_this_domain}, which was at

In addition to all the same headaches caused by In Plain Sight, Digging For Soup caused me a great deal of work early in the competition. I had originally intended for this problem to involve 'guessing' the subdomain easyctf or finding it by more advanced methods we won't cover here. Turns out most competitors were immediately turning to dictionary attacks, attempting to brute force the subdomain. There are a few problems with this:

  • I was using Cloudflare for DNS, and it was starting to drop requests from people who were spamming its name servers. So even if easyctf was in their dictionary, they wouldn't get a response with the flag
  • Popular subdomain dictionaries don't contain easyctf
  • Apparently guessing easyctf is harder than I thought

After I considered these issues (as well as some feedback from competitors who had solved it) I decided the problem was due for a revision. Unfortunately for my Sunday afternoon, this meant deploying a custom nameserver using PowerDNS and Poweradmin. That's a long story for another time.

Additionally, I gave the IP of the name server for difficulty reasons. I knew that many teams would be able to dig the TLD and subsequently find the IP of the name server, but I worried that too many others would see "Close, but no cigar" and start looking somewhere other than the DNS server. At 150 / 500 points, giving the IP address of the custom name server felt appropriate.

If I were to do things over, I'd have deployed the custom name server from the start and valued it at around 200 or so points. At this level (and possibly a bit higher) I would have felt justified leaving the IP of the name server up to the competitors to figure out.

Little Language

This challenge wants us to gain root access to a special programming portal... giving a mystery file as a clue. We also get this snippet of code named parser.txt:

S : E                           { ExpS $1 }
  | global var '=' E            { GlobalVarS $2 $4 }

Lastly, the hint is: One small step for man...

We'll start with the file. It doesn't have an extension, so we need to find out what it is. Let's open it up in a hex editor such as HxD to check the file signatures. The first few bytes are 89 50 4E 47 0D 0A 1A 0A, or ‰PNG.... in ASCII. This looks like the file signature for a PNG image file, but let's check at Gary Kessler's file signature page to be sure:

89 50 4E 47 0D 0A 1A 0A     ‰PNG....
                       PNG  Portable Network Graphics file
                            Trailer: 49 45 4E 44 AE 42 60 82 (IEND®B`‚...)

Sure enough, this looks to be a PNG! Let's search for the trailer 49 45 4E 44 AE 42 60 82 in the hex editor just to make sure there isn't anything else hidden in this file. Sure enough, after the end of the PNG we find some text: note: the password is l7&4C&Cg. This must be the password to the programming portal... great! Let's add the .png extension to the file to view it as an image:


What on earth is that? At first glance, it looks like some sort of mathematical formula, but it doesn't seem to involve any numbers or operations. Let's think about it for a moment... it seems to be describing something about FLAG, as that word is being used as a label on the left. The top of the horizontal line seems to say that username = root, and password = REDACTED, but there is this E() around both that confuses things. We can infer from the note we found in the image that password = l7&4C&Cg, but not much else.

Before we try to understand what's going on, let's go ahead and try to log in to the programming portal with the command nc 21480. We're met with a prompt that suggests we try typing :help, here's the result:

commands begin with ":" (try :help)
:help   show this message
:end    stop current multi-line parse
:q      exit
note:   certain language features only available to root users

Looks like we need root to access more functions, but this is called ctflang, so is it actually a language? Let's try some things:

1 + 1
1 = 1
1 = 2
if 1 + 1 = 2 then 10 else "test"

Looks like this is an entire programming REPL! We don't currently know how to enter our username and password, so let's go back to this image:


We need to try and break this down and understand the individual components, namely the $\vdash$ and $\Downarrow$ symbols. Turns out $\vdash$ is called a turnstile, while $\Downarrow$ is called downwards double arrow (shocking). According to Wikipedia, a turnstile reads as "yields" or "entails." Additionally, when searching for a downwards double arrow, we find a Google Books result for Programming Languages and Systems. Wait - the images in that book look just like what we have! The title of the paper is "Pretty Big-Step Semantics" and our hint is "One small step for man..." so maybe our formula is something like this, but smaller? If we do a little more digging, we find that we're looking at Small-Step Semantics.

After reading up on small-step semantics notation, we get the plain-English translation of the semantics for the FLAG language feature:

The flag feature evaluates to a flag when the global environment has username = root and password = l7&4C&Cg.

With some help from the parser.txt we were given (turns our it's a grammar), let's try to set the proper global variables then call the flag function:

global username = "root"
global password = "l7&4C&Cg"

Awesome! We got the flag.

I'm not really sure where to begin with this one. I created this problem as an attempt to completely stump people and force them to figure out what the contents of the image actually meant. Unfortunately it seems that most competitors threw in the towel when faced with notation they did not understand, and a snippet of a parser that wasn't in any language they understood.

I hoped that people would follow a similar process as what I just explained above, but obviously even that was a bit convoluted. I didn't intend for that 'perfect' chain of events to happen, but I wanted teams to break down the problem and try to understand its components instead of digesting the entire semantics notation in one pass. Much of the feedback I received was about how it was not a problem that made you feel satisfied having solved it. I think this is because most who solved it did so by guessing different combinations of trying to set the username and password, while not having any clue if they were even making progress.

If you solved it that way, then sure - it wasn't satisfying, and perhaps it was even a little stupid. But I hope that someone, somewhere figured out that they were looking at small-step semantics, and were proud to be able to translate that image into something that made perfect sense. For future CTFs I will likely refrain from using advanced notation as a key to solving a problem, but will rather use it as a supplement for those who want further understanding.

Special Endings

This forensics challenge gives us a file named encrypted_lines.txt full of jumbled text. Here's a snippet:


The hint is RFC 4648, which is the name of the standard for Base64, 32, and 16 data encodings. Let's try decoding the snippet above using Base64:

Love doesn't just sit there, like a stone, it has to be made, like bread; remade all the time, made new.
- Ursula K. Le Guin, The Lathe of Heaven

While that's a powerful statement, it definitely isn't our flag. If we decode the entire file, we get more quotes from the author Ursula K. Le Guin. There isn't anything out of the ordinary with the text, so there must be something we're missing. The problem is called Special Endings, so perhaps it's got something to do with the ends of the encoded text. Let's take a look at the RFC 4648 standard and see if there's anything special about the ends of the encryted lines.

According to the standard, base64 encoding works by mapping 6-bit groups of bits in the data to characters in a 65-character subset of US-ASCII.

The alphabet is: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

So, the information to be encoded is split into 6-bit groups, and as the maximum 6-bit number (111111) is 63, the decimal value of each 6-bit pair can be used as an index into the alphabet array. To help wrap our heads around this, let's encode "hello" by hand using Base64:

We start by converting "hello" to binary, which yields:

01101000 01100101 01101100 01101100 01101111

Now we rearrange it into 6-bit pairs:

(011010) (000110) (010101) (101100) (011011) (000110) (1111 ??)

Looks like we've run into an issue. Accoring to the standard, when our bits aren't evenly divisible by 6, we have to add padding characters to the end of our base64 encoded string. Because we have 2 bits leftover, the padding looks like this:

(011010) (000110) (010101) (101100) (011011) (000110) (1111=00)

We fill the extra space with zeros, which are then flagged by the = character for the decoding algorithm. That tells it to ignore the last 2 bits of the encoded string. With ==, it ignores the last 4 bits. Let's translate this grouping to base64:

    6 Bit Groups: (011010) (000110) (010101) (101100) (011011) (000110) (1111=00)
   Decimal Index:    26       6        21       44       27       6        60
Base64 Character:    a        G        V        s        b        G        8=

This yields a base64 string of aGVsbG8=. However, in performing this exercise it has become apparent that base64 decoding algorithms will ignore 2 bits in the 8= portion of this string. To test this, let's add 000010 (2) to 8=, yielding +=. If we add that to the encoded string, we get aGVsbG+=. If we decode this using a base64 decoder, it still decodes to hello! This must be what "Special Endings" means, and why the hint led us to the standard for base64.

It looks like we can hide some bits in the padding of base64 encoded text. If the encoded text ends in =, the decoder ignores 2 bits, and if 4 bits if it ends in ==. If we take each line that ends in = or ==, we can grab the appropriate bits and try to put them together into a string. Here's a python script that does just that:

def get_base64_diff_value(s1, s2):
 base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
 res = 0
 for i in xrange(len(s1)):
  if s1[i] != s2[i]:
   return abs(base64chars.index(s1[i]) - base64chars.index(s2[i]))
 return res

def solve_stego():
 with open('encrypted_lines.txt', 'rb') as f:
  file_lines = f.readlines()

 bin_str = ''
 for line in file_lines:
  steg_line = line.replace('\n', '')
  norm_line = line.replace('\n', '').decode('base64').encode('base64').replace('\n', '')

  diff = get_base64_diff_value(steg_line, norm_line)
  pads_num = steg_line.count('=')
  if diff:
   bin_str += bin(diff)[2:].zfill(pads_num * 2)
   bin_str += '0' * pads_num * 2

 res_str = ''
 for i in xrange(0, len(bin_str), 8):
  res_str += chr(int(bin_str[i:i+8], 2))
 print res_str


If we run this, we get ill_miss_you which is the flag!

First things first, I must give credit where credit is due. The solve script comes from Delimetry's writeup that was linked to in a DEFCON oCTF 2016 writeup where I got my inspiration for this problem. I loved the idea of hiding information in base64 padding characters, and was blown away by how intuitive it actually was. Many forensics problems don't make much sense even when you do figure out what has been done, but this just makes sense. I love that about it.

Now, about this problem being a tribute. Some of you may have heard, but Ursula K. Le Guin passed away January 22, 2018, just a few weeks before EasyCTF IV launched. As she is one of my favorite science fiction authors, I thought it fitting to write a problem involving her works. The encrypted text contains many of my favorite quotes by her, from books I've thoroughly enjoyed over the years. Le Guin has a way of sending you to places beyond imagination. My goal with this problem was simple: beyond having a neat forensics challenge in the competition, if I could get just one person to look up Le Guin and read one of her books, I'll have succeeded. Her works enriched many, and I hope that some of the competitors who toiled over this challenge come to love her books just as I do.

The flag, ill_miss_you, is self-explanatory... I was devastated at the news of such a loss. It's always tough when the world loses a great mind, and tougher still when that mind was the source of such joy in your life. Her books were an escape, and I thank her for that. I'll leave you with this conversation I had with a competitor who solved this challenge, as it means a lot to me to know that someone added her to their reading list:

ztaylor54 - Today at 11:36 AM
One of my favorites of all time. Science Fiction. I recommend The Left Hand of Darkness and The Lathe of Heaven
She recently passed, so this problem is a bit of a tribute.
accio-books - Today at 11:37 AM
I noticed :(
I'll add them to my to-read list
ztaylor54 - Today at 11:37 AM
I figured if I could get her name out to several people, I'd be doing right by her - bringing her just one new reader will make me immensely happy.
accio-books - Today at 11:40 AM
I'm now wondering what she has to do with the flag though :thinking:
Thanks for the help
ztaylor54 - Today at 11:45 AM
Oh, she's just the means to an end.
And once you read some of her work, especially The Lathe of Heaven, you'll appreciate what I just said there.

I encourage anyone reading this to give The Lathe of Heaven a read. You can find the full text here, as well as on Amazon here. While the text for this problem could have been anything from the Articles of Confederation to the Bee Movie script, I chose Le Guin's text so that people might be inspired to dig deeper into her work. So yes, in the scope of the problem, she was simply a means to an end. A Special Ending, perhaps.

Zachary Taylor

Zachary Taylor

Founder and lead visionary behind Signum. I've been an entrepreneur for as long as I can remember, and a tinkerer long before that.

View Comments...