Recently I went hunting for Zip traversal vulnerabilities within the PHP ecosystem. While looking at well known PHP ZIP dependencies, I noticed that, both pclzip and zipper were vulnerable to traversal attacks.
For Zipper, I was able to verify, write a patch and contact maintainers of zipper and modern zipper forks to push a new release. Details regarding that can be found below.
- https://snyk.io/vuln/SNYK-PHP-CHUMPERZIPPER-552162
- https://snyk.io/vuln/SNYK-PHP-DARIUSIIIZIPPER-552163
- https://snyk.io/vuln/SNYK-PHP-MADNESTMADZIPPER-552164
This short blog post will try to explain how to patch this issue manually within pclzip.
ZIP Path Traversal
Zip Path Traversal is a vulnerability where it is possible to traversal directories of a program during archive/zip extraction due to insufficient validation of user provided filenames. By leveraging this vulnerability, it is often possible to conduct arbitrary file writes within a vulnerable system, which can lead to arbitrary code execution.
This vulnerability has existed over a decade and some good research around this can be found below:
- http://phrack.org/issues/34/5.html#article
- https://snyk.io/research/zip-slip-vulnerability
- https://www.youtube.com/watch?v=Ry_yb5Oipq0
pclzip Vulnerability
This library has existed since 2009 and is extremely popular within the PHP ecosystem. Content Management systems such as WordPress and Joola currently use a forked version of pclzip within their software.
pclzip within its default state is vulnerable to ZIP traversal attacks, and due to its poor outdated documentation and badly written classes, it is extremely hard to understand how to mitigate against this issue.
Eventhough this package now exists within packagist, its original maintainer is now where to be found and multiple forks of this module exists within GitHub: https://github.com/search?q=pclzip
Verifying ZIP Traversal
To test if the vulnerability, start by creating two different files and creating a ZIP archive with them as follows.
1
2
3
4
5
6
7
8
snoopy@snoopy-XPS-15-9570:~/ziptest/poc$ echo foo > test1.txt
snoopy@snoopy-XPS-15-9570:~/ziptest/poc$ echo bar > xxxxxxxxxxxxxxxxxxxxxxxxxxxxtest2.txt
snoopy@snoopy-XPS-15-9570:~/ziptest/poc$ ls
test1.txt xxxxxxxxxxxxxxxxxxxxxxxxxxxxtest2.txt
snoopy@snoopy-XPS-15-9570:~/ziptest/poc$ zip -r ziptest.zip test1.txt xxxxxxxxxxxxxxxxxxxxxxxxxxxxtest2.txt
adding: test1.txt (stored 0%)
adding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxtest2.txt (stored 0%)
snoopy@snoopy-XPS-15-9570:~/ziptest/poc$
This ZIP can then be opened using a HEX editor and the test2 filename can be edited to be something such as ../../../../../../../../tmp/test2.txt.
his can then be provided to the below example usage code.
The following code demonstrates usage of PclZIp, The below code will open a ZIP file and extract its contents to a given location.
1
2
3
4
5
<?php
require __DIR__ . '/vendor/autoload.php';
$zip = new PclZip('/home/snoopy/ziptest/poc/ziptest.zip');
$zip->extract(PCLZIP_OPT_PATH, '/home/snoopy/ziptest/uploads');
Once executed, this will then extract both files. the test2 file has successfully extracted to the tmp directory.
1
2
3
snoopy@snoopy-XPS-15-9570:/tmp$ cat test2.txt
bar
snoopy@snoopy-XPS-15-9570:/tmp$
When looking for these vulnerabilities in the wild, the following can also be useful.
- https://github.com/ptoomey3/evilarc
- https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/fileformat/zip_slip.rb
- https://github.com/snyk/zip-slip-vulnerability/tree/master/archives
PCLZIP_OPT_EXTRACT_DIR_RESTRICTION Feature
According to version 2.5 notes (https://github.com/ivanlanin/pclzip), this version introduced the PCLZIP_OPT_EXTRACT_DIR_RESTRICTION feature.
PclZip can extract any file in any folder of a system. People may use this to upload a zip file and try to override a system file. The PCLZIP_OPT_EXTRACT_DIR_RESTRICTION will give the ability to forgive any directory transversal behavior.
However, this option seems to be lacking documentation. Furthermore, this functionality seems to not work if an absolute path is specified via PCLZIP_OPT_PATH. This seems to be a problem for multiple maintainers using this package https://github.com/matomo-org/matomo/issues/1812.
Patching Manually
An easy way to fix this feature would be to patch this yourself within the library. Within the pclzip.lib.php itself, When the extract() function is initially called within the library, this will parse the given arguments and set the necessary options. privExtractByRule() function is then executed. This function will extract a file or directory depending on arguments initially provided e.g. by index, by name etc. Within this loaded function, the $p_file_list argument is the array which contains all the properties of the zip file, including the filenames.
Within this function, the privExtractFile function is then called (https://github.com/ivanlanin/pclzip/blob/master/pclzip.lib.php#L3424). This function actually does the extraction.
Inside this function, it is possible to validate the filename arguments, and based on the rewrite them to remove malicious characters.
The following code can be inserted here: https://github.com/ivanlanin/pclzip/blob/master/pclzip.lib.php#L3514
1
2
3
4
if (strpos($p_entry['stored_filename'], '../') !== false || strpos($p_entry['stored_filename'], '..\\') !== false) {
$p_entry['stored_filename'] = basename($p_entry['stored_filename']);
$p_entry['filename'] = basename($p_entry['stored_filename']);
echo $p_entry['stored_filename'];
During extraction, the above code will check if the [‘stored_filename’] array value contains .. characters, and if so, will extract the basename of the parameter removing the special characters. E.g. ../../../tmp/foo.txt becomes foo.txt. Then, extraction will continue as normal.