Zip Slip in Sigil (CVE-2019-14452)

This is a follow-up article to my series on how using a fuzzer helped me discover three vulnerabilities. If you haven’t already, you can read that series here.

Introduction

I had been working at Canonical on the Ubuntu Security Team for about a year when I decided to try my hand at vulnerability research. With the help of American fuzzy lop, I discovered CVE-2019-13032 in FlightCrew and CVE-2019-13453 in Zipios. The discovery of these CVEs led me to analyze some of the code in FlightCrew, where I discovered CVE-2019-13241, a Zip Slip vulnerability.

At the request of FlightCrew’s maintainers, I created issues on GitHub for each of the vulnerabilities. My conversation with FlightCrew’s developers led me to discover another Zip Slip vulnerability — this time in Sigil Ebook.

Discovery of a Zip Slip in Sigil (issue #52)

Before I could complete a more thorough analysis, kevinhendricks quickly patched the issue:

Sigil maintainers patch the vulnerability (issue #52)

Through an analysis of the code and a Zip Slip sample archive from Snyk, I was able to confirm that Sigil v0.9.15 is vulnerable to Zip Slip.

Vulnerability Analysis

If you’ve read my previous posts on fuzzing FlightCrew, you’ll know that EPUBs — which are a common format for e-books — are just ZIP archives that contain a specific set of files. In order to read the contents of an EPUB, Sigil extracts the archive and writes the contents to a temporary directory.

src/Importers/ImportEPUB.cpp

        do {
            // Get the name of the file in the archive.
            char file_name[MAX_PATH] = {0};
            unz_file_info64 file_info;
            unzGetCurrentFileInfo64(zfile, &file_info, file_name, MAX_PATH, NULL, 0, NULL, 0);
            QString qfile_name;
            QString cp437_file_name;
            qfile_name = QString::fromUtf8(file_name);
            if (!(file_info.flag & (1<<11))) {
                // General purpose bit 11 says the filename is utf-8 encoded. If not set then
                // IBM 437 encoding might be used.
                cp437_file_name = cp437->toUnicode(file_name);
            }

            // If there is no file name then we can't do anything with it.
            if (!qfile_name.isEmpty()) {
                // We use the dir object to create the path in the temporary directory.
                // Unfortunately, we need a dir ojbect to do this as it's not a static function.
                QDir dir(m_ExtractedFolderPath);
                // Full file path in the temporary directory.
                QString file_path = m_ExtractedFolderPath + "/" + qfile_name;
                QFileInfo qfile_info(file_path);

                // Is this entry a directory?
                if (file_info.uncompressed_size == 0 && qfile_name.endsWith('/')) {
                    dir.mkpath(qfile_name);
                    continue;
                } else {
                    dir.mkpath(qfile_info.path());
                    // add it to the list of files found inside the zip
                    if (cp437_file_name.isEmpty()) {
                        m_ZipFilePaths << qfile_name;
                    } else {
                        m_ZipFilePaths << cp437_file_name;
                    }
                }

                // Open the file entry in the archive for reading.
                if (unzOpenCurrentFile(zfile) != UNZ_OK) {
                    unzClose(zfile);
                    throw (EPUBLoadParseError(QString(QObject::tr("Cannot extract file: %1")).arg(qfile_name).toStdString()));
                }

                // Open the file on disk to write the entry in the archive to.
                QFile entry(file_path);

                if (!entry.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
                    unzCloseCurrentFile(zfile);
                    unzClose(zfile);
                    throw (EPUBLoadParseError(QString(QObject::tr("Cannot extract file: %1")).arg(qfile_name).toStdString()));
                }

                // Buffered reading and writing.
                char buff[BUFF_SIZE] = {0};
                int read = 0;

                while ((read = unzReadCurrentFile(zfile, buff, BUFF_SIZE)) > 0) {
                    entry.write(buff, read);
                }

As can be seen in the above code snippet, the value in file_path is created by concatenating m_ExtractedFolderPath and qfile_name, where qfile_name is ultimately taken directly from the ZIP archive (line 415). On lines 454–457, a write-only file handle for file_path is opened. The file is then written on lines 467–469.

The trouble arises because no validation is performed on the extraction paths. This allows an attacker to use a crafted ZIP archive with relative paths to write arbitrary files to the filesystem.

Security Impact

In order to exploit this vulnerability, a local user must open a maliciously crafted EPUB file. That EPUB file can overwrite the user’s configuration files or other files, which might grant the attacker remote access or cause malicious code to be executed. This attack does not allow for privilege escalation and is, therefore, limited by the permissions and capabilities of the user.

Remediation

This vulnerability has been fixed in Sigil v0.9.16 and later. Users are encouraged to upgrade to the latest version. Ubuntu users have access to a patched version of Sigil. Other Sigil users can apply the following patches and rebuild:

Disclosure Timeline

While analyzing CVE-2019-13453, a Zip Slip vulnerability in FlightCrew is discovered.

Upstream developers are notified via a secure channel. A CRD of July 22, 2019 is proposed.

Upstream developers respond, requesting that the issue be made public on GitHub.

Through the discussion on GitHub, it is discovered that Sigil is also vulnerable to Zip Slip.

Sigil is patched by the upstream developers.

Sigil v0.9.16 is released.

MITRE assigns CVE-2019-14452 to this vulnerability.

Conclusion

For those of you keeping track, the discovery of this vulnerability raises the count from three to four. That is, fuzzing one application led me to discover four vulnerabilities in three different software packages. It would appear that the old adage, “Where there’s one, there’s more,” applies as much to security vulnerabilities as it does to everything else.

Further Reading

CVE: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14452

Three-part series on fuzzing FlightCrew: https://salvatoresecurity.com/fun-with-fuzzers-or-how-i-discovered-three-vulnerabilities-part-1-of-3

GitHub issue: https://github.com/Sigil-Ebook/flightcrew/issues/52

Snyk Zip Slip whitepaper: https://res.cloudinary.com/snyk/image/upload/v1528192501/zip-slip-vulnerability/technical-whitepaper.pdf