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.
- souleyez/__init__.py +1 -1
- souleyez/assets/__init__.py +1 -0
- souleyez/assets/souleyez-icon.png +0 -0
- souleyez/core/msf_sync_manager.py +15 -5
- souleyez/core/tool_chaining.py +126 -26
- souleyez/detection/validator.py +4 -2
- souleyez/docs/README.md +2 -2
- souleyez/docs/user-guide/installation.md +14 -1
- souleyez/engine/background.py +17 -1
- souleyez/engine/result_handler.py +89 -0
- souleyez/main.py +103 -4
- souleyez/parsers/crackmapexec_parser.py +101 -43
- souleyez/parsers/dnsrecon_parser.py +50 -35
- souleyez/parsers/enum4linux_parser.py +101 -21
- souleyez/parsers/http_fingerprint_parser.py +319 -0
- souleyez/parsers/hydra_parser.py +56 -5
- souleyez/parsers/impacket_parser.py +123 -44
- souleyez/parsers/john_parser.py +47 -14
- souleyez/parsers/msf_parser.py +20 -5
- souleyez/parsers/nmap_parser.py +48 -27
- souleyez/parsers/smbmap_parser.py +39 -23
- souleyez/parsers/sqlmap_parser.py +18 -9
- souleyez/parsers/theharvester_parser.py +21 -13
- souleyez/plugins/http_fingerprint.py +592 -0
- souleyez/plugins/nuclei.py +41 -17
- souleyez/ui/interactive.py +99 -7
- souleyez/ui/setup_wizard.py +93 -5
- souleyez/ui/tool_setup.py +52 -52
- souleyez/utils/tool_checker.py +45 -5
- {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/METADATA +16 -3
- {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/RECORD +35 -31
- {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/WHEEL +0 -0
- {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.22.0.dist-info → souleyez-2.26.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
]
|
souleyez/parsers/hydra_parser.py
CHANGED
|
@@ -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
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
'
|
|
60
|
-
'
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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()]
|
souleyez/parsers/john_parser.py
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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'
|
|
74
|
+
elif any(x in output for x in ['Session aborted', 'session aborted', 'Interrupted']):
|
|
53
75
|
results['session_status'] = 'aborted'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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):
|