Holiday Hack 2019: Get Access To The Steam Tunnels
Objective
Terminal - Frosty Keypad
Challenge
On my way to find Minty in the dormitory, I find the door locked, with Tangle Coalbox outside:
Hey kid, it’s me, Tangle Coalbox.
I’m sleuthing again, and I could use your help.
Ya see, this here number lock’s been popped by someone.
I think I know who, but it’d sure be great if you could open this up for me.
I’ve got a few clues for you.
- One digit is repeated once.
- The code is a prime number.
- You can probably tell by looking at the keypad which buttons are used.
Clicking on the keypad gives me the challenge:
Solution
From the image, it’s clear that the buttons 1, 3, and 7 have the most pushes. I also know from the hints that one of the digits is repeated, so it’s got to be four digits.
If I enter 1, 3, 3, 7, and hit enter, the message “INVALID CODE!” scrolls across the display at the bottom. I’ll open the Firefox dev tools (Ctrl+Shift+I) and switch to the Network tab. When I enter a code again, I see that no network traffic is sent, until I hit submit, and then I see:
That’s a GET request to https://keypad.elfu.org/checkpass.php?i=[pin]&resourceId=e6022b8c-061e-420e-a3dc-2bb0493aed28
.
I’ll write a quick Python script that will look at all numbers 0-9999. For each number, it will check:
- Digits only 1, 3, and 7.
- Length of the string is 4.
- The number is not prime.
If that’s all true, it will try to submit it as the pin, and check the response.
#!/usr/bin/env python3
import requests
import sys
def is_prime(x):
for i in range(2, x):
if (x % i) == 0:
return False
return True
for i in range(1000, 10000):
if not set(str(i)) == set("137") or len(str(i)) != 4 or not is_prime(i):
continue
print(f"[+] Found potential pin: {i}")
resp = requests.get(
f"https://keypad.elfu.org/checkpass.php?i={i}&resourceId=de312b0e-9d6a-4c2a-92c5-8fe074c64d37"
)
if resp.json()["success"]:
print(f"[+] Found correct code: {i}")
Running it gives the pin:
$ python3 lock.py
[+] Found potential pin: 1373
[+] Found potential pin: 1733
[+] Found potential pin: 3137
[+] Found potential pin: 3371
[+] Found potential pin: 7331
[+] Found correct code: 7331
Back in the game, I can enter the correct pin and unlock the door:
Schlage Afterall
While collecting images to make this post, I noticed something on the lock itself:
It’s a Schlage lock. I suspect that means the Schlage template from Deviant Ollam’s GitHub would have worked fine.
Hints
On opening the door, Tangle offers some advice, but it’s for challenges I’ve already solved:
Yep, that’s it. Thanks for the assist, gumshoe.
Hey, if you think you can help with another problem, Prof. Banas could use a hand too.
Head west to the other side of the quad into Hermey Hall and find him in the Laboratory.
Terminal - Holiday Hack Trail
Challenge
Now inside the dorm, I find Minty Candycane to the right of the door next to a terminal:
Hi! I’m Minty Candycane!
I just LOVE this old game!
I found it on a 5 1/4” floppy in the attic.
You should give it a go!
If you get stuck at all, check out this year’s talks.
One is about web application penetration testing.
Good luck, and don’t get dysentery!
When I enter the terminal, I’m playing The Holiday Hack Trail, a take on the old Oregon Trail game:
Solution
Easy
I’ll start with the easy game, because I want to win. The first screen is an opportunity to buy things:
Since I’m going to figure out how to cheat the game, I doesn’t really matter what I do here. I’ll hit BUY to move on. Now I’m at the general game:
At the top of the game, there’s a box with a HTML input
object of class urlbar
that holds an interesting uri:
hhc://trail.hhc/trail/?difficulty=0&distance=0&money=5000&pace=0&curmonth=7&curday=1&reindeer=2&runners=2&ammo=100&meds=20&food=400&name0=Michael&health0=100&cond0=0&causeofdeath0=&deathday0=0&deathmonth0=0&name1=Savvy&health1=100&cond1=0&causeofdeath1=&deathday1=0&deathmonth1=0&name2=Mathias&health2=100&cond2=0&causeofdeath2=&deathday2=0&deathmonth2=0&name3=Jo&health3=100&cond3=0&causeofdeath3=&deathday3=0&deathmonth3=0
I’ll notice the second value is distance, currently 0. What if I change that to 5000, and hit the >
button? The distance remaining goes to 3000 (8000-5000):
If I change it to 8000, the distance remaining goes to 0. If I then hit GO, I win:
Medium
It looks like the levels are not only how difficult the overt game is, but also how difficult it is to hack. When I select Medium, the uri is still there, but now without the values:
Still, the values are stored in the page. Looking at the source, I see a div
with id="statusContainer"
inside a form:
<form id="urlform" action="/trail/" method="post" class="urlform">
<input type="text" class="urlbar" value="hhc://trail.hhc/trail/" name="url" size="40">
<input type="hidden" class="playerid" name="playerid" value="JebediahSpringfield">
<input type="submit" class="urlbtn" name="submit" value=">"/>
</form>
</div>
<h1 class="tight"><img src="art/pieces/hht.png" alt="header" hidden="True"></h1>
<h1 class="tight"><img src="art/pieces/header.png" alt="header" hidden="True"></h1>
</div>
<div id="page-container"> <table id="action"><form id="actionform" action="/trail/" method="post">
<table id="progress">
<tr><td><b>Distance Remaining</td><td><b>Day</td><td><b>Month</td><td><b>Difficulty</td><td><b>Pace</td></tr>
<tr><td>8000</td><td>1</td><td>August<td>Medium</td></td><td><select name="pace" class="pace"><option value="0" selected>Steady</option><option value="1">Strenuous</option><option value="2">Grueling</option></select></td></tr>
</table>
<!-- <table id="displayWindow" class="noborder">
<tr>
<td><img src="art/pieces/mountains.png" id="mountainSpot"></td>
</tr>
<tr><td><table id='displayWindowInner' class="noborder">
<tr>
<td><img src="art/pieces/black300.png" id="leadSpot"></td>
<td><img src="art/pieces/black70.png" id="actionSpot"></td>
<td><img src="art/pieces/reindeer1.png" id="reindeerSpot"></td>
<td><img src="art/pieces/sleigh.png" id="sleighSpot"></td>
<td><img src="art/pieces/black100.png" id="behindSpot"></td>
</tr>
</table></td></tr>
<tr>
<td><img src="art/pieces/land.png" id="landSpot"></td>
</tr>
</table> -->
<table width="640px">
<img src="art/pieces/mountains.png" id="mountainSpot" class="tight"><br>
<img src="art/pieces/black300.png" id="leadSpot" class="tight">
<img src="art/pieces/black70.png" id="actionSpot" class="tight">
<img src="art/pieces/reindeer1.png" id="reindeerSpot" class="tight">
<img src="art/pieces/sleigh.png" id="sleighSpot" class="tight">
<img src="art/pieces/black100.png" id="behindSpot" class="tight"><br>
<img src="art/pieces/land.png" id="landSpot" class="tight">
</table> <br><br>
<input type="hidden" class="playerid" name="playerid" value="JebediahSpringfield" />
<button type="submit" class="btn" name="action" value="meds">Meds</button>
<button type="submit" class="btn" name="action" value="hunt">Hunt</button>
<button type="submit" class="btn" name="action" value="trade">Trade</button>
<button type="submit" class="btn" name="action" value="go" onmouseover="reindeerWiggle()" onmouseout="reindeerWaggle()">Go</button><br><br>
<div id="statusContainer">
<input type="hidden" name="difficulty" class="difficulty" value="1">
<input type="hidden" name="money" class="difficulty" value="3000">
<input type="hidden" name="distance" class="distance" value="0">
<input type="hidden" name="curmonth" class="difficulty" value="8">
<input type="hidden" name="curday" class="difficulty" value="1">
<input type="hidden" name="name0" class="name0" value="Joshua">
<input type="hidden" name="health0" class="health0" value="100">
<input type="hidden" name="cond0" class="cond0" value="0">
<input type="hidden" name="cause0" class="cause0" value="">
<input type="hidden" name="deathday0" class="deathday0" value="0">
<input type="hidden" name="deathmonth0" class="deathmonth0" value="0">
<input type="hidden" name="name1" class="name1" value="Emmanuel">
<input type="hidden" name="health1" class="health1" value="100">
<input type="hidden" name="cond1" class="cond1" value="0">
<input type="hidden" name="cause1" class="cause1" value="">
<input type="hidden" name="deathday1" class="deathday1" value="0">
<input type="hidden" name="deathmonth1" class="deathmonth1" value="0">
<input type="hidden" name="name2" class="name2" value="Ron">
<input type="hidden" name="health2" class="health2" value="100">
<input type="hidden" name="cond2" class="cond2" value="0">
<input type="hidden" name="cause2" class="cause2" value="">
<input type="hidden" name="deathday2" class="deathday2" value="0">
<input type="hidden" name="deathmonth2" class="deathmonth2" value="0">
<input type="hidden" name="name3" class="name3" value="Evie">
<input type="hidden" name="health3" class="health3" value="100">
<input type="hidden" name="cond3" class="cond3" value="0">
<input type="hidden" name="cause3" class="cause3" value="">
<input type="hidden" name="deathday3" class="deathday3" value="0">
<input type="hidden" name="deathmonth3" class="deathmonth3" value="0">
<input type="hidden" name="reindeer" class="reindeer" value="2">
<input type="hidden" name="runners" class="runners" value="2">
<input type="hidden" name="ammo" class="ammo" value="50">
<input type="hidden" name="meds" class="meds" value="10">
<input type="hidden" name="food" class="food" value="200">
<input type="hidden" name="hash" class="hash" value="HASH">
</div></form></table>
HTTP has no built in way to keep state. Typically this is done by using something like a cookie to associate a user with a request coming into the server, but it is also possible to send all the state to the user, and then have them send it back to the server, so that server doesn’t have to keep track of it. All those inputs
of type hidden
are the game state. Of course the risk here is that the user can change it.
I’ll open the Firefox dev tools and edit the html so that the distance is 8000:
This won’t lead to any changes in the game, but when I click GO, I win:
Hard
When a server wants to send state to the user and then get it back, it can use a hash to make sure it knows the integrity of the information. In fact, in the medium game above, there was a hidden
field called hash
, but it was just set to “HASH”. In the hard version, the hash is populated:
If I change the distance to 8000 and then GO, I get this error:
Sorry, something’s just not right about your status: badHash You have fallen off the trail.™
Best practice would be to use a keyed hash, with a long random password that couldn’t be brute forced. I’m going to guess that this 32 character has is an MD5. So if I can figure out what values are hashed and how they are combined, I can generate the new hash value after I alter the data, and still cheat.
There’s a hint inside Chris Elgee’s talk at KringleCon 2019 when he talks about how to do this kind of hash. The code in his example looks to apply to this game:
Zooming in on the code:
The hashvalue
is a combination of various parameters from the hidden inputs. I started by building my own string like this in a Python3 terminal. While food doesn’t show up in the code, I’m going to assume it does just off screen:
>>> money, distance, day, month, reindeer, runners, ammo, meds, food = 1500, 36, 2, 9, 2, 2, 10, 2, 92
>>> hashvalue = f'{money}{distance}{day}{month}{reindeer}{runners}{ammo}{meds}{food}'
>>> hashlib.md5(hashvalue.encode()).hexdigest()
'e43f0d792ad0fc0f88efb1d8708d8a03'
Unfortunately, this did not match the hash
of cc42acc8ce334185e0193753adb6cb77 from the page source. I tried without food, and some other combinations, but didn’t have any luck.
Then it occurred to me that status
could be holding integers, as opposed to strings, and it worked:
>>> hashvalue = str(money+distance+day+month+reindeer+runners+ammo+meds+food)
>>> hashlib.md5(hashvalue.encode()).hexdigest()
'cc42acc8ce334185e0193753adb6cb77'
Later it occurred to me to look up that original hash on hashes.org. It finds it, and has a value of 1655. That fits with the finding.
Now that I understand the algorithm used to create hashes, I can recreate the hash values, so I can change values and update the hash. I’ll change the distance to 8000, and calculate the new hash:
>>> distance = 8000
>>> hashvalue = str(money+distance+day+month+reindeer+runners+ammo+meds+food)
>>> hashlib.md5(hashvalue.encode()).hexdigest()
'e4f67a0e4293245fba713c412fc63e28'
Now I update that hash and hit GO:
And with any of these levels, if I care about score, I can maximize that even more. I tried to set money to 150000, but I got back an error:
Sorry, something’s just not right about your status: badMoneyAmt You have fallen off the trail.™
The max money seems to be 65535. I can also set the date to 12/25, so that when I go one more time, it’s 12/26, 364 days until Christmas. Reindeer seem to max out at 255.
Calculate the hash, update values, and win with high score:
Hints
On solving any of the difficulties, I get hints from Minty on how to create keys:
You made it - congrats!
Have you played with the key grinder in my room? Check it out!
It turns out: if you have a good image of a key, you can physically copy it.
Maybe you’ll see someone hopping around with a key here on campus.
Sometimes you can find it in the Network tab of the browser console.
Deviant has a great talk on it at this year’s Con.
He even has a collection of key bitting templates for common vendors like Kwikset, Schlage, and Yale.
There’s one additional set of hints when solving in hard mode that I’ll find in objective 11.
Accessing Steam Tunnels
Find the Key
When I walk into the open room to the right of Minty, I see a man hop up into the door and close it behind him:
Right away I notice the key on his belt. I’ll open the dev tools and refresh the page without cache (Ctrl+Shift+r) and look at the images. I can hover over them to see the images and find this one. It’s krampus.png
:
Interestingly, Krampus is a half goat, half demon who punishes kids who misbehave during the Christmas season. The two-pointed hat matches the Krampus’ horns.
Bitting Machine
There’s a bitting machine on the desk in this room, which allows me to enter six digits, and then cut a key:
When I hit Cut, a key appears:
I can click the key to download it.
Measure Key
Deviant Ollam’s awesome KringleCon 2019 talk, Optical Decoding of Keys walks through how to go from an image of a key to the bitting value of the key, which defines how to cut a new copy.
I’ll pull the image into Gimp and cut out the key part into a new image. Then I’ll use the rotate tool or Shift+R to turn the image until the key is sitting on its side (turns out -90 degrees is perfect):
Now I’ll grab the templates from Deviant’s GitHub with git clone https://github.com/deviantollam/decoding
. I played with various templates, but couldn’t get a great fit.
I decided to make my own. I made two keys that contained all the bitting values 0-9. I opened the first key which has values 0-5, and dropped it into Gimp. Then I can create a horizontal line by selecting the pencil tool and clicking once where I want the line to start, and then moving to the end point with Ctrl+Shift held down, and clicking at the end point. I’ll do this in a new layer, so I can remove the demo key later. I’ll them set the first key invisible in the layers panel, bring in the second, and draw those lines as well. I end up with:
Now I can click the eye to hide the imported key, and bring in the krampus key. Using Shift+S to go into scale mode, and m to go into move mode, I scale the krampus key so that it fits under my lines. Now I can label the bittings:
Make Key
Now I can go back to the bitting machine, and make a key:
It looks very similar to the krampus key:
Enter the Steam Tunnels
Now I can head up into the room and find a keyhole:
Clicking on the keyhole takes me to Minty’s Closet:
Clicking on the keychain allows me to select a file, and when I select the key I made from the bitting machine, a key appears on screen that moves around with my mouse:
If I select a wrong key, it won’t unlock:
However, when I select the one based on the measurements from the Krampus’ key, it opens:
Talk to Krampus
To solve this objective, I need the first and last name of the person who took the turtle doves. I’ll go into the open door, and walk to the end of the tunnel where I find Krampus:
Hello there! I’m Krampus Hollyfeld.
I maintain the steam tunnels underneath Elf U,
Keeping all the elves warm and jolly.
Though I spend my time in the tunnels and smoke,
In this whole wide world, there’s no happier bloke!
Yes, I borrowed Santa’s turtle doves for just a bit.
Someone left some scraps of paper near that fireplace, which is a big fire hazard.
I sent the turtle doves to fetch the paper scraps.
Entering “Krampus Hollyfeld” solves the objective.