iOS解压缩_路径穿越_解析

2,932 阅读3分钟

盘古实验室公布的一个由目录穿越导致RCE的漏洞,称为ZipperDown。

由于现有的iOS App基本上采用SSZipArchive或Ziparchive来实现解压,因此漏洞是来自使用第三方Zip库解压Zip文件的过程中没有对Zip内文件名做校验导致的;例如 SSZipArchive解压时会把文件名直接拼接到目标路径后面,如果文件名中含有“../”则可以实现目录的上一级跳转,从而实现应用内任意目录的跳转,进一步可以实现文件覆盖。

(如果你想测试下这个漏洞,可去github下载这个Demo:https://github.com/muzipiao/ZipperDown)


这件事之后Ziparchive修复了该漏洞。代码中增加了一个 - (NSString *)_sanitizedPath 方法用于专门处理压缩包内的文件名,清除其中有害部分。

Ziparchive库源码地址:https://github.com/ZipArchive/ZipArchive


SSZipArchive.m 的 _sanitizedPath 方法的源代码:

- (NSString *)_sanitizedPath
{
    // Change Windows paths to Unix paths: https://en.wikipedia.org/wiki/Path_(computing)
    // Possible improvement: only do this if the archive was created on a non-Unix system
    NSString *strPath = [self stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];

    // Percent-encode file path (where path is defined by https://tools.ietf.org/html/rfc8089)
    // The key part is to allow characters "." and "/" and disallow "%".
    // CharacterSet.urlPathAllowed seems to do the job
#if (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1090 || __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 || __WATCH_OS_VERSION_MIN_REQUIRED >= 20000 || __TV_OS_VERSION_MIN_REQUIRED >= 90000)
    strPath = [strPath stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
#else
    // Testing availability of @available (https://stackoverflow.com/a/46927445/1033581)
#if __clang_major__ < 9
    // Xcode 8-
    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4) {
#else
    // Xcode 9+
    if (@available(macOS 10.9, iOS 7.0, watchOS 2.0, tvOS 9.0, *)) {
#endif
        strPath = [strPath stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
    } else {
        strPath = [strPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    }
#endif

    // `NSString.stringByAddingPercentEncodingWithAllowedCharacters:` may theorically fail: https://stackoverflow.com/questions/33558933/
    // But because we auto-detect encoding using `NSString.stringEncodingForData:encodingOptions:convertedString:usedLossyConversion:`,
    // we likely already prevent UTF-16, UTF-32 and invalid Unicode in the form of unpaired surrogate chars: https://stackoverflow.com/questions/53043876/
    // To be on the safe side, we will still perform a guard check.
    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];

    // Remove the percent-encoding
#if (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1090 || __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 || __WATCH_OS_VERSION_MIN_REQUIRED >= 20000 || __TV_OS_VERSION_MIN_REQUIRED >= 90000)
    strPath = strPath.stringByRemovingPercentEncoding;
#else
    // Testing availability of @available (https://stackoverflow.com/a/46927445/1033581)
#if __clang_major__ < 9
    // Xcode 8-
    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4) {
#else
    // Xcode 9+
    if (@available(macOS 10.9, iOS 7.0, watchOS 2.0, tvOS 9.0, *)) {
#endif
        strPath = strPath.stringByRemovingPercentEncoding;
    } else {
        strPath = [strPath stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    }
#endif

    return strPath;
}


解析 _sanitizedPath 方法

这个方法清理了路径中的有害部分: 

 (1)把windows下的路径转成Unix下的路径,把路径中的"\\"替换成"/": 

 NSString *strPath = [self stringByReplacingOccurrencesOfString:@"\\" withString:@"/"]; 

 (2)路径中允许"." 和 "/" ,但不能有"%",对百分号做了处理: 

strPath = [strPath stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet]; 

效果示例:

 处理前: http%3a%2f%2fwww.rfc-editor.org%2finfo%2frfc1738%3e 

处理后: http%253a%252f%252fwww.rfc-editor.org%252finfo%252frfc1738%253e 

可以看到 "%" 被 "%25" 替换了 

 (3)添加 scheme 的 "file:///" 前缀, 以便转成NSURL时可以处理路径中的"../"从而获取绝对路径:

 //为路径增加 "file:///" 前缀 
strPath = [@"file:///" stringByAppendingString:strPath]; 
 //路径转成NSURL,并使用 .standardizedURL.absoluteString 从而最终清理路径中的"../"获得最终的绝对路径 
strPath = [NSURL URLWithString:strPath].standardizedURL.absoluteString; 
 // 删除 "file:///" 前缀
 strPath = [strPath substringFromIndex:8];

 效果示例:

 原路径:test/../code.png 

添加前缀后:file:///test/../code.png 

NSURL处理后获得绝对路径:file:///code.png

 删除前缀:code.png 

 (4)清除百分号编码 

strPath = strPath.stringByRemovingPercentEncoding; 

效果示例: 

处理前: http%253a%252f%252fwww.rfc-editor.org%252finfo%252frfc1738%253e 

处理后: http%3a%2f%2fwww.rfc-editor.org%2finfo%2frfc1738%3e 

可以看到 "%25" 被 "%" 替换了。

 经过以上几步的处理,最终得到可用的strPath 


拓展: 

从源码的调试中,也发现一个小问题: 当文件名包含"..\"时,文件名被读取时会被默认加入一个"\"转义符,并最终造成了路径穿越。 

实践步骤如下:

 1、原文件名是"..\code.png", test 是文件夹:


2、原文件路径是"test/..\code.png",被读取时转义为"test/..\\code.png":


3、在 _sanitizedPath 方法中把 "\\" 替换成了 "/",因此 "test/..\\code.png" 变成了 "test/../code.png":


4、为"test/../code.png"增加了"file:///"前缀:


5、接着又做绝对路径的处理,"file:///test/../code.png" 变成了 "file:///code.png",test文件夹没了!


6、删除"file:///"前缀,得到"code.png",最终 _sanitizedPath 方法返回值就是"code.png":


7、执行完解压后,app的Documents路径下, "..\code.png"文件穿越到了test文件夹外: