EXPLORE
← Back to Explore
crowdstrike_cqlHunting

Citrix NetScaler - CVE-2026-88771 Pitboss Command-Injection String in Logs

CVE-2026-88771 is a pre-auth command injection in ns_monuploadd_err.pl. The script greps logs for 'pitboss.*PPE.*(missed too many heartbeats|unexpectedly died)', extracts a core-file name with sed/awk without validation, and interpolates it into a backtick find command as root. Any attacker-controlled logged value (login field, User-Agent, parameters) can carry the payload, so every exploit attempt must leave a line matching that grep. This query applies the exact grep and scores lines that deviate from a legitimate pitboss crash record. Score >= 70 means an exploit string was logged; treat as probable compromise because the payload executes on the next ns_monuploadd_err.pl run (up to ~24h later) regardless of an observed crash. Space-free payloads (${IFS}, base64) are expected because awk splits on whitespace. Score 10 rows are well-formed PPE crash records - context for CVE-2026-88772 and the DoS CVEs, not exploitation. nsaaad lines often lack a client IP; pivot on timestamp to adjacent adc.sslvpn / adc.aaatm lines. Requires local0 and local1 syslog facilities forwarded. False positives: none expected at >= 85; 70 may catch unusual firmware log formats.

MITRE ATT&CK

initial-accessexecutiondefense-evasion

Detection Query

#Vendor=citrix #event.module=adc
  | @rawstring=/pitboss.*PPE.*(missed too many heartbeats|unexpectedly died)/iF
  | case {
      @rawstring=/\$\{?IFS|b64decode|base64/iF
        | ioc.match := "IFS/base64 space-evasion payload" | ioc.score := 100 ;
      @rawstring=/NSPPE[^\s(]*[;`|&$]/iF
        | ioc.match := "shell metacharacter after NSPPE token" | ioc.score := 95 ;
      @rawstring=/(login req|authenticate user|AAAD RESP|user:\s*<|User-?Agent|Browser_type|Username)/iF
        | ioc.match := "crash string inside auth/header field" | ioc.score := 85 ;
      @rawstring!=/pitboss.*NSPPE-\d{2}\s*\(\d+\).*(missed too many heartbeats|unexpectedly died)/F
        | ioc.match := "malformed crash record (fails patched-parser regex)" | ioc.score := 70 ;
      * | ioc.match := "well-formed PPE crash record" | ioc.score := 10 ;
  }
  | regex("(?:Client[_ ]?ip|ClientIP|Source|Remote_?ip)\s*:?\s*(?<ns.client_ip>\d{1,3}(?:\.\d{1,3}){3})", field=@rawstring, flags=i, strict=false)
  | regex("(?<ns.payload>pitboss.{0,200})", field=@rawstring, flags=i, strict=false)
  | ns.host := coalesce([log.syslog.hostname, host.name, observer.hostname, host.hostname])
  | ns.client_ip := coalesce([ns.client_ip, source.ip, client.ip])
  | default(field=[ns.host, ns.client_ip], value="-", replaceEmpty=true)
  | dataset := #event.dataset
  | groupBy([ns.host, dataset, ioc.match], function=[count(as=Events), max(ioc.score, as=Score), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen), collect([ns.client_ip], limit=50), selectLast([ns.payload])], limit=max)
  | formatTime("%F %T %Z", field=FirstSeen, as=FirstSeen)
  | formatTime("%F %T %Z", field=LastSeen, as=LastSeen)
  | table([Score, ns.host, dataset, ioc.match, Events, FirstSeen, LastSeen, ns.client_ip, ns.payload], limit=1000, sortby=Score, order=desc)

Author

Travis Baldwin

Data Sources

Network

Platforms

network

Tags

HuntingMonitoringDetection
Raw Content
# --- Query Metadata ---
# Human-readable name for the query. Will be displayed as the title.
name: "Citrix NetScaler - CVE-2026-88771 Pitboss Command-Injection String in Logs"

# MITRE ATT&CK technique IDs
mitre_ids:
  - "T1190"
  - "T1059.004"
  - "T1027.010"

# Description of what the query does and its purpose.
description: "CVE-2026-88771 is a pre-auth command injection in ns_monuploadd_err.pl. The script greps logs\n  for 'pitboss.*PPE.*(missed too many heartbeats|unexpectedly died)', extracts a core-file name\n  with sed/awk without validation, and interpolates it into a backtick find command as root.\n  Any attacker-controlled logged value (login field, User-Agent, parameters) can carry the\n  payload, so every exploit attempt must leave a line matching that grep. This query applies the\n  exact grep and scores lines that deviate from a legitimate pitboss crash record."

# The author or team that created the query.
author: "Travis Baldwin"

# The required log sources to run this query successfully in Next-Gen SIEM.
log_sources:
  - Network

# Tags for filtering and categorization.
tags:
  - Hunting
  - Monitoring
  - Detection

# --- Query Content ---
# The actual CrowdStrike Query Language (CQL) code.
cql: |
  #Vendor=citrix #event.module=adc
    | @rawstring=/pitboss.*PPE.*(missed too many heartbeats|unexpectedly died)/iF
    | case {
        @rawstring=/\$\{?IFS|b64decode|base64/iF
          | ioc.match := "IFS/base64 space-evasion payload" | ioc.score := 100 ;
        @rawstring=/NSPPE[^\s(]*[;`|&$]/iF
          | ioc.match := "shell metacharacter after NSPPE token" | ioc.score := 95 ;
        @rawstring=/(login req|authenticate user|AAAD RESP|user:\s*<|User-?Agent|Browser_type|Username)/iF
          | ioc.match := "crash string inside auth/header field" | ioc.score := 85 ;
        @rawstring!=/pitboss.*NSPPE-\d{2}\s*\(\d+\).*(missed too many heartbeats|unexpectedly died)/F
          | ioc.match := "malformed crash record (fails patched-parser regex)" | ioc.score := 70 ;
        * | ioc.match := "well-formed PPE crash record" | ioc.score := 10 ;
    }
    | regex("(?:Client[_ ]?ip|ClientIP|Source|Remote_?ip)\s*:?\s*(?<ns.client_ip>\d{1,3}(?:\.\d{1,3}){3})", field=@rawstring, flags=i, strict=false)
    | regex("(?<ns.payload>pitboss.{0,200})", field=@rawstring, flags=i, strict=false)
    | ns.host := coalesce([log.syslog.hostname, host.name, observer.hostname, host.hostname])
    | ns.client_ip := coalesce([ns.client_ip, source.ip, client.ip])
    | default(field=[ns.host, ns.client_ip], value="-", replaceEmpty=true)
    | dataset := #event.dataset
    | groupBy([ns.host, dataset, ioc.match], function=[count(as=Events), max(ioc.score, as=Score), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen), collect([ns.client_ip], limit=50), selectLast([ns.payload])], limit=max)
    | formatTime("%F %T %Z", field=FirstSeen, as=FirstSeen)
    | formatTime("%F %T %Z", field=LastSeen, as=LastSeen)
    | table([Score, ns.host, dataset, ioc.match, Events, FirstSeen, LastSeen, ns.client_ip, ns.payload], limit=1000, sortby=Score, order=desc)

# Explanation of the query. Uses markdown for formatting on the webpage.
explanation: |
  Score >= 70 means an exploit string was logged; treat as probable compromise because the
    payload executes on the next ns_monuploadd_err.pl run (up to ~24h later) regardless of an
    observed crash. Space-free payloads (${IFS}, base64) are expected because awk splits on
    whitespace. Score 10 rows are well-formed PPE crash records - context for CVE-2026-88772 and
    the DoS CVEs, not exploitation. nsaaad lines often lack a client IP; pivot on timestamp to
    adjacent adc.sslvpn / adc.aaatm lines. Requires local0 and local1 syslog facilities forwarded.
    False positives: none expected at >= 85; 70 may catch unusual firmware log formats.