Skip to content

Threat Hunt Plan: ABB dynovaPRO and Keycloak Reset-Credentials Account Takeover — Identity, Cloud Session and DER Control Hunt

Revision 1.1 — September 22, 2026

Hunt Objective and Scope

This hunt determines whether the Keycloak reset-credentials authentication bypass (CVE-2026-18963, CVSS v3.1 9.1) was used against the organization, either through its ABB dynovaPRO energy management accounts before ABB's cloud fix on 2026-08-27 12:00 (CEST), or through any self-hosted Keycloak or Red Hat build of Keycloak server the organization operates. The objective is to identify accounts whose passwords were reset without the owner's involvement, sessions established with those credentials, and any follow-on configuration change or control command issued to EV chargers, battery energy storage, or photovoltaic assets supervised by dynovaPRO.

Environment in scope: internet-facing and internal Keycloak 26.x servers (bare metal, VM, container, Kubernetes) and their reverse proxies; Active Directory where Keycloak federates users over LDAP with password writeback; corporate email that receives dynovaPRO notification mail; endpoints used by facility, fleet and energy managers to reach dynovaPRO; and site OT networks hosting chargers, storage and PV inverters with their site controllers or gateways. Affected upstream releases are 26.0.0–26.4.14, 26.5.0–26.6.5 and 26.7.0–26.7.1; releases before 26.0.0 are outside the published affected range.

Time window: 2026-08-17 (earliest public disclosure date cited; the CVE record lists 2026-08-18) to present for self-hosted Keycloak; 2026-08-17 to 2026-08-27 12:00 CEST for dynovaPRO account resets, extended to present for sessions and control activity attributable to any account reset in that window. Where logging retention allows, extend back 30 days before disclosure to establish a password-reset baseline.

Exploit mechanics that drive the detections (from the public proof of concept and the upstream fix): the attacker opens the reset-credentials flow, posts tryAnotherWay=on to corrupt the flow's session state, submits the victim's username (this sends the victim a genuine reset email), re-enters the flow, and is advanced to the UPDATE_PASSWORD required action without ever presenting the emailed action token. The observable difference from a legitimate reset is therefore a password update with no preceding EXECUTE_ACTION_TOKEN event or /login-actions/action-token request in the same authentication session.

External attack-surface hypothesis H-1 is included because Keycloak is a web-facing identity service that is routinely published to the internet; execute it first, as its output scopes H-2. CVE-2026-18963 was not in the CISA KEV catalog as of the 2026-09-21 catalog release.

Hypotheses and Hunt Procedures

Hypothesis 1: an external actor can reach an internet-facing instance of Keycloak or Red Hat build of Keycloak operated by the organization, observable as an exposed Keycloak login or admin service in passive internet-scan indices. Execute FIRST; its output scopes the internal hypotheses.

MITRE ATT&CK: Reconnaissance | T1595 — Active Scanning | defender-side passive discovery of the Keycloak endpoints an actor would enumerate before sending crafted reset-credentials requests. Initial Access | T1190 — Exploit Public-Facing Application | CVE-2026-18963 is exploitable by any unauthenticated party that can reach the realm login endpoints. ICS | T0883 — Internet Accessible Device | applies where the Keycloak realm fronts an OT or DER management application.

Collection Queries — passive external indices (STRICTLY passive; never probe target infrastructure; API keys by env-var name only: SHODAN_API_KEY, CENSYS_API_SECRET, CENSYS_ORG_ID, NETLAS_API_KEY):

// Shodan — unscoped first, to prove each filter matches the Keycloak population
http.title:"Keycloak"
http.title:"Welcome to Keycloak"
http.html:"/realms/" http.html:"login-actions"

 

// Shodan — scoped; run each ownership anchor as its own query (never AND two anchors together)
http.html:"/realms/" net:<your_public_CIDR>
http.html:"/realms/" org:"<Your Org Name>"
http.title:"Keycloak" asn:AS<your_ASN>

 

// Censys CenQL (current form — host.services.*)
host.services.endpoints.http.html_title: "Keycloak"
host.services.software.product: "keycloak"
host.services.software.product: "keycloak" and (host.autonomous_system.asn = <your_ASN> or host.ip: "<your_public_CIDR>")

 

// Netlas (tie-breaker)
http.title:"Keycloak"
host:<your_public_CIDR> AND http.title:"Keycloak"
→ Cross-reference every confirmed-owned instance against the CISA Known Exploited Vulnerabilities catalog (https://www.cisa.gov/known-exploited-vulnerabilities-catalog) for CVE-2026-18963 and record its advertised version, if any, against the fixed releases (Keycloak 26.4.15, 26.6.6, 26.7.2, or the fixed Red Hat build of Keycloak errata).

 

Analysis Queries:

  • Attribute each hit with at least two independent ownership signals (PTR, ASN, certificate CN/SAN, RDAP, own cloud inventory) before reporting; an unattributable hit is inconclusive, not negative.
  • Version-triage each banner or login-page resource path (Keycloak serves theme resources under a versioned resources path) against the affected ranges, and record each index's scan timestamp; a stale banner is a prompt to verify, not evidence of safety.
  • Corroborate in at least two of Shodan, Censys and Netlas before escalating. Absence from an index is evidence only that the index did not observe the host.
  • Historical pivot: establish whether each exposed instance was reachable between 2026-08-17 and its patch date; continuous exposure across that window means the instance should be presumed enumerated and receives the exhaustive H-2 pass.
  • Record whether the exposed realm fronts an OT, DER or remote-access application; internet reachability of identity infrastructure serving a control environment is a finding under IEC 62443 and NERC CIP independent of confirmed exploitation.

Hypothesis 2: an unauthenticated actor has reset user passwords on a self-hosted Keycloak server through the reset-credentials flow without the email verification step, observable as SEND_RESET_PASSWORD followed by UPDATE_PASSWORD/UPDATE_CREDENTIAL for the same user with no intervening EXECUTE_ACTION_TOKEN event, and as reset-credentials POSTs carrying tryAnotherWay=on, in Keycloak event logs, reverse-proxy access logs and LDAP writeback events in Active Directory.

MITRE ATT&CK: Initial Access | T1190 — Exploit Public-Facing Application | crafted requests to /realms/<realm>/login-actions/reset-credentials bypass email verification. Persistence | T1098 — Account Manipulation | the attacker sets a new password on the victim account. Defense Evasion | T1078 — Valid Accounts | subsequent sign-ins use the attacker-set credential and appear legitimate.

Collection Queries:

CrowdStrike Falcon LogScale (CQL) — inventory every host running a Keycloak server and extract the distribution version where the install path carries it (the official container image installs to an unversioned /opt/keycloak, so an empty kcVersion means "check the image tag", not "unaffected"):

#event_simpleName=ProcessRollup2
| CommandLine=/keycloak|kc\.sh|io\.quarkus\.bootstrap\.runner/i
| regex("keycloak-(?<kcVersion>\d+\.\d+\.\d+)", field=CommandLine, strict=false)
| groupBy([ComputerName, ImageFileName, kcVersion], function=[count(as=cnt), max(@timestamp, as=lastSeen)], limit=max)
| sort(lastSeen, order=desc, limit=max)

 

CrowdStrike Falcon LogScale (CQL) — inbound connections from public addresses to the default Keycloak HTTP/HTTPS listener ports:

#event_simpleName=NetworkReceiveAcceptIP4
| in(LocalPort, values=["8080", "8443"])
| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"])
| groupBy([ComputerName, LocalPort, RemoteAddressIP4], function=[count(as=cnt), min(@timestamp, as=firstSeen)], limit=max)
| sort(cnt, order=desc, limit=max)

 

Keycloak server-side logging prerequisites (enable before the hunt if not already on): Realm settings → Events → User events settings → Save events ON with SEND_RESET_PASSWORD, RESET_PASSWORD, EXECUTE_ACTION_TOKEN, UPDATE_PASSWORD, UPDATE_CREDENTIAL, LOGIN and LOGIN_ERROR selected; Admin events ON with representation. The jboss-logging listener writes SUCCESS events at DEBUG by default, so raise it to INFO or they never reach stdout; for the Datadog Keycloak integration, also emit JSON over syslog. Keycloak 26 logs both the deprecated UPDATE_PASSWORD and the new UPDATE_CREDENTIAL (credential_type=password) for the same change; hunt on both. Saved events in the database are retained only per the realm's expiration setting — export them before they age out.

bin/kc.sh start --http-access-log-enabled=true --spi-events-listener-jboss-logging-success-level=info --spi-events-listener-jboss-logging-error-level=warn

 

bin/kc.sh start --log=console,syslog --log-syslog-output=json --log-syslog-endpoint=<datadog_agent_host>:514

 

BPF / tcpdump — rolling capture on the reverse-proxy-to-Keycloak backend segment where TLS is terminated at the proxy (plaintext HTTP on the backend):

tcpdump -i eth0 -s 0 -w /var/captures/kc_backend_%Y%m%d_%H%M%S.pcap -G 3600 -C 500 'tcp port 8080 and host 10.20.30.40'

 

tcpdump -i eth0 -s 0 -A 'tcp port 8080 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)' | grep -i "tryAnotherWay"

 

Datadog Log Search (Keycloak integration; source:keycloak is the integration's fixed source value — attribute names below depend on your pipeline, confirm them in the Log Explorer facet panel):

// time range: 2026-08-17T00:00Z to current
source:keycloak ("SEND_RESET_PASSWORD" OR "EXECUTE_ACTION_TOKEN" OR "UPDATE_PASSWORD" OR "UPDATE_CREDENTIAL")

 

// time range: 2026-08-17T00:00Z to current — Kubernetes fallback where no keycloak source pipeline exists
source:kubernetes @kubernetes.container_name:keycloak message:"reset-credentials"

 

// time range: 2026-08-17T00:00Z to current — Windows-hosted Keycloak fallback
source:windows service:keycloak "SEND_RESET_PASSWORD"

 

Datadog Live Process Monitoring (Infrastructure > Processes — NOT a log source):

command:java user:keycloak
// free text fallback: kc.sh

 

Data source gap note: if Keycloak event logging was not enabled at INFO before the hunt, reset events for the exposure window may exist only in the realm's saved-events store (Realm settings → Events → User events) or not at all; fall back to reverse-proxy access logs (source:nginx or source:haproxy) and Active Directory LDAP writeback events below, and record the gap in the report.

Windows Event IDs to collect (domain controllers, where Keycloak LDAP user federation has password writeback):

  • 4724 — An attempt was made to reset an account's password (Subject = Keycloak LDAP bind account)
  • 4723 — An attempt was made to change an account's password
  • 4738 — A user account was changed (pwdLastSet updated)
  • 4624 — Successful logon for a reset account shortly after the reset (Logon Type 3 or 10)
$start = [datetime]"2026-08-17"
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4724,4723,4738; StartTime=$start} |
Select-Object TimeCreated, Id, @{n='Subject';e={$_.Properties[4].Value}}, @{n='Target';e={$_.Properties[0].Value}} |
Export-Csv -NoTypeInformation .\kc_ldap_pwd_resets.csv

 

YARA file-system scan — staged exploit scripts on Keycloak hosts, jump hosts and analyst workstations:

yara -r rules/keycloak_resetcreds.yar /home /tmp /var/tmp /opt >> keycloak_resetcreds_hits.txt

 

Analysis Queries:

CrowdStrike Falcon LogScale (CQL) — where Keycloak event logs are ingested into Falcon Next-Gen SIEM, flag users whose password changed after a reset request with no EXECUTE_ACTION_TOKEN event in between (the emailed link was never used):

/type="?(SEND_RESET_PASSWORD|EXECUTE_ACTION_TOKEN|UPDATE_PASSWORD|UPDATE_CREDENTIAL)"?/i
| regex("type=\"?(?<kcType>[A-Z_]+)\"?", strict=true)
| regex("userId=\"?(?<kcUser>[0-9a-f-]{36})", strict=false)
| regex("ipAddress=\"?(?<kcIp>[0-9a-fA-F.:]+)", strict=false)
| case { kcType="SEND_RESET_PASSWORD" | sendTs := @timestamp; kcType="EXECUTE_ACTION_TOKEN" | tokenTs := @timestamp; in(kcType, values=["UPDATE_PASSWORD", "UPDATE_CREDENTIAL"]) | chgTs := @timestamp; * }
| groupBy([kcUser], function=[min(sendTs, as=firstSend), max(tokenTs, as=lastToken), max(chgTs, as=lastChange), collect([kcIp])], limit=max)
| test(lastChange > firstSend)
| case { lastToken!=* | tokenUsed := "no"; test(lastToken < firstSend) | tokenUsed := "no"; * | tokenUsed := "yes" }
| tokenUsed="no"
| deltaSec := (lastChange - firstSend) / 1000
| sort(deltaSec, order=asc, limit=max)

 

CrowdStrike Falcon LogScale (CQL) — rarest source IPs completing password changes (one IP resetting many users is the campaign signature):

/type="?(UPDATE_PASSWORD|UPDATE_CREDENTIAL)"?/i
| regex("userId=\"?(?<kcUser>[0-9a-f-]{36})", strict=false)
| regex("ipAddress=\"?(?<kcIp>[0-9a-fA-F.:]+)", strict=false)
| groupBy([kcIp], function=[count(kcUser, distinct=true, as=usersReset), count(as=events)], limit=max)
| sort(usersReset, order=desc, limit=max)

 

Wireshark display filters (backend PCAP):

http.request.method == "POST" && http.request.uri contains "/login-actions/reset-credentials" && http.file_data contains "tryAnotherWay=on"
http.request.uri contains "/login-actions/required-action" && http.request.uri contains "execution=UPDATE_PASSWORD"
http.request.uri contains "/login-actions/action-token"
http.user_agent contains "cve-2026-18963"
tshark -r kc_backend.pcap -Y 'http.request.uri contains "login-actions"' -T fields -e frame.time -e ip.src -e http.x_forwarded_for -e http.user_agent -e http.request.method -e http.request.uri

 

Datadog Log Analytics:

// time range: 2026-08-17T00:00Z to current
source:keycloak ("UPDATE_PASSWORD" OR "UPDATE_CREDENTIAL")
// Use Table view; group by @userId, @ipAddress; time range 2026-08-17 to current
// Use Timeseries view; group by @ipAddress; 1-minute rollup to surface reset bursts

 

// time range: 2026-08-17T00:00Z to current — proxy-side: password set via required-action with no action-token GET from the same client
source:nginx @http.url_details.path:*required-action* @http.method:POST
// Use Top List view; group by @network.client.ip; compare against source:nginx @http.url_details.path:*action-token*

 

Datadog Audit Trail (Datadog itself federated to Keycloak via SAML — a hijacked Keycloak account signs straight into Datadog):

// time range: 2026-08-17T00:00Z to current
source:datadog @evt.name:Authentication
// Use Table view; group by @usr.email, @network.client.ip; flag first-seen IPs for reset users

 

Datadog CloudTrail (AWS IAM federated to Keycloak via SAML or OIDC):

// time range: 2026-08-17T00:00Z to current
source:cloudtrail @evt.name:(AssumeRoleWithSAML OR AssumeRoleWithWebIdentity) -@network.client.ip:10.* -@network.client.ip:172.16.* -@network.client.ip:192.168.*
// Use Table view; group by @network.client.ip, @userIdentity.arn

 

Datadog Monitor:

Type: Log Alert
Query: source:keycloak "UPDATE_CREDENTIAL"
Evaluation window: last 5 minutes
Alert condition: count > 3 by @ipAddress
Message: "ALERT: Keycloak password updates burst from one IP — possible CVE-2026-18963 exploitation @slack-soc-alerts"
Prerequisites: Keycloak user events enabled; jboss-logging success level raised to INFO; Keycloak logs sent as JSON over syslog to the Datadog Agent (source:keycloak) with the client IP parsed
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors

 

Windows Event Log PowerShell analysis — LDAP writeback resets performed by the Keycloak bind account, followed by a logon within one hour:

$kcBind = "svc-keycloak-ldap"
$resets = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4724; StartTime=[datetime]"2026-08-17"} |
Where-Object { $_.Properties[4].Value -like "*$kcBind*" }
foreach ($r in $resets) {
$target = $r.Properties[0].Value
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=$r.TimeCreated; EndTime=$r.TimeCreated.AddHours(1)} |
Where-Object { $_.Properties[5].Value -eq $target } |
Select-Object @{n='ResetTime';e={$r.TimeCreated}}, TimeCreated, @{n='User';e={$target}}, @{n='SrcIP';e={$_.Properties[18].Value}}
} | Export-Csv -NoTypeInformation .\kc_reset_then_logon.csv

 

YARA memory scan — scripting interpreters holding a crafted reset-credentials request (Falcon RTR can run the same command remotely via runscript on an isolated host):

Get-Process python*,node*,ruby*,perl* -ErrorAction SilentlyContinue | ForEach-Object { yara rules/keycloak_resetcreds_mem.yar $_.Id } >> keycloak_mem_hits.txt
pgrep -f 'python|node|ruby|perl' | xargs -I{} yara rules/keycloak_resetcreds_mem.yar {} >> keycloak_mem_hits.txt

 

Hypothesis 3: an unauthenticated actor has taken over ABB dynovaPRO user accounts belonging to the organization before the 2026-08-27 cloud fix, observable as unsolicited dynovaPRO password reset emails in mail logs followed by user-reported lockouts, ABB account notifications, or new sign-in sources in dynovaPRO user activity.

MITRE ATT&CK: Initial Access | T1078.004 — Valid Accounts: Cloud Accounts | the attacker signs in to the dynovaPRO SaaS tenant with a credential it set itself. Persistence | T1098 — Account Manipulation | the victim's password is replaced without their knowledge.

Collection Queries:

Exchange Online message trace — identify the dynovaPRO notification sender from one known-good reset mail first. Get-MessageTraceV2 returns at most 10 days per query and 5000 rows, and reaches back 90 days, so the window is split in two and should be run before 2026-11-15:

Connect-ExchangeOnline
$sender = "<dynovaPRO_notification_sender>"
$w1 = Get-MessageTraceV2 -SenderAddress $sender -StartDate "2026-08-17" -EndDate "2026-08-24" -ResultSize 5000
$w2 = Get-MessageTraceV2 -SenderAddress $sender -StartDate "2026-08-24" -EndDate "2026-08-28" -ResultSize 5000
$w1 + $w2 | Select-Object Received, RecipientAddress, Subject, Status, MessageId |
Export-Csv -NoTypeInformation .\dynovapro_reset_mails.csv

 

Datadog Log Search (email gateway or Microsoft 365 logs forwarded to Datadog; adjust source to your integration's tag):

// time range: 2026-08-17T00:00Z to 2026-08-28T00:00Z
source:microsoft-365 "dynovaPRO" "password"

 

CrowdStrike Falcon LogScale (CQL) — endpoints that resolve dynovaPRO hosts, to build the population of legitimate users (ABB does not publish the dynovaPRO FQDN; confirm it from a user's browser and tighten the pattern):

#event_simpleName=DnsRequest
| DomainName=/dynovapro/i
| groupBy([ComputerName, DomainName], function=[count(as=cnt), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen)], limit=max)
| sort(firstSeen, order=desc, limit=max)

 

BPF / tcpdump — site-gateway uplink capture to preserve cloud-to-site sessions for H-4 (metadata only; payload is TLS):

tcpdump -i eth1 -s 128 -w /var/captures/der_uplink_%Y%m%d_%H.pcap -G 3600 'tcp port 443 and host 192.168.50.10'

 

Windows Event IDs to collect: none are generated for a SaaS password reset; collect 4624/4625 on the manager workstations only if a reset user also reused the same password for a domain account (credential reuse check).

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624,4625; StartTime=[datetime]"2026-08-17"} | Export-Csv -NoTypeInformation .\mgr_logons.csv

 

Analysis Queries:

  • Join the reset-mail list against the user roster: every recipient who did not request a reset (confirm with the user) is a presumptive takeover and must reset per ABB's direction; ABB has already disabled accounts it identified, so any account still active with an unrequested reset needs a direct query to ABB service.
  • Request from ABB (via the local ABB service organization) the sign-in and change history for each affected user between 2026-08-17 and the account's forced reset.

CrowdStrike Falcon LogScale (CQL) — hosts with a first-ever dynovaPRO resolution in the window (a new machine using a manager's account); 1786924800000 is 2026-08-17T00:00Z:

#event_simpleName=DnsRequest
| DomainName=/dynovapro/i
| groupBy([ComputerName], function=min(@timestamp, as=firstSeen), limit=max)
| test(firstSeen > 1786924800000)

 

Wireshark display filters (site uplink PCAP — TLS metadata):

tls.handshake.extensions_server_name contains "dynovapro"
tshark -r der_uplink.pcap -Y 'tls.handshake.type == 1' -T fields -e frame.time -e ip.dst -e tls.handshake.extensions_server_name

 

Datadog Log Analytics:

// time range: 2026-08-17T00:00Z to 2026-08-28T00:00Z
source:microsoft-365 "dynovaPRO" "password"
// Use Table view; group by @recipient; time range 2026-08-17 to 2026-08-28

 

Datadog Audit Trail: not applicable to the dynovaPRO SaaS itself; use the Hypothesis 2 Audit Trail query where Keycloak also backs Datadog SSO.

Datadog Monitor:

Type: Log Alert
Query: source:microsoft-365 "dynovaPRO" "password"
Evaluation window: last 15 minutes
Alert condition: count > 0
Message: "ALERT: dynovaPRO password reset email delivered — confirm the user requested it @slack-soc-alerts"
Prerequisites: Microsoft 365 or email gateway message logs forwarded to Datadog with subject and recipient parsed
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors

 

Windows Event Log PowerShell analysis — failed-then-successful domain logons for reset users (password reuse by the attacker):

$users = Import-Csv .\dynovapro_reset_mails.csv | ForEach-Object { ($_.RecipientAddress -split '@')[0] }
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625,4624; StartTime=[datetime]"2026-08-17"} |
Where-Object { $users -contains $_.Properties[5].Value } |
Select-Object TimeCreated, Id, @{n='User';e={$_.Properties[5].Value}}, @{n='SrcIP';e={$_.Properties[18].Value}} |
Export-Csv -NoTypeInformation .\reset_users_domain_logons.csv

 

Hypothesis 4: an actor holding a hijacked dynovaPRO account has issued unauthorized configuration changes or control commands to EV chargers, battery energy storage or PV inverters, observable as setpoint, schedule or mode changes originating from the cloud-connected site controller in OT monitoring platforms, site controller logs and SNMP data.

MITRE ATT&CK: ICS Initial Access | T0859 — Valid Accounts | control arrives through an authenticated cloud session. ICS Impair Process Control | T0836 — Modify Parameter | charging limits, dispatch and inverter setpoints altered. ICS Impair Process Control | T0855 — Unauthorized Command Message | start/stop or mode commands relayed from cloud to device.

Collection Queries:

OT Data Collection: Claroty xDome — `Network → Communication → Communication Analysis` → Side A: the DER site controller/gateway asset · Communication: `Protocol = Modbus`, `Port = 502` · Side B: `Purdue Level = 1` · Time Frame `Past Month`; then `Alerts & Threats → OT Activity` filtered to write and mode-change events for the same assets, exported CSV. API: `POST /api/v1/ot_activity_events/`.

OT Data Collection: Claroty CTD — on-premises: Assets filtered to the DER site controller and inverter/BMS assets → Events export for the window; retrieve PCAP from the CTD appliance while it is still within local retention.

OT Data Collection: Dragos Platform — Query the Industrial Protocols QFD for Modbus writes from the site controller to battery and inverter assets between 2026-08-17 and present; in the `Communications Hub`, filter source = site controller, protocol = Modbus, and export PCAP for the window from SiteStore.

OT Data Collection: Nozomi Guardian — run the N2QL queries below via `GET /api/open/query/do?query=<N2QL>` and review the `Variable view` for setpoint excursions.

links | where protocol == "modbus" | where last_activity_time > days_ago(40)
variables | where name ~= "setpoint" and last_change > days_ago(40)

 

OT Data Collection: Armis Centrix — ASQ for cloud sessions from site assets to dynovaPRO and for Modbus sessions into storage and inverter devices:

in:ipConnections
serverPort:443
endpointB:(networkLocation:External)
decisionData:(host:(matches(".*dynovapro.*")))

 

in:ipConnections
serverPort:502
endpointB:(role:Server)

 

OT Data Collection: Tenable OT — `Events`: filter `Event Category` = Configuration Events with event types `Change in state` across the window for the site controller and inverter assets; API `POST /graphql` `eventAggregations(...)`.

OT Data Collection: Forescout eyeInspect — `Command Center → Alerts` for the window with `l7_proto` = Modbus and `src_ip` = the site controller; retrieve the PCAP for each `alert_id`.

curl -sS -u "$EYEINSPECT_USER:$EYEINSPECT_PASS" "https://<cc_host>/api/v1/alerts?start_timestamp=<epoch_ms>&end_timestamp=<epoch_ms>&l7_proto=modbus&limit=500"

 

SNMP polling — site switch port facing the DER site controller and the controller itself:

snmpwalk -v2c -c <community> <switch_ip> IF-MIB::ifTable
snmpget -v2c -c <community> <switch_ip> IF-MIB::ifHCInOctets.<ifIndex> IF-MIB::ifHCOutOctets.<ifIndex> IF-MIB::ifInErrors.<ifIndex> IF-MIB::ifOutErrors.<ifIndex>
snmpwalk -v2c -c <community> <device_ip> system
snmpwalk -v3 -l authPriv -u <user> -a SHA-256 -A <authpass> -x AES-256 -X <privpass> <device_ip> SNMPv2-MIB::sysUpTime.0
Repeat the ifHC counter poll every 60 seconds and diff; any v1/v2c community access to DER assets is itself a finding. From the trap receiver, pull coldStart (1.3.6.1.6.3.1.1.5.1), warmStart (1.3.6.1.6.3.1.1.5.2), linkDown/linkUp (1.3.6.1.6.3.1.1.5.3 / .4) and authenticationFailure (1.3.6.1.6.3.1.1.5.5) traps from DER device ranges for the window.

 

BPF / tcpdump — Modbus TCP writes on the site OT segment:

tcpdump -i eth2 -s 0 -w /var/captures/der_modbus_%Y%m%d_%H.pcap -G 3600 'tcp port 502'

 

Historian / SCADA correlation: export battery state-of-charge, charge/discharge power, charger session and inverter active/reactive power setpoints for the window; mark every step change with no matching operator action, schedule or ABB service ticket.

CrowdStrike Falcon LogScale (CQL) — Windows or Linux site controllers and HMIs with a Falcon sensor: outbound sessions to dynovaPRO hosts, by process:

#event_simpleName=ProcessRollup2
| join({#event_simpleName=DnsRequest | DomainName=/dynovapro/i}, field=[aid, TargetProcessId], key=[aid, ContextProcessId], mode=inner, include=[DomainName])
| groupBy([ComputerName, ImageFileName, DomainName], function=count(as=cnt), limit=max)
| sort(cnt, order=desc, limit=max)

 

Datadog Log Search (site controller syslog forwarded to Datadog):

// time range: 2026-08-17T00:00Z to current
source:syslog "setpoint" OR "schedule" OR "remote command"

 

Windows Event IDs: on Windows-based site HMIs, collect 4624 (Logon Type 10) and 4688 process creation to rule out a parallel local path.

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624,4688; StartTime=[datetime]"2026-08-17"} | Export-Csv -NoTypeInformation .\der_hmi_events.csv

 

Analysis Queries:

Wireshark display filters (Modbus writes and function codes):

modbus.func_code == 5 || modbus.func_code == 6 || modbus.func_code == 15 || modbus.func_code == 16
tshark -r der_modbus.pcap -Y 'modbus.func_code in {5 6 15 16}' -T fields -e frame.time -e ip.src -e ip.dst -e modbus.func_code -e modbus.reference_num -e modbus.regval_uint16
tshark -r der_modbus.pcap -q -z conv,tcp -Y 'modbus'

 

Baseline deviation: compare the register write set and write rate per source from the site controller against the 30 days before 2026-08-17; a new register, a new writer, or writes outside operating schedules are escalation candidates.

CrowdStrike Falcon LogScale (CQL) — rare processes on site controllers reaching dynovaPRO (anything other than the vendor agent):

#event_simpleName=ProcessRollup2
| join({#event_simpleName=DnsRequest | DomainName=/dynovapro/i}, field=[aid, TargetProcessId], key=[aid, ContextProcessId], mode=inner)
| groupBy([ImageFileName], function=count(aid, distinct=true, as=hosts), limit=max)
| sort(hosts, order=asc, limit=max)

 

Datadog Log Analytics:

// time range: 2026-08-17T00:00Z to current
source:syslog "setpoint"
// Use Timeseries view; group by host; 1-hour rollup

 

Datadog Audit Trail: not applicable; control activity is in the OT and site-controller sources above.

Datadog Monitor:

Type: Log Alert
Query: source:syslog "remote command"
Evaluation window: last 5 minutes
Alert condition: count > 0
Message: "ALERT: remote command on DER site controller — verify against dynovaPRO operator activity @slack-ot-soc"
Prerequisites: DER site controller syslog forwarded to Datadog; log-level set to record remote/cloud commands
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors

 

Windows Event Log PowerShell analysis — new services or scheduled tasks on Windows site HMIs in the window:

Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045; StartTime=[datetime]"2026-08-17"} | Select-Object TimeCreated, Message | Export-Csv -NoTypeInformation .\der_hmi_services.csv
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-TaskScheduler/Operational'; Id=106; StartTime=[datetime]"2026-08-17"} | Select-Object TimeCreated, Message | Export-Csv -NoTypeInformation .\der_hmi_tasks.csv

 

Threat Actor Profile

Opportunistic actors: the most likely population. A public Python proof of concept and a single-prerequisite attack (a known username or email address) put the flaw within reach of low-sophistication actors who spray reset requests at internet-facing Keycloak realms. ABB's observation of 26 password change events across 9 customers is consistent with this testing or reconnaissance pattern. TTPs: scanning for /realms/ endpoints, scripted reset-credentials requests, credential set and immediate sign-in.

Ransomware and initial access brokers: moderate sophistication; value hijacked identity-provider accounts, including realm administrators, as a single sign-on foothold into every federated application, including VPNs and cloud consoles. TTPs: T1190, T1078, follow-on MFA registration and session persistence.

Hacktivists: low to moderate sophistication; energy and EV charging assets are attractive for visible disruption. Access path is the dynovaPRO account itself; TTPs are parameter changes and start/stop commands (T0836, T0855).

Nation-state actors: high sophistication; interest in distributed energy resources and grid-edge control is documented, and a cloud management plane that aggregates many sites is a high-value target. No attribution to this CVE has been published.

Insiders: a former employee who knows colleagues' usernames or email addresses needs no other information to exploit the flaw.

Data Sources Required

Identity: Keycloak user and admin event logs (jboss-logging at INFO, or the realm saved-events store), Keycloak HTTP access logs, reverse-proxy or load-balancer access logs, Active Directory Security logs where Keycloak performs LDAP password writeback.

Email: Exchange Online message trace (90-day reach, 10-day windows) or secure email gateway logs covering dynovaPRO notification mail.

Endpoint: CrowdStrike Falcon (ProcessRollup2, DnsRequest, NetworkReceiveAcceptIP4), Windows Security, System and Task Scheduler logs, Sysmon where deployed.

Network: PCAP from the proxy-to-Keycloak backend and the DER site uplink and OT segment; firewall and NetFlow at the site edge.

OT/ICS: Claroty xDome or CTD, Dragos, Nozomi, Armis, Tenable OT and Forescout eyeInspect exports; historian trends for charger, storage and PV setpoints; site controller logs; SNMP counters and traps.

Vendor: dynovaPRO user sign-in and change history requested from ABB service for affected accounts.

External attack surface: Shodan, Censys and Netlas queried read-only for exposed Keycloak services; the CISA KEV catalog; the organization's authoritative public IP and cloud asset inventories, without which exposure attribution cannot be made.

Detection Signatures

SIGMA rules:

title: Keycloak Server Running Affected Distribution
id: 3b1f6c2a-8e4d-4a7b-9c1e-5d2f0a6b7c81
status: experimental
description: Detects a Keycloak Quarkus distribution started from a versioned path inside the CVE-2026-18963 affected ranges (26.0.0-26.4.14, 26.5.0-26.6.5, 26.7.0-26.7.1).
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-18963
author: 1898 & Co.
date: 2026-09-22
tags:
- attack.initial-access
- attack.t1190
- cve.2026-18963
logsource:
product: linux
category: process_creation
detection:
selection:
CommandLine|re: 'keycloak-26\.([0-3]\.\d+|4\.([0-9]|1[0-4])|5\.\d+|6\.[0-5]|7\.[01])(\D|$)'
condition: selection
falsepositives:
- Red Hat build of Keycloak installs whose path version differs from the fixed errata version; container images at /opt/keycloak carry no version and will not match
level: medium

 

title: Keycloak Reset-Credentials Try-Another-Way Abuse
id: 7c2d9e4f-1a3b-4c5d-8e6f-0a1b2c3d4e5f
status: experimental
description: Detects repeated POST requests to the Keycloak reset-credentials endpoint from one client, the request pattern of the public CVE-2026-18963 exploit, which posts tryAnotherWay=on and re-enters the flow several times.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-18963
author: 1898 & Co.
date: 2026-09-22
tags:
- attack.initial-access
- attack.t1190
logsource:
category: webserver
detection:
selection:
cs-method: 'POST'
cs-uri-stem|contains: '/login-actions/reset-credentials'
condition: selection | count() by c-ip > 3
timeframe: 5m
falsepositives:
- Help-desk staff resetting several users from one NAT address
level: high

 

title: Keycloak LDAP Bind Account Resetting AD Passwords
id: 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
status: experimental
description: Detects Active Directory password resets performed by the Keycloak LDAP federation bind account, the AD-side trace of a Keycloak reset-credentials flow with writeback.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-18963
author: 1898 & Co.
date: 2026-09-22
tags:
- attack.persistence
- attack.t1098
logsource:
product: windows
service: security
detection:
selection:
EventID: 4724
SubjectUserName|contains: 'keycloak'
condition: selection
falsepositives:
- Legitimate user self-service resets through Keycloak; correlate with a preceding EXECUTE_ACTION_TOKEN event
level: medium

 

title: Workstation Resolving dynovaPRO Hosts
id: 4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8
status: experimental
description: Inventories hosts that reach the ABB dynovaPRO cloud so new or unexpected users of manager accounts can be identified.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-18963
author: 1898 & Co.
date: 2026-09-22
tags:
- attack.initial-access
- attack.t1078.004
logsource:
category: dns_query
product: windows
detection:
selection:
QueryName|contains: 'dynovapro'
condition: selection
falsepositives:
- All legitimate dynovaPRO users; use for baselining and first-seen analysis
level: informational

 

Snort/Suricata rules (Suricata 8 syntax; deploy on the plaintext proxy-to-Keycloak backend or on a TLS-inspecting sensor):

alert http any any -> $HOME_NET any (msg:"LOCAL Keycloak reset-credentials tryAnotherWay POST possible CVE-2026-18963"; flow:established,to_server; http.method; content:"POST"; http.uri; content:"/login-actions/reset-credentials"; http.request_body; content:"tryAnotherWay=on"; fast_pattern; reference:cve,2026-18963; classtype:attempted-user; sid:1000001; rev:2; metadata:created_at 2026_09_22;)

 

alert http any any -> $HOME_NET any (msg:"LOCAL Keycloak reset-credentials POST burst possible CVE-2026-18963"; flow:established,to_server; http.method; content:"POST"; http.uri; content:"/login-actions/reset-credentials"; fast_pattern; detection_filter:track by_src, count 5, seconds 60; reference:cve,2026-18963; classtype:attempted-user; sid:1000002; rev:2; metadata:created_at 2026_09_22;)

 

alert http any any -> $HOME_NET any (msg:"LOCAL CVE-2026-18963 public PoC default User-Agent"; flow:established,to_server; http.user_agent; content:"cve-2026-18963-poc"; nocase; reference:cve,2026-18963; classtype:attempted-user; sid:1000003; rev:1; metadata:created_at 2026_09_22;)

 

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"LOCAL Keycloak admin console access from external"; flow:established,to_server; http.uri; content:"/admin/master/console"; fast_pattern; threshold:type limit, track by_src, count 1, seconds 3600; classtype:policy-violation; sid:1000004; rev:1; metadata:created_at 2026_09_22;)

 

YARA rules:

Keycloak_ResetCreds_Exploit_Script targets staged exploit tooling on disk, keyed to the public proof of concept and any script built on the same chain. It requires the reset-credentials path together with the tryAnotherWay flow-corruption parameter or two of the Keycloak form identifiers the chain scrapes, and a scripting HTTP-client marker; Keycloak's own theme templates contain the form ids but not a client library, and the 200 KB cap excludes Keycloak JARs and log archives. The PoC's default User-Agent string is a standalone high-confidence branch.

rule Keycloak_ResetCreds_Exploit_Script
{
meta:
description = "Script automating the Keycloak reset-credentials bypass (CVE-2026-18963 tooling)"
author = "1898 & Co."
date = "2026-09-22"
reference = "https://nvd.nist.gov/vuln/detail/CVE-2026-18963"
strings:
$ua = "cve-2026-18963-poc" ascii wide nocase // public PoC default User-Agent
$path = "login-actions/reset-credentials" ascii wide nocase // vulnerable endpoint
$bug1 = "tryAnotherWay" ascii wide // flow-state corruption parameter
$f1 = "kc-reset-password-form" ascii // choose-user form id
$f2 = "kc-select-credential-form" ascii // try-another-way selector form id
$f3 = "kc-passwd-update-form" ascii // update-password form id
$f4 = "KC_AUTH_SESSION_HASH" ascii // auth-session cookie handled by hand
$c1 = "import requests" ascii // Python HTTP client
$c2 = "requests.Session" ascii // Python session handling
$c3 = "axios" ascii // Node HTTP client
$c4 = "curl " ascii // shell HTTP client
condition:
filesize < 200KB and ($ua or ($path and ($bug1 or 2 of ($f*)) and any of ($c*)))
}

 

Keycloak_ResetCreds_Exploit_Memory targets the in-memory form of the same tooling in a running interpreter, where the request has been assembled but no file may remain on disk. It requires the tryAnotherWay=on form body or the PoC User-Agent, alongside an update-password form field, so a browser rendering a normal Keycloak page, which never holds both, does not match; scan interpreters only, not browsers.

rule Keycloak_ResetCreds_Exploit_Memory
{
meta:
description = "Interpreter memory holding a crafted Keycloak reset-credentials request chain"
author = "1898 & Co."
date = "2026-09-22"
reference = "https://nvd.nist.gov/vuln/detail/CVE-2026-18963"
strings:
$b1 = "tryAnotherWay=on" ascii wide // flow-corruption form body
$ua = "cve-2026-18963-poc" ascii wide nocase // public PoC default User-Agent
$p1 = "password-new=" ascii wide // update-password form body
$p2 = "password-confirm=" ascii wide // update-password form body
$e1 = "execution=UPDATE_PASSWORD" ascii wide // required-action step reached without token
condition:
($b1 or $ua) and (1 of ($p*) or $e1)
}

 

Indicators of Compromise

Network IOCs: POST to /realms/<realm>/login-actions/reset-credentials with body tryAnotherWay=on; several reset-credentials re-entries for the same tab_id from one client; POST to /login-actions/required-action?execution=UPDATE_PASSWORD with no preceding GET to /login-actions/action-token from the same client; User-Agent cve-2026-18963-poc (public PoC default — trivially changed, so absence proves nothing); one source IP completing resets for several users; external access to the master realm admin console.

Host IOCs: Keycloak SEND_RESET_PASSWORD followed by UPDATE_PASSWORD/UPDATE_CREDENTIAL for the same userId with no EXECUTE_ACTION_TOKEN in between; AD event 4724 by the Keycloak bind account followed by 4624 from an unfamiliar address; scripts matching Keycloak_ResetCreds_Exploit_Script on disk.

OT/operational IOCs: dynovaPRO reset emails that the recipient did not request before 2026-08-27 12:00 CEST (the exploit sends the victim a genuine reset email); charger, storage or PV setpoint and schedule changes with no operator record; new Modbus writers or register sets from the DER site controller; controller restarts (coldStart/warmStart traps) coinciding with cloud sessions.

External-exposure indicators: a two-engine-confirmed, owned Keycloak login or admin service visible in passive indices with an affected version and exposure spanning 2026-08-17 to its patch date.

False Positive Baseline

1. Users legitimately requesting their own password reset: these show an EXECUTE_ACTION_TOKEN event and an action-token request between the reset email and the password update.

2. Help-desk or administrator-initiated resets through the Keycloak admin console, recorded as admin events rather than user reset-credentials events.

3. Users who genuinely select "Try Another Way" during sign-in with multiple credential types; the reset-credentials flow rarely offers it, so repeated occurrences from one client remain suspicious.

4. Keycloak LDAP synchronization jobs updating AD attributes (4738) without a 4724 reset.

5. ABB's own remediation: users forced through a reset after 2026-08-27 by ABB, and dynovaPRO accounts ABB disabled; these appear as resets without prior compromise and should be confirmed with ABB.

6. Scheduled charging, storage dispatch and PV curtailment programs that change setpoints on a recurring schedule.

7. Security scanners, authorized penetration tests using the public PoC, and uptime monitors requesting Keycloak pages from fixed addresses.

8. EASM misattribution: shared hosting, CDN or cloud load-balancer addresses returning a Keycloak page that belongs to another tenant; require two ownership signals before treating a hit as the organization's.

Escalation Criteria

1. Any Keycloak user whose password changed after a reset request with no EXECUTE_ACTION_TOKEN in between, or any single IP completing resets for two or more users.

2. Any reset-credentials POST carrying tryAnotherWay=on followed by a required-action UPDATE_PASSWORD POST from the same client, or any request with the cve-2026-18963-poc User-Agent that was not an authorized test.

3. Any dynovaPRO user who received an unrequested reset email and whose account shows sign-ins or changes that the user does not recognize.

4. Any unexplained charger, battery or PV setpoint, schedule or mode change during or after the window that correlates with a reset account's session.

5. Any YARA hit on Keycloak_ResetCreds_Exploit_Script against files on Keycloak servers, jump hosts or user workstations.

6. Any YARA hit on Keycloak_ResetCreds_Exploit_Memory against a running interpreter process.

7. A two-engine-confirmed, owned, internet-exposed Keycloak instance running an affected version, or one whose exposure spans the 2026-08-17 to patch-date window.

8. Discovery of an exposed Keycloak instance not in the organization's asset inventory (shadow infrastructure), or a future CISA KEV listing for CVE-2026-18963.

9. A reset of any Keycloak realm administrator (including the master realm) or AD 4724 by the Keycloak bind account for a privileged or OT administrative account.

Hunt Completion Criteria and Reporting

The hunt is complete when every Keycloak server has been inventoried with version and patch status; Keycloak, proxy and AD reset activity has been reviewed for the full window (or the logging gap documented); every dynovaPRO reset email in the window has been dispositioned with the recipient; OT and historian data for every site supervised by dynovaPRO has been reviewed for unexplained changes; sessions for any account reset in the window have been revoked; and the external sweep has been completed in at least two engines.

The report must contain: the Keycloak inventory and patch status; the list of reset events and dispositions; affected dynovaPRO users and the ABB-provided history; OT change findings with timestamps and assets; YARA scan coverage and results for both rules; escalations raised; external sweep coverage, per-hit provenance (engine, query, scan timestamp, ownership signals); data source gaps; and a recommendation to make the Keycloak exposure sweep and reset-without-token monitors recurring controls.

Advisory IoC Reference

IOC Type IOC
CVE CVE-2026-18963 | CVSS v3.1 9.1 | ABB dynovaPRO cloud before 2026-08-27 12:00 CEST; Keycloak 26.0.0-26.4.14, 26.5.0-26.6.5, 26.7.0-26.7.1 (fixed 26.4.15 / 26.6.6 / 26.7.2) | Keycloak reset-credentials flow lets an unauthenticated attacker set a new password without the emailed action token (CWE-640); not in CISA KEV as of 2026-09-21
Threat Actor None attributed in source material — ABB observed 26 password change events across 9 customers consistent with testing or reconnaissance
Malware Public CVE-2026-18963 proof of concept | Python 3 requests script (exploit.py) that chains tryAnotherWay flow corruption with the missing action-token check
Network IOC User-Agent: cve-2026-18963-poc (public PoC default; trivially changed)
Network IOC URI + body: POST /realms/*/login-actions/reset-credentials with tryAnotherWay=on
File IOC None published in source material — no hashes published; use YARA rule Keycloak_ResetCreds_Exploit_Script
Behavioral Keycloak SEND_RESET_PASSWORD then UPDATE_PASSWORD/UPDATE_CREDENTIAL for same userId with no EXECUTE_ACTION_TOKEN in between
Behavioral POST /login-actions/required-action?execution=UPDATE_PASSWORD with no GET /login-actions/action-token from the same client
Behavioral One source IP completing password resets for two or more Keycloak users
Behavioral AD event 4724 by the Keycloak LDAP bind account followed by 4624 from a first-seen address
Behavioral dynovaPRO password reset email received before 2026-08-27 12:00 CEST that the user did not request
Behavioral Charger, battery or PV setpoint/schedule change from the DER site controller with no operator record