Hackvent 2024 provided it’s typically mix of fun and challenging CTF problems delivered daily for the first 24 days of December. This year instead of challenges going from easy to leet ending with the hardesst challenge on December 24, they went easy to leet, peaking on days 11-13, and then back down to easy. I was able to complete all but one of the challenges this year. The easy challenges this year had a bunch of barcode manipulation, an interesting PowerShell webserver, some simple web exploitation, an interesting codeded language, and of course, Rick-Rolling.
HV24.01
Challenge
HV24.01 Twisted Colors
Categories:
FUN
Level:
easy
Author:
dr_nick
An elf accidentally mixed up the colors while painting the Christmas ornaments. Can you help Santa fix it?
Analyze the GIF and get the flag.
I’m given this GIF:
The QR code in this image translates to “Come and have a closer look:)”.
Solution
GIF Color Palette
The interesting this about this GIF is in the color palette. I’ll open it in Gimp, and under Image –> Mode see it’s set to Indexed. GIF images store data in a palette-based color model. The image has a table with up to 256 entries, each being containing three bytes representing a color. Then for each pixel, it can store only one value 0-255, which represents the color at that offset in the table.
Gimp will show the color palette (assuming the image is in Indexed mode) from Windows –> Dockable Dialogs –> Coloar Map.
Palette
There are 236 colors used in this image (0-235):
The interesting bit is that index 235 is all 0s (black), but so is index 0:
If I change the first one to something that will stand out, like 0000ff (blue), the QR code in the image looks like:
There are some black that aren’t changed. I’ll reset that, and set 235 to 00ff00 (green):
There’s a few odd pixels there that use the second black.
234 is pure white, and interestingly, the only pure white in the image:
Changing these to a light blue (00ffff) shows just some other pixels in the QR code:
Swap
The scenario says that “An elf accidentally mixed up the colors while painting the Christmas ornaments.” I’ll try swapping these two colors, setting 234 to 000000 and 235 to ffffff. This updates only the QR:
This can also be done at the command line using Gifsicle:
December. Nightshift at Santa’s gift factory. Gifts still to be wrapped are starting to pile up as they cannot be processed any further. One elve still on duty sent Santa some pictures asking for help as all the other elves are already celebrating at the bar. The elve has problems decrypting the pictures as it is still learning the alphabet.
Each of the images have either one or two barcodes. For example, example1.png:
Or example4.png:
EAN-8 Analysis
EAN-8 Background
EAN-8 holds 8 digits, each made up of 7 bits, four on the left and four on the right side of a middle guard. On each side, the encoding for the possible digits is different:
Digit
Left (L) Pattern
Right (R) Pattern
0
0001101
1110010
1
0011001
1100110
2
0010011
1101100
3
0111101
1000010
4
0100011
1011100
5
0110001
1001110
6
0101111
1010000
7
0111011
1000100
8
0110111
1001000
9
0001011
1110100
I got this table from ChatGPT, but I could have also pieced it together from Wikipedia articles.
Manual Decode
Each barcode is an EAN-8 barcode, but each has a gap in it of 8 0s (white) in a row, which isn’t valid:
Still, I can decode seven of the eight digits for each barcode:
Image
Digits
example1.png
?3371333 13378?26
example2.png
1337548?
example3.png
133782?6 1337233?
example4.png
1337003?
example5.png
1337?545 13370?08
example6.png
1337?234 1337777?
example7.png
1337517? 1337?327
example8.png
1337517? 1337999?
example9.png
1337?545 13374?27
Calculate Missing
The last digit in an EAN-8 barcode is a checksum, calculated by multiplying the odd index digits by 3 and the even index digits by 1, and then summing the results. The last digit should be calculated such that when it’s added the total is a multiple of ten. For example, ?3371333:
Given that x must be between 0 and 9, it’s clearly 1.
To avoid doing this manually 16 times, I’ll write a Python script to calculate the missing digit for me:
importsysdefcalculate_check_digit(ean8):odd_sum=sum(int(ean8[i])*3foriinrange(0,7,2))# Odd positions
even_sum=sum(int(ean8[i])foriinrange(1,6,2))# Even positions
total_sum=odd_sum+even_sumcheck_digit=(10-(total_sum%10))%10returncheck_digitdeffind_missing_digit(ean8):missing_index=ean8.index('?')fordigitinrange(10):ean_candidate=ean8[:missing_index]+str(digit)+ean8[missing_index+1:]ifcalculate_check_digit(ean_candidate)==int(ean_candidate[-1]):returndigitraiseValueError("No valid digit found for the EAN-8 code.")defmain():iflen(sys.argv)!=2:print("Usage: python script.py <EAN-8 code>")sys.exit(1)ean8_code=sys.argv[1]iflen(ean8_code)!=8orean8_code.count('?')!=1:print("Invalid EAN-8 code. It must have exactly 8 characters and one '?' for the missing digit.")sys.exit(1)try:missing_digit=find_missing_digit(ean8_code)print(f"The missing digit is: {missing_digit}")exceptValueErrorase:print(e)sys.exit(1)if__name__=="__main__":main()
To avoid complex math, it simply tries every possible digit and returns when it finds the checksum matches.
oxdf@hacky$python checksum.py ?3371333
The missing digit is: 1
I can run this for the rest of the images, and get the following result:
Image
Digits
example1.png
12
example2.png
5
example3.png
20
example4.png
9
example5.png
20
example6.png
19
example7.png
14
example8.png
15
example9.png
23
Where an image has two codes, I handle that as a tens and ones digit.
Find Flag
The resulting numbers above are all between 1 and 26, so they could represent characters in the English alphabet. A quick way to check is from the Python REPL, adding 0x40 to get the ASCII characters:
oxdf@hacky$pythonPython 3.12.3 (main, Nov 6 2024, 18:32:19) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>chr(0x40+12)'L'
>>>chr(0x40+5)'E'
>>>chr(0x40+20)'T'
>>>chr(0x40+9)'I'
>>>chr(0x40+20)'T'
>>>chr(0x40+19)'S'
>>>chr(0x40+14)'N'
>>>chr(0x40+15)'O'
>>>chr(0x40+23)'W'
There’s the flag:
Flag: HV24{LETITSNOW}
HV24.03
Challenge
HV24.03 PowerHell
Categories:
WEB_SECURITY FUN
Level:
easy
Author:
coderion
Oh no! The devil has found some secret information about santa! And even worse, he hides them in a webserver written in powershell! Help Santa save christmas and hack yourself into the server.
There’s a spawnable Docker instance and a download zip archive.
It’s writing a flag to flag.txt, and then running pwsh (powershell) on server.ps1.
server.ps1 is listening on port 8080 for HTTP and processing it:
$listener=New-ObjectSystem.Net.HttpListener$listener.Prefixes.Add("http://*:8080/")$listener.Start()Write-Output"Server started on http://*:8080/"while($true){$context=$listener.GetContext()$request=$context.Request$response=$context.Responseif($request.Url.AbsolutePath-ceq"/"){# Serve login page$htmlContent=Get-Content-Raw-Path"./templates/index.html"../helpers.ps1$response$htmlContent"text/html"}elseif($request.Url.AbsolutePath-ceq"/style.css"){# Serve cool css$htmlContent=Get-Content-Raw-Path"./style.css"../helpers.ps1$response$htmlContent"text/css"}elseif($request.Url.AbsolutePath-ceq"/login"){# Authentication../authenticate.ps1$request$response}elseif($request.Url.AbsolutePath-ceq"/admin"){# Admin dashboard../admin.ps1$request$response}elseif($request.Url.AbsolutePath-ceq"/dashboard"){# User dashboard$htmlContent=Get-Content-Raw-Path"./templates/user.html"../helpers.ps1$response$htmlContent"text/html"}$response.Close()}
There’s an index page, style.css, and three other routes.
/login calls ``:
param($request,$response)$username=$request.QueryString["username"]$password=$request.QueryString["password"]if(Test-Path"passwords/$username.txt"){$storedPassword=Get-Content-Raw-Path"passwords/$username.txt"$isAuthenticated=$truefor($i=0;$i-lt$password.Length;$i++){if($password[$i]-cne$storedPassword[$i]){$isAuthenticated=$falseStart-Sleep-Milliseconds500# brute-force preventionbreak}}if($isAuthenticated){if($username-ceq"admin"){$response.Redirect("/admin?username=$username&password=$password")}else{$response.Redirect("/dashboard?username=$username&password=$password")}}else{../helpers.ps1$response"<h1>Invalid password :c</h1>""text/html"}}else{../helpers.ps1$response"<h1>User not found :c</h1>""text/html"}
The interesting part is how it gets the password from the file based on username, and then checks the input password to see if it matches the contents of the file. If that works, it redirects to either /admin or /dashboard. If it fails, it sends back error messages.
The route for /dashboard just returns a static file. /admin checks again for the admin password (more securely this time), and then prints a message and the contents of secret.txt:
param($request,$response)$adminPass=Get-Content-Path"passwords/admin.txt"if($request.QueryString["username"]-cne"admin"){$response.StatusCode=403../helpers.ps1$response"Santa, go away!""text/html"return}if($request.QueryString["password"]-cne$adminPass){$response.StatusCode=403../helpers.ps1$response"Nope :3""text/html"return}$file=Get-Content-Path"secret.txt"$template="<h1>Hello, Devil</h1><hr><br>Here is your secret intel on Santa: <br><pre>$file</pre>"../helpers.ps1$response$template"text/html"
Website
The index page presents a login form:
If I enter 0xdf as the user and some password, it responds:
This matches what I saw above with authenticate.ps1. If I put the user to admin or user, it shows:
The user password, “cat”, works, redirecting to /dashboard?username=user&password=cat:
Exploit
Auth Bypass
I can bypass the initial auth simply be giving it an empty password. The check in authenticate.ps1 loops over the input comparing it to the password from the file until the end of the input:
If the input is empty, it’ll just leave $isAuthenticated as true.
This doesn’t buy me much, as I already know the user’s password (“cat”), and entering admin redirects to admin.ps1 which does another check of the password and returns a failure message.
Directory Traversal
$storedPassword in authenticate.ps1 is read from passwords/$username.txt:
This code is vulnerable to a directory traversal. If I enter a username of ../secret, then it will read passwords/../secret.txt. With an empty password, this goes to the dashboard:
With a junk password, it returns “Invalid password :c”, which implies it was able to read the file. This allows me to check for the existence of a file. If I do a file that doesn’t exist, it says the user doesn’t exist.
File Read
That same bit of code allows for brute forcing file read:
$storedPassword=Get-Content-Raw-Path"passwords/$username.txt"$isAuthenticated=$truefor($i=0;$i-lt$password.Length;$i++){if($password[$i]-cne$storedPassword[$i]){$isAuthenticated=$falseStart-Sleep-Milliseconds500# brute-force preventionbreak}}if($isAuthenticated){if($username-ceq"admin"){$response.Redirect("/admin?username=$username&password=$password")}else{$response.Redirect("/dashboard?username=$username&password=$password")}}else{../helpers.ps1$response"<h1>Invalid password :c</h1>""text/html"}}else{../helpers.ps1$response"<h1>User not found :c</h1>""text/html"
I can try all possible one character passwords, and the returns a 302 redirect to the dashboard is the correct first letter in that file. Then I can try two character passwords starting with the known first letter, and so one.
The first thing I originally tried was to read secret.txt, but it is very slow, and after a few minutes I realized I could read the admin password and then just viewsecret.txt.
That flag happens to be the YouTube ID for RickRoll.
HV24.21
Challenge
HV24.21 Silent Post
Categories:
WEB_SECURITY CRYPTO
Level:
easy
Author:
rtfmkiesel
The elves have launched a new service to transmit the most valuable secrets, like the naughty lists, over a secure line. Using Silent Post, secrets get encrypted, and the decryption key is right in the link. How clever! Sadly, one elves has lost the link to one of the lists. Can you help him recover the list?
Start the website and get the flag.
There’s a spawnable Docker instance.
Website Enumeration
Functionality
The website offers a capability to store a secret:
It says clearly that each link can only be viewed one time.
If I enter some text and click “Generate link”, it replaces the text box with a link:
The URL seems to have an ID (133) and then an anchor. The anchor part of the URL is base64-encoded:
On visiting the link to get back the message, I’ll note two requests to a URL ending in /134:
The first is the page itself. Then it loads four JavaScript pages, and then there’s a call to /api/fetch/134 from app-view.js line 12.
Source
The source for the view page is pretty basic, with several JavaScript references:
app-view.js waits for the page to load, and then processes the id and key variables from the URL. Then it calls /api/fetch/<id> and decrypts the response using a function decrypt that must be defined elsewhere.
This is an obfuscation technique known as JSFuck, modeled off of the Brainfuck esoteric language. For some reason, it actually doesn’t decode as JSFuck on de4js, but does decode very nicely as “Eval”
The resulting code has three functions. generateKey takes a SHA1 hash of the current timestamp and returns it as hex:
I’ll write a Python script that will brute force across the API to find data by id (this is an IDOR vulnerability). Then when it gets that data it breaks and tries to brute force the timestamp on the encryption starting at right now and going backwards:
If I run once and find the missing message, but don’t capture it, I will have to reset the Docker to a fresh image to be able to recover it.
Flag: HV24{s0metim3s_t1me_is_k3y}
HV24.22
Challenge
HV24.22 Santa's Secret Git Feature
Categories:
FUN
Level:
easy
Author:
explo1t
Santa found a new awesome git feature to hide presents. However, he thinks it does not fit the Christmas theme, but maybe his good friend, the easter bunny, can use it… Can you find his hidden present?
oxdf@hacky$git clone https://github.com/santawoods/christmas-secret-feature
Cloning into 'christmas-secret-feature'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (3/3), done.
oxdf@hacky$cd christmas-secret-feature/
oxdf@hacky$lshello.txt
oxdf@hacky$cat hello.txt
Hello everybody
oxdf@hacky$git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
git show will show the differences for the middle commit:
oxdf@hacky$git show 06af9d2
commit 06af9d20cc50d124bd35cf322180d380325a8030
Author: Santa <santa@christmas.town>
Date: Sat Nov 16 20:07:41 2024 +0100
Notes added by 'git notes add'
diff --git a/5c1dff6bd6b05a44e41d786a99fa1f95219e2d62 b/5c1dff6bd6b05a44e41d786a99fa1f95219e2d62
new file mode 100644
index 0000000..3617dbd
--- /dev/null
+++ b/5c1dff6bd6b05a44e41d786a99fa1f95219e2d62
@@ -0,0 +1 @@
+SGVyZSBpcyB0aGUgZmxhZzogSFYyNHtzM2NyM3RfbjB0M19mbDRnX2Z1bn0=
The base64 data decodes to the flag:
oxdf@hacky$echo"SGVyZSBpcyB0aGUgZmxhZzogSFYyNHtzM2NyM3RfbjB0M19mbDRnX2Z1bn0=" | base64-dHere is the flag: HV24{s3cr3t_n0t3_fl4g_fun}
Flag: HV24{s3cr3t_n0t3_fl4g_fun}
HV24.23
Challenge
HV24.23 Santa's Packet Analyser
Categories:
FUN OPEN_SOURCE_INTELLIGENCE
Level:
easy
Author:
brp64
Santa was invited to an Open Source conference in fall to talk about his newest development on “elfilter – a packet inspector to balance elf load”. Due to the sensitive nature of the talk, it was only open to a very select audience. Nonetheless, he came back with a nice scarf. The elfs suspect there might be a message behind the pattern.
</picture>
Wrap the text of the flag into HV24{} before submitting it. Make sure that the rest of the text is all in lowercase.
Analyze the image and get the flag.
Solve
The image is Ogham, a historic alphabet explained here. The language didn’t directly translate to the alphabeta used by modern English, so there are many difference translators each with slight differences. I used this one:
Flag: HV24{santafilterspacket}
HV24.24
Challenge
HV24.24 Stranger Sounds
Categories:
FUN
Level:
easy
Author:
coderion
Santa received a file with some very strange audio. He’s kind of scared, it sounds like some monster who’s about to hunt him down. Help him get to the bottom of it.
Analyze the audio and get the flag.
Solve
The audio sounds like garbled eerie noise that goes on for several minutes. Viewed in Audacity, it looks like:
It’s over 35 minutes long!
I’ll make two changes. The first is Effect –> Pitch and tempo –> Change speed and pitch:
I’ll set it to a speed multiplier of 10:
The length drops from 35:20.88 to 3:32.088.
The next change is Effect –> Special –> Reverse. The result is “Never Gonna Give You Up” by Rick Astley.