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('