This is graffiti in the security office, not a broken front gate
CVE-2026-28703 is a stored XSS bug in the Mails Exchanged Between Users report of ManageEngine Exchange Reporter Plus. The affected range is builds 5801 and below, and the fix landed in build 5802 on 2026-03-19, with public disclosure on 2026-04-03. The vendor says an attacker must already be authenticated and have Exchange administrative privileges to inject a payload that later executes when another user opens the affected report.
The vendor's HIGH 7.3 overstates operational urgency for most enterprises. This is not unauthenticated internet RCE; it is a post-auth, post-privilege, UI-driven bug in a niche admin/reporting console, and the attacker precondition already implies meaningful prior compromise. The presence of an NVD enrichment at 4.8/MEDIUM is closer to reality than the CNA score because the practical friction is the whole story here.
4 steps from start to impact.
Land in the admin plane
- Authenticated access into the environment
- Exchange administrative privileges inside the Exchange organization
- Reachability to the Exchange Reporter Plus web console
- This is not unauthenticated remote exploitation
- Many deployments keep the console internal or VPN-restricted
- MFA, PAM, and admin segmentation should reduce reachable population
305773 is local/Windows agent coverage, not reliable external exploit-path validation.Seed the stored payload
- Ability to influence data rendered in the vulnerable report
- Privileges sufficient to inject content accepted by the report workflow
- The vulnerable sink is one specific report, not every page in the product
- Payload quality matters because modern browsers, CSP, and encoding quirks can break sloppy XSS
- The attacker must understand how this report ingests and stores the relevant fields
Wait for an operator or admin to open the report
- A privileged or semi-privileged product user opens the affected report
- The stored entry remains present until review
- This is a low-volume console with a narrow victim pool
- Some teams rarely open this specific report
- Session timeouts, browser hardening, and limited technician use reduce hit rate
Steal session or act in the victim's console context
- Victim browser executes the injected JavaScript
- Victim session has useful privileges in Exchange Reporter Plus
- Impact depends on who views the page
- Operator-only victims may offer visibility but not full product control
- No direct server-side execution is described by the vendor
The supporting signals.
| In-the-wild status | No public exploitation evidence found in CISA KEV, and no vendor statement or primary-source campaign reporting was found. |
|---|---|
| Proof-of-concept availability | No public PoC located on GitHub/Exploit-DB in source review. That said, this is plain stored XSS, so a custom payload is trivial for a capable operator. |
| EPSS | 0.00023 (~0.023%) from the user-supplied intel, which is extremely low and consistent with the narrow exploit path. |
| KEV status | Not listed in the CISA Known Exploited Vulnerabilities Catalog as of this assessment. |
| CVSS disagreement | Vendor/CNA scores it 7.3 HIGH with PR:L; NVD enrichment shows 4.8 MEDIUM with PR:H/UI:R/S:C. The NVD view better matches the vendor's own impact text that requires Exchange administrative privileges. |
| Affected versions | ManageEngine Exchange Reporter Plus builds 5801 and below; vendor advisory explicitly names build 5802 as fixed. |
| Fixed versions | Patch to build 5802. Vendor service-pack guidance shows 5800-5801 -> 5802 as the direct upgrade path. |
| Exposure and scanning reality | The console is a Windows-hosted web app that uses port 8181 by default. Nessus has coverage via plugin 305773, but it is a local Windows check, which tells you asset state, not internet exposure. |
| Privilege model | ManageEngine documentation says Exchange reporting/content-search collection uses accounts in Domain Admins and Organization Management. Product users are mainly Admin or Operator, and only those users view the report that triggers the bug. |
| Disclosure and reporter | Fixed on 2026-03-19, disclosed on 2026-04-03, and credited to C311 via the Zoho BugBounty program. |
noisgate verdict.
The decisive factor is the attacker prerequisite: this bug already assumes authenticated Exchange administrative access, which means the attacker has crossed multiple control layers before the XSS even matters. That makes this a post-compromise console pivot with a small victim pool, not a mass-exploitable initial-access event.
Why this verdict
- Downgrade from 7.3 because the attacker already needs authenticated Exchange admin privileges; that is not 'low privilege' in any enterprise sense and implies prior compromise or delegated high-trust access.
- Further downward pressure: user interaction is mandatory; someone with report access must open one specific report for the payload to execute.
- Reachable population is narrow; Exchange Reporter Plus is a specialized internal admin/reporting console, usually used by a small helpdesk or messaging team rather than exposed broadly to the internet.
- Detection and exposure tooling reflect niche reality; the main scanner coverage found is a local Windows Nessus plugin, not broad internet-wormable exploit pressure.
- Impact is real but bounded; stored XSS can hijack a victim session and abuse product actions, but there is no vendor claim of server-side code execution or pre-auth takeover.
Why not higher?
If this were unauthenticated, widely internet-exposed, or known exploited, it would move up fast. It is none of those. The exploit chain requires internal access, authentication, admin-grade privileges, and a victim viewing a specific report, which compounds friction at every stage.
Why not lower?
This is still stored XSS in an administrative console, not harmless reflected UI noise. If a real product admin opens the report, the attacker can inherit that browser session context and take actions that matter operationally, so it should not be ignored as mere backlog trivia.
What to do — in priority order.
- Restrict console reachability — Put Exchange Reporter Plus behind VPN, jump-host, or admin-subnet ACLs if it is reachable more broadly than the messaging team needs. For a MEDIUM verdict there is no mitigation SLA; use this as risk reduction while you schedule patching inside the 365-day remediation window.
- Prune report viewers — Reduce the number of Admin and Operator accounts that can open reports, especially shared or stale technician accounts. Fewer viewers means fewer viable victims for stored XSS, and with no mitigation SLA you should fold this into normal access hygiene rather than emergency change windows.
- Enforce HTTPS and short admin sessions — Enable HTTPS for the console and shorten idle session lifetime where operationally acceptable. This does not fix XSS, but it reduces the value and lifetime of stolen session material while you work toward the vendor patch under the 365-day remediation window.
- Monitor report access and admin actions — Correlate access to the Mails Exchanged Between Users report with follow-on technician or admin activity such as account edits, exports, or configuration changes. There is no mitigation SLA here, but this is useful detective coverage for a post-compromise pivot.
- A perimeter email gateway is not a dependable control here; the vendor frames exploitation around authenticated administrative access inside the product workflow, not commodity user-phishing alone.
- Standard antivirus on the server does not stop browser-side JavaScript executing in a legitimate admin session.
- A generic WAF is weak coverage if the console is internal and the malicious content is already stored in application data; the dangerous render happens after the app serves trusted content to an authenticated user.
Crowdsourced verification payload.
Run this on the Windows host where Exchange Reporter Plus is installed. Invoke from an elevated PowerShell prompt, for example: powershell -ExecutionPolicy Bypass -File .\check-exrp-cve-2026-28703.ps1. Local admin is recommended so the script can read both uninstall hives and service data; the script outputs VULNERABLE, PATCHED, or UNKNOWN.
# check-exrp-cve-2026-28703.ps1
# Detect ManageEngine Exchange Reporter Plus build and assess exposure to CVE-2026-28703.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
$ErrorActionPreference = 'SilentlyContinue'
$TargetBuild = 5802
function Get-BuildFromText {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
# Prefer explicit build tokens like "Build 5801"
$m = [regex]::Match($Text, '(?i)build\s*[:#-]?\s*(\d{4,5})')
if ($m.Success) { return [int]$m.Groups[1].Value }
# Fallback: choose the largest 4-5 digit number in the string
$nums = [regex]::Matches($Text, '(?<!\d)(\d{4,5})(?!\d)') | ForEach-Object { [int]$_.Groups[1].Value }
if ($nums -and $nums.Count -gt 0) {
return ($nums | Sort-Object -Descending | Select-Object -First 1)
}
return $null
}
function Get-InstallEvidence {
$evidence = @()
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($p in $paths) {
Get-ItemProperty $p | Where-Object {
$_.DisplayName -match 'ManageEngine\s+Exchange\s+Reporter\s+Plus'
} | ForEach-Object {
$joined = @($_.DisplayName, $_.DisplayVersion, $_.Publisher, $_.InstallLocation) -join ' '
$build = Get-BuildFromText -Text $joined
$evidence += [pscustomobject]@{
Source = 'UninstallRegistry'
DisplayName = $_.DisplayName
DisplayVersion = $_.DisplayVersion
InstallLocation = $_.InstallLocation
Build = $build
}
}
}
$svc = Get-Service -Name 'ManageEngine Exchange Reporter Plus' -ErrorAction SilentlyContinue
if ($svc) {
$evidence += [pscustomobject]@{
Source = 'Service'
DisplayName = $svc.DisplayName
DisplayVersion = $null
InstallLocation = $null
Build = $null
}
}
return $evidence
}
$evidence = Get-InstallEvidence
if (-not $evidence -or $evidence.Count -eq 0) {
Write-Output 'UNKNOWN - ManageEngine Exchange Reporter Plus not found in uninstall registry or service list.'
exit 2
}
# Prefer entries with a parsed build number
$withBuild = $evidence | Where-Object { $_.Build }
if ($withBuild.Count -gt 0) {
$best = $withBuild | Sort-Object Build -Descending | Select-Object -First 1
if ($best.Build -lt $TargetBuild) {
Write-Output ("VULNERABLE - {0} {1} (parsed build {2}) is below fixed build {3}." -f $best.DisplayName, $best.DisplayVersion, $best.Build, $TargetBuild)
exit 1
}
else {
Write-Output ("PATCHED - {0} {1} (parsed build {2}) meets or exceeds fixed build {3}." -f $best.DisplayName, $best.DisplayVersion, $best.Build, $TargetBuild)
exit 0
}
}
# Product exists but build could not be parsed
$names = ($evidence | ForEach-Object { $_.DisplayName } | Where-Object { $_ } | Select-Object -Unique) -join ', '
Write-Output ("UNKNOWN - Product detected ({0}) but build number could not be parsed. Check the web client License dialog and confirm whether build is below {1}." -f $names, $TargetBuild)
exit 2
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.