SANS 2024 Holiday Hack Challenge - Prologue
If you aren't familiar, I highly suggest taking a look at the 2024 SANS Holiday Hack Challenge and the previous years as well. These challenges are so well put together and cover very modern and applicable topics in security.
This year, the challenge is split into four time-released acts. So I will do a write-up for each act. The Prologue drops you off at Frosty's Beach on the Geese Islands. I won't dig into the story too much unless necessary.
Elf Connect
The first objective is the complete the Elf Connect terminal game. The Elf Connect utilizes Phaser, which is an HTML 5 game development framework. So, I might have to learn a bit more about that.
First let's load the game up. Looking in Developer Tools in the Applicaiton tab under storage, we see there are many items. But of interest, are the Cookies for this page. Specifically, CreativeCookieName
seems a little sus and the value looks a lot like a JWT.
This did not pan out though. But it turns out there is a score
variable stored in Session Storage. The score seems to be only stored client-side until a round is complete. After completing a round, you can see the score variable and number of rounds complete gets updated in Session Storage. As a note: there will be nothing here until completion of at least one round. This game me some heartburn initially.
Tracing the application function in the Console Log, we see a request to hhc24-elfconnect.holidayhackchallenge.com
with the parameter round
which returns a JavaScript source file.
Digging into the code, near the beginning we see that the score value is only read at game initialization, so I need to find a way to have the score value exist upon initialization or get re-read at some point. Because I could not figure out how to initialize the variable before load time, I decided just to proxy the traffic and edit the JavaScript file as it was retrieved. Maybe a little bit of a hammer looking for a nail situation.
To do this, I simply set the condition for score
initialization when it does not already exist to initialize at 55000 on line 90 of the response. I had to configure Burp to intercept responses, which is not a default behavior, but that was a simple config setting.
That simple change and completing a round was enough to unseat the previous high score.
Elf Minder 9000
The next objective was to help one of the elves pass the time playing Elf Minder 9000. Looking at the proxied traffic when loading this game, we can see that game2.js
is loaded. This might indicate the presence of a game1
or game
file.
Sure enough though, game.js
returns what appears to be an earlier version of the code.
There is also an interesting section of the game2.js
code. Specifically, towards the beginning in the variable definition section is this line:
const adminControls = document.querySelector('.admin-controls');
If we trace that variable, we find this section of code:
const isEditor = !!urlParams.edit;
if (isEditor) {
adminControls.classList.remove('hidden');
console.log('âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡');
console.log('âš¡ Hey, I noticed you are in edit mode! Awesome!');
console.log('âš¡ Use the tools to create your own level.');
console.log('âš¡ Level data is saved to a variable called `game.entities`.');
console.log('âš¡ I\'d love to check out your level--');
console.log('âš¡ Email `JSON.stringify(game.entities)` to [email protected]');
console.log('âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡âš¡');
}
After some tinkering with Burp's request match and replace feature I found that if you intercept the game loading traffic and add an edit=true
URL parameter, you get a new edit menu in the game.
To make this happen on every request, I decided to replace the "useless" DNA parameter (just sets avatar look) with the edit
parameter in Burp.
Ultimately, usage of the admin controls did not contribute to the overall solution. I had originally thought I could use edit mode to just remove any obstacles...womp womp.
Also, while reviewing the traffic, I saw that there is a request for GET /game/new?rid=2668b6b6-8576-45f5-aa0c-dc010e90de77&edit=true&level= HTTP/1.1
that returns the completed levels. You can simply add the levels to the response to skip ahead in the game as doing this opens up the "secret level" A Real Pickle. To be clear though, this only operates client side and does not set your completion with the server, so more work is needed.
Digging into the code more, we see this interesting snippet in guide.js
. Notice the fix this
comment.
getSpringTarget(springCell) {
const journey = this.hero.journey;
const dy = journey[1][1] - journey[0][1];
const dx = journey[1][0] - journey[0][0];
let nextPoint = [ springCell[0], springCell[1] ];
let entityHere;
let searchLimit = 15;
let searchIndex = 0;
let validTarget;
do {
searchIndex += 1;
nextPoint = [ nextPoint[0] + dx, nextPoint[1] + dy ];
entityHere = this.entities.find(entity =>
~[
EntityTypes.PORTAL,
EntityTypes.SPRING,
].indexOf(entity[2]) &&
searchIndex &&
entity[0] === nextPoint[0] &&
entity[1] === nextPoint[1]);
if (searchIndex >= searchLimit) {
break;
}
validTarget = this.isPointInAnySegment(nextPoint) || entityHere;
} while (!validTarget);
if (this.isPointInAnySegment(nextPoint) || entityHere) {
if (entityHere) return this.segments[0][0]; // fix this
return nextPoint;
} else {
return;
}
If I am understanding this right (and Google Gemini helped), this code looks up to 15 spaces in the direction of travel to see if there is a valid next point on a path. If no valid next point is found, then it takes the final searched point and sees if that is on a segment and if so, jumps you there. If the next point is not on a segment, it jumps you to the start. In this way you can kind of use the spring as a tunnel/portal.
It also turns out you can use 2 springs, so almost 3 portals available.
Using the completed level trick above, we can go directly to the final level "A Real Pickle" and try this out. Some trial and error later, we come up with a solution that works and solves the challenge.
That's it for the Prologue, on to Act 1!