Getting To

Based on the comments from the elves at Rainraster Cliffs, I’ll head to Steampunk Island, where I find Brass Bouy Port on the eastern side on the tip of the nose:


Location Layout

My boat docks at the north-east corner:

image-20231221144906124Click for full size image

The Goose of Pixel Island is offers a “cluck cluck” (odd for a Goose).

Faster Lock


The Faster Lock Combination challenge is in the badge:


Bow Ninecandle has lost the combination to the padlock on the bathroom:


I’m asked for help:

Bow Ninecandle

Bow Ninecandle

Hey there! I’m Bow Ninecandle, and I’ve got a bit of a… ‘pressing’ situation.

You see, I need to get into the lavatory, but here’s the twist: it’s secured with a combination padlock.

Talk about bad timing, right? I could really use your help to figure this out before things get… well, urgent.

I’m sure there are some clever tricks and tips floating around the web that can help us crack this code without too much of a flush… I mean fuss.

Remember, we’re aiming for quick and easy solutions here - nothing too complex.

Once we’ve gathered a few possible combinations, let’s team up and try them out.

I’m crossing my legs - I mean fingers - hoping we can unlock this door soon.

After all, everyone knows that the key to holiday happiness is an accessible lavatory!

Let’s dive into this challenge and hopefully, we won’t have to ‘hold it’ for too long! Ready to help me out?

Clicking on the lock opens up interaction with the lock:



Collect numbers

I’ll follow the steps in the reference video. The numbers for the lock change on each page refresh, but I’ll note the ones I found while solving.

First, I need the “sticky number”. I’ll apply light tension and rotate around and around noting where it gets slowed down / slightly stuck. The combination changes each time the page is loaded, but for this example, I’ll identify 30.

Next I’ll find the two guess numbers. These are between 0 and 11, and with full tension, I’ll start at 0 turning looking for places where it gets stuck between two half numbers. In my care, it gets stuck between 4.5 and 5.5, so I’ll note 5. And then again it gets stuck between 9.5 and 10.5, so I’ll note 10.

The Math

The first number is the sticky number plus five, so it’s 35.

To find the third number, I’ll divide the first number by 4 to get 8 reminder 3. I’ll make a chart from my guess numbers:

5 15 25 35

10 20 30 0

I’ll divide each of these by four and check the remainder:

1 3 1 3

2 0 2 0

I only need the ones that match the remainder above, so that’s 15 and 35. I can go to each of these and put on full tension and see which one is looser. It’s 15, so that’s likely the third number.

For the second number, I’ll start two rows again, this time with the first number being the remainder above plus 2 and plus 6, and then adding 8 four times:

5 13 21 29 37

9 17 25 33 1

I can eliminate 13 and 17 as they are too close to the third number, 15.

Try Them

With eight possibilities left, I’ll try then, and when I try 35-1-15, it opens:


Hack Solution

Leak Numbers

In the JavaScript for the iFrame, this section jumps out as most interesting:

        if (moved) {
          lock_dial.angle += degree_change
          if (stage == 0) {
            if (cursors.right.isDown) {
              if (current_mark == lock_numbers.first_number) {
                first_set = true
              } else {
                first_set = false
            } else {
              if (cursors.left.isDown && first_set) {
                stage = 1
              } else {
                statusBox.clear().fillStyle(0xFF0000, 1).fillRect(530, 660, 20, 20); // Red color for locked status
                stage = -1
          if (stage == 1) {
            if (cursors.left.isDown) {
              degrees_traversed += degree_change
              if (Math.abs(degrees_traversed) >= 720) {
                statusBox.clear().fillStyle(0xFF0000, 1).fillRect(530, 660, 20, 20); // Red color for locked status
                stage = -1
              } else if (Math.abs(degrees_traversed) >= 360) {
                first_passed = true
              if (first_passed) {
                if (current_mark == lock_numbers.second_number) {
                  second_set = true
                } else {
                  second_set = false
            } else {
              if (cursors.right.isDown && second_set) {
                stage = 2
                degrees_traversed = 0
              } else {
                statusBox.clear().fillStyle(0xFF0000, 1).fillRect(530, 660, 20, 20); // Red color for locked status
                stage = -1
          } else if (stage == 2 || stage == 3) {
            degrees_traversed += degree_change
            if (Math.abs(degrees_traversed) <= 360) {
              if (cursors.right.isDown) {
                if (current_mark == lock_numbers.third_number) {
                  stage = 3
                } else {
                  stage = 2
              } else {
                statusBox.clear().fillStyle(0xFF0000, 1).fillRect(530, 660, 20, 20); // Red color for locked status
                stage = -1
            } else {
              statusBox.clear().fillStyle(0xFF0000, 1).fillRect(530, 660, 20, 20); // Red color for locked status
              stage = -1

This is what handles the turns to the three spots. It’s referencing lock_numbers throughout. I’ll enter that into the console:


One way to solve is just use first_number, second_number, and third_number to open it.

Skip First Two Numbers

But I can do better. As I move to the first number and then start moving back, it sets the stage variable from 0 to 1. If I go more than a full rotation and then to the second number, stage increments to 2. Then when I turn after the second number, it sets stage to 3.

I’ll set stage = 3 in the console, and go directly to the third number and the lock opens!

Invoke Open

That solution still involves turning the lock to the correct third number and pulling down. There’s also this function in the code:

    function moveLockIntoUnlockedPosition() {
      isTweenActive = true;
        targets: [lock_dial, lock_body],
        y: '+=75',
        duration: 500,  // can adjust the duration as needed
        ease: 'Power2',
        onComplete: function () {
            targets: lockContainer,
            y: '-=215',
            x: '+=105',
            angle: '-=90', // Rotate 90 degrees to the left
            duration: 500,
            ease: 'Power2',
            onComplete: function () {
                targets: lockContainer,
                y: '+=800',
                x: '-=200',
                duration: 1000,
                ease: 'Linear',
                onComplete: function () {
                    targets: latch_open,
                    scaleX: -1,
                    x: '+=45',
                    duration: 500,
                    ease: 'Linear',
                    yoyo: false,
                    onComplete: function () {

It’s called moveLockIntoUnlockedPosition. That seems like exactly what I want to do. I’ll just enter that into the console (with trailing () to call it), and hit enter, and the lock opens!


Bow is pleased:

Bow Ninecandle

Bow Ninecandle

Oh, thank heavens! You’re a lifesaver! With your knack for cracking codes, we’ve just turned a potential ‘loo catastrophe’ into a holiday triumph!

The Captain’s Comms


The badge gives the background for the Captain’s Comms as well as the user group to get access to:


Chimney Scissorsticks waits next to a table labeled The Captain’s Comms:

Chimney Scissorsticks

Chimney Scissorsticks

Ahoy there, I’m Chimney Scissorsticks!

You may have noticed some mischief-makers planning to stir up trouble ashore.

They’ve made many radio broadcasts which the captain has been monitoring with his new software defined radio (SDR).

The new SDR uses some fancy JWT technology to control access.

The captain has a knack for shortening words, some sorta abbreviation trick.

Not familiar with JWT values? No worries; just think of it as a clue-solving game.

I’ve seen that the Captain likes to carry his journal with him wherever he goes.

If only I could find the planned “go-date”, “go-time”, and radio frequency they plan to use.

Remember, the captain’s abbreviations are your guiding light through this mystery!

Once we find a JWT value, these villains won’t stand a chance.

The closer we are, the sooner we’ll be thwarting their pesky plans!

We need to recreate an administrative JWT value to successfully transmit a message.

Good luck, matey! I’ve no doubts about your cleverness in cracking this conundrum!”



The challenge starts with a “Background” message:


Major takeaways:

  • Different items in the communications area can be interacted with by clicking on them.
  • Some items require authorization with a specific role.
  • I need to use the Captain’s transmitter to send a misleading message with the correct date, frequency, and modified time (4 hours early).

Comms Area

At the start, there are several areas that highlight as clickable:


The clickable items bring up either reading pages or an interface to that equipment. The speaker turns on or off radio static. At this time, the other equipment requires some role. For example, the SDR:


The roles required are:

  • SDR - radioMonitor
  • Transmitter - JWT Radio Administrator

Reading Summary

The books and papers present additional information:

  • Roles are set using bearer tokens. [JWT Owners Manual Vol 1]
  • Running requires lowest role, “radioUser”. [JWT Owners Manual Vol 1]
  • To listen to transmissions, need “radioMonitor”. [JWT Owners Manual Vol 1]
  • To decode messages, need “radioDecoder”. [JWT Owners Manual Vol 1]
  • To use the TRANSMITTER, need “specific JWT system administrator ROLE”. [JWT Owners Manual Vol 1]
  • It provides references to, confirming that these tokens are JWTs. [JWT Owners Manual Vol 1]
  • The administrator role is uniquely created when the software is installed. [JWT Owners Manual Vol 2]
  • Keys are used to sign the JWTs, and they are created during install. It recommends putting them in folders with limited access. [JWT Owners Manual Vol 2]
  • There are different types of transmissions that JWT can receive. Comes with different decoders (CW = Morse Code, RadioFax = Weather Fax or WeFax). [JWT Appendix A]
  • There are also “numbers stations” that transmit coded messages. [JWT Appendix A]
  • An example of a numbers station is the Lincolnshire Poacher which used the format “Music-{5-digits}-{6 chimes}-{5-digit number groups}-{6 chimes}-Music” and gives this reference. [JWT Appendix A]
  • With the SDR window optn, clicking on a signal peak while having the “radioDecoder” role will hear and decode the signal. [JWT Appendix A]
  • The rMonitor.tok file was created in the /jwtDefault directory during installation. It contains a token for the “radioMonitor” role. [JWT Owners Card]
  • The Captain is asked ChatNPT about storing JWT keys, and it provided a list that is not yet worked through. [Captain’s To-Do List]
  • There’s a reference to the Captain’s private journal and his leaving it on Pixel Island, hinting there’s a clue about a role in there. [Captain’s To-Do List]
  • The private key for JWT is in a folder that is not named. [ChatNPT Output]
  • The public key is in a keys folder in the same directory as the “roleMonitor” token, named capsPubKey.key. [ChatNPT Output]
  • File permissions for the key may be restricted. [ChatNPT Output]

Get radioMonitor Role


Just visiting the site, my browser stores two cookies:


Both have a Java Web Token (JWT) like format, though the CaptainsCooke doesn’t seem to decode like a typical JWT. I’ll focus on the justWatchThisRole. shows it uses public key crypto (matching the reading above) and has several fields:


The role is the most important part. “radioUser” let’s met get basic access to the system, and nothing more.


The references mentioned a /jwtDefault folder with a rMonitor.tok file. If I try to go directly to the site, it returns an authorization error:


The references mentioned sending the role token as an Authorization header using the “Bearer” method, which is to put in a header of the form Authorization: Bearer {token}. It works:

oxdf@hacky$ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg"

The returned token looks very similar, but the role is now “radioMonitor”:


I’ll update the cookie in my browser. Now I can access the SDR.


The peaks are clickable, but clicking them just throws an error:


Public Key

There is also a note suggesting that the public key is in /jwtDefault/keys/capsPubKey.key. I am able to grab that as well:

oxdf@hacky$ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg"
-----END PUBLIC KEY-----

I’ll save that to a file for future use.

Get radioDecoder Role

It’s a reasonable assumption that if JWT stores the token for the “radioMonitor” role in /jwtDefault/rMonitor.tok, that it might store a token for the “radioDecoder” role in /jetDefault/rDecoder.tok. If I request that with the original token, it returns an invalid token error:

oxdf@hacky$ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg"
Invalid authorization token provided.

This is different from what I get if I request a random file name that doesn’t exist. That’s a good sign I’m on the right track.

I’ll try with the new token for “radioMonitor”. It works:

oxdf@hacky$ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg"

Get Admin Access

Decode Messages

There are X clickable peaks in the SDR. Each of them gives a different kind of decoded message.


The one marked 1 is a morse code (CW) message, coming through as a series of beeps that the JWT decoder kindly (albeit slowly) writes to the screen:


I’ll note that folder name down, THECAPSPR1V4T3F0LD3R.

The second one provides a Lincolnshire Poacher message:


The third one is a Radio Fax, which slow shows an image of the Geese Islands with a frequency in it:


I’ll note 10426 Hz.

Get Private Key

I’ll use the info from the message above to get the private key. Some guessing at folder paths finds it at /jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/capsPrivKey.key, and using the “roleDecoder” token returns the key:

oxdf@hacky$ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg"

I’ll save that to a file.

Generate JWT

I now I have all I need to sign JWTs for this application, which means I can give myself any role I want. I’ll look back at the Captain’s Journal from Rainraster Cliffs and see the Captain talking about his new role as “GeeseIslandSuperChiefCommunicationsOfficer” (it’s also mentioned in the badge objective). I’ll set role to that.

I can do this in, but it’s a bit tricky. It’s important to put the keys in first, and then edit values:


If I update my cookie to this new token, now I can load the transmitter:


Transmit Message

Decode Lincolnshire Poacher

Actually messages from Lincolnshire Poacher were not cracked, and are suspected of using a one-time pad, which would be uncrackable without the pad.

This message is simpler. It repeats the same two numbers:

12249 12249 16009 16009 12249 12249 16009 16009

They both end with 9, and removing it gives something that fits the pattern I’m looking for, a date and time - 12/24 at 1600.

Send Message

I’ll enter the frequency from the RadioFax (10426), the date from the Lincolnshire Poarcher, and the time four hours early (as instructed):


When I click “Transmit”, it solves the challenge:



Chimney is pleased:

Chimney Scissorsticks

Chimney Scissorsticks

Brilliant work! You’ve outsmarted those scoundrels with finesse!