SANS 2024 Holiday Hack Challenge - Prologue

SANS 2024 Holiday Hack Challenge - Prologue
Welcome back to the Geese Islands!

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.

💡
Spoilers Ahead

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.

Inspecting a suspicious cookie value

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.

The useful Session Storage variables

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.

Snippet of game code showing how 'score' is initialized

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.

Check out my 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.

Well game1.js doesn't exist 😑

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.

Shiny new edit menu!

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.

My match and replace rule to make all requests have the edit menu parameter

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.

Intercepting the new game response and setting all levels completed

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!