1. Amit Aronovitch
  2. python-ntlm

Commits

Amit Aronovitch  committed d0dfb6e Merge

merge default (svn r89)

  • Participants
  • Parent commits 0882bae, cb943b9
  • Branches imap_smtp

Comments (0)

Files changed (6)

File python26/ntlm/HTTPNtlmAuthHandler.py

View file
 from urllib import addinfourl
 import ntlm
 import string
+import re
 
 class AbstractNtlmAuthHandler:
     def __init__(self, password_mgr=None, debuglevel=0):
+        """Initialize an instance of a AbstractNtlmAuthHandler.
+
+Verify operation with all default arguments.
+>>> abstrct = AbstractNtlmAuthHandler()
+
+Verify "normal" operation.
+>>> abstrct = AbstractNtlmAuthHandler(urllib2.HTTPPasswordMgrWithDefaultRealm())
+"""
         if password_mgr is None:
-            password_mgr = HTTPPasswordMgr()
+            password_mgr = urllib2.HTTPPasswordMgr()
         self.passwd = password_mgr
         self.add_password = self.passwd.add_password
         self._debuglevel = debuglevel
     def retry_using_http_NTLM_auth(self, req, auth_header_field, realm, headers):
         user, pw = self.passwd.find_user_password(realm, req.get_full_url())
         if pw is not None:
+            user_parts = user.split('\\', 1)
+            if len(user_parts) == 1:
+                UserName = user_parts[0]
+                DomainName = ''
+                type1_flags = ntlm.NTLM_TYPE1_FLAGS & ~ntlm.NTLM_NegotiateOemDomainSupplied
+            else:
+                DomainName = user_parts[0].upper()
+                UserName = user_parts[1]
+                type1_flags = ntlm.NTLM_TYPE1_FLAGS
             # ntlm secures a socket, so we must use the same socket for the complete handshake
             headers = dict(req.headers)
             headers.update(req.unredirected_hdrs)
-            auth = 'NTLM %s' % asbase64(ntlm.create_NTLM_NEGOTIATE_MESSAGE(user))
+            auth = 'NTLM %s' % asbase64(ntlm.create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags))
             if req.headers.get(self.auth_header, None) == auth:
                 return None
             headers[self.auth_header] = auth
                 headers['Cookie'] = r.getheader('set-cookie')
             r.fp = None # remove the reference to the socket, so that it can not be closed by the response object (we want to keep the socket open)
             auth_header_value = r.getheader(auth_header_field, None)
+
+            # some Exchange servers send two WWW-Authenticate headers, one with the NTLM challenge
+            # and another with the 'Negotiate' keyword - make sure we operate on the right one
+            m = re.match('(NTLM [A-Za-z0-9+\-/=]+)', auth_header_value)
+            if m:
+                auth_header_value, = m.groups()
+
             (ServerChallenge, NegotiateFlags) = ntlm.parse_NTLM_CHALLENGE_MESSAGE(base64.decodestring(auth_header_value[5:]))
-            user_parts = user.split('\\', 1)
-            DomainName = user_parts[0].upper()
-            UserName = user_parts[1]
             auth = 'NTLM %s' % asbase64(ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, UserName, DomainName, pw, NegotiateFlags))
             headers[self.auth_header] = auth
             headers["Connection"] = "Close"
                 def notimplemented():
                     raise NotImplementedError
                 response.readline = notimplemented
-                return addinfourl(response, response.msg, req.get_full_url())
+                infourl = addinfourl(response, response.msg, req.get_full_url())
+                infourl.code = response.status
+                infourl.msg = response.reason
+                return infourl
             except socket.error, err:
                 raise urllib2.URLError(err)
         else:
     return string.replace(base64.encodestring(msg), '\n', '')
 
 if __name__ == "__main__":
-    url = "http://ntlmprotectedserver/securedfile.html"
-    user = u'DOMAIN\\User'
-    password = 'Password'
+    import doctest
+    doctest.testmod()
 
-    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
-    passman.add_password(None, url, user , password)
-    auth_basic = urllib2.HTTPBasicAuthHandler(passman)
-    auth_digest = urllib2.HTTPDigestAuthHandler(passman)
-    auth_NTLM = HTTPNtlmAuthHandler(passman)
-
-    # disable proxies (just for testing)
-    proxy_handler = urllib2.ProxyHandler({})
-
-    opener = urllib2.build_opener(proxy_handler, auth_NTLM) #, auth_digest, auth_basic)
-
-    urllib2.install_opener(opener)
-
-    response = urllib2.urlopen(url)
-    print(response.read())
-
+### TODO: Move this to the ntlm examples directory.
+##if __name__ == "__main__":
+##    url = "http://ntlmprotectedserver/securedfile.html"
+##    user = u'DOMAIN\\User'
+##    password = 'Password'
+##
+##    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
+##    passman.add_password(None, url, user , password)
+##    auth_basic = urllib2.HTTPBasicAuthHandler(passman)
+##    auth_digest = urllib2.HTTPDigestAuthHandler(passman)
+##    auth_NTLM = HTTPNtlmAuthHandler(passman)
+##
+##    # disable proxies (just for testing)
+##    proxy_handler = urllib2.ProxyHandler({})
+##
+##    opener = urllib2.build_opener(proxy_handler, auth_NTLM) #, auth_digest, auth_basic)
+##
+##    urllib2.install_opener(opener)
+##
+##    response = urllib2.urlopen(url)
+##    print(response.read())

File python26/ntlm/ntlm.py

View file
 import hashlib
 import hmac
 import random
+import re
+import binascii
 from socket import gethostname
 
 NTLM_NegotiateUnicode                =  0x00000001
     if NegotiateFlags & NTLM_Negotiate56:
         print "NTLM_Negotiate56 set"                    
 
-def create_NTLM_NEGOTIATE_MESSAGE(user):
+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', NTLM_TYPE1_FLAGS)
+    flags =  struct.pack('<I', type1_flags)
     Workstation = gethostname().upper().encode('ascii')
     user_parts = user.split('\\', 1)
     DomainName = user_parts[0].upper().encode('ascii')
     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')
+    if NegotiateFlags & NTLM_NegotiateTargetInfo:
+        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):
 def create_LM_hashed_password_v1(passwd):
     "setup LanManager password"
     "create LanManager hashed password"
-    
+    # if the passwd provided is already a hash, we just return the first half
+    if re.match(r'^[\w]{32}:[\w]{32}$',passwd):
+        return binascii.unhexlify(passwd.split(':')[0])
+
     # fix the password length to 14 bytes
     passwd = string.upper(passwd)
     lm_pw = passwd + '\0' * (14 - len(passwd))
     
 def create_NT_hashed_password_v1(passwd, user=None, domain=None):
     "create NT hashed password"
+    # if the passwd provided is already a hash, we just return the second half
+    if re.match(r'^[\w]{32}:[\w]{32}$',passwd):
+        return binascii.unhexlify(passwd.split(':')[1])
+        
     digest = hashlib.new('md4', passwd.encode('utf-16le')).digest()
     return digest
 
     # 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
-    
+    

File python26/ntlm_examples/test_ntlmauth.py

View file
+"""\
+Demonstrate various defects (or their repair!) in the ntml module.
+"""
+
+
+from StringIO import StringIO
+import httplib
+import urllib2
+from ntlm import HTTPNtlmAuthHandler
+import traceback
+
+
+# The headers seen during an initial NTML rejection.
+initial_rejection = '''HTTP/1.1 401 Unauthorized
+Server: Apache-Coyote/1.1
+WWW-Authenticate: NTLM
+Connection: close
+Date: Tue, 03 Feb 2009 11:47:33 GMT
+Connection: close
+
+'''
+
+
+# The headers and data seen following a successful NTML connection.
+eventual_success = '''HTTP/1.1 200 OK
+Server: Apache-Coyote/1.1
+WWW-Authenticate: NTLM TlRMTVNTUAACAAAABAAEADgAAAAFgomi3k7KRx+HGYQAAAAAAAAAALQAtAA8AAAABgGwHQAAAA9OAEEAAgAEAE4AQQABABYATgBBAFMAQQBOAEUAWABIAEMAMAA0AAQAHgBuAGEALgBxAHUAYQBsAGMAbwBtAG0ALgBjAG8AbQADADYAbgBhAHMAYQBuAGUAeABoAGMAMAA0AC4AbgBhAC4AcQB1AGEAbABjAG8AbQBtAC4AYwBvAG0ABQAiAGMAbwByAHAALgBxAHUAYQBsAGMAbwBtAG0ALgBjAG8AbQAHAAgADXHouNLjzAEAAAAA
+Date: Tue, 03 Feb 2009 11:47:33 GMT
+Connection: close
+
+Hello, world!'''
+
+
+# A collection of transactions representing various defects in NTLM
+# processing. Each is indexed according the the issues number recorded
+# for the defect at github.  Each consists of a series of server
+# responses that should be seen as a connection is attempted.
+issues = {
+    27: [
+        initial_rejection,
+        '''HTTP/1.1 401 Unauthorized
+Server: Apache-Coyote/1.1
+WWW-Authenticate: NTLM TlRMTVNTUAACAAAABAAEADgAAAAFgomi3k7KRx+HGYQAAAAAAAAAALQAtAA8AAAABgGwHQAAAA9OAEEAAgAEAE4AQQABABYATgBBAFMAQQBOAEUAWABIAEMAMAA0AAQAHgBuAGEALgBxAHUAYQBsAGMAbwBtAG0ALgBjAG8AbQADADYAbgBhAHMAYQBuAGUAeABoAGMAMAA0AC4AbgBhAC4AcQB1AGEAbABjAG8AbQBtAC4AYwBvAG0ABQAiAGMAbwByAHAALgBxAHUAYQBsAGMAbwBtAG0ALgBjAG8AbQAHAAgADXHouNLjzAEAAAAA
+WWW-Authenticate: Negotiate
+Content-Length: 0
+Date: Tue, 03 Feb 2009 11:47:33 GMT
+Connection: close
+
+''',
+        eventual_success,
+        ],
+    28: [
+        initial_rejection,
+        '''HTTP/1.1 401 Unauthorized
+Server: Apache-Coyote/1.1
+WWW-Authenticate: NTLM TlRMTVNTUAACAAAAAAAAAAAAAAABAgAAO/AU3OJc3g0=
+Content-Length: 0
+Date: Tue, 03 Feb 2009 11:47:33 GMT
+Connection: close
+
+''',
+        eventual_success,
+        ],
+    }
+
+
+class FakeSocket(StringIO):
+    '''Extends StringIO just enough to look like a socket.'''
+    def makefile(self, *args, **kwds):
+        '''The instance already looks like a file.'''
+        return self
+    def sendall(self, *args, **kwds):
+        '''Ignore any data that may be sent.'''
+        pass
+    def close(self):
+        '''Ignore any calls to close.'''
+        pass
+
+
+class FakeHTTPConnection(httplib.HTTPConnection):
+    '''Looks like a normal HTTPConnection, but returns a FakeSocket.
+    The connection's port number is used to choose a set of transactions
+    to replay to the user.  A class static variable is used to track
+    how many transactions have been replayed.'''
+    attempt = {}
+    def connect(self):
+        '''Returns a FakeSocket containing the data for a single
+        transaction.'''
+        nbr = self.attempt.setdefault(self.port, 0)
+        self.attempt[self.port] = nbr + 1
+        print 'connecting to %s:%s (attempt %s)' % (self.host, self.port, nbr)
+        self.sock = FakeSocket(issues[self.port][nbr])
+
+
+class FakeHTTPHandler(urllib2.HTTPHandler):
+    connection = FakeHTTPConnection
+    def http_open(self, req):
+        print 'opening', self.connection
+        return self.do_open(self.connection, req)
+
+
+def process(*issue_nbrs):
+    '''Run the specified tests, or all of them.'''
+
+    if issue_nbrs:
+        # Make sure the tests are ints.
+        issue_nbrs = map(int, issue_nbrs)
+    else:
+        # If no tests were specified, run them all.
+        issue_nbrs = issues.keys()
+
+    assert all(i in issues for i in issue_nbrs)
+
+    user = 'DOMAIN\User'
+    password = "Password"
+    url = "http://www.example.org:%s/"
+
+    # Set passwords for each of the "servers" to which we will be connecting.
+    # Each distinct port on a server requires it's own set of credentials.
+    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
+    for k in issue_nbrs:
+        passman.add_password(None, url % k, user, password)
+
+    # Create the NTLM authentication handler.
+    auth_NTLM = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(passman, debuglevel=0)
+
+    # Create and install openers for both the NTLM Auth handler and
+    # our fake HTTP handler.
+    opener = urllib2.build_opener(auth_NTLM, FakeHTTPHandler)
+    urllib2.install_opener(opener)
+
+    # The following is a massive kludge; let me explain why it is needed.
+    HTTPNtlmAuthHandler.httplib.HTTPConnection = FakeHTTPConnection
+    # At the heart of the urllib2 module is the opener director. Whenever a
+    # URL is opened, the director Is responsible for locating the proper
+    # handler for the protocol specified in the URL. Frequently, an existing
+    # protocol handler will be subclassed and then added to the collection
+    # maintained by the director. When urlopen is called, the specified
+    # request is immediately handed off to the director's "open" method
+    # which finds the correct handler and invokes the protocol-specific
+    # XXX_open method. At least in the case of the HTTP protocols, if an
+    # error occurs then the director is called again to find and invoke a
+    # handler for the error; these handlers generally open a new connection
+    # after adding headers to avoid the error going forward. Finally, it is
+    # important to note that at the present time, the HTTP handlers in
+    # urllib2 are built using a class that isn't prepared to deal with a
+    # persistent connection, so they always add a "Connection: close" header
+    # to the request.
+    # 
+    # Unfortunately, NTLM only certifies the current connection, meaning
+    # that  a "Connection: keep-alive" header must be used to keep it open
+    # throughout the authentication process. Furthermore, because the opener
+    # director only provides a do_open method, there is no way to discover
+    # the type of connection without also opening it. This means that the
+    # HTTPNtlmAuthHandler cannot use the normal HTTPHandler and must
+    # therefore must hardcode the HTTPConnection class. If a custom class is
+    # required for whatever reason, the only way to cause it to be used is
+    # to monkey-patch the code, as is done in the line above.
+
+    for i in sorted(issue_nbrs):
+        print '\nissue %d' % i
+        try:
+            f = urllib2.urlopen(url % i)
+        except:
+            traceback.print_exc()
+        else:
+            print f.read()
+
+
+# The following is copied from Guido van van Rossum's suggestion.
+# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
+
+import sys
+import getopt
+
+class Usage(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv
+    try:
+        try:
+            opts, args = getopt.getopt(argv[1:], "h", ["help"])
+        except getopt.error, msg:
+             raise Usage(msg)
+        process(*args)
+    except Usage, err:
+        print >>sys.stderr, err.msg
+        print >>sys.stderr, "for help use --help"
+        return 2
+
+if __name__ == "__main__":
+    sys.exit(main())

File python26/setup.py

View file
 ENTRY_POINTS = { "console_scripts":[ "ntlm_example_simple=ntlm_examples.simple:main",
                                      "ntlm_example_extended=ntlm_examples.extended:main",] }
 
-DEPENDANCIES = []
+DEPENDENCIES = []
 
 if sys.version_info < ( 2,5 ):
-    DEPENDANCIES.append( "hashlib==20060408a" )
+    DEPENDENCIES.append( "hashlib" )
     
 setup(name='python-ntlm',
-      version='1.0',
-      description='Python library that provides NTLM support, including an authentication handler for urllib2.',
+      version='1.0.1',
+      description='Python library that provides NTLM support, including an authentication handler for urllib2. Works with pass-the-hash in additon to password authentication.',
+      long_description="""
+      This package allows Python clients running on any operating
+      system to provide NTLM authentication to a supporting server.
+      
+      python-ntlm is probably most useful on platforms that are not
+      Windows, since on Windows it is possible to take advantage of
+      platform-specific NTLM support.
+
+      This is also useful for passing hashes to servers requiring
+      ntlm authentication in instances where using windows tools is 
+      not desirable.""",
       author='Matthijs Mullender',
+      author_email='info@zopyx.org',
+      maintainer='Daniel Holth',
+      maintainer_email='dholth@gmail.com',
+      url="http://code.google.com/p/python-ntlm",
       packages=["ntlm",],
       zip_safe=False,
       entry_points = ENTRY_POINTS,
-      install_requires = DEPENDANCIES,
+      install_requires = DEPENDENCIES,
       )

File python30/ntlm/HTTPNtlmAuthHandler.py

View file
     def retry_using_http_NTLM_auth(self, req, auth_header_field, realm, headers):
         user, pw = self.passwd.find_user_password(realm, req.get_full_url())
         if pw is not None:
+            user_parts = user.split('\\', 1)
+            if len(user_parts) == 1:
+                UserName = user_parts[0]
+                DomainName = ''
+                type1_flags = ntlm.NTLM_TYPE1_FLAGS & ~ntlm.NTLM_NegotiateOemDomainSupplied
+            else:
+                DomainName = user_parts[0].upper()
+                UserName = user_parts[1]
+                type1_flags = ntlm.NTLM_TYPE1_FLAGS
             # ntlm secures a socket, so we must use the same socket for the complete handshake
             headers = dict(req.headers)
             headers.update(req.unredirected_hdrs)
-            auth = 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(user)
+            auth = 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags)
             if req.headers.get(self.auth_header, None) == auth:
                 return None
             headers[self.auth_header] = auth
             r.fp = None # remove the reference to the socket, so that it can not be closed by the response object (we want to keep the socket open)
             auth_header_value = r.getheader(auth_header_field, None)
             (ServerChallenge, NegotiateFlags) = ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value[5:])
-            user_parts = user.split('\\', 1)
-            DomainName = user_parts[0].upper()
-            UserName = user_parts[1]
             auth = 'NTLM %s' % ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, UserName, DomainName, pw, NegotiateFlags)
             headers[self.auth_header] = auth
             headers["Connection"] = "Close"
     urllib.request.install_opener(opener)
     
     response = urllib.request.urlopen(url)
-    print((response.read()))
+    print((response.read()))

File python30/ntlm/ntlm.py

View file
 import hashlib
 import hmac
 import random
+import re
+import binascii
 from socket import gethostname
 
 NTLM_NegotiateUnicode                =  0x00000001
 def create_LM_hashed_password_v1(passwd):
     "setup LanManager password"
     "create LanManager hashed password"
+    # if the passwd provided is already a hash, we just return the first half
+    if re.match(r'^[\w]{32}:[\w]{32}$',passwd):
+        return binascii.unhexlify(passwd.split(':')[0])
     
     # fix the password length to 14 bytes
     passwd = passwd.upper()
     
 def create_NT_hashed_password_v1(passwd, user=None, domain=None):
     "create NT hashed password"
+    # if the passwd provided is already a hash, we just return the second half
+    if re.match(r'^[\w]{32}:[\w]{32}$',passwd):
+        return binascii.unhexlify(passwd.split(':')[1])
+        
     digest = hashlib.new('md4', passwd.encode('utf-16le')).digest()
     return digest