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 = """ + + %s + %s +""" + +def getHeader(action): + return """ + """ + NAMESPACES['wsen'] + "/" + action + """ + %(url)s + http://schemas.microsoft.com/wbem/wsman/1/wmi/%(namespace)s/* + uuid:""" + str(uuid1()) + """ + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + """ + +ENUMERATE_BODY = """ + + %(query)s + + """ + +PULL_BODY = """ + + %(EnumerationContext)s + + """ + +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 or . + +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('