Microsoft Exchange CVE-2026-42897: KEV OWA Mitigation Exposure
CISA added Exchange Server CVE-2026-42897 to KEV on 2026-05-15. MSRC marks exploitation detected and points to Exchange Emergency Mitigation Service mitigation ID M2 rather than a normal update table.
On this page 0% read
Executive Summary
CISA added CVE-2026-42897 to KEV on 2026-05-15 with a due date of 2026-05-29 CISA KEV. MSRC marks exploitation detected and describes a crafted-email path where OWA user interaction can execute arbitrary JavaScript in the browser context MSRC.
MSRC does not provide a normal update table in the public advisory. The closure anchor is Exchange Emergency Mitigation Service mitigation ID M2 and evidence that the service is running, connected, and not blocked Microsoft Learn.
Key Facts
cve: "CVE-2026-42897"
vendor: "Microsoft"
product: "Exchange Server OWA"
kev_added: "2026-05-15"
kev_due: "2026-05-29"
vulnerability: "Cross-site scripting / spoofing via crafted email opened in Outlook Web Access"
cwe: ["CWE-79"]
affected_products:
- "Exchange Server 2016"
- "Exchange Server 2019"
- "Exchange Server Subscription Edition"
mitigation: "Exchange Emergency Mitigation Service mitigation ID M2"
permanent_update_table: "not_available_as_of_msrc_publication"
cvss_v31: "8.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"
msrc_exploited: true
msrc_publicly_disclosed: false
Source Confidence & Evidence Mapping
- confirmed: CISA KEV lists CVE-2026-42897 as known exploited CISA KEV.
- confirmed: MSRC marks exploitation detected and describes the OWA crafted-email interaction path MSRC.
- confirmed: Microsoft documents Exchange Emergency Mitigation Service and the Exchange mitigation scripts used for service and mitigation visibility Microsoft Learn.
- confirmed: NVD lists CWE-79 and CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:NNVD.
Impact Determination
| Classification | Criteria | Required evidence | Remediation trigger | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | OWA telemetry or mailbox audit evidence shows crafted-email interaction and follow-on mailbox/session activity while M2 is absent, blocked, or failed. | Exchange version, OWA logs, EEMS state, mailbox audit rows, timestamp, and user identity. | Preserve HttpProxy OWA logs, EEMS logs, mailbox audit logs, and suspect messages. | M2 is applied and downstream OWA/mailbox audit has no unexplained access or permission changes. |
| Presumed exposed | Exchange 2016, 2019, or Subscription Edition with OWA exposure has EEMS disabled, disconnected, blocked, or missing M2. | Get-ExchangeServer, MSExchangeMitigation service state, and mitigation script output. | Keep the server in scope until EEMS M2 is verified. | EEMS is running, connectivity succeeds, and M2 is present and not blocked. |
| Potentially exposed | Exchange server exists but OWA exposure, EEMS state, or mitigation output is missing. | Exchange Management Shell, CMDB, scanner, or proxy evidence naming Exchange/OWA. | Collect Exchange and EEMS outputs. | Server resolves to confirmed compromise, presumed exposed, not exposed, or unknown. |
| Not exposed | No affected Exchange server or OWA surface is present, or M2 is verified on the server. | Negative asset evidence or mitigation verification output. | None for this CVE. | Evidence is attached to the server record. |
| Unknown | Exchange shell, EEMS logs, or OWA logs are unavailable. | Gap statement naming unavailable sources. | Keep internet-facing Exchange OWA servers in scope. | Evidence is recovered or the risk owner accepts the named gap. |
Timeline
- 2026-05-14: MSRC publishes CVE-2026-42897 with exploitation detected MSRC.
- 2026-05-15: CISA adds CVE-2026-42897 to KEV with due date 2026-05-29 CISA KEV.
- 2026-05-18: MSRC updates FAQ information for CVE-2026-42897 MSRC.
What Happened
The public handling path is mitigation verification, not a package update. MSRC says EEMS provides mitigation automatically when enabled and identifies mitigation M2 as the required control path for CVE-2026-42897 MSRC.
Technical Analysis
The exploitation path requires a crafted email and OWA interaction conditions. Because the payload executes JavaScript in the browser context, mailbox session activity and OWA proxy logs are more useful than host-only process telemetry.
Affected Assets and Blast Radius
asset_selectors:
- "Exchange Server 2016"
- "Exchange Server 2019"
- "Exchange Server Subscription Edition"
- "Outlook Web Access"
- "OWA"
mitigation_selectors:
- "M2"
- "MSExchangeMitigation"
- "MitigationsApplied"
- "MitigationsBlocked"
data_at_risk:
- "OWA browser sessions"
- "mailbox access"
- "delegated mailbox permissions"
- "message access through affected OWA sessions"
Indicators And Detection Selectors
cves: ["CVE-2026-42897"]
mitigation_id: "M2"
services: ["MSExchangeMitigation"]
paths:
- "Logging/MitigationService"
- "Logging/HttpProxy/Owa"
telemetry_selectors:
- "Outlook Web Access"
- "OWA"
- "MitigationsApplied"
- "MitigationsBlocked"
- "M2"
Detection and Hunting
Script: local repository and exported telemetry scope
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
LOG_ROOT = os.environ.get("LOG_ROOT", "")
OUT = Path(os.environ.get("OUT", "hp-microsoft-exchange-cve-2026-42897-kev-scope"))
SINCE = "2026-05-26T00:00:00Z"
UNTIL = "2026-05-26T23:59:59Z"
PACKAGES = [
]
VERSIONS = [
]
FILES = [
]
DOMAINS = [
"www.cisa.gov",
"msrc.microsoft.com",
"learn.microsoft.com",
"nvd.nist.gov",
]
URLS = [
"https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
"https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2026-42897",
"https://learn.microsoft.com/en-us/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-emergency-mitigation-service",
"https://nvd.nist.gov/vuln/detail/CVE-2026-42897",
]
IPS = [
]
HASHES = [
]
PROCESS_PATTERNS = [
]
NETWORK_PATTERNS = [
]
# Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
# Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
OUT.mkdir(parents=True, exist_ok=True)
indicators_file = OUT / "indicators.txt"
# Collect unique indicators
indicators = set()
for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
for val in group:
if val:
indicators.add(val)
with open(indicators_file, "w") as f:
for ind in sorted(indicators):
f.write(ind + "\n")
print(f"[+] Written unique selectors to {indicators_file}")
# Walk local directory
print(f"[+] Scanning directory: {ROOT} for selectors...")
matches = []
exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if matches:
(OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
print(f"[!] Found {len(matches)} matches in codebase!")
# Optional Log Scanning
if LOG_ROOT and os.path.exists(LOG_ROOT):
print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
log_matches = []
for root, _, filenames in os.walk(LOG_ROOT):
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
log_matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if log_matches:
(OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
print(f"[!] Found {len(log_matches)} matches in logs!")
if PACKAGES:
registry_dir = OUT / "registry"
registry_dir.mkdir(exist_ok=True)
print(f"[+] Wrote scope artifacts under {OUT}")
Patch, Mitigation, and Verification
$ErrorActionPreference = "Stop"
$Out = $env:OUT
if ([string]::IsNullOrWhiteSpace($Out)) { $Out = "hp-exchange-cve-2026-42897-closure" }
New-Item -ItemType Directory -Force -Path $Out | Out-Null
$Cve = "CVE-2026-42897"
$MitigationId = "M2"
$ExchangePath = $env:ExchangeInstallPath
if ([string]::IsNullOrWhiteSpace($ExchangePath)) {
throw "ExchangeInstallPath is not set. Run in Exchange Management Shell."
}
$Scripts = Join-Path $ExchangePath "Scripts"
if (Test-Path (Join-Path $Scripts "Get-Mitigations.ps1")) {
& (Join-Path $Scripts "Get-Mitigations.ps1") | Out-File -Encoding utf8 -FilePath (Join-Path $Out "get-mitigations.txt")
}
if (Test-Path (Join-Path $Scripts "Test-MitigationServiceConnectivity.ps1")) {
& (Join-Path $Scripts "Test-MitigationServiceConnectivity.ps1") | Out-File -Encoding utf8 -FilePath (Join-Path $Out "test-mitigation-service-connectivity.txt")
}
Get-ExchangeServer | Select-Object Name, MitigationsEnabled, MitigationsApplied, MitigationsBlocked |
ConvertTo-Json -Depth 5 | Out-File -Encoding utf8 -FilePath (Join-Path $Out "exchange-m2-state.json")
[pscustomobject]@{
cve = $Cve
required_mitigation_id = $MitigationId
msrc_source = "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2026-42897"
eems_source = "https://learn.microsoft.com/en-us/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-emergency-mitigation-service"
remediation_trigger = "MSExchangeMitigation stopped, EEMS connectivity failure, M2 absent, or M2 blocked keeps CVE-2026-42897 open."
} | ConvertTo-Json | Out-File -Encoding utf8 -FilePath (Join-Path $Out "exchange-cve-2026-42897-closure-metadata.json")
# Remediation trigger: MSExchangeMitigation stopped, EEMS connectivity failure, M2 absent, or M2 blocked keeps CVE-2026-42897 open.
Write-Host "wrote $Out"
Downstream Abuse Audits
Script: GitHub organization run, release, secret, and workflow audit
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
if "ORG" not in os.environ:
print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
sys.exit(1)
ORG = os.environ["ORG"]
SINCE = "2026-05-26T00:00:00Z"
UNTIL = "2026-05-26T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-microsoft-exchange-cve-2026-42897-kev-github-audit"))
SELECTORS = [
"www.cisa.gov",
"msrc.microsoft.com",
"learn.microsoft.com",
"nvd.nist.gov",
"https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
"https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2026-42897",
"https://learn.microsoft.com/en-us/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-emergency-mitigation-service",
"https://nvd.nist.gov/vuln/detail/CVE-2026-42897",
]
# Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
# Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "runs").mkdir(exist_ok=True)
(OUT / "logs").mkdir(exist_ok=True)
(OUT / "repos").mkdir(exist_ok=True)
# 1. Write incident-selectors file
selectors_file = OUT / "incident-selectors.txt"
with open(selectors_file, "w") as sf:
for s in SELECTORS:
if s:
sf.write(s + "\n")
# 2. Get list of repos
print(f"[+] Fetching repositories for organization: {ORG}")
repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
if repo_res.returncode != 0:
print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
sys.exit(1)
repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
for repo in repos:
safe_repo = repo.replace("/", "__")
print(f"[+] Auditing repository: {repo}")
# Check runs in the window
runs_res = subprocess.run([
"gh", "api", f"/repos/{repo}/actions/runs",
"-f", "per_page=100",
"-f", f"created=>={SINCE}",
"--paginate"
], capture_output=True, text=True)
if runs_res.returncode == 0:
try:
all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
if filtered_runs:
with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
for run in filtered_runs:
rf.write(json.dumps(run) + "\n")
# Fetch log dynamically
run_id = str(run["id"])
log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
if log_res.returncode == 0:
(OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
# Fetch details
view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
if view_res.returncode == 0:
(OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
except Exception as e:
print(f"[-] Error parsing runs for {repo}: {e}")
# Check releases in window
subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check repo secrets updated in window
subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check deploy keys
subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
# Scan output directory for any indicator selector matches
print("[+] Scanning gathered telemetry for indicator matches...")
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
print(f"[+] Wrote GitHub audit artifacts under {OUT}")
Script: cloud OIDC and deployment credential follow-on audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-05-26T00:00:00Z"
UNTIL = "2026-05-26T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-microsoft-exchange-cve-2026-42897-kev-cloud-audit"))
AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
# Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
# Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
OUT.mkdir(parents=True, exist_ok=True)
# 1. AWS CloudTrail Audit
print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
aws_events = []
for region in AWS_REGIONS:
res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
"--output", "json"
], capture_output=True, text=True)
if res.returncode == 0:
try:
events = json.loads(res.stdout).get("Events", [])
for event in events:
ct = json.loads(event.get("CloudTrailEvent", "{}"))
ct["region"] = region
aws_events.append(ct)
except Exception as e:
print(f"[-] Error parsing AWS CloudTrail events: {e}")
if aws_events:
with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
for ev in aws_events:
f.write(json.dumps(ev) + "\n")
# Audit follow-on events for returned access keys
for ev in aws_events:
access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
region = ev.get("region", "us-east-1")
if access_key:
print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
f_res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
"--output", "json"
], capture_output=True, text=True)
if f_res.returncode == 0:
try:
f_events = json.loads(f_res.stdout).get("Events", [])
with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
for fe in f_events:
ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
except Exception as e:
print(f"[-] Error writing follow-on events: {e}")
# 2. Azure Activity Log Audit
print("[+] Querying Azure activity logs...")
az_res = subprocess.run([
"az", "monitor", "activity-log", "list",
"--start-time", SINCE,
"--end-time", UNTIL,
"--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
"-o", "json"
], capture_output=True, text=True)
if az_res.returncode == 0:
(OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
# 3. GCP Logging Audit
print("[+] Querying GCP Cloud Logging...")
gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
gcp_res = subprocess.run([
"gcloud", "logging", "read", gcp_filter,
"--format", "json"
], capture_output=True, text=True)
if gcp_res.returncode == 0:
(OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
print(f"[+] Wrote cloud audit artifacts under {OUT}")
Script: registry metadata and artifact audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-05-26T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-microsoft-exchange-cve-2026-42897-kev-registry-audit"))
PACKAGES = [
]
VERSIONS = [
]
# Positive signal: workflow files or extensions reference the affected action/extension names or versions.
# Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
OUT.mkdir(parents=True, exist_ok=True)
with open(OUT / "affected-versions.txt", "w") as av:
for version in VERSIONS:
if version:
av.write(version + "\n")
# 1. Search local workspace files for the affected actions/extensions
print("[+] Scanning workspace workflows for selectors...")
for file in Path(".").glob(".github/workflows/**/*.yml"):
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
# 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
# Overwrite compromised secrets with newly generated credentials:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
# For organization-level secrets:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
# Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
# subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
IOC Clipboard
2 IOCsLogging/MitigationService Logging/MitigationService Logging/HttpProxy/Owa Logging/HttpProxy/Owa