diff --git a/SUMMARY.md b/SUMMARY.md index e45b79ee5..6e8a4361d 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -507,6 +507,10 @@ * [Online Platforms with API](online-platforms-with-api.md) * [Stealing Sensitive Information Disclosure from a Web](stealing-sensitive-information-disclosure-from-a-web.md) * [iOS Pentesting](ios-pentesting/README.md) + * [iOS Custom URI Handlers / Deeplinks / Custom Schemes](ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md) + * [iOS Universal Links](ios-pentesting/ios-universal-links.md) + * [iOS UIPasteboard](ios-pentesting/ios-uipasteboard.md) + * [iOS Serialisation and Encoding](ios-pentesting/ios-serialisation-and-encoding.md) * [iOS Protocol Handlers](ios-pentesting/ios-protocol-handlers.md) * [iOS WebViews](ios-pentesting/ios-webviews.md) * [Basic iOS Testing Operations](ios-pentesting/basic-ios-testing-operations.md) diff --git a/ios-pentesting/README.md b/ios-pentesting/README.md index 7ace6520f..5641f9fa3 100644 --- a/ios-pentesting/README.md +++ b/ios-pentesting/README.md @@ -914,266 +914,11 @@ If vulnerable, the module will automatically bypass the login form. ### Custom URI Handlers / Deeplinks / Custom Schemes -Custom URL schemes [allow apps to communicate via a custom protocol](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html#//apple_ref/doc/uid/TP40007072-CH6-SW1). An app must declare support for the schemes and handle incoming URLs that use those schemes. - -> URL schemes offer a potential attack vector into your app, so make sure to **validate all URL parameters** and **discard any malformed URLs**. In addition, limit the available **actions** to those that d**o not risk the user’s data**. - -For example, the URI: `myapp://hostname?data=123876123` will **invoke** the **application** mydata \(the one that has **register** the scheme `mydata`\) to the **action** related to the **hostname** `hostname` sending the **parameter** `data` with value `123876123` - -One vulnerable example is the following [bug in the Skype Mobile app](http://www.dhanjani.com/blog/2010/11/insecure-handling-of-url-schemes-in-apples-ios.html), discovered in 2010: The Skype app registered the `skype://` protocol handler, which **allowed other apps to trigger calls to other Skype users and phone numbers**. Unfortunately, Skype didn't ask users for permission before placing the calls, so any app could call arbitrary numbers without the user's knowledge. Attackers exploited this vulnerability by putting an invisible `` \(where `xxx` was replaced by a premium number\), so any Skype user who inadvertently visited a malicious website called the premium number. - -You can find the **schemes registered by an application** in the app's **`Info.plist`** file searching for **`CFBundleURLTypes`** \(example from [iGoat-Swift](https://github.com/OWASP/iGoat-Swift)\): - -```markup -CFBundleURLTypes - - - CFBundleURLName - com.iGoat.myCompany - CFBundleURLSchemes - - iGoat - - - -``` - -However, note that **malicious applications can re-register URIs** already registered by applications. So, if you are sending **sensitive information via URIs** \(myapp://hostname?password=123456\) a **malicious** application can **intercept** the URI with the **sensitive** **information**. - -Also, the input of these URIs **should be checked and sanitised,** as it can be coming from **malicious** **origins** trying to exploit SQLInjections, XSS, CSRF, Path Traversals, or other possible vulnerabilities. - -#### Application Query Schemes Registration - -Apps can call [`canOpenURL:`](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl?language=objc) to verify that the **target app is available**. However, as this method was being used by malicious app as a way to **enumerate installed apps**, [from iOS 9.0 the URL schemes passed to it must be also declared](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl?language=objc#discussion) by adding the `LSApplicationQueriesSchemes` key to the app's `Info.plist` file and an array of **up to 50 URL schemes**. - -```markup -LSApplicationQueriesSchemes - - url_scheme1 - url_scheme2 - -``` - -`canOpenURL` will always return `NO` for undeclared schemes, whether or not an appropriate app is installed. However, this restriction only applies to `canOpenURL`. - -#### Testing URL Handling and Validation - -In order to determine how a URL path is built and validated, if you have the original source code, you can **search for the following methods**: - -* `application:didFinishLaunchingWithOptions:` method or `application:will-FinishLaunchingWithOptions:`: verify how the decision is made and how the information about the URL is retrieved. -* [`application:openURL:options:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application?language=objc): verify how the resource is being opened, i.e. how the data is being parsed, verify the [options](https://developer.apple.com/documentation/uikit/uiapplication/openurloptionskey), especially if access by the calling app \([`sourceApplication`](https://developer.apple.com/documentation/uikit/uiapplication/openurloptionskey/1623128-sourceapplication)\) should be allowed or denied. The app might also need user permission when using the custom URL scheme. - -In Telegram you will [find four different methods being used](https://github.com/peter-iakovlev/Telegram-iOS/blob/87e0a33ac438c1d702f2a0b75bf21f26866e346f/Telegram-iOS/AppDelegate.swift#L1250): - -```swift -func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool { - self.openUrl(url: url) - return true -} - -func application(_ application: UIApplication, open url: URL, sourceApplication: String?, -annotation: Any) -> Bool { - self.openUrl(url: url) - return true -} - -func application(_ app: UIApplication, open url: URL, -options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { - self.openUrl(url: url) - return true -} - -func application(_ application: UIApplication, handleOpen url: URL) -> Bool { - self.openUrl(url: url) - return true -} -``` - -#### Testing URL Requests to Other Apps - -The method [`openURL:options:completionHandler:`](https://developer.apple.com/documentation/uikit/uiapplication/1648685-openurl?language=objc) and the [deprecated `openURL:` method of `UIApplication`](https://developer.apple.com/documentation/uikit/uiapplication/1622961-openurl?language=objc) are responsible for **opening URLs** \(i.e. to send requests / make queries to other apps\) that may be local to the current app or it may be one that must be provided by a different app. If you have the original source code you can search directly for usages of those methods. - -Additionally, if you are interested into knowing if the app is querying specific services or apps, and if the app is well-known, you can also search for common URL schemes online and include them in your **greps \(l**[**ist of iOS app schemes**](https://ios.gadgethacks.com/how-to/always-updated-list-ios-app-url-scheme-names-paths-for-shortcuts-0184033/)**\)**. - -```bash -egrep -nr "open.*options.*completionHandler" ./Telegram-iOS/ -egrep -nr "openURL\(" ./Telegram-iOS/ -egrep -nr "mt-encrypted-file://" ./Telegram-iOS/ -egrep -nr "://" ./Telegram-iOS/ -``` - -#### Testing for Deprecated Methods - -Search for deprecated methods like: - -* [`application:handleOpenURL:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622964-application?language=objc) -* [`openURL:`](https://developer.apple.com/documentation/uikit/uiapplication/1622961-openurl?language=objc) -* [`application:openURL:sourceApplication:annotation:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623073-application) - -For example, here we find those three: - -```bash -$ rabin2 -zzq Telegram\ X.app/Telegram\ X | grep -i "openurl" - -0x1000d9e90 31 30 UIApplicationOpenURLOptionsKey -0x1000dee3f 50 49 application:openURL:sourceApplication:annotation: -0x1000dee71 29 28 application:openURL:options: -0x1000dee8e 27 26 application:handleOpenURL: -0x1000df2c9 9 8 openURL: -0x1000df766 12 11 canOpenURL: -0x1000df772 35 34 openURL:options:completionHandler: -... -``` - -#### Calling arbitrary URLs - -* **Safari**: To quickly test one URL scheme you can open the URLs on Safari and observe how the app behaves. For example, if you write `tel://123456789` safari will try to start calling the number. -* **Notes App**: Long press the links you've written in order to test custom URL schemes. Remember to exit the editing mode in order to be able to open them. Note that you can click or long press links including custom URL schemes only if the app is installed, if not they won't be highlighted as _clickable links_. -* [**IDB**](https://github.com/facebook/idb): - * Start IDB, connect to your device and select the target app. You can find details in the [IDB documentation](https://www.idbtool.com/documentation/setup.html). - * Go to the **URL Handlers** section. In **URL schemes**, click **Refresh**, and on the left you'll find a list of all custom schemes defined in the app being tested. You can load these schemes by clicking **Open**, on the right side. By simply opening a blank URI scheme \(e.g., opening `myURLscheme://`\), you can discover hidden functionality \(e.g., a debug window\) and bypass local authentication. -* **Frida**: - - If you simply want to open the URL scheme you can do it using Frida: - - ```javascript - $ frida -U iGoat-Swift - - [iPhone::iGoat-Swift]-> function openURL(url) { - var UIApplication = ObjC.classes.UIApplication.sharedApplication(); - var toOpen = ObjC.classes.NSURL.URLWithString_(url); - return UIApplication.openURL_(toOpen); - } - [iPhone::iGoat-Swift]-> openURL("tel://234234234") - true - ``` - - In this example from [Frida CodeShare](https://codeshare.frida.re/@dki/ios-url-scheme-fuzzing/) the author uses the non-public API `LSApplicationWorkspace.openSensitiveURL:withOptions:` to open the URLs \(from the SpringBoard app\): - - ```javascript - function openURL(url) { - var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace(); - var toOpen = ObjC.classes.NSURL.URLWithString_(url); - return w.openSensitiveURL_withOptions_(toOpen, null); - } - ``` - - > Note that the use of non-public APIs is not permitted on the App Store, that's why we don't even test these but we are allowed to use them for our dynamic analysis. - -#### Fuzzing URL Schemes - -If the app parses parts of the URL, you can also perform input fuzzing to detect memory corruption bugs. - -What we have learned above can be now used to build your own fuzzer on the language of your choice, e.g. in Python and call the `openURL` using [Frida's RPC](https://www.frida.re/docs/javascript-api/#rpc). That fuzzer should do the following: - -* Generate payloads. -* For each of them call `openURL`. -* Check if the app generates a crash report \(`.ips`\) in `/private/var/mobile/Library/Logs/CrashReporter`. - -The [FuzzDB](https://github.com/fuzzdb-project/fuzzdb) project offers fuzzing dictionaries that you can use as payloads. - -**Fuzzing Using Frida** - -Doing this with Frida is pretty easy, you can refer to this [blog post](https://grepharder.github.io/blog/0x03_learning_about_universal_links_and_fuzzing_url_schemes_on_ios_with_frida.html) to see an example that fuzzes the iGoat-Swift app \(working on iOS 11.1.2\). - -Before running the fuzzer we need the URL schemes as inputs. From the static analysis we know that the iGoat-Swift app supports the following URL scheme and parameters: `iGoat://?contactNumber={0}&message={0}`. - -```bash -$ frida -U SpringBoard -l ios-url-scheme-fuzzing.js -[iPhone::SpringBoard]-> fuzz("iGoat", "iGoat://?contactNumber={0}&message={0}") -Watching for crashes from iGoat... -No logs were moved. -Opened URL: iGoat://?contactNumber=0&message=0 -``` +{% page-ref page="ios-custom-uri-handlers-deeplinks-custom-schemes.md" %} ### Universal Links -Universal links allows to **redirect users directly** to the app without passing through safari for redirection. -Universal links are **unique**, so they **can't be claimed by other app**s because they use standard HTTP\(S\) links to the **website where the owner has uploaded a file to make sure that the website and the app are related**. -As these links uses HTTP\(S\) schemes, when the **app isn't installed, safari will open the link** redirecting the users to the page. These allows **apps to communicate with the app even if it isn't installed**. - -To create universal links it's needed to **create a JSON file called `apple-app-site-association`** with the details. Then this file needs to be **hosted in the root directory of your webserver** \(e.g. [https://google.com/apple-app-site-association](https://google.com/apple-app-site-association)\). -For the pentester this file is very interesting as it **discloses paths**. It can even be disclosing paths of releases that haven't been published yet. - -​**Checking the Associated Domains Entitlement** - -n Xcode, go to the **Capabilities** tab and search for **Associated Domains**. You can also inspect the `.entitlements` file looking for `com.apple.developer.associated-domains`. Each of the domains must be prefixed with `applinks:`, such as `applinks:www.mywebsite.com`. - -Here's an example from Telegram's `.entitlements` file: - -```markup - com.apple.developer.associated-domains - - applinks:telegram.me - applinks:t.me - -``` - -More detailed information can be found in the [archived Apple Developer Documentation](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html#//apple_ref/doc/uid/TP40016308-CH12-SW2). - -If you only has the compiled application you can extract the entitlements following this guide: - -{% page-ref page="extracting-entitlements-from-compiled-application.md" %} - -**Retrieving the Apple App Site Association File** - -Try to retrieve the `apple-app-site-association` file from the server using the associated domains you got from the previous step. This file needs to be accessible via HTTPS, without any redirects, at `https:///apple-app-site-association` or `https:///.well-known/apple-app-site-association`. - -You can retrieve it yourself with your browser or use the [Apple App Site Association \(AASA\) Validator](https://branch.io/resources/aasa-validator/). - -**Checking the Link Receiver Method** - -In order to receive links and handle them appropriately, the app delegate has to implement [`application:continueUserActivity:restorationHandler:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623072-application). If you have the original project try searching for this method. - -Please note that if the app uses [`openURL:options:completionHandler:`](https://developer.apple.com/documentation/uikit/uiapplication/1648685-openurl?language=objc) to open a universal link to the app's website, the link won't open in the app. As the call originates from the app, it won't be handled as a universal link. - -* The scheme of the `webpageURL` must be HTTP or HTTPS \(any other scheme should throw an exception\). The [`scheme` instance property](https://developer.apple.com/documentation/foundation/urlcomponents/1779624-scheme) of `URLComponents` / `NSURLComponents` can be used to verify this. - -**Checking the Data Handler Method** - -When iOS opens an app as the result of a universal link, the app receives an `NSUserActivity` object with an `activityType` value of `NSUserActivityTypeBrowsingWeb`. The activity object’s `webpageURL` property contains the HTTP or HTTPS URL that the user accesses. The following example in Swift verifies exactly this before opening the URL: - -```swift -func application(_ application: UIApplication, continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - // ... - if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { - application.open(url, options: [:], completionHandler: nil) - } - - return true -} -``` - -In addition, remember that if the URL includes parameters, they should not be trusted before being carefully sanitized and validated \(even when coming from trusted domain\). For example, they might have been spoofed by an attacker or might include malformed data. If that is the case, the whole URL and therefore the universal link request must be discarded. - -The `NSURLComponents` API can be used to parse and manipulate the components of the URL. This can be also part of the method `application:continueUserActivity:restorationHandler:` itself or might occur on a separate method being called from it. The following [example](https://developer.apple.com/documentation/uikit/core_app/allowing_apps_and_websites_to_link_to_your_content/handling_universal_links#3001935) demonstrates this: - -```swift -func application(_ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([Any]?) -> Void) -> Bool { - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL, - let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), - let path = components.path, - let params = components.queryItems else { - return false - } - - if let albumName = params.first(where: { $0.name == "albumname" })?.value, - let photoIndex = params.first(where: { $0.name == "index" })?.value { - // Interact with album name and photo index - - return true - - } else { - // Handle when album and/or album name or photo index missing - - return false - } -} -``` +{% page-ref page="ios-universal-links.md" %} ### UIActivity Sharing @@ -1181,78 +926,7 @@ func application(_ application: UIApplication, ### UIPasteboard -The [`UIPasteboard`](https://developer.apple.com/documentation/uikit/uipasteboard) enables sharing data within an app, and from an app to other apps. There are two kinds of pasteboards: - -* **systemwide general pasteboard**: for sharing data with **any app**. Persistent by default across device restarts and app uninstalls \(since iOS 10\). -* **custom / named pasteboards**: for sharing data **with another app** \(having the same team ID as the app to share from\) or with the **app itself** \(they are only available in the process that creates them\). Non-persistent by default \(since iOS 10\), that is, they exist only until the owning \(creating\) app quits. - -Some security considerations: - -* Users **cannot grant or deny permission** for apps to read the **pasteboard**. -* Since iOS 9, apps [cannot access the pasteboard while in background](https://forums.developer.apple.com/thread/13760), this mitigates background pasteboard monitoring. -* [Apple warns about persistent named pasteboards](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc) and **discourages their use**. Instead, shared containers should be used. -* Starting in iOS 10 there is a new Handoff feature called **Universal Clipboard** that is enabled by default. It allows the **general pasteboard contents to automatically transfer between devices**. This feature can be disabled if the developer chooses to do so and it is also possible to set an expiration time and date for copied data. - -Then, it's important to **check that sensitive information isn't being saved inside the global pasteboard**. -It's also important to check that an **application isn't using the global pasteboard data to perform actions**, as malicious application could tamper this data. - -An **application can also prevent its users to copy sensitive data to the clipboard** \(which is recommended\). - -#### Static Analysis - -The **systemwide general pasteboard** can be obtained by using [`generalPasteboard`](https://developer.apple.com/documentation/uikit/uipasteboard/1622106-generalpasteboard?language=objc), search the source code or the compiled binary for this method. Using the systemwide general pasteboard should be avoided when dealing with sensitive data. - -**Custom pasteboards** can be created with [`pasteboardWithName:create:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622074-pasteboardwithname?language=objc) or [`pasteboardWithUniqueName`](https://developer.apple.com/documentation/uikit/uipasteboard/1622087-pasteboardwithuniquename?language=objc). Verify if custom pasteboards are set to be persistent as this is deprecated since iOS 10. A shared container should be used instead. - -#### Dynamic analysis - -Hook or trace the following: - -* `generalPasteboard` for the system-wide general pasteboard. -* `pasteboardWithName:create:` and `pasteboardWithUniqueName` for custom pasteboards. - -You can also Hook or trace the deprecated [`setPersistent:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622096-setpersistent?language=objc) method and verify if it's being called. - -When **monitoring** the **pasteboards**, there is several **details** that may be dynamically **retrieved**: - -* Obtain **pasteboard name** by hooking `pasteboardWithName:create:` and inspecting its input parameters or `pasteboardWithUniqueName` and inspecting its return value. -* Get the **first available pasteboard item**: e.g. for strings use `string` method. Or use any of the other methods for the [standard data types](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc#1654275). -* Get the **number of items** with `numberOfItems`. -* Check for **existence of standard data types** with the [convenience methods](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc#2107142), e.g. `hasImages`, `hasStrings`, `hasURLs` \(starting in iOS 10\). -* Check for **other data types** \(typically UTIs\) with [`containsPasteboardTypes:inItemSet:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622100-containspasteboardtypes?language=objc). You may inspect for more concrete data types like, for example an picture as public.png and public.tiff \([UTIs](http://web.archive.org/web/20190616231857/https://developer.apple.com/documentation/mobilecoreservices/uttype)\) or for custom data such as com.mycompany.myapp.mytype. Remember that, in this case, only those apps that _declare knowledge_ of the type are able to understand the data written to the pasteboard. Retrieve them using [`itemSetWithPasteboardTypes:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622071-itemsetwithpasteboardtypes?language=objc) and setting the corresponding UTIs. -* Check for excluded or expiring items by hooking `setItems:options:` and inspecting its options for `UIPasteboardOptionLocalOnly` or `UIPasteboardOptionExpirationDate`. - -If only looking for strings you may want to use **objection's** command `ios pasteboard monitor`: - -> Hooks into the iOS UIPasteboard class and polls the generalPasteboard every 5 seconds for data. If new data is found, different from the previous poll, that data will be dumped to screen. - -You may also build your own pasteboard monitor that monitors specific information as seen above. - -For example, this script \(inspired from the script behind [objection's pasteboard monitor](https://github.com/sensepost/objection/blob/b39ee53b5ba2e9a271797d2f3931d79c46dccfdb/agent/src/ios/pasteboard.ts)\) reads the pasteboard items every 5 seconds, if there's something new it will print it: - -```javascript -const UIPasteboard = ObjC.classes.UIPasteboard; - const Pasteboard = UIPasteboard.generalPasteboard(); - var items = ""; - var count = Pasteboard.changeCount().toString(); - -setInterval(function () { - const currentCount = Pasteboard.changeCount().toString(); - const currentItems = Pasteboard.items().toString(); - - if (currentCount === count) { return; } - - items = currentItems; - count = currentCount; - - console.log('[* Pasteboard changed] count: ' + count + - ' hasStrings: ' + Pasteboard.hasStrings().toString() + - ' hasURLs: ' + Pasteboard.hasURLs().toString() + - ' hasImages: ' + Pasteboard.hasImages().toString()); - console.log(items); - - }, 1000 * 5); -``` +{% page-ref page="ios-uipasteboard.md" %} ### App Extensions @@ -1262,6 +936,10 @@ setInterval(function () { {% page-ref page="ios-webviews.md" %} +### Serialisation and Encoding + +{% page-ref page="ios-serialisation-and-encoding.md" %} + ## Network Communication It's important to check that no communication is occurring **without encryption** and also that the application is correctly **validating the TLS certificate** of the server. @@ -1292,11 +970,11 @@ You can also use **objection's** `ios sslpinning disable` * **`iTunesMetadata.plist`**: Info of the app used in the App Store * **`/Library/*`**: Contains the preferences and cache. In **`/Library/Cache/Snapshots/*`** you can find the snapshot performed to the application before sending it to the background. -### Hot Patching +### Hot Patching/Enforced Updateing The developers can remotely **patch all installations of their app instantly** without having to resubmit the application to the App store and wait until it's approved. -For this purpose it's usually use [**JSPatch**](https://github.com/bang590/JSPatch)**. -This is a dangerous mechanism that could be abused by malicious third party SDKs.** +For this purpose it's usually use [**JSPatch**](https://github.com/bang590/JSPatch)**.** But there are other options also such as [Siren](https://github.com/ArtSabintsev/Siren) and [react-native-appstore-version-checker](https://www.npmjs.com/package/react-native-appstore-version-checker). +**This is a dangerous mechanism that could be abused by malicious third party SDKs therefore it's recommended to check which method is used to automatic updating \(if any\) and test it.** You could try to download a previous version of the app for this purpose. ### Third Parties diff --git a/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md b/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md new file mode 100644 index 000000000..233f3495c --- /dev/null +++ b/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md @@ -0,0 +1,180 @@ +# iOS Custom URI Handlers / Deeplinks / Custom Schemes + +Custom URL schemes [allow apps to communicate via a custom protocol](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html#//apple_ref/doc/uid/TP40007072-CH6-SW1). An app must declare support for the schemes and handle incoming URLs that use those schemes. + +> URL schemes offer a potential attack vector into your app, so make sure to **validate all URL parameters** and **discard any malformed URLs**. In addition, limit the available **actions** to those that d**o not risk the user’s data**. + +For example, the URI: `myapp://hostname?data=123876123` will **invoke** the **application** mydata \(the one that has **register** the scheme `mydata`\) to the **action** related to the **hostname** `hostname` sending the **parameter** `data` with value `123876123` + +One vulnerable example is the following [bug in the Skype Mobile app](http://www.dhanjani.com/blog/2010/11/insecure-handling-of-url-schemes-in-apples-ios.html), discovered in 2010: The Skype app registered the `skype://` protocol handler, which **allowed other apps to trigger calls to other Skype users and phone numbers**. Unfortunately, Skype didn't ask users for permission before placing the calls, so any app could call arbitrary numbers without the user's knowledge. Attackers exploited this vulnerability by putting an invisible `` \(where `xxx` was replaced by a premium number\), so any Skype user who inadvertently visited a malicious website called the premium number. + +You can find the **schemes registered by an application** in the app's **`Info.plist`** file searching for **`CFBundleURLTypes`** \(example from [iGoat-Swift](https://github.com/OWASP/iGoat-Swift)\): + +```markup +CFBundleURLTypes + + + CFBundleURLName + com.iGoat.myCompany + CFBundleURLSchemes + + iGoat + + + +``` + +However, note that **malicious applications can re-register URIs** already registered by applications. So, if you are sending **sensitive information via URIs** \(myapp://hostname?password=123456\) a **malicious** application can **intercept** the URI with the **sensitive** **information**. + +Also, the input of these URIs **should be checked and sanitised,** as it can be coming from **malicious** **origins** trying to exploit SQLInjections, XSS, CSRF, Path Traversals, or other possible vulnerabilities. + +### Application Query Schemes Registration + +Apps can call [`canOpenURL:`](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl?language=objc) to verify that the **target app is available**. However, as this method was being used by malicious app as a way to **enumerate installed apps**, [from iOS 9.0 the URL schemes passed to it must be also declared](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl?language=objc#discussion) by adding the `LSApplicationQueriesSchemes` key to the app's `Info.plist` file and an array of **up to 50 URL schemes**. + +```markup +LSApplicationQueriesSchemes + + url_scheme1 + url_scheme2 + +``` + +`canOpenURL` will always return `NO` for undeclared schemes, whether or not an appropriate app is installed. However, this restriction only applies to `canOpenURL`. + +### Testing URL Handling and Validation + +In order to determine how a URL path is built and validated, if you have the original source code, you can **search for the following methods**: + +* `application:didFinishLaunchingWithOptions:` method or `application:will-FinishLaunchingWithOptions:`: verify how the decision is made and how the information about the URL is retrieved. +* [`application:openURL:options:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application?language=objc): verify how the resource is being opened, i.e. how the data is being parsed, verify the [options](https://developer.apple.com/documentation/uikit/uiapplication/openurloptionskey), especially if access by the calling app \([`sourceApplication`](https://developer.apple.com/documentation/uikit/uiapplication/openurloptionskey/1623128-sourceapplication)\) should be allowed or denied. The app might also need user permission when using the custom URL scheme. + +In Telegram you will [find four different methods being used](https://github.com/peter-iakovlev/Telegram-iOS/blob/87e0a33ac438c1d702f2a0b75bf21f26866e346f/Telegram-iOS/AppDelegate.swift#L1250): + +```swift +func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool { + self.openUrl(url: url) + return true +} + +func application(_ application: UIApplication, open url: URL, sourceApplication: String?, +annotation: Any) -> Bool { + self.openUrl(url: url) + return true +} + +func application(_ app: UIApplication, open url: URL, +options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + self.openUrl(url: url) + return true +} + +func application(_ application: UIApplication, handleOpen url: URL) -> Bool { + self.openUrl(url: url) + return true +} +``` + +### Testing URL Requests to Other Apps + +The method [`openURL:options:completionHandler:`](https://developer.apple.com/documentation/uikit/uiapplication/1648685-openurl?language=objc) and the [deprecated `openURL:` method of `UIApplication`](https://developer.apple.com/documentation/uikit/uiapplication/1622961-openurl?language=objc) are responsible for **opening URLs** \(i.e. to send requests / make queries to other apps\) that may be local to the current app or it may be one that must be provided by a different app. If you have the original source code you can search directly for usages of those methods. + +Additionally, if you are interested into knowing if the app is querying specific services or apps, and if the app is well-known, you can also search for common URL schemes online and include them in your **greps \(l**[**ist of iOS app schemes**](https://ios.gadgethacks.com/how-to/always-updated-list-ios-app-url-scheme-names-paths-for-shortcuts-0184033/)**\)**. + +```bash +egrep -nr "open.*options.*completionHandler" ./Telegram-iOS/ +egrep -nr "openURL\(" ./Telegram-iOS/ +egrep -nr "mt-encrypted-file://" ./Telegram-iOS/ +egrep -nr "://" ./Telegram-iOS/ +``` + +### Testing for Deprecated Methods + +Search for deprecated methods like: + +* [`application:handleOpenURL:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622964-application?language=objc) +* [`openURL:`](https://developer.apple.com/documentation/uikit/uiapplication/1622961-openurl?language=objc) +* [`application:openURL:sourceApplication:annotation:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623073-application) + +For example, here we find those three: + +```bash +$ rabin2 -zzq Telegram\ X.app/Telegram\ X | grep -i "openurl" + +0x1000d9e90 31 30 UIApplicationOpenURLOptionsKey +0x1000dee3f 50 49 application:openURL:sourceApplication:annotation: +0x1000dee71 29 28 application:openURL:options: +0x1000dee8e 27 26 application:handleOpenURL: +0x1000df2c9 9 8 openURL: +0x1000df766 12 11 canOpenURL: +0x1000df772 35 34 openURL:options:completionHandler: +... +``` + +### Calling arbitrary URLs + +* **Safari**: To quickly test one URL scheme you can open the URLs on Safari and observe how the app behaves. For example, if you write `tel://123456789` safari will try to start calling the number. +* **Notes App**: Long press the links you've written in order to test custom URL schemes. Remember to exit the editing mode in order to be able to open them. Note that you can click or long press links including custom URL schemes only if the app is installed, if not they won't be highlighted as _clickable links_. +* [**IDB**](https://github.com/facebook/idb): + * Start IDB, connect to your device and select the target app. You can find details in the [IDB documentation](https://www.idbtool.com/documentation/setup.html). + * Go to the **URL Handlers** section. In **URL schemes**, click **Refresh**, and on the left you'll find a list of all custom schemes defined in the app being tested. You can load these schemes by clicking **Open**, on the right side. By simply opening a blank URI scheme \(e.g., opening `myURLscheme://`\), you can discover hidden functionality \(e.g., a debug window\) and bypass local authentication. +* **Frida**: + + If you simply want to open the URL scheme you can do it using Frida: + + ```javascript + $ frida -U iGoat-Swift + + [iPhone::iGoat-Swift]-> function openURL(url) { + var UIApplication = ObjC.classes.UIApplication.sharedApplication(); + var toOpen = ObjC.classes.NSURL.URLWithString_(url); + return UIApplication.openURL_(toOpen); + } + [iPhone::iGoat-Swift]-> openURL("tel://234234234") + true + ``` + + In this example from [Frida CodeShare](https://codeshare.frida.re/@dki/ios-url-scheme-fuzzing/) the author uses the non-public API `LSApplicationWorkspace.openSensitiveURL:withOptions:` to open the URLs \(from the SpringBoard app\): + + ```javascript + function openURL(url) { + var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace(); + var toOpen = ObjC.classes.NSURL.URLWithString_(url); + return w.openSensitiveURL_withOptions_(toOpen, null); + } + ``` + + > Note that the use of non-public APIs is not permitted on the App Store, that's why we don't even test these but we are allowed to use them for our dynamic analysis. + +### Fuzzing URL Schemes + +If the app parses parts of the URL, you can also perform input fuzzing to detect memory corruption bugs. + +What we have learned above can be now used to build your own fuzzer on the language of your choice, e.g. in Python and call the `openURL` using [Frida's RPC](https://www.frida.re/docs/javascript-api/#rpc). That fuzzer should do the following: + +* Generate payloads. +* For each of them call `openURL`. +* Check if the app generates a crash report \(`.ips`\) in `/private/var/mobile/Library/Logs/CrashReporter`. + +The [FuzzDB](https://github.com/fuzzdb-project/fuzzdb) project offers fuzzing dictionaries that you can use as payloads. + +### **Fuzzing Using Frida** + +Doing this with Frida is pretty easy, you can refer to this [blog post](https://grepharder.github.io/blog/0x03_learning_about_universal_links_and_fuzzing_url_schemes_on_ios_with_frida.html) to see an example that fuzzes the iGoat-Swift app \(working on iOS 11.1.2\). + +Before running the fuzzer we need the URL schemes as inputs. From the static analysis we know that the iGoat-Swift app supports the following URL scheme and parameters: `iGoat://?contactNumber={0}&message={0}`. + +```bash +$ frida -U SpringBoard -l ios-url-scheme-fuzzing.js +[iPhone::SpringBoard]-> fuzz("iGoat", "iGoat://?contactNumber={0}&message={0}") +Watching for crashes from iGoat... +No logs were moved. +Opened URL: iGoat://?contactNumber=0&message=0 +``` + +## References + +{% embed url="https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction\#testing-object-persistence-mstg-platform-8" %} + + + diff --git a/ios-pentesting/ios-serialisation-and-encoding.md b/ios-pentesting/ios-serialisation-and-encoding.md new file mode 100644 index 000000000..166f479ad --- /dev/null +++ b/ios-pentesting/ios-serialisation-and-encoding.md @@ -0,0 +1,154 @@ +# iOS Serialisation and Encoding + + + +#### NSCoding and NSSecureCoding + +iOS comes with two protocols for object **serialisation** for Objective-C or `NSObject`s: **`NSCoding`** and **`NSSecureCoding`**. When a **class conforms** to either of the protocols, the data is serialized to **`NSData`**: a wrapper for **byte buffers**. Note that `Data` in Swift is the same as `NSData` or its mutable counterpart: `NSMutableData`. The `NSCoding` protocol declares the two methods that must be implemented in order to encode/decode its instance-variables. **A class using `NSCoding` needs to implement `NSObject` or be annotated as an @objc class**. The `NSCoding` protocol requires to implement encode and init as shown below. + +```swift +class CustomPoint: NSObject, NSCoding { + + //required by NSCoding: + func encode(with aCoder: NSCoder) { + aCoder.encode(x, forKey: "x") + aCoder.encode(name, forKey: "name") + } + + var x: Double = 0.0 + var name: String = "" + + init(x: Double, name: String) { + self.x = x + self.name = name + } + + // required by NSCoding: initialize members using a decoder. + required convenience init?(coder aDecoder: NSCoder) { + guard let name = aDecoder.decodeObject(forKey: "name") as? String + else {return nil} + self.init(x:aDecoder.decodeDouble(forKey:"x"), + name:name) + } + + //getters/setters/etc. +} +``` + +The issue with `NSCoding` is that the object is often already **constructed and inserted before you can evaluate** the class-type. This **allows an attacker to easily inject all sorts of data**. Therefore, the **`NSSecureCoding`** protocol has been introduced. When conforming to [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/NSSecureCoding) you need to include: + +```swift +static var supportsSecureCoding: Bool { + return true +} +``` + +when `init(coder:)` is part of the class. Next, when decoding the object, a check should be made, e.g.: + +```swift +let obj = decoder.decodeObject(of:MyClass.self, forKey: "myKey") +``` + +The conformance to `NSSecureCoding` ensures that objects being instantiated are indeed the ones that were expected. However, there are **no additional integrity checks done** over the data and the data is not encrypted. Therefore, any secret data needs additional **encryption** and data of which the integrity must be protected, should get an additional HMAC. + +#### Object Archiving with NSKeyedArchiver + +`NSKeyedArchiver` is a concrete subclass of `NSCoder` and provides a way to encode objects and store them in a file. The `NSKeyedUnarchiver` decodes the data and recreates the original data. Let's take the example of the `NSCoding` section and now archive and unarchive them: + +```swift +// archiving: +NSKeyedArchiver.archiveRootObject(customPoint, toFile: "/path/to/archive") + +// unarchiving: +guard let customPoint = NSKeyedUnarchiver.unarchiveObjectWithFile("/path/to/archive") as? + CustomPoint else { return nil } +``` + +You can also save the info in primary plist `NSUserDefaults`: + +```swift +// archiving: +let data = NSKeyedArchiver.archivedDataWithRootObject(customPoint) +NSUserDefaults.standardUserDefaults().setObject(data, forKey: "customPoint") + +// unarchiving: +if let data = NSUserDefaults.standardUserDefaults().objectForKey("customPoint") as? NSData { + let customPoint = NSKeyedUnarchiver.unarchiveObjectWithData(data) +} +``` + +#### Codable + +It is a combination of the `Decodable` and `Encodable` protocols. A `String`, `Int`, `Double`, `Date`, `Data` and `URL` are `Codable` by nature: meaning they can easily be encoded and decoded without any additional work. Let's take the following example: + +```swift +struct CustomPointStruct:Codable { + var x: Double + var name: String +} +``` + +By adding `Codable` to the inheritance list for the `CustomPointStruct` in the example, the methods `init(from:)` and `encode(to:)` are automatically supported. Fore more details about the workings of `Codable` check [the Apple Developer Documentation](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types). + +You can also use codable to save the data in the primary property list `NSUserDefaults`: + +```swift +struct CustomPointStruct: Codable { + var point: Double + var name: String + } + + var points: [CustomPointStruct] = [ + CustomPointStruct(point: 1, name: "test"), + CustomPointStruct(point: 2, name: "test"), + CustomPointStruct(point: 3, name: "test"), + ] + + UserDefaults.standard.set(try? PropertyListEncoder().encode(points), forKey: "points") + if let data = UserDefaults.standard.value(forKey: "points") as? Data { + let points2 = try? PropertyListDecoder().decode([CustomPointStruct].self, from: data) + } +``` + +#### JSON Encoding + +There are a lot of thrid party libraries to encode data in JSON \(like exposed [here](https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction#json-and-codable)\). However, Apple provides support for JSON encoding/decoding directly by combining `Codable` together with a `JSONEncoder` and a `JSONDecoder`: + +```swift +struct CustomPointStruct: Codable { + var point: Double + var name: String +} + +let encoder = JSONEncoder() +encoder.outputFormatting = .prettyPrinted + +let test = CustomPointStruct(point: 10, name: "test") +let data = try encoder.encode(test) +let stringData = String(data: data, encoding: .utf8) + +// stringData = Optional ({ +// "point" : 10, +// "name" : "test" +// }) +``` + +#### XML + +There are multiple ways to do XML encoding. Similar to JSON parsing, there are various third party libraries, such as: [Fuzi](https://github.com/cezheng/Fuzi), [Ono](https://github.com/mattt/Ono), [AEXML](https://github.com/tadija/AEXML), [RaptureXML](https://github.com/ZaBlanc/RaptureXML), [SwiftyXMLParser](https://github.com/yahoojapan/SwiftyXMLParser), [SWXMLHash](https://github.com/drmohundro/SWXMLHash) + +They vary in terms of speed, memory usage, object persistence and more important: differ in how they handle XML external entities. See [XXE in the Apple iOS Office viewer](https://nvd.nist.gov/vuln/detail/CVE-2015-3784) as an example. Therefore, it is key to disable external entity parsing if possible. See the [OWASP XXE prevention cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html) for more details. Next to the libraries, you can make use of Apple's [`XMLParser` class](https://developer.apple.com/documentation/foundation/xmlparser) + +When not using third party libraries, but Apple's `XMLParser`, be sure to let `shouldResolveExternalEntities` return `false`. + +{% hint style="danger" %} +All these ways of serialising/encoding data can be **used to store data in the file system**. In those scenarios, check if the stored data contains any kind of **sensitive information**. +Moreover, in some cases you may be able to **abuse some serialised** data \(capturing it via MitM or modifying it inside the filesystem\) deserializing arbitrary data and **making the application perform unexpected actions** \(see [Deserialization page](../pentesting-web/deserialization/)\). In these cases, it's recommended to send/save the serialised data encrypted and signed. +{% endhint %} + +### References + +{% embed url="https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction\#testing-object-persistence-mstg-platform-8" %} + + + diff --git a/ios-pentesting/ios-uipasteboard.md b/ios-pentesting/ios-uipasteboard.md new file mode 100644 index 000000000..99c2ac409 --- /dev/null +++ b/ios-pentesting/ios-uipasteboard.md @@ -0,0 +1,81 @@ +# iOS UIPasteboard + +The [`UIPasteboard`](https://developer.apple.com/documentation/uikit/uipasteboard) enables sharing data within an app, and from an app to other apps. There are two kinds of pasteboards: + +* **systemwide general pasteboard**: for sharing data with **any app**. Persistent by default across device restarts and app uninstalls \(since iOS 10\). +* **custom / named pasteboards**: for sharing data **with another app** \(having the same team ID as the app to share from\) or with the **app itself** \(they are only available in the process that creates them\). Non-persistent by default \(since iOS 10\), that is, they exist only until the owning \(creating\) app quits. + +Some security considerations: + +* Users **cannot grant or deny permission** for apps to read the **pasteboard**. +* Since iOS 9, apps [cannot access the pasteboard while in background](https://forums.developer.apple.com/thread/13760), this mitigates background pasteboard monitoring. +* [Apple warns about persistent named pasteboards](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc) and **discourages their use**. Instead, shared containers should be used. +* Starting in iOS 10 there is a new Handoff feature called **Universal Clipboard** that is enabled by default. It allows the **general pasteboard contents to automatically transfer between devices**. This feature can be disabled if the developer chooses to do so and it is also possible to set an expiration time and date for copied data. + +Then, it's important to **check that sensitive information isn't being saved inside the global pasteboard**. +It's also important to check that an **application isn't using the global pasteboard data to perform actions**, as malicious application could tamper this data. + +An **application can also prevent its users to copy sensitive data to the clipboard** \(which is recommended\). + +### Static Analysis + +The **systemwide general pasteboard** can be obtained by using [`generalPasteboard`](https://developer.apple.com/documentation/uikit/uipasteboard/1622106-generalpasteboard?language=objc), search the source code or the compiled binary for this method. Using the systemwide general pasteboard should be avoided when dealing with sensitive data. + +**Custom pasteboards** can be created with [`pasteboardWithName:create:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622074-pasteboardwithname?language=objc) or [`pasteboardWithUniqueName`](https://developer.apple.com/documentation/uikit/uipasteboard/1622087-pasteboardwithuniquename?language=objc). Verify if custom pasteboards are set to be persistent as this is deprecated since iOS 10. A shared container should be used instead. + +### Dynamic analysis + +Hook or trace the following: + +* `generalPasteboard` for the system-wide general pasteboard. +* `pasteboardWithName:create:` and `pasteboardWithUniqueName` for custom pasteboards. + +You can also Hook or trace the deprecated [`setPersistent:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622096-setpersistent?language=objc) method and verify if it's being called. + +When **monitoring** the **pasteboards**, there is several **details** that may be dynamically **retrieved**: + +* Obtain **pasteboard name** by hooking `pasteboardWithName:create:` and inspecting its input parameters or `pasteboardWithUniqueName` and inspecting its return value. +* Get the **first available pasteboard item**: e.g. for strings use `string` method. Or use any of the other methods for the [standard data types](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc#1654275). +* Get the **number of items** with `numberOfItems`. +* Check for **existence of standard data types** with the [convenience methods](https://developer.apple.com/documentation/uikit/uipasteboard?language=objc#2107142), e.g. `hasImages`, `hasStrings`, `hasURLs` \(starting in iOS 10\). +* Check for **other data types** \(typically UTIs\) with [`containsPasteboardTypes:inItemSet:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622100-containspasteboardtypes?language=objc). You may inspect for more concrete data types like, for example an picture as public.png and public.tiff \([UTIs](http://web.archive.org/web/20190616231857/https://developer.apple.com/documentation/mobilecoreservices/uttype)\) or for custom data such as com.mycompany.myapp.mytype. Remember that, in this case, only those apps that _declare knowledge_ of the type are able to understand the data written to the pasteboard. Retrieve them using [`itemSetWithPasteboardTypes:`](https://developer.apple.com/documentation/uikit/uipasteboard/1622071-itemsetwithpasteboardtypes?language=objc) and setting the corresponding UTIs. +* Check for excluded or expiring items by hooking `setItems:options:` and inspecting its options for `UIPasteboardOptionLocalOnly` or `UIPasteboardOptionExpirationDate`. + +If only looking for strings you may want to use **objection's** command `ios pasteboard monitor`: + +> Hooks into the iOS UIPasteboard class and polls the generalPasteboard every 5 seconds for data. If new data is found, different from the previous poll, that data will be dumped to screen. + +You may also build your own pasteboard monitor that monitors specific information as seen above. + +For example, this script \(inspired from the script behind [objection's pasteboard monitor](https://github.com/sensepost/objection/blob/b39ee53b5ba2e9a271797d2f3931d79c46dccfdb/agent/src/ios/pasteboard.ts)\) reads the pasteboard items every 5 seconds, if there's something new it will print it: + +```javascript +const UIPasteboard = ObjC.classes.UIPasteboard; + const Pasteboard = UIPasteboard.generalPasteboard(); + var items = ""; + var count = Pasteboard.changeCount().toString(); + +setInterval(function () { + const currentCount = Pasteboard.changeCount().toString(); + const currentItems = Pasteboard.items().toString(); + + if (currentCount === count) { return; } + + items = currentItems; + count = currentCount; + + console.log('[* Pasteboard changed] count: ' + count + + ' hasStrings: ' + Pasteboard.hasStrings().toString() + + ' hasURLs: ' + Pasteboard.hasURLs().toString() + + ' hasImages: ' + Pasteboard.hasImages().toString()); + console.log(items); + + }, 1000 * 5); +``` + +## References + +{% embed url="https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction\#testing-object-persistence-mstg-platform-8" %} + + + diff --git a/ios-pentesting/ios-universal-links.md b/ios-pentesting/ios-universal-links.md new file mode 100644 index 000000000..b9621ef7c --- /dev/null +++ b/ios-pentesting/ios-universal-links.md @@ -0,0 +1,95 @@ +# iOS Universal Links + +Universal links allows to **redirect users directly** to the app without passing through safari for redirection. +Universal links are **unique**, so they **can't be claimed by other app**s because they use standard HTTP\(S\) links to the **website where the owner has uploaded a file to make sure that the website and the app are related**. +As these links uses HTTP\(S\) schemes, when the **app isn't installed, safari will open the link** redirecting the users to the page. These allows **apps to communicate with the app even if it isn't installed**. + +To create universal links it's needed to **create a JSON file called `apple-app-site-association`** with the details. Then this file needs to be **hosted in the root directory of your webserver** \(e.g. [https://google.com/apple-app-site-association](https://google.com/apple-app-site-association)\). +For the pentester this file is very interesting as it **discloses paths**. It can even be disclosing paths of releases that haven't been published yet. + +### ​**Checking the Associated Domains Entitlement** + +n Xcode, go to the **Capabilities** tab and search for **Associated Domains**. You can also inspect the `.entitlements` file looking for `com.apple.developer.associated-domains`. Each of the domains must be prefixed with `applinks:`, such as `applinks:www.mywebsite.com`. + +Here's an example from Telegram's `.entitlements` file: + +```markup + com.apple.developer.associated-domains + + applinks:telegram.me + applinks:t.me + +``` + +More detailed information can be found in the [archived Apple Developer Documentation](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html#//apple_ref/doc/uid/TP40016308-CH12-SW2). + +If you only has the compiled application you can extract the entitlements following this guide: + +{% page-ref page="extracting-entitlements-from-compiled-application.md" %} + +### R**etrieving the Apple App Site Association File** + +Try to retrieve the `apple-app-site-association` file from the server using the associated domains you got from the previous step. This file needs to be accessible via HTTPS, without any redirects, at `https:///apple-app-site-association` or `https:///.well-known/apple-app-site-association`. + +You can retrieve it yourself with your browser or use the [Apple App Site Association \(AASA\) Validator](https://branch.io/resources/aasa-validator/). + +### **Checking the Link Receiver Method** + +In order to receive links and handle them appropriately, the app delegate has to implement [`application:continueUserActivity:restorationHandler:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623072-application). If you have the original project try searching for this method. + +Please note that if the app uses [`openURL:options:completionHandler:`](https://developer.apple.com/documentation/uikit/uiapplication/1648685-openurl?language=objc) to open a universal link to the app's website, the link won't open in the app. As the call originates from the app, it won't be handled as a universal link. + +* The scheme of the `webpageURL` must be HTTP or HTTPS \(any other scheme should throw an exception\). The [`scheme` instance property](https://developer.apple.com/documentation/foundation/urlcomponents/1779624-scheme) of `URLComponents` / `NSURLComponents` can be used to verify this. + +### **Checking the Data Handler Method** + +When iOS opens an app as the result of a universal link, the app receives an `NSUserActivity` object with an `activityType` value of `NSUserActivityTypeBrowsingWeb`. The activity object’s `webpageURL` property contains the HTTP or HTTPS URL that the user accesses. The following example in Swift verifies exactly this before opening the URL: + +```swift +func application(_ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // ... + if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { + application.open(url, options: [:], completionHandler: nil) + } + + return true +} +``` + +In addition, remember that if the URL includes parameters, they should not be trusted before being carefully sanitized and validated \(even when coming from trusted domain\). For example, they might have been spoofed by an attacker or might include malformed data. If that is the case, the whole URL and therefore the universal link request must be discarded. + +The `NSURLComponents` API can be used to parse and manipulate the components of the URL. This can be also part of the method `application:continueUserActivity:restorationHandler:` itself or might occur on a separate method being called from it. The following [example](https://developer.apple.com/documentation/uikit/core_app/allowing_apps_and_websites_to_link_to_your_content/handling_universal_links#3001935) demonstrates this: + +```swift +func application(_ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL, + let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), + let path = components.path, + let params = components.queryItems else { + return false + } + + if let albumName = params.first(where: { $0.name == "albumname" })?.value, + let photoIndex = params.first(where: { $0.name == "index" })?.value { + // Interact with album name and photo index + + return true + + } else { + // Handle when album and/or album name or photo index missing + + return false + } +} +``` + +## References + +{% embed url="https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction\#testing-object-persistence-mstg-platform-8" %} + + + diff --git a/ios-pentesting/ios-webviews.md b/ios-pentesting/ios-webviews.md index 7d2c01f48..fe79ed4ac 100644 --- a/ios-pentesting/ios-webviews.md +++ b/ios-pentesting/ios-webviews.md @@ -211,6 +211,79 @@ hasOnlySecureContent: false allowUniversalAccessFromFileURLs: 0 ``` +## 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: + +```objectivec +[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:)`](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537172-add). + +### Enabling JavascriptBridge + +```swift +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](https://github.com/authenticationfailure/WheresMyBrowser.iOS/blob/d4e2d9efbde8841bf7e4a8800418dda6bb116ec6/WheresMyBrowser/web/WKWebView/scenario3.html#L33): + +```javascript +function invokeNativeOperation() { + value1 = document.getElementById("value1").value + value2 = document.getElementById("value2").value + window.webkit.messageHandlers.javaScriptBridge.postMessage(["multiplyNumbers", value1, value2]); +} +``` + +### Called Function + +The called function resides in [`JavaScriptBridgeMessageHandler.swift`](https://github.com/authenticationfailure/WheresMyBrowser.iOS/blob/b8d4abda4000aa509c7a5de79e5c90360d1d0849/WheresMyBrowser/JavaScriptBridgeMessageHandler.swift#L29): + +```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: + +* Change the servers response \(MitM\) +* Perform a dynamic instrumentation and inject the JavaScript payload by using frameworks like Frida and the corresponding JavaScript evaluation functions available for the iOS WebViews \([`stringByEvaluatingJavaScriptFromString:`](https://developer.apple.com/documentation/uikit/uiwebview/1617963-stringbyevaluatingjavascriptfrom?language=objc) for `UIWebView` and [`evaluateJavaScript:completionHandler:`](https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript?language=objc) for `WKWebView`\). + ## References * [https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06h-testing-platform-interaction\#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)