After being at my last job for almost three years, it was hard to walk away from such an awesome group of engineers. I wanted to leave them something interesting — so I secretly built a Capture The Flag which kicked off after my last day ended.

The only thing I told the team is that there’s a ~$200 prize waiting for the first person to finish a CTF that I had built, and eventually they would find out how to start it.

With permission from the CTO (thanks Aaron Higbee!) my goal was to build something that required spelunking through our codebases, applications, and maybe learning something new along the way.

The Starting Line

To kick off the whole process, I scheduled a Cofense PhishMe scenario to deliver to the engineers after my company email account had been deactivated. I made the email appear to come from my me (my account was deactivated by this point)

I built this as a Double Barrel Scenario, which means after the phishing email (above) delivered, it waited a predetermined amount of time, then delivered a followup lure. I built the lure to appear as if it came from the same email address. I wanted to be sure people didn’t miss the start of the CTF, so I tipped them off in the lure:

Wait nevermind. My email isn’t working, that was just a phishing email. Or maybe it was the start of the CTF?

By this point, hopefully everyone understood the first email was the lead-in to the CTF they were waiting for.

To get started, they had to go back and click the dinosaur — which took them to the education for the scenario. The education was just a trollface.

Viewing the source for the education told them they were on the right track.

<!-- You're on the right track! -->
<img alt="Could it be a clue?" src="" style="height: 100%;">

The image was hotlinked from an external site, with the name check_out_index2.html.jpg — leading users to visit index2.html at the host.

A handful of people missed that clue and tried visiting the root of the domain, which clued them into going back and paying better attention:

Nope. Nothing here. Maybe you need more Education.

Education being capitalized here was an extra clue — the content they were viewing comes from the Education rails model. Visiting index2.html finally brought users to the start of the CTF:

You found the beginning of the CTF!
Your goal is to keep finding tokens until you reach the end. Each time you find a token, navigate to /TOKEN.html on this domain for a clue to the next token.
Keep track of how you found each token - you'll need to submit a writeup to claim the prize. Please don't try to brute force them!
Most of the tokens will look like a UUID. The first one is f9f69912-6347-4c69-94e7-b1e742c9c6c4 - go ahead and try it when you're ready to get started.

The first token is right there, but a surprising number of people still tried visiting /TOKEN.html first, which would give them another nudge.

Woah there. Slow down. Go back and read the whole thing, alright? Your next token is not TOKEN.

f9f69912–6347–4c69–94e7-b1e742c9c6c4.html takes us to Stage 1.

Stage 1

You made it to Stage 1 already. Go you!
Hey, remember that dinosaur? Now that I think about it, I am pretty sure it was a steganosaurus. You can tell by its tail. If you were hungry, you could probably take 56 bytes out of that thing.

This clue leads users to find that the dinosaur picture has some sort of Steganography. Saving the image from the email and extracting the tailing 56 bytes yields the next token.

$ tail -c 56 stegosaurus.jpg
Your next token is d4138880-918b-4300-ba61-b21453a3e4f8

d4138880–918b-4300-ba61-b21453a3e4f8.html leads us to Stage 2

Stage 2

You made it to Stage 2. You’re almost there! Actually… you have a long way to go.
I thought about being nice and just giving you the next token, but I broke it somehow. Here it is: 6s3ss094–32r6–450r-o0q2–1180618rn2q1

If users tried that UUID as the next token, they were presented with a reminder that the token is broken:

I said that token was broken. Why did you try it anyway? Go back and try again.

Going back and viewing the source, we see a JavaScript function named fixTheBrokenToken (a simple ROT13).

function fixTheBrokenToken(s) {
return (s ? s : this).split('').map(function(_)
if (!_.match(/[A-Za-z]/)) return _;
c = Math.floor(_.charCodeAt(0) / 97);
k = (_.toLowerCase().charCodeAt(0) - 83) % 26 || 26;
return String.fromCharCode(k + ((c == 0) ? 64 : 96));

Passing our broken token to the function fixes it for us.

6f3ff094–32e6–450e-b0d2–1180618ea2d1.html leads us to Stage 3.

Stage 3

You made it to Stage 3. Impressive, I guess.
I’m sorry to disappoint you, but the next token is not a UUID. It’s some other interesting string. I could only remember the beginning of it “8342lA” — you’ll have to find the rest. Good luck landing on the next stage!

The clue here lead to grepping the codebase for our landing application to complete the rest of the string.

8342lAS3KIH425DFJE39.html takes us to Stage 4

Stage 4

You made it to Stage 4. Not bad. Not great either, but not bad.
I left the next token in Slack. I tried to put it in #7e47db2037c937d77f3b38ed47c868bc but it turns out that is too long for a channel name. I guess I’ll have to find some other name for the channel.

This also came with a picture:

The clue lead users to search rainbow tables (or google, because who needs rainbow tables any more?) for 7e47db2037c937d77f3b38ed47c868bc — discovering that it is an MD5 hash of happyeaster

Upon joining the #happyeaster slack channel, users would see that I had pinned the next token.

eda2c850-e5b0–426c-b067-b0d77280207c.html takes us to Stage 5

Stage 5

You made it to Stage 5?! Maybe I should have made this harder.
You’re going to have to do a little work to get the next token. It’s a thing_env Terraform workspace name in Thing Cloud Resources.

To find this token, users had to look at the list of Terraform Workspaces for a specific terraform configuration.

72b58724-bbef-4ea9–9c6b-4ecc5f2257fb.html takes us to Stage 6

Stage 6

You made it to Stage 36 of 43. Actually you only made it to 6, and I dont know how many there are.
Remember when you started this whole thing from a phishing email, and then you got a lure? Or maybe I have that backwards…. either way, one of those might have your next token…. or maybe I got that backwards too…

This sent users back to the original emails they received when they started. Viewing the source of the lure email yields a token hidden in an HTML comment.

The token is a545103f-8aba-4942-82c9-14059a67d144
-->Wait nevermind. My email isn't working, that was just a phishing email. Or maybe it was the start of the CTF?

If users tried to use that token as-is, they were presented with an error:

You didn’t quite get that right. Go back and read the clue again.

The clue alluded to the fact that maybe the token was backwards too. Reversing the token yielded 441d76a95041–9c28–2494-aba8-f301545a

441d76a95041–9c28–2494-aba8-f301545a.html takes us to Stage 7

Stage 7

Yay, you made it! Not all the way to the end, but Stage 7 is pretty impressive.
Where did the phishing emails even come from? Maybe there’s a description that would help you find the next token…

This clue leads users to log into the PhishMe application where the emails came from. Then they had to find the scenario I launched these emails from, which took some digging because I launched it under a fake company, and flagged it as a test scenario so it wouldn’t show on the default dashboard views. Once they found it, they had to look at the description:

Hey, you found me! The next token is: d0b8503a-52c7–4ff8-a3d6-fd211505c6ad

d0b8503a-52c7–4ff8-a3d6-fd211505c6ad.html takes us to Stage 8

Stage 8

Now that you’ve made it to Stage 8 you probably wonder how many more stages there are. Me too.
One time there was a Great Jira Fire. I still remember the first ticket I created after we got a new Jira instance.

A couple years ago, there was a hardware failure which took down Jira for some time — it has been known as the Great Jira Fire ever since. While that was being recovered, we stood up a new instance.

To find this token, users had to query Jira for the first ticket I created in this new instance. They had to write the JQL from scratch because once an account is deactivated, Jira won’t pre-populate it in dropdowns.

reporter = "" ORDER BY createdDate ASC

This took them to a ticket that was closed about two years ago — with a recent edit on the comment, adding the token.

efa4b6eb-42fd-4b75–85e2-ef067344a564.html takes us to Stage 9

Stage 9

Hooray, you made it to Stage 9.

The text wasn’t much help here, but there was a picture. Not just any picture — my Slack avatar.

In case you’re wondering, this is how my daughter draws me.

The source for this page gave another hint that the answer might be on Slack (my username has been @deprecated for the past few weeks)

<p>Hooray, you made it to <b>Stage 9</b>.</p>
<img alt="@deprecated" src="slack_profile.jpg">

Viewing my profile in Slack reveals the next token

I was worried that users might find this token early (they did) so I required another transform here. If they tried using this token directly, they were presented with an error.

Well this is awkward. You came here because you thought you found a token in my slack profile. Did you think it would be that easy?

To correct the token, there was also an HTML comment on the page

<!-- token_when_you_find_it.gsub(/[aeiou]/,'x') -->

After replacing vowels with x, they got ended up with the right token. I’m not sure why I put the whole set of vowels in there, since UUIDs are hexadecimal ¯\_(ツ)_/¯

x46f8x7d-fb1d-4xc6-x3xx-c1x35bd0f15f.html takes us to Stage 10

Stage 10

Wait, you made it to Stage 10 just by looking at my profile picture? That’s creepy.
Anyway, you should probably take a look at the Company that launched this whole scenario. Be sure to take Notes.

This was a fairly low hanging stage. Users had already found the Scenario I had launched for this — now they just needed to look at the Company I created to launch it.

To make this slightly more difficult, I flagged it as a Trial company so it wouldn’t appear on most dashboards.

The token could be found In the Notes field for that Company.

Oh man, will you ever stop looking for those tokens? Here’s the next one:

If users tried to use that as their next token, they were presented with another error. Once again I was afraid that they might stumble upon this before reaching the stage.

You did something wrong. It’s okay. Go back and look a little bit harder.

Hidden in the HTML again was a comment, letting users know they need to upcase the token (hooray for case sensitive nginx)

<!-- Also, upcase it for good measure. -->

A41A75783–32B9–4648-BB53–7F7CE5D433BF.html takes us to Stage 11

Stage 11

That one was tricky, but you made it to Stage 11!

This was another easy one — it’s just a base64 encoded string. Decoding it yields the token.

The next token is: ccb0384f-e80e-4336-b293-cf3808efb981

ccb0384f-e80e-4336-b293-cf3808efb981.html leads us to Stage 12

Stage 12

You figured that one out too? Good job on making it to Stage 12
Your next token is the SHA of when we Added Trololo guy to mgmt.

Mgmt is another one of our application repositories. A simple git search finds the commit we are looking for. I never did check to see what Trololo guy is, I wonder if he’s still around.

9d78bbdf9c50821d07533d85239cc4c415d24571.html takes us to Stage 13

Stage 13

You made it to Stage 13. You should stop to take a moment and pat yourself on the back.
Have you ever sat outside and spent the day cloud watching?
There sure are a lot of them… you might want to watch them from a stage. I wonder if any of them will ever land?
If you were watching them between 29/3/2018 and 30/3/2018 you might have seen your level13token

This one had a whole bunch of clues. Users had to search the cloudwatch logs from the staging instance of our landing application, looking for the level13token keyword, between those two dates. When they did that, they would see HTTP requests that I made with the token.

Parsing the UUID out of that request yields our next token.

b76d2743-f9b4–44a1–853e-522450ba37d1.html takes us to Stage 14

Stage 14

That one probably took some work, but you did it! You made it to Stage 14
You know what they say… “Give a bot a fish and it scores for a day, teach a phishbot the level14token and it gives it away.” Or something like that.

One of our Slack bots is named phishbot. It has the common PlusPlus / Karma script (ie. @user++ for being awesome) and you can retrieve scores with the score command.

It doesn’t limit you to incrementing / decrementing scores for slack users, so prior to launching the CTF, I privately messaged it to increment the score for level14token

When a user messaged the bot score level14token to get the score, they would see the token.

I don’t know why the bot talks about raisins.

52a6ed56–5575–4656-aee3-d50a84031f22.html leads to Stage 15

Stage 15

You made it to Stage 15! Yay!
Did you know there’s two halves to a brick? If you ask nicely, each half might give you half of the next token.

Our team has two engineering managers — Brian and Rick. Or Brick, if you want to address them together. I gave Brian and Rick a heads up that people might be asking for tokens (thanks for playing along, Brian and Rick!). Each one of them received half of the token to give out.

  • Rick had 85-b92a-06d35358a095
  • Brian had 662c19ef-c71a-48

Putting them together yields the next token.

662c19ef-c71a-4885-b92a-06d35358a095.html leads to Stage 16

Stage 16

Good job, you got the brick tokens and made it to Stage 16!
My robot name is 31115c3b-d3d0–44ad-ace5–42165f0a0f91. I’m sad because I am Disallowed from seeing the next stage.

If users tried using the UUID in the clue as a token, they were given an error.

Try harder, agent.

To find the token, users had to look at the domain’s robots.txt file. It contained hundreds of directives, all with random UUIDs.

[about 100 directives truncated]
User-agent: c9b936b7-243a-4dac-a705-c949f97bfa9b
Disallow: 5b97f687-7657-4f22-a87e-82996a32f1bd.html
User-agent: 18eaaf58-b5ea-4b62-8ee8-04c3efd8d59e
Disallow: cc057112-2cb0-4eb3-a707-1dd5d270a22e.html
User-agent: 31115c3b-d3d0-44ad-ace5-42165f0a0f91
Disallow: 2ba43e40-b053-4314-ba19-57942a361631.html
User-agent: e5b5a996-ebbb-4bb4-a83d-f42ee7c9b09b
Disallow: 05fdd41b-bdc6-4480-bca6-a8dff5f217df.html
User-agent: 7b28988e-1d1a-44bf-910f-28dd1d4e0aab
Disallow: f08e562b-d157-49f6-8361-337e694468d8.html
[about 100 directives truncated]

One directive is for the 31115c3b-d3d0–44ad-ace5–42165f0a0f91 useragent — and it disallowed access to the 804d6a5f-fcce-43d3-b706–84909760bc30.html

804d6a5f-fcce-43d3-b706–84909760bc30.html leads to Stage 17

Stage 17

Congratulations, you’re not a robot! You made it to Stage 17. Maybe I made this too easy…

This one also had two buttons and one picture:

The source for it was not a lot of help

<p>Congratulations, you're not a robot! You made it to <b>Stage 17</b>. Maybe I made this too easy...</p>
function eatCookies() {
alert('Who is this guy? I bet if you asked him, he could give you a sequence of stages to follow and come back.');
function maybeYouDidItRight() {
if (document.cookie.length == 36) {
alert('Your token might be ' + document.cookie);
} else {
alert('You probably did something wrong.');
<p><button onclick="eatCookies()">Help.</button></p>
<p><button onclick="maybeYouDidItRight()">I think I figured it out?</button></p>
<p>i<img src="who_is_this_guy.jpg"></p>

This one was tricky for a few reasons. If users landed here after the previous step, their document.cookie contained a 36 character UUID. When they clicked the “I think I figured it out?” button, they would get an alert:

Your token might be ec9be1b4–56bd-41c1–9be7-a75dcfcad9e8

When users tried that as their next token, they would get an error message

Nope, that’s not the right token. Did you really think I would just put it in the cookie like that?

The reason that didn’t work is because Stage 16 (immediately before this one) set document.cookie to a fake token:

<script>document.cookie = 'ec9be1b4–56bd-41c1–9be7-a75dcfcad9e8';</script>

Clicking the Help button gave users a different alert

Who is this guy? I bet if you asked him, he could give you a sequence of stages to follow and come back.

Users had to perform a reverse image search (ie. via Google Images) to discover that this guy was Fibonacci (or I guess they could just know that?)

Once users figured out who it was, they would then have to visit each of the previous stages exactly following the Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13

I made it so they had to visit Stage 1 twice in a row by having it initialize document.cookie with an incorrect value on the first request, then conditionally set it to the correct value on the second request.

if (document.cookie == '1fb567d0') {
document.cookie = '1b6f5241';
} else {
document.cookie = '1fb567d0';

Note 1b6f5241 is the correct beginning of the token, 1fb567d0 is not.

After visiting that twice to get the correct start of the token, the next stages in the fibonacci sequence each appended to the existing cookie. 2, 3, 5, 8, and 13 had scripts similar to the following:

<script>document.cookie += '-f281';</script>

Someone spotted these scripts before reaching this stage and thought it was a bug.

Additionally, visiting stages which were not in the sequence (ie. Stage 4) would cause the whole cookie to be cleared:

<script>document.cookie = '';</script>

After users managed to go through each of those stages in the exact order, building the correct cookie, and then came back to Stage 17, the alert would show them their correct token.

Your token might be 1b6f5241-f281–4e00–9c82-e1e105dec126

1b6f5241-f281–4e00–9c82-e1e105dec126.html leads to Stage 18

Stage 18

You’re seriously almost there now. You made it all the way to Stage 18, and theres probably only 20 stages.
Wouldn’t it be funny if after all this work, the next token is just something I randomly tweeted? But maybe it needs to be downcased.

I figured after going through all that work, I would make an easy one. I tweeted the token for this stage. I tweeted it in uppercase, and specified it needed to be downcased in the clue, just in case anyone saw it on my feed before reaching this stage.

5d8121a9–1a6c-4909–8626–56557e601253 leads to Stage 19

Stage 19

I’m sorry. I lied. There’s not really 20 stages. I spent the money on a cake instead. Thanks for playing along!

And another picture

It turns out I didn’t actually spend that money. The picture was named a108f414–7ecc-42d3-a7c4–301b9ac9bd50.jpg, which looks like another UUID.

a108f414–7ecc-42d3-a7c4–301b9ac9bd50.html leads to Stage 20

Stage 20

This was finally the end — nothing more to solve. I gave the users an email address to send their writeup to, so I could send the reward to the first person to complete it.


Overall I had a lot of fun building this — I’ve never built a CTF before, but in the future I will probably give myself a little more than three days to think of the challenges.

One of the developers made it through these in exactly two hours — I was hoping it would at least take people through the weekend to solve everything. Next time I will probably build one or two challenges that are time gates (ie. adding yourself to an email distribution list which receives the next clue after 24 hours) to give others a chance to catch up.

P.S. Most of the challenges are no longer functional, and obviously required internal access — so please don’t bother trying to poke around and solve them.

Like what you read? Give Matt Metzger a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.