souleyez 2.43.29__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 +22827 -10678
- 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.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Handler for ldapsearch LDAP enumeration tool.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from souleyez.handlers.base import BaseToolHandler
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
STATUS_DONE = "done"
|
|
17
|
+
STATUS_ERROR = "error"
|
|
18
|
+
STATUS_WARNING = "warning"
|
|
19
|
+
STATUS_NO_RESULTS = "no_results"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LdapsearchHandler(BaseToolHandler):
|
|
23
|
+
"""Handler for ldapsearch LDAP queries."""
|
|
24
|
+
|
|
25
|
+
tool_name = "ldapsearch"
|
|
26
|
+
display_name = "ldapsearch"
|
|
27
|
+
|
|
28
|
+
has_error_handler = True
|
|
29
|
+
has_warning_handler = True
|
|
30
|
+
has_no_results_handler = True
|
|
31
|
+
has_done_handler = True
|
|
32
|
+
|
|
33
|
+
# Patterns indicating successful LDAP enumeration
|
|
34
|
+
SUCCESS_PATTERNS = [
|
|
35
|
+
r"namingContexts:",
|
|
36
|
+
r"dn:\s+\S+",
|
|
37
|
+
r"objectClass:",
|
|
38
|
+
r"DC=\w+",
|
|
39
|
+
r"CN=\w+",
|
|
40
|
+
r"# numEntries:\s*[1-9]",
|
|
41
|
+
r"result:\s*0\s+Success",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Patterns indicating errors
|
|
45
|
+
ERROR_PATTERNS = [
|
|
46
|
+
(r"Can\'t contact LDAP server", "LDAP server unreachable"),
|
|
47
|
+
(r"Invalid credentials", "Authentication failed"),
|
|
48
|
+
(r"No such object", "Base DN not found"),
|
|
49
|
+
(r"Operations error", "LDAP operations error"),
|
|
50
|
+
(r"Confidentiality required", "TLS/SSL required"),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def parse_job(
|
|
54
|
+
self,
|
|
55
|
+
engagement_id: int,
|
|
56
|
+
log_path: str,
|
|
57
|
+
job: Dict[str, Any],
|
|
58
|
+
host_manager: Optional[Any] = None,
|
|
59
|
+
findings_manager: Optional[Any] = None,
|
|
60
|
+
credentials_manager: Optional[Any] = None,
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Parse ldapsearch results."""
|
|
63
|
+
try:
|
|
64
|
+
target = job.get("target", "")
|
|
65
|
+
|
|
66
|
+
if not log_path or not os.path.exists(log_path):
|
|
67
|
+
return {
|
|
68
|
+
"tool": "ldapsearch",
|
|
69
|
+
"status": STATUS_ERROR,
|
|
70
|
+
"error": "Log file not found",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
74
|
+
log_content = f.read()
|
|
75
|
+
|
|
76
|
+
# Check exit code first - non-zero means error
|
|
77
|
+
exit_code_match = re.search(r"Exit Code:\s*(\d+)", log_content)
|
|
78
|
+
if exit_code_match:
|
|
79
|
+
exit_code = int(exit_code_match.group(1))
|
|
80
|
+
if exit_code != 0:
|
|
81
|
+
# Special case: "successful bind must be completed" means anonymous access blocked
|
|
82
|
+
# This is not a hard error - kerbrute or authenticated ldapsearch will handle it
|
|
83
|
+
# Normalize LDIF line continuations (newline + space) before checking
|
|
84
|
+
normalized_content = re.sub(r"\n ", "", log_content.lower())
|
|
85
|
+
if (
|
|
86
|
+
"successful bind must be completed" in normalized_content
|
|
87
|
+
or "bind must be completed" in normalized_content
|
|
88
|
+
or "000004dc" in normalized_content
|
|
89
|
+
): # LDAP error code for anonymous blocked
|
|
90
|
+
return {
|
|
91
|
+
"tool": "ldapsearch",
|
|
92
|
+
"status": STATUS_NO_RESULTS,
|
|
93
|
+
"target": target,
|
|
94
|
+
"note": "Anonymous bind not allowed - authentication required",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Map common LDAP exit codes to error messages
|
|
98
|
+
ldap_errors = {
|
|
99
|
+
32: "No such object (invalid base DN)",
|
|
100
|
+
49: "Invalid credentials",
|
|
101
|
+
1: "Operations error",
|
|
102
|
+
2: "Protocol error",
|
|
103
|
+
52: "Server unavailable",
|
|
104
|
+
}
|
|
105
|
+
error_msg = ldap_errors.get(
|
|
106
|
+
exit_code, f"LDAP error (exit code {exit_code})"
|
|
107
|
+
)
|
|
108
|
+
return {
|
|
109
|
+
"tool": "ldapsearch",
|
|
110
|
+
"status": STATUS_ERROR,
|
|
111
|
+
"target": target,
|
|
112
|
+
"error": error_msg,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Check for errors in output
|
|
116
|
+
for pattern, error_msg in self.ERROR_PATTERNS:
|
|
117
|
+
if re.search(pattern, log_content, re.IGNORECASE):
|
|
118
|
+
return {
|
|
119
|
+
"tool": "ldapsearch",
|
|
120
|
+
"status": STATUS_ERROR,
|
|
121
|
+
"target": target,
|
|
122
|
+
"error": error_msg,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Check for success patterns
|
|
126
|
+
success_found = False
|
|
127
|
+
naming_contexts = []
|
|
128
|
+
entries_count = 0
|
|
129
|
+
|
|
130
|
+
for pattern in self.SUCCESS_PATTERNS:
|
|
131
|
+
if re.search(pattern, log_content, re.IGNORECASE):
|
|
132
|
+
success_found = True
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Extract naming contexts (AD domain info)
|
|
136
|
+
nc_matches = re.findall(r"namingContexts:\s*(.+)", log_content)
|
|
137
|
+
naming_contexts = [nc.strip() for nc in nc_matches]
|
|
138
|
+
|
|
139
|
+
# Extract entry count
|
|
140
|
+
entries_match = re.search(r"# numEntries:\s*(\d+)", log_content)
|
|
141
|
+
if entries_match:
|
|
142
|
+
entries_count = int(entries_match.group(1))
|
|
143
|
+
|
|
144
|
+
# Check for "result: 0 Success" which indicates successful query
|
|
145
|
+
if "result: 0 Success" in log_content:
|
|
146
|
+
success_found = True
|
|
147
|
+
|
|
148
|
+
if success_found or naming_contexts or entries_count > 0:
|
|
149
|
+
domains = self._extract_domains(naming_contexts)
|
|
150
|
+
# Extract base DN for chaining (e.g., "DC=baby,DC=vl")
|
|
151
|
+
base_dn = self._extract_base_dn(naming_contexts)
|
|
152
|
+
|
|
153
|
+
# Parse user entries if this is a user enumeration query
|
|
154
|
+
users = self._parse_user_entries(log_content)
|
|
155
|
+
usernames = [
|
|
156
|
+
u.get("sAMAccountName", "")
|
|
157
|
+
for u in users
|
|
158
|
+
if u.get("sAMAccountName")
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Check for passwords in descriptions
|
|
162
|
+
credentials_found = []
|
|
163
|
+
for user in users:
|
|
164
|
+
description = user.get("description", "")
|
|
165
|
+
if description:
|
|
166
|
+
password = self._extract_password_from_description(description)
|
|
167
|
+
if password:
|
|
168
|
+
credentials_found.append(
|
|
169
|
+
{
|
|
170
|
+
"username": user.get("sAMAccountName", ""),
|
|
171
|
+
"password": password,
|
|
172
|
+
"source": "ldap_description",
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"CREDENTIAL FOUND in LDAP description: "
|
|
177
|
+
f"{user.get('sAMAccountName')} - check description field"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Store credentials if found
|
|
181
|
+
if credentials_found and credentials_manager:
|
|
182
|
+
if host_manager is None:
|
|
183
|
+
from souleyez.storage.hosts import HostManager
|
|
184
|
+
|
|
185
|
+
host_manager = HostManager()
|
|
186
|
+
|
|
187
|
+
host = host_manager.get_host_by_ip(engagement_id, target)
|
|
188
|
+
if host:
|
|
189
|
+
for cred in credentials_found:
|
|
190
|
+
credentials_manager.add_credential(
|
|
191
|
+
engagement_id=engagement_id,
|
|
192
|
+
host_id=host["id"],
|
|
193
|
+
username=cred["username"],
|
|
194
|
+
password=cred["password"],
|
|
195
|
+
service="ldap",
|
|
196
|
+
credential_type="password",
|
|
197
|
+
tool="ldapsearch",
|
|
198
|
+
status="potential",
|
|
199
|
+
notes="Found in LDAP user description field",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
logger.info(
|
|
203
|
+
f"ldapsearch parse complete: {len(naming_contexts)} naming contexts, "
|
|
204
|
+
f"{len(domains)} domains, {len(users)} users, {len(credentials_found)} creds, base_dn={base_dn}"
|
|
205
|
+
)
|
|
206
|
+
return {
|
|
207
|
+
"tool": "ldapsearch",
|
|
208
|
+
"status": STATUS_DONE,
|
|
209
|
+
"target": target,
|
|
210
|
+
"naming_contexts": naming_contexts,
|
|
211
|
+
"entries_count": entries_count,
|
|
212
|
+
"domains": domains,
|
|
213
|
+
"base_dn": base_dn, # For LDAP user enumeration chains
|
|
214
|
+
"users": usernames,
|
|
215
|
+
"users_found": len(users),
|
|
216
|
+
"credentials_found": credentials_found,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {"tool": "ldapsearch", "status": STATUS_NO_RESULTS, "target": target}
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Error parsing ldapsearch job: {e}")
|
|
223
|
+
return {"tool": "ldapsearch", "status": STATUS_ERROR, "error": str(e)}
|
|
224
|
+
|
|
225
|
+
def _extract_base_dn(self, naming_contexts):
|
|
226
|
+
"""Extract the base DN for user enumeration (e.g., 'DC=baby,DC=vl').
|
|
227
|
+
|
|
228
|
+
Returns the primary domain DN, filtering out zone-specific contexts.
|
|
229
|
+
"""
|
|
230
|
+
# Zone prefixes to skip
|
|
231
|
+
zone_prefixes = {"domaindnszones", "forestdnszones", "configuration", "schema"}
|
|
232
|
+
|
|
233
|
+
for nc in naming_contexts:
|
|
234
|
+
nc = nc.strip()
|
|
235
|
+
# Match patterns like DC=active,DC=htb
|
|
236
|
+
dc_match = re.findall(r"DC=([^,]+)", nc, re.IGNORECASE)
|
|
237
|
+
if dc_match and len(dc_match) >= 2:
|
|
238
|
+
# Skip if first component is a zone prefix
|
|
239
|
+
if dc_match[0].lower() in zone_prefixes:
|
|
240
|
+
continue
|
|
241
|
+
# This is the primary domain DN (e.g., DC=baby,DC=vl)
|
|
242
|
+
return nc
|
|
243
|
+
|
|
244
|
+
return ""
|
|
245
|
+
|
|
246
|
+
def _extract_domains(self, naming_contexts):
|
|
247
|
+
"""Extract domain names from naming contexts like DC=active,DC=htb.
|
|
248
|
+
|
|
249
|
+
Filters out zone-specific naming contexts (DomainDnsZones, ForestDnsZones)
|
|
250
|
+
to extract the actual AD domain name.
|
|
251
|
+
"""
|
|
252
|
+
domains = []
|
|
253
|
+
seen_domains = set()
|
|
254
|
+
|
|
255
|
+
# Zone prefixes to skip (these are subzones, not the main domain)
|
|
256
|
+
zone_prefixes = {"domaindnszones", "forestdnszones", "configuration", "schema"}
|
|
257
|
+
|
|
258
|
+
for nc in naming_contexts:
|
|
259
|
+
# Match patterns like DC=active,DC=htb
|
|
260
|
+
dc_match = re.findall(r"DC=([^,]+)", nc, re.IGNORECASE)
|
|
261
|
+
if dc_match and len(dc_match) >= 2:
|
|
262
|
+
# Skip if first component is a zone prefix
|
|
263
|
+
if dc_match[0].lower() in zone_prefixes:
|
|
264
|
+
# Use remaining components as domain
|
|
265
|
+
dc_match = dc_match[1:]
|
|
266
|
+
if len(dc_match) < 2:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
domain = ".".join(dc_match)
|
|
270
|
+
domain_lower = domain.lower()
|
|
271
|
+
|
|
272
|
+
if domain_lower not in seen_domains:
|
|
273
|
+
seen_domains.add(domain_lower)
|
|
274
|
+
domains.append({"domain": domain, "source": "ldapsearch"})
|
|
275
|
+
logger.info(f"Extracted AD domain from LDAP: {domain}")
|
|
276
|
+
|
|
277
|
+
if domains:
|
|
278
|
+
logger.info(
|
|
279
|
+
f"ldapsearch found {len(domains)} domain(s): {[d['domain'] for d in domains]}"
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"ldapsearch: No domains extracted from naming contexts: {naming_contexts}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return domains
|
|
287
|
+
|
|
288
|
+
def display_done(
|
|
289
|
+
self,
|
|
290
|
+
job: Dict[str, Any],
|
|
291
|
+
log_path: str,
|
|
292
|
+
show_all: bool = False,
|
|
293
|
+
show_passwords: bool = False,
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Display successful ldapsearch results."""
|
|
296
|
+
click.echo()
|
|
297
|
+
click.echo(click.style("=" * 70, fg="green"))
|
|
298
|
+
click.echo(click.style("LDAP ENUMERATION SUCCESSFUL", fg="green", bold=True))
|
|
299
|
+
click.echo(click.style("=" * 70, fg="green"))
|
|
300
|
+
click.echo()
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
304
|
+
log_content = f.read()
|
|
305
|
+
|
|
306
|
+
# Show naming contexts (domain structure query)
|
|
307
|
+
nc_matches = re.findall(r"namingContexts:\s*(.+)", log_content)
|
|
308
|
+
if nc_matches:
|
|
309
|
+
click.echo(
|
|
310
|
+
click.style(" NAMING CONTEXTS (Domain Structure)", bold=True)
|
|
311
|
+
)
|
|
312
|
+
for nc in nc_matches:
|
|
313
|
+
click.echo(f" {nc.strip()}")
|
|
314
|
+
click.echo()
|
|
315
|
+
|
|
316
|
+
# Parse user enumeration results (sAMAccountName, description, memberOf)
|
|
317
|
+
users = self._parse_user_entries(log_content)
|
|
318
|
+
if users:
|
|
319
|
+
click.echo(
|
|
320
|
+
click.style(f" USERS FOUND ({len(users)})", bold=True, fg="cyan")
|
|
321
|
+
)
|
|
322
|
+
click.echo()
|
|
323
|
+
|
|
324
|
+
# Check for passwords in descriptions
|
|
325
|
+
password_hints = []
|
|
326
|
+
for user in users:
|
|
327
|
+
username = user.get("sAMAccountName", "Unknown")
|
|
328
|
+
description = user.get("description", "")
|
|
329
|
+
groups = user.get("memberOf", [])
|
|
330
|
+
ou = user.get("ou", "")
|
|
331
|
+
|
|
332
|
+
# Display user
|
|
333
|
+
click.echo(f" {click.style(username, bold=True)}")
|
|
334
|
+
if ou:
|
|
335
|
+
click.echo(f" OU: {ou}")
|
|
336
|
+
if groups:
|
|
337
|
+
click.echo(f" Groups: {', '.join(groups)}")
|
|
338
|
+
|
|
339
|
+
# Check for password hints in description
|
|
340
|
+
if description:
|
|
341
|
+
# Detect potential passwords
|
|
342
|
+
password_keywords = [
|
|
343
|
+
"password",
|
|
344
|
+
"pass",
|
|
345
|
+
"pwd",
|
|
346
|
+
"credential",
|
|
347
|
+
"secret",
|
|
348
|
+
"initial",
|
|
349
|
+
]
|
|
350
|
+
has_password_hint = any(
|
|
351
|
+
kw in description.lower() for kw in password_keywords
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if has_password_hint:
|
|
355
|
+
click.echo(
|
|
356
|
+
click.style(
|
|
357
|
+
f" Description: {description}",
|
|
358
|
+
fg="red",
|
|
359
|
+
bold=True,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
click.echo(
|
|
363
|
+
click.style(
|
|
364
|
+
" ^^^ POTENTIAL PASSWORD IN DESCRIPTION!",
|
|
365
|
+
fg="red",
|
|
366
|
+
bold=True,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
password_hints.append(
|
|
370
|
+
{"user": username, "description": description}
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
click.echo(f" Description: {description}")
|
|
374
|
+
click.echo()
|
|
375
|
+
|
|
376
|
+
# Summary of password findings
|
|
377
|
+
if password_hints:
|
|
378
|
+
click.echo()
|
|
379
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
380
|
+
click.echo(
|
|
381
|
+
click.style(
|
|
382
|
+
" CREDENTIALS FOUND IN DESCRIPTIONS!", fg="red", bold=True
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
386
|
+
for hint in password_hints:
|
|
387
|
+
click.echo(f" User: {click.style(hint['user'], bold=True)}")
|
|
388
|
+
click.echo(
|
|
389
|
+
f" Desc: {click.style(hint['description'], fg='red')}"
|
|
390
|
+
)
|
|
391
|
+
# Try to extract actual password
|
|
392
|
+
password = self._extract_password_from_description(
|
|
393
|
+
hint["description"]
|
|
394
|
+
)
|
|
395
|
+
if password:
|
|
396
|
+
click.echo(
|
|
397
|
+
f" Password: {click.style(password, fg='red', bold=True)}"
|
|
398
|
+
)
|
|
399
|
+
click.echo()
|
|
400
|
+
|
|
401
|
+
# Show entry count
|
|
402
|
+
entries_match = re.search(r"# numEntries:\s*(\d+)", log_content)
|
|
403
|
+
if entries_match and not users:
|
|
404
|
+
click.echo(f" Entries found: {entries_match.group(1)}")
|
|
405
|
+
click.echo()
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
click.echo(f" Error reading log: {e}")
|
|
409
|
+
logger.debug(f"Error in display_done: {e}")
|
|
410
|
+
|
|
411
|
+
click.echo()
|
|
412
|
+
|
|
413
|
+
def _parse_user_entries(self, log_content: str) -> list:
|
|
414
|
+
"""Parse user entries from ldapsearch output."""
|
|
415
|
+
users = []
|
|
416
|
+
current_user = {}
|
|
417
|
+
|
|
418
|
+
for line in log_content.split("\n"):
|
|
419
|
+
line = line.strip()
|
|
420
|
+
|
|
421
|
+
# New entry starts with "dn:"
|
|
422
|
+
if line.startswith("dn:"):
|
|
423
|
+
if current_user and current_user.get("sAMAccountName"):
|
|
424
|
+
users.append(current_user)
|
|
425
|
+
current_user = {}
|
|
426
|
+
# Extract OU from DN
|
|
427
|
+
ou_match = re.search(r"OU=([^,]+)", line, re.IGNORECASE)
|
|
428
|
+
if ou_match:
|
|
429
|
+
current_user["ou"] = ou_match.group(1)
|
|
430
|
+
# Extract CN from DN as potential username (for broader queries)
|
|
431
|
+
# DN format: CN=Caroline Robinson,OU=it,DC=baby,DC=vl
|
|
432
|
+
cn_match = re.search(r"dn:\s*CN=([^,]+)", line, re.IGNORECASE)
|
|
433
|
+
if cn_match:
|
|
434
|
+
cn_name = cn_match.group(1).strip()
|
|
435
|
+
# Convert "Caroline Robinson" to "Caroline.Robinson" format
|
|
436
|
+
# Skip non-user entries (groups, OUs, etc.)
|
|
437
|
+
if " " in cn_name and not cn_name.lower().startswith(
|
|
438
|
+
(
|
|
439
|
+
"domain ",
|
|
440
|
+
"enterprise ",
|
|
441
|
+
"schema ",
|
|
442
|
+
"cert ",
|
|
443
|
+
"group ",
|
|
444
|
+
"read-",
|
|
445
|
+
"allowed ",
|
|
446
|
+
"denied ",
|
|
447
|
+
"protected ",
|
|
448
|
+
"key ",
|
|
449
|
+
"dns",
|
|
450
|
+
"ras ",
|
|
451
|
+
)
|
|
452
|
+
):
|
|
453
|
+
current_user["cn_username"] = cn_name.replace(" ", ".")
|
|
454
|
+
|
|
455
|
+
elif line.startswith("sAMAccountName:"):
|
|
456
|
+
current_user["sAMAccountName"] = line.split(":", 1)[1].strip()
|
|
457
|
+
|
|
458
|
+
elif line.startswith("description:"):
|
|
459
|
+
current_user["description"] = line.split(":", 1)[1].strip()
|
|
460
|
+
|
|
461
|
+
elif line.startswith("memberOf:"):
|
|
462
|
+
if "memberOf" not in current_user:
|
|
463
|
+
current_user["memberOf"] = []
|
|
464
|
+
# Extract CN from memberOf
|
|
465
|
+
cn_match = re.search(r"CN=([^,]+)", line, re.IGNORECASE)
|
|
466
|
+
if cn_match:
|
|
467
|
+
current_user["memberOf"].append(cn_match.group(1))
|
|
468
|
+
|
|
469
|
+
# Don't forget last user
|
|
470
|
+
if current_user and current_user.get("sAMAccountName"):
|
|
471
|
+
users.append(current_user)
|
|
472
|
+
|
|
473
|
+
# Second pass: add users found only via CN (no sAMAccountName returned)
|
|
474
|
+
# This catches users from broader queries like (objectClass=*)
|
|
475
|
+
current_user = {}
|
|
476
|
+
for line in log_content.split("\n"):
|
|
477
|
+
line = line.strip()
|
|
478
|
+
if line.startswith("dn:"):
|
|
479
|
+
if current_user.get("cn_username") and not current_user.get(
|
|
480
|
+
"sAMAccountName"
|
|
481
|
+
):
|
|
482
|
+
# Check if we already have this user
|
|
483
|
+
existing = [
|
|
484
|
+
u
|
|
485
|
+
for u in users
|
|
486
|
+
if u.get("sAMAccountName") == current_user.get("cn_username")
|
|
487
|
+
]
|
|
488
|
+
if not existing:
|
|
489
|
+
current_user["sAMAccountName"] = current_user["cn_username"]
|
|
490
|
+
users.append(current_user)
|
|
491
|
+
current_user = {}
|
|
492
|
+
cn_match = re.search(r"dn:\s*CN=([^,]+)", line, re.IGNORECASE)
|
|
493
|
+
ou_match = re.search(r"OU=([^,]+)", line, re.IGNORECASE)
|
|
494
|
+
if cn_match:
|
|
495
|
+
cn_name = cn_match.group(1).strip()
|
|
496
|
+
if " " in cn_name and not cn_name.lower().startswith(
|
|
497
|
+
(
|
|
498
|
+
"domain ",
|
|
499
|
+
"enterprise ",
|
|
500
|
+
"schema ",
|
|
501
|
+
"cert ",
|
|
502
|
+
"group ",
|
|
503
|
+
"read-",
|
|
504
|
+
"allowed ",
|
|
505
|
+
"denied ",
|
|
506
|
+
"protected ",
|
|
507
|
+
"key ",
|
|
508
|
+
"dns",
|
|
509
|
+
"ras ",
|
|
510
|
+
)
|
|
511
|
+
):
|
|
512
|
+
current_user["cn_username"] = cn_name.replace(" ", ".")
|
|
513
|
+
if ou_match:
|
|
514
|
+
current_user["ou"] = ou_match.group(1)
|
|
515
|
+
elif line.startswith("sAMAccountName:"):
|
|
516
|
+
current_user["sAMAccountName"] = line.split(":", 1)[1].strip()
|
|
517
|
+
|
|
518
|
+
# Last entry from second pass
|
|
519
|
+
if current_user.get("cn_username") and not current_user.get("sAMAccountName"):
|
|
520
|
+
existing = [
|
|
521
|
+
u
|
|
522
|
+
for u in users
|
|
523
|
+
if u.get("sAMAccountName") == current_user.get("cn_username")
|
|
524
|
+
]
|
|
525
|
+
if not existing:
|
|
526
|
+
current_user["sAMAccountName"] = current_user["cn_username"]
|
|
527
|
+
users.append(current_user)
|
|
528
|
+
|
|
529
|
+
return users
|
|
530
|
+
|
|
531
|
+
def _extract_password_from_description(self, description: str) -> str:
|
|
532
|
+
"""Try to extract password from description text."""
|
|
533
|
+
# Common patterns for passwords in descriptions
|
|
534
|
+
# Order matters - more specific patterns first
|
|
535
|
+
patterns = [
|
|
536
|
+
# Phrases with "to" or "is" before the password
|
|
537
|
+
r"password\s+(?:is|to|=)\s*[:\s]*(\S+)",
|
|
538
|
+
r"set\s+.*?password\s+(?:to|is|=)\s*(\S+)",
|
|
539
|
+
r"initial\s+password\s+(?:to|is|=)?\s*[:\s]*(\S+)",
|
|
540
|
+
# Direct assignment patterns
|
|
541
|
+
r"password[:\s]*=\s*(\S+)",
|
|
542
|
+
r"pwd[:\s]*[:=]\s*(\S+)",
|
|
543
|
+
r"pass[:\s]*[:=]\s*(\S+)",
|
|
544
|
+
r"credential[:\s]*[:=]\s*(\S+)",
|
|
545
|
+
# Fallback: password followed by colon then value
|
|
546
|
+
r"password:\s*(\S+)",
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
for pattern in patterns:
|
|
550
|
+
match = re.search(pattern, description, re.IGNORECASE)
|
|
551
|
+
if match:
|
|
552
|
+
# Keep ! in passwords (common special char), only strip trailing periods/commas
|
|
553
|
+
password = match.group(1).rstrip(".,")
|
|
554
|
+
# Skip if captured word is a common filler (to, is, the, etc.)
|
|
555
|
+
if password.lower() in ["to", "is", "the", "a", "an", "set", "as"]:
|
|
556
|
+
continue
|
|
557
|
+
return password
|
|
558
|
+
|
|
559
|
+
return ""
|
|
560
|
+
|
|
561
|
+
def display_error(
|
|
562
|
+
self,
|
|
563
|
+
job: Dict[str, Any],
|
|
564
|
+
log_path: str,
|
|
565
|
+
show_all: bool = False,
|
|
566
|
+
) -> None:
|
|
567
|
+
"""Display ldapsearch error."""
|
|
568
|
+
click.echo()
|
|
569
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
570
|
+
click.echo(click.style("LDAP QUERY FAILED", fg="red", bold=True))
|
|
571
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
572
|
+
click.echo()
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
576
|
+
log_content = f.read()
|
|
577
|
+
|
|
578
|
+
for pattern, error_msg in self.ERROR_PATTERNS:
|
|
579
|
+
if re.search(pattern, log_content, re.IGNORECASE):
|
|
580
|
+
click.echo(f" Error: {error_msg}")
|
|
581
|
+
break
|
|
582
|
+
else:
|
|
583
|
+
click.echo(" LDAP query failed - check log for details")
|
|
584
|
+
|
|
585
|
+
except Exception:
|
|
586
|
+
click.echo(" Could not read error details")
|
|
587
|
+
|
|
588
|
+
click.echo()
|
|
589
|
+
|
|
590
|
+
def display_warning(
|
|
591
|
+
self,
|
|
592
|
+
job: Dict[str, Any],
|
|
593
|
+
log_path: str,
|
|
594
|
+
show_all: bool = False,
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Display ldapsearch warning."""
|
|
597
|
+
click.echo()
|
|
598
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
599
|
+
click.echo(click.style("LDAP QUERY - PARTIAL RESULTS", fg="yellow", bold=True))
|
|
600
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
601
|
+
click.echo()
|
|
602
|
+
click.echo(" Query completed with warnings - may need authentication")
|
|
603
|
+
click.echo()
|
|
604
|
+
|
|
605
|
+
def display_no_results(
|
|
606
|
+
self,
|
|
607
|
+
job: Dict[str, Any],
|
|
608
|
+
log_path: str,
|
|
609
|
+
show_all: bool = False,
|
|
610
|
+
) -> None:
|
|
611
|
+
"""Display ldapsearch no results."""
|
|
612
|
+
click.echo()
|
|
613
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
614
|
+
click.echo(click.style("LDAP QUERY - NO DATA RETURNED", fg="yellow", bold=True))
|
|
615
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
616
|
+
click.echo()
|
|
617
|
+
|
|
618
|
+
# Check if this was an auth-required case
|
|
619
|
+
parse_result = job.get("parse_result", {})
|
|
620
|
+
note = parse_result.get("note", "")
|
|
621
|
+
if "authentication required" in note.lower():
|
|
622
|
+
click.echo(" Anonymous LDAP bind blocked - authentication required.")
|
|
623
|
+
click.echo()
|
|
624
|
+
click.echo(click.style(" Next steps:", dim=True))
|
|
625
|
+
click.echo(" - kerbrute user enumeration (auto-chained)")
|
|
626
|
+
click.echo(
|
|
627
|
+
" - Once valid credentials found, authenticated LDAP will run"
|
|
628
|
+
)
|
|
629
|
+
else:
|
|
630
|
+
click.echo(" The LDAP query returned no entries.")
|
|
631
|
+
click.echo()
|
|
632
|
+
click.echo(click.style(" Tips:", dim=True))
|
|
633
|
+
click.echo(" - Try a different base DN")
|
|
634
|
+
click.echo(" - Anonymous binds may be disabled")
|
|
635
|
+
click.echo(" - Try with credentials: -D 'user@domain' -W")
|
|
636
|
+
click.echo()
|