Why Your WAF Rules Didn't Stop the MOVEit Transfer Breach
#The Payload That Made Web Application Firewalls Irrelevant#link
On May 31, 2023, security teams at Progress Software realized their managed file transfer appliance was being used as a door into hundreds of organizations. The CVE assigned was CVE-2023-34362. The vulnerability class was SQL injection. And for the first seventy-two hours, the most common reaction from defenders wasn't panic over a zero-day—it was confusion over why their perimeter defenses hadn't flagged a single request. The Cl0p ransomware group didn't use a novel exploitation framework or an undocumented protocol quirk. They used a standard HTTP POST request carrying a SQL injection payload that every commercial web application firewall on the market failed to block out of the box.
TL;DR 1. Standard WAF rulesets failed against CVE-2023-34362 because they look for generic SQL syntax, not Microsoft SQL Server's specific tokenization behavior. 2. The attackers exploited MSSQL's handling of bracket-delimited identifiers and stacked queries to bypass regex-based inspection entirely. 3. Relying on virtual patching via WAF for critical SQLi is a temporary stopgap, not a defense—the only fix is patching the underlying code or isolating the asset. 4. Defenders need detection logic tuned to MSSQL-specific system procedures (like xp_cmdshell), not just generic UNION SELECT signatures.
The Anatomy of a Bypassed Request
To understand why the WAF stayed quiet, you have to look at what the WAF was actually looking for. Most commercial and open-source WAFs rely on the OWASP ModSecurity Core Rule Set (CRS) or proprietary derivatives of it. These rulesets are heavily optimized for MySQL, PostgreSQL, and Oracle syntax because those databases dominate the public web. They trigger on keywords like UNION, SELECT, SLEEP(), and BENCHMARK(). When a request matches these patterns, the WAF blocks it or logs it.
The MOVEit Transfer application runs on a Windows backend and uses Microsoft SQL Server (MSSQL). MSSQL parses SQL differently than MySQL. Specifically, MSSQL allows identifiers (table names, column names) to be wrapped in square brackets, like [sys].[objects]. It also handles batch separators (semicolons) and inline comments (-- or /* */) in ways that allow an attacker to break a WAF's regular expression without breaking the database's query parser. The WAF sees a string of characters that doesn't match its regex for a SQL attack. The MSSQL engine sees a perfectly valid instruction.
Reconstructed Exploitation Request (Sanitized)
A representation of how the injected parameter was structured to evade standard regex matching while remaining executable by MSSQL. Target IP uses RFC 5737 documentation range.
transaction=login&username=admin%27%3B+DECLARE+%40q+NVARCHAR(4000)%3B+SET+%40q%3D(SELECT+CHAR(101)%2BCHAR(120)%2BCHAR(101))%3B+EXEC(%40q)%3B--&password=test
{"status":"success","redirect":"/dashboard"}Look closely at the body parameter above. There is no UNION SELECT. There is no SLEEP(5). Instead, the payload closes the original string with a single quote, terminates the statement with a semicolon, declares a variable (@q), builds a string dynamically using the CHAR() function to avoid typing the actual command in plaintext, and executes it. A WAF looking for the word 'exec' or 'xp_cmdshell' in the raw HTTP body finds nothing. The database, however, concatenates the characters and executes the resulting string flawlessly.
Why Virtual Patching Failed Here
When a critical CVE drops, the immediate playbook for many teams is to apply a virtual patch at the WAF layer while waiting for the vendor to release a binary update. For MOVEit, Progress Software released emergency guidance that included WAF rules intended to block the specific payloads observed in the wild. But this approach fundamentally misunderstands the difference between blocking a known payload and mitigating a vulnerability class.
Virtual patching a SQL injection vulnerability at the WAF layer is a race condition you will eventually lose. You are trying to write a regular expression that covers every possible way a Turing-complete language (T-SQL) can express a malicious intent. The attacker only needs to find one encoding trick, one comment injection, or one dynamic execution method your regex missed.
The Cl0p operators didn't just run one script. They iterated. When initial payloads were blocked by updated WAF rules, they shifted to alternative T-SQL features. They used hex-encoded strings instead of CHAR() concatenation. They abused MSSQL's ability to execute stored procedures via fully qualified names ([master]..[sp_executesql]). Every time a defender added a new string to a blocklist, the attackers leveraged another syntactic feature of MSSQL that the WAF's generic SQLi rules weren't designed to parse.
The Detection Gap: Signatures vs. Behavior
If your WAF couldn't block it, could your SIEM or EDR catch it? Only if you were looking for the right things. Many security operations centers had alerts configured for SQL injection attempts based on web server access logs returning 500 errors, or based on the presence of SQL keywords in URI parameters. The MOVEit exploitation frequently returned 200 OK status codes because the injected queries executed successfully without crashing the application thread. Furthermore, the exploitation happened via POST bodies, which many network-level logging tools truncate or ignore entirely to save storage costs.
Comparing detection strategies reveals a stark trade-off. Signature-based detection (looking for known bad strings) is fast and has low false positives, but it is inherently reactive and brittle against obfuscation. Behavioral detection (monitoring for anomalous database activity) is harder to tune but catches the exploitation regardless of how the payload was encoded. If you are running MSSQL, you should not be relying on web-layer logs to detect SQL injection. You need database-layer telemetry.
| Detection Layer | What It Catches | Where It Fails Against MSSQL Obfuscation | Recommended Action |
|---|---|---|---|
| WAF (Network Edge) | Generic SQLi (UNION, SLEEP, basic tautologies) | Bracket delimiters, stacked queries, dynamic execution (sp_executesql) | Tune for T-SQL specifics, but treat as speedbump, not wall |
| Web Server Logs (IIS/Apache) | URI-based payloads, 500 Internal Server Errors | POST body payloads, successful injections returning 200 OK | Enable full POST body logging for sensitive endpoints temporarily |
| Database Audit Logs | Execution of xp_cmdshell, sp_OACreate, bulk inserts | Requires audit policies to be pre-configured before the breach | Enable C2 audit mode or extended events for sensitive servers |
| EDR (Endpoint) | File writes, child processes spawned by sqlservr.exe | In-memory operations, pure data exfiltration without file drop | Monitor sqlservr.exe for unexpected child processes (cmd, powershell) |
What You Should Actually Be Doing
Hindsight is cheap, but the technical lessons from CVE-2023-34362 are concrete. The failure wasn't that WAFs are useless; it's that they were deployed as the primary control for a vulnerability class they are architecturally ill-equipped to handle completely. A WAF inspects text. A database engine interprets logic. When the text is deliberately crafted to look benign while retaining its logical malice, the text inspector loses.
If you operate applications backed by MSSQL, your defensive posture needs to shift away from generic web-layer filtering toward database-specific hardening. This means disabling features that web applications rarely need but attackers love. The xp_cmdshell stored procedure, which allows SQL queries to execute operating system commands, should be disabled at the instance level unless there is a documented, approved business requirement for it. Even then, it should be restricted via strict role-based access control.
-- Check if xp_cmdshell is enabled on your MSSQL instances-- Run this across your estate to find exposed attack surfaceSELECT name, CONVERT(INT, ISNULL(value, value_in_use)) AS IsEnabledFROM sys.configurationsWHERE name = 'xp_cmdshell';-- If it returns 1, and your web app doesn't strictly require it:EXEC sp_configure 'show advanced options', 1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell', 0;RECONFIGURE;
Disabling xp_cmdshell after a breach has already occurred does not undo lateral movement. Attackers who gained initial access via MOVEit frequently used the SQLi to create new local administrator accounts or deploy webshells directly into the IIS directory. Disabling the procedure is a hardening step, not an incident response action.
Moving Past the Illusion of Perimeter Safety
The MOVEit breach cost organizations billions of dollars in remediation, notification, and legal fees. It didn't happen because defenders were lazy. It happened because the industry accepted a comfortable abstraction: that a sufficiently expensive WAF acts as a universal translator between malicious internet traffic and safe application logic. CVE-2023-34362 proved that when the application speaks a dialect the WAF doesn't fluently parse, that abstraction collapses.
- ▪Audit your MSSQL instances specifically for dynamic SQL execution capabilities and disable unused stored procedures like xp_cmdshell and sp_OACreate immediately.
- ▪Stop treating WAF rule updates as permanent fixes for SQL injection vulnerabilities. Use them to buy hours for patching, not weeks.
- ▪Implement database-level monitoring that alerts on the execution of system-level procedures, regardless of whether the originating web request looked suspicious.
- ▪Review your logging architecture to ensure POST request bodies are captured and retained for critical authentication and data-handling endpoints, not just GET parameters.
- ▪Test your WAF rulesets against MSSQL-specific obfuscation techniques (bracket identifiers, hex encoding, CHAR concatenation) rather than relying solely on default vendor configurations.
The gap between a WAF's regex engine and a database's query parser isn't a bug you can patch; it's an architectural reality. The only reliable way to prevent SQL injection is ensuring the application uses parameterized queries everywhere. Everything else—WAFs, IPS signatures, virtual patches—is just buying time until the developers fix the code.