high Threat analysis

Microsoft Defender CVE-2026-41091: KEV Engine EoP Exposure

CISA added Microsoft Defender CVE-2026-41091 to KEV on 2026-05-20. MSRC marks exploitation detected and gives the exact fixed Malware Protection Engine version 1.1.26040.8.

#microsoft-defender#cisa-kev#vulnerability-response#windows#privilege-escalation
On this page 0% read

    Executive Summary

    CISA added CVE-2026-41091 to KEV on 2026-05-20 with a due date of 2026-06-03 CISA KEV. MSRC marks exploitation detected and identifies the first fixed Microsoft Malware Protection Engine version as 1.1.26040.8, with 1.1.26030.3008 as the last affected version reference MSRC.

    Key Facts

    cve: "CVE-2026-41091"
    vendor: "Microsoft"
    product: "Microsoft Defender Malware Protection Engine"
    kev_added: "2026-05-20"
    kev_due: "2026-06-03"
    vulnerability: "Link following elevation of privilege"
    cwe: ["CWE-59"]
    affected_versions: ["1.1.26030.3008 <= engine < 1.1.26040.8"]
    fixed_versions: ["1.1.26040.8"]
    cvss_v31: "7.8 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"
    msrc_exploited: true
    msrc_publicly_disclosed: true

    Source Confidence & Evidence Mapping

    • confirmed: CISA KEV lists CVE-2026-41091 as known exploited CISA KEV.
    • confirmed: MSRC marks exploitation detected and lists engine version 1.1.26040.8 as the first fixed version MSRC.
    • confirmed: NVD lists CWE-59 and CVSS vector CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H NVD.

    Impact Determination

    ClassificationCriteriaRequired evidenceRemediation triggerClosure condition
    Confirmed compromiseHost telemetry shows local privilege escalation or SYSTEM-level follow-on activity on a host with affected Defender engine version.Engine version, host, event timestamp, local user context, and privilege-change evidence.Preserve Security, System, Defender Operational, process, service, and task telemetry.Engine is at least 1.1.26040.8 and downstream local-privilege audit has no unexplained events.
    Presumed exposedDefender engine is >= 1.1.26030.3008 and < 1.1.26040.8.Get-MpComputerStatus output or EDR inventory.Keep the host in scope until fixed engine verification succeeds.Version verifier returns in_affected_range: false.
    Potentially exposedMicrosoft Defender is present but engine version is missing.Host inventory or scanner row naming Defender or CVE-2026-41091.Collect Defender engine version.Host resolves to confirmed compromise, presumed exposed, not exposed, or unknown.
    Not exposedDefender engine is absent, disabled in a non-exploitable state per MSRC context, or version is at least 1.1.26040.8.Version output and Defender state.None for this CVE.Evidence is attached to the host record.
    UnknownHost cannot provide Defender version or relevant Windows event exports.Gap statement naming missing hosts or telemetry.Keep high-value Windows hosts in scope.Evidence is recovered or the risk owner accepts the named gap.

    Timeline

    • 2026-05-19: MSRC publishes CVE-2026-41091 with exploitation detected MSRC.
    • 2026-05-20: CISA adds CVE-2026-41091 to KEV with due date 2026-06-03 CISA KEV.
    • 2026-05-20: NVD publication timestamp for CVE-2026-41091 NVD.

    What Happened

    This is a local elevation-of-privilege issue in Microsoft Defender’s Malware Protection Engine. The strongest closure artifact is the engine version: 1.1.26040.8 or newer.

    Technical Analysis

    MSRC describes a link-following vulnerability that allows an authorized local attacker to gain SYSTEM privileges MSRC. The scripts focus on exact engine version and local privilege-change telemetry.

    Affected Assets and Blast Radius

    asset_selectors:
      - "Microsoft Defender"
      - "Microsoft Malware Protection Engine"
      - "CVE-2026-41091"
    version_selectors:
      affected_start: "1.1.26030.3008"
      fixed_engine: "1.1.26040.8"
    windows_event_ids:
      - 4688
      - 4698
      - 4732
      - 7045
    privilege_context:
      - "SYSTEM privileges"
      - "local administrators group changes"
      - "new services"
      - "new scheduled tasks"

    Indicators And Detection Selectors

    cves: ["CVE-2026-41091"]
    product: "Microsoft Defender Malware Protection Engine"
    affected_engine_range: "1.1.26030.3008 <= engine < 1.1.26040.8"
    fixed_engine: "1.1.26040.8"
    telemetry_selectors:
      - "EventID 4688"
      - "EventID 4698"
      - "EventID 4732"
      - "EventID 7045"

    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-defender-cve-2026-41091-kev-scope"))
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.cisa.gov",
      "msrc.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-41091",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-41091",
    ]
    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-defender-cve-2026-41091-closure" }
    New-Item -ItemType Directory -Force -Path $Out | Out-Null
    
    $Cve = "CVE-2026-41091"
    $FixedEngine = [version]"1.1.26040.8"
    $AffectedStart = [version]"1.1.26030.3008"
    $Status = Get-MpComputerStatus
    $Engine = [version]$Status.AMEngineVersion
    $Closed = ($Engine -ge $FixedEngine)
    
    [pscustomobject]@{
      cve = $Cve
      computer = $env:COMPUTERNAME
      engine_version = $Status.AMEngineVersion
      affected_start = "1.1.26030.3008"
      fixed_engine = "1.1.26040.8"
      closure_state = $Closed
      msrc_source = "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2026-41091"
      remediation_trigger = "Engine version below 1.1.26040.8 on a host at or above 1.1.26030.3008 keeps CVE-2026-41091 open."
    } | ConvertTo-Json | Out-File -Encoding utf8 -FilePath (Join-Path $Out "defender-cve-2026-41091-closure.json")
    
    # Remediation trigger: engine version below 1.1.26040.8 on a host at or above 1.1.26030.3008 keeps CVE-2026-41091 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-defender-cve-2026-41091-kev-github-audit"))
    
    SELECTORS = [
      "www.cisa.gov",
      "msrc.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-41091",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-41091",
    ]
    
    # 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-defender-cve-2026-41091-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-defender-cve-2026-41091-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-41091
    3. NVD CVE-2026-41091