HIGH nginx / Apache access logs Data Exfiltration LTR-0015 ✓ validated

Detect SQL Injection and Follow-on Data Exfiltration in nginx Access Logs

Published

MITRE ATT&CK: T1190T1595.002

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:

  1. Injection payloads in the URI — UNION SELECT, boolean-based AND 1=1 / AND 1=2 pairs, ORDER BY 1-- column probing, time-based SLEEP(, and the URL-encoded %27%20OR%20 (' OR ) pattern.
  2. 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:

PatternPhaseWhat the attacker learns
AND 1=1 / AND 1=2Boolean testWhether the parameter is injectable (responses differ)
ORDER BY 1--Column enumerationHow many columns the vulnerable query returns
UNION SELECTExtractionArbitrary rows from arbitrary tables
SLEEP(Blind confirmationInjectability even when output is suppressed
%27%20OR%20Auth/filter bypassEncoded ' 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) and id=1 AND 1=2 (200, 0 bytes) — the differing response confirms injectability
  • ORDER BY 1-- returns 200; ORDER BY 10-- returns 500 — column count found
  • UNION SELECT NULL-- (200), the encoded q=test%27%20OR%20%271%27%3D%271 against /api/v1/search (200, 8,821 bytes), and SLEEP(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

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.