Skip to content
More Critical Bugs In Adobe Experience Manager Pre Auth Rce

More Critical Bugs In Adobe Experience Manager Pre Auth Rce

www.slcyber.io • October 15, 2026

It has been a little while since we last looked at Adobe Experience Manager (AEM). We had some excellent findings last time but were just shy of full remote code execution. This time we were able to achieve full unauthenticated remote code execution on AEM Cloud.

AEM is an exceptionally popular CMS and is widely used amongst large enterprises. Because of the popularity of AEM and recent developments in AI driven vulnerability research we decided to take another look at the product.

There has been a lot of hype around the use of AI tooling in the cybersecurity space. But, we should note that the bulk of this article was written at the beginning of 2026. The models we have been using have improved quite a bit since then. So, while we cover some of the successes and failures of the tooling in this article, it's likely many of the newer, more capable models would not have had the same issues.

The full unauthenticated RCE chain we show below is only applicable for the cloud version and has been patched. However, many steps in the chain are high impact vulnerabilities on their own and as such on-premises versions should be upgraded as well. In this article we detail the following findings:

VULN-34973 — Unauthenticated DAM bulk import

VULN-34975 — Stored XSS via DownloadServlet inline selector

VULN-34976 — XXE in XmlSchemaReader , in-band via CreateCoralTreeServlet

VULN-34977 — Groovy execution via /bin/asynccommand

AEM is a little unique in how it handles requests and data, so it is helpful to give a brief overview of some of the key components. This is all covered in much greater detail in our post on AEM .

The first component is the dispatcher. This is an Apache web server with a custom submodule deployed. The dispatcher submodule provides caching, loading balancing and, most importantly, request filtering. The dispatcher will often block access to many of the available endpoints and some kind of dispatcher bypass is usually required in order to exploit AEM.

The component is the actual AEM application which is a Java bundle deployed to a Jetty webserver. AEM can run in either author or publish mode. Author mode is typically only accessible to developers and content authors. Publish mode is accessible on the public internet (behind the dispatcher) and serves up the site content.

Author mode always requires authentication, whereas publish mode is accessible anonymously but does still perform some authorisation. Both modes deploy the same application, which means that servlets used for authoring content are also available on publish instances but are hidden behind the dispatcher.

The last component is the Java Content Repository (JCR). This is the underlying database used by AEM. The JCR provides a tree structure where content and configuration are stored on the nodes of the tree. All content in JCR is accessible via a path, for example /content/ /index.html would traverse the content -> -> index nodes and render the final node index using some kind of HTML servlet. A key primitive used as part of this chain and in our work is the ability to create a node in the JCR without authentication.

The web framework used by AEM is Apache Sling. Sling has a novel approach to parsing URLs, they are split up as follows:

Resource Path - The longest path matched to a resource in the JCR or the full URI

Selectors - Flags separated by dots after the Resource Path

Extension - The last selector

Suffix - If there is a slash after the Resource Path, Selectors and Extension the path from that slash onwards is the Suffix

Path Parameters - Extra variables in the form ;key=value or ;key='value' are allowed immediately after the Resource Path or the Extension

Sling uses this parsing to route requests, a decision is made based on the best match of servlet to request. For example, a request to /index.foo.bar.json would match a servlet

We began our audit as usual, setting up a local instance of AEM in author mode and decompiling the roughly 650 jar bundles it unpacks. We then took this output and provided it to the latest AI model available to us at the time which was Opus 4.6. We asked the agent to "look through all the servlets and identify any with potentially high impact, preauthentication vulnerabilities". This was an expensive query and it yielded twenty potential findings.

We reviewed each finding manually and for any that showed potential, we asked the agent to attempt to verify it against our test instance. This was quite a time consuming process. We would often have to nudge the agent back on track if it started going down a path we knew led nowhere.

Unfortunately at the end of this process we had few small issues that required specific configuration and a lot of false positives. Not nothing, but also not the cornucopia of critical vulnerabilities we, perhaps naively, expected to come out of the agent's audit. Still, it was an impressive outcome and it gave us a few things to look into manually to see if we could make any further progress.

Going through some of the issues brought us to something we looked at quite a bit last time, the BulkImportConfigServlet . The endpoint offered by this servlet uses a service account to create a configuration node in the JCR. Our exploit relied on setting arbitrary properties on this node, however Adobe had fixed that by restricting the list of permitted properties. Luckily for us, no additional authorisation was applied as part of the patch.

The servlet is part of the bulk import tool which is used to import assets into AEM from a service such as Dropbox, S3 or Azure. From what we can tell, the feature is only available on AEM Cloud. There is no documentation explaining how properly enable it for the on-premises version of AEM.

We did try to actually use this feature in 2025, but were unsuccessful. This time we were able to get it working locally, but our configuration was very hacky. Once we finally got the feature enabled, we were able to go through the available import sources.

The one we were able to get working quickest was S3. Working through it with a debugger we learnt that AEM would request a listing from S3, transfer all items to its own Azure blob store and then create nodes for each item. This is why we had so much trouble before, the feature only worked if AEM was configured to use an Azure blob store.

The import was performed using the createrendition service. For the most impact, we wanted to find a location that was writable by createrendition and readable by everyone . The createrendition service is composed of multiple user principals and we used the following JCR query to list all the nodes that were writable.

And another to list all the nodes that were readable by everyone .

Going through these two lists we identified /content/dam/collections/public as a good location to put our imported assets.

To run the import, we first created the configuration with the following request. Note that we were testing against a local author instance and so a login-token was still required at this stage.

The import was then triggered with the following request.

Things were looking good, but before we continued we needed to confirm one more thing.

A Dispatcher Roadblock

Until this point, our testing was done directly against the Jetty server running AEM in author mode as this is very fast to setup. But for our unauthenticated import to have any impact, we would need to get it working on a real deployment, this meant bypassing the dispatcher. Our exploit took two requests, the POST to create the configuration and the GET to run the job.

The GET request was easy, we could use the same bypass from our work last year. The default dispatcher configuration included the following filter rule:

Any GET request is permitted if it has one of the specified extensions. The dispatcher does not parse path parameters, but AEM does. As such the following request

is seen by the dispatcher as

Resource Path: /conf/global/settings/dam/import/import00

Selectors: importJob , json;x='

Resource Path: /conf/global/settings/dam/import/import00

Since the dispatcher sees it as a GET request with the extension .css , this passes through without issue.

The POST request required a bit more work, of the applicable filter rules, we went with the following.

This rule matched on /url instead of /path which meant none of the Sling decomposition rules were applied and the dispatcher would try to match the full URL. Therefore, we could send through the following request

and AEM would see it as

Resource Path: /content/x

Selectors: bulkimportConfig

Since BulkImportConfigServlet did not consider suffix our request hit the servlet with zero issues.

Back to the Bulk Import

Well, not quite "zero issues". The request above hit the servlet but always resulted in a 500 exception being thrown with an out of bounds error from java.lang.String.substring .

After a bit of digging, we found the culprit. To determine the name of the configuration node, BulkImportConfigServlet looks at the whole URL and extracts everything from the last slash to the last instance of .bulkimportConfig . The function below is responsible:

This meant we had to tweak our request to the following:

Now, when the request was parsed, the first bulkimportConfig would be used as the selector to route the request and the second bulkimportConfig in the suffix would be used to extract the config name.

We also had one other issue. The import was only successful if we were authenticated, even though the job executor used a service account to read the configuration. Again a niche quirk of Sling was the culprit. When the import is triggered the ImportJobServlet places the current resource path on the job it creates and queues in the job system.

Remember that the resource path is the longest path matched to a resource in the JCR or the full URI if there is no match. For an authenticated user this is our configuration node, the exact node name:

However, an unauthenticated user does not have access to that node, so the path does not match. Instead, an unauthenticated user gets the full URI as the resource path which for our request was

When the job executes it would look up import00.importJob.json , be unable to find it and then stop.

Luckily for us it was an easy fix, we could force BulkImportConfigServlet to create a configuration node called import00.importJob.json . We now had our final requests, we would create a job with the request below.

And then trigger it as follows.

Lastly, we could verify our import worked by requesting our imported content.

We were pleased, but unfortunately, the Content-Disposition header meant cross-site scripting was not possible. We would need to find another vulnerability to escalate the issue further to have a meaningful impact.

Another Round of the AI Slot Machine

Creating JCR content was a very useful primitive as it expanded the attack surface significantly. AEM has over a hundred servlets tagged sling/servlet/default , which means they can be applied to any resource. Usually these servlets are not exploitable because they operate on the contents of the resource. With our content import we could now revisit all these servlets and look for new exploits.

We asked the AI agent to perform an audit of this new attack surface. Unfortunately, in this case the AI agent did not find much in terms of vulnerabilities, however it was effective at shortlisting servlets that processed content from the resource.

Going through this shortlist manually we identified com.adobe.cq.wcm.core.components.internal.servlets.DownloadServlet as a potential target. This servlet had an optional inline selector that was checked manually, outside of the usual Sling routing process.

If the request included the inline selector the Content-Disposition was changed from attachment to inline . This can be seen below.

This meant persistent cross-site scripting was now possible. The inline value ensured that browsers would now display the content as a regular web page and execute any script content we included. Our new request became the following:

At this stage we were ready to wrap things up. We decided we would do one final pass in a last-ditch effort to find some way to use our content import primitive. We prompted the AI agent to compile a list of every servlet in the system and to audit each one for any potential security vulnerability.

We hit the token limit almost immediately and had to continue the the following day. Going through the summary and the few dozen markdown files marked as "potential", we encountered many more false positives and duplicates. However, one finding under the unremarkable heading "Gated Edge Cases" we had not seen before, the CreateCoralTreeServlet .

XXE with CreateCoralTreeServlet

CreateCoralTreeServlet was not super interesting on its own. The servlet expected the resource to contain an XML Schema Definition . The servlet would parse the schema, convert it to JSON and then return the JSON response. The actual parsing was delegated to XmlSchemaCollection from xmlschema-core-2.2.3 .

Within XmlSchemaCollection the document is parsed with DTD processing and external entities enabled.

After seeing this, we understood why this issue had been missed previously. Since we did not decompile all nested dependencies any we did for DocumentBuilderFactory would not include this particular class. We were very impressed that the AI agent was able to identify the particular issue, even if it did not fully comprehend the severity.

As the servlet returned the schema as JSON we were able to use the XXE to read files and return them in-band. A DTD would load the file and create an entity which is then inserted into the XML schema as an attribute value. This can be seen below:

After hosting the DTD and running the bulk import to create import00.xsd we were finally able to read the /etc/passwd file of our AEM publish container.

What was nice this approach is that Java will provide a listing if the path specified is a directory. This makes enumerating files much easier. Running the exploit on an AEM Cloud target we were able poke around the filesystem of the Kubernetes pod running AEM. Access to /proc was restricted, however we still found a whole directory of files containing environment variables.

/var/run/s6/container_environment

We also found credentials for the Azure Blob Storage. The credentials granted read/write access, but appeared to be only used as a rolling backup of the JCR for the AEM publish instance. We were able to connect our own JCR in read-only mode with this blob storage as its backend and dump the full contents, but apart from the admin password hash there was nothing sensitive in there.

The most notable thing we found were several files related to Adobe's OAuth identity management solution (IMS). IMS is how users typically authenticate to AEM Cloud. We found two service accounts and the credentials were duplicated across multiple files.

/mnt/ims-yaml-file/rt-service-user.yaml

Of the files we found, we were only able to authenticate with the one above. The file was a little odd as a private key is not used in any of the popular OAuth authentication flows. After some searching we found this library and this deprecation notice from Adobe stating the following:

Not deterred, we installed the library and wrote a little program to send of the request.

We ran the program and were immediately rewarded with a JWT token, which we were able to use to login to the author instance associated with this AEM tenant.

We were very pleased with this result, access to the author instance was not something we were able to achieve in our research.

Remote Code Execution

During some of our first passes of the codebase we had identified a problematic endpoint /bin/asynccommand . This endpoint let us execute Groovy scripts, but required authentication to be exploitable. It was an interesting finding, but not a high priority to analyse further as we were focused on the preauth attack surface.

However, now that we could login, it was worth revisiting. The endpoint is handled by the AsyncOperationServlet and it supports many operations, however we were only interested in BULK_OPERATION .

The servlet copies the request parameters onto a job which is handled asynchronously by the job system. When the job is executed, the script parameter is retrieved, compiled and executed by GroovyScriptRunner .

The final request was relatively simple, the example below demonstrates executing ping f5l4hjszma9d3177zxiudrg3eukl8kw9.oastify.com .

Since the script was executed asynchronously, it was difficult to retrieve the command output. We were able to use the following script which would take the command output and save it to a JCR node that we could then read.

This way we did not need to run any extra infrastructure to catch callbacks, we could read the results directly.

This was one of our first big projects which made heavy use of the latest AI models. At the time the hype for these tools was reaching incredible highs (it's safe to say it still is). We have definitely had some really good success with these tools on other projects, but for AEM the results were more mixed.

The AI agents were good at identifying potential issues, but not so great when it came to verifying them. This was still helpful as it gave us a much smaller surface to investigate manually. However, something to be cautious of when doing this is to remember that the agents do still miss things. Key findings like in CreateCoralTreeServlet were missed multiple times before the agent found them.

Our experience highlights something that has always been an issue with vulnerability research and which may be even more important in the age of AI assisted research. Very little is said of all the paths considered that do not lead to a vulnerability. People want to read the exciting double-free, heap spray, ROP gadget that led to RCE. Nobody wants a write-up the hundred malloc / free pairs that were audited and deemed secure. The same is true for AI, "frontier model finds kernel exploit" is an article that gets global attention, "we ran a frontier model across a hundred files and then sifted through dozens of false positives" is far less interesting and probably doesn't even get written.

This does not mean that these tools are not useful. AI agents still massively reduce the space, instead of manually auditing all 800 servlets, attention can now be focused on the few dozen that are actually problematic. Perhaps with an unlimited budget the agent could review everything and keep trying until an exploit is found, but in our experience this is not economically viable. Maybe in the future these tools will be cheap enough to run that it doesn't matter. In the meantime it seems the best value comes from targeted guidance and operator oversight.

AEM continues to be a unique and complex target. We noted last time that the strange architecture of the dispatcher, Apache Sling and the JCR was potentially a barrier to significant security research being performed. Perhaps this contributed to the AI agent's difficulty at creating working exploits. AEM does deviate from a lot of common patterns for building web applications, perhaps this esoteric combination of technologies is just not meaningfully represented in the training data.

Fortunately, the security posture of AEM has improved significantly in recent years, making it harder to find these high impact vulnerabilities. We hope this article gives a little bit more insight into the internals of AEM and how to approach it as a security research target. Whether you are using AI tooling or not, understanding the system you are testing is critical to producing high quality outcomes.

Security Researcher at Searchlight Cyber

Explore related Content