Security

One SQL injection flaw in a file-transfer product gave attackers a path into its databases

The oldest web flaw was behind a 2023 data-theft campaign. Here it is in six lines, with the change that closes it.

Published 6 min read

Jump to the fix
Lines of web page source code glowing on a dark screen

Photo: Programming code, Martin Vorel, CC BY-SA 4.0, via Wikimedia Commons.

In May 2023, the CL0P ransomware group exploited a previously unknown SQL injection vulnerability in Progress Software's MOVEit Transfer, a managed file-transfer product. A joint CISA and FBI advisory, published on 7 June 2023 and updated on 16 June, says the group began exploiting the flaw, tracked as CVE-2023-34362, on 27 May 2023. CISA added it to its Known Exploited Vulnerabilities Catalog on 2 June.

The advisory says the attackers installed a web shell and used it to reach the product's underlying database. That is the part of the story this article is about. A web application accepted input and let some of it be treated as SQL.

What the advisory says the attackers did

  • They exploited the SQL injection flaw in a MOVEit Transfer web application.
  • They deployed a web shell, which CISA calls LEMURLOOT. It was first seen under the name human2.aspx, chosen to resemble the legitimate human.aspx file.
  • The shell created a random 36-character password at installation, and the operators authenticated with an HTTP header carrying that value.
  • Per the advisory, it could retrieve Microsoft Azure system settings and storage details, enumerate the underlying SQL database, retrieve files, and create an administrator account with the real name "Health Check Service".

The advisory also notes that Cl0p had run earlier campaigns against other file-transfer products. The defensive advice it gives is broad: patch quickly, audit accounts, require multi-factor authentication, keep offline backups and segment networks. It warns that rebuilding the web server without cleaning the database can leave the attackers' accounts in place.

What SQL injection looks like

SQL injection happens when text from a user is pasted into a SQL command as if it were part of the command. This Python and SQLite sketch builds a query by string formatting. A name containing a quote changes the meaning of the whole statement. The output below was produced by running the code.

Python
import sqlite3
c = sqlite3.connect(':memory:')
c.execute('CREATE TABLE files (id INTEGER PRIMARY KEY, name TEXT, owner TEXT)')
c.executemany('INSERT INTO files (name, owner) VALUES (?, ?)', [('a.pdf', 'ann'), ('b.pdf', 'bob')])

name = "x' OR '1'='1"
print(c.execute("SELECT name, owner FROM files WHERE name = '%s'" % name).fetchall())
# [('a.pdf', 'ann'), ('b.pdf', 'bob')]  every row, for a name that does not exist

The attacker's text closed the quote, added an always-true condition and returned the whole table. Real attacks go further, because depending on the database engine and its settings, injected SQL can read other tables, change data or run extra statements.

Pass values as parameters

With a parameter, the database receives the SQL text and the value separately. The value is data, never code, whatever characters it contains.

Python
print(c.execute('SELECT name, owner FROM files WHERE name = ?', (name,)).fetchall())
# []  the odd name matches nothing

The placeholder style depends on the driver: ? in SQLite, %s in psycopg for PostgreSQL, :name or @name elsewhere. Whatever the syntax, the rule is the same. Never build SQL by concatenating or formatting user input into the string.

ApproachSafe against injection?Note
Parameters or prepared statementsYes, for valuesThe standard answer. Use it everywhere.
Escaping quotes by handFragileEasy to get wrong, and different engines have different rules.
ORM query buildersUsuallyThey parameterize values. Raw-SQL escape hatches can still be injectable.
Stored proceduresNot by themselvesA procedure that builds dynamic SQL from its arguments is just as exposed.
Blocklists of bad wordsNoAttackers choose other spellings. Do not rely on filtering.
Python
ALLOWED_SORTS = {'name', 'owner', 'id'}

def list_files(sort):
    if sort not in ALLOWED_SORTS:
        raise ValueError('bad sort column')
    return c.execute(f'SELECT name, owner FROM files ORDER BY {sort}').fetchall()

Second-order injection

A value can be stored safely and used unsafely later. Suppose a user registers with a name that contains SQL, and a nightly report builds a query from stored names by string formatting. The first insert was parameterized and the report is not. Treat data from your own database as untrusted when you build SQL from it.

Limit what a flaw can reach

Parameters prevent the bug. Least privilege shrinks the damage when a bug exists anyway. If the web application's database account can only touch the tables it needs, an injection flaw gives an attacker only those. If it is an owner or administrator, an injection can reach everything.

SQL
-- PostgreSQL: an application role with only what the app needs
CREATE ROLE web_app LOGIN PASSWORD 'use-a-secret-manager';
GRANT CONNECT ON DATABASE app TO web_app;
GRANT USAGE ON SCHEMA public TO web_app;
GRANT SELECT, INSERT, UPDATE ON files TO web_app;
-- No DELETE, no access to the accounts or audit tables, not a superuser.
  • Run the application with a dedicated account, never as an administrator or owner.
  • Keep the database off the public internet, reachable only from the application servers.
  • Store sensitive tables, such as credentials and audit logs, where the web account has no access at all.
  • Separate the account used for migrations from the account used at runtime.

Look for what an attacker leaves behind

The advisory recommends auditing administrator accounts, because the attackers created one. The general habit is worth having: list privileged accounts and ask about any you do not recognise. A generic version for a table of application users:

SQL
SELECT id, username, role, created_at
FROM app_users
WHERE role = 'admin'
ORDER BY created_at DESC;
-- Any admin created recently, or with an odd real name, deserves a question.

Also log database errors and unusually large exports. A sudden burst of syntax errors from one address is a common sign that someone is probing for injection.

A short checklist

  1. 01Search the codeFind every place that builds SQL from strings. Look for formatting, concatenation and f-strings near query calls.
  2. 02Convert to parametersReplace each with placeholders and pass values separately.
  3. 03Cut privilegesCheck the grants of the application account and remove anything it does not use.
  4. 04Patch fastSubscribe to vendor and CISA notices for the products you run, especially on internet-facing systems.
  5. 05Audit accounts after an incidentClean the database as well as the server, and review administrator accounts.

The same idea at the database: prepared statements

Most drivers send parameters separately for you. PostgreSQL also lets you do it explicitly with PREPARE and EXECUTE, which shows the principle clearly. The statement is parsed once, with a placeholder, and values are supplied later as data. This was run on a test server.

SQL
PREPARE find_sub(int) AS
  SELECT id, plan FROM subs WHERE id = $1;

EXECUTE find_sub(1);
--  id | plan
--   1 | pro

Whatever you pass to EXECUTE is a value of type integer, so it cannot change the shape of the query. Passing text where a number is expected is an error, not an injection.

Test your own application, safely

Only test systems you own or have written permission to test, and prefer a staging copy with fake data. A few harmless inputs reveal a lot:

  • A single quote in every text field, search box and URL parameter. A database error page, or a different result, suggests the input reaches SQL unescaped.
  • A name that looks like SQL, such as one containing a semicolon or two dashes, to see whether it is stored and later reused unsafely.
  • A very long value, to see whether the application truncates it or fails with an error that reveals the query.

Keep error messages short for users and detailed in logs. A message that shows the failing SQL tells an attacker exactly how the query was built.

Know what is being exploited right now

CISA's Known Exploited Vulnerabilities Catalog, where this flaw was added on 2 June 2023, lists vulnerabilities attackers are using in practice. If you run internet-facing products, check each against the catalogue and treat anything on it as urgent. A weekly look takes minutes, and it is more useful than reading every advisory.

Questions that come up

Does an ORM prevent SQL injection?
Usually, for ordinary queries, because it parameterizes values. Raw query escape hatches and string-built fragments can still be injectable.
Is escaping quotes enough?
It is fragile and easy to get wrong. Use parameters.
Would parameterized queries have stopped this particular attack?
The advisory says the flaw was a SQL injection, which parameters are designed to prevent. It does not publish the vulnerable code, so no claim about the exact fix can be drawn from it.
Does a web application firewall solve it?
It can block some known patterns. It is an extra layer, not a substitute for fixing the code.

Sources