critical Threat analysis

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.

#microsoft-exchange#cisa-kev#zero-day#vulnerability-response#owa
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:N NVD.

    Impact Determination

    ClassificationCriteriaRequired evidenceRemediation triggerClosure condition
    Confirmed compromiseOWA 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 exposedExchange 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 exposedExchange 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 exposedNo 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.
    UnknownExchange 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

    1. CISA Known Exploited Vulnerabilities catalog JSON
    2. MSRC CVE-2026-42897
    3. Microsoft Learn: Exchange Emergency Mitigation Service
    4. NVD CVE-2026-42897

    IOC Clipboard

    2 IOCs
    Defang IOCs
    path Logging/MitigationService Logging/MitigationService
    path Logging/HttpProxy/Owa Logging/HttpProxy/Owa