PoC Archive PoC Archive
Medium CVE-2026-42897 unpatched

Exchange Health Checker Outbound Rule Blind Spot (CVE-2026-42897)

by atiilla · 2026-05-15


Metadata

FieldValue
Date Added2026-05-15
Author / Researcheratiilla
CVE / AdvisoryCVE-2026-42897
Categoryweb
SeverityMedium
CVSS Score5.3 (CVSSv3)
StatusResearched
TagsExchange, HealthChecker, IIS, URL-Rewrite, outbound-rules, EOMT, CSP, detection-gap

Affected Target

FieldValue
Software / SystemMicrosoft CSS-Exchange Health Checker (HealthChecker.ps1)
Versions AffectedVersions where Get-URLRewriteRule.ps1 only parses rewrite.rules and not rewrite.outboundRules
Language / PlatformPowerShell / Exchange on Windows Server with IIS
Authentication RequiredYes (administrator/operator running diagnostics)
Network Access RequiredNo

Summary

CVE-2026-42897 describes a diagnostic blind spot in Exchange Health Checker. The analyzer only enumerates inbound IIS URL Rewrite rules and ignores outbound rules. The EOMT mitigation for this CVE installs an outbound Content-Security-Policy rewrite rule (EOMT OWA CSP - outbound), so Health Checker reports omit this mitigation and can produce false-negative mitigation verification results.


Vulnerability Details

Root Cause

Get-URLRewriteRule.ps1 reads only .rewrite.rules across all three parsing paths (web.config, applicationHost per-location, and applicationHost global). It does not read .rewrite.outboundRules. Invoke-AnalyzerIISInformation.ps1 then displays only names from the inbound .rule collection.

Attack Vector

This is an audit/visibility weakness rather than direct code execution. An administrator applies EOMT mitigations and relies on Health Checker output for validation. Because outbound rules are excluded from parsing, the applied mitigation rule remains invisible in the report.

Impact

  • False negatives when verifying EOMT mitigation deployment.
  • Reduced confidence in Health Checker as a sole mitigation-audit source.
  • Incident-response and compliance workflows may miss mitigation evidence unless IIS configs are checked manually.

Environment / Lab Setup

OS:          Windows Server with Exchange/IIS context (or PowerShell lab)
Target:      Health Checker parsing logic for IIS rewrite configuration
Attacker:    N/A (diagnostic blind spot PoC)
Tools:       PowerShell 7+, mock IIS XML in PoC script

Setup Steps

1
pwsh ./poc_cve_2026_42897.ps1

Proof of Concept

Step-by-Step Reproduction

  1. Run poc_cve_2026_42897.ps1 to generate mock IIS configs containing both inbound and outbound rules.
  2. Observe vulnerable-mode output: only inbound rules are surfaced.
  3. Observe patched-mode output: inbound and outbound rules are both surfaced, including EOMT OWA CSP - outbound.
  4. Compare JSON excerpts printed by the script to confirm mitigation visibility gap.

Exploit Code

See poc_cve_2026_42897.ps1 in this folder.

1
2
$rules = $content.configuration.'system.webServer'.rewrite.rules
$displayRewriteRules = ($rules.rule | Where-Object { $_.enabled -ne "false" }).name

Expected Output

[*] Vulnerable path (inbound only):
    Rules found: Redirect to HTTPS
    MISSING: EOMT OWA CSP - outbound

[*] Patched path (inbound + outbound):
    Rules found: Redirect to HTTPS, EOMT OWA CSP - outbound

Screenshots / Evidence

  • screenshots/ — add authorized lab captures of vulnerable vs patched script output

Detection & Indicators of Compromise

SIEM / IDS Rule (example):

Flag Exchange health-audit results where EOMT mitigation is marked missing,
then cross-check IIS rewrite outbound rules from applicationHost.config/web.config.

Remediation

ActionDetail
PatchUpdate Health Checker logic to parse both rewrite.rules and rewrite.outboundRules at all config paths
WorkaroundValidate EOMT outbound rule presence directly in IIS config and/or IIS Manager instead of relying solely on Health Checker
Config HardeningAdd secondary validation checks in operational runbooks to compare Health Checker output against IIS rewrite configuration

References


Notes

Auto-ingested from https://github.com/atiilla/CVE-2026-42897 on 2026-05-15.

poc_cve_2026_42897.ps1
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#!/usr/bin/env pwsh
<#
.SYNOPSIS
    CVE-2026-42897 - Exchange Health Checker Outbound Rewrite Rule Blind Spot
    Proof of Concept

.DESCRIPTION
    Demonstrates that Get-URLRewriteRule.ps1 in Exchange Health Checker only
    reads inbound IIS URL Rewrite rules and silently ignores outbound rules.

    The EOMT mitigation for CVE-2026-42897 deploys its CSP rule as an outbound
    rule ("EOMT OWA CSP - outbound"). Because Health Checker never reads
    outboundRules, the mitigation is invisible to the diagnostic tool - producing
    a false negative for administrators verifying EOMT deployment.

    This script:
      1. Builds mock IIS config XML (web.config and applicationHost.config)
         containing both inbound and outbound rewrite rules.
      2. Runs the VULNERABLE parsing logic (inbound-only, mirrors Health Checker).
      3. Runs the PATCHED parsing logic (inbound + outbound).
      4. Prints a diff so the blind spot is visible.

    Covers all three config paths Health Checker uses:
      - web.config
      - applicationHost.config per-location entry
      - applicationHost.config global system.webServer

.NOTES
    Affected files in CSS-Exchange repo:
      Diagnostics/HealthChecker/Analyzer/Get-URLRewriteRule.ps1       (L49, L72, L97)
      Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 (L442-459)

    Related mitigation:
      Security/src/EOMT/Mitigations/CVE-2026-42897.ps1               (L147-254)

    FOR AUTHORIZED SECURITY RESEARCH ONLY.
    Run on your own systems or in isolated lab environments.
#>

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

# ---------------------------------------------------------------------------
# Mock IIS configuration XML
# ---------------------------------------------------------------------------
# Represents the structure Health Checker parses from a live Exchange server.
# Contains one inbound rule (what Health Checker sees) and one outbound rule
# (the EOMT CSP mitigation - what Health Checker misses).

[xml]$MockWebConfig = @'
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Redirect to HTTPS" enabled="true" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{HTTPS}" pattern="^OFF$" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:0}" redirectType="Permanent" />
        </rule>
      </rules>
      <outboundRules>
        <rule name="EOMT OWA CSP - outbound" enabled="true">
          <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" />
          <action type="Rewrite"
                  value="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none';" />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>
'@

# applicationHost.config with a per-location entry (mirrors Health Checker path 2)
[xml]$MockAppHostConfig = @'
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <location path="Default Web Site/owa">
    <system.webServer>
      <rewrite>
        <rules>
          <rule name="OWA Inbound Block" enabled="true">
            <match url="^(.*)$" />
            <conditions>
              <add input="{REQUEST_URI}" pattern="suspicious_pattern" />
            </conditions>
            <action type="AbortRequest" />
          </rule>
        </rules>
        <outboundRules>
          <rule name="EOMT OWA CSP - outbound" enabled="true">
            <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" />
            <action type="Rewrite"
                    value="default-src 'self'; script-src 'self' 'unsafe-inline';" />
          </rule>
        </outboundRules>
      </rewrite>
    </system.webServer>
  </location>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Global Inbound Rule" enabled="true">
          <match url=".*" />
          <action type="None" />
        </rule>
      </rules>
      <outboundRules>
        <rule name="EOMT OWA CSP - outbound" enabled="true">
          <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" />
          <action type="Rewrite" value="default-src 'self';" />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>
'@

# ---------------------------------------------------------------------------
# Helper: display rule names collected by a parsing path
# ---------------------------------------------------------------------------
function Show-RuleResults {
    param(
        [string]   $PathLabel,
        [string[]] $InboundNames,
        [string[]] $OutboundNames,
        [bool]     $IsVulnerable
    )

    $mode = if ($IsVulnerable) { "VULNERABLE (inbound only)" } else { "PATCHED (inbound + outbound)" }
    Write-Host "`n  [$PathLabel - $mode]" -ForegroundColor Cyan

    if ($InboundNames.Count -gt 0) {
        foreach ($n in $InboundNames) {
            Write-Host "    [INBOUND ] $n" -ForegroundColor Green
        }
    } else {
        Write-Host "    [INBOUND ] (none)" -ForegroundColor DarkGray
    }

    if (-not $IsVulnerable) {
        if ($OutboundNames.Count -gt 0) {
            foreach ($n in $OutboundNames) {
                Write-Host "    [OUTBOUND] $n" -ForegroundColor Yellow
            }
        } else {
            Write-Host "    [OUTBOUND] (none)" -ForegroundColor DarkGray
        }
    } else {
        $outboundCount = $OutboundNames.Count
        Write-Host "    [OUTBOUND] *** $outboundCount rule(s) silently ignored ***" -ForegroundColor Red
        foreach ($n in $OutboundNames) {
            Write-Host "               MISSING: $n" -ForegroundColor Red
        }
    }
}

# ---------------------------------------------------------------------------
# PATH 1: web.config
# Mirrors Get-URLRewriteRule.ps1 L49
# ---------------------------------------------------------------------------
function Test-WebConfigPath {
    Write-Host "`n=== PATH 1: web.config ===" -ForegroundColor White

    # --- Vulnerable logic (exactly as in Health Checker) ---
    $rules    = $MockWebConfig.configuration.'system.webServer'.rewrite.rules
    $inbound  = @($rules.rule | Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outbound = @($MockWebConfig.configuration.'system.webServer'.rewrite.outboundRules.rule |
                  Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "web.config" -InboundNames $inbound -OutboundNames $outbound -IsVulnerable $true

    # --- Patched logic ---
    $inboundFixed  = @($MockWebConfig.configuration.'system.webServer'.rewrite.rules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outboundFixed = @($MockWebConfig.configuration.'system.webServer'.rewrite.outboundRules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "web.config" -InboundNames $inboundFixed -OutboundNames $outboundFixed -IsVulnerable $false
}

# ---------------------------------------------------------------------------
# PATH 2: applicationHost.config - per-location entry
# Mirrors Get-URLRewriteRule.ps1 L72
# ---------------------------------------------------------------------------
function Test-AppHostPerLocationPath {
    Write-Host "`n=== PATH 2: applicationHost.config (per-location) ===" -ForegroundColor White

    $location = $MockAppHostConfig.configuration.location

    # --- Vulnerable logic ---
    $rules    = $location.'system.webServer'.rewrite.rules
    $inbound  = @($rules.rule | Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outbound = @($location.'system.webServer'.rewrite.outboundRules.rule |
                  Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "appHost/location" -InboundNames $inbound -OutboundNames $outbound -IsVulnerable $true

    # --- Patched logic ---
    $inboundFixed  = @($location.'system.webServer'.rewrite.rules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outboundFixed = @($location.'system.webServer'.rewrite.outboundRules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "appHost/location" -InboundNames $inboundFixed -OutboundNames $outboundFixed -IsVulnerable $false
}

# ---------------------------------------------------------------------------
# PATH 3: applicationHost.config - global system.webServer
# Mirrors Get-URLRewriteRule.ps1 L97
# ---------------------------------------------------------------------------
function Test-AppHostGlobalPath {
    Write-Host "`n=== PATH 3: applicationHost.config (global) ===" -ForegroundColor White

    # --- Vulnerable logic ---
    $rules    = $MockAppHostConfig.configuration.'system.webServer'.rewrite.rules
    $inbound  = @($rules.rule | Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outbound = @($MockAppHostConfig.configuration.'system.webServer'.rewrite.outboundRules.rule |
                  Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "appHost/global" -InboundNames $inbound -OutboundNames $outbound -IsVulnerable $true

    # --- Patched logic ---
    $inboundFixed  = @($MockAppHostConfig.configuration.'system.webServer'.rewrite.rules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    $outboundFixed = @($MockAppHostConfig.configuration.'system.webServer'.rewrite.outboundRules.rule |
                       Where-Object { $_.enabled -ne "false" } | Select-Object -ExpandProperty name)
    Show-RuleResults -PathLabel "appHost/global" -InboundNames $inboundFixed -OutboundNames $outboundFixed -IsVulnerable $false
}

# ---------------------------------------------------------------------------
# Simulate what Health Checker JSON output looks like (vulnerable vs patched)
# ---------------------------------------------------------------------------
function Show-HealthCheckerOutputComparison {
    Write-Host "`n=== Health Checker Report Output Comparison ===" -ForegroundColor White

    Write-Host "`n  [Vulnerable Health Checker JSON excerpt]" -ForegroundColor Cyan
    $vulnerableOutput = [PSCustomObject]@{
        IISURLRewrite = [PSCustomObject]@{
            Rules = @("Redirect to HTTPS")   # outbound rule absent
        }
        MitigationStatus = [PSCustomObject]@{
            "EOMT OWA CSP - outbound" = "NOT DETECTED"
            Note = "Outbound rules are not enumerated - EOMT mitigation invisible"
        }
    }
    $vulnerableOutput | ConvertTo-Json -Depth 4

    Write-Host "`n  [Patched Health Checker JSON excerpt]" -ForegroundColor Green
    $patchedOutput = [PSCustomObject]@{
        IISURLRewrite = [PSCustomObject]@{
            InboundRules  = @("Redirect to HTTPS")
            OutboundRules = @("EOMT OWA CSP - outbound")
        }
        MitigationStatus = [PSCustomObject]@{
            "EOMT OWA CSP - outbound" = "DETECTED (enabled)"
            Note = "Outbound rules enumerated - EOMT mitigation visible"
        }
    }
    $patchedOutput | ConvertTo-Json -Depth 4
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

Write-Host @"

  ╔══════════════════════════════════════════════════════════════════════════╗
  ║  CVE-2026-42897 - Exchange Health Checker Outbound Rewrite Blind Spot  ║
  ║                                                                          ║
  ║  Affected:  Get-URLRewriteRule.ps1 (L49, L72, L97)                      ║
  ║             Invoke-AnalyzerIISInformation.ps1 (L442-459)                ║
  ║  Impact:    EOMT OWA CSP outbound mitigation invisible in HC reports    ║
  ║  For authorized security research only.                                  ║
  ╚══════════════════════════════════════════════════════════════════════════╝

"@ -ForegroundColor Magenta

Write-Host "[*] Building mock IIS configuration with inbound + outbound rules..." -ForegroundColor Gray
Write-Host "    Inbound rule:  'Redirect to HTTPS'" -ForegroundColor Gray
Write-Host "    Outbound rule: 'EOMT OWA CSP - outbound'  <-- EOMT mitigation for CVE-2026-42897" -ForegroundColor Gray

Test-WebConfigPath
Test-AppHostPerLocationPath
Test-AppHostGlobalPath
Show-HealthCheckerOutputComparison

Write-Host "`n=== Summary ===" -ForegroundColor White
Write-Host @"

  The three code paths in Get-URLRewriteRule.ps1 each access only:
      .rewrite.rules           (inbound)

  None access:
      .rewrite.outboundRules   (outbound)

  The EOMT mitigation for CVE-2026-42897 creates its CSP rule in outboundRules.
  Running Health Checker after EOMT deployment will NOT show this rule in the
  report. Administrators cannot use Health Checker as a sole verification tool
  for EOMT outbound mitigation status.

  Remediation:
    In Get-URLRewriteRule.ps1 - read both .rewrite.rules and .rewrite.outboundRules
    at all three config paths (L49, L72, L97).
    In Invoke-AnalyzerIISInformation.ps1 - iterate .rule from both collections.

"@ -ForegroundColor Yellow

Write-Host "[!] FOR AUTHORIZED SECURITY RESEARCH ONLY`n" -ForegroundColor DarkRed