Skip to content

Commit 5799c22

Browse files
committed
add ldap_shell (fortra#2057)
1 parent 35f3594 commit 5799c22

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

examples/ldap_shell.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Description:
4+
# Implementation of ldap_shell.py, an interactive ldap client.
5+
#
6+
# Author:
7+
# Andreas Vikerup (@vikerup)
8+
#
9+
10+
import argparse
11+
import atexit
12+
import logging
13+
import sys
14+
from getpass import getpass
15+
from pathlib import Path
16+
17+
from impacket import version
18+
from impacket.examples import logger
19+
from impacket.examples.ldap_shell import LdapShell
20+
from impacket.examples.utils import EMPTY_LM_HASH, init_ldap_session, parse_target
21+
import ldapdomaindump
22+
from ldapdomaindump import reportWriter as _ReportWriter
23+
24+
25+
class FakeShell:
26+
def __init__(self):
27+
self.stdin = sys.stdin
28+
self.stdout = sys.stdout
29+
self._readline = None
30+
self._history_file = None
31+
self._init_line_editing()
32+
33+
def _init_line_editing(self):
34+
if not self.stdin.isatty():
35+
return
36+
37+
try:
38+
import readline
39+
except ImportError:
40+
return
41+
42+
self._readline = readline
43+
history_path = Path.home() / '.impacket_ldap_shell_history'
44+
45+
try:
46+
readline.read_history_file(str(history_path))
47+
except (FileNotFoundError, OSError):
48+
pass
49+
50+
readline.parse_and_bind('set editing-mode emacs')
51+
readline.parse_and_bind('set enable-meta-key on')
52+
readline.parse_and_bind('tab: complete')
53+
54+
self._history_file = history_path
55+
atexit.register(self._persist_history)
56+
57+
def _persist_history(self):
58+
if self._readline is None or self._history_file is None:
59+
return
60+
try:
61+
self._readline.write_history_file(str(self._history_file))
62+
except OSError:
63+
pass
64+
65+
def close(self):
66+
self._persist_history()
67+
68+
69+
def _ensure_safe_report_writer():
70+
if getattr(_ReportWriter, '_impacket_safe_patch', False):
71+
return
72+
73+
def safe_format_string(self, value):
74+
from datetime import datetime
75+
76+
if isinstance(value, datetime):
77+
try:
78+
return value.strftime('%x %X')
79+
except ValueError:
80+
return '0'
81+
if isinstance(value, (bytes, bytearray)):
82+
return value.decode('utf-8', errors='ignore')
83+
if isinstance(value, str):
84+
return value
85+
if isinstance(value, int):
86+
return str(value)
87+
if value is None:
88+
return ''
89+
return str(value)
90+
91+
def safe_html_escape(self, html):
92+
if isinstance(html, (bytes, bytearray)):
93+
html = html.decode('utf-8', errors='ignore')
94+
elif not isinstance(html, str):
95+
html = str(html)
96+
return (html.replace("&", "&")
97+
.replace("<", "&lt;")
98+
.replace(">", "&gt;")
99+
.replace("'", "&#39;")
100+
.replace('"', "&quot;"))
101+
102+
_ReportWriter.formatString = safe_format_string
103+
_ReportWriter.htmlescape = safe_html_escape
104+
_ReportWriter._impacket_safe_patch = True
105+
106+
107+
class DomainDumper:
108+
def __init__(self, ldap_server, ldap_session, base_path, root):
109+
_ensure_safe_report_writer()
110+
config = ldapdomaindump.domainDumpConfig()
111+
if base_path is not None:
112+
config.basepath = base_path
113+
self._dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, config, root)
114+
115+
def domainDump(self):
116+
self._dumper.domainDump()
117+
118+
@property
119+
def root(self):
120+
return self._dumper.root
121+
122+
@root.setter
123+
def root(self, value):
124+
self._dumper.root = value
125+
126+
127+
def main():
128+
print(version.BANNER)
129+
130+
parser = argparse.ArgumentParser(add_help=True, description='Interactive LDAP shell using impacket\'s helpers')
131+
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<target>')
132+
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
133+
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
134+
135+
auth_group = parser.add_argument_group('authentication')
136+
auth_group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH')
137+
auth_group.add_argument('-no-pass', action='store_true', help="don't ask for password (useful for -k)")
138+
auth_group.add_argument('-k', action='store_true', help='Use Kerberos authentication. Grabs credentials from ccache file '
139+
'(KRB5CCNAME) based on target parameters. If valid credentials '
140+
'cannot be found, it will use the ones specified in the command '
141+
'line')
142+
auth_group.add_argument('-aesKey', action='store', metavar='hex key', help='AES key to use for Kerberos Authentication '
143+
'(128 or 256 bits)')
144+
145+
conn_group = parser.add_argument_group('connection')
146+
conn_group.add_argument('-dc-ip', action='store', metavar='ip address',
147+
help='IP Address or hostname of the domain controller (KDC) for Kerberos. If omitted it will '
148+
'use the target portion of the connection string')
149+
conn_group.add_argument('-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
150+
conn_group.add_argument('-dump-dir', action='store', metavar='path', default='.',
151+
help='Directory where domain dump files will be stored (default: current directory)')
152+
153+
if len(sys.argv) == 1:
154+
parser.print_help()
155+
sys.exit(1)
156+
157+
options = parser.parse_args()
158+
159+
logger.init(options.ts, options.debug)
160+
161+
domain, username, password, address = parse_target(options.target)
162+
163+
if domain is None:
164+
domain = ''
165+
if username is None:
166+
username = ''
167+
if password is None:
168+
password = ''
169+
170+
dc_ip = options.dc_ip
171+
dc_host = None
172+
if options.k:
173+
if dc_ip is None and address is not None:
174+
dc_host = address
175+
else:
176+
if dc_ip is None and address is not None:
177+
dc_ip = address
178+
179+
if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
180+
password = getpass('Password:')
181+
182+
if options.aesKey is not None:
183+
options.k = True
184+
185+
if options.no_pass:
186+
password = ''
187+
188+
if options.hashes is not None:
189+
try:
190+
lmhash, nthash = options.hashes.split(':')
191+
except ValueError:
192+
logging.error('Hashes must be supplied in LMHASH:NTHASH format')
193+
sys.exit(1)
194+
if lmhash == '':
195+
lmhash = EMPTY_LM_HASH
196+
else:
197+
lmhash = ''
198+
nthash = ''
199+
200+
console = None
201+
try:
202+
ldap_server, ldap_session = init_ldap_session(domain, username, password, lmhash, nthash, options.k,
203+
dc_ip, dc_host, options.aesKey, options.ldaps)
204+
server_info = ldap_session.server.info if ldap_session else None
205+
root_dn = None
206+
if server_info is not None:
207+
other = server_info.other or {}
208+
default_nc = other.get('defaultNamingContext')
209+
if default_nc:
210+
root_dn = default_nc[0]
211+
212+
if root_dn is None:
213+
logging.error('Could not determine defaultNamingContext from the LDAP server')
214+
sys.exit(1)
215+
216+
console = FakeShell()
217+
domain_dumper = DomainDumper(ldap_server, ldap_session, options.dump_dir, root_dn)
218+
shell = LdapShell(console, domain_dumper, ldap_session)
219+
shell.use_rawinput = True
220+
shell.cmdloop()
221+
except Exception as e:
222+
if logging.getLogger().level == logging.DEBUG:
223+
import traceback
224+
traceback.print_exc()
225+
logging.error(str(e))
226+
finally:
227+
if console is not None:
228+
console.close()
229+
230+
if __name__ == "__main__":
231+
main()

0 commit comments

Comments
 (0)