Turla Cereal on Exchange: Hunt the Hidden Mail Collector


What this means for your clients: If you still run mail on a server you own, or in a hybrid setup, an intruder on that server can copy selected messages as they flow, and a hunt that only watches outbound beacons will miss it.
ESET's November 10 research describes Cereal, a modular .NET toolset found on on-prem Microsoft Exchange servers, and ESET published a white paper with the technical detail. The intrusions are older. ESET attributes Cereal with high confidence to the Russia-aligned group Turla, also known as Snake. It detected the toolset at a governmental organization in Kazakhstan on November 28, 2025, and later at a governmental organization in Romania compromised from December 23, 2025, the only two confirmed victims in ESET's Turla Cereal report. In Romania, CerealBox was set to pick out mail to or from NATO and EU addresses, including nato.int and europa.eu. ESET could not determine initial access. Exploitation of an internet-facing Exchange vulnerability is possible, not confirmed.
How Cereal hides inside Exchange
CerealLoader is the only recognizable .NET file on disk. Everything else arrives encrypted, runs from memory, and is obfuscated with ConfuserEx. CerealHermit is a fallback that reads commands from encrypted files on disk.
The System32\wbem DLL sideload
Operators wrote CerealLoader as C:\Windows\System32\wbem\ncobjapi.dll, loaded by the WMI Provider Host, or as C:\Windows\System32\wbem\wbemcomn.dll so WmiPrvSE.exe would load it from its own folder instead of the real library in System32. ESET also lists version.dll among CerealLoader names but did not publish where it was dropped. We check System32\wbem for it because the related loaders sit there, which is our choice, not an ESET finding.
Tampering with the NativeImages cache
Operators also hand-copied fake native images into C:\Windows\assembly\NativeImages_v4.0.30319_64, using names such as Microsoft.Exchange.Search.Mdb.ni.dll, Microsoft.Exchange.Data.Transport.ni.dll, EdgeTransport.ni.exe and Microsoft.Exchange.EdgeSyncSvc.ni.exe. ESET says these were not built with ngen.exe. Exchange loaded the malicious image first, then fell back to the real assembly. In one sample, the code ran only if the parent was Microsoft.Exchange.Search.Service.
The HTTP.sys listener on Exchange-looking URLs
CerealNeuron is a passive backdoor and does not beacon out. It uses .NET's HttpListener to wait on the server's own HTTPS for prefixes that look like Outlook Web Access, Autodiscover or ActiveSync: /owa/sdds/, /autodiscover/microsoft.exchange.proxyservice/, /autodiscover/integrated/api/, /autodiscover/proxyservice/ and /microsoft-server-activesync/integrated/api/. Those paths are not normal Exchange routes. A failed check gets a legitimate-looking error page. ESET found no operator IPs and no external command infrastructure.
CerealIron injects .NET code into processes that are not .NET applications. ESET assesses that CerealBox was injected into EdgeTransport.exe and wrapped existing transport agents in memory, so no new agent appears in Exchange configuration. Matching .doc, .docx or .pdf attachments are staged to a file. Commands ESET saw include mailbox reads, Active Directory enumeration, WMI queries for processes, shadow copies and TCP connections, LSA secrets, and file listing or deletion, often of staged mail.
What this means for a company
Exchange is a durable collection point: mail copied in transit, LSA secrets for later movement, and a backdoor with no outbound command channel for network tools to flag. A clean list of transport agents is not proof of a clean server. ESET confirmed this at two governmental organizations, not at companies. Shops that still run on-prem or hybrid Microsoft Exchange should treat the technique as in scope.
Reduce the risk on the servers you still run
These are our recommendations, not steps from ESET's report. Put Outlook Web Access and ActiveSync behind pre-authentication or a reverse proxy, because CerealNeuron hides on those paths. Watch C:\Windows\System32\wbem and the NativeImages_* folders for unexpected changes, because that is where the loader lives. Turn on Credential Guard and LSA protection, because the toolset pulls LSA secrets. Patch Exchange, shrink exposure, keep AMSI and Exchange Emergency Mitigation on, run EDR on those servers, and treat Exchange administrators as tier 0. Where Exchange Online is a fit, plan the move. You still have to hunt.
Before you hunt
We wrote these from vendor documentation and haven't run them in a live SIEM, so test each one in your environment before you trust it.
Prepare in order. First, list every on-prem and hybrid Exchange server from your asset system or EDR console, not from the SIEM. Second, confirm each host sends the telemetry these hunts use, including IIS logs where you collect them. Third, reconcile against sensor check-ins and collector health before any time filter, since a later filter cannot prove completeness. Then run the coverage test. An Exchange server that returns nothing is a gap, not a clean result.
All 28 blocks share one window, 2026-10-11T07:00:00Z inclusive to 2026-11-10T07:00:00Z exclusive, midnight October 11 to midnight November 10 Phoenix time (MST). Widen it as retention allows, and change every block together.
CrowdStrike event names in these hunts are an assumption. Image-load and registry-read events may be sampled or missing on every platform, so a quiet result is not proof the activity did not happen.
Coverage test: can you see your Exchange servers
Every Exchange server on your list should appear here, by host and event type.
Field mapping: This test is host and sensor telemetry only: Sysmon in YOUR_SYSMON_INDEX, Elastic Defend streams, CrowdStrike sensor events, and Defender device tables. Confirm IIS log coverage separately where you collect IIS or HTTPERR logs, because Hunt 3 depends on them and they are not in this test.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 7, 8, 10, 11, 12, 13) earliest=1791702000 latest=1794294000
| where _time >= 1791702000 AND _time < 1794294000
| stats count AS events min(_time) AS first_seen max(_time) AS last_seen BY host EventCodeElastic
FROM logs-endpoint.events.*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| STATS events = COUNT(*), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name, data_stream.datasetCrowdStrike
#event_simpleName=/^(ProcessRollup2|ClassifiedModuleLoad|PackedExecutableModuleLoad|InjectedThread|RemoteThread|PeFileWritten|\w+Written|RegGenericValueUpdate|AsepValueUpdate|NetworkConnectIP4)$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| groupBy([aid, #event_simpleName], function=[count(as=events), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])Microsoft KQL
union withsource=SourceTable DeviceProcessEvents, DeviceFileEvents, DeviceImageLoadEvents, DeviceRegistryEvents, DeviceEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| summarize Events = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName, SourceTableHunt 1: the System32\wbem DLL sideload
This looks for ncobjapi.dll, wbemcomn.dll or version.dll written under C:\Windows\System32\wbem, and for WmiPrvSE.exe loading those libraries or a .NET runtime. Windows Update writes in that folder too, so a write is a lead until you see who created the file and whether WmiPrvSE.exe loaded it.
Field mapping: Sysmon EventCode 7 is often off. ESET published version.dll only as a file name, so the wbem path for that name follows the hunt list, not a documented drop path.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (7, 11) earliest=1791702000 latest=1794294000
| where _time >= 1791702000 AND _time < 1794294000
| eval f=lower(coalesce(TargetFilename, "")), img=lower(coalesce(Image, "")), loaded=lower(coalesce(ImageLoaded, "")), signed=lower(coalesce(SignatureStatus, Signed, ""))
| where (EventCode=11 AND like(f, "c:_windows_system32_wbem_%") AND match(f, "(ncobjapi|wbemcomn|version)\.dll$")) OR (EventCode=7 AND like(img, "%_wmiprvse.exe") AND (match(loaded, "(ncobjapi|wbemcomn)\.dll$") OR match(loaded, "(mscoree|clr|clrjit|coreclr)\.dll$") OR (match(loaded, "c:_windows_system32_wbem_") AND signed!="valid" AND signed!="signed")))
| table _time host EventCode User Image TargetFilename ImageLoaded Signature Signed SignatureStatusElastic
FROM logs-endpoint.events.file-*, logs-endpoint.events.library-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL f = TO_LOWER(COALESCE(file.path, "")), img = TO_LOWER(COALESCE(process.executable, "")), loaded = TO_LOWER(COALESCE(dll.path, file.path, "")), sig = TO_LOWER(COALESCE(dll.code_signature.status, ""))
| WHERE (data_stream.dataset == "endpoint.events.file" AND f LIKE "c:?windows?system32?wbem?*" AND f RLIKE ".*(ncobjapi|wbemcomn|version)\\.dll") OR (data_stream.dataset == "endpoint.events.library" AND img LIKE "*?wmiprvse.exe" AND (loaded RLIKE ".*(ncobjapi|wbemcomn)\\.dll" OR loaded RLIKE ".*(mscoree|clr|clrjit|coreclr)\\.dll" OR (loaded LIKE "c:?windows?system32?wbem?*" AND sig != "" AND sig != "valid" AND sig != "signed")))
| KEEP @timestamp, host.name, user.name, process.executable, file.path, dll.path, dll.code_signature.status, file.code_signature.status
| SORT @timestampCrowdStrike
#event_simpleName=/ClassifiedModuleLoad|PackedExecutableModuleLoad|\w+Written$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| case {
#event_simpleName=/\w+Written$/ | TargetFileName=/\\Windows\\System32\\wbem\\.*(ncobjapi|wbemcomn|version)\.dll$/i ;
ContextBaseFileName=/^WmiPrvSE\.exe$/i | ImageFileName=/(ncobjapi|wbemcomn)\.dll$|(mscoree|clr|clrjit|coreclr)\.dll$/i ;
ContextBaseFileName=/^WmiPrvSE\.exe$/i | ImageFileName=/\\Windows\\System32\\wbem\\/i | SignatureStatus!=/^$/ | SignatureStatus!=/^(valid|signed)$/i ;
}
| table([@timestamp, aid, #event_simpleName, ContextBaseFileName, ImageFileName, TargetFileName, SHA256HashData, SignatureStatus])Microsoft KQL
let certs = DeviceFileCertificateInfo
| where isnotempty(SHA1)
| summarize IsUnsigned = countif(IsSigned == false) > 0
by DeviceId, SHA1;
union withsource=SourceTable DeviceFileEvents, DeviceImageLoadEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| join kind=leftouter certs on DeviceId, SHA1
| where (SourceTable == "DeviceFileEvents" and FolderPath contains @"\Windows\System32\wbem" and FileName in~ ("ncobjapi.dll", "wbemcomn.dll", "version.dll"))
or (SourceTable == "DeviceImageLoadEvents" and InitiatingProcessFileName =~ "WmiPrvSE.exe" and (FileName in~ ("ncobjapi.dll", "wbemcomn.dll", "mscoree.dll", "clr.dll", "clrjit.dll", "coreclr.dll") or (FolderPath contains @"\Windows\System32\wbem" and IsUnsigned == true)))
| project Timestamp, DeviceName, SourceTable, ActionType, FolderPath, FileName, SHA1, IsUnsigned, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessAccountNameHunt 2: unexpected writes to the NativeImages cache
This looks for files written under NativeImages_v4.0.30319 by anything other than ngen.exe or mscorsvw.exe, especially Microsoft.Exchange .ni names, which ESET says were copied by hand.
Field mapping: CrowdStrike file-write events may miss silent copies. Image-load is not required here, but a later load of the same path is stronger evidence.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=11 earliest=1791702000 latest=1794294000
| where _time >= 1791702000 AND _time < 1794294000
| eval f=lower(coalesce(TargetFilename, "")), img=lower(coalesce(Image, "")), proc=lower(mvindex(split(Image, "\\"), -1))
| where like(f, "c:_windows_assembly_nativeimages_v4.0.30319%") AND NOT in(proc, "ngen.exe", "mscorsvw.exe")
| table _time host User Image TargetFilenameElastic
FROM logs-endpoint.events.file-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL f = TO_LOWER(COALESCE(file.path, "")), proc = TO_LOWER(COALESCE(process.name, ""))
| WHERE f LIKE "c:?windows?assembly?nativeimages_v4.0.30319*" AND proc NOT IN ("ngen.exe", "mscorsvw.exe")
| KEEP @timestamp, host.name, user.name, process.executable, process.name, file.path
| SORT @timestampCrowdStrike
#event_simpleName=/\w+Written$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| TargetFileName=/\\Windows\\assembly\\NativeImages_v4\.0\.30319/i
| default(field=ContextBaseFileName, value="missing")
| ContextBaseFileName!=/^(ngen|mscorsvw)\.exe$/i
| table([@timestamp, aid, #event_simpleName, ContextBaseFileName, ContextProcessId, TargetFileName])Microsoft KQL
DeviceFileEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| where FolderPath contains @"\Windows\assembly\NativeImages_v4.0.30319"
| where InitiatingProcessFileName !in~ ("ngen.exe", "mscorsvw.exe")
| project Timestamp, DeviceName, ActionType, FolderPath, FileName, SHA1, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessAccountNameHunt 3: HTTP.sys hits on CerealNeuron's URL prefixes
This looks in IIS, HTTPERR and proxy logs for the five prefixes CerealNeuron listened on, and for netsh commands that add HTTP.sys URL reservations, including netsh http add urlacl. That check is our hunting method, not something ESET reported. An in-process HttpListener running as SYSTEM may never create a reservation, so this netsh leg will not catch that case. The prefixes will not appear if IIS logs were never onboarded.
Field mapping: URL stems need YOUR_IIS_INDEX in Splunk, logs-iis.access-* or an equivalent HTTP stream in Elastic, and W3CIISLog in Sentinel. Those streams are not on by default, and they are absent from a Defender-only workspace. Unless IIS logs are ingested into LogScale, the CrowdStrike half of this hunt is command lines only.
Splunk
(index=YOUR_IIS_INDEX) OR (index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1)
| where _time >= 1791702000 AND _time < 1794294000
| eval p=lower(coalesce(cs_uri_stem, uri_stem, uri_path, url, cs_uri, "")), cmd=lower(coalesce(CommandLine, "")), proc=lower(mvindex(split(coalesce(Image, ""), "\\"), -1))
| where match(p, "/owa/sdds(/|$)|/autodiscover/microsoft\.exchange\.proxyservice(/|$)|/autodiscover/integrated/api(/|$)|/autodiscover/proxyservice(/|$)|/microsoft-server-activesync/integrated/api(/|$)") OR (match(cmd, "netsh") AND match(cmd, "http") AND match(cmd, "add|urlacl|urlreserve"))
| table _time host p cs_uri_stem src_ip status Image CommandLineElastic
FROM logs-iis.access-*, logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL p = TO_LOWER(COALESCE(url.path, url.original, "")), cmd = TO_LOWER(COALESCE(process.command_line, "")), proc = TO_LOWER(COALESCE(process.name, ""))
| WHERE p RLIKE ".*(/owa/sdds|/autodiscover/microsoft\\.exchange\\.proxyservice|/autodiscover/integrated/api|/autodiscover/proxyservice|/microsoft-server-activesync/integrated/api)(/.*|$)" OR (cmd LIKE "*netsh*" AND cmd LIKE "*http*" AND (cmd LIKE "*add*" OR cmd LIKE "*urlacl*" OR cmd LIKE "*urlreserve*"))
| KEEP @timestamp, host.name, url.path, source.ip, http.response.status_code, process.executable, process.command_line
| SORT @timestampCrowdStrike
#event_simpleName=ProcessRollup2 or @rawstring=/\/owa\/sdds|\/autodiscover\/microsoft\.exchange\.proxyservice|\/autodiscover\/integrated\/api|\/autodiscover\/proxyservice|\/microsoft-server-activesync\/integrated\/api/i
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| case {
CommandLine=/netsh/i | CommandLine=/http/i | CommandLine=/add|urlacl|urlreserve/i ;
@rawstring=/\/owa\/sdds|\/autodiscover\/microsoft\.exchange\.proxyservice|\/autodiscover\/integrated\/api|\/autodiscover\/proxyservice|\/microsoft-server-activesync\/integrated\/api/i ;
}
| table([@timestamp, aid, #event_simpleName, FileName, CommandLine, @rawstring])Microsoft KQL
union withsource=SourceTable W3CIISLog, DeviceProcessEvents
| extend EventTime = coalesce(Timestamp, TimeGenerated)
| where EventTime >= datetime(2026-10-11T07:00:00Z) and EventTime < datetime(2026-11-10T07:00:00Z)
| where (csUriStem matches regex @"(?i)/owa/sdds|/autodiscover/microsoft\.exchange\.proxyservice|/autodiscover/integrated/api|/autodiscover/proxyservice|/microsoft-server-activesync/integrated/api")
or (ProcessCommandLine matches regex @"(?i)netsh" and ProcessCommandLine matches regex @"(?i)http" and ProcessCommandLine matches regex @"(?i)add|urlacl|urlreserve")
| project EventTime, DeviceName, Computer, SourceTable, csUriStem, cIP, scStatus, FileName, ProcessCommandLineHunt 4: injection and unexpected .NET in EdgeTransport and search
This looks for remote threads into EdgeTransport.exe as the target, not as the source. In WmiPrvSE.exe it also looks for a .NET runtime or .ni load, or an unsigned module, which is odd in a process that is not a .NET application. In EdgeTransport.exe and Microsoft.Exchange.Search.Service.exe it matches only unsigned modules, or .ni images whose path was written by a process other than ngen.exe or mscorsvw.exe.
Field mapping: Each platform pairs .ni loads with NativeImages writes by a process other than ngen.exe or mscorsvw.exe on the same host, and matches EdgeTransport.exe only as the injection target. In Microsoft KQL the OpenProcessApiCall leg cannot see EdgeTransport.exe, because Defender forwards that event only for lsass.exe targets, so the remote-thread and image-load legs carry EdgeTransport.exe where that telemetry exists. The unsigned leg comes from certificate data joined in from DeviceFileCertificateInfo, and a file with no certificate record stays unknown, not unsigned. Elastic has no CreateRemoteThread API event, so its injection leg relies on OpenProcess, VirtualAllocEx, WriteProcessMemory and related API events, some of which only arrive with windows.advanced.events.api_verbose turned on. On every platform, a missing signature status leaves the unsigned leg incomplete.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (7, 8, 10, 11) earliest=1791702000 latest=1794294000
| where _time >= 1791702000 AND _time < 1794294000
| eval tgt=lower(coalesce(TargetImage, "")), img=lower(coalesce(Image, "")), loaded=lower(coalesce(ImageLoaded, "")), signed=lower(coalesce(SignatureStatus, Signed, "")), f=lower(coalesce(TargetFilename, "")), proc=lower(mvindex(split(coalesce(Image, ""), "\\"), -1))
| eval ni_path=if(EventCode=11, f, loaded)
| eval unexpected_ni_write=if(EventCode=11 AND like(f, "c:_windows_assembly_nativeimages_v4.0.30319%") AND match(f, "\\.ni\\.(dll|exe)$") AND NOT in(proc, "ngen.exe", "mscorsvw.exe"), 1, 0)
| eventstats max(unexpected_ni_write) AS ni_bad_write BY host, ni_path
| where (EventCode IN (8, 10) AND like(tgt, "%_edgetransport.exe")) OR (EventCode=7 AND like(img, "%_wmiprvse.exe") AND (match(loaded, "\\.ni\\.(dll|exe)$") OR match(loaded, "(mscoree|clr|clrjit|coreclr)\.dll$") OR (signed!="" AND signed!="valid" AND signed!="signed"))) OR (EventCode=7 AND (like(img, "%_edgetransport.exe") OR like(img, "%_microsoft.exchange.search.service.exe")) AND ((signed!="" AND signed!="valid" AND signed!="signed") OR (match(loaded, "\\.ni\\.(dll|exe)$") AND ni_bad_write=1)))
| table _time host EventCode Image TargetImage ImageLoaded TargetFilename SignatureStatus SignedElastic
FROM logs-endpoint.events.process-*, logs-endpoint.events.library-*, logs-endpoint.events.file-*, logs-endpoint.events.api-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL img = TO_LOWER(COALESCE(process.executable, "")), tgt = TO_LOWER(COALESCE(`Target.process.executable`, "")), api = TO_LOWER(COALESCE(process.Ext.api.name, "")), loaded = TO_LOWER(COALESCE(dll.path, "")), f = TO_LOWER(COALESCE(file.path, "")), proc = TO_LOWER(COALESCE(process.name, "")), action = TO_LOWER(COALESCE(event.action, "")), sig = TO_LOWER(COALESCE(dll.code_signature.status, "")), path = TO_LOWER(COALESCE(dll.path, file.path, "")), hostn = TO_LOWER(COALESCE(host.name, ""))
| EVAL is_ni_write = (data_stream.dataset == "endpoint.events.file" AND f LIKE "c:?windows?assembly?nativeimages_v4.0.30319*" AND f RLIKE ".*\\.ni\\.(dll|exe)" AND proc NOT IN ("ngen.exe", "mscorsvw.exe"))
| EVAL is_inject = (data_stream.dataset == "endpoint.events.api" AND api IN ("openprocess", "virtualallocex", "writeprocessmemory", "ntqueueapcthread", "queueuserapc", "setthreadcontext", "wow64setthreadcontext") AND tgt LIKE "*?edgetransport.exe")
| EVAL is_wmiprvse = (data_stream.dataset == "endpoint.events.library" AND img LIKE "*?wmiprvse.exe" AND (loaded RLIKE ".*\\.ni\\.(dll|exe)" OR loaded RLIKE ".*(mscoree|clr|clrjit|coreclr)\\.dll" OR (sig != "" AND sig != "valid" AND sig != "signed")))
| EVAL is_exch_unsigned = (data_stream.dataset == "endpoint.events.library" AND (img LIKE "*?edgetransport.exe" OR img LIKE "*?microsoft.exchange.search.service.exe") AND sig != "" AND sig != "valid" AND sig != "signed")
| EVAL is_exch_ni = (data_stream.dataset == "endpoint.events.library" AND (img LIKE "*?edgetransport.exe" OR img LIKE "*?microsoft.exchange.search.service.exe") AND loaded RLIKE ".*\\.ni\\.(dll|exe)")
| EVAL grp = CASE(is_inject OR is_wmiprvse OR is_exch_unsigned, CONCAT("evt:", hostn, ":", TO_STRING(@timestamp), ":", path, ":", img, ":", action), is_exch_ni OR is_ni_write, CONCAT("ni:", hostn, ":", path), "drop")
| WHERE grp != "drop"
| STATS bad_ni = MAX(CASE(is_ni_write, 1, 0)), exni = MAX(CASE(is_exch_ni, 1, 0)), inject = MAX(CASE(is_inject, 1, 0)), wmirt = MAX(CASE(is_wmiprvse, 1, 0)), exunsigned = MAX(CASE(is_exch_unsigned, 1, 0)), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp), images = VALUES(img), targets = VALUES(tgt), paths = VALUES(path), actions = VALUES(action), sigs = VALUES(sig) BY grp, host.name
| WHERE inject == 1 OR wmirt == 1 OR exunsigned == 1 OR (exni == 1 AND bad_ni == 1)
| SORT first_seenCrowdStrike
defineTable(name="bad_ni", query={#event_simpleName=/\w+Written$/ | test(@timestamp >= 1791702000000) | test(@timestamp < 1794294000000) | TargetFileName=/\\Windows\\assembly\\NativeImages_v4\.0\.30319/i | TargetFileName=/\.ni\.(dll|exe)$/i | ContextBaseFileName!=/^(ngen|mscorsvw)\.exe$/i | lower(TargetFileName, as=ni_key) | ni_written := "1"}, include=[aid, ni_key, ni_written])
| #event_simpleName=/^(InjectedThread|RemoteThread|ClassifiedModuleLoad|PackedExecutableModuleLoad)$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| lower(ImageFileName, as=ni_key)
| match(table="bad_ni", field=[aid, ni_key], strict=false)
| case {
#event_simpleName=/InjectedThread|RemoteThread/ | TargetFileName=/EdgeTransport\.exe$/i ;
#event_simpleName=/ClassifiedModuleLoad|PackedExecutableModuleLoad/ | ContextBaseFileName=/^WmiPrvSE\.exe$/i | ImageFileName=/\.ni\.(dll|exe)$|(mscoree|clr|clrjit|coreclr)\.dll$/i ;
#event_simpleName=/ClassifiedModuleLoad|PackedExecutableModuleLoad/ | ContextBaseFileName=/^WmiPrvSE\.exe$/i | SignatureStatus!=/^$/ | SignatureStatus!=/^(valid|signed)$/i ;
#event_simpleName=/ClassifiedModuleLoad|PackedExecutableModuleLoad/ | ContextBaseFileName=/^(EdgeTransport|Microsoft\.Exchange\.Search\.Service)\.exe$/i | SignatureStatus!=/^$/ | SignatureStatus!=/^(valid|signed)$/i ;
#event_simpleName=/ClassifiedModuleLoad|PackedExecutableModuleLoad/ | ContextBaseFileName=/^(EdgeTransport|Microsoft\.Exchange\.Search\.Service)\.exe$/i | ImageFileName=/\.ni\.(dll|exe)$/i | ni_written="1" ;
}
| table([@timestamp, aid, #event_simpleName, ContextBaseFileName, TargetFileName, ImageFileName, SHA256HashData, SignatureStatus, ni_written])Microsoft KQL
let certs = DeviceFileCertificateInfo
| where isnotempty(SHA1)
| summarize IsUnsigned = countif(IsSigned == false) > 0
by DeviceId, SHA1;
let badNi = DeviceFileEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| where FolderPath contains @"\Windows\assembly\NativeImages_v4.0.30319"
| where InitiatingProcessFileName !in~ ("ngen.exe", "mscorsvw.exe")
| where FileName endswith ".ni.dll" or FileName endswith ".ni.exe"
| extend NiKey = tolower(strcat(DeviceName, "|", FolderPath, "\\", FileName))
| project NiKey;
union withsource=SourceTable DeviceEvents, DeviceImageLoadEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| join kind=leftouter certs on DeviceId, SHA1
| extend NiKey = tolower(strcat(DeviceName, "|", FolderPath, "\\", FileName))
| extend IsBadNi = NiKey in (badNi)
| where (SourceTable == "DeviceEvents" and ActionType in~ ("CreateRemoteThreadApiCall", "OpenProcessApiCall") and FileName =~ "EdgeTransport.exe")
or (SourceTable == "DeviceImageLoadEvents" and InitiatingProcessFileName =~ "WmiPrvSE.exe" and (FileName endswith ".ni.dll" or FileName endswith ".ni.exe" or FileName in~ ("mscoree.dll", "clr.dll", "clrjit.dll", "coreclr.dll") or IsUnsigned == true))
or (SourceTable == "DeviceImageLoadEvents" and InitiatingProcessFileName in~ ("EdgeTransport.exe", "Microsoft.Exchange.Search.Service.exe") and (IsUnsigned == true or ((FileName endswith ".ni.dll" or FileName endswith ".ni.exe") and IsBadNi)))
| project Timestamp, DeviceName, SourceTable, ActionType, InitiatingProcessFileName, FileName, FolderPath, SHA1, IsUnsignedHunt 5: LSA secrets and WMI reconnaissance from Exchange
This looks for registry access to HKLM\SECURITY\Policy\Secrets and process access to lsass.exe from Exchange processes, plus WMI queries for Win32_ShadowCopy and netstat-style TCP enumeration from Exchange processes or service accounts. ESET saw those commands after the implant was in place. Registry reads and WMI query text are often not logged.
Field mapping: Registry events on these platforms usually record writes, not reads of LSA secrets. WMI query text needs WMI-Activity, in YOUR_WMI_INDEX or the Elastic equivalent when collected, and the CrowdStrike hunt is command lines without it. Admins and backup tools also query shadow copies, and lsass.exe touching itself is expected.
Splunk
(index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 10, 12, 13)) OR (index=YOUR_WMI_INDEX source="XmlWinEventLog:Microsoft-Windows-WMI-Activity/Operational")
| where _time >= 1791702000 AND _time < 1794294000
| eval cmd=lower(coalesce(CommandLine, Message, "")), img=lower(coalesce(Image, "")), tgt=lower(coalesce(TargetImage, "")), reg=lower(coalesce(TargetObject, "")), usr=lower(coalesce(User, UserName, SubjectUserName, ""))
| where (EventCode IN (12, 13) AND like(reg, "%_security_policy_secrets%") AND (like(img, "%_edgetransport.exe") OR like(img, "%_microsoft.exchange.search.service.exe") OR like(img, "%_wmiprvse.exe") OR like(img, "%_w3wp.exe") OR like(img, "%_microsoft.exchange.%"))) OR (EventCode=10 AND like(tgt, "%_lsass.exe") AND NOT like(img, "%_lsass.exe") AND (like(img, "%_edgetransport.exe") OR like(img, "%_microsoft.exchange.search.service.exe") OR like(img, "%_wmiprvse.exe") OR like(img, "%_w3wp.exe") OR like(img, "%_microsoft.exchange.%"))) OR ((match(cmd, "win32_shadowcopy") OR match(cmd, "get-nettcpconnection") OR match(cmd, "msft_nettcpconnection") OR match(cmd, "netstat") OR match(cmd, "select.*tcp")) AND (like(img, "%_edgetransport.exe") OR like(img, "%_microsoft.exchange.search.service.exe") OR like(img, "%_wmiprvse.exe") OR like(img, "%_w3wp.exe") OR like(img, "%_wmic.exe") OR like(img, "%_powershell.exe") OR like(img, "%_pwsh.exe") OR like(usr, "%msexchange%") OR like(usr, "%microsoft exchange%")))
| table _time host EventCode User Image TargetImage TargetObject GrantedAccess CommandLineElastic
FROM logs-endpoint.events.registry-*, logs-endpoint.events.process-*, logs-endpoint.events.api-*, logs-windows.wmi_activity-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL img = TO_LOWER(COALESCE(process.executable, "")), tgt = TO_LOWER(COALESCE(`Target.process.executable`, "")), reg = TO_LOWER(COALESCE(registry.path, "")), cmd = TO_LOWER(COALESCE(process.command_line, message, "")), usr = TO_LOWER(COALESCE(user.name, ""))
| WHERE (reg LIKE "*?security?policy?secrets*" AND (img LIKE "*?edgetransport.exe" OR img LIKE "*?microsoft.exchange.search.service.exe" OR img LIKE "*?wmiprvse.exe" OR img LIKE "*?w3wp.exe" OR img LIKE "*?microsoft.exchange.*")) OR (tgt LIKE "*?lsass.exe" AND img NOT LIKE "*?lsass.exe" AND (img LIKE "*?edgetransport.exe" OR img LIKE "*?microsoft.exchange.search.service.exe" OR img LIKE "*?wmiprvse.exe" OR img LIKE "*?w3wp.exe" OR img LIKE "*?microsoft.exchange.*")) OR ((cmd LIKE "*win32_shadowcopy*" OR cmd LIKE "*get-nettcpconnection*" OR cmd LIKE "*msft_nettcpconnection*" OR cmd LIKE "*netstat*" OR cmd RLIKE ".*select.*tcp.*") AND (img LIKE "*?edgetransport.exe" OR img LIKE "*?microsoft.exchange.search.service.exe" OR img LIKE "*?wmiprvse.exe" OR img LIKE "*?w3wp.exe" OR img LIKE "*?wmic.exe" OR img LIKE "*?powershell.exe" OR img LIKE "*?pwsh.exe" OR usr LIKE "*msexchange*" OR usr LIKE "*microsoft exchange*"))
| KEEP @timestamp, host.name, user.name, event.action, process.executable, `Target.process.executable`, process.command_line, registry.path, message
| SORT @timestampCrowdStrike
#event_simpleName=/^(RegGenericValueUpdate|AsepValueUpdate|ProcessAccess|CredentialAccess|ProcessRollup2)$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| case {
RegObjectName=/SECURITY\\Policy\\Secrets/i | ContextBaseFileName=/^(EdgeTransport|Microsoft\.Exchange\.Search\.Service|WmiPrvSE|w3wp)\.exe$/i or ContextBaseFileName=/^Microsoft\.Exchange\./i ;
#event_simpleName=/ProcessAccess|CredentialAccess/ | (TargetFileName=/lsass\.exe$/i or ImageFileName=/lsass\.exe$/i) | ContextBaseFileName=/^(EdgeTransport|Microsoft\.Exchange\.Search\.Service|WmiPrvSE|w3wp)\.exe$/i or ContextBaseFileName=/^Microsoft\.Exchange\./i ;
#event_simpleName=ProcessRollup2 | CommandLine=/Win32_ShadowCopy|Get-NetTCPConnection|MSFT_NetTCPConnection|netstat|(?i)select.*tcp/i | FileName=/^(EdgeTransport|Microsoft\.Exchange\.Search\.Service|WmiPrvSE|w3wp|wmic|powershell|pwsh)\.exe$/i or UserName=/(?i)MSExchange|Microsoft Exchange/ ;
}
| table([@timestamp, aid, #event_simpleName, UserName, ContextBaseFileName, FileName, TargetFileName, ImageFileName, CommandLine, RegObjectName])Microsoft KQL
union withsource=SourceTable DeviceRegistryEvents, DeviceEvents, DeviceProcessEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| where (
(InitiatingProcessFileName in~ ("EdgeTransport.exe", "Microsoft.Exchange.Search.Service.exe", "WmiPrvSE.exe", "w3wp.exe") or InitiatingProcessFileName startswith "Microsoft.Exchange.")
and (
RegistryKey contains @"\SECURITY\Policy\Secrets"
or (ActionType in~ ("OpenProcessApiCall", "CreateRemoteThreadApiCall") and FileName =~ "lsass.exe")
)
)
or (
ProcessCommandLine matches regex @"(?i)Win32_ShadowCopy|Get-NetTCPConnection|MSFT_NetTCPConnection|netstat|select.*tcp"
and (
FileName in~ ("EdgeTransport.exe", "Microsoft.Exchange.Search.Service.exe", "WmiPrvSE.exe", "w3wp.exe", "wmic.exe", "powershell.exe", "pwsh.exe")
or AccountName has_any ("MSExchange", "Microsoft Exchange")
or InitiatingProcessFileName in~ ("EdgeTransport.exe", "Microsoft.Exchange.Search.Service.exe", "WmiPrvSE.exe", "w3wp.exe")
)
)
| project Timestamp, DeviceName, SourceTable, ActionType, InitiatingProcessFileName, InitiatingProcessAccountName, AccountName, FileName, ProcessCommandLine, RegistryKeyHunt 6: ESET SHA-1 hash sweep
This side query matches the SHA-1 hashes ESET published for CerealLoader, CerealIron, CerealHermit and related components. A hash hit is enough to isolate the host. A miss proves little, because most of the toolset never lands on disk as a recognizable file. The live values are in the queries.
Field mapping: Splunk needs SHA1 in HashAlgorithms, and many configs log only SHA256. Elastic Defend and CrowdStrike often store SHA256 only, so SHA-1 matches may need enrichment. The SHA1 field in Microsoft KQL is not always populated.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 7, 11) earliest=1791702000 latest=1794294000
| where _time >= 1791702000 AND _time < 1794294000
| eval h=upper(coalesce(SHA1, sha1, ""))
| eval hashes=upper(coalesce(Hashes, hashes, ""))
| where h IN ("4A3C263977417FB6977B0E598C9E3BC5924F390C","7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3","AFE448DD95494CD14438F04F0C5F6B384664A3B7","4A65040646E354303F067D47AA1C953D467F65CB","D1E17520A2435EDCDCC47F582381416FBDF75F94","7AB784D0049890D9844AEE7F214BA173E3CC23BE","5D80118B1EE974936322475F740AB3AEFF70B8F3","D51C58C2E95D4561848E0A5A8B9FB33C847C50A4","5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B","1408A8F62B09A5AD0DFF279B2878D8F59F70A330","3D1A534CE0D0C293F62ECE556CBC6D0728213104","AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387","3EA03188DBA8898D97E965732C1D6A68C5437EDC","3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F","EB9BA1DA5363F5D04E2F98673595FCE93F39A78E","0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60","A4E477CE257F2C635401EF554E05506C080967C0","A5BE8706032A0798FD63214A9F22EFA6309355F5") OR match(hashes, "SHA1=(4A3C263977417FB6977B0E598C9E3BC5924F390C|7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3|AFE448DD95494CD14438F04F0C5F6B384664A3B7|4A65040646E354303F067D47AA1C953D467F65CB|D1E17520A2435EDCDCC47F582381416FBDF75F94|7AB784D0049890D9844AEE7F214BA173E3CC23BE|5D80118B1EE974936322475F740AB3AEFF70B8F3|D51C58C2E95D4561848E0A5A8B9FB33C847C50A4|5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B|1408A8F62B09A5AD0DFF279B2878D8F59F70A330|3D1A534CE0D0C293F62ECE556CBC6D0728213104|AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387|3EA03188DBA8898D97E965732C1D6A68C5437EDC|3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F|EB9BA1DA5363F5D04E2F98673595FCE93F39A78E|0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60|A4E477CE257F2C635401EF554E05506C080967C0|A5BE8706032A0798FD63214A9F22EFA6309355F5)")
| table _time host EventCode Image ImageLoaded TargetFilename SHA1 HashesElastic
FROM logs-endpoint.events.file-*, logs-endpoint.events.library-*, logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-10-11T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-10T07:00:00Z")
| EVAL h = TO_UPPER(COALESCE(file.hash.sha1, process.hash.sha1, dll.hash.sha1, ""))
| WHERE h IN ("4A3C263977417FB6977B0E598C9E3BC5924F390C", "7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3", "AFE448DD95494CD14438F04F0C5F6B384664A3B7", "4A65040646E354303F067D47AA1C953D467F65CB", "D1E17520A2435EDCDCC47F582381416FBDF75F94", "7AB784D0049890D9844AEE7F214BA173E3CC23BE", "5D80118B1EE974936322475F740AB3AEFF70B8F3", "D51C58C2E95D4561848E0A5A8B9FB33C847C50A4", "5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B", "1408A8F62B09A5AD0DFF279B2878D8F59F70A330", "3D1A534CE0D0C293F62ECE556CBC6D0728213104", "AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387", "3EA03188DBA8898D97E965732C1D6A68C5437EDC", "3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F", "EB9BA1DA5363F5D04E2F98673595FCE93F39A78E", "0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60", "A4E477CE257F2C635401EF554E05506C080967C0", "A5BE8706032A0798FD63214A9F22EFA6309355F5")
| KEEP @timestamp, host.name, process.executable, file.path, dll.path, file.hash.sha1, process.hash.sha1, dll.hash.sha1
| SORT @timestampCrowdStrike
#event_simpleName=/^(ProcessRollup2|ClassifiedModuleLoad|PackedExecutableModuleLoad|\w+Written)$/
| test(@timestamp >= 1791702000000)
| test(@timestamp < 1794294000000)
| SHA1HashData=/(?i)(4A3C263977417FB6977B0E598C9E3BC5924F390C|7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3|AFE448DD95494CD14438F04F0C5F6B384664A3B7|4A65040646E354303F067D47AA1C953D467F65CB|D1E17520A2435EDCDCC47F582381416FBDF75F94|7AB784D0049890D9844AEE7F214BA173E3CC23BE|5D80118B1EE974936322475F740AB3AEFF70B8F3|D51C58C2E95D4561848E0A5A8B9FB33C847C50A4|5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B|1408A8F62B09A5AD0DFF279B2878D8F59F70A330|3D1A534CE0D0C293F62ECE556CBC6D0728213104|AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387|3EA03188DBA8898D97E965732C1D6A68C5437EDC|3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F|EB9BA1DA5363F5D04E2F98673595FCE93F39A78E|0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60|A4E477CE257F2C635401EF554E05506C080967C0|A5BE8706032A0798FD63214A9F22EFA6309355F5)/ or @rawstring=/(?i)(4A3C263977417FB6977B0E598C9E3BC5924F390C|7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3|AFE448DD95494CD14438F04F0C5F6B384664A3B7|4A65040646E354303F067D47AA1C953D467F65CB|D1E17520A2435EDCDCC47F582381416FBDF75F94|7AB784D0049890D9844AEE7F214BA173E3CC23BE|5D80118B1EE974936322475F740AB3AEFF70B8F3|D51C58C2E95D4561848E0A5A8B9FB33C847C50A4|5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B|1408A8F62B09A5AD0DFF279B2878D8F59F70A330|3D1A534CE0D0C293F62ECE556CBC6D0728213104|AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387|3EA03188DBA8898D97E965732C1D6A68C5437EDC|3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F|EB9BA1DA5363F5D04E2F98673595FCE93F39A78E|0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60|A4E477CE257F2C635401EF554E05506C080967C0|A5BE8706032A0798FD63214A9F22EFA6309355F5)/
| table([@timestamp, aid, #event_simpleName, FileName, ImageFileName, TargetFileName, SHA1HashData, SHA256HashData])Microsoft KQL
union withsource=SourceTable DeviceFileEvents, DeviceImageLoadEvents, DeviceProcessEvents
| where Timestamp >= datetime(2026-10-11T07:00:00Z) and Timestamp < datetime(2026-11-10T07:00:00Z)
| where SHA1 in~ ("4A3C263977417FB6977B0E598C9E3BC5924F390C", "7742C6B0AAF114335CEF7EBAB11290CB18A0B8B3", "AFE448DD95494CD14438F04F0C5F6B384664A3B7", "4A65040646E354303F067D47AA1C953D467F65CB", "D1E17520A2435EDCDCC47F582381416FBDF75F94", "7AB784D0049890D9844AEE7F214BA173E3CC23BE", "5D80118B1EE974936322475F740AB3AEFF70B8F3", "D51C58C2E95D4561848E0A5A8B9FB33C847C50A4", "5B94FE76EC4FEB0B5CB6EA3BCA4C48D2FE22597B", "1408A8F62B09A5AD0DFF279B2878D8F59F70A330", "3D1A534CE0D0C293F62ECE556CBC6D0728213104", "AD85FBF2CC879D4325BCE1E6FB5F12AB59E3D387", "3EA03188DBA8898D97E965732C1D6A68C5437EDC", "3FBA18AF966DEACF7AE09E4D4238952DD90A6D6F", "EB9BA1DA5363F5D04E2F98673595FCE93F39A78E", "0B4E4A1CE7FEE91BFC62B2A2EC572C325642BB60", "A4E477CE257F2C635401EF554E05506C080967C0", "A5BE8706032A0798FD63214A9F22EFA6309355F5")
| project Timestamp, DeviceName, SourceTable, ActionType, FolderPath, FileName, SHA1, SHA256, InitiatingProcessFileName, ProcessCommandLineWhat the results mean and when to escalate
Start with coverage. A missing Exchange host, or one with process events but no file, image-load or IIS data, is unproven.
Treat a new ncobjapi.dll or wbemcomn.dll in System32\wbem, written outside a Windows update or TrustedInstaller, as a strong lead, especially if WmiPrvSE.exe then loads it or a .NET runtime. Treat version.dll in that folder the same way; ESET did not publish a path for it. Escalate a NativeImages write by a process other than ngen.exe or mscorsvw.exe when Exchange later loads that image.
A hit on one of the five URL prefixes is not normal Outlook Web Access, Autodiscover or ActiveSync traffic. Isolate the server and pull HTTP logs; Falcon and a Defender-only workspace will miss them unless IIS was ingested.
Escalate remote threads into EdgeTransport.exe, unsigned modules in that process or the search service, or a .NET runtime or .ni load in WmiPrvSE.exe, after you rule out your EDR and backup tools. A .ni load in EdgeTransport.exe or the search service matters when that image was written by something other than ngen.exe or mscorsvw.exe.
LSA secrets access, or Exchange processes opening lsass.exe, is credential theft until an admin explains it. A published SHA-1 match is enough to isolate.
After a confirmed hit, find which mail was staged, which credentials sat on the server, and whether CerealHermit left command files on disk. A quiet egress log does not clear the box.
Protect the Exchange servers and the telemetry behind them
Inception Protection helps teams review the endpoint and Microsoft telemetry from on-prem and hybrid Exchange, and treats a silent sensor as a gap, not a clean result. If those servers sit in a hybrid tenant, start with a free Inception Foresight M365 Assessment.



