Skip to content

Commit 1080ae0

Browse files
committed
Initial commit
0 parents  commit 1080ae0

File tree

3 files changed

+326
-0
lines changed

3 files changed

+326
-0
lines changed

LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
MIT License
3+
4+
Copyright (c) 2019 Mitch Kucia
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
NTLM Challenger
3+
===============
4+
5+
ntlm_challenger will send a NTLM negotiate message to a provided HTTP endpoint that accepts NTLM authentication, parse the challenge message, and print information received from the server.
6+
7+
Requirements
8+
------------
9+
10+
ntlm_challenger supports Python 3.
11+
12+
Usage
13+
-----
14+
15+
Send NTLM negotiate message over HTTP(S) to the provided URL and parse the challenge message.
16+
17+
```
18+
python3 ntlm_challenger.py <URL>
19+
```
20+
21+
Example:
22+
23+
```
24+
$ python3 ntlm_challenger.py 'https://autodiscover.hackin.club/autodiscover/'
25+
26+
Target (Domain): HACKIN
27+
28+
Version: Server 2012 / Windows 8 (build 9200)
29+
30+
TargetInfo:
31+
MsvAvNbDomainName: HACKIN
32+
MsvAvNbComputerName: EXCH01
33+
MsvAvDnsDomainName: hackin.club
34+
MsvAvDnsComputerName: EXCH01.hackin.club
35+
MsvAvDnsTreeName: hackin.club
36+
MsvAvTimestamp: Nov 3, 2019 01:07:16.573170
37+
38+
Negotiate Flags:
39+
NTLMSSP_NEGOTIATE_UNICODE
40+
NTLMSSP_REQUEST_TARGET
41+
NTLMSSP_NEGOTIATE_ALWAYS_SIGN
42+
NTLMSSP_TARGET_TYPE_DOMAIN
43+
NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
44+
NTLMSSP_NEGOTIATE_TARGET_INFO
45+
NTLMSSP_NEGOTIATE_VERSION
46+
```

ntlm_challenger.py

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#!/usr/bin/env python3
2+
3+
# parsing from "NT LAN Manager (NTLM) Authentication Protocol" v20190923, revision 31.0
4+
# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-NLMP/%5bMS-NLMP%5d.pdf
5+
6+
import argparse
7+
8+
import requests
9+
import sys
10+
import base64
11+
import datetime
12+
13+
from collections import OrderedDict
14+
15+
16+
def decode_string(byte_string):
17+
return byte_string.decode('UTF-8').replace('\x00', '')
18+
19+
def decode_int(byte_string):
20+
return int.from_bytes(byte_string, 'little')
21+
22+
23+
def parse_version(version_bytes):
24+
25+
major_version = version_bytes[0]
26+
minor_version = version_bytes[1]
27+
product_build = decode_int(version_bytes[2:4])
28+
29+
version = 'Unknown'
30+
31+
if major_version == 5 and minor_version == 1:
32+
version = 'Windows XP (SP2)'
33+
elif major_version == 5 and minor_version == 2:
34+
version = 'Server 2003'
35+
elif major_version == 6 and minor_version == 0:
36+
version = 'Server 2008 / Windows Vista'
37+
elif major_version == 6 and minor_version == 1:
38+
version = 'Server 2008 R2 / Windows 7'
39+
elif major_version == 6 and minor_version == 2:
40+
version = 'Server 2012 / Windows 8'
41+
elif major_version == 6 and minor_version == 3:
42+
version = 'Server 2012 R2 / Windows 8.1'
43+
elif major_version == 10 and minor_version == 0:
44+
version = 'Server 2016 or 2019 / Windows 10'
45+
46+
return '{} (build {})'.format(version, product_build)
47+
48+
49+
def parse_negotiate_flags(negotiate_flags_int):
50+
51+
flags = OrderedDict()
52+
53+
flags['NTLMSSP_NEGOTIATE_UNICODE'] = 0x00000001
54+
flags['NTLM_NEGOTIATE_OEM'] = 0x00000002
55+
flags['NTLMSSP_REQUEST_TARGET'] = 0x00000004
56+
flags['UNUSED_10'] = 0x00000008
57+
flags['NTLMSSP_NEGOTIATE_SIGN'] = 0x00000010
58+
flags['NTLMSSP_NEGOTIATE_SEAL'] = 0x00000020
59+
flags['NTLMSSP_NEGOTIATE_DATAGRAM'] = 0x00000040
60+
flags['NTLMSSP_NEGOTIATE_LM_KEY'] = 0x00000080
61+
flags['UNUSED_9'] = 0x00000100
62+
flags['NTLMSSP_NEGOTIATE_NTLM'] = 0x00000400
63+
flags['UNUSED_8'] = 0x00000400
64+
flags['NTLMSSP_ANONYMOUS'] = 0x00000800
65+
flags['NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED'] = 0x00001000
66+
flags['NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED'] = 0x00002000
67+
flags['UNUSED_7'] = 0x00004000
68+
flags['NTLMSSP_NEGOTIATE_ALWAYS_SIGN'] = 0x00008000
69+
flags['NTLMSSP_TARGET_TYPE_DOMAIN'] = 0x00010000
70+
flags['NTLMSSP_TARGET_TYPE_SERVER'] = 0x00020000
71+
flags['UNUSED_6'] = 0x00040000
72+
flags['NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY'] = 0x00080000
73+
flags['NTLMSSP_NEGOTIATE_IDENTIFY'] = 0x00100000
74+
flags['UNUSED_5'] = 0x00200000
75+
flags['NTLMSSP_REQUEST_NON_NT_SESSION_KEY'] = 0x00400000
76+
flags['NTLMSSP_NEGOTIATE_TARGET_INFO'] = 0x00800000
77+
flags['UNUSED_4'] = 0x01000000
78+
flags['NTLMSSP_NEGOTIATE_VERSION'] = 0x02000000
79+
flags['UNUSED_3'] = 0x10000000
80+
flags['UNUSED_2'] = 0x08000000
81+
flags['UNUSED_1'] = 0x04000000
82+
flags['NTLMSSP_NEGOTIATE_128'] = 0x20000000
83+
flags['NTLMSSP_NEGOTIATE_KEY_EXCH'] = 0x40000000
84+
flags['NTLMSSP_NEGOTIATE_56'] = 0x80000000
85+
86+
negotiate_flags = []
87+
88+
for name,value in flags.items():
89+
if negotiate_flags_int & value:
90+
negotiate_flags.append(name)
91+
92+
return negotiate_flags
93+
94+
95+
def parse_target_info(target_info_bytes):
96+
97+
MsvAvEOL = 0x0000
98+
MsvAvNbComputerName = 0x0001
99+
MsvAvNbDomainName = 0x0002
100+
MsvAvDnsComputerName = 0x0003
101+
MsvAvDnsDomainName = 0x0004
102+
MsvAvDnsTreeName = 0x0005
103+
MsvAvFlags = 0x0006
104+
MsvAvTimestamp = 0x0007
105+
MsvAvSingleHost = 0x0008
106+
MsvAvTargetName = 0x0009
107+
MsvAvChannelBindings = 0x000A
108+
109+
target_info = OrderedDict()
110+
info_offset = 0
111+
112+
while info_offset < len(target_info_bytes):
113+
av_id = decode_int(target_info_bytes[info_offset:info_offset+2])
114+
av_len = decode_int(target_info_bytes[info_offset+2:info_offset+4])
115+
av_value = target_info_bytes[info_offset+4:info_offset+4+av_len]
116+
117+
info_offset = info_offset + 4 + av_len
118+
119+
if av_id == MsvAvEOL:
120+
pass
121+
elif av_id == MsvAvNbComputerName:
122+
target_info['MsvAvNbComputerName'] = decode_string(av_value)
123+
elif av_id == MsvAvNbDomainName:
124+
target_info['MsvAvNbDomainName'] = decode_string(av_value)
125+
elif av_id == MsvAvDnsComputerName:
126+
target_info['MsvAvDnsComputerName'] = decode_string(av_value)
127+
elif av_id == MsvAvDnsDomainName:
128+
target_info['MsvAvDnsDomainName'] = decode_string(av_value)
129+
elif av_id == MsvAvDnsTreeName:
130+
target_info['MsvAvDnsTreeName'] = decode_string(av_value)
131+
elif av_id == MsvAvFlags:
132+
pass
133+
elif av_id == MsvAvTimestamp:
134+
filetime = decode_int(av_value)
135+
microseconds = (filetime - 116444736000000000) / 10
136+
time = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds = microseconds)
137+
target_info['MsvAvTimestamp'] = time.strftime("%b %d, %Y %H:%M:%S.%f")
138+
elif av_id == MsvAvSingleHost:
139+
target_info['MsvAvSingleHost'] = decode_string(av_value)
140+
elif av_id == MsvAvTargetName:
141+
target_info['MsvAvTargetName'] = decode_string(av_value)
142+
elif av_id == MsvAvChannelBindings:
143+
target_info['MsvAvChannelBindings'] = av_value
144+
145+
return target_info
146+
147+
148+
def parse_challenge(challenge_message):
149+
150+
# Signature
151+
signature = decode_string(challenge_message[0:7]) # b'NTLMSSP\x00' --> NTLMSSP
152+
153+
# MessageType
154+
message_type = decode_int(challenge_message[8:12]) # b'\x02\x00\x00\x00' --> 2
155+
156+
# TargetNameFields
157+
target_name_fields = challenge_message[12:20]
158+
target_name_len = decode_int(target_name_fields[0:2])
159+
target_name_max_len = decode_int(target_name_fields[2:4])
160+
target_name_offset = decode_int(target_name_fields[4:8])
161+
162+
# NegotiateFlags
163+
negotiate_flags_int = decode_int(challenge_message[20:24])
164+
165+
negotiate_flags = parse_negotiate_flags(negotiate_flags_int)
166+
167+
# ServerChallenge
168+
server_challenge = challenge_message[24:32]
169+
170+
# Reserved
171+
reserved = challenge_message[32:40]
172+
173+
# TargetInfoFields
174+
target_info_fields = challenge_message[40:48]
175+
target_info_len = decode_int(target_info_fields[0:2])
176+
target_info_max_len = decode_int(target_info_fields[2:4])
177+
target_info_offset = decode_int(target_info_fields[4:8])
178+
179+
# Version
180+
version_bytes = challenge_message[48:56]
181+
version = parse_version(version_bytes)
182+
183+
# TargetName
184+
target_name = challenge_message[target_name_offset:target_name_offset+target_name_len]
185+
target_name = decode_string(target_name)
186+
187+
# TargetInfo
188+
target_info_bytes = challenge_message[target_info_offset:target_info_offset+target_info_len]
189+
190+
target_info = parse_target_info(target_info_bytes)
191+
192+
return {
193+
'target_name': target_name,
194+
'version': version,
195+
'target_info': target_info,
196+
'negotiate_flags': negotiate_flags
197+
}
198+
199+
200+
def print_challenge(challenge):
201+
202+
if 'NTLMSSP_TARGET_TYPE_DOMAIN' in challenge['negotiate_flags']:
203+
print('\nTarget (Domain): {}'.format(challenge['target_name']))
204+
elif 'NTLMSSP_TARGET_TYPE_SERVER' in challenge['negotiate_flags']:
205+
print('\nTarget (Server): {}'.format(challenge['target_name']))
206+
207+
print('\nVersion: {}'.format(challenge['version']))
208+
209+
print('\nTargetInfo:')
210+
211+
for name,value in challenge['target_info'].items():
212+
print(' {}: {}'.format(name, value))
213+
214+
print('\nNegotiate Flags:')
215+
216+
for flag in challenge['negotiate_flags']:
217+
print(' {}'.format(flag))
218+
219+
220+
def main():
221+
222+
# setup arguments
223+
parser = argparse.ArgumentParser(description='Fetch and parse NTLM challenge ' +
224+
'messages from HTTP (e.g. Autodiscover/ActiveSync/OWA) services')
225+
parser.add_argument('url', help='URL to fetch NTLM challenge from')
226+
args = parser.parse_args()
227+
228+
# setup request
229+
headers = {'Authorization': 'NTLM TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA='}
230+
request = requests.get(args.url, headers=headers)
231+
232+
if request.status_code not in [401, 302]:
233+
print('[!] Expecting response code 401 or 302, received: {}'.format(request.status_code))
234+
sys.exit()
235+
236+
# get auth header
237+
auth_header = request.headers.get('WWW-Authenticate')
238+
239+
if not auth_header:
240+
print('[!] NTLM Challenge response not found (WWW-Authenticate header missing)')
241+
sys.exit()
242+
243+
if not 'NTLM' in auth_header:
244+
print('[!] NTLM Challenge response not found (WWW-Authenticate does not contain "NTLM")')
245+
sys.exit()
246+
247+
# get challenge message from header
248+
challenge_message = base64.b64decode(auth_header.split(' ')[1].replace(',', ''))
249+
250+
# parse challenge
251+
challenge = parse_challenge(challenge_message)
252+
253+
# print challenge
254+
print_challenge(challenge)
255+
256+
257+
if __name__ == '__main__':
258+
main()

0 commit comments

Comments
 (0)