souleyez 2.43.26__py3-none-any.whl → 2.43.34__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.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/parsers/msf_parser.py
CHANGED
|
@@ -9,13 +9,13 @@ from typing import Dict, Any
|
|
|
9
9
|
def strip_ansi_codes(text: str) -> str:
|
|
10
10
|
"""Remove ANSI escape codes and other terminal control sequences from text."""
|
|
11
11
|
# Pattern 1: Standard ANSI escape sequences
|
|
12
|
-
text = re.sub(r
|
|
12
|
+
text = re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", text)
|
|
13
13
|
# Pattern 2: OSC sequences (Operating System Command)
|
|
14
|
-
text = re.sub(r
|
|
14
|
+
text = re.sub(r"\x1B\].*?\x07", "", text)
|
|
15
15
|
# Pattern 3: Simple color codes
|
|
16
|
-
text = re.sub(r
|
|
16
|
+
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
17
17
|
# Pattern 4: Carriage returns and other control chars (except newlines)
|
|
18
|
-
text = re.sub(r
|
|
18
|
+
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)
|
|
19
19
|
return text
|
|
20
20
|
|
|
21
21
|
|
|
@@ -36,47 +36,51 @@ def parse_msf_ssh_version(output: str, target: str) -> Dict[str, Any]:
|
|
|
36
36
|
clean_output = strip_ansi_codes(output)
|
|
37
37
|
|
|
38
38
|
# Extract SSH version
|
|
39
|
-
version_match = re.search(r
|
|
39
|
+
version_match = re.search(r"SSH server version:\s*(.+)", clean_output)
|
|
40
40
|
if version_match:
|
|
41
41
|
ssh_version = version_match.group(1).strip()
|
|
42
42
|
|
|
43
43
|
# Extract just the version number and product
|
|
44
44
|
# e.g., "SSH-2.0-OpenSSH_4.7p1 Debian-8ubuntu1"
|
|
45
|
-
product_match = re.search(r
|
|
45
|
+
product_match = re.search(r"SSH-[\d.]+-(\S+)", ssh_version)
|
|
46
46
|
if product_match:
|
|
47
47
|
product = product_match.group(1)
|
|
48
48
|
|
|
49
|
-
services.append(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
services.append(
|
|
50
|
+
{
|
|
51
|
+
"port": 22,
|
|
52
|
+
"protocol": "tcp",
|
|
53
|
+
"service_name": "ssh",
|
|
54
|
+
"service_version": product,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
55
57
|
|
|
56
58
|
# Extract OS information
|
|
57
59
|
os_version = None
|
|
58
|
-
os_match = re.search(r
|
|
60
|
+
os_match = re.search(r"os\.version\s+(.+)", clean_output)
|
|
59
61
|
if os_match:
|
|
60
62
|
os_version = os_match.group(1).strip()
|
|
61
63
|
|
|
62
64
|
os_vendor = None
|
|
63
|
-
vendor_match = re.search(r
|
|
65
|
+
vendor_match = re.search(r"os\.vendor\s+(.+)", clean_output)
|
|
64
66
|
if vendor_match:
|
|
65
67
|
os_vendor = vendor_match.group(1).strip()
|
|
66
68
|
|
|
67
69
|
if os_vendor and os_version:
|
|
68
|
-
findings.append(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
findings.append(
|
|
71
|
+
{
|
|
72
|
+
"title": f"SSH OS Detection: {os_vendor} {os_version}",
|
|
73
|
+
"severity": "info",
|
|
74
|
+
"description": f"SSH banner reveals OS: {os_vendor} {os_version}",
|
|
75
|
+
"port": 22,
|
|
76
|
+
"service": "ssh",
|
|
77
|
+
}
|
|
78
|
+
)
|
|
75
79
|
|
|
76
80
|
# Extract deprecated encryption algorithms
|
|
77
81
|
deprecated_algos = []
|
|
78
|
-
for line in clean_output.split(
|
|
79
|
-
if
|
|
82
|
+
for line in clean_output.split("\n"):
|
|
83
|
+
if "Deprecated" in line and "encryption.encryption" in line:
|
|
80
84
|
# Extract algorithm name
|
|
81
85
|
parts = line.split()
|
|
82
86
|
if len(parts) >= 2:
|
|
@@ -84,54 +88,57 @@ def parse_msf_ssh_version(output: str, target: str) -> Dict[str, Any]:
|
|
|
84
88
|
deprecated_algos.append(algo)
|
|
85
89
|
|
|
86
90
|
if deprecated_algos:
|
|
87
|
-
findings.append(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
findings.append(
|
|
92
|
+
{
|
|
93
|
+
"title": "SSH Deprecated Encryption Algorithms",
|
|
94
|
+
"severity": "medium",
|
|
95
|
+
"description": f'SSH server supports deprecated encryption: {", ".join(deprecated_algos[:5])}{"..." if len(deprecated_algos) > 5 else ""}',
|
|
96
|
+
"port": 22,
|
|
97
|
+
"service": "ssh",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
94
100
|
|
|
95
101
|
# Extract deprecated HMAC algorithms
|
|
96
102
|
deprecated_hmac = []
|
|
97
|
-
for line in clean_output.split(
|
|
98
|
-
if
|
|
103
|
+
for line in clean_output.split("\n"):
|
|
104
|
+
if "Deprecated" in line and "encryption.hmac" in line:
|
|
99
105
|
parts = line.split()
|
|
100
106
|
if len(parts) >= 2:
|
|
101
107
|
algo = parts[1]
|
|
102
108
|
deprecated_hmac.append(algo)
|
|
103
109
|
|
|
104
110
|
if deprecated_hmac:
|
|
105
|
-
findings.append(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
findings.append(
|
|
112
|
+
{
|
|
113
|
+
"title": "SSH Deprecated HMAC Algorithms",
|
|
114
|
+
"severity": "low",
|
|
115
|
+
"description": f'SSH server supports deprecated HMAC: {", ".join(deprecated_hmac[:3])}{"..." if len(deprecated_hmac) > 3 else ""}',
|
|
116
|
+
"port": 22,
|
|
117
|
+
"service": "ssh",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
112
120
|
|
|
113
121
|
# Extract weak key exchange methods
|
|
114
122
|
weak_kex = []
|
|
115
|
-
for line in clean_output.split(
|
|
116
|
-
if
|
|
123
|
+
for line in clean_output.split("\n"):
|
|
124
|
+
if "Deprecated" in line and "encryption.key_exchange" in line:
|
|
117
125
|
parts = line.split()
|
|
118
126
|
if len(parts) >= 2:
|
|
119
127
|
algo = parts[1]
|
|
120
128
|
weak_kex.append(algo)
|
|
121
129
|
|
|
122
130
|
if weak_kex:
|
|
123
|
-
findings.append(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
findings.append(
|
|
132
|
+
{
|
|
133
|
+
"title": "SSH Weak Key Exchange Methods",
|
|
134
|
+
"severity": "medium",
|
|
135
|
+
"description": f'SSH server supports weak key exchange: {", ".join(weak_kex)}',
|
|
136
|
+
"port": 22,
|
|
137
|
+
"service": "ssh",
|
|
138
|
+
}
|
|
139
|
+
)
|
|
130
140
|
|
|
131
|
-
return {
|
|
132
|
-
'services': services,
|
|
133
|
-
'findings': findings
|
|
134
|
-
}
|
|
141
|
+
return {"services": services, "findings": findings}
|
|
135
142
|
|
|
136
143
|
|
|
137
144
|
def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -152,9 +159,26 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
|
|
|
152
159
|
credentials = []
|
|
153
160
|
clean_output = strip_ansi_codes(output)
|
|
154
161
|
|
|
162
|
+
# Check if scan was skipped due to unsupported version
|
|
163
|
+
# MSF still reports "credential was successful" even when skipped, which is misleading
|
|
164
|
+
scan_skipped = (
|
|
165
|
+
"Unsupported target version" in clean_output and "Skipping" in clean_output
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if scan_skipped:
|
|
169
|
+
findings.append(
|
|
170
|
+
{
|
|
171
|
+
"title": "MySQL Login Scan Skipped",
|
|
172
|
+
"severity": "info",
|
|
173
|
+
"description": "MySQL version is unsupported by the mysql_login module. The target may be running a very old or very new MySQL version.",
|
|
174
|
+
"port": 3306,
|
|
175
|
+
"service": "mysql",
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
155
179
|
# Extract MySQL version
|
|
156
180
|
# Format: [+] 10.0.0.73:3306 - 10.0.0.73:3306 - Found remote MySQL version 5.0.51a
|
|
157
|
-
version_pattern = r
|
|
181
|
+
version_pattern = r"\[\+\]\s+([\d.]+):(\d+).*Found remote MySQL version\s+(\S+)"
|
|
158
182
|
version_match = re.search(version_pattern, clean_output)
|
|
159
183
|
|
|
160
184
|
if version_match:
|
|
@@ -162,27 +186,31 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
|
|
|
162
186
|
port = int(version_match.group(2))
|
|
163
187
|
mysql_version = version_match.group(3)
|
|
164
188
|
|
|
165
|
-
services.append(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
services.append(
|
|
190
|
+
{
|
|
191
|
+
"port": port,
|
|
192
|
+
"protocol": "tcp",
|
|
193
|
+
"service_name": "mysql",
|
|
194
|
+
"service_version": f"MySQL {mysql_version}",
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
findings.append(
|
|
199
|
+
{
|
|
200
|
+
"title": f"MySQL Version Detected: {mysql_version}",
|
|
201
|
+
"severity": "info",
|
|
202
|
+
"description": f"MySQL server version {mysql_version} detected.",
|
|
203
|
+
"port": port,
|
|
204
|
+
"service": "mysql",
|
|
205
|
+
}
|
|
206
|
+
)
|
|
179
207
|
|
|
180
208
|
# Check for successful logins
|
|
181
209
|
# Format: [+] 10.0.0.73:3306 - 10.0.0.73:3306 - Success: 'root:password'
|
|
182
210
|
# Format: [+] 10.0.0.73:3306 - Login Successful: root:password@database
|
|
183
211
|
success_patterns = [
|
|
184
212
|
r'\[\+\]\s+[\d.]+:(\d+).*Success:\s+[\'"]?([^:\'\"]+):([^\'\"@\s]+)',
|
|
185
|
-
r
|
|
213
|
+
r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]+):([^@\s]+)",
|
|
186
214
|
]
|
|
187
215
|
|
|
188
216
|
seen_creds = set()
|
|
@@ -196,27 +224,49 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
|
|
|
196
224
|
if cred_key not in seen_creds:
|
|
197
225
|
seen_creds.add(cred_key)
|
|
198
226
|
|
|
199
|
-
findings.append(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
findings.append(
|
|
228
|
+
{
|
|
229
|
+
"title": "MySQL Valid Credentials Found",
|
|
230
|
+
"severity": "critical",
|
|
231
|
+
"description": f"Valid MySQL credentials: {username}:{password}",
|
|
232
|
+
"port": port,
|
|
233
|
+
"service": "mysql",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
credentials.append(
|
|
238
|
+
{
|
|
239
|
+
"username": username,
|
|
240
|
+
"password": password,
|
|
241
|
+
"service": "mysql",
|
|
242
|
+
"port": port,
|
|
243
|
+
"status": "valid",
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Check for newer MSF format: "Bruteforce completed, X credential was successful"
|
|
248
|
+
# This format doesn't show the actual credential in logs
|
|
249
|
+
# IMPORTANT: Skip this if scan was skipped - MSF incorrectly reports success for pre-existing creds
|
|
250
|
+
bruteforce_pattern = (
|
|
251
|
+
r"\[\*\]\s+[\d.]+:(\d+).*Bruteforce completed,\s*(\d+)\s+credential.*successful"
|
|
252
|
+
)
|
|
253
|
+
bf_match = re.search(bruteforce_pattern, clean_output)
|
|
254
|
+
if (
|
|
255
|
+
bf_match and not credentials and not scan_skipped
|
|
256
|
+
): # Only if we didn't find explicit credentials AND scan wasn't skipped
|
|
257
|
+
port = int(bf_match.group(1))
|
|
258
|
+
num_creds = int(bf_match.group(2))
|
|
259
|
+
findings.append(
|
|
260
|
+
{
|
|
261
|
+
"title": f"MySQL Credentials Found ({num_creds} valid)",
|
|
262
|
+
"severity": "critical",
|
|
263
|
+
"description": f'{num_creds} valid MySQL credential(s) discovered. Run "creds" command in msfconsole to view credentials.',
|
|
264
|
+
"port": port,
|
|
265
|
+
"service": "mysql",
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return {"services": services, "findings": findings, "credentials": credentials}
|
|
220
270
|
|
|
221
271
|
|
|
222
272
|
def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
@@ -238,79 +288,94 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
|
|
|
238
288
|
clean_output = strip_ansi_codes(output)
|
|
239
289
|
|
|
240
290
|
# Determine service name from module
|
|
241
|
-
service =
|
|
242
|
-
if
|
|
243
|
-
service =
|
|
244
|
-
elif
|
|
245
|
-
service =
|
|
246
|
-
elif
|
|
247
|
-
service =
|
|
248
|
-
elif
|
|
249
|
-
service =
|
|
250
|
-
elif
|
|
251
|
-
service =
|
|
252
|
-
elif
|
|
253
|
-
service =
|
|
254
|
-
elif
|
|
255
|
-
service =
|
|
256
|
-
elif
|
|
257
|
-
service =
|
|
258
|
-
elif
|
|
259
|
-
service =
|
|
291
|
+
service = "unknown"
|
|
292
|
+
if "ssh" in module:
|
|
293
|
+
service = "ssh"
|
|
294
|
+
elif "telnet" in module:
|
|
295
|
+
service = "telnet"
|
|
296
|
+
elif "mysql" in module:
|
|
297
|
+
service = "mysql"
|
|
298
|
+
elif "postgres" in module:
|
|
299
|
+
service = "postgresql"
|
|
300
|
+
elif "vnc" in module:
|
|
301
|
+
service = "vnc"
|
|
302
|
+
elif "rlogin" in module:
|
|
303
|
+
service = "rlogin"
|
|
304
|
+
elif "smb" in module:
|
|
305
|
+
service = "smb"
|
|
306
|
+
elif "rdp" in module:
|
|
307
|
+
service = "rdp"
|
|
308
|
+
elif "ftp" in module:
|
|
309
|
+
service = "ftp"
|
|
260
310
|
|
|
261
311
|
seen_creds = set() # Avoid duplicates
|
|
262
312
|
|
|
263
313
|
# Pattern 1: [+] 10.0.0.82:22 - Success: 'username:password' 'additional info'
|
|
264
314
|
# Also handles: [+] 10.0.0.82:22 - Success: "username:password"
|
|
265
|
-
success_pattern1 =
|
|
315
|
+
success_pattern1 = (
|
|
316
|
+
r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+Success:\s+[\'"]([^:]+):([^\'\"]+)[\'"]'
|
|
317
|
+
)
|
|
266
318
|
|
|
267
319
|
# Pattern 2: [+] IP:PORT - IP:PORT - Login Successful: user:pass@database
|
|
268
320
|
# Note: MSF often outputs IP:PORT twice. Using .* to handle both cases.
|
|
269
|
-
success_pattern2 = r
|
|
321
|
+
success_pattern2 = r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]*):([^@\s]+)"
|
|
270
322
|
|
|
271
323
|
# Pattern 3: VNC format [+] 10.0.0.73:5900 - 10.0.0.73:5900 - Login Successful: :password
|
|
272
324
|
# Note: VNC often has empty username
|
|
273
|
-
success_pattern3 = r
|
|
325
|
+
success_pattern3 = r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s*:(\S+)"
|
|
274
326
|
|
|
275
327
|
# Pattern 4: Telnet format [+] 192.168.2.230:23 - 192.168.2.230:23 - msfadmin:msfadmin login: Login OK
|
|
276
328
|
# MSF telnet_login uses "username:password login: Login OK" format
|
|
277
|
-
success_pattern_telnet =
|
|
329
|
+
success_pattern_telnet = (
|
|
330
|
+
r"\[\+\]\s+[\d.]+:(\d+).*-\s+([^:\s]+):([^\s]+)\s+login:\s+Login OK"
|
|
331
|
+
)
|
|
278
332
|
|
|
279
333
|
# Pattern 5: Flexible [+] with credentials anywhere (fallback)
|
|
280
334
|
# Handles: [+] 10.0.0.82:22 Found credentials: user:pass
|
|
281
335
|
success_pattern_flexible = r'\[\+\]\s+[\d.]+:(\d+).*(?:credential|found|valid).*?[\'"]?([^:\s\'\"]+):([^\'\"@\s]+)[\'"]?'
|
|
282
336
|
|
|
283
337
|
# Pattern 6: RDP format [+] 10.0.0.82:3389 - DOMAIN\user:password - Success
|
|
284
|
-
success_pattern_rdp =
|
|
338
|
+
success_pattern_rdp = (
|
|
339
|
+
r"\[\+\]\s+[\d.]+:(\d+).*?([^\\:\s]+\\)?([^:\s]+):([^\s-]+)\s*-\s*Success"
|
|
340
|
+
)
|
|
285
341
|
|
|
286
342
|
# Try pattern 3 first (VNC with empty username)
|
|
287
343
|
for match in re.finditer(success_pattern3, clean_output):
|
|
288
344
|
port = int(match.group(1))
|
|
289
345
|
password = match.group(2)
|
|
290
|
-
username =
|
|
346
|
+
username = "" # VNC typically has no username
|
|
291
347
|
|
|
292
348
|
cred_key = (port, username, password)
|
|
293
349
|
if cred_key not in seen_creds:
|
|
294
350
|
seen_creds.add(cred_key)
|
|
295
351
|
|
|
296
|
-
findings.append(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
findings.append(
|
|
353
|
+
{
|
|
354
|
+
"title": f"{service.upper()} Valid Credentials Found",
|
|
355
|
+
"severity": "critical",
|
|
356
|
+
"description": f"Valid {service} password found (no username required): {password}",
|
|
357
|
+
"port": port,
|
|
358
|
+
"service": service,
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
credentials.append(
|
|
363
|
+
{
|
|
364
|
+
"username": username,
|
|
365
|
+
"password": password,
|
|
366
|
+
"service": service,
|
|
367
|
+
"port": port,
|
|
368
|
+
"status": "valid",
|
|
369
|
+
}
|
|
370
|
+
)
|
|
311
371
|
|
|
312
372
|
# Try other patterns (username:password style)
|
|
313
|
-
for pattern in [
|
|
373
|
+
for pattern in [
|
|
374
|
+
success_pattern1,
|
|
375
|
+
success_pattern2,
|
|
376
|
+
success_pattern_telnet,
|
|
377
|
+
success_pattern_flexible,
|
|
378
|
+
]:
|
|
314
379
|
for match in re.finditer(pattern, clean_output, re.IGNORECASE):
|
|
315
380
|
port = int(match.group(1))
|
|
316
381
|
username = match.group(2)
|
|
@@ -320,55 +385,79 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
|
|
|
320
385
|
if cred_key not in seen_creds:
|
|
321
386
|
seen_creds.add(cred_key)
|
|
322
387
|
|
|
323
|
-
findings.append(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
388
|
+
findings.append(
|
|
389
|
+
{
|
|
390
|
+
"title": f"{service.upper()} Valid Credentials Found",
|
|
391
|
+
"severity": "critical",
|
|
392
|
+
"description": f"Valid {service} credentials: {username}:{password}",
|
|
393
|
+
"port": port,
|
|
394
|
+
"service": service,
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
credentials.append(
|
|
399
|
+
{
|
|
400
|
+
"username": username,
|
|
401
|
+
"password": password,
|
|
402
|
+
"service": service,
|
|
403
|
+
"port": port,
|
|
404
|
+
"status": "valid",
|
|
405
|
+
}
|
|
406
|
+
)
|
|
338
407
|
|
|
339
408
|
# Check for session opened (login modules can spawn sessions)
|
|
340
409
|
# Format: [*] Command shell session 1 opened (192.168.1.224:37125 -> 192.168.1.240:23)
|
|
341
|
-
session_pattern =
|
|
410
|
+
session_pattern = (
|
|
411
|
+
r"\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)"
|
|
412
|
+
)
|
|
342
413
|
for match in re.finditer(session_pattern, clean_output, re.IGNORECASE):
|
|
343
414
|
session_type = match.group(1)
|
|
344
415
|
session_id = match.group(2)
|
|
345
416
|
tunnel = match.group(3)
|
|
346
417
|
|
|
347
|
-
sessions.append(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
findings.append({
|
|
355
|
-
'title': f'{service.upper()} Session Opened',
|
|
356
|
-
'severity': 'critical',
|
|
357
|
-
'description': f'{session_type} session {session_id} opened via {service} login. Tunnel: {tunnel}',
|
|
358
|
-
'service': service,
|
|
359
|
-
'data': {
|
|
360
|
-
'module': module,
|
|
361
|
-
'session_id': session_id,
|
|
362
|
-
'session_type': session_type.lower(),
|
|
363
|
-
'tunnel': tunnel
|
|
418
|
+
sessions.append(
|
|
419
|
+
{
|
|
420
|
+
"id": session_id,
|
|
421
|
+
"type": session_type.lower(),
|
|
422
|
+
"tunnel": tunnel,
|
|
423
|
+
"module": module,
|
|
364
424
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
findings.append(
|
|
428
|
+
{
|
|
429
|
+
"title": f"{service.upper()} Session Opened",
|
|
430
|
+
"severity": "critical",
|
|
431
|
+
"description": f"{session_type} session {session_id} opened via {service} login. Tunnel: {tunnel}",
|
|
432
|
+
"service": service,
|
|
433
|
+
"data": {
|
|
434
|
+
"module": module,
|
|
435
|
+
"session_id": session_id,
|
|
436
|
+
"session_type": session_type.lower(),
|
|
437
|
+
"tunnel": tunnel,
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Check for newer MSF format: "Bruteforce completed, X credential was successful"
|
|
443
|
+
# This format doesn't show the actual credential in logs
|
|
444
|
+
if not credentials: # Only if we didn't find explicit credentials
|
|
445
|
+
bruteforce_pattern = r"\[\*\]\s+[\d.]+:?(\d*)\s*-?\s*Bruteforce completed,\s*(\d+)\s+credential.*successful"
|
|
446
|
+
bf_match = re.search(bruteforce_pattern, clean_output)
|
|
447
|
+
if bf_match:
|
|
448
|
+
port = int(bf_match.group(1)) if bf_match.group(1) else None
|
|
449
|
+
num_creds = int(bf_match.group(2))
|
|
450
|
+
findings.append(
|
|
451
|
+
{
|
|
452
|
+
"title": f"{service.upper()} Credentials Found ({num_creds} valid)",
|
|
453
|
+
"severity": "critical",
|
|
454
|
+
"description": f'{num_creds} valid {service} credential(s) discovered via bruteforce. Run "creds" command in msfconsole to view.',
|
|
455
|
+
"service": service,
|
|
456
|
+
"port": port,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return {"findings": findings, "credentials": credentials, "sessions": sessions}
|
|
372
461
|
|
|
373
462
|
|
|
374
463
|
def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -388,13 +477,18 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
|
|
|
388
477
|
# Pattern 1: Host could not be identified: Unix (Samba 3.0.20-Debian)
|
|
389
478
|
# Pattern 2: Host is running Windows 10 Pro (build 19041)
|
|
390
479
|
# Pattern 3: SMB Detected (versions:1, 2, 3) (preferred dialect:SMB 3.1.1)
|
|
480
|
+
# Pattern 4: [+] Host is running Version X.X.X (unknown OS)
|
|
391
481
|
version_patterns = [
|
|
392
482
|
# Samba detection - [*] 10.0.0.73:445 - Host could not be identified: Unix (Samba 3.0.20-Debian)
|
|
393
|
-
r
|
|
483
|
+
r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host could not be identified:\s+(\S+)\s+\(([^)]+)\)",
|
|
394
484
|
# Windows detection - [*] 10.0.0.73:445 - Host is running Windows 10 Pro (build 19041)
|
|
395
|
-
r
|
|
485
|
+
r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host is running\s+(.+)",
|
|
486
|
+
# Version detection with [+] - [+] 10.0.0.73:445 - Host is running Version 6.1.0 (unknown OS)
|
|
487
|
+
r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+Host is running\s+(.+)",
|
|
396
488
|
# Generic SMB version - [+] 10.0.0.73:445 - SMB Detected...
|
|
397
|
-
r
|
|
489
|
+
r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+SMB Detected\s+(.+)",
|
|
490
|
+
# SMB Detected with [*] - [*] 10.0.0.73:445 - SMB Detected (versions:1, 2, 3)
|
|
491
|
+
r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+SMB Detected\s+(.+)",
|
|
398
492
|
]
|
|
399
493
|
|
|
400
494
|
smb_version = None
|
|
@@ -405,7 +499,7 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
|
|
|
405
499
|
match = re.search(pattern, clean_output)
|
|
406
500
|
if match:
|
|
407
501
|
port = int(match.group(2))
|
|
408
|
-
if
|
|
502
|
+
if "could not be identified" in pattern:
|
|
409
503
|
# Samba format: OS type and version in parens
|
|
410
504
|
os_info = match.group(3) # e.g., "Unix"
|
|
411
505
|
smb_version = match.group(4) # e.g., "Samba 3.0.20-Debian"
|
|
@@ -415,50 +509,82 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
|
|
|
415
509
|
break
|
|
416
510
|
|
|
417
511
|
if smb_version:
|
|
418
|
-
services.append(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
512
|
+
services.append(
|
|
513
|
+
{
|
|
514
|
+
"port": port,
|
|
515
|
+
"protocol": "tcp",
|
|
516
|
+
"service_name": "smb",
|
|
517
|
+
"service_version": smb_version,
|
|
518
|
+
}
|
|
519
|
+
)
|
|
424
520
|
|
|
425
521
|
# Check for known vulnerable Samba versions
|
|
426
522
|
vuln_samba_versions = {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
523
|
+
"Samba 3.0.20": (
|
|
524
|
+
"CVE-2007-2447",
|
|
525
|
+
"critical",
|
|
526
|
+
"Samba username map script command injection",
|
|
527
|
+
),
|
|
528
|
+
"Samba 3.0.21": (
|
|
529
|
+
"CVE-2007-2447",
|
|
530
|
+
"critical",
|
|
531
|
+
"Samba username map script command injection",
|
|
532
|
+
),
|
|
533
|
+
"Samba 3.0.22": (
|
|
534
|
+
"CVE-2007-2447",
|
|
535
|
+
"critical",
|
|
536
|
+
"Samba username map script command injection",
|
|
537
|
+
),
|
|
538
|
+
"Samba 3.0.23": (
|
|
539
|
+
"CVE-2007-2447",
|
|
540
|
+
"critical",
|
|
541
|
+
"Samba username map script command injection",
|
|
542
|
+
),
|
|
543
|
+
"Samba 3.0.24": (
|
|
544
|
+
"CVE-2007-2447",
|
|
545
|
+
"critical",
|
|
546
|
+
"Samba username map script command injection",
|
|
547
|
+
),
|
|
548
|
+
"Samba 3.0.25": (
|
|
549
|
+
"CVE-2007-2447",
|
|
550
|
+
"critical",
|
|
551
|
+
"Samba username map script command injection",
|
|
552
|
+
),
|
|
553
|
+
"Samba 3.5.0": (
|
|
554
|
+
"CVE-2017-7494",
|
|
555
|
+
"critical",
|
|
556
|
+
"SambaCry/EternalRed remote code execution",
|
|
557
|
+
),
|
|
434
558
|
}
|
|
435
559
|
|
|
436
560
|
# Check if version matches any known vulnerable version
|
|
437
561
|
for vuln_ver, (cve, severity, desc) in vuln_samba_versions.items():
|
|
438
562
|
if vuln_ver in smb_version:
|
|
439
|
-
findings.append(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
563
|
+
findings.append(
|
|
564
|
+
{
|
|
565
|
+
"title": f"Vulnerable Samba Version ({cve})",
|
|
566
|
+
"severity": severity,
|
|
567
|
+
"description": f"{desc}. Detected version: {smb_version}",
|
|
568
|
+
"port": port,
|
|
569
|
+
"service": "smb",
|
|
570
|
+
"data": {"cve": cve, "version": smb_version},
|
|
571
|
+
}
|
|
572
|
+
)
|
|
447
573
|
break
|
|
448
574
|
else:
|
|
449
575
|
# No specific vulnerability, just report version as info
|
|
450
|
-
findings.append(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
576
|
+
findings.append(
|
|
577
|
+
{
|
|
578
|
+
"title": f"SMB Version Detected: {smb_version}",
|
|
579
|
+
"severity": "info",
|
|
580
|
+
"description": f"SMB/Samba version detected: {smb_version}"
|
|
581
|
+
+ (f" (OS: {os_info})" if os_info else ""),
|
|
582
|
+
"port": port,
|
|
583
|
+
"service": "smb",
|
|
584
|
+
}
|
|
585
|
+
)
|
|
457
586
|
|
|
458
|
-
return {
|
|
459
|
-
'services': services,
|
|
460
|
-
'findings': findings
|
|
461
|
-
}
|
|
587
|
+
return {"services": services, "findings": findings}
|
|
462
588
|
|
|
463
589
|
|
|
464
590
|
def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -476,7 +602,7 @@ def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
|
|
|
476
602
|
# Parse share lines
|
|
477
603
|
# Format: [+] 10.0.0.82:445 - ADMIN$ - (DISK) Remote Admin
|
|
478
604
|
# Format: [+] 10.0.0.82:445 - IPC$ - (IPC) Remote IPC
|
|
479
|
-
share_pattern = r
|
|
605
|
+
share_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(\S+)\s+-\s+\((\w+)\)\s*(.*)"
|
|
480
606
|
|
|
481
607
|
shares = []
|
|
482
608
|
for match in re.finditer(share_pattern, clean_output):
|
|
@@ -485,32 +611,29 @@ def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
|
|
|
485
611
|
share_type = match.group(3)
|
|
486
612
|
comment = match.group(4).strip()
|
|
487
613
|
|
|
488
|
-
shares.append(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
'comment': comment,
|
|
492
|
-
'port': port
|
|
493
|
-
})
|
|
614
|
+
shares.append(
|
|
615
|
+
{"name": share_name, "type": share_type, "comment": comment, "port": port}
|
|
616
|
+
)
|
|
494
617
|
|
|
495
618
|
if shares:
|
|
496
619
|
# Determine severity based on share types
|
|
497
|
-
severity =
|
|
498
|
-
if any(s[
|
|
499
|
-
severity =
|
|
500
|
-
|
|
501
|
-
share_list =
|
|
502
|
-
findings.append(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
620
|
+
severity = "info"
|
|
621
|
+
if any(s["name"] not in ["IPC$", "ADMIN$", "C$"] for s in shares):
|
|
622
|
+
severity = "medium" # Non-default shares found
|
|
623
|
+
|
|
624
|
+
share_list = ", ".join([s["name"] for s in shares])
|
|
625
|
+
findings.append(
|
|
626
|
+
{
|
|
627
|
+
"title": f"SMB Shares Discovered ({len(shares)} shares)",
|
|
628
|
+
"severity": severity,
|
|
629
|
+
"description": f"Found {len(shares)} SMB shares: {share_list}",
|
|
630
|
+
"port": 445,
|
|
631
|
+
"service": "smb",
|
|
632
|
+
"data": {"shares": shares},
|
|
633
|
+
}
|
|
634
|
+
)
|
|
510
635
|
|
|
511
|
-
return {
|
|
512
|
-
'findings': findings
|
|
513
|
-
}
|
|
636
|
+
return {"findings": findings}
|
|
514
637
|
|
|
515
638
|
|
|
516
639
|
def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -521,11 +644,25 @@ def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
|
|
|
521
644
|
{
|
|
522
645
|
'findings': [] # Discovered SSH users
|
|
523
646
|
'credentials': [] # Username-only credentials
|
|
647
|
+
'status': str # Optional status override (e.g., 'warning' for false positives)
|
|
648
|
+
'warning': str # Optional warning message
|
|
524
649
|
}
|
|
525
650
|
"""
|
|
526
651
|
findings = []
|
|
527
652
|
credentials = []
|
|
528
653
|
clean_output = strip_ansi_codes(output)
|
|
654
|
+
result = {}
|
|
655
|
+
|
|
656
|
+
# Check for false positive detection (module aborted)
|
|
657
|
+
# Format: [-] 192.168.1.157:22 - SSH - throws false positive results. Aborting.
|
|
658
|
+
if "false positive" in clean_output.lower() and "aborting" in clean_output.lower():
|
|
659
|
+
result["status"] = "warning"
|
|
660
|
+
result["warning"] = (
|
|
661
|
+
"SSH user enumeration aborted: target throws false positive results"
|
|
662
|
+
)
|
|
663
|
+
result["findings"] = []
|
|
664
|
+
result["credentials"] = []
|
|
665
|
+
return result
|
|
529
666
|
|
|
530
667
|
# Parse user enumeration results
|
|
531
668
|
# Format: [+] 10.0.0.82:22 - SSH - User 'root' found
|
|
@@ -537,31 +674,88 @@ def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
|
|
|
537
674
|
port = int(match.group(1))
|
|
538
675
|
username = match.group(2)
|
|
539
676
|
users.append(username)
|
|
540
|
-
|
|
677
|
+
|
|
541
678
|
# Add as credential (username-only, no password)
|
|
542
|
-
credentials.append(
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
679
|
+
credentials.append(
|
|
680
|
+
{
|
|
681
|
+
"username": username,
|
|
682
|
+
"password": "", # Empty password for username-only
|
|
683
|
+
"service": "ssh",
|
|
684
|
+
"port": port,
|
|
685
|
+
"status": "untested", # Username discovered but not validated
|
|
686
|
+
}
|
|
687
|
+
)
|
|
549
688
|
|
|
550
689
|
if users:
|
|
551
|
-
user_list =
|
|
552
|
-
findings.append(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
690
|
+
user_list = ", ".join(users)
|
|
691
|
+
findings.append(
|
|
692
|
+
{
|
|
693
|
+
"title": f"SSH Users Enumerated ({len(users)} users)",
|
|
694
|
+
"severity": "medium",
|
|
695
|
+
"description": f"Found {len(users)} SSH users: {user_list}",
|
|
696
|
+
"port": 22,
|
|
697
|
+
"service": "ssh",
|
|
698
|
+
"data": {"users": users},
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
result["findings"] = findings
|
|
703
|
+
result["credentials"] = credentials
|
|
704
|
+
return result
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def parse_msf_kerberos_enumusers(output: str, target: str) -> Dict[str, Any]:
|
|
708
|
+
"""
|
|
709
|
+
Parse Kerberos user enumeration output (kerberos_enumusers module).
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
{
|
|
713
|
+
'findings': [] # Discovered Kerberos users
|
|
714
|
+
'credentials': [] # Username-only credentials
|
|
715
|
+
}
|
|
716
|
+
"""
|
|
717
|
+
findings = []
|
|
718
|
+
credentials = []
|
|
719
|
+
clean_output = strip_ansi_codes(output)
|
|
720
|
+
|
|
721
|
+
# Parse valid usernames - MSF kerberos_enumusers format
|
|
722
|
+
# Format: [+] 10.129.234.72 - User: "administrator" is present
|
|
723
|
+
# Format: [+] 10.129.234.72 - User: "guest" is present
|
|
724
|
+
valid_pattern = r'\[\+\]\s+[\d.]+\s+-\s+User:\s+"([^"]+)"\s+is present'
|
|
725
|
+
|
|
726
|
+
users = []
|
|
727
|
+
port = 88 # Kerberos port
|
|
728
|
+
|
|
729
|
+
for match in re.finditer(valid_pattern, clean_output, re.IGNORECASE):
|
|
730
|
+
username = match.group(1)
|
|
731
|
+
if username not in users:
|
|
732
|
+
users.append(username)
|
|
733
|
+
|
|
734
|
+
# Add as credential (username-only, no password)
|
|
735
|
+
credentials.append(
|
|
736
|
+
{
|
|
737
|
+
"username": username,
|
|
738
|
+
"password": "", # Empty password for username-only
|
|
739
|
+
"service": "kerberos",
|
|
740
|
+
"port": port,
|
|
741
|
+
"status": "untested", # Username discovered but not validated
|
|
742
|
+
}
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
if users:
|
|
746
|
+
user_list = ", ".join(users)
|
|
747
|
+
findings.append(
|
|
748
|
+
{
|
|
749
|
+
"title": f"Kerberos Users Enumerated ({len(users)} users)",
|
|
750
|
+
"severity": "medium",
|
|
751
|
+
"description": f"Found {len(users)} valid Kerberos users: {user_list}",
|
|
752
|
+
"port": port,
|
|
753
|
+
"service": "kerberos",
|
|
754
|
+
"data": {"users": users},
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
return {"findings": findings, "credentials": credentials}
|
|
565
759
|
|
|
566
760
|
|
|
567
761
|
def parse_msf_smtp_enum(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -583,45 +777,46 @@ def parse_msf_smtp_enum(output: str, target: str) -> Dict[str, Any]:
|
|
|
583
777
|
users = []
|
|
584
778
|
|
|
585
779
|
# Method 1: Users found line
|
|
586
|
-
users_found_pattern = r
|
|
780
|
+
users_found_pattern = r"Users found:\s*(.+)"
|
|
587
781
|
match = re.search(users_found_pattern, clean_output)
|
|
588
782
|
if match:
|
|
589
783
|
user_list = match.group(1).strip()
|
|
590
|
-
users = [u.strip() for u in user_list.split(
|
|
784
|
+
users = [u.strip() for u in user_list.split(",") if u.strip()]
|
|
591
785
|
|
|
592
786
|
# Method 2: Individual user lines
|
|
593
787
|
# Format: [+] 10.0.0.82:25 - Found user: root
|
|
594
|
-
user_pattern = r
|
|
788
|
+
user_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+Found user:\s+(\S+)"
|
|
595
789
|
for match in re.finditer(user_pattern, clean_output):
|
|
596
790
|
username = match.group(2)
|
|
597
791
|
if username not in users:
|
|
598
792
|
users.append(username)
|
|
599
793
|
|
|
600
794
|
if users:
|
|
601
|
-
user_list =
|
|
602
|
-
findings.append(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
795
|
+
user_list = ", ".join(users)
|
|
796
|
+
findings.append(
|
|
797
|
+
{
|
|
798
|
+
"title": f"SMTP Users Enumerated ({len(users)} users)",
|
|
799
|
+
"severity": "medium",
|
|
800
|
+
"description": f"Found {len(users)} SMTP users: {user_list}",
|
|
801
|
+
"port": 25,
|
|
802
|
+
"service": "smtp",
|
|
803
|
+
"data": {"users": users},
|
|
804
|
+
}
|
|
805
|
+
)
|
|
610
806
|
|
|
611
807
|
# Add each user as a credential (username only, no password)
|
|
612
808
|
for username in users:
|
|
613
|
-
credentials.append(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
809
|
+
credentials.append(
|
|
810
|
+
{
|
|
811
|
+
"username": username,
|
|
812
|
+
"password": None,
|
|
813
|
+
"service": "smtp",
|
|
814
|
+
"port": 25,
|
|
815
|
+
"status": "enumerated", # Not validated, just discovered
|
|
816
|
+
}
|
|
817
|
+
)
|
|
620
818
|
|
|
621
|
-
return {
|
|
622
|
-
'findings': findings,
|
|
623
|
-
'credentials': credentials
|
|
624
|
-
}
|
|
819
|
+
return {"findings": findings, "credentials": credentials}
|
|
625
820
|
|
|
626
821
|
|
|
627
822
|
def parse_msf_ftp_anonymous(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -640,35 +835,36 @@ def parse_msf_ftp_anonymous(output: str, target: str) -> Dict[str, Any]:
|
|
|
640
835
|
|
|
641
836
|
# Pattern: [+] 10.0.0.73:21 - 10.0.0.73:21 - Anonymous READ (220 (vsFTPd 2.3.4))
|
|
642
837
|
# Pattern: [+] 10.0.0.73:21 - Anonymous READ/WRITE (...)
|
|
643
|
-
anon_pattern = r
|
|
838
|
+
anon_pattern = r"\[\+\]\s+[\d.]+:(\d+).*Anonymous\s+(READ|WRITE|READ/WRITE)"
|
|
644
839
|
|
|
645
840
|
for match in re.finditer(anon_pattern, clean_output, re.IGNORECASE):
|
|
646
841
|
port = int(match.group(1))
|
|
647
842
|
access_type = match.group(2).upper()
|
|
648
843
|
|
|
649
|
-
severity =
|
|
844
|
+
severity = "high" if "WRITE" in access_type else "medium"
|
|
650
845
|
|
|
651
|
-
findings.append(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
846
|
+
findings.append(
|
|
847
|
+
{
|
|
848
|
+
"title": f"FTP Anonymous Access ({access_type})",
|
|
849
|
+
"severity": severity,
|
|
850
|
+
"description": f"FTP server allows anonymous access with {access_type} permissions. This may expose sensitive files.",
|
|
851
|
+
"port": port,
|
|
852
|
+
"service": "ftp",
|
|
853
|
+
}
|
|
854
|
+
)
|
|
658
855
|
|
|
659
856
|
# Add anonymous credential
|
|
660
|
-
credentials.append(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
857
|
+
credentials.append(
|
|
858
|
+
{
|
|
859
|
+
"username": "anonymous",
|
|
860
|
+
"password": "anonymous@",
|
|
861
|
+
"service": "ftp",
|
|
862
|
+
"port": port,
|
|
863
|
+
"status": "valid",
|
|
864
|
+
}
|
|
865
|
+
)
|
|
667
866
|
|
|
668
|
-
return {
|
|
669
|
-
'findings': findings,
|
|
670
|
-
'credentials': credentials
|
|
671
|
-
}
|
|
867
|
+
return {"findings": findings, "credentials": credentials}
|
|
672
868
|
|
|
673
869
|
|
|
674
870
|
def parse_msf_nfs_mount(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -687,56 +883,52 @@ def parse_msf_nfs_mount(output: str, target: str) -> Dict[str, Any]:
|
|
|
687
883
|
|
|
688
884
|
# Pattern 1: New MSF format
|
|
689
885
|
# Format: [+] 10.0.0.73:111 - 10.0.0.73 Mountable NFS Export: / [*]
|
|
690
|
-
export_pattern1 = r
|
|
886
|
+
export_pattern1 = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+[\d.]+\s+Mountable NFS Export:\s+(\S+)\s*(\[.*?\])?"
|
|
691
887
|
for match in re.finditer(export_pattern1, clean_output):
|
|
692
888
|
port = int(match.group(1))
|
|
693
889
|
mount_path = match.group(2)
|
|
694
|
-
permissions = match.group(3) or
|
|
890
|
+
permissions = match.group(3) or "*"
|
|
695
891
|
|
|
696
|
-
exports.append(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
'port': port
|
|
700
|
-
})
|
|
892
|
+
exports.append(
|
|
893
|
+
{"path": mount_path, "permissions": permissions.strip(), "port": port}
|
|
894
|
+
)
|
|
701
895
|
|
|
702
896
|
# Pattern 2: Old format
|
|
703
897
|
# Format: [+] 10.0.0.82:111 - /home *
|
|
704
898
|
# Format: [+] 10.0.0.82:2049 - /var/nfs *(rw,sync,no_subtree_check)
|
|
705
899
|
if not exports:
|
|
706
|
-
export_pattern2 = r
|
|
900
|
+
export_pattern2 = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(/\S+)\s+(.*)"
|
|
707
901
|
for match in re.finditer(export_pattern2, clean_output):
|
|
708
902
|
port = int(match.group(1))
|
|
709
903
|
mount_path = match.group(2)
|
|
710
904
|
permissions = match.group(3).strip()
|
|
711
905
|
|
|
712
|
-
exports.append(
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
'port': port
|
|
716
|
-
})
|
|
906
|
+
exports.append(
|
|
907
|
+
{"path": mount_path, "permissions": permissions, "port": port}
|
|
908
|
+
)
|
|
717
909
|
|
|
718
910
|
if exports:
|
|
719
911
|
# Determine severity based on path and permissions
|
|
720
|
-
severity =
|
|
912
|
+
severity = "medium"
|
|
721
913
|
# Root export is always high severity
|
|
722
|
-
if any(e[
|
|
723
|
-
severity =
|
|
724
|
-
elif any(
|
|
725
|
-
severity =
|
|
726
|
-
|
|
727
|
-
export_list =
|
|
728
|
-
findings.append(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
914
|
+
if any(e["path"] == "/" for e in exports):
|
|
915
|
+
severity = "high"
|
|
916
|
+
elif any("rw" in e["permissions"] for e in exports):
|
|
917
|
+
severity = "high" # Writable mounts are more severe
|
|
918
|
+
|
|
919
|
+
export_list = ", ".join([e["path"] for e in exports])
|
|
920
|
+
findings.append(
|
|
921
|
+
{
|
|
922
|
+
"title": f"NFS Exports Discovered ({len(exports)} mounts)",
|
|
923
|
+
"severity": severity,
|
|
924
|
+
"description": f"Found {len(exports)} NFS exports: {export_list}",
|
|
925
|
+
"port": 2049,
|
|
926
|
+
"service": "nfs",
|
|
927
|
+
"data": {"exports": exports},
|
|
928
|
+
}
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
return {"findings": findings}
|
|
740
932
|
|
|
741
933
|
|
|
742
934
|
def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -755,7 +947,7 @@ def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
|
|
|
755
947
|
|
|
756
948
|
# Parse Java RMI detection
|
|
757
949
|
# Format: [+] 10.0.0.82:1099 - Java RMI Endpoint Detected: Class Loader Enabled
|
|
758
|
-
rmi_pattern = r
|
|
950
|
+
rmi_pattern = r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+(.+)"
|
|
759
951
|
|
|
760
952
|
for match in re.finditer(rmi_pattern, clean_output):
|
|
761
953
|
ip = match.group(1)
|
|
@@ -763,37 +955,40 @@ def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
|
|
|
763
955
|
message = match.group(3).strip()
|
|
764
956
|
|
|
765
957
|
# Check for specific vulnerabilities
|
|
766
|
-
severity =
|
|
767
|
-
if
|
|
768
|
-
severity =
|
|
769
|
-
findings.append(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
958
|
+
severity = "medium"
|
|
959
|
+
if "Class Loader Enabled" in message:
|
|
960
|
+
severity = "high"
|
|
961
|
+
findings.append(
|
|
962
|
+
{
|
|
963
|
+
"title": "Java RMI Class Loader Enabled",
|
|
964
|
+
"severity": severity,
|
|
965
|
+
"description": f"Java RMI endpoint with Class Loader enabled detected. This may allow remote code execution via deserialization attacks.",
|
|
966
|
+
"port": port,
|
|
967
|
+
"service": "java-rmi",
|
|
968
|
+
}
|
|
969
|
+
)
|
|
970
|
+
elif "Endpoint Detected" in message:
|
|
971
|
+
findings.append(
|
|
972
|
+
{
|
|
973
|
+
"title": "Java RMI Endpoint Detected",
|
|
974
|
+
"severity": "medium",
|
|
975
|
+
"description": f"Java RMI endpoint detected: {message}",
|
|
976
|
+
"port": port,
|
|
977
|
+
"service": "java-rmi",
|
|
978
|
+
}
|
|
979
|
+
)
|
|
784
980
|
|
|
785
981
|
# Add service info
|
|
786
|
-
services.append(
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
982
|
+
services.append(
|
|
983
|
+
{
|
|
984
|
+
"port": port,
|
|
985
|
+
"protocol": "tcp",
|
|
986
|
+
"service_name": "java-rmi",
|
|
987
|
+
"service_version": "Java RMI Registry",
|
|
988
|
+
}
|
|
989
|
+
)
|
|
792
990
|
|
|
793
|
-
return {
|
|
794
|
-
'findings': findings,
|
|
795
|
-
'services': services
|
|
796
|
-
}
|
|
991
|
+
return {"findings": findings, "services": services}
|
|
797
992
|
|
|
798
993
|
|
|
799
994
|
def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -813,7 +1008,9 @@ def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
|
|
|
813
1008
|
# Parse VNC security types
|
|
814
1009
|
# Format: [+] 10.0.0.82:5900 - VNC server security types supported: VNC
|
|
815
1010
|
# Format: [+] 10.0.0.82:5900 - VNC server security types supported: None
|
|
816
|
-
vnc_pattern =
|
|
1011
|
+
vnc_pattern = (
|
|
1012
|
+
r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server security types supported:\s*(.+)"
|
|
1013
|
+
)
|
|
817
1014
|
|
|
818
1015
|
for match in re.finditer(vnc_pattern, clean_output):
|
|
819
1016
|
ip = match.group(1)
|
|
@@ -821,52 +1018,223 @@ def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
|
|
|
821
1018
|
sec_types = match.group(3).strip()
|
|
822
1019
|
|
|
823
1020
|
# Check for no authentication
|
|
824
|
-
if
|
|
825
|
-
findings.append(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1021
|
+
if "None" in sec_types:
|
|
1022
|
+
findings.append(
|
|
1023
|
+
{
|
|
1024
|
+
"title": "VNC No Authentication Required",
|
|
1025
|
+
"severity": "critical",
|
|
1026
|
+
"description": f"VNC server at port {port} allows connections without authentication. Security types: {sec_types}",
|
|
1027
|
+
"port": port,
|
|
1028
|
+
"service": "vnc",
|
|
1029
|
+
}
|
|
1030
|
+
)
|
|
832
1031
|
else:
|
|
833
|
-
findings.append(
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1032
|
+
findings.append(
|
|
1033
|
+
{
|
|
1034
|
+
"title": f"VNC Security Types Detected",
|
|
1035
|
+
"severity": "info",
|
|
1036
|
+
"description": f"VNC server security types: {sec_types}",
|
|
1037
|
+
"port": port,
|
|
1038
|
+
"service": "vnc",
|
|
1039
|
+
}
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
services.append(
|
|
1043
|
+
{
|
|
1044
|
+
"port": port,
|
|
1045
|
+
"protocol": "tcp",
|
|
1046
|
+
"service_name": "vnc",
|
|
1047
|
+
"service_version": f"Security: {sec_types}",
|
|
1048
|
+
}
|
|
1049
|
+
)
|
|
847
1050
|
|
|
848
1051
|
# Also check for vnc_none_auth specific output
|
|
849
1052
|
# Format: [*] 10.0.0.82:5900 - VNC server protocol version: ...
|
|
850
1053
|
# Format: [+] 10.0.0.82:5900 - VNC server does not require authentication
|
|
851
|
-
no_auth_pattern =
|
|
1054
|
+
no_auth_pattern = (
|
|
1055
|
+
r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server does not require authentication"
|
|
1056
|
+
)
|
|
852
1057
|
for match in re.finditer(no_auth_pattern, clean_output):
|
|
853
1058
|
ip = match.group(1)
|
|
854
1059
|
port = int(match.group(2))
|
|
855
1060
|
|
|
856
1061
|
# Avoid duplicates
|
|
857
|
-
if not any(
|
|
858
|
-
findings
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1062
|
+
if not any(
|
|
1063
|
+
f["port"] == port and "No Authentication" in f["title"] for f in findings
|
|
1064
|
+
):
|
|
1065
|
+
findings.append(
|
|
1066
|
+
{
|
|
1067
|
+
"title": "VNC No Authentication Required",
|
|
1068
|
+
"severity": "critical",
|
|
1069
|
+
"description": f"VNC server at port {port} does not require authentication.",
|
|
1070
|
+
"port": port,
|
|
1071
|
+
"service": "vnc",
|
|
1072
|
+
}
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
return {"findings": findings, "services": services}
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def parse_msf_endpoint_mapper(output: str, target: str) -> Dict[str, Any]:
|
|
1079
|
+
"""
|
|
1080
|
+
Parse MSF endpoint_mapper (RPC enumeration) output.
|
|
865
1081
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1082
|
+
The endpoint_mapper module discovers RPC endpoints which can reveal:
|
|
1083
|
+
- Services running on the target
|
|
1084
|
+
- Named pipes available for exploitation
|
|
1085
|
+
- Protocol bindings and UUIDs
|
|
1086
|
+
|
|
1087
|
+
Returns:
|
|
1088
|
+
{
|
|
1089
|
+
'findings': [], # RPC endpoint discoveries
|
|
1090
|
+
'services': [] # Services detected
|
|
1091
|
+
}
|
|
1092
|
+
"""
|
|
1093
|
+
findings = []
|
|
1094
|
+
services = []
|
|
1095
|
+
clean_output = strip_ansi_codes(output)
|
|
1096
|
+
|
|
1097
|
+
endpoints = []
|
|
1098
|
+
pipes = []
|
|
1099
|
+
service_names = []
|
|
1100
|
+
|
|
1101
|
+
# Pattern for endpoint entries - actual MSF format:
|
|
1102
|
+
# [*] 10.129.48.183:135 - d95afe70-a6d5-4259-822e-2c84da1ddb0d v1.0 TCP (49152) 10.129.48.183
|
|
1103
|
+
# [*] 10.129.48.183:135 - 897e2e5f-93f3-4376-9c9c-fd2277495c27 v1.0 LRPC (...) [Frs2 Service]
|
|
1104
|
+
uuid_pattern = r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+([a-f0-9-]{36})\s+v?([0-9.]+)\s+(\w+)\s+\([^)]+\)(?:\s+[\d.]+)?(?:\s+\[([^\]]+)\])?"
|
|
1105
|
+
for match in re.finditer(uuid_pattern, clean_output, re.IGNORECASE):
|
|
1106
|
+
port = int(match.group(1))
|
|
1107
|
+
uuid = match.group(2)
|
|
1108
|
+
version = match.group(3) or ""
|
|
1109
|
+
protocol = match.group(4) # TCP, LRPC, PIPE, HTTP
|
|
1110
|
+
svc_name = match.group(5) # Service name in brackets if present
|
|
1111
|
+
endpoints.append(
|
|
1112
|
+
{
|
|
1113
|
+
"uuid": uuid,
|
|
1114
|
+
"version": version,
|
|
1115
|
+
"protocol": protocol,
|
|
1116
|
+
"port": port,
|
|
1117
|
+
"service": svc_name,
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
if svc_name and svc_name not in service_names:
|
|
1121
|
+
service_names.append(svc_name)
|
|
1122
|
+
|
|
1123
|
+
# Pattern for PIPE entries specifically
|
|
1124
|
+
# [*] 10.129.48.183:135 - e3514235-4b06-11d1-ab04-00c04fc2dcd2 v4.0 PIPE (\pipe\lsass) \\DC [MS NT Directory DRS Interface]
|
|
1125
|
+
pipe_pattern = (
|
|
1126
|
+
r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+[a-f0-9-]+\s+v?[0-9.]+\s+PIPE\s+\(([^)]+)\)"
|
|
1127
|
+
)
|
|
1128
|
+
for match in re.finditer(pipe_pattern, clean_output, re.IGNORECASE):
|
|
1129
|
+
port = int(match.group(1))
|
|
1130
|
+
pipe = match.group(2)
|
|
1131
|
+
if pipe not in pipes:
|
|
1132
|
+
pipes.append(pipe)
|
|
1133
|
+
|
|
1134
|
+
# Also look for old format patterns as fallback
|
|
1135
|
+
# Format: [*] 10.0.0.1:135 - UUID: d95afe70-a6d5-4259-822e-2c84da1ddb0d v1.0
|
|
1136
|
+
old_uuid_pattern = (
|
|
1137
|
+
r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+UUID:\s+([a-f0-9-]+)\s+v?([0-9.]+)?"
|
|
1138
|
+
)
|
|
1139
|
+
for match in re.finditer(old_uuid_pattern, clean_output, re.IGNORECASE):
|
|
1140
|
+
port = int(match.group(1))
|
|
1141
|
+
uuid = match.group(2)
|
|
1142
|
+
version = match.group(3) or ""
|
|
1143
|
+
if not any(e.get("uuid") == uuid for e in endpoints):
|
|
1144
|
+
endpoints.append({"uuid": uuid, "version": version, "port": port})
|
|
1145
|
+
|
|
1146
|
+
# Pattern for [+] success lines (discovered services)
|
|
1147
|
+
success_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(.+)"
|
|
1148
|
+
for match in re.finditer(success_pattern, clean_output):
|
|
1149
|
+
port = int(match.group(1))
|
|
1150
|
+
message = match.group(2).strip()
|
|
1151
|
+
if message and message not in [e.get("message") for e in endpoints]:
|
|
1152
|
+
endpoints.append({"message": message, "port": port})
|
|
1153
|
+
|
|
1154
|
+
# Create findings if we discovered anything
|
|
1155
|
+
if endpoints or pipes or service_names:
|
|
1156
|
+
# Unique endpoints count
|
|
1157
|
+
unique_uuids = len(set(e.get("uuid", "") for e in endpoints if e.get("uuid")))
|
|
1158
|
+
desc_parts = []
|
|
1159
|
+
if unique_uuids:
|
|
1160
|
+
desc_parts.append(f"{unique_uuids} RPC UUIDs")
|
|
1161
|
+
if pipes:
|
|
1162
|
+
desc_parts.append(f"{len(pipes)} named pipes")
|
|
1163
|
+
if service_names:
|
|
1164
|
+
desc_parts.append(f"{len(service_names)} services")
|
|
1165
|
+
|
|
1166
|
+
# Build description with discovered services
|
|
1167
|
+
description = f'RPC endpoint enumeration found: {", ".join(desc_parts)}.'
|
|
1168
|
+
if service_names:
|
|
1169
|
+
svc_list = ", ".join(service_names[:8])
|
|
1170
|
+
if len(service_names) > 8:
|
|
1171
|
+
svc_list += f"... (+{len(service_names) - 8} more)"
|
|
1172
|
+
description += f" Services: {svc_list}"
|
|
1173
|
+
if pipes:
|
|
1174
|
+
description += (
|
|
1175
|
+
f' Pipes: {", ".join(pipes[:5])}{"..." if len(pipes) > 5 else ""}'
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
findings.append(
|
|
1179
|
+
{
|
|
1180
|
+
"title": f'RPC Endpoints Discovered ({", ".join(desc_parts)})',
|
|
1181
|
+
"severity": "medium", # Upgraded from info - this is useful recon
|
|
1182
|
+
"description": description,
|
|
1183
|
+
"port": 135,
|
|
1184
|
+
"service": "msrpc",
|
|
1185
|
+
"data": {
|
|
1186
|
+
"endpoints": endpoints[:20], # Limit for storage
|
|
1187
|
+
"pipes": pipes,
|
|
1188
|
+
"service_names": service_names,
|
|
1189
|
+
},
|
|
1190
|
+
}
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# Add msrpc service
|
|
1194
|
+
services.append(
|
|
1195
|
+
{
|
|
1196
|
+
"port": 135,
|
|
1197
|
+
"protocol": "tcp",
|
|
1198
|
+
"service_name": "msrpc",
|
|
1199
|
+
"service_version": f"RPC ({len(endpoints)} endpoints)",
|
|
1200
|
+
}
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
# Check for interesting pipes that indicate attack vectors
|
|
1204
|
+
interesting_pipes = {
|
|
1205
|
+
"spoolss": ("Print Spooler", "medium", "PrintNightmare potential"),
|
|
1206
|
+
"samr": ("SAM Remote", "medium", "User enumeration possible"),
|
|
1207
|
+
"lsass": ("LSASS", "medium", "Domain/credential operations"),
|
|
1208
|
+
"lsarpc": ("LSA Remote", "medium", "Domain enumeration possible"),
|
|
1209
|
+
"netlogon": ("Netlogon", "high", "ZeroLogon check recommended"),
|
|
1210
|
+
"epmapper": ("Endpoint Mapper", "info", "RPC enumeration confirmed"),
|
|
1211
|
+
"protected_storage": (
|
|
1212
|
+
"Protected Storage",
|
|
1213
|
+
"medium",
|
|
1214
|
+
"Credential storage access",
|
|
1215
|
+
),
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
found_interesting = set()
|
|
1219
|
+
for pipe in pipes:
|
|
1220
|
+
# Normalize pipe path - handles \pipe\lsass and \\pipe\\lsass formats
|
|
1221
|
+
pipe_lower = (
|
|
1222
|
+
pipe.lower().replace("\\pipe\\", "").replace("\\", "").replace("/", "")
|
|
1223
|
+
)
|
|
1224
|
+
for key, (name, sev, desc) in interesting_pipes.items():
|
|
1225
|
+
if key in pipe_lower and key not in found_interesting:
|
|
1226
|
+
found_interesting.add(key)
|
|
1227
|
+
findings.append(
|
|
1228
|
+
{
|
|
1229
|
+
"title": f"{name} Pipe Available",
|
|
1230
|
+
"severity": sev,
|
|
1231
|
+
"description": f"{desc}. Pipe: {pipe}",
|
|
1232
|
+
"port": 135,
|
|
1233
|
+
"service": "msrpc",
|
|
1234
|
+
}
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
return {"findings": findings, "services": services}
|
|
870
1238
|
|
|
871
1239
|
|
|
872
1240
|
def parse_msf_ghostcat(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -883,38 +1251,40 @@ def parse_msf_ghostcat(output: str, target: str) -> Dict[str, Any]:
|
|
|
883
1251
|
|
|
884
1252
|
# Look for file content (indicates successful file read)
|
|
885
1253
|
# Ghostcat returns file contents like web.xml
|
|
886
|
-
has_xml_content =
|
|
887
|
-
has_file_content =
|
|
1254
|
+
has_xml_content = "<?xml" in clean_output or "<web-app" in clean_output
|
|
1255
|
+
has_file_content = "WEB-INF" in clean_output or "servlet" in clean_output.lower()
|
|
888
1256
|
|
|
889
1257
|
# Check for explicit success messages
|
|
890
1258
|
# Format: [+] File contents retrieved successfully
|
|
891
|
-
success_pattern = r
|
|
1259
|
+
success_pattern = r"\[\+\]\s+(.+)"
|
|
892
1260
|
success_messages = re.findall(success_pattern, clean_output)
|
|
893
1261
|
|
|
894
1262
|
if has_xml_content or has_file_content:
|
|
895
|
-
findings.append(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1263
|
+
findings.append(
|
|
1264
|
+
{
|
|
1265
|
+
"title": "Tomcat Ghostcat File Read (CVE-2020-1938)",
|
|
1266
|
+
"severity": "high",
|
|
1267
|
+
"description": "Successfully read file contents via Tomcat AJP connector. This confirms CVE-2020-1938 vulnerability allowing arbitrary file read from the web application directory.",
|
|
1268
|
+
"port": 8009,
|
|
1269
|
+
"service": "ajp13",
|
|
1270
|
+
"data": {"evidence": "XML/file content returned in response"},
|
|
1271
|
+
}
|
|
1272
|
+
)
|
|
903
1273
|
elif success_messages:
|
|
904
1274
|
for msg in success_messages:
|
|
905
|
-
if
|
|
906
|
-
findings.append(
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1275
|
+
if "File" in msg or "content" in msg.lower():
|
|
1276
|
+
findings.append(
|
|
1277
|
+
{
|
|
1278
|
+
"title": "Tomcat Ghostcat Vulnerability Confirmed",
|
|
1279
|
+
"severity": "high",
|
|
1280
|
+
"description": f"Ghostcat vulnerability (CVE-2020-1938) confirmed: {msg}",
|
|
1281
|
+
"port": 8009,
|
|
1282
|
+
"service": "ajp13",
|
|
1283
|
+
}
|
|
1284
|
+
)
|
|
913
1285
|
break
|
|
914
1286
|
|
|
915
|
-
return {
|
|
916
|
-
'findings': findings
|
|
917
|
-
}
|
|
1287
|
+
return {"findings": findings}
|
|
918
1288
|
|
|
919
1289
|
|
|
920
1290
|
def parse_msf_exploit(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
@@ -932,82 +1302,92 @@ def parse_msf_exploit(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
|
932
1302
|
clean_output = strip_ansi_codes(output)
|
|
933
1303
|
|
|
934
1304
|
# Extract module name for display
|
|
935
|
-
module_name = module.split(
|
|
1305
|
+
module_name = module.split("/")[-1] if "/" in module else module
|
|
936
1306
|
|
|
937
1307
|
# Check for session opened
|
|
938
1308
|
# Format: [*] Command shell session 1 opened (192.168.1.224:4444 -> 192.168.1.240:35807)
|
|
939
1309
|
# Format: [*] Meterpreter session 1 opened (10.0.0.1:4444 -> 10.0.0.82:45678)
|
|
940
|
-
session_pattern =
|
|
1310
|
+
session_pattern = (
|
|
1311
|
+
r"\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)"
|
|
1312
|
+
)
|
|
941
1313
|
for match in re.finditer(session_pattern, clean_output, re.IGNORECASE):
|
|
942
1314
|
session_type = match.group(1)
|
|
943
1315
|
session_id = match.group(2)
|
|
944
1316
|
tunnel = match.group(3)
|
|
945
1317
|
|
|
946
|
-
sessions.append(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1318
|
+
sessions.append(
|
|
1319
|
+
{
|
|
1320
|
+
"id": session_id,
|
|
1321
|
+
"type": session_type.lower(),
|
|
1322
|
+
"tunnel": tunnel,
|
|
1323
|
+
"exploit": module,
|
|
1324
|
+
}
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
findings.append(
|
|
1328
|
+
{
|
|
1329
|
+
"title": f"Exploit Successful: {module_name}",
|
|
1330
|
+
"severity": "critical",
|
|
1331
|
+
"description": f"{session_type} session {session_id} opened via {module}. Tunnel: {tunnel}",
|
|
1332
|
+
"service": "exploit",
|
|
1333
|
+
"data": {
|
|
1334
|
+
"exploit": module,
|
|
1335
|
+
"session_id": session_id,
|
|
1336
|
+
"session_type": session_type.lower(),
|
|
1337
|
+
"tunnel": tunnel,
|
|
1338
|
+
},
|
|
963
1339
|
}
|
|
964
|
-
|
|
1340
|
+
)
|
|
965
1341
|
|
|
966
1342
|
# Also check for "Session X created in the background"
|
|
967
|
-
bg_session_pattern = r
|
|
1343
|
+
bg_session_pattern = r"\[\*\]\s+Session\s+(\d+)\s+created in the background"
|
|
968
1344
|
for match in re.finditer(bg_session_pattern, clean_output, re.IGNORECASE):
|
|
969
1345
|
session_id = match.group(1)
|
|
970
1346
|
# Only add if not already found
|
|
971
|
-
if not any(s[
|
|
972
|
-
sessions.append(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1347
|
+
if not any(s["id"] == session_id for s in sessions):
|
|
1348
|
+
sessions.append(
|
|
1349
|
+
{
|
|
1350
|
+
"id": session_id,
|
|
1351
|
+
"type": "unknown",
|
|
1352
|
+
"tunnel": "background",
|
|
1353
|
+
"exploit": module,
|
|
1354
|
+
}
|
|
1355
|
+
)
|
|
978
1356
|
|
|
979
1357
|
# Check for exploit failure indicators
|
|
980
1358
|
if not sessions:
|
|
981
1359
|
# Look for common failure patterns
|
|
982
1360
|
failure_patterns = [
|
|
983
|
-
(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
(r
|
|
1361
|
+
(
|
|
1362
|
+
r"Exploit completed, but no session was created",
|
|
1363
|
+
"Exploit ran but target not vulnerable or payload failed",
|
|
1364
|
+
),
|
|
1365
|
+
(r"Exploit failed", "Exploit execution failed"),
|
|
1366
|
+
(r"Target is not vulnerable", "Target not vulnerable to this exploit"),
|
|
1367
|
+
(r"Connection refused", "Could not connect to target service"),
|
|
1368
|
+
(r"Connection timed out", "Connection to target timed out"),
|
|
988
1369
|
]
|
|
989
1370
|
|
|
990
1371
|
for pattern, desc in failure_patterns:
|
|
991
1372
|
if re.search(pattern, clean_output, re.IGNORECASE):
|
|
992
|
-
findings.append(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1373
|
+
findings.append(
|
|
1374
|
+
{
|
|
1375
|
+
"title": f"Exploit Failed: {module_name}",
|
|
1376
|
+
"severity": "info",
|
|
1377
|
+
"description": desc,
|
|
1378
|
+
"service": "exploit",
|
|
1379
|
+
"data": {"exploit": module, "reason": desc},
|
|
1380
|
+
}
|
|
1381
|
+
)
|
|
999
1382
|
break
|
|
1000
1383
|
|
|
1001
|
-
return {
|
|
1002
|
-
'findings': findings,
|
|
1003
|
-
'sessions': sessions
|
|
1004
|
-
}
|
|
1384
|
+
return {"findings": findings, "sessions": sessions}
|
|
1005
1385
|
|
|
1006
1386
|
|
|
1007
1387
|
def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
1008
1388
|
"""
|
|
1009
1389
|
Generic parser for MSF modules without specific parsers.
|
|
1010
|
-
Extracts [+] success lines as findings.
|
|
1390
|
+
Extracts [+] success lines and important [*] info lines as findings.
|
|
1011
1391
|
|
|
1012
1392
|
Returns:
|
|
1013
1393
|
{
|
|
@@ -1021,9 +1401,18 @@ def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
|
1021
1401
|
|
|
1022
1402
|
# Extract all [+] lines (success indicators in MSF)
|
|
1023
1403
|
# Format: [+] 10.0.0.82:port - Message
|
|
1024
|
-
success_pattern = r
|
|
1404
|
+
success_pattern = r"\[\+\]\s+([\d.]+):?(\d*)\s*-?\s*(.+)"
|
|
1405
|
+
|
|
1406
|
+
# Also extract important [*] lines (info that contains findings)
|
|
1407
|
+
# These patterns indicate actual results worth reporting
|
|
1408
|
+
important_info_patterns = [
|
|
1409
|
+
r"\[\*\]\s+([\d.]+):?(\d*)\s*-?\s*(.*(?:Host is running|SMB Detected|detected|found|version|running).*)",
|
|
1410
|
+
r"\[\*\]\s+([\d.]+):?(\d*)\s*-?\s*(.*(?:credential|successful|authenticated|session).*)",
|
|
1411
|
+
]
|
|
1025
1412
|
|
|
1026
1413
|
seen_messages = set() # Avoid duplicate findings
|
|
1414
|
+
|
|
1415
|
+
# Process [+] lines first (always findings)
|
|
1027
1416
|
for match in re.finditer(success_pattern, clean_output):
|
|
1028
1417
|
ip = match.group(1)
|
|
1029
1418
|
port_str = match.group(2)
|
|
@@ -1037,29 +1426,79 @@ def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
|
|
|
1037
1426
|
port = int(port_str) if port_str else None
|
|
1038
1427
|
|
|
1039
1428
|
# Determine severity based on message content
|
|
1040
|
-
severity =
|
|
1041
|
-
if any(
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1429
|
+
severity = "info"
|
|
1430
|
+
if any(
|
|
1431
|
+
word in message.lower()
|
|
1432
|
+
for word in ["vulnerability", "vulnerable", "exploit", "rce", "injection"]
|
|
1433
|
+
):
|
|
1434
|
+
severity = "high"
|
|
1435
|
+
elif any(
|
|
1436
|
+
word in message.lower()
|
|
1437
|
+
for word in ["password", "credential", "authentication", "success"]
|
|
1438
|
+
):
|
|
1439
|
+
severity = "critical"
|
|
1440
|
+
elif any(
|
|
1441
|
+
word in message.lower() for word in ["detected", "found", "enabled", "open"]
|
|
1442
|
+
):
|
|
1443
|
+
severity = "medium"
|
|
1047
1444
|
|
|
1048
1445
|
# Extract module name for title
|
|
1049
|
-
module_name = module.split(
|
|
1050
|
-
|
|
1051
|
-
findings.append(
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1446
|
+
module_name = module.split("/")[-1] if "/" in module else module
|
|
1447
|
+
|
|
1448
|
+
findings.append(
|
|
1449
|
+
{
|
|
1450
|
+
"title": f'{module_name}: {message[:60]}{"..." if len(message) > 60 else ""}',
|
|
1451
|
+
"severity": severity,
|
|
1452
|
+
"description": message,
|
|
1453
|
+
"port": port,
|
|
1454
|
+
"service": "unknown",
|
|
1455
|
+
}
|
|
1456
|
+
)
|
|
1058
1457
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1458
|
+
# Process important [*] lines (only if they contain real findings)
|
|
1459
|
+
for pattern in important_info_patterns:
|
|
1460
|
+
for match in re.finditer(pattern, clean_output, re.IGNORECASE):
|
|
1461
|
+
ip = match.group(1)
|
|
1462
|
+
port_str = match.group(2)
|
|
1463
|
+
message = match.group(3).strip()
|
|
1464
|
+
|
|
1465
|
+
# Skip empty, duplicate, or progress messages
|
|
1466
|
+
if not message or message in seen_messages:
|
|
1467
|
+
continue
|
|
1468
|
+
if "Scanned" in message and "of" in message and "hosts" in message:
|
|
1469
|
+
continue # Skip "Scanned X of Y hosts" progress messages
|
|
1470
|
+
if "module execution completed" in message.lower():
|
|
1471
|
+
continue # Skip completion messages
|
|
1472
|
+
|
|
1473
|
+
seen_messages.add(message)
|
|
1474
|
+
|
|
1475
|
+
port = int(port_str) if port_str else None
|
|
1476
|
+
|
|
1477
|
+
# Determine severity
|
|
1478
|
+
severity = "info"
|
|
1479
|
+
if any(
|
|
1480
|
+
word in message.lower()
|
|
1481
|
+
for word in ["credential", "successful", "password"]
|
|
1482
|
+
):
|
|
1483
|
+
severity = "critical"
|
|
1484
|
+
elif any(
|
|
1485
|
+
word in message.lower() for word in ["detected", "version", "running"]
|
|
1486
|
+
):
|
|
1487
|
+
severity = "medium"
|
|
1488
|
+
|
|
1489
|
+
module_name = module.split("/")[-1] if "/" in module else module
|
|
1490
|
+
|
|
1491
|
+
findings.append(
|
|
1492
|
+
{
|
|
1493
|
+
"title": f'{module_name}: {message[:60]}{"..." if len(message) > 60 else ""}',
|
|
1494
|
+
"severity": severity,
|
|
1495
|
+
"description": message,
|
|
1496
|
+
"port": port,
|
|
1497
|
+
"service": "unknown",
|
|
1498
|
+
}
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
return {"findings": findings, "services": services}
|
|
1063
1502
|
|
|
1064
1503
|
|
|
1065
1504
|
def parse_msf_log(log_path: str) -> Dict[str, Any]:
|
|
@@ -1073,16 +1512,16 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
|
|
|
1073
1512
|
Parsed data with services and findings
|
|
1074
1513
|
"""
|
|
1075
1514
|
try:
|
|
1076
|
-
with open(log_path,
|
|
1515
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
1077
1516
|
content = f.read()
|
|
1078
1517
|
|
|
1079
1518
|
# Extract module and target from header
|
|
1080
1519
|
# New format: "=== Plugin: Metasploit Auxiliary ===" + "Args: ['auxiliary/...', '-o', ...]"
|
|
1081
1520
|
# Old format: "Module: auxiliary/..." + "Target: ..."
|
|
1082
|
-
module_match = re.search(r
|
|
1521
|
+
module_match = re.search(r"^Module:\s*(.+)$", content, re.MULTILINE)
|
|
1083
1522
|
# Fixed regex: extract first element from Args list (handles lists with multiple items)
|
|
1084
1523
|
args_match = re.search(r'^Args:\s*\[[\'"]([^\'"]+)[\'"]', content, re.MULTILINE)
|
|
1085
|
-
target_match = re.search(r
|
|
1524
|
+
target_match = re.search(r"^Target:\s*(.+)$", content, re.MULTILINE)
|
|
1086
1525
|
|
|
1087
1526
|
# Determine module name (prefer Module: header, fallback to Args: parsing)
|
|
1088
1527
|
if module_match:
|
|
@@ -1091,11 +1530,13 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
|
|
|
1091
1530
|
module = args_match.group(1).strip()
|
|
1092
1531
|
else:
|
|
1093
1532
|
# Try to find module from "use" command in output
|
|
1094
|
-
use_match = re.search(r
|
|
1533
|
+
use_match = re.search(r"use\s+(auxiliary/\S+|exploit/\S+)", content)
|
|
1095
1534
|
if use_match:
|
|
1096
1535
|
module = use_match.group(1).strip()
|
|
1097
1536
|
else:
|
|
1098
|
-
return {
|
|
1537
|
+
return {
|
|
1538
|
+
"error": "Could not parse MSF log header - no module/args found"
|
|
1539
|
+
}
|
|
1099
1540
|
|
|
1100
1541
|
if not target_match:
|
|
1101
1542
|
return {"error": "Could not parse MSF log header - no target found"}
|
|
@@ -1103,37 +1544,41 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
|
|
|
1103
1544
|
target = target_match.group(1).strip()
|
|
1104
1545
|
|
|
1105
1546
|
# Route to appropriate parser based on module
|
|
1106
|
-
if
|
|
1547
|
+
if "ssh_version" in module:
|
|
1107
1548
|
return parse_msf_ssh_version(content, target)
|
|
1108
|
-
elif
|
|
1549
|
+
elif "smb_version" in module:
|
|
1109
1550
|
return parse_msf_smb_version(content, target)
|
|
1110
|
-
elif
|
|
1551
|
+
elif "ssh_enumusers" in module:
|
|
1111
1552
|
return parse_msf_ssh_enumusers(content, target)
|
|
1112
|
-
elif
|
|
1553
|
+
elif "kerberos_enumusers" in module:
|
|
1554
|
+
return parse_msf_kerberos_enumusers(content, target)
|
|
1555
|
+
elif "smb_enumshares" in module:
|
|
1113
1556
|
return parse_msf_smb_enumshares(content, target)
|
|
1114
|
-
elif
|
|
1557
|
+
elif "smtp_enum" in module:
|
|
1115
1558
|
return parse_msf_smtp_enum(content, target)
|
|
1116
|
-
elif
|
|
1559
|
+
elif "nfsmount" in module:
|
|
1117
1560
|
return parse_msf_nfs_mount(content, target)
|
|
1118
|
-
elif
|
|
1561
|
+
elif "ftp/anonymous" in module or "ftp_anonymous" in module:
|
|
1119
1562
|
# FTP anonymous access scanner
|
|
1120
1563
|
return parse_msf_ftp_anonymous(content, target)
|
|
1121
|
-
elif
|
|
1564
|
+
elif "mysql_login" in module:
|
|
1122
1565
|
# MySQL login scanner - extracts version + credentials
|
|
1123
1566
|
return parse_msf_mysql_login(content, target)
|
|
1124
|
-
elif
|
|
1567
|
+
elif "vnc_login" in module:
|
|
1125
1568
|
# VNC login scanner - route to login parser for credentials
|
|
1126
1569
|
return parse_msf_login_success(content, target, module)
|
|
1127
|
-
elif any(x in module for x in [
|
|
1570
|
+
elif any(x in module for x in ["_login", "brute"]):
|
|
1128
1571
|
# Any login/brute force module
|
|
1129
1572
|
return parse_msf_login_success(content, target, module)
|
|
1130
|
-
elif
|
|
1573
|
+
elif "java_rmi" in module:
|
|
1131
1574
|
return parse_msf_java_rmi(content, target)
|
|
1132
|
-
elif
|
|
1575
|
+
elif "vnc_none_auth" in module:
|
|
1133
1576
|
return parse_msf_vnc_auth(content, target)
|
|
1134
|
-
elif
|
|
1577
|
+
elif "ghostcat" in module or "tomcat_ghostcat" in module:
|
|
1135
1578
|
return parse_msf_ghostcat(content, target)
|
|
1136
|
-
elif module
|
|
1579
|
+
elif "endpoint_mapper" in module:
|
|
1580
|
+
return parse_msf_endpoint_mapper(content, target)
|
|
1581
|
+
elif module.startswith("exploit/"):
|
|
1137
1582
|
# Route exploit modules to exploit parser
|
|
1138
1583
|
return parse_msf_exploit(content, target, module)
|
|
1139
1584
|
else:
|