RCE Vulnerability in QBittorrent


In qBittorrent, the DownloadManager class has ignored every SSL certificate validation error that has ever happened, on every platform, for 14 years and 6 months since April 6 2010 with commit 9824d86. The default behaviour changed to verifying on October 12 2024 with commit 3d9e971. The first patched release is version 5.0.1, released 2 days ago.

The usages of DownloadManager across the program are extensive, and affect searches, .torrent downloads, RSS feeds, favicon downloads and more. All of the following code paths accept any certificate whatsoever, whether expired, self-signed or both, in descending order of severity:

1. Malicious Executable loader with stealth functionality

If you are running Windows and you do not have a recent enough build of Python installed, at launch qBittorrent will prompt you to install/update Python from a hardcoded URL so you can use the search plugin which requires it, source:

#ifdef Q_OS_WIN
            const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Missing Python Runtime")
                , tr("Python is required to use the search engine but it does not seem to be installed.\nDo you want to install it now?")
                , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
            if (buttonPressed == QMessageBox::Yes)
                installPython();
...

#ifdef Q_OS_WIN
void MainWindow::installPython()
{
    setCursor(QCursor(Qt::WaitCursor));
    // Download python
    const auto installerURL = u"https://www.python.org/ftp/python/3.12.4/python-3.12.4-amd64.exe"_s;
    Net::DownloadManager::instance()->download(
            Net::DownloadRequest(installerURL).saveToFile(true)
            , Preferences::instance()->useProxyForGeneralPurposes()
            , this, &MainWindow::pythonDownloadFinished);
}

If you click or hit enter on the auto-selected ‘Yes’ option, qBittorrent will then **download, execute and then delete the .exe**, source;

void MainWindow::pythonDownloadFinished(const Net::DownloadResult &result)
{
    if (result.status != Net::DownloadStatus::Success)
    {
        ...
    }

    setCursor(QCursor(Qt::ArrowCursor));
    QProcess installer;
    qDebug("Launching Python installer in passive mode...");

    const Path exePath = result.filePath + u".exe";
    Utils::Fs::renameFile(result.filePath, exePath);
    installer.start(exePath.toString(), {u"/passive"_s});

    // Wait for setup to complete
    installer.waitForFinished(10 * 60 * 1000);

    qDebug("Installer stdout: %s", installer.readAllStandardOutput().data());
    qDebug("Installer stderr: %s", installer.readAllStandardError().data());
    qDebug("Setup should be complete!");

    // Delete temp file
    Utils::Fs::removeFile(exePath);

    // Reload search engine
    if ...
}
#endif // Q_OS_WIN

qBittorrent has had this behaviour from June 2015 until the present, affecting v3.2.1 through v5.0.0 inclusive. The behaviour does not appear to be replicated for other OS variants, they just disable the search widget if Python is not found or is not recent enough.

When the executable has finished downloading, it will be stored in for example:

C:\Users\_user_\AppData\Local\Temp\\is-G61QK.tmp

Perhaps due to how `QProcess` instances behave, there will be two threads of the exe running and only one of them is killed after execution, the other persists in a sleeping state.

Obligatory screenshot of calc.exe, injected using mitmproxy:

2. Browser Hijacking + Executable Download (Software Upgrade Context)

If you are running an installed version of qBittorrent on Windows or Linux and not an `appImage` file, then it will conduct an update check by default on launch, and this entails downloading an RSS feed from a hardcoded URL in the form of an XML document to parse the information about program releases:

    void ProgramUpdater::checkForUpdates() const
{
    const auto RSS_URL = u"https://www.fosshub.com/feed/5b8793a7f9ee5a5c3e97a3b2.xml"_s;
    // Don't change this User-Agent. In case our updater goes haywire,
    // the filehost can identify it and contact us.
    Net::DownloadManager::instance()->download(
            Net::DownloadRequest(RSS_URL).userAgent(QStringLiteral("qBittorrent/" QBT_VERSION_2 " ProgramUpdater (www.qbittorrent.org)"))
            , Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::rssDownloadFinished);
}

Then it will parse the XML, extract the URL if the version is higher than currently running, and prompt the user to visit this URL without any filtering or verification:

...
    if (type.compare(variant, Qt::CaseInsensitive) == 0)
            {
                qDebug("The last update available is %s", qUtf8Printable(version));
                if (!version.isEmpty())
                {
                    qDebug("Detected version is %s", qUtf8Printable(version));
                    if (isVersionMoreRecent(version))
                    {
                        m_newVersion = version;
                        m_updateURL = updateLink;
                    }
                }
                break;
            }
...

If the user accepts this prompt, they open this URL in their default browser. The expectation of the user is to receive an exe file and the context is one of trust, as the link/download came from software they run:

bool ProgramUpdater::updateProgram() const
{
    return QDesktopServices::openUrl(m_updateURL);
}

So, if a user is directed to any given filesharing site such as mediafire etc, they can download an attacker-controlled exe they expect to function like an upgrade. As the project is OSS, the latest version can easily be recompiled with additional backdoor functionality. The devs provide a key to check the signature of the binary, but it has to be checked and a verification failure must be respected in order to protect a user.

3. RSS Feeds (Arbitrary URL injection)

All RSS feeds parsed by the application go through DownloadManager, so they can be hijacked. The URLs visited by a victim can be observed and catalogued, and by nature they are static, long-lived URLs. The `link` element inside each entry is parsed directly and will be downloaded if double-clicked. This applies even without MITM tampering – you are a double click away from any URL inserted into any RSS feed you follow, either by the authors or by attackers who poison the feed.

            else if (name == u"link")
            {
                const QString link = (xml.attributes().isEmpty()
                                ? xml.readElementText().trimmed()
                                : xml.attributes().value(u"href"_s).toString());

                if (link.startsWith(u"magnet:", Qt::CaseInsensitive))
                {
                    article[Article::KeyTorrentURL] = link; // magnet link instead of a news URL
                }
                else
                {
                    // Atom feeds can have relative links, work around this and
                    // take the stress of figuring article full URI from UI
                    // Assemble full URI
                    article[Article::KeyLink] = (m_baseUrl.isEmpty() ? link : m_baseUrl + link);
                }
            }

What makes this one even more damaging is CVE-2019-13640 which allowed remote command execution via shell metacharacters in the torrent name or current tracker parameters. This combination means the authentic RSS author need not send malicious data, as a MITM attacker can also do it.

4. Decompression library attack surface (0-click)

At launch, by default the program will automatically download a .gz extension binary MaxMind GeopIP database from a hardcoded URL, and then extract it. If there are any vulnerabilities in zlib decompression, such as CVE-2022-37434 the CVSS 9.8 Critical buffer overflow from 2022, which does not apply here since the `inflateGetHeader()` function is not called, an attacker can target this attack surface with an arbitrary file up to 64MB, logging and returning after any errors;

    ...

    bool ok = false;
    const QByteArray data = Utils::Gzip::decompress(result.data, &ok);
    if (!ok)
    {
        LogMsg(tr("Could not decompress IP geolocation database file."), Log::WARNING);
        return;
    }

The code which parses the binary MaxMind database after decompression is well guarded as of 2024 but used to look different, potentially providing more attack surface. There is also an interesting commit where a contributor makes adjustments to the `gzip::decompress()` function which hints at a stack overflow, as the destination buffer was changed from static allocation on the stack to dynamic allocation on the heap, though it was not exploitable due to checks before it is written to:

-105           char tmpBuf[BUFSIZE] = {0};
+107           std::vector<char> tmpBuf(BUFSIZE);

Exploit Possibilities

Because all the Python installer exe URLs and update RSS feed URLs are hardcoded, they can be trivially enumerated and a malicious script in MITM context can attack every vulnerable version. Better, the RSS feed URLs provide a fingerprint for the software as there is no other reason to visit one of those URLs unless qBittorrent is running. This means mass surveillance programs like PRISM and equivalent can easily detect qBittorrent users via passive traffic monitoring. Then, because there is no cert validation, there is no need for expensive and complex deployments like QUANTUM to perform a Man-On-The-Side attack – you can just spoof the destination server.

Another aspect of the URLs being hardcoded is that an adversary can selectively intercept only requests from qBittorrent which do not verify the connection, instead of alerting a victim when his browser/other applications fails to securely connect to the internet.

Because this software is open source, it is trivial to add a backdoor to it and recompile, and therefore give a victim fully functional software, avoiding the victim’s suspicion.

Scripting possibilities, when used in conjunction with mitmproxy using `-s` to supply a python script:

  • Automated replacement of all Python exes with arbitrary exe: RCE with a single click
  • Automated replacement of all qBittorrent update URLs in RSS feed: Browser Hijacking/RCE with moderate user interaction
  • Automated replacement of all/specific links in qBittorrent RSS viewer: RCE until 2019, Download Hijacking

Mitigations

Just use another torrent client. Deluge and Transmission etc do not have this vulnerability.

Notes

People dismiss attacks requiring MITM access as theoretical, dangerously forgetting the lessons of recent history, and present realities in non-free countries like China, UAE and Kazakhstan. I have applied for a CVE, and have been in contact with the maintainers of the repo, though they have not commented on whether or not they intend to release a security advisory on Github.

,

Leave a Reply

Your email address will not be published. Required fields are marked *