Detect SQL Injection and Follow-on Data Exfiltration in nginx Access Logs
Published
Sigma Rule
title: SQL Injection Probing and Tooling in Web Access Logs
id: 231c0417-c9db-4261-b090-8108ab062878
status: experimental
description: >
Detects SQL injection attempts in web-server access logs via two independent
signals: classic injection payloads in the query string (UNION SELECT,
boolean-based AND 1=1 / AND 1=2 pairs, ORDER BY column probing, time-based
SLEEP, and the URL-encoded quote-OR pattern), and the user-agent strings of
automated injection tools. Either alone justifies an alert; together they
indicate an active sqlmap-style campaign. Watch response sizes on requests
from the same source afterwards — a large 200 following the probes is the
exfiltration.
references:
- https://attack.mitre.org/techniques/T1190/
- https://attack.mitre.org/techniques/T1595/002/
author: LogTriage
date: 2026/09/18
logsource:
category: webserver
detection:
sqli_payload:
cs-uri-query|contains:
- 'UNION SELECT'
- ' AND 1=1'
- ' AND 1=2'
- 'ORDER BY 1--'
- 'SLEEP('
- '%27%20OR%20'
sqli_tool:
c-useragent|contains:
- 'sqlmap'
- 'havij'
- 'jsql'
condition: sqli_payload or sqli_tool
falsepositives:
- Authorized DAST scans (allowlist scanner source IPs)
- Application paths that legitimately accept SQL fragments (exclude the path)
- Announced pentest or bug-bounty engagement traffic
level: high
tags:
- attack.initial_access
- attack.t1190
- attack.reconnaissance
- attack.t1595.002
What this rule detects
SQL injection in a web access log is unusually legible: the attack payload travels in the query string, which nginx and Apache log verbatim. This rule fires on two independent signals:
- Injection payloads in the URI —
UNION SELECT, boolean-basedAND 1=1/AND 1=2pairs,ORDER BY 1--column probing, time-basedSLEEP(, and the URL-encoded%27%20OR%20(' OR) pattern. - Injection tooling in the user-agent —
sqlmap,havij,jsql.
Either alone fires the rule. A single UNION SELECT in a query string has no benign
interpretation on a production API, so there is no count threshold.
Detection logic
The payload list is ordered by what each pattern proves:
| Pattern | Phase | What the attacker learns |
|---|---|---|
AND 1=1 / AND 1=2 | Boolean test | Whether the parameter is injectable (responses differ) |
ORDER BY 1-- | Column enumeration | How many columns the vulnerable query returns |
UNION SELECT | Extraction | Arbitrary rows from arbitrary tables |
SLEEP( | Blind confirmation | Injectability even when output is suppressed |
%27%20OR%20 | Auth/filter bypass | Encoded ' OR — the classic tautology, as tools actually send it |
The two selections are deliberately independent (sqli_payload or sqli_tool): the default
sqlmap user-agent is trivially changed with one flag, and hand-driven injection carries a browser
UA — so neither signal can be a required conjunct without creating an evasion path.
What the rule cannot see is whether the injection worked. That is a response-size question: probes return small bodies, extraction returns large ones. The nginx SQLi use case shows how LogTriage correlates the probe burst with the follow-on bulk reads automatically.
Validated against a real sample
Validated against nginx_sqli_exfiltration.log (shipped with LogTriage): a single source,
203.0.113.50, running sqlmap/1.7.8#stable (https://sqlmap.org) against /api/v1/users?id=1.
The 25-second session contains the full injection lifecycle:
- Boolean probes
id=1 AND 1=1(200, 512 bytes) andid=1 AND 1=2(200, 0 bytes) — the differing response confirms injectability ORDER BY 1--returns 200;ORDER BY 10--returns 500 — column count foundUNION SELECT NULL--(200), the encodedq=test%27%20OR%20%271%27%3D%271against/api/v1/search(200, 8,821 bytes), andSLEEP(5)against/api/v1/products- Then the exfiltration:
/api/v1/users/export?format=csv(200, 204,800 bytes) and/api/v1/accounts?page=1&limit=1000(200, 307,200 bytes), with one blocked attempt at/api/v1/admin/dump(403)
The rule fires on that sample and stays silent on the matching benign browsing log — normal
Chrome/Firefox/Safari sessions against /api/v1/dashboard, /api/v1/tasks, and
/api/v1/settings with zero SQL fragments or tool user-agents.
False positives
The dominant one is your own security testing: authorized DAST scans produce byte-identical payloads, so allowlist the scanner’s source IPs — do not remove patterns from the rule, because every pattern you remove for the scanner you also remove for the attacker.
The rarer case is an application that genuinely accepts SQL fragments in query strings
(reporting UIs that pass ORDER BY clauses through, database teaching tools). Exclude those
specific paths. If unknown external IPs are sending your API UNION SELECT, the rule is
working as intended regardless of what the endpoint does with it.
Frequently Asked Questions
- Why are the payload and the user-agent separate selections instead of one combined signal?
- Because attackers control both independently. sqlmap's default user-agent announces itself, but one command-line flag randomizes it — so a rule that requires the tool UA misses every operator who read the manual. Conversely, a hand-crafted injection through a normal browser carries no tool UA at all. Keeping the selections independent means either evasion still trips the other signal; requiring both would let either evasion defeat the rule.
- What does the ORDER BY 1-- pattern actually tell you?
- It is column-count enumeration — the setup step before a UNION-based extraction. The attacker increments the column index (ORDER BY 1, ORDER BY 5, ORDER BY 10) until the database errors, which reveals exactly how many columns the vulnerable query returns. In the validation sample you can watch it work: ORDER BY 1-- returns 200, ORDER BY 10-- returns 500. That 500 is the database telling the attacker the answer, and a 200/500 flip on adjacent ORDER BY probes is close to proof the parameter is injectable.
- How do I tell whether the injection actually succeeded and data left?
- Response sizes. Probing requests return small, consistent bodies (512 bytes in the sample); extraction returns bodies orders of magnitude larger. In the validation sample the same source that ran the probes then pulls /api/v1/users/export?format=csv at 204,800 bytes and /api/v1/accounts?page=1&limit=1000 at 307,200 bytes — roughly 500 KB leaving within seconds of the probes confirming the vulnerability. LogTriage's path-sensitivity classifier and compound scoring flag exactly this combination of sensitive endpoint plus anomalous response size.
- Won't URL encoding or comment-based obfuscation evade the substring matches?
- Partially, and the rule is honest about that. It includes the most common encoded variant (%27%20OR%20 — the encoded quote-space-OR) because that is what tools emit by default, but a determined attacker using /**/ inline comments or double encoding will slip past static substrings. Treat this rule as the high-confidence tripwire for the 95% of automated traffic, and pair it with a WAF or a normalization layer that decodes URIs before matching for the rest. The tool-UA selection also acts as a backstop: obfuscated payloads are usually generated by tools that still announce themselves.
Related Resources
- Use case: Detecting SQL Injection and Data Exfiltration in nginx Logs →
- Guide: Analyze nginx / Apache access logs Logs →
- Detect SSH Brute Force and User Enumeration in Linux auth.log →
- Detect Security Control Tampering in AWS Config — IAM Policy Deletion, Open Security Groups, CloudTrail Disablement →
- Detect Vulnerability Scanners and Scripted Clients Hitting Admin/Auth Endpoints (AWS ALB / ELB Logs) →
See this detection run on a real report
Try the live demo with a pre-loaded malicious log set — no signup required — or upload your own log file and get a full AI-reviewed threat report in minutes.