souleyez 2.22.0__py3-none-any.whl → 2.26.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.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

Files changed (35) hide show
  1. souleyez/__init__.py +1 -1
  2. souleyez/assets/__init__.py +1 -0
  3. souleyez/assets/souleyez-icon.png +0 -0
  4. souleyez/core/msf_sync_manager.py +15 -5
  5. souleyez/core/tool_chaining.py +126 -26
  6. souleyez/detection/validator.py +4 -2
  7. souleyez/docs/README.md +2 -2
  8. souleyez/docs/user-guide/installation.md +14 -1
  9. souleyez/engine/background.py +17 -1
  10. souleyez/engine/result_handler.py +89 -0
  11. souleyez/main.py +103 -4
  12. souleyez/parsers/crackmapexec_parser.py +101 -43
  13. souleyez/parsers/dnsrecon_parser.py +50 -35
  14. souleyez/parsers/enum4linux_parser.py +101 -21
  15. souleyez/parsers/http_fingerprint_parser.py +319 -0
  16. souleyez/parsers/hydra_parser.py +56 -5
  17. souleyez/parsers/impacket_parser.py +123 -44
  18. souleyez/parsers/john_parser.py +47 -14
  19. souleyez/parsers/msf_parser.py +20 -5
  20. souleyez/parsers/nmap_parser.py +48 -27
  21. souleyez/parsers/smbmap_parser.py +39 -23
  22. souleyez/parsers/sqlmap_parser.py +18 -9
  23. souleyez/parsers/theharvester_parser.py +21 -13
  24. souleyez/plugins/http_fingerprint.py +592 -0
  25. souleyez/plugins/nuclei.py +41 -17
  26. souleyez/ui/interactive.py +99 -7
  27. souleyez/ui/setup_wizard.py +93 -5
  28. souleyez/ui/tool_setup.py +52 -52
  29. souleyez/utils/tool_checker.py +45 -5
  30. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/METADATA +16 -3
  31. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/RECORD +35 -31
  32. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/WHEEL +0 -0
  33. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/entry_points.txt +0 -0
  34. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/licenses/LICENSE +0 -0
  35. {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ souleyez.parsers.http_fingerprint_parser
4
+
5
+ Parses HTTP fingerprint output to extract WAF, CDN, managed hosting,
6
+ and technology information.
7
+ """
8
+ import json
9
+ import re
10
+ from typing import Dict, Any, List, Optional
11
+
12
+
13
+ def parse_http_fingerprint_output(output: str, target: str = "") -> Dict[str, Any]:
14
+ """
15
+ Parse HTTP fingerprint output and extract detection results.
16
+
17
+ Args:
18
+ output: Raw output from http_fingerprint plugin
19
+ target: Target URL from job
20
+
21
+ Returns:
22
+ Dict with structure:
23
+ {
24
+ 'target': str,
25
+ 'status_code': int,
26
+ 'server': str,
27
+ 'server_version': str,
28
+ 'waf': [str],
29
+ 'cdn': [str],
30
+ 'managed_hosting': str or None,
31
+ 'technologies': [str],
32
+ 'tls': {'version': str, 'cipher': str, 'bits': int},
33
+ 'headers': {str: str},
34
+ 'redirect_url': str or None,
35
+ 'error': str or None,
36
+ }
37
+ """
38
+ result = {
39
+ 'target': target,
40
+ 'status_code': None,
41
+ 'server': None,
42
+ 'server_version': None,
43
+ 'waf': [],
44
+ 'cdn': [],
45
+ 'managed_hosting': None,
46
+ 'technologies': [],
47
+ 'tls': None,
48
+ 'headers': {},
49
+ 'redirect_url': None,
50
+ 'error': None,
51
+ }
52
+
53
+ # Try to extract JSON result first (most reliable)
54
+ json_match = re.search(r'=== JSON_RESULT ===\n(.+?)\n=== END_JSON_RESULT ===', output, re.DOTALL)
55
+ if json_match:
56
+ try:
57
+ json_result = json.loads(json_match.group(1))
58
+ result.update(json_result)
59
+ result['target'] = target or result.get('target', '')
60
+ return result
61
+ except json.JSONDecodeError:
62
+ pass
63
+
64
+ # Fall back to parsing text output
65
+ lines = output.split('\n')
66
+
67
+ for line in lines:
68
+ line = line.strip()
69
+
70
+ # Parse HTTP status
71
+ if line.startswith('HTTP Status:'):
72
+ match = re.search(r'HTTP Status:\s+(\d+)', line)
73
+ if match:
74
+ result['status_code'] = int(match.group(1))
75
+
76
+ # Parse server
77
+ elif line.startswith('Server:'):
78
+ result['server'] = line.replace('Server:', '').strip()
79
+
80
+ # Parse redirect
81
+ elif line.startswith('Redirected to:'):
82
+ result['redirect_url'] = line.replace('Redirected to:', '').strip()
83
+
84
+ # Parse TLS
85
+ elif line.startswith('TLS:'):
86
+ match = re.search(r'TLS:\s+(\S+)\s+\((.+?)\)', line)
87
+ if match:
88
+ result['tls'] = {
89
+ 'version': match.group(1),
90
+ 'cipher': match.group(2),
91
+ }
92
+
93
+ # Parse managed hosting
94
+ elif line.startswith('MANAGED HOSTING DETECTED:'):
95
+ result['managed_hosting'] = line.replace('MANAGED HOSTING DETECTED:', '').strip()
96
+
97
+ # Parse WAF (multi-line section)
98
+ elif line.startswith('WAF/Protection Detected:'):
99
+ continue # Header line, actual entries follow
100
+
101
+ # Parse CDN (multi-line section)
102
+ elif line.startswith('CDN Detected:'):
103
+ continue # Header line, actual entries follow
104
+
105
+ # Parse technologies (multi-line section)
106
+ elif line.startswith('Technologies:'):
107
+ continue # Header line, actual entries follow
108
+
109
+ # Parse list items (WAF, CDN, Technologies)
110
+ elif line.startswith('- '):
111
+ item = line[2:].strip()
112
+ # Determine which list this belongs to based on context
113
+ # This is a simple heuristic - JSON parsing is more reliable
114
+ if any(waf_keyword in item.lower() for waf_keyword in ['waf', 'cloudflare', 'akamai', 'imperva', 'sucuri', 'f5']):
115
+ if item not in result['waf']:
116
+ result['waf'].append(item)
117
+ elif any(cdn_keyword in item.lower() for cdn_keyword in ['cdn', 'cloudfront', 'fastly', 'varnish', 'edge']):
118
+ if item not in result['cdn']:
119
+ result['cdn'].append(item)
120
+ else:
121
+ if item not in result['technologies']:
122
+ result['technologies'].append(item)
123
+
124
+ # Parse error
125
+ elif line.startswith('ERROR:'):
126
+ result['error'] = line.replace('ERROR:', '').strip()
127
+
128
+ return result
129
+
130
+
131
+ def is_managed_hosting(parsed_data: Dict[str, Any]) -> bool:
132
+ """
133
+ Check if target is a managed hosting platform.
134
+
135
+ Args:
136
+ parsed_data: Output from parse_http_fingerprint_output()
137
+
138
+ Returns:
139
+ True if managed hosting platform detected
140
+ """
141
+ return parsed_data.get('managed_hosting') is not None
142
+
143
+
144
+ def get_managed_hosting_platform(parsed_data: Dict[str, Any]) -> Optional[str]:
145
+ """
146
+ Get the name of the managed hosting platform.
147
+
148
+ Args:
149
+ parsed_data: Output from parse_http_fingerprint_output()
150
+
151
+ Returns:
152
+ Platform name or None
153
+ """
154
+ return parsed_data.get('managed_hosting')
155
+
156
+
157
+ def has_waf(parsed_data: Dict[str, Any]) -> bool:
158
+ """
159
+ Check if WAF is detected.
160
+
161
+ Args:
162
+ parsed_data: Output from parse_http_fingerprint_output()
163
+
164
+ Returns:
165
+ True if WAF detected
166
+ """
167
+ return len(parsed_data.get('waf', [])) > 0
168
+
169
+
170
+ def get_wafs(parsed_data: Dict[str, Any]) -> List[str]:
171
+ """
172
+ Get list of detected WAFs.
173
+
174
+ Args:
175
+ parsed_data: Output from parse_http_fingerprint_output()
176
+
177
+ Returns:
178
+ List of WAF names
179
+ """
180
+ return parsed_data.get('waf', [])
181
+
182
+
183
+ def has_cdn(parsed_data: Dict[str, Any]) -> bool:
184
+ """
185
+ Check if CDN is detected.
186
+
187
+ Args:
188
+ parsed_data: Output from parse_http_fingerprint_output()
189
+
190
+ Returns:
191
+ True if CDN detected
192
+ """
193
+ return len(parsed_data.get('cdn', [])) > 0
194
+
195
+
196
+ def get_cdns(parsed_data: Dict[str, Any]) -> List[str]:
197
+ """
198
+ Get list of detected CDNs.
199
+
200
+ Args:
201
+ parsed_data: Output from parse_http_fingerprint_output()
202
+
203
+ Returns:
204
+ List of CDN names
205
+ """
206
+ return parsed_data.get('cdn', [])
207
+
208
+
209
+ def build_fingerprint_context(parsed_data: Dict[str, Any]) -> Dict[str, Any]:
210
+ """
211
+ Build context dict for use in tool chaining.
212
+
213
+ This is used to pass fingerprint data to downstream tools
214
+ so they can make smarter decisions.
215
+
216
+ Args:
217
+ parsed_data: Output from parse_http_fingerprint_output()
218
+
219
+ Returns:
220
+ Context dict for tool chaining
221
+ """
222
+ return {
223
+ 'http_fingerprint': {
224
+ 'managed_hosting': parsed_data.get('managed_hosting'),
225
+ 'waf': parsed_data.get('waf', []),
226
+ 'cdn': parsed_data.get('cdn', []),
227
+ 'server': parsed_data.get('server'),
228
+ 'technologies': parsed_data.get('technologies', []),
229
+ 'status_code': parsed_data.get('status_code'),
230
+ }
231
+ }
232
+
233
+
234
+ def get_tool_recommendations(parsed_data: Dict[str, Any]) -> Dict[str, Any]:
235
+ """
236
+ Get recommendations for tool configuration based on fingerprint.
237
+
238
+ Args:
239
+ parsed_data: Output from parse_http_fingerprint_output()
240
+
241
+ Returns:
242
+ Dict with tool-specific recommendations
243
+ """
244
+ recommendations = {
245
+ 'nikto': {
246
+ 'skip_cgi': False,
247
+ 'extra_args': [],
248
+ 'reason': None,
249
+ },
250
+ 'nuclei': {
251
+ 'extra_args': [],
252
+ 'skip_tags': [],
253
+ 'reason': None,
254
+ },
255
+ 'sqlmap': {
256
+ 'tamper_scripts': [],
257
+ 'extra_args': [],
258
+ 'reason': None,
259
+ },
260
+ 'general': {
261
+ 'notes': [],
262
+ }
263
+ }
264
+
265
+ # Managed hosting recommendations
266
+ if parsed_data.get('managed_hosting'):
267
+ platform = parsed_data['managed_hosting']
268
+ recommendations['nikto']['skip_cgi'] = True
269
+ recommendations['nikto']['extra_args'] = ['-C', 'none', '-Tuning', 'x6']
270
+ recommendations['nikto']['reason'] = f"Managed hosting ({platform}) - CGI enumeration skipped"
271
+
272
+ recommendations['general']['notes'].append(
273
+ f"Target is hosted on {platform} - limited vulnerability surface expected"
274
+ )
275
+
276
+ # WAF recommendations
277
+ wafs = parsed_data.get('waf', [])
278
+ if wafs:
279
+ waf_list = ', '.join(wafs)
280
+ recommendations['general']['notes'].append(f"WAF detected: {waf_list}")
281
+
282
+ # SQLMap tamper scripts for common WAFs
283
+ for waf in wafs:
284
+ waf_lower = waf.lower()
285
+ if 'cloudflare' in waf_lower:
286
+ recommendations['sqlmap']['tamper_scripts'].extend(['between', 'randomcase', 'space2comment'])
287
+ elif 'akamai' in waf_lower:
288
+ recommendations['sqlmap']['tamper_scripts'].extend(['charencode', 'space2plus'])
289
+ elif 'imperva' in waf_lower or 'incapsula' in waf_lower:
290
+ recommendations['sqlmap']['tamper_scripts'].extend(['randomcase', 'between'])
291
+
292
+ if recommendations['sqlmap']['tamper_scripts']:
293
+ # Dedupe
294
+ recommendations['sqlmap']['tamper_scripts'] = list(set(recommendations['sqlmap']['tamper_scripts']))
295
+ recommendations['sqlmap']['reason'] = f"WAF bypass tamper scripts for {waf_list}"
296
+
297
+ # CDN recommendations
298
+ cdns = parsed_data.get('cdn', [])
299
+ if cdns:
300
+ cdn_list = ', '.join(cdns)
301
+ recommendations['general']['notes'].append(
302
+ f"CDN detected: {cdn_list} - responses may be cached, hitting edge not origin"
303
+ )
304
+
305
+ return recommendations
306
+
307
+
308
+ # Export the main functions
309
+ __all__ = [
310
+ 'parse_http_fingerprint_output',
311
+ 'is_managed_hosting',
312
+ 'get_managed_hosting_platform',
313
+ 'has_waf',
314
+ 'get_wafs',
315
+ 'has_cdn',
316
+ 'get_cdns',
317
+ 'build_fingerprint_context',
318
+ 'get_tool_recommendations',
319
+ ]
@@ -85,13 +85,24 @@ def parse_hydra_output(output: str, target: str = "") -> Dict[str, Any]:
85
85
  'password': attempt_match.group(3)
86
86
  }
87
87
 
88
- # Parse successful login lines
89
- # Format: [PORT][SERVICE] host: HOST login: USER password: PASS
88
+ # Parse successful login lines with multiple format support
89
+ # Format 1: [PORT][SERVICE] host: HOST login: USER password: PASS
90
+ # Format 2: [PORT][SERVICE] host: HOST login: USER password: PASS (single space)
91
+ # Format 3: [SERVICE][PORT] host: HOST login: USER password: PASS (swapped)
92
+ # Format 4: [PORT][SERVICE] HOST login: USER password: PASS (no "host:")
93
+
94
+ login_match = None
95
+ port = None
96
+ service = None
97
+ host = None
98
+ username = None
99
+ password = None
100
+
101
+ # Try standard format: [PORT][SERVICE] host: HOST login: USER password: PASS
90
102
  login_match = re.search(
91
- r'\[(\d+)\]\[([\w-]+)\]\s+host:\s+(\S+)\s+login:\s+(\S+)\s+password:\s+(.+)',
92
- line_stripped
103
+ r'\[(\d+)\]\[([\w-]+)\]\s+host:\s*(\S+)\s+login:\s*(\S+)\s+password:\s*(.+)',
104
+ line_stripped, re.IGNORECASE
93
105
  )
94
-
95
106
  if login_match:
96
107
  port = int(login_match.group(1))
97
108
  service = login_match.group(2).lower()
@@ -99,6 +110,46 @@ def parse_hydra_output(output: str, target: str = "") -> Dict[str, Any]:
99
110
  username = login_match.group(4)
100
111
  password = login_match.group(5).strip()
101
112
 
113
+ # Try swapped format: [SERVICE][PORT]
114
+ if not login_match:
115
+ login_match = re.search(
116
+ r'\[([\w-]+)\]\[(\d+)\]\s+host:\s*(\S+)\s+login:\s*(\S+)\s+password:\s*(.+)',
117
+ line_stripped, re.IGNORECASE
118
+ )
119
+ if login_match:
120
+ service = login_match.group(1).lower()
121
+ port = int(login_match.group(2))
122
+ host = login_match.group(3)
123
+ username = login_match.group(4)
124
+ password = login_match.group(5).strip()
125
+
126
+ # Try format without "host:" label
127
+ if not login_match:
128
+ login_match = re.search(
129
+ r'\[(\d+)\]\[([\w-]+)\]\s+(\d+\.\d+\.\d+\.\d+|\S+)\s+login:\s*(\S+)\s+password:\s*(.+)',
130
+ line_stripped, re.IGNORECASE
131
+ )
132
+ if login_match:
133
+ port = int(login_match.group(1))
134
+ service = login_match.group(2).lower()
135
+ host = login_match.group(3)
136
+ username = login_match.group(4)
137
+ password = login_match.group(5).strip()
138
+
139
+ # Try flexible format with any whitespace between fields
140
+ if not login_match:
141
+ login_match = re.search(
142
+ r'\[(\d+)\]\[([\w-]+)\].*?(?:host:?\s*)?(\d+\.\d+\.\d+\.\d+|\S+\.\S+).*?login:?\s*(\S+).*?password:?\s*(.+)',
143
+ line_stripped, re.IGNORECASE
144
+ )
145
+ if login_match:
146
+ port = int(login_match.group(1))
147
+ service = login_match.group(2).lower()
148
+ host = login_match.group(3)
149
+ username = login_match.group(4)
150
+ password = login_match.group(5).strip()
151
+
152
+ if login_match and port and service and username:
102
153
  # Store service info if not already set
103
154
  if not result['service']:
104
155
  result['service'] = service
@@ -30,35 +30,80 @@ def parse_secretsdump(log_path: str, target: str) -> Dict[str, Any]:
30
30
  with open(log_path, 'r', encoding='utf-8') as f:
31
31
  content = f.read()
32
32
 
33
- # Parse NTLM hashes (format: username:RID:LM:NT:::)
34
- hash_pattern = r'([^:\s]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::'
35
- for match in re.finditer(hash_pattern, content):
36
- username, rid, lm_hash, nt_hash = match.groups()
37
-
38
- # Skip empty hashes
39
- if nt_hash.lower() == '31d6cfe0d16ae931b73c59d7e0c089c0':
40
- continue
41
-
42
- hashes.append({
43
- 'username': username,
44
- 'rid': rid,
45
- 'lm_hash': lm_hash,
46
- 'nt_hash': nt_hash,
47
- 'hash_type': 'NTLM'
48
- })
49
-
50
- # Parse plaintext passwords (format: DOMAIN\username:password)
51
- plaintext_pattern = r'([^\\:\s]+)\\([^:\s]+):(.+?)(?:\n|$)'
52
- for match in re.finditer(plaintext_pattern, content):
53
- domain, username, password = match.groups()
54
-
55
- if password and not password.startswith('(null)'):
56
- credentials.append({
57
- 'domain': domain,
33
+ # Parse NTLM hashes with multiple format support
34
+ # Format 1: username:RID:LM:NT::: (standard secretsdump)
35
+ # Format 2: username:RID:LM:NT:: (without trailing colon)
36
+ # Format 3: DOMAIN\username:RID:LM:NT::: (with domain prefix)
37
+ # Format 4: username:$NT$hash (simplified format)
38
+
39
+ # Standard format with 32-char hashes and trailing colons
40
+ hash_patterns = [
41
+ r'([^:\s\\]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::?', # Standard
42
+ r'([^:\s]+)\\([^:\s]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::?', # Domain\user
43
+ ]
44
+
45
+ for pattern in hash_patterns:
46
+ for match in re.finditer(pattern, content):
47
+ groups = match.groups()
48
+ if len(groups) == 4:
49
+ username, rid, lm_hash, nt_hash = groups
50
+ elif len(groups) == 5:
51
+ # Domain\username format
52
+ domain, username, rid, lm_hash, nt_hash = groups
53
+ username = f"{domain}\\{username}"
54
+ else:
55
+ continue
56
+
57
+ # Skip empty hashes (blank password indicator)
58
+ if nt_hash.lower() == '31d6cfe0d16ae931b73c59d7e0c089c0':
59
+ continue
60
+
61
+ hashes.append({
58
62
  'username': username,
59
- 'password': password,
60
- 'credential_type': 'plaintext'
63
+ 'rid': rid,
64
+ 'lm_hash': lm_hash,
65
+ 'nt_hash': nt_hash,
66
+ 'hash_type': 'NTLM'
61
67
  })
68
+
69
+ # Parse plaintext passwords with multiple format support
70
+ # Format 1: DOMAIN\username:password
71
+ # Format 2: DOMAIN\\username:password (escaped backslash)
72
+ # Format 3: username@DOMAIN:password
73
+ # Format 4: [*] DOMAIN\username:password (with prefix)
74
+
75
+ plaintext_patterns = [
76
+ r'([^\\:\s]+)[\\]+([^:\s]+):([^\n\r]+)', # DOMAIN\user:pass
77
+ r'([^@:\s]+)@([^:\s]+):([^\n\r]+)', # user@DOMAIN:pass
78
+ r'\[\*\]\s*([^\\:\s]+)[\\]+([^:\s]+):([^\n\r]+)', # [*] DOMAIN\user:pass
79
+ ]
80
+
81
+ for pattern in plaintext_patterns:
82
+ for match in re.finditer(pattern, content):
83
+ groups = match.groups()
84
+ if len(groups) == 3:
85
+ part1, part2, password = groups
86
+ password = password.strip()
87
+
88
+ # Skip null/empty passwords and hash-like values
89
+ if not password or password.startswith('(null)'):
90
+ continue
91
+ # Skip if password looks like a hash (32+ hex chars)
92
+ if re.match(r'^[0-9a-fA-F]{32,}$', password):
93
+ continue
94
+
95
+ # Determine domain/username based on pattern
96
+ if '@' in match.group(0):
97
+ username, domain = part1, part2
98
+ else:
99
+ domain, username = part1, part2
100
+
101
+ credentials.append({
102
+ 'domain': domain,
103
+ 'username': username,
104
+ 'password': password,
105
+ 'credential_type': 'plaintext'
106
+ })
62
107
 
63
108
  # Parse Kerberos keys (format: username:$krb5...)
64
109
  krb_pattern = r'([^:\s]+):(\$krb5[^\s]+)'
@@ -113,25 +158,46 @@ def parse_getnpusers(log_path: str, target: str) -> Dict[str, Any]:
113
158
  with open(log_path, 'r', encoding='utf-8') as f:
114
159
  content = f.read()
115
160
 
116
- # Parse AS-REP hashes (format: $krb5asrep$...)
117
- hash_pattern = r'\$krb5asrep\$23\$([^@]+)@([^:]+):([^\s]+)'
118
- for match in re.finditer(hash_pattern, content):
119
- username, domain, hash_value = match.groups()
120
-
121
- hashes.append({
122
- 'username': username,
123
- 'domain': domain,
124
- 'hash': f'$krb5asrep$23${username}@{domain}:{hash_value}',
125
- 'hash_type': 'AS-REP',
126
- 'crackable': True
127
- })
128
-
161
+ # Parse AS-REP hashes with multiple format support
162
+ # Format 1: $krb5asrep$23$user@DOMAIN:hash (etype 23)
163
+ # Format 2: $krb5asrep$18$user@DOMAIN:hash (etype 18)
164
+ # Format 3: $krb5asrep$user@DOMAIN:hash (no etype)
165
+ # Format 4: username:$krb5asrep... (username:hash format)
166
+
167
+ # Full format with etype: $krb5asrep$ETYPE$user@DOMAIN:hash
168
+ hash_patterns = [
169
+ r'\$krb5asrep\$(\d+)\$([^@]+)@([^:]+):([^\s]+)', # With etype
170
+ r'\$krb5asrep\$([^@$]+)@([^:]+):([^\s]+)', # Without etype
171
+ ]
172
+
173
+ for pattern in hash_patterns:
174
+ for match in re.finditer(pattern, content):
175
+ groups = match.groups()
176
+ if len(groups) == 4:
177
+ etype, username, domain, hash_value = groups
178
+ full_hash = f'$krb5asrep${etype}${username}@{domain}:{hash_value}'
179
+ elif len(groups) == 3:
180
+ username, domain, hash_value = groups
181
+ etype = '23' # Default etype
182
+ full_hash = f'$krb5asrep${username}@{domain}:{hash_value}'
183
+ else:
184
+ continue
185
+
186
+ hashes.append({
187
+ 'username': username,
188
+ 'domain': domain,
189
+ 'hash': full_hash,
190
+ 'hash_type': 'AS-REP',
191
+ 'etype': etype,
192
+ 'crackable': True
193
+ })
194
+
129
195
  # Also check for simple format (username:hash)
130
196
  if not hashes:
131
197
  simple_pattern = r'^([^:\s]+):(\$krb5asrep[^\s]+)'
132
198
  for match in re.finditer(simple_pattern, content, re.MULTILINE):
133
199
  username, hash_value = match.groups()
134
-
200
+
135
201
  hashes.append({
136
202
  'username': username,
137
203
  'hash': hash_value,
@@ -174,9 +240,22 @@ def parse_psexec(log_path: str, target: str) -> Dict[str, Any]:
174
240
  with open(log_path, 'r', encoding='utf-8') as f:
175
241
  content = f.read()
176
242
 
177
- # Check for successful connection
178
- if '[*] Requesting shares on' in content or 'C:\\Windows\\system32>' in content:
179
- success = True
243
+ # Check for successful connection with multiple indicators
244
+ success_indicators = [
245
+ '[*] Requesting shares on',
246
+ 'C:\\Windows\\system32>',
247
+ 'C:\\WINDOWS\\system32>',
248
+ '[*] Uploading',
249
+ '[*] Opening SVCManager',
250
+ 'Microsoft Windows', # Version banner
251
+ '[*] Starting service',
252
+ 'Process .+ created', # Process creation message
253
+ ]
254
+
255
+ for indicator in success_indicators:
256
+ if re.search(indicator, content, re.IGNORECASE):
257
+ success = True
258
+ break
180
259
 
181
260
  # Extract command output (everything after the prompt)
182
261
  output_lines = [line for line in content.split('\n') if line.strip()]
@@ -32,11 +32,18 @@ def parse_john_output(output: str, hash_file: str = None) -> Dict:
32
32
  if loaded_match:
33
33
  results['total_loaded'] = int(loaded_match.group(1))
34
34
 
35
- # Parse cracked passwords from live output
36
- # Format: "password (username)"
35
+ # Parse cracked passwords from live output with multiple format support
36
+ # Format 1: "password (username)"
37
+ # Format 2: "password (username)"
38
+ # Format 3: "username:password"
39
+ # Format 4: "password (username) [hash_type]"
37
40
  for line in output.split('\n'):
38
- # Look for cracked passwords in format: password (username)
39
- match = re.match(r'^(\S+)\s+\((\S+)\)\s*$', line.strip())
41
+ line = line.strip()
42
+ if not line or line.startswith('#') or line.startswith('['):
43
+ continue
44
+
45
+ # Try format: password (username) with optional hash type
46
+ match = re.match(r'^(\S+)\s+\(([^)]+)\)(?:\s+\[.+\])?\s*$', line)
40
47
  if match:
41
48
  password = match.group(1)
42
49
  username = match.group(2)
@@ -45,18 +52,44 @@ def parse_john_output(output: str, hash_file: str = None) -> Dict:
45
52
  'password': password,
46
53
  'source': 'john_live'
47
54
  })
48
-
49
- # Check session status
50
- if 'Session completed' in output:
55
+ continue
56
+
57
+ # Try format: username:password (from --show output)
58
+ if ':' in line and not line.startswith('Loaded'):
59
+ parts = line.split(':')
60
+ if len(parts) >= 2 and len(parts[0]) > 0 and len(parts[-1]) > 0:
61
+ # Skip if it looks like a hash (32+ hex chars)
62
+ if not re.match(r'^[0-9a-fA-F]{32,}$', parts[-1]):
63
+ username = parts[0]
64
+ password = parts[-1]
65
+ results['cracked'].append({
66
+ 'username': username,
67
+ 'password': password,
68
+ 'source': 'john_live'
69
+ })
70
+
71
+ # Check session status with multiple format support
72
+ if any(x in output for x in ['Session completed', 'session completed', 'Proceeding with next']):
51
73
  results['session_status'] = 'completed'
52
- elif 'Session aborted' in output:
74
+ elif any(x in output for x in ['Session aborted', 'session aborted', 'Interrupted']):
53
75
  results['session_status'] = 'aborted'
54
-
55
- # Parse summary line
56
- # Format: "2g 0:00:00:01 DONE..."
57
- summary_match = re.search(r'(\d+)g\s+[\d:]+\s+(DONE|Session)', output)
58
- if summary_match:
59
- results['total_cracked'] = int(summary_match.group(1))
76
+ elif 'No password hashes left to crack' in output:
77
+ results['session_status'] = 'completed'
78
+
79
+ # Parse summary line with multiple formats
80
+ # Format 1: "2g 0:00:00:01 DONE..."
81
+ # Format 2: "2g 0:00:00:01 100% DONE..."
82
+ # Format 3: "Session completed, 2g"
83
+ summary_patterns = [
84
+ r'(\d+)g\s+[\d:]+\s+(?:\d+%\s+)?(DONE|Session)',
85
+ r'Session completed[,\s]+(\d+)g',
86
+ r'(\d+)\s+password hashes? cracked',
87
+ ]
88
+ for pattern in summary_patterns:
89
+ summary_match = re.search(pattern, output, re.IGNORECASE)
90
+ if summary_match:
91
+ results['total_cracked'] = int(summary_match.group(1))
92
+ break
60
93
 
61
94
  # If hash_file provided, also parse john.pot or run --show
62
95
  if hash_file and os.path.isfile(hash_file):