man-spider 1.1.1__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
man_spider/lib/smb.py CHANGED
@@ -1,22 +1,26 @@
1
1
  import ntpath
2
2
  import logging
3
- from .errors import *
4
3
  from contextlib import suppress
5
4
  from impacket.nmb import NetBIOSError, NetBIOSTimeout
6
5
  from impacket.smbconnection import SessionError, SMBConnection
7
6
 
7
+ from man_spider.lib.errors import *
8
+
8
9
  # set up logging
9
- log = logging.getLogger('manspider.smb')
10
+ log = logging.getLogger("manspider.smb")
10
11
 
11
12
 
12
13
  class SMBClient:
13
- '''
14
+ """
14
15
  Wrapper around impacket's SMBConnection() object
15
- '''
16
+ """
16
17
 
17
- def __init__(self, server, username, password, domain, nthash, use_kerberos=False, aes_key="", dc_ip=None):
18
+ def __init__(
19
+ self, server, username, password, domain, nthash, use_kerberos=False, aes_key="", dc_ip=None, port=445
20
+ ):
18
21
 
19
22
  self.server = server
23
+ self.port = port
20
24
 
21
25
  self.conn = None
22
26
 
@@ -30,22 +34,33 @@ class SMBClient:
30
34
  self.hostname = None
31
35
  self.dns_domain = None
32
36
  if self.nthash:
33
- self.lmhash = 'aad3b435b51404eeaad3b435b51404ee'
37
+ self.lmhash = "aad3b435b51404eeaad3b435b51404ee"
34
38
  else:
35
- self.lmhash = ''
39
+ self.lmhash = ""
36
40
  self._shares = None
37
41
 
38
-
39
42
  def list_shares(self):
40
- '''
43
+ """
41
44
  List shares on the SMB server
42
- '''
45
+ """
43
46
  resp = self.conn.listShares()
47
+ log.debug(f"{self.server}: Response length: {len(resp)}")
44
48
  for i in range(len(resp)):
45
- sharename = resp[i]['shi1_netname'][:-1]
46
- log.debug(f'{self.server}: Found share: {sharename}')
47
- yield sharename
48
-
49
+ try:
50
+ sharename = resp[i]["shi1_netname"].rstrip("\x00")
51
+ try:
52
+ share_type = resp[i]["shi1_type"]
53
+ except (KeyError, AttributeError):
54
+ share_type = "unknown"
55
+ try:
56
+ share_comment = resp[i]["shi1_remark"].rstrip("\x00")
57
+ except (KeyError, AttributeError):
58
+ share_comment = ""
59
+ log.debug(f"{self.server}: Share {i}: name='{sharename}', type={share_type}, comment='{share_comment}'")
60
+ yield sharename
61
+ except Exception as e:
62
+ log.debug(f"{self.server}: Error processing share {i}: {e}")
63
+ continue
49
64
 
50
65
  @property
51
66
  def shares(self):
@@ -54,27 +69,26 @@ class SMBClient:
54
69
  self._shares = list(self.list_shares())
55
70
  except Exception as e:
56
71
  e = self.handle_impacket_error(e)
57
- log.debug(f'{self.server}: Error listing shares: {e}, retrying...')
72
+ log.debug(f"{self.server}: Error listing shares: {e}, retrying...")
58
73
  self.rebuild(e)
59
74
  try:
60
75
  self._shares = list(self.list_shares())
61
76
  except Exception as e:
62
77
  e = self.handle_impacket_error(e)
63
- log.warning(f'{self.server}: Error listing shares: {e}')
78
+ log.warning(f"{self.server}: Error listing shares: {e}")
64
79
  self.rebuild(e)
65
80
  return self._shares or []
66
81
 
67
-
68
82
  def get_hostname(self):
69
- '''
83
+ """
70
84
  Get the hostname from the SMB connection
71
- '''
85
+ """
72
86
  try:
73
87
  conn = SMBConnection(
74
88
  self.server,
75
89
  self.server,
76
90
  None,
77
- 445,
91
+ self.port,
78
92
  timeout=10,
79
93
  )
80
94
  with suppress(Exception):
@@ -85,36 +99,36 @@ class SMBClient:
85
99
  # Get the server name from SMB
86
100
  self.hostname = str(conn.getServerName()).strip().replace("\x00", "").lower()
87
101
  if self.hostname:
88
- log.debug(f'{self.server}: Got hostname: {self.hostname}')
102
+ log.debug(f"{self.server}: Got hostname: {self.hostname}")
89
103
  else:
90
- log.debug(f'{self.server}: No hostname found')
104
+ log.debug(f"{self.server}: No hostname found")
91
105
  except Exception as e:
92
- log.debug(f'{self.server}: Error getting hostname from SMB: {e}')
106
+ log.debug(f"{self.server}: Error getting hostname from SMB: {e}")
93
107
  self.hostname = ""
94
108
 
95
109
  if self.dns_domain is None:
96
110
  try:
97
111
  self.dns_domain = str(conn.getServerDNSDomainName()).strip().replace("\x00", "").lower()
98
112
  if self.dns_domain:
99
- log.debug(f'{self.server}: Got DNS domain: {self.dns_domain}')
113
+ log.debug(f"{self.server}: Got DNS domain: {self.dns_domain}")
100
114
  else:
101
- log.debug(f'{self.server}: No DNS domain found')
115
+ log.debug(f"{self.server}: No DNS domain found")
102
116
  except Exception as e:
103
- log.debug(f'{self.server}: Error getting DNS domain: {e}')
104
- self.dns_domain = (self.domain if self.domain else "")
117
+ log.debug(f"{self.server}: Error getting DNS domain: {e}")
118
+ self.dns_domain = self.domain if self.domain else ""
105
119
 
106
120
  except Exception as e:
107
- log.debug(f'{self.server}: Error getting hostname: {e}')
121
+ log.debug(f"{self.server}: Error getting hostname: {e}")
108
122
 
109
123
  return self.hostname, self.domain
110
124
 
111
125
  def login(self, refresh=False, first_try=True):
112
- '''
126
+ """
113
127
  Create a new SMBConnection object (if there isn't one already or if refresh is True)
114
128
  Attempt to log in, and switch to null session if logon fails
115
129
  Return True if logon succeeded
116
130
  Return False if logon failed
117
- '''
131
+ """
118
132
 
119
133
  target_server = self.server
120
134
  if self.use_kerberos:
@@ -126,20 +140,19 @@ class SMBClient:
126
140
 
127
141
  if self.conn is None or refresh:
128
142
  try:
129
- self.conn = SMBConnection(target_server, target_server, sess_port=445, timeout=20)
143
+ self.conn = SMBConnection(target_server, target_server, sess_port=self.port, timeout=20)
130
144
  except Exception as e:
131
145
  log.debug(impacket_error(e))
132
146
  return None
133
147
 
134
148
  try:
135
-
136
- if self.username in [None, '', 'Guest'] and first_try:
149
+ if self.username in [None, "", "Guest"] and first_try:
137
150
  # skip to guest / null session
138
151
  assert False
139
152
 
140
153
  user_str = self.username
141
154
  if self.domain:
142
- user_str = f'{self.domain}\\{self.username}'
155
+ user_str = f"{self.domain}\\{self.username}"
143
156
  log.debug(f'{target_server} ({self.server}): Authenticating as "{user_str}"')
144
157
 
145
158
  if self.use_kerberos:
@@ -156,7 +169,7 @@ class SMBClient:
156
169
  elif self.nthash and not self.password:
157
170
  self.conn.login(
158
171
  self.username,
159
- '',
172
+ "",
160
173
  lmhash=self.lmhash,
161
174
  nthash=self.nthash,
162
175
  domain=self.domain,
@@ -173,28 +186,26 @@ class SMBClient:
173
186
  return True
174
187
 
175
188
  except Exception as e:
176
-
177
189
  if type(e) != AssertionError:
178
190
  e = self.handle_impacket_error(e, display=True)
179
191
 
180
192
  # try guest account, then null session if logon failed
181
193
  if first_try:
182
-
183
- bad_statuses = ['LOGON_FAIL', 'PASSWORD_EXPIRED', 'LOCKED_OUT', 'SESSION_DELETED']
194
+ bad_statuses = ["LOGON_FAIL", "PASSWORD_EXPIRED", "LOCKED_OUT", "SESSION_DELETED"]
184
195
  if any([s in str(e) for s in bad_statuses]):
185
196
  for s in bad_statuses:
186
197
  if s in str(e):
187
- log.warning(f'{self.server}: {s}: {self.username}')
198
+ log.warning(f"{self.server}: {s}: {self.username}")
188
199
 
189
- log.debug(f'{self.server}: Trying guest session')
190
- self.username = 'Guest'
191
- self.password = ''
192
- self.domain = ''
193
- self.nthash = ''
200
+ log.debug(f"{self.server}: Trying guest session")
201
+ self.username = "Guest"
202
+ self.password = ""
203
+ self.domain = ""
204
+ self.nthash = ""
194
205
  guest_success = self.login(refresh=True, first_try=False)
195
206
  if not guest_success:
196
- log.debug(f'{self.server}: Switching to null session')
197
- self.username = ''
207
+ log.debug(f"{self.server}: Switching to null session")
208
+ self.username = ""
198
209
  self.login(refresh=True, first_try=False)
199
210
 
200
211
  return False
@@ -202,54 +213,51 @@ class SMBClient:
202
213
  else:
203
214
  return True
204
215
 
205
-
206
216
  def ls(self, share, path):
207
- '''
217
+ """
208
218
  List files in share/path
209
219
  Raise FileListError if there's a problem
210
220
  @byt3bl33d3r it's really not that bad
211
- '''
221
+ """
212
222
 
213
- nt_path = ntpath.normpath(f'{path}\\*')
223
+ nt_path = ntpath.normpath(f"{path}\\*")
214
224
 
215
225
  # for every file/dir in "path"
216
226
  try:
217
227
  for f in self.conn.listPath(share, nt_path):
218
228
  # exclude current and parent directory
219
- if f.get_longname() not in ['', '.', '..']:
229
+ if f.get_longname() not in ["", ".", ".."]:
220
230
  yield f
221
231
  except Exception as e:
222
232
  e = self.handle_impacket_error(e)
223
233
  raise FileListError(f'{e.args}: Error listing files at "{share}{nt_path}"')
224
234
 
225
-
226
- def rebuild(self, error=''):
227
- '''
235
+ def rebuild(self, error=""):
236
+ """
228
237
  Rebuild our SMBConnection() if it gets borked
229
- '''
230
- log.debug(f'Rebuilding connection to {self.server} after error: {error}')
238
+ """
239
+ log.debug(f"Rebuilding connection to {self.server} after error: {error}")
231
240
  self.login(refresh=True)
232
241
 
233
-
234
- def handle_impacket_error(self, e, share='', filename='', display=False):
235
- '''
242
+ def handle_impacket_error(self, e, share="", filename="", display=False):
243
+ """
236
244
  Handle arbitrary Impacket errors
237
245
  this is needed because the library doesn't implement proper inheritance for its exceptions
238
- '''
239
- resource_str = '/'.join([self.server, share, filename]).rstrip('/')
246
+ """
247
+ resource_str = "/".join([self.server, share, filename]).rstrip("/")
240
248
 
241
249
  if type(e) == KeyboardInterrupt:
242
250
  raise
243
251
  elif type(e) in (NetBIOSError, NetBIOSTimeout, BrokenPipeError, SessionError, CSessionError):
244
252
  # the connection may need to be rebuilt
245
253
  if type(e) in (SessionError, CSessionError):
246
- if any([x in str(e) for x in ('PASSWORD_EXPIRED',)]):
254
+ if any([x in str(e) for x in ("PASSWORD_EXPIRED",)]):
247
255
  self.rebuild(e)
248
256
  else:
249
257
  self.rebuild(e)
250
258
  if type(e) in native_impacket_errors:
251
259
  e = impacket_error(e)
252
260
  if display:
253
- log.debug(f'{resource_str}: {str(e)[:150]}')
261
+ log.debug(f"{resource_str}: {str(e)[:150]}")
254
262
 
255
263
  return e
man_spider/lib/spider.py CHANGED
@@ -1,53 +1,52 @@
1
1
  import re
2
- import sys
3
2
  import queue
4
- import threading
5
3
  from time import sleep
6
4
  import multiprocessing
7
5
  from pathlib import Path
8
- from .spiderling import *
9
- from .parser import FileParser
6
+
7
+ from man_spider.lib.spiderling import *
8
+ from man_spider.lib.parser import FileParser
10
9
 
11
10
  # set up logging
12
- log = logging.getLogger('manspider')
11
+ log = logging.getLogger("manspider")
13
12
 
14
13
 
15
14
  class MANSPIDER:
16
-
17
15
  def __init__(self, options):
18
16
 
19
- self.targets = options.targets
20
- self.threads = options.threads
21
- self.maxdepth = options.maxdepth
22
- self.quiet = options.quiet
17
+ self.targets = options.targets
18
+ self.threads = options.threads
19
+ self.maxdepth = options.maxdepth
20
+ self.quiet = options.quiet
23
21
 
24
- self.username = options.username
25
- self.password = options.password
26
- self.domain = options.domain
27
- self.nthash = options.hash
28
- self.use_kerberos = options.kerberos
29
- self.aes_key = options.aes_key
30
- self.dc_ip = options.dc_ip
31
- self.max_failed_logons = options.max_failed_logons
32
- self.max_filesize = options.max_filesize
22
+ self.username = options.username
23
+ self.password = options.password
24
+ self.domain = options.domain
25
+ self.nthash = options.hash
26
+ self.use_kerberos = options.kerberos
27
+ self.aes_key = options.aes_key
28
+ self.dc_ip = options.dc_ip
29
+ self.max_failed_logons = options.max_failed_logons
30
+ self.max_filesize = options.max_filesize
33
31
 
34
- self.share_whitelist = options.sharenames
35
- self.share_blacklist = options.exclude_sharenames
32
+ self.share_whitelist = options.sharenames
33
+ self.share_blacklist = options.exclude_sharenames
36
34
 
37
- self.dir_whitelist = options.dirnames
38
- self.dir_blacklist = options.exclude_dirnames
35
+ self.dir_whitelist = options.dirnames
36
+ self.dir_blacklist = options.exclude_dirnames
39
37
 
40
- self.no_download = options.no_download
38
+ self.no_download = options.no_download
41
39
 
42
40
  # applies "or" logic instead of "and"
43
41
  # e.g. file is downloaded if filename OR extension OR content match
44
- self.or_logic = options.or_logic
42
+ self.or_logic = options.or_logic
43
+
44
+ self.extension_blacklist = options.exclude_extensions
45
+ self.file_extensions = options.extensions
45
46
 
46
- self.extension_blacklist= options.exclude_extensions
47
- self.file_extensions = options.extensions
48
47
  if self.file_extensions:
49
48
  extensions_str = '"' + '", "'.join(list(self.file_extensions)) + '"'
50
- log.info(f'Searching by file extension: {extensions_str}')
49
+ log.info(f"Searching by file extension: {extensions_str}")
51
50
 
52
51
  self.init_filename_filters(options.filenames)
53
52
  self.parser = FileParser(options.content, quiet=self.quiet)
@@ -62,20 +61,27 @@ class MANSPIDER:
62
61
  self.smb_client_cache = dict()
63
62
 
64
63
  # directory to store documents when searching contents
65
- self.tmp_dir = Path('/tmp/.manspider')
64
+ self.tmp_dir = Path("/tmp/.manspider")
66
65
  self.tmp_dir.mkdir(exist_ok=True)
67
66
 
68
67
  # directory to store matching documents
69
- self.loot_dir = Path.home() / '.manspider' / 'loot'
68
+ self.loot_dir = Path.home() / ".manspider" / "loot"
69
+
70
+ if options.loot_dir:
71
+ self.loot_dir = Path(options.loot_dir)
70
72
 
71
- if(options.loot_dir):
72
- self.loot_dir=Path(options.loot_dir)
73
-
74
73
  self.loot_dir.mkdir(parents=True, exist_ok=True)
75
74
 
76
75
  if not options.no_download:
77
- log.info(f'Matching files will be downloaded to {self.loot_dir}')
76
+ log.info(f"Matching files will be downloaded to {self.loot_dir}")
77
+
78
+ self.modified_after = options.modified_after
79
+ self.modified_before = options.modified_before
78
80
 
81
+ if self.modified_after:
82
+ log.info(f"Filtering files modified after: {self.modified_after.strftime('%Y-%m-%d')}")
83
+ if self.modified_before:
84
+ log.info(f"Filtering files modified before: {self.modified_before.strftime('%Y-%m-%d')}")
79
85
 
80
86
  def start(self):
81
87
 
@@ -100,7 +106,7 @@ class MANSPIDER:
100
106
  continue
101
107
 
102
108
  # save on CPU
103
- sleep(.1)
109
+ sleep(0.1)
104
110
 
105
111
  while 1:
106
112
  self.check_spiderling_queue()
@@ -111,23 +117,19 @@ class MANSPIDER:
111
117
  # make sure the queue is empty
112
118
  self.check_spiderling_queue()
113
119
 
114
-
115
-
116
120
  def init_file_extensions(self, file_extensions):
117
- '''
121
+ """
118
122
  Get ready to search by file extension
119
- '''
123
+ """
120
124
 
121
125
  self.file_extensions = FileExtensions()
122
126
  if file_extensions:
123
127
  self.file_extensions.update(file_extensions)
124
-
125
-
126
128
 
127
129
  def init_filename_filters(self, filename_filters):
128
- '''
130
+ """
129
131
  Get ready to search by filename
130
- '''
132
+ """
131
133
 
132
134
  # strings to look for in filenames
133
135
  # if empty, all filenames are matched
@@ -135,23 +137,22 @@ class MANSPIDER:
135
137
  for f in filename_filters:
136
138
  regex_str = str(f)
137
139
  try:
138
- if not any([f.startswith(x) for x in ['^', '.*']]):
139
- regex_str = rf'.*{regex_str}'
140
- if not any([f.endswith(x) for x in ['$', '.*']]):
141
- regex_str = rf'{regex_str}.*'
140
+ if not any([f.startswith(x) for x in ["^", ".*"]]):
141
+ regex_str = rf".*{regex_str}"
142
+ if not any([f.endswith(x) for x in ["$", ".*"]]):
143
+ regex_str = rf"{regex_str}.*"
142
144
  self.filename_filters.append(re.compile(regex_str, re.I))
143
145
  except re.error as e:
144
146
  log.error(f'Unsupported filename regex "{f}": {e}')
145
147
  sleep(1)
146
148
  if self.filename_filters:
147
149
  filename_filter_str = '"' + '", "'.join([f.pattern for f in self.filename_filters]) + '"'
148
- log.info(f'Searching by filename: {filename_filter_str}')
149
-
150
+ log.info(f"Searching by filename: {filename_filter_str}")
150
151
 
151
152
  def check_spiderling_queue(self):
152
- '''
153
+ """
153
154
  Empty the spiderling queue
154
- '''
155
+ """
155
156
 
156
157
  while 1:
157
158
  try:
@@ -161,55 +162,53 @@ class MANSPIDER:
161
162
  except queue.Empty:
162
163
  break
163
164
 
164
-
165
165
  def process_message(self, message):
166
- '''
166
+ """
167
167
  Process messages from spiderlings
168
168
  Log messages, errors, files, etc.
169
- '''
170
- if message.type == 'a':
169
+ """
170
+ if message.type == "a":
171
171
  if message.content == False:
172
172
  self.failed_logons += 1
173
173
  if self.lockout_threshold():
174
- log.error(f'REACHED MAXIMUM FAILED LOGONS OF {self.max_failed_logons:,}')
175
- log.error('KILLING EXISTING SPIDERLINGS AND CONTINUING WITH GUEST/NULL SESSIONS')
176
- #for spiderling in self.spiderling_pool:
174
+ log.error(f"REACHED MAXIMUM FAILED LOGONS OF {self.max_failed_logons:,}")
175
+ log.error("KILLING EXISTING SPIDERLINGS AND CONTINUING WITH GUEST/NULL SESSIONS")
176
+ # for spiderling in self.spiderling_pool:
177
177
  # spiderling.kill()
178
- self.username = ''
179
- self.password = ''
180
- self.nthash = ''
181
- self.domain = ''
182
-
178
+ self.username = ""
179
+ self.password = ""
180
+ self.nthash = ""
181
+ self.domain = ""
183
182
 
184
183
  def lockout_threshold(self):
185
- '''
184
+ """
186
185
  Return True if we've reached max failed logons
187
- '''
186
+ """
188
187
 
189
188
  if self.max_failed_logons is not None:
190
189
  if self.failed_logons >= self.max_failed_logons and self.domain:
191
190
  return True
192
191
  return False
193
192
 
194
-
195
193
  def get_smb_client(self, target):
196
- '''
194
+ """
197
195
  Check if we already have an smb_client cached
198
196
  If not, then create it
199
- '''
197
+ """
200
198
 
201
199
  smb_client = self.smb_client_cache.get(target, None)
202
200
 
203
201
  if smb_client is None:
204
202
  smb_client = SMBClient(
205
- target,
203
+ target.host,
206
204
  self.username,
207
205
  self.password,
208
206
  self.domain,
209
207
  self.nthash,
210
208
  self.use_kerberos,
211
209
  self.aes_key,
212
- self.dc_ip
210
+ self.dc_ip,
211
+ port=target.port,
213
212
  )
214
213
  logon_result = smb_client.login()
215
214
  if logon_result == False: