Holiday Hack Challenge 2023 Report Cody Travis <cwtravis@gmail.com>
Top

Elf Hunt

Difficulty:

Description:

Piney Sappington needs a lesson in JSON web tokens. Hack Elf Hunt and score 75 points.

Solution

Solution:

Elf Hunt is a game reminiscent of the NES classic game Duck Hunt. Elves will spawn every half second or so and you simply click the elf to shoot it down. The elves will actively dodge your mouse cursor, so you must be quick.

Elf Hunt Game

Beating this game legit is pretty difficult, so I resorted to hacking the game instead. I found a few methods to either beat it automatically or make it significantly easier.


JWT Manipulation

JWT Manipulation

JSON Web Tokens are base 64 encoded JSON data that is used to share information between the client and the server. These are usually used to store state or session data. JWT has 3 parts: a header that contains the algorithm that was used to sign it, the data block, and then the signature. Each part is separated by a period. For more information on JWT, check out jwt.io.

JWT is used in Elf Hunt to set the speed of the elves. Because the game is rendered and played client-side, JWT's are not particularly secure or useful in this situation. One reason JWT is not ideal here is that the game has no real way of ensuring that the token hasn't been tampered with. Usually JWT's are signed with a secure hashing algorithm, and then that signature is verified on the server-side. Since the game is rendered client-side, no signature verification can take place. Even if it could be verified, the hashing algorithm is set to "none", meaning no signature is supplied at all.

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The header and data sections decode to:

  </>
JSON
{
  "alg": "HS256",
  "typ": "JWT"
}
  </>
JSON
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

The JWT from Elf Hunt is stored as a cookie, and can be viewed and edited by any browser by using the devtools. For example in Chrome, press F12 to show the devtools and click the tab Application > Storage > Cookies > https://elfhunt.org. The cookie "ElfHunt_JWT" will be visible. Double click the value to edit it.

Here is an example of the JWT from the game:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.

Which decodes to:

  </>
JSON
{
  "alg": "none",
  "typ": "JWT"
}
{
  "speed": -500
}

Since the Elf Hunt JWT is not signed, we can safely edit the "-500" to "-50" and base64 encode the sections. After setting the speed to "-50", the JWT looks like this:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwfQ.

Note that in JWT, the padding "=" are not included that are typical in base64 encoded strings. Simply replace your cookie with the above value and reload the Elf Hunt game. The elves will be slower and easier to click.

Elves at -50 Speed


Local Overrides

Local Overrides

Another way to beat Elf Hunt is to use a browser feature called "Local Overrides". This allows you to save an offline copy of a web page. The offline or "overridden" file can be edited and have those changes be persisted even when the page is reloaded.

Here is an example of how to enable Local Overrides in Chrome: Local Overrides

Once Overrides are enabled, just load the Elf Hunt game and show the devtools with F12. In the network tab, find the Elf Hunt URL, right-click it and select "Override Content". This will open the page source in the Sources tab. Any changes you make to the page source will be reflected on the next refresh of the page.

To quickly beat Elf Hunt, find the javascript function that is called that spawns new elves. It is located in the function "create()", which sets up the game when the page loads:

  </>
Javascript
function create() {
    gameScene = this, this.input.topOnly = !0, this.input.setDefaultCursor("none"), (crosshair = this.add.image(200, 50, "crosshair").setOrigin(.5, .5).setDepth(2)).setScale(.08);
    var e = this.add.image(0, 0, "overlay").setDepth(1).setOrigin(.5, 1);
    e.setScale(.25);
    let t = this.cameras.main.centerX,
        a = 2 * this.cameras.main.centerY;
    e.setPosition(t, a), landing = gameScene.add.image(400, 250, "landing").setScale(.6).setDepth(2).setOrigin(.5, .5).setInteractive(), gameScene.input.once("pointerdown", (function() {
        landing.destroy()
    }));
    var s = !1;
    button = this.add.image(50, 550, "button").setDepth(1).setScale(.6).setInteractive(), button.on("pointerdown", (function() {
        s ? (gameScene.sound.play("ding"), s = !1, hint.destroy()) : (gameScene.sound.play("ding"), hint = gameScene.add.image(400, 250, "hint").setDepth(2).setOrigin(.5, .5), hint.setScale(.6), s = !0)
    })), (scoreText = this.add.text(t, a - 20, "Score: 0", {
        fontFamily: "DigitalFont",
        fill: "#fff",
        fontSize: "40px"
    }).setOrigin(.5, 1)).setDepth(1), elves = this.physics.add.group({
        key: "elf",
        setXY: {
            x: 12,
            y: 600,
            stepX: 70
        }
    }), spawnElf = () => {
        if (elves.countActive(!0) < 10) {
            const a = Phaser.Math.Between(100, 700),
                s = elves.create(a, 2 * this.cameras.main.centerY, "elf");
            s.isHit = !1, s.setVelocityY(speed);
            var e = Math.random() < .5 ? -1 : 1;
            s.flipX = e < 0;
            var t = 80 * e;
            s.setVelocityX(t), s.setScale(.1), s.setOrigin(0, 0), s.setInteractive(), s.on("pointerdown", (function(e) {
                s.isHit || (s.isHit = !0, s.snow = gameScene.add.image(s.x, s.y, "snow").setOrigin(0, 0).setDepth(1), s.snow.scale = .1, gameScene.sound.play("splat"), s.setVelocityX(0), s.setVelocityY(0), gameScene.tweens.add({
                    targets: [s, s.snow],
                    scaleX: 0,
                    scaleY: 0,
                    alpha: 0,
                    angle: "+=1900",
                    y: 2 * gameScene.cameras.main.centerY,
                    duration: 2e3,
                    onComplete: function() {
                        s.destroy()
                    }
                }), score++, scoreText.setText("Score: " + score))
            }), s)
        }
        this.time.addEvent({
            delay: Phaser.Math.Between(200, 1e3),
            callback: spawnElf,
            callbackScope: this
        })
    }, vToken ? (__POST_RESULTS__(vToken), landing.setAlpha(0), button.setAlpha(0), scoreText.setAlpha(0), youwin = gameScene.add.image(400, 200, "youWin").setScale(1).setDepth(2), tokenbutton = gameScene.add.image(400, 475, "token").setScale(.8).setDepth(1).setInteractive(), tokenbutton.on("pointerdown", (function() {
        youwin.setAlpha(0), tokenbutton.setAlpha(0), hint = gameScene.add.image(400, 250, "journal").setScale(.8).setDepth(2).setOrigin(.5, .5)
    }))) : (spawnElf(), gameScene.time.addEvent({
        delay: 12e3,
        callback: displayRandomPoster,
        callbackScope: this
    }))
}

At the bottom of the spawnElf function, I added a "setTimeout" function that will simply cause each newly create elf to emit a click event on itself 500 ms after its spawned. The code I added looks like this:

  </>
Javascript
setTimeout(function(){
	s.emit("pointerdown");
}, 500)

Adding it to the spawnElf function results in the following code:

  </>
Javascript
spawnElf = () => {
if (elves.countActive(!0) < 10) {
	const a = Phaser.Math.Between(100, 700),
		s = elves.create(a, 2 * this.cameras.main.centerY, "elf");
	s.isHit = !1, s.setVelocityY(speed);
	var e = Math.random() < .5 ? -1 : 1;
	s.flipX = e < 0;
	var t = 80 * e;
	s.setVelocityX(t), s.setScale(.1), s.setOrigin(0, 0), s.setInteractive(), s.on("pointerdown", (function(e) {
		s.isHit || (s.isHit = !0, s.snow = gameScene.add.image(s.x, s.y, "snow").setOrigin(0, 0).setDepth(1), s.snow.scale = .1, gameScene.sound.play("splat"), s.setVelocityX(0), s.setVelocityY(0), gameScene.tweens.add({
			targets: [s, s.snow],
			scaleX: 0,
			scaleY: 0,
			alpha: 0,
			angle: "+=1900",
			y: 2 * gameScene.cameras.main.centerY,
			duration: 2e3,
			onComplete: function() {
				s.destroy()
			}
		}), score++, scoreText.setText("Score: " + score))
	}), s),
	setTimeout(function(){
		s.emit("pointerdown");
	}, 500)
}

Now each elf clicks itself directly after spawning!

Clicking Elves Automatically

There are plenty of other approaches to altering the game to be easier (e.g. setting the score programatically to 75, making the elves stationary, making the elves die when they leave the game area, etc.) One thing I wanted to mention here is that there is a value "vToken" that is set to false at the top of the script. If you set that value to true and refresh, the game thinks you have won and displays the victory screen.


Victory

Victory

When Elf Hunt is beaten, a screen is shown prompting you to click the game token.

Victory Screen

Once the game token is clicked, you get to view the Captain's Log which reveals clues about the plot of Holiday Hack Challenge 2023.

Captain's Journal

Captain's Journal