Skip to main content

Fuzzing Custom URL Schemes

A custom URL scheme[^1] provides the developer a way for users to access resources or functionality that are in the app. When a user taps a link with a custom URL scheme that the app has registered then that specific part of the app will be launched. Other apps can launch the app in the specific context of the URL scheme by crafting a link that will work in the source app (assuming the source app allows this).

Apple has created some common URL schemes that are associated with system apps that they control. These schemes cannot be overridden by third-party apps. For instance, Apple has registered "mailto", "tel", and "sms". When a link is crafted with these schemes, the respective app will open and go to the specific context of the link.

For example, if a link is created with "tel://7045551234" then the phone app will launch and dial the 704-555-1234 telephone number for the user.

If the app has registered a URL scheme, it must have code to handle the incoming link which ensures that it is parsed correctly, and properly handles any malformed input. Apple provides the following warning regarding URL schemes:

As of iOS 11, these URL scheme's work on a first registered process. The first app on the device that claims the URL scheme will be the owner for as long as the app remains installed. If a second app gets installed that also tries to register that scheme, it will be denied and may break functionality in the second app. Obviously, this is to prevent a malicious app from hijacking the URL scheme.

Attack Vector

As the Apple warning above states, these registered URL schemes' offer a potential attack vector into your app. Sending malformed URLs could expose user data, or access other sensitive data in the app. This typically starts with fuzzing the URL scheme with malformed data and watching to see how the app handles the input. During the fuzzing process, we will want to see if the application crashes. If the app continues to run, then it is safe to say that there is code in place to handle the malformed fuzzed input. If the app does crash, this will allow us to perform further specific testing to manipulate the app into revealing data, or even taking control of its execution on the device.

Identifying URL Schemes

URL schemes are included in the application Info.plist file, using the CFBundleURLTypes key.

  "CFBundleURLTypes" => [
0 => {
"CFBundleURLSchemes" => [
0 => "fb152408702211"
1 => "ak152408702211"
2 => "public.hearsaysocial.Hearsay-Messages-for-Blackberry.sc2"
3 => "public.hearsaysocial.Hearsay-Messages-for-Blackberry.sc3"
4 => "com.good.gd.discovery"
]
}
]

This lists the custom URL schemes that have been registered for the app. There is also a section on the URL's that the app can use, such as the tel:// URL. These are also listed in the Info.plist file in the LSApplicationQueriesSchemes array that are listed.

  "LSApplicationQueriesSchemes" => [
0 => "fbapi"
1 => "fbauth2"
2 => "fbshareextension"
3 => "tel"
]

Semi-automated Fuzzing

During testing, we will want to fuzz these custom URL schemes. In the example below, we will use the iGoat application. There are no fully automated ways to fuzz these, as each URL scheme will require custom payloads for their various parameters. Since this is a dynamic test, we can use Frida to partially automate some of this process. There is a (modified) script at the URL below that will assist on fuzzing.

https://gist.github.com/stingerIO/7f9213004aa57bf8f5c7bc3974c49e3d

Copy the script from the link above and save it locally as fuzzer.js (or whatever you want). You will need to modify the "fuzzStrings" variable to include payloads that you want to use. Inside of this array you will put each string on a single line in quotes followed by a comma. You can also see in the script that you can use Array($number).join($payload), to create a long string.

To properly fuzz the app, you will need to connect Frida to a different app so that the app you connect to can call the URL scheme of the app under test. I would recommend just connecting to the SpringBoard application and then use that to spawn the URL's. This way, we can see if the app under test crashes or not. We are looking for an app crash from our fuzzing strings.

Fuzzing Example (iGoat)

Review the app Info.plist file for the custom URL schemes.

## plutil Info.plist
...
"CFBundleURLTypes" => [
0 => {
"CFBundleTypeRole" => "Editor"
"CFBundleURLName" => "com.iGoat.myCompany"
"CFBundleURLSchemes" => [
0 => "iGoat"
]
}
]

In this case, there is one scheme called "iGoat". To find out the parameters for this URL, we can look through the binary strings. Keep in mind that a simple grep may not work since the parameters will likely be on separate lines in the strings file. In our example, we see this for the iGoat:// search:

T@"UITextField",N,W,VmobileNumberTxtField
T@"UITextField",N,W,VmessageTxtField
iGoat://?contactNumber=
&message=
Error:
v8@?0^{?=^vIB}4

So, our URL string will look something like: iGoat://?contactNumber={value}&message={value}. We will want to focus our fuzzing on the two {value} sections.

At this point, we will need to edit the fuzzer.js script to include our payloads in the fuzzStrings variable.

// add/remove default fuzz strings here, or at the command line
var fuzzStrings = ["0",
"1",
"-1",
"null",
"nil",
"99999999999999999999999999999999999",
Array(257).join("A"),
Array(1026).join("A"),
"'",
"%20d",
"%20n",
"%20x",
"%20s"
];

Once this is setup, then you can launch the fuzzer using Frida:

% frida -U -l fuzzer.js SpringBoard
____
/ _ | Frida 12.8.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/

[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
OK!
Opened URL: iGoat://?contactNumber=1&message=1
OK!

At this point, the device will open several times as the SpringBoard app sends each of the fuzzStrings payloads to the iGoat app. If the app handles the string well, then it will display Ok!. If the app does not handle the strings well, it will cause the app to crash -- which is what we are looking for! In this example, the app handles all of the fuzz strings well and no crash was caused.

Application Crash

If we do get a crash, the app crash file will be stored on the device at /private/var/mobile/Library/Logs/CrashReporter

To review the crash file, it is best to launch Xcode with the device attached. Then go to Window Devices & Simulator in the menu, then click on "View Device Logs". This information can be reviewed in Xcode, or you can right click on the entry and export it to the computer.

Please review the section on analyzing a crash file (still being developed)!

Over time, URL schemes were abused by Developer's to send a user to specific pages of their website (commonly called "deep linking"). For instance, instead of having a URL scheme such as appUrl://, a developer could create it as appUrl://profile/12345678 to link to a specific page.

Starting in iOS 9, Apple released Universal Links as an alternative to using URL schemes for deep linking. A Universal Link is a standard HTTP web link. When this link is opened, iOS checks to see if any app on the device is registered for that domain. If there is an app that has it registered, it is launched in place of a web browser. Otherwise, the link is loaded in Safari as a regular web link.

For an app to use a Universal Link, the domain must be registered with Apple in the Xcode IDE, and it must contain the proper "Associated Domains" entitlements. Additionally, an "apple-app-site-association" (AASA) file be placed at the root of the domain in use (or in a hidden directory called .well-known also at the root of the domain). When the application gets installed, iOS will check the domain for the AASA file to ensure that it is setup correctly, and that no other app can hijack your link.

The best way to determine if an app is using Universal Links is to review the entitlements of the app. This can be done with jtool or jtool2 on the device, or using the security command on macOS.

Note: with jtool2 you will need to setup a variable for the architecture of the binary!

iOS:

# ARCH=arm64 jtool2 --ent BinaryName 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>5ZXXX9VNR6.io.stinger.rage</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:stinger.io</string>
</array>
<key>com.apple.developer.team-identifier</key>
<string>5ZXXX9VNR6</string>
<key>get-task-allow</key>
<true/>
</dict>
</plist>

macOS:

% security cms -D -i embedded.mobileprovision > ent.plist
% /usr/libexec/PlistBuddy -c 'Print :Entitlements' ent.plist
Dict {
get-task-allow = true
com.apple.developer.team-identifier = 5ZXXX9VNR6
com.apple.developer.associated-domains = webcredentials:stinger.io
application-identifier = 5ZXXX9VNR6.io.stinger.rage
keychain-access-groups = Array {
5ZXXX9VNR6.*
}
}

If the app is using Universal Links, you can test the AASA file that is located on the domain with this Apple tool: https://search.developer.apple.com/appsearch-validation-tool/

In the case above, it will fail for my domain since I did not properly set it up on my domain.


References: Custom URL Schemes