mirror of
https://github.com/carlospolop/hacktricks
synced 2024-12-30 06:53:11 +00:00
95 lines
6.1 KiB
Markdown
95 lines
6.1 KiB
Markdown
# 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
|
||
<key>com.apple.developer.associated-domains</key>
|
||
<array>
|
||
<string>applinks:telegram.me</string>
|
||
<string>applinks:t.me</string>
|
||
</array>
|
||
```
|
||
|
||
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:
|
||
|
||
{% content-ref url="extracting-entitlements-from-compiled-application.md" %}
|
||
[extracting-entitlements-from-compiled-application.md](extracting-entitlements-from-compiled-application.md)
|
||
{% endcontent-ref %}
|
||
|
||
### 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://<domain>/apple-app-site-association` or `https://<domain>/.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" %}
|
||
|