diff --git a/README.hyperv b/README.hyperv new file mode 100644 index 0000000..532f64e --- /dev/null +++ b/README.hyperv @@ -0,0 +1,28 @@ +In order to make virt-who connection to Hyper-V work, some steps needs to be done first. + +1. Windows Remote Management must be enabled and HTTP or HTTPS listener must be running. + + Following command can be used on Hyper-V server: + + winrm quickconfig + + +2. Firewall must allow Remote Administration + + Following command can be used on Hyper-V server: + + netsh advfirewall firewall set rule group="Remote Administration" new enable=yes + + +3. Unencrypted connection must be enabled for HTTP (not required for HTTPS) + + Following command can be used on Hyper-V server: + + winrm set winrm/config/service @{AllowUnencrypted="true"} + + +4. Only Basic and NTLM authentication methods are supported + + Verify that at least one of methods Basic or Negotiate is enabled (True) + + winrm get winrm/config/service/auth diff --git a/hyperv.py b/hyperv.py new file mode 100644 index 0000000..2fcb1a3 --- /dev/null +++ b/hyperv.py @@ -0,0 +1,292 @@ + +import sys +import httplib +import urlparse +import base64 +from uuid import uuid1 + +# Import XML parser +try: + from elementtree import ElementTree +except ImportError: + from xml.etree import ElementTree + +import ntlm + +NAMESPACES = { + 's': 'http://www.w3.org/2003/05/soap-envelope', + 'wsa': 'http://schemas.xmlsoap.org/ws/2004/08/addressing', + 'wsman': 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd', + 'wsen': 'http://schemas.xmlsoap.org/ws/2004/09/enumeration' +} + +ENVELOPE = """<?xml version="1.0" encoding="UTF-8"?> +<s:Envelope """ + " ".join(('xmlns:%s="%s"' % (k, v) for k, v in NAMESPACES.items())) + """> + %s + %s +</s:Envelope>""" + +def getHeader(action): + return """<s:Header> + <wsa:Action s:mustUnderstand="true">""" + NAMESPACES['wsen'] + "/" + action + """</wsa:Action> + <wsa:To s:mustUnderstand="true">%(url)s</wsa:To> + <wsman:ResourceURI s:mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/wmi/%(namespace)s/*</wsman:ResourceURI> + <wsa:MessageID s:mustUnderstand="true">uuid:""" + str(uuid1()) + """</wsa:MessageID> + <wsa:ReplyTo> + <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address> + </wsa:ReplyTo> + </s:Header>""" + +ENUMERATE_BODY = """<s:Body> + <wsen:Enumerate> + <wsman:Filter Dialect="http://schemas.microsoft.com/wbem/wsman/1/WQL">%(query)s</wsman:Filter> + </wsen:Enumerate> + </s:Body>""" + +PULL_BODY = """<s:Body> + <wsen:Pull> + <wsen:EnumerationContext>%(EnumerationContext)s</wsen:EnumerationContext> + </wsen:Pull> + </s:Body>""" + +ENUMERATE_XML = ENVELOPE % (getHeader("Enumerate"), ENUMERATE_BODY) +PULL_XML = ENVELOPE % (getHeader("Pull"), PULL_BODY) + +class HyperVSoap(object): + def __init__(self, url, connection, headers): + self.url = url + self.connection = connection + self.headers = headers + + def post(self, body): + self.headers["Content-Length"] = "%d" % len(body) + self.connection.request("POST", self.url, body=body, headers=self.headers) + response = self.connection.getresponse() + if response.status == 401: + raise HyperVAuthFailed("Authentication failed") + if response.status != 200: + raise HyperVException("Communication with Hyper-V failed, HTTP error: %d" % response.status) + if response is None: + raise HyperVException("No reply from Hyper-V") + return response + + @classmethod + def _Instance(cls, xml): + def stripNamespace(tag): + return tag[tag.find("}") + 1:] + children = xml.getchildren() + if len(children) < 1: + return None + child = children[0] + properties = {} + for ch in child.getchildren(): + properties[stripNamespace(ch.tag)] = ch.text + return properties + + def Enumerate(self, query, namespace="root/virtualization"): + data = ENUMERATE_XML % { 'url': self.url, 'query': query, 'namespace': namespace } + response = self.post(data) + d = response.read() + xml = ElementTree.fromstring(d) + if xml.tag != "{%(s)s}Envelope" % NAMESPACES: + raise HyperVException("Wrong reply format") + responses = xml.findall("{%(s)s}Body/{%(wsen)s}EnumerateResponse" % NAMESPACES) + if len(responses) < 1: + raise HyperVException("Wrong reply format") + contexts = responses[0].getchildren() + if len(contexts) < 1: + raise HyperVException("Wrong reply format") + + if contexts[0].tag != "{%(wsen)s}EnumerationContext" % NAMESPACES: + raise HyperVException("Wrong reply format") + return contexts[0].text + + def _PullOne(self, uuid, namespace): + data = PULL_XML % { 'url': self.url, 'EnumerationContext': uuid, 'namespace': namespace } + response = self.post(data) + d = response.read() + xml = ElementTree.fromstring(d) + if xml.tag != "{%(s)s}Envelope" % NAMESPACES: + raise HyperVException("Wrong reply format") + responses = xml.findall("{%(s)s}Body/{%(wsen)s}PullResponse" % NAMESPACES) + if len(responses) < 0: + raise HyperVException("Wrong reply format") + + uuid = None + instance = None + + for node in responses[0].getchildren(): + if node.tag == "{%(wsen)s}EnumerationContext" % NAMESPACES: + uuid = node.text + elif node.tag == "{%(wsen)s}Items" % NAMESPACES: + instance = HyperVSoap._Instance(node) + + return uuid, instance + + def Pull(self, uuid, namespace="root/virtualization"): + instances = [] + while uuid is not None: + uuid, instance = self._PullOne(uuid, namespace) + if instance is not None: + instances.append(instance) + return instances + + +class HyperVException(Exception): + pass + +class HyperVAuthFailed(HyperVException): + pass + + +class HyperV: + def __init__(self, logger, url, username, password): + self.logger = logger + self.username = username + self.password = password + + # Parse URL and create proper one + if "//" not in url: + url = "//" + url + parsed = urlparse.urlsplit(url, "http") + if ":" not in parsed[1]: + if parsed[0] == "https": + self.host = parsed[1] + ":5986" + else: + self.host = parsed[1] + ":5985" + else: + self.host = parsed[1] + if parsed[2] == "": + path = "wsman" + else: + path = parsed[2] + self.url = urlparse.urlunsplit((parsed[0], self.host, path, "", "")) + + logger.debug("Hyper-V url: %s" % self.url) + + # Check if we have domain defined and set flags accordingly + user_parts = username.split('\\', 1) + if len(user_parts) == 1: + self.username = user_parts[0] + self.domainname = '' + self.type1_flags = ntlm.NTLM_TYPE1_FLAGS & ~ntlm.NTLM_NegotiateOemDomainSupplied + else: + self.domainname = user_parts[0].upper() + self.username = user_parts[1] + self.type1_flags = ntlm.NTLM_TYPE1_FLAGS + + def connect(self): + if self.url.startswith("https"): + connection = httplib.HTTPSConnection(self.host) + else: + connection = httplib.HTTPConnection(self.host) + + headers = {} + headers["Connection"] = "Keep-Alive" + headers["Content-Length"] = "0" + + connection.request("POST", self.url, headers=headers) + response = connection.getresponse() + response.read() + if response.status == 200: + return connection, headers + elif response.status == 404: + raise HyperVException("Invalid HyperV url: %s" % self.url) + elif response.status != 401: + raise HyperVException("Unable to connect to HyperV at: %s" % self.url) + # 401 - need authentication + + authenticate_header = response.getheader("WWW-Authenticate", "") + if 'Negotiate' in authenticate_header: + try: + self.ntlmAuth(connection, headers) + except HyperVAuthFailed: + if 'Basic' in authenticate_header: + self.basicAuth(connection, headers) + else: + raise + elif 'Basic' in authenticate_header: + self.basicAuth(connection, headers) + else: + raise HyperVAuthFailed("Server doesn't known any supported authentication method") + return connection, headers + + def ntlmAuth(self, connection, headers): + self.logger.debug("Using NTLM authentication") + # Use ntlm + headers["Authorization"] = "Negotiate %s" % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.username, self.type1_flags) + + connection.request("POST", self.url, headers=headers) + response = connection.getresponse() + response.read() + if response.status != 401: + raise HyperVAuthFailed("NTLM negotiation failed") + + auth_header = response.getheader("WWW-Authenticate", "") + if auth_header == "": + raise HyperVAuthFailed("NTLM negotiation failed") + + nego, challenge = auth_header.split(" ") + if nego != "Negotiate": + print >>sys.stderr, "Wrong header: ", auth_header + sys.exit(1) + + nonce, flags = ntlm.parse_NTLM_CHALLENGE_MESSAGE(challenge) + headers["Authorization"] = "Negotiate %s" % ntlm.create_NTLM_AUTHENTICATE_MESSAGE(nonce, self.username, self.domainname, self.password, flags) + + connection.request("POST", self.url, headers=headers) + response = connection.getresponse() + response.read() + if response.status == 200: + headers.pop("Authorization") + self.logger.debug("NTLM authentication successful") + else: + raise HyperVAuthFailed("NTLM negotiation failed") + + def basicAuth(self, connection, headers): + self.logger.debug("Using Basic authentication") + + passphrase = "%s:%s" % (self.username, self.password) + encoded = base64.encodestring(passphrase) + headers["Authorization"] = "Basic %s" % encoded.replace('\n', '') + + @classmethod + def decodeWinUUID(cls, uuid): + """ Windows UUID needs to be decoded using following key + From: {78563412-AB90-EFCD-1234-567890ABCDEF} + To: 12345678-90AB-CDEF-1234-567890ABCDEF + """ + if uuid[0] == "{": + s = uuid[1:-1] + else: + s = uuid + return s[6:8] + s[4:6] + s[2:4] + s[0:2] + "-" + s[11:13] + s[9:11] + "-" + s[16:18] + s[14:16] + s[18:] + + def getHostGuestMapping(self): + guests = [] + connection, headers = self.connect() + hypervsoap = HyperVSoap(self.url, connection, headers) + # SettingType == 3 means current setting, 5 is snapshot - we don't want snapshots + uuid = hypervsoap.Enumerate("select BIOSGUID from Msvm_VirtualSystemSettingData where SettingType = 3") + for instance in hypervsoap.Pull(uuid): + guests.append(HyperV.decodeWinUUID(instance["BIOSGUID"])) + uuid = hypervsoap.Enumerate("select UUID from Win32_ComputerSystemProduct", "root/cimv2") + host = None + for instance in hypervsoap.Pull(uuid, "root/cimv2"): + host = HyperV.decodeWinUUID(instance["UUID"]) + return { host: guests } + + def ping(self): + return True + +if __name__ == '__main__': + # TODO: read from config + if len(sys.argv) < 4: + print "Usage: %s url username password" + sys.exit(0) + + import logging + logger = logging.Logger("") + logger.addHandler(logging.StreamHandler()) + hyperv = HyperV(logger, sys.argv[1], sys.argv[2], sys.argv[3]) + print hyperv.getHostGuestMapping() diff --git a/ntlm.py b/ntlm.py new file mode 100644 index 0000000..d950611 --- /dev/null +++ b/ntlm.py @@ -0,0 +1,494 @@ +# This library is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version. + +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/> or <http://www.gnu.org/licenses/lgpl.txt>. + +import struct +import base64 +import string +import hashlib +import hmac +import random +from socket import gethostname + +import M2Crypto + + +# DES handling functions + +def des_encrypt(key_str, plain_text): + k = str_to_key56(key_str) + k = key56_to_key64(k) + key_str = '' + for i in k: + key_str += chr(i & 0xFF) + des = M2Crypto.EVP.Cipher("des_ecb", key=key_str, op=M2Crypto.encrypt, iv='\0'*16) + + return des.update(plain_text) + +def str_to_key56(key_str): + if len(key_str) < 7: + key_str = key_str + '\000\000\000\000\000\000\000'[:(7 - len(key_str))] + key_56 = [] + for i in key_str[:7]: key_56.append(ord(i)) + + return key_56 + +def key56_to_key64(key_56): + "" + key = [] + for i in range(8): key.append(0) + + key[0] = key_56[0]; + key[1] = ((key_56[0] << 7) & 0xFF) | (key_56[1] >> 1); + key[2] = ((key_56[1] << 6) & 0xFF) | (key_56[2] >> 2); + key[3] = ((key_56[2] << 5) & 0xFF) | (key_56[3] >> 3); + key[4] = ((key_56[3] << 4) & 0xFF) | (key_56[4] >> 4); + key[5] = ((key_56[4] << 3) & 0xFF) | (key_56[5] >> 5); + key[6] = ((key_56[5] << 2) & 0xFF) | (key_56[6] >> 6); + key[7] = (key_56[6] << 1) & 0xFF; + + key = set_key_odd_parity(key) + + return key + +def set_key_odd_parity(key): + "" + for i in range(len(key)): + for k in range(7): + bit = 0 + t = key[i] >> k + bit = (t ^ bit) & 0x1 + key[i] = (key[i] & 0xFE) | bit + + return key + +# NTLM implemntation + +NTLM_NegotiateUnicode = 0x00000001 +NTLM_NegotiateOEM = 0x00000002 +NTLM_RequestTarget = 0x00000004 +NTLM_Unknown9 = 0x00000008 +NTLM_NegotiateSign = 0x00000010 +NTLM_NegotiateSeal = 0x00000020 +NTLM_NegotiateDatagram = 0x00000040 +NTLM_NegotiateLanManagerKey = 0x00000080 +NTLM_Unknown8 = 0x00000100 +NTLM_NegotiateNTLM = 0x00000200 +NTLM_NegotiateNTOnly = 0x00000400 +NTLM_Anonymous = 0x00000800 +NTLM_NegotiateOemDomainSupplied = 0x00001000 +NTLM_NegotiateOemWorkstationSupplied = 0x00002000 +NTLM_Unknown6 = 0x00004000 +NTLM_NegotiateAlwaysSign = 0x00008000 +NTLM_TargetTypeDomain = 0x00010000 +NTLM_TargetTypeServer = 0x00020000 +NTLM_TargetTypeShare = 0x00040000 +NTLM_NegotiateExtendedSecurity = 0x00080000 +NTLM_NegotiateIdentify = 0x00100000 +NTLM_Unknown5 = 0x00200000 +NTLM_RequestNonNTSessionKey = 0x00400000 +NTLM_NegotiateTargetInfo = 0x00800000 +NTLM_Unknown4 = 0x01000000 +NTLM_NegotiateVersion = 0x02000000 +NTLM_Unknown3 = 0x04000000 +NTLM_Unknown2 = 0x08000000 +NTLM_Unknown1 = 0x10000000 +NTLM_Negotiate128 = 0x20000000 +NTLM_NegotiateKeyExchange = 0x40000000 +NTLM_Negotiate56 = 0x80000000 + +# we send these flags with our type 1 message +NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | \ + NTLM_NegotiateOEM | \ + NTLM_RequestTarget | \ + NTLM_NegotiateNTLM | \ + NTLM_NegotiateOemDomainSupplied | \ + NTLM_NegotiateOemWorkstationSupplied | \ + NTLM_NegotiateAlwaysSign | \ + NTLM_NegotiateExtendedSecurity | \ + NTLM_NegotiateVersion | \ + NTLM_Negotiate128 | \ + NTLM_Negotiate56 ) +NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | \ + NTLM_RequestTarget | \ + NTLM_NegotiateNTLM | \ + NTLM_NegotiateAlwaysSign | \ + NTLM_NegotiateExtendedSecurity | \ + NTLM_NegotiateTargetInfo | \ + NTLM_NegotiateVersion | \ + NTLM_Negotiate128 | \ + NTLM_Negotiate56) + +NTLM_MsvAvEOL = 0 # Indicates that this is the last AV_PAIR in the list. AvLen MUST be 0. This type of information MUST be present in the AV pair list. +NTLM_MsvAvNbComputerName = 1 # The server's NetBIOS computer name. The name MUST be in Unicode, and is not null-terminated. This type of information MUST be present in the AV_pair list. +NTLM_MsvAvNbDomainName = 2 # The server's NetBIOS domain name. The name MUST be in Unicode, and is not null-terminated. This type of information MUST be present in the AV_pair list. +NTLM_MsvAvDnsComputerName = 3 # The server's Active Directory DNS computer name. The name MUST be in Unicode, and is not null-terminated. +NTLM_MsvAvDnsDomainName = 4 # The server's Active Directory DNS domain name. The name MUST be in Unicode, and is not null-terminated. +NTLM_MsvAvDnsTreeName = 5 # The server's Active Directory (AD) DNS forest tree name. The name MUST be in Unicode, and is not null-terminated. +NTLM_MsvAvFlags = 6 # A field containing a 32-bit value indicating server or client configuration. 0x00000001: indicates to the client that the account authentication is constrained. 0x00000002: indicates that the client is providing message integrity in the MIC field (section 2.2.1.3) in the AUTHENTICATE_MESSAGE. +NTLM_MsvAvTimestamp = 7 # A FILETIME structure ([MS-DTYP] section 2.3.1) in little-endian byte order that contains the server local time.<12> +NTLM_MsAvRestrictions = 8 #A Restriction_Encoding structure (section 2.2.2.2). The Value field contains a structure representing the integrity level of the security principal, as well as a MachineID created at computer startup to identify the calling machine. <13> + + +""" +utility functions for Microsoft NTLM authentication + +References: +[MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol Specification +http://download.microsoft.com/download/a/e/6/ae6e4142-aa58-45c6-8dcf-a657e5900cd3/%5BMS-NLMP%5D.pdf + +[MS-NTHT]: NTLM Over HTTP Protocol Specification +http://download.microsoft.com/download/a/e/6/ae6e4142-aa58-45c6-8dcf-a657e5900cd3/%5BMS-NTHT%5D.pdf + +Cntlm Authentication Proxy +http://cntlm.awk.cz/ + +NTLM Authorization Proxy Server +http://sourceforge.net/projects/ntlmaps/ + +Optimized Attack for NTLM2 Session Response +http://www.blackhat.com/presentations/bh-asia-04/bh-jp-04-pdfs/bh-jp-04-seki.pdf +""" +def dump_NegotiateFlags(NegotiateFlags): + if NegotiateFlags & NTLM_NegotiateUnicode: + print "NTLM_NegotiateUnicode set" + if NegotiateFlags & NTLM_NegotiateOEM: + print "NTLM_NegotiateOEM set" + if NegotiateFlags & NTLM_RequestTarget: + print "NTLM_RequestTarget set" + if NegotiateFlags & NTLM_Unknown9: + print "NTLM_Unknown9 set" + if NegotiateFlags & NTLM_NegotiateSign: + print "NTLM_NegotiateSign set" + if NegotiateFlags & NTLM_NegotiateSeal: + print "NTLM_NegotiateSeal set" + if NegotiateFlags & NTLM_NegotiateDatagram: + print "NTLM_NegotiateDatagram set" + if NegotiateFlags & NTLM_NegotiateLanManagerKey: + print "NTLM_NegotiateLanManagerKey set" + if NegotiateFlags & NTLM_Unknown8: + print "NTLM_Unknown8 set" + if NegotiateFlags & NTLM_NegotiateNTLM: + print "NTLM_NegotiateNTLM set" + if NegotiateFlags & NTLM_NegotiateNTOnly: + print "NTLM_NegotiateNTOnly set" + if NegotiateFlags & NTLM_Anonymous: + print "NTLM_Anonymous set" + if NegotiateFlags & NTLM_NegotiateOemDomainSupplied: + print "NTLM_NegotiateOemDomainSupplied set" + if NegotiateFlags & NTLM_NegotiateOemWorkstationSupplied: + print "NTLM_NegotiateOemWorkstationSupplied set" + if NegotiateFlags & NTLM_Unknown6: + print "NTLM_Unknown6 set" + if NegotiateFlags & NTLM_NegotiateAlwaysSign: + print "NTLM_NegotiateAlwaysSign set" + if NegotiateFlags & NTLM_TargetTypeDomain: + print "NTLM_TargetTypeDomain set" + if NegotiateFlags & NTLM_TargetTypeServer: + print "NTLM_TargetTypeServer set" + if NegotiateFlags & NTLM_TargetTypeShare: + print "NTLM_TargetTypeShare set" + if NegotiateFlags & NTLM_NegotiateExtendedSecurity: + print "NTLM_NegotiateExtendedSecurity set" + if NegotiateFlags & NTLM_NegotiateIdentify: + print "NTLM_NegotiateIdentify set" + if NegotiateFlags & NTLM_Unknown5: + print "NTLM_Unknown5 set" + if NegotiateFlags & NTLM_RequestNonNTSessionKey: + print "NTLM_RequestNonNTSessionKey set" + if NegotiateFlags & NTLM_NegotiateTargetInfo: + print "NTLM_NegotiateTargetInfo set" + if NegotiateFlags & NTLM_Unknown4: + print "NTLM_Unknown4 set" + if NegotiateFlags & NTLM_NegotiateVersion: + print "NTLM_NegotiateVersion set" + if NegotiateFlags & NTLM_Unknown3: + print "NTLM_Unknown3 set" + if NegotiateFlags & NTLM_Unknown2: + print "NTLM_Unknown2 set" + if NegotiateFlags & NTLM_Unknown1: + print "NTLM_Unknown1 set" + if NegotiateFlags & NTLM_Negotiate128: + print "NTLM_Negotiate128 set" + if NegotiateFlags & NTLM_NegotiateKeyExchange: + print "NTLM_NegotiateKeyExchange set" + if NegotiateFlags & NTLM_Negotiate56: + print "NTLM_Negotiate56 set" + +def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): + BODY_LENGTH = 40 + Payload_start = BODY_LENGTH # in bytes + protocol = 'NTLMSSP\0' #name + + type = struct.pack('<I',1) #type 1 + + flags = struct.pack('<I', type1_flags) + Workstation = gethostname().upper().encode('ascii') + user_parts = user.split('\\', 1) + DomainName = user_parts[0].upper().encode('ascii') + EncryptedRandomSessionKey = "" + + WorkstationLen = struct.pack('<H', len(Workstation)) + WorkstationMaxLen = struct.pack('<H', len(Workstation)) + WorkstationBufferOffset = struct.pack('<I', Payload_start) + Payload_start += len(Workstation) + DomainNameLen = struct.pack('<H', len(DomainName)) + DomainNameMaxLen = struct.pack('<H', len(DomainName)) + DomainNameBufferOffset = struct.pack('<I',Payload_start) + Payload_start += len(DomainName) + ProductMajorVersion = struct.pack('<B', 5) + ProductMinorVersion = struct.pack('<B', 1) + ProductBuild = struct.pack('<H', 2600) + VersionReserved1 = struct.pack('<B', 0) + VersionReserved2 = struct.pack('<B', 0) + VersionReserved3 = struct.pack('<B', 0) + NTLMRevisionCurrent = struct.pack('<B', 15) + + msg1 = protocol + type + flags + \ + DomainNameLen + DomainNameMaxLen + DomainNameBufferOffset + \ + WorkstationLen + WorkstationMaxLen + WorkstationBufferOffset + \ + ProductMajorVersion + ProductMinorVersion + ProductBuild + \ + VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent + assert BODY_LENGTH==len(msg1), "BODY_LENGTH: %d != msg1: %d" % (BODY_LENGTH,len(msg1)) + msg1 += Workstation + DomainName + msg1 = base64.encodestring(msg1) + msg1 = string.replace(msg1, '\n', '') + return msg1 + +def parse_NTLM_CHALLENGE_MESSAGE(msg2): + "" + msg2 = base64.decodestring(msg2) + Signature = msg2[0:8] + msg_type = struct.unpack("<I",msg2[8:12])[0] + assert(msg_type==2) + TargetNameLen = struct.unpack("<H",msg2[12:14])[0] + TargetNameMaxLen = struct.unpack("<H",msg2[14:16])[0] + TargetNameOffset = struct.unpack("<I",msg2[16:20])[0] + TargetName = msg2[TargetNameOffset:TargetNameOffset+TargetNameMaxLen] + NegotiateFlags = struct.unpack("<I",msg2[20:24])[0] + ServerChallenge = msg2[24:32] + Reserved = msg2[32:40] + TargetInfoLen = struct.unpack("<H",msg2[40:42])[0] + TargetInfoMaxLen = struct.unpack("<H",msg2[42:44])[0] + TargetInfoOffset = struct.unpack("<I",msg2[44:48])[0] + TargetInfo = msg2[TargetInfoOffset:TargetInfoOffset+TargetInfoLen] + i=0 + TimeStamp = '\0'*8 + while(i<TargetInfoLen): + AvId = struct.unpack("<H",TargetInfo[i:i+2])[0] + AvLen = struct.unpack("<H",TargetInfo[i+2:i+4])[0] + AvValue = TargetInfo[i+4:i+4+AvLen] + i = i+4+AvLen + if AvId == NTLM_MsvAvTimestamp: + TimeStamp = AvValue + #~ print AvId, AvValue.decode('utf-16') + return (ServerChallenge, NegotiateFlags) + +def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, NegotiateFlags): + "" + is_unicode = NegotiateFlags & NTLM_NegotiateUnicode + is_NegotiateExtendedSecurity = NegotiateFlags & NTLM_NegotiateExtendedSecurity + + flags = struct.pack('<I',NTLM_TYPE2_FLAGS) + + BODY_LENGTH = 72 + Payload_start = BODY_LENGTH # in bytes + + Workstation = gethostname().upper() + DomainName = domain.upper() + UserName = user + EncryptedRandomSessionKey = "" + if is_unicode: + Workstation = Workstation.encode('utf-16-le') + DomainName = DomainName.encode('utf-16-le') + UserName = UserName.encode('utf-16-le') + EncryptedRandomSessionKey = EncryptedRandomSessionKey.encode('utf-16-le') + LmChallengeResponse = calc_resp(create_LM_hashed_password_v1(password), nonce) + NtChallengeResponse = calc_resp(create_NT_hashed_password_v1(password), nonce) + + if is_NegotiateExtendedSecurity: + pwhash = create_NT_hashed_password_v1(password, UserName, DomainName) + ClientChallenge = "" + for i in range(8): + ClientChallenge+= chr(random.getrandbits(8)) + (NtChallengeResponse, LmChallengeResponse) = ntlm2sr_calc_resp(pwhash, nonce, ClientChallenge) #='\x39 e3 f4 cd 59 c5 d8 60') + Signature = 'NTLMSSP\0' + MessageType = struct.pack('<I',3) #type 3 + + DomainNameLen = struct.pack('<H', len(DomainName)) + DomainNameMaxLen = struct.pack('<H', len(DomainName)) + DomainNameOffset = struct.pack('<I', Payload_start) + Payload_start += len(DomainName) + + UserNameLen = struct.pack('<H', len(UserName)) + UserNameMaxLen = struct.pack('<H', len(UserName)) + UserNameOffset = struct.pack('<I', Payload_start) + Payload_start += len(UserName) + + WorkstationLen = struct.pack('<H', len(Workstation)) + WorkstationMaxLen = struct.pack('<H', len(Workstation)) + WorkstationOffset = struct.pack('<I', Payload_start) + Payload_start += len(Workstation) + + LmChallengeResponseLen = struct.pack('<H', len(LmChallengeResponse)) + LmChallengeResponseMaxLen = struct.pack('<H', len(LmChallengeResponse)) + LmChallengeResponseOffset = struct.pack('<I', Payload_start) + Payload_start += len(LmChallengeResponse) + + NtChallengeResponseLen = struct.pack('<H', len(NtChallengeResponse)) + NtChallengeResponseMaxLen = struct.pack('<H', len(NtChallengeResponse)) + NtChallengeResponseOffset = struct.pack('<I', Payload_start) + Payload_start += len(NtChallengeResponse) + + EncryptedRandomSessionKeyLen = struct.pack('<H', len(EncryptedRandomSessionKey)) + EncryptedRandomSessionKeyMaxLen = struct.pack('<H', len(EncryptedRandomSessionKey)) + EncryptedRandomSessionKeyOffset = struct.pack('<I',Payload_start) + Payload_start += len(EncryptedRandomSessionKey) + NegotiateFlags = flags + + ProductMajorVersion = struct.pack('<B', 5) + ProductMinorVersion = struct.pack('<B', 1) + ProductBuild = struct.pack('<H', 2600) + VersionReserved1 = struct.pack('<B', 0) + VersionReserved2 = struct.pack('<B', 0) + VersionReserved3 = struct.pack('<B', 0) + NTLMRevisionCurrent = struct.pack('<B', 15) + + MIC = struct.pack('<IIII',0,0,0,0) + msg3 = Signature + MessageType + \ + LmChallengeResponseLen + LmChallengeResponseMaxLen + LmChallengeResponseOffset + \ + NtChallengeResponseLen + NtChallengeResponseMaxLen + NtChallengeResponseOffset + \ + DomainNameLen + DomainNameMaxLen + DomainNameOffset + \ + UserNameLen + UserNameMaxLen + UserNameOffset + \ + WorkstationLen + WorkstationMaxLen + WorkstationOffset + \ + EncryptedRandomSessionKeyLen + EncryptedRandomSessionKeyMaxLen + EncryptedRandomSessionKeyOffset + \ + NegotiateFlags + \ + ProductMajorVersion + ProductMinorVersion + ProductBuild + \ + VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent + assert BODY_LENGTH==len(msg3), "BODY_LENGTH: %d != msg3: %d" % (BODY_LENGTH,len(msg3)) + Payload = DomainName + UserName + Workstation + LmChallengeResponse + NtChallengeResponse + EncryptedRandomSessionKey + msg3 += Payload + msg3 = base64.encodestring(msg3) + msg3 = string.replace(msg3, '\n', '') + return msg3 + +def calc_resp(password_hash, server_challenge): + """calc_resp generates the LM response given a 16-byte password hash and the + challenge from the Type-2 message. + @param password_hash + 16-byte password hash + @param server_challenge + 8-byte challenge from Type-2 message + returns + 24-byte buffer to contain the LM response upon return + """ + # padding with zeros to make the hash 21 bytes long + password_hash = password_hash + '\0' * (21 - len(password_hash)) + return des_encrypt(password_hash[ 0: 7], server_challenge[0:8]) + \ + des_encrypt(password_hash[ 7:14], server_challenge[0:8]) + \ + des_encrypt(password_hash[14:21], server_challenge[0:8]) + +def ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge='\xaa'*8, Time='\0'*8): + LmChallengeResponse = hmac.new(ResponseKeyLM, ServerChallenge+ClientChallenge).digest() + ClientChallenge + + Responserversion = '\x01' + HiResponserversion = '\x01' + temp = Responserversion + HiResponserversion + '\0'*6 + Time + ClientChallenge + '\0'*4 + ServerChallenge + '\0'*4 + NTProofStr = hmac.new(ResponseKeyNT, ServerChallenge + temp).digest() + NtChallengeResponse = NTProofStr + temp + + SessionBaseKey = hmac.new(ResponseKeyNT, NTProofStr).digest() + return (NtChallengeResponse, LmChallengeResponse) + +def ntlm2sr_calc_resp(ResponseKeyNT, ServerChallenge, ClientChallenge='\xaa'*8): + LmChallengeResponse = ClientChallenge + '\0'*16 + sess = hashlib.md5(ServerChallenge+ClientChallenge).digest() + NtChallengeResponse = calc_resp(ResponseKeyNT, sess[0:8]) + return (NtChallengeResponse, LmChallengeResponse) + +def create_LM_hashed_password_v1(passwd): + "setup LanManager password" + "create LanManager hashed password" + + # fix the password length to 14 bytes + passwd = string.upper(passwd) + lm_pw = passwd + '\0' * (14 - len(passwd)) + lm_pw = passwd[0:14] + + # do hash + magic_str = "KGS!@#$%" # page 57 in [MS-NLMP] + + return des_encrypt(lm_pw[0:7], magic_str) + des_encrypt(lm_pw[7:14], magic_str) + +def create_NT_hashed_password_v1(passwd, user=None, domain=None): + "create NT hashed password" + digest = hashlib.new('md4', passwd.encode('utf-16le')).digest() + return digest + +def create_NT_hashed_password_v2(passwd, user, domain): + "create NT hashed password" + digest = create_NT_hashed_password_v1(passwd) + + return hmac.new(digest, (user.upper()+domain).encode('utf-16le')).digest() + return digest + +def create_sessionbasekey(password): + return hashlib.new('md4', create_NT_hashed_password_v1(password)).digest() + +if __name__ == "__main__": + def ByteToHex( byteStr ): + """ + Convert a byte string to it's hex string representation e.g. for output. + """ + return ' '.join( [ "%02X" % ord( x ) for x in byteStr ] ) + + def HexToByte( hexStr ): + """ + Convert a string hex byte values into a byte string. The Hex Byte values may + or may not be space separated. + """ + bytes = [] + + hexStr = ''.join( hexStr.split(" ") ) + + for i in range(0, len(hexStr), 2): + bytes.append( chr( int (hexStr[i:i+2], 16 ) ) ) + + return ''.join( bytes ) + + ServerChallenge = HexToByte("01 23 45 67 89 ab cd ef") + ClientChallenge = '\xaa'*8 + Time = '\x00'*8 + Workstation = "COMPUTER".encode('utf-16-le') + ServerName = "Server".encode('utf-16-le') + User = "User" + Domain = "Domain" + Password = "Password" + RandomSessionKey = '\55'*16 + assert HexToByte("e5 2c ac 67 41 9a 9a 22 4a 3b 10 8f 3f a6 cb 6d") == create_LM_hashed_password_v1(Password) # [MS-NLMP] page 72 + assert HexToByte("a4 f4 9c 40 65 10 bd ca b6 82 4e e7 c3 0f d8 52") == create_NT_hashed_password_v1(Password) # [MS-NLMP] page 73 + assert HexToByte("d8 72 62 b0 cd e4 b1 cb 74 99 be cc cd f1 07 84") == create_sessionbasekey(Password) + assert HexToByte("67 c4 30 11 f3 02 98 a2 ad 35 ec e6 4f 16 33 1c 44 bd be d9 27 84 1f 94") == calc_resp(create_NT_hashed_password_v1(Password), ServerChallenge) + assert HexToByte("98 de f7 b8 7f 88 aa 5d af e2 df 77 96 88 a1 72 de f1 1c 7d 5c cd ef 13") == calc_resp(create_LM_hashed_password_v1(Password), ServerChallenge) + + (NTLMv1Response,LMv1Response) = ntlm2sr_calc_resp(create_NT_hashed_password_v1(Password), ServerChallenge, ClientChallenge) + assert HexToByte("aa aa aa aa aa aa aa aa 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00") == LMv1Response # [MS-NLMP] page 75 + assert HexToByte("75 37 f8 03 ae 36 71 28 ca 45 82 04 bd e7 ca f8 1e 97 ed 26 83 26 72 32") == NTLMv1Response + + assert HexToByte("0c 86 8a 40 3b fd 7a 93 a3 00 1e f2 2e f0 2e 3f") == create_NT_hashed_password_v2(Password, User, Domain) # [MS-NLMP] page 76 + ResponseKeyLM = ResponseKeyNT = create_NT_hashed_password_v2(Password, User, Domain) + (NTLMv2Response,LMv2Response) = ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge, Time) + assert HexToByte("86 c3 50 97 ac 9c ec 10 25 54 76 4a 57 cc cc 19 aa aa aa aa aa aa aa aa") == LMv2Response # [MS-NLMP] page 76 + + # expected failure + # According to the spec in section '3.3.2 NTLM v2 Authentication' the NTLMv2Response should be longer than the value given on page 77 (this suggests a mistake in the spec) + #~ assert HexToByte("68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c") == NTLMv2Response, "\nExpected: 68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c\nActual: %s" % ByteToHex(NTLMv2Response) # [MS-NLMP] page 77 diff --git a/virt-who.8 b/virt-who.8 index f26175e..4b2c283 100644 --- a/virt-who.8 +++ b/virt-who.8 @@ -2,7 +2,7 @@ .SH NAME virt-who - Agent for reporting virtual guest IDs to Subscription Asset Manager. .SH SYNOPSIS -virt-who [-d] [-i INTERVAL] [-b] [-o] [--libvirt|--vdsm|--esx|--rhevm] +virt-who [-d] [-i INTERVAL] [-b] [-o] [--libvirt|--vdsm|--esx|--rhevm|--hyperv] .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR @@ -31,6 +31,9 @@ Register ESX machines using vCenter .TP \fB\-\-rhevm\fR Register guests using RHEV\-M +.TP +\fB\-\-hyperv\fR +Register guests using Hyper\-V .IP .SS vCenter/ESX options .IP @@ -69,6 +72,25 @@ Username for connecting to RHEV\-M .TP \fB\-\-rhevm\-password\fR=\fIPASSWORD\fR Password for connecting to RHEV\-M +.IP +.SS Hyper\-V options +.IP +Use this options with \fB\-\-hyperv\fR +.TP +\fB\-\-hyperv\-owner\fR=\fIOWNER\fR +Organization who has purchased subscriptions of the products +.TP +\fB\-\-hyperv\-env\fR=\fIENV\fR +Environment where the Hyper\-V belongs to +.TP +\fB\-\-hyperv\-server\fR=\fISERVER\fR +URL of the Hyper\-V server to connect to +.TP +\fB\-\-hyperv\-username\fR=\fIUSERNAME\fR +Username for connecting to Hyper\-V +.TP +\fB\-\-hyperv\-password\fR=\fIPASSWORD\fR +Password for connecting to Hyper\-V .PP .SH ENVIRONMENT virt-who also reads environmental variables. They have the same name as command line arguments but upper-cased, with underscore instead of dash and prefixed with VIRTWHO_ (e.g. VIRTWHO_ONE_SHOT). Empty variables are considered as disabled, non-empty as enabled @@ -127,6 +149,13 @@ Use ESX (vCenter) as virtualization backend and specify option required to conne Use RHEV-M as virtualization backend and specify option required to connect to RHEV-M server. +.TP +4. Hyper-V + +# virt-who --hyperv --hyperv-owner=HYPERV_OWNER --hyperv-env=HYPERV_ENV --hyperv-server=HYPERV_SERVER --hyperv-username=HYPERV_USERNAME --hyperv-password=HYPERV_PASSWORD + +Use Hyper-V as virtualization backend and specify option required to connect to Hyper-V server. + .SH LOGGING virt-who always writes error output to file /var/log/rhsm/rhsm.log. In all modes, excluding background ("-b"), it writes same output also to the standard error output. diff --git a/virt-who.conf b/virt-who.conf index 043b695..2c117d2 100644 --- a/virt-who.conf +++ b/virt-who.conf @@ -18,7 +18,7 @@ VIRTWHO_DEBUG=0 # configuration. #VIRTWHO_INTERVAL=0 -# virt-who mode, enable only one option from following 4: +# virt-who mode, enable only one option from following 5: # Use libvirt to list virtual guests [default] #VIRTWHO_LIBVIRT=1 # Use vdsm to list virtual guests @@ -27,6 +27,8 @@ VIRTWHO_DEBUG=0 #VIRTWHO_ESX=0 # Register guests using RHEV-M #VIRTWHO_RHEVM=0 +# Register guest using Hyper-V +#VIRTWHO_HYPERV=0 # Option for ESX mode #VIRTWHO_ESX_OWNER= @@ -41,3 +43,10 @@ VIRTWHO_DEBUG=0 #VIRTWHO_RHEVM_SERVER= #VIRTWHO_RHEVM_USERNAME= #VIRTWHO_RHEVM_PASSWORD= + +# Options for HYPER-V mode +#VIRTWHO_HYPERV_OWNER= +#VIRTWHO_HYPERV_ENV= +#VIRTWHO_HYPERV_SERVER= +#VIRTWHO_HYPERV_USERNAME= +#VIRTWHO_HYPERV_PASSWORD= diff --git a/virt-who.py b/virt-who.py index 7b15f14..85d998a 100644 --- a/virt-who.py +++ b/virt-who.py @@ -28,6 +28,7 @@ from virt import Virt, VirtError from vdsm import VDSM from vsphere import VSphere from rhevm import RHEVM +from hyperv import HyperV from event import virEventLoopPureStart from subscriptionmanager import SubscriptionManager, SubscriptionManagerError @@ -98,6 +99,8 @@ class VirtWho(object): self.tryRegisterEventCallback() elif self.options.virtType == "rhevm": self.virt = RHEVM(self.logger, self.options.server, self.options.username, self.options.password) + elif self.options.virtType == "hyperv": + self.virt = HyperV(self.logger, self.options.server, self.options.username, self.options.password) else: # ESX self.virt = VSphere(self.logger, self.options.server, self.options.username, self.options.password) @@ -171,7 +174,7 @@ class VirtWho(object): return False try: - if self.options.virtType not in ["esx", "rhevm"]: + if self.options.virtType not in ["esx", "rhevm", "hyperv"]: virtualGuests = self.virt.listDomains() else: virtualGuests = self.virt.getHostGuestMapping() @@ -187,7 +190,7 @@ class VirtWho(object): return False try: - if self.options.virtType not in ["esx", "rhevm"]: + if self.options.virtType not in ["esx", "rhevm", "hyperv"]: self.subscriptionManager.sendVirtGuests(virtualGuests) else: result = self.subscriptionManager.hypervisorCheckIn(self.options.owner, self.options.env, virtualGuests) @@ -325,6 +328,7 @@ def main(): parser.add_option("--vdsm", action="store_const", dest="virtType", const="vdsm", help="Use vdsm to list virtual guests") parser.add_option("--esx", action="store_const", dest="virtType", const="esx", help="Register ESX machines using vCenter") parser.add_option("--rhevm", action="store_const", dest="virtType", const="rhevm", help="Register guests using RHEV-M") + parser.add_option("--hyperv", action="store_const", dest="virtType", const="hyperv", help="Register guests using Hyper-V") esxGroup = OptionGroup(parser, "vCenter/ESX options", "Use this options with --esx") esxGroup.add_option("--esx-owner", action="store", dest="owner", default="", help="Organization who has purchased subscriptions of the products") @@ -342,6 +346,14 @@ def main(): rhevmGroup.add_option("--rhevm-password", action="store", dest="password", default="", help="Password for connecting to RHEV-M") parser.add_option_group(rhevmGroup) + hypervGroup = OptionGroup(parser, "Hyper-V options", "Use this options with --hyperv") + hypervGroup.add_option("--hyperv-owner", action="store", dest="owner", default="", help="Organization who has purchased subscriptions of the products") + hypervGroup.add_option("--hyperv-env", action="store", dest="env", default="", help="Environment where the Hyper-V belongs to") + hypervGroup.add_option("--hyperv-server", action="store", dest="server", default="", help="URL of the Hyper-V server to connect to") + hypervGroup.add_option("--hyperv-username", action="store", dest="username", default="", help="Username for connecting to Hyper-V") + hypervGroup.add_option("--hyperv-password", action="store", dest="password", default="", help="Password for connecting to Hyper-V") + parser.add_option_group(hypervGroup) + (options, args) = parser.parse_args() # Handle enviromental variables @@ -379,6 +391,11 @@ def main(): if env in ["1", "true"]: options.virtType = "rhevm" + env = os.getenv("VIRTWHO_HYPERV", "0").strip().lower() + if env in ["1", "true"]: + options.virtType = "hyperv" + + def checkEnv(variable, option, name): """ If `option` is empty, check enviromental `variable` and return its value. @@ -407,6 +424,14 @@ def main(): if len(options.password) == 0: options.password = os.getenv("VIRTWHO_RHEVM_PASSWORD", "") + if options.virtType == "hyperv": + options.owner = checkEnv("VIRTWHO_HYPERV_OWNER", options.owner, "owner") + options.env = checkEnv("VIRTWHO_HYPERV_ENV", options.env, "env") + options.server = checkEnv("VIRTWHO_HYPERV_SERVER", options.server, "server") + options.username = checkEnv("VIRTWHO_HYPERV_USERNAME", options.username, "username") + if len(options.password) == 0: + options.password = os.getenv("VIRTWHO_HYPERV_PASSWORD", "") + if options.interval < 0: logger.warning("Interval is not positive number, ignoring") options.interval = 0 @@ -434,7 +459,7 @@ def main(): logger.debug("Starting event loop") virEventLoopPureStart() else: - logger.warning("Listening for events is not available in VDSM, ESX or RHEV-M mode") + logger.warning("Listening for events is not available in VDSM, ESX, RHEV-M or Hyper-V mode") global RetryInterval if options.interval < RetryInterval: