做IOS开发的同学经常用到UIWebView,大多时候是加载外部地址,但是有一些时候也会用来加载本地的html文件。
UIWebView加载外部地址的时候遵循了“同源”策略,而加载本地网页的时候却绕够了“同源”策略,导致可以访问系统任意路径。
这就是UIWebView中存在的UXSS漏洞。已知尚未修复该漏洞的App有:微盘、文件全能王、QQ阅读。
漏洞复现方式大体相似,现在微盘为例:
在PC上编辑一个网页,命名为test.html. 内容如下:
<script> alert(document.location); var aim='file:///private/etc/passwd'; var d=document; function doAttack() { var xhr1= new XMLHttpRequest(); xhr1.overrideMimeType('text/plain; charset=iso-8859-1'); xhr1.open('GET',aim); xhr1.onreadystatechange = function() { if(xhr1.readyState ==4) { var txt=xhr1.responseText; alert(txt); } }; xhr1.send(); } doAttack(); </script>
通过文件发送到微信手机端,在微信手机端点击刚才发过来的文件,选择用其他应用打开,在弹出来的应用列表里选择“微盘”,这个时候会进入微盘界面,点击上传按钮,上传完毕后,在我的微盘文件列表中点击刚才上传的文件,这个时候会弹出一个alert框显示当前文件所在路径,点击“好”,接着就会显示系统账户和密码信息(也就是passwd文件的内容)。
效果图如下:
修复方案
<1>禁用从外部打开HTML文件;(切断攻击入口)
<2>针对本地HTML文件中脚本做一些权限限制;(初步防范措施)
<3>新增一个NSURLProtocol, 专门用来处理本地网页的加载,根据同源策略来安全地加载本地文件。(彻底的解决方案)
前面两种方案相对简单一些,这里不赘述。
这里主要讲讲第三种方案,
我们知道IOS中对于各种协议(http,https, ftp, file)的处理都是通过NSURLProtocol来实现的,
每一种对应了一个NSURLProtocol,有以下几个重要的方法:
+ (BOOL)registerClass:(Class)protocolClass
注册NSURLProtocol,
+ (void)unregisterClass:(Class)protocolClass
反注册NSURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
表示是否走该NSURLProtocol的处理逻辑,返回YES,表示走,NO 表示不走,
- (void)startLoading
表示开始加载请求,由系统调用该方法,我们只需在该方法内部做网络数据请求就可以
- (void)stopLoading
表示停止加载请求,由系统调用该方法,我们只需在该方法内部做一些取消请求操作
我们新建一个类派生自NSURLProtocol, 暂且命名为SeMobSandBoxFileProtocol
在AppDelegate的application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions回调中调用
[NSURLProtocol registerClass:[SeMobSandBoxFileProtocol class]]; //注册我们的协议
SeMobSandBoxFileProtocol.h、SeMobSandBoxFileProtocol.m内容分别如下:
#import <Foundation/Foundation.h> @interface SeMobSandBoxFileProtocol : NSURLProtocol @end
#import "SeMobSandBoxFileProtocol.h" @implementation SeMobSandBoxFileProtocol + (NSArray *)supportedScheme { return [NSArray arrayWithObjects:@"file", nil]; } + (BOOL)canInitWithRequest:(NSURLRequest *)request { NSURL* url=[request URL]; NSUInteger index = [[self supportedScheme] indexOfObject:[url scheme]]; if (index!=NSNotFound) { NSURL* baseURL=[[request mainDocumentURL] URLByDeletingLastPathComponent]; NSString* baseString=[[baseURL absoluteString] lowercaseString]; //得到主资源的路径 NSRange sharpRange=[baseString rangeOfString:@"#"]; if (sharpRange.length) { baseString=[baseString substringToIndex:sharpRange.location]; //路径过滤处理,去掉#号以及#号后面的内容 } if([baseURL isFileURL]) { BOOL ok=![[[url absoluteString] lowercaseString] hasPrefix:baseString]; //判断子资源路径是否包含主资源路径前缀 return ok; } else { return baseString.length>0; } } return NO; } - (void)stopLoading { } -(void)startLoading { [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:@"CFNetwork" code:kCFURLErrorUnknown userInfo:@{@"NSErrorFailingURLKey":self.request.URL}]]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } @end
代码分析
总体思路是根据主资源与子资源的文件路径判断它们是不是父子目录关系,如果是的话,就允许访问子资源,否则就不允许,这样就阻止了子资源访问主资源对应目录以外的目录,因为判断是否为父子目录关系,是根据是否包含目录前缀来判断的,所以需要对路径进行过滤处理,把路径中#号后面的内容连同#一起过滤掉。