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