Holiday Hack 2021: Frost Tower Website Checkup
Objective
Terminal - The Elf Code
Challenge
The Elf Code archade game is back in Santa’s dining area, along with Ribb Bonbowford, and this year it’s got a Python logo on the side:
Hello, I’m Ribb Bonbowford. Nice to meet you!
Are you new to programming? It’s a handy skill for anyone in cyber security.
This here machine lets you control an Elf using Python 3. It’s pretty fun, but I’m having trouble getting beyond Level 8.
Tell you what… if you help me get past Level 8, I’ll share some of my SQLi tips with you. You may find them handy sometime around the North Pole this season.
Most of the information you’ll need is provided during the game, but I’ll give you a few more pointers, if you want them.
Not sure what a lever requires? Click it in the
Current Level Objectives
panel.You can move the elf with commands like
elf.moveLeft(5)
,elf.moveTo({"x":2,"y":2})
, orelf.moveTo(lever0.position)
.Looping through long movements? Don’t be afraid to
moveUp(99)
or whatever. You elf will stop at any obstacle.You can call functions like
myFunction()
. If you ever need to pass a function to a munchkin, you can usemyFunction
without the()
.
There’s also four hints in the badge:
- Not sure what a lever requires? Click it in the
Current Level Objectives
panel. - You can move the elf with commands like
elf.moveLeft(5)
,elf.moveTo({"x":2,"y":2})
, orelf.moveTo(lever0.position)
. - Looping through long movements? Don’t be afraid to
moveUp(99)
or whatever. You elf will stop at any obstacle. - You can call functions like
myFunction()
. If you ever need to pass a function to a munchkin, you can usemyFunction
without the()
.
Solution
There are 10 levels to beat. I’ll include my code for each level, along with this video showing how I solved each:
level 1
import elf, munchkins, levers, lollipops, yeeters, pits
elf.moveLeft(10)
elf.moveUp(11)
level 2
import elf, munchkins, levers, lollipops, yeeters, pits
all_lollipops = lollipops.get()
for lollipop in all_lollipops[::-1]:
elf.moveTo(lollipop.position)
elf.moveTo({"x":2, "y":2})
level 3
import elf, munchkins, levers, lollipops, yeeters, pits
lever0 = levers.get(0)
elf.moveTo(lever0.position)
lever0.pull(lever0.data()+2)
elf.moveTo({"x": 2, "y": 2})
level 4
import elf, munchkins, levers, lollipops, yeeters, pits
lever0, lever1, lever2, lever3, lever4 = levers.get()
elf.moveLeft(2)
lever4.pull("a string")
elf.moveUp(2)
answers = [(lever3, True),
(lever2, 2),
(lever1, []),
(lever0, {})]
for lever, ans in answers:
lever.pull(ans)
elf.moveUp(2)
level 5
import elf, munchkins, levers, lollipops, yeeters, pits
lever0, lever1, lever2, lever3, lever4 = levers.get()
elf.moveLeft(2)
lever4.pull(lever4.data() + " concatenate")
elf.moveUp(2)
answers = [(lever3, lambda x: not x),
(lever2, lambda x: x + 1),
(lever1, lambda x: x + [1]),
(lever0, lambda x: x.update({"strkey": "strvalue"}) or x)]
for lever, ans in answers:
lever.pull(ans(lever.data()))
elf.moveUp(2)
level 6
import elf, munchkins, levers, lollipops, yeeters, pits
lever = levers.get(0)
data = lever.data()
if type(data) == bool:
data = not data
elif type(data) == int:
data = data * 2
elif type(data) == list:
data = [x+1 for x in data]
elif type(data) == str:
data = data + data
elif type(data) == dict:
data["a"] += 1
elf.moveUp(2)
lever.pull(data)
elf.moveUp(2)
level 7
import elf, munchkins, levers, lollipops, yeeters, pits
for num in range(3):
elf.moveLeft(3)
elf.moveUp(11)
elf.moveLeft(2)
elf.moveDown(11)
level 8
import elf, munchkins, levers, lollipops, yeeters, pits
for lollipop in lollipops.get():
elf.moveTo(lollipop.position)
elf.moveTo({"x": 2, "y": 4})
munch = munchkins.get(0)
data = munch.ask()
munch.answer([k for k,v in data.items() if v == "lollipop"][0])
elf.moveUp(2)
or
import elf, munchkins, levers, lollipops, yeeters, pits
for lollipop in lollipops.get():
elf.moveTo(lollipop.position)
elf.moveTo({"x":8, "y": 0})
lever = levers.get(0)
lever.pull(["munchkins rule"] + lever.data())
elf.moveTo({"x":2, "y": 4})
elf.moveUp(2)
level 9
import elf, munchkins, levers, lollipops, yeeters, pits
all_levers = levers.get()
moves = [elf.moveDown, elf.moveLeft, elf.moveUp, elf.moveRight] * 2
for i, move in enumerate(moves):
move(i+1)
if i < len(all_levers):
all_levers[i].pull(i)
elf.moveUp(2)
elf.moveLeft(4)
munchkins.get(0).answer(lambda l2: sum([x for l1 in l2 for x in l1 if type(x)==int]))
elf.moveUp(2)
level 10
import elf, munchkins, levers, lollipops, yeeters, pits
import time
muns = munchkins.get()
lols = lollipops.get()[::-1]
for i, mun in enumerate(muns):
while abs(elf.position["x"]-mun.position["x"]) < 6:
time.sleep(0.05)
elf.moveTo(lols[i].position)
elf.moveTo({"x": 2, "y": 2})
Frost Tower Website Checkup
Hints
Ribb is impressed:
Gosh, with skills like that, I’ll bet you could help figure out what’s really going on next door…
And, as I promised, let me tell you what I know about SQL injection.
I hear that having source code for vulnerability discovery dramatically changes the vulnerability discovery process.
I imagine it changes how you approach an assessment too.
When you have the source code, API documentation becomes tremendously valuable.
Who knows? Maybe you’ll even find more than one vulnerability in the code.
Wow - even the bonus levels! That’s amazing!
The badge gives a single hint about SQL injection:
- When you have the source code, API documentation becomes tremendously valuable.
Challenge
The objective says to investigate the Frost Tower website, and provides the source.
Solution
The full solution is in this video:
Auth Bypass
Looking at the source, two vulnerabilities become clear. First, much of the site requires the user be logged in, using express-session cookies. These are unique ids with state information stored server side. For each path requiring login, it matches the pattern:
app.get('/dashboard', function(req, res, next){
session = req.session;
if (session.uniqueID){
...[snip]...
} else {
res.redirect("/login");
}
This means that if I can get something into session.uniqueID
, the site will treat me as logged in. This is set on logging in, but it’s also set in the contact form. There’s a query to see if the user is already in the database, and if so, it returns that error:
var rowlength = rows.length;
if (rowlength >= "1"){
session = req.session;
session.uniqueID = email;
req.flash('info', 'Email Already Exists');
res.redirect("/contact");
} else {
For some reason, it’s also setting the uniqueID
there as well. So if I submit to the contact form with the same email twice, it will log in the cookie in that session.
SQL Injection
Most of the site is really good about using parameterized queries for the SQL calls, but in the /detail/:id
path, when the id
value has a comma in it, it will break that up so that it finds all records that match any of the values:
var query = "SELECT * FROM uniquecontact WHERE id=";
if (session.uniqueID){
try {
if (reqparam.indexOf(',') > 0){
var ids = reqparam.split(',');
reqparam = "0";
for (var i=0; i<ids.length; i++){
query += tempCont.escape(m.raw(ids[i]));
query += " OR id="
}
query += "?";
}else{
query = "SELECT * FROM uniquecontact WHERE id=?"
}
When it does this, it builds a query that I can inject into.
Union Injection
The biggest challenge from this point is to build a UNION injection that doesn’t use commas (because they are replaced with “ OR id=”). This post describes how to do just that. Using the structure described there, I’ll build queries to enumerate the database, eventually pulling Jack’s todo list, with the following url:
https://staging.jackfrosttower.com/detail/-1,-1 union select * from ((select 1)A join (select group_concat(id) from todo)B join (select group_concat(note) from todo)C join (select group_concat(completed) from todo)D join (select 5)E join (select 6)F join (select 7)G);-- -
The list includes the position he plans to offer Santa:
Flag: clerk