souleyez 3.0.0__py3-none-any.whl → 3.0.7__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 -1
- souleyez/ai/__init__.py +7 -7
- souleyez/ai/action_mapper.py +3 -2
- souleyez/ai/chain_advisor.py +2 -1
- souleyez/ai/claude_provider.py +2 -2
- souleyez/ai/context_builder.py +4 -2
- souleyez/ai/executor.py +9 -6
- souleyez/ai/feedback_handler.py +4 -2
- souleyez/ai/llm_provider.py +2 -2
- souleyez/ai/ollama_provider.py +2 -2
- souleyez/ai/ollama_service.py +10 -26
- souleyez/ai/path_scorer.py +2 -1
- souleyez/ai/recommender.py +6 -4
- souleyez/ai/report_context.py +2 -2
- souleyez/ai/report_service.py +5 -5
- souleyez/ai/result_parser.py +3 -2
- souleyez/ai/safety.py +5 -2
- souleyez/auth/__init__.py +6 -6
- souleyez/auth/audit.py +2 -2
- souleyez/auth/engagement_access.py +5 -7
- souleyez/auth/permissions.py +1 -1
- souleyez/auth/session_manager.py +5 -5
- souleyez/auth/user_manager.py +4 -5
- souleyez/commands/audit.py +6 -5
- souleyez/commands/auth.py +6 -5
- souleyez/commands/deliverables.py +2 -3
- souleyez/commands/engagement.py +3 -3
- souleyez/commands/license.py +3 -2
- souleyez/commands/screenshots.py +5 -4
- souleyez/commands/user.py +10 -8
- souleyez/config.py +4 -2
- souleyez/core/credential_tester.py +4 -2
- souleyez/core/cve_mappings.py +2 -1
- souleyez/core/cve_matcher.py +2 -1
- souleyez/core/msf_auto_mapper.py +2 -0
- souleyez/core/msf_chain_engine.py +3 -1
- souleyez/core/msf_database.py +7 -13
- souleyez/core/msf_integration.py +2 -2
- souleyez/core/msf_rpc_client.py +3 -2
- souleyez/core/msf_rpc_manager.py +4 -4
- souleyez/core/msf_sync_manager.py +7 -7
- souleyez/core/network_utils.py +1 -1
- souleyez/core/parser_handler.py +2 -1
- souleyez/core/pending_chains.py +4 -3
- souleyez/core/templates.py +5 -2
- souleyez/core/tool_chaining.py +101 -70
- souleyez/core/version_utils.py +1 -0
- souleyez/core/vuln_correlation.py +3 -2
- souleyez/core/web_utils.py +2 -1
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +1 -1
- souleyez/detection/mitre_mappings.py +1 -2
- souleyez/detection/validator.py +5 -4
- souleyez/devtools.py +4 -2
- souleyez/docs/README.md +2 -2
- souleyez/engine/background.py +168 -7
- souleyez/engine/base.py +2 -1
- souleyez/engine/loader.py +4 -2
- souleyez/engine/log_sanitizer.py +1 -0
- souleyez/engine/manager.py +3 -1
- souleyez/engine/result_handler.py +50 -67
- souleyez/engine/worker_manager.py +6 -4
- souleyez/export/evidence_bundle.py +1 -0
- souleyez/handlers/base.py +1 -0
- souleyez/handlers/bash_handler.py +1 -0
- souleyez/handlers/bloodhound_handler.py +1 -0
- souleyez/handlers/certipy_handler.py +1 -0
- souleyez/handlers/crackmapexec_handler.py +2 -20
- souleyez/handlers/dnsrecon_handler.py +2 -1
- souleyez/handlers/enum4linux_handler.py +65 -37
- souleyez/handlers/evil_winrm_handler.py +1 -0
- souleyez/handlers/ffuf_handler.py +3 -1
- souleyez/handlers/gobuster_handler.py +7 -6
- souleyez/handlers/gpp_extract_handler.py +1 -0
- souleyez/handlers/hashcat_handler.py +1 -0
- souleyez/handlers/hydra_handler.py +5 -2
- souleyez/handlers/impacket_getuserspns_handler.py +1 -0
- souleyez/handlers/impacket_psexec_handler.py +1 -0
- souleyez/handlers/impacket_secretsdump_handler.py +1 -0
- souleyez/handlers/john_handler.py +1 -0
- souleyez/handlers/katana_handler.py +39 -2
- souleyez/handlers/kerbrute_handler.py +1 -0
- souleyez/handlers/ldapsearch_handler.py +90 -17
- souleyez/handlers/lfi_extract_handler.py +1 -0
- souleyez/handlers/msf_auxiliary_handler.py +1 -0
- souleyez/handlers/msf_exploit_handler.py +1 -0
- souleyez/handlers/nikto_handler.py +2 -1
- souleyez/handlers/nmap_handler.py +2 -1
- souleyez/handlers/nuclei_handler.py +2 -1
- souleyez/handlers/nxc_handler.py +3 -18
- souleyez/handlers/rdp_sec_check_handler.py +1 -0
- souleyez/handlers/registry.py +1 -0
- souleyez/handlers/responder_handler.py +1 -0
- souleyez/handlers/service_explorer_handler.py +2 -1
- souleyez/handlers/smbclient_handler.py +1 -0
- souleyez/handlers/smbmap_handler.py +3 -2
- souleyez/handlers/sqlmap_handler.py +6 -4
- souleyez/handlers/theharvester_handler.py +2 -1
- souleyez/handlers/web_login_test_handler.py +1 -0
- souleyez/handlers/whois_handler.py +3 -2
- souleyez/handlers/wpscan_handler.py +2 -1
- souleyez/history.py +4 -3
- souleyez/importers/msf_importer.py +5 -3
- souleyez/importers/smart_importer.py +6 -4
- souleyez/integrations/siem/__init__.py +6 -6
- souleyez/integrations/siem/base.py +1 -1
- souleyez/integrations/siem/elastic.py +3 -3
- souleyez/integrations/siem/factory.py +1 -2
- souleyez/integrations/siem/googlesecops.py +4 -4
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +1 -1
- souleyez/integrations/siem/sentinel.py +3 -3
- souleyez/integrations/siem/splunk.py +3 -3
- souleyez/integrations/siem/wazuh.py +4 -4
- souleyez/integrations/wazuh/__init__.py +1 -1
- souleyez/integrations/wazuh/client.py +3 -2
- souleyez/integrations/wazuh/config.py +3 -2
- souleyez/integrations/wazuh/host_mapper.py +3 -1
- souleyez/integrations/wazuh/sync.py +4 -1
- souleyez/intelligence/__init__.py +1 -1
- souleyez/intelligence/correlation_analyzer.py +6 -5
- souleyez/intelligence/exploit_knowledge.py +4 -4
- souleyez/intelligence/exploit_suggestions.py +4 -3
- souleyez/intelligence/gap_analyzer.py +5 -3
- souleyez/intelligence/gap_detector.py +2 -0
- souleyez/intelligence/sensitive_tables.py +1 -1
- souleyez/intelligence/service_parser.py +1 -0
- souleyez/intelligence/surface_analyzer.py +9 -9
- souleyez/intelligence/target_parser.py +1 -0
- souleyez/licensing/__init__.py +3 -3
- souleyez/main.py +25 -18
- souleyez/migrations/fix_job_counter.py +2 -1
- souleyez/parsers/bloodhound_parser.py +1 -0
- souleyez/parsers/crackmapexec_parser.py +2 -1
- souleyez/parsers/dalfox_parser.py +3 -2
- souleyez/parsers/dnsrecon_parser.py +2 -1
- souleyez/parsers/enum4linux_parser.py +2 -1
- souleyez/parsers/ffuf_parser.py +2 -1
- souleyez/parsers/gobuster_parser.py +2 -1
- souleyez/parsers/hashcat_parser.py +3 -2
- souleyez/parsers/http_fingerprint_parser.py +2 -1
- souleyez/parsers/hydra_parser.py +2 -1
- souleyez/parsers/impacket_parser.py +2 -1
- souleyez/parsers/john_parser.py +4 -3
- souleyez/parsers/katana_parser.py +134 -2
- souleyez/parsers/msf_parser.py +2 -1
- souleyez/parsers/nikto_parser.py +2 -1
- souleyez/parsers/nmap_parser.py +14 -3
- souleyez/parsers/nuclei_parser.py +3 -2
- souleyez/parsers/responder_parser.py +1 -0
- souleyez/parsers/searchsploit_parser.py +3 -2
- souleyez/parsers/service_explorer_parser.py +1 -0
- souleyez/parsers/smbmap_parser.py +2 -1
- souleyez/parsers/sqlmap_parser.py +36 -2
- souleyez/parsers/theharvester_parser.py +2 -1
- souleyez/parsers/whois_parser.py +2 -1
- souleyez/parsers/wpscan_parser.py +3 -2
- souleyez/plugins/afp.py +3 -1
- souleyez/plugins/afp_brute.py +3 -1
- souleyez/plugins/ard.py +3 -1
- souleyez/plugins/bloodhound.py +3 -2
- souleyez/plugins/certipy.py +1 -0
- souleyez/plugins/crackmapexec.py +11 -7
- souleyez/plugins/dalfox.py +5 -2
- souleyez/plugins/dns_hijack.py +3 -1
- souleyez/plugins/dnsrecon.py +3 -1
- souleyez/plugins/enum4linux.py +3 -1
- souleyez/plugins/evil_winrm.py +1 -0
- souleyez/plugins/ffuf.py +3 -1
- souleyez/plugins/firmware_extract.py +3 -2
- souleyez/plugins/gobuster.py +6 -3
- souleyez/plugins/gpp_extract.py +1 -0
- souleyez/plugins/hashcat.py +2 -1
- souleyez/plugins/http_fingerprint.py +57 -7
- souleyez/plugins/hydra.py +5 -3
- souleyez/plugins/impacket_common.py +40 -0
- souleyez/plugins/impacket_getnpusers.py +19 -2
- souleyez/plugins/impacket_getuserspns.py +158 -0
- souleyez/plugins/impacket_psexec.py +19 -2
- souleyez/plugins/impacket_secretsdump.py +19 -2
- souleyez/plugins/impacket_smbclient.py +19 -2
- souleyez/plugins/john.py +2 -1
- souleyez/plugins/katana.py +48 -6
- souleyez/plugins/kerbrute.py +1 -0
- souleyez/plugins/lfi_extract.py +1 -0
- souleyez/plugins/macos_ssh.py +3 -1
- souleyez/plugins/mdns.py +3 -1
- souleyez/plugins/msf_auxiliary.py +3 -2
- souleyez/plugins/msf_exploit.py +6 -5
- souleyez/plugins/nikto.py +5 -2
- souleyez/plugins/nmap.py +6 -4
- souleyez/plugins/nuclei.py +3 -1
- souleyez/plugins/nxc.py +1 -0
- souleyez/plugins/plugin_base.py +3 -2
- souleyez/plugins/plugin_template.py +3 -2
- souleyez/plugins/rdp_sec_check.py +1 -0
- souleyez/plugins/responder.py +2 -1
- souleyez/plugins/router_http_brute.py +3 -1
- souleyez/plugins/router_ssh_brute.py +3 -1
- souleyez/plugins/router_telnet_brute.py +3 -1
- souleyez/plugins/routersploit.py +5 -3
- souleyez/plugins/routersploit_exploit.py +5 -3
- souleyez/plugins/searchsploit.py +1 -0
- souleyez/plugins/service_explorer.py +2 -1
- souleyez/plugins/smbmap.py +3 -1
- souleyez/plugins/smbpasswd.py +1 -0
- souleyez/plugins/sqlmap.py +3 -1
- souleyez/plugins/theharvester.py +3 -1
- souleyez/plugins/tr069.py +3 -1
- souleyez/plugins/upnp.py +3 -1
- souleyez/plugins/upnp_abuse.py +4 -2
- souleyez/plugins/vnc_access.py +4 -2
- souleyez/plugins/vnc_brute.py +3 -1
- souleyez/plugins/web_login_test.py +1 -0
- souleyez/plugins/whois.py +3 -1
- souleyez/plugins/wpscan.py +3 -1
- souleyez/reporting/attack_chain.py +2 -1
- souleyez/reporting/charts.py +1 -0
- souleyez/reporting/compliance_mappings.py +1 -0
- souleyez/reporting/detection_report.py +10 -10
- souleyez/reporting/formatters.py +7 -12
- souleyez/reporting/generator.py +34 -46
- souleyez/reporting/metrics.py +2 -1
- souleyez/scanner.py +6 -3
- souleyez/security/__init__.py +7 -5
- souleyez/security/scope_validator.py +5 -4
- souleyez/security.py +5 -2
- souleyez/storage/credentials.py +14 -19
- souleyez/storage/crypto.py +7 -4
- souleyez/storage/database.py +6 -6
- souleyez/storage/db.py +8 -8
- souleyez/storage/deliverable_evidence.py +2 -1
- souleyez/storage/deliverable_exporter.py +3 -2
- souleyez/storage/deliverable_templates.py +2 -1
- souleyez/storage/deliverables.py +2 -1
- souleyez/storage/engagements.py +6 -4
- souleyez/storage/evidence.py +5 -4
- souleyez/storage/execution_log.py +4 -2
- souleyez/storage/exploit_attempts.py +3 -2
- souleyez/storage/exploits.py +3 -1
- souleyez/storage/findings.py +3 -1
- souleyez/storage/hosts.py +5 -2
- souleyez/storage/migrate_to_engagements.py +14 -24
- souleyez/storage/migrations/_001_add_credential_enhancements.py +12 -21
- souleyez/storage/migrations/_003_add_execution_log.py +8 -13
- souleyez/storage/migrations/_005_screenshots.py +2 -4
- souleyez/storage/migrations/_006_deliverables.py +2 -4
- souleyez/storage/migrations/_007_deliverable_templates.py +4 -8
- souleyez/storage/migrations/_008_add_nuclei_table.py +2 -4
- souleyez/storage/migrations/_010_evidence_linking.py +6 -12
- souleyez/storage/migrations/_012_team_collaboration.py +12 -24
- souleyez/storage/migrations/_013_add_host_tags.py +2 -4
- souleyez/storage/migrations/_014_exploit_attempts.py +10 -20
- souleyez/storage/migrations/_015_add_mac_os_fields.py +4 -8
- souleyez/storage/migrations/_016_add_domain_field.py +2 -4
- souleyez/storage/migrations/_017_msf_sessions.py +8 -16
- souleyez/storage/migrations/_018_add_osint_target.py +4 -8
- souleyez/storage/migrations/_019_add_engagement_type.py +4 -8
- souleyez/storage/migrations/_020_add_rbac.py +9 -17
- souleyez/storage/migrations/_021_wazuh_integration.py +4 -8
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +2 -4
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +4 -8
- souleyez/storage/migrations/_026_add_engagement_scope.py +4 -8
- souleyez/storage/migrations/_027_multi_siem_persistence.py +8 -16
- souleyez/storage/migrations/__init__.py +1 -4
- souleyez/storage/migrations/migration_manager.py +6 -9
- souleyez/storage/msf_sessions.py +1 -1
- souleyez/storage/osint.py +3 -1
- souleyez/storage/recommendation_engine.py +3 -2
- souleyez/storage/screenshots.py +2 -1
- souleyez/storage/smb_shares.py +3 -1
- souleyez/storage/sqlmap_data.py +6 -4
- souleyez/storage/team_collaboration.py +3 -2
- souleyez/storage/timeline_tracker.py +2 -1
- souleyez/storage/wazuh_vulns.py +3 -1
- souleyez/storage/web_paths.py +3 -1
- souleyez/testing/credential_tester.py +2 -0
- souleyez/ui/__init__.py +2 -1
- souleyez/ui/ai_quotes.py +1 -1
- souleyez/ui/attack_surface.py +50 -28
- souleyez/ui/chain_rules_view.py +6 -3
- souleyez/ui/correlation_view.py +3 -2
- souleyez/ui/dashboard.py +85 -139
- souleyez/ui/deliverables_view.py +1 -1
- souleyez/ui/design_system.py +5 -3
- souleyez/ui/errors.py +3 -1
- souleyez/ui/evidence_linking_view.py +2 -1
- souleyez/ui/evidence_vault.py +11 -6
- souleyez/ui/exploit_suggestions_view.py +11 -7
- souleyez/ui/export_view.py +3 -1
- souleyez/ui/gap_analysis_view.py +6 -3
- souleyez/ui/help_system.py +4 -1
- souleyez/ui/intelligence_view.py +7 -3
- souleyez/ui/interactive.py +1280 -558
- souleyez/ui/interactive_selector.py +3 -2
- souleyez/ui/log_formatter.py +1 -0
- souleyez/ui/menu_components.py +3 -1
- souleyez/ui/msf_auxiliary_menu.py +4 -1
- souleyez/ui/pending_chains_view.py +15 -12
- souleyez/ui/progress_indicators.py +5 -2
- souleyez/ui/recommendations_view.py +4 -2
- souleyez/ui/rule_builder.py +4 -1
- souleyez/ui/setup_wizard.py +10 -8
- souleyez/ui/shortcuts.py +1 -1
- souleyez/ui/splunk_gap_analysis_view.py +7 -4
- souleyez/ui/splunk_vulns_view.py +4 -1
- souleyez/ui/team_dashboard.py +7 -5
- souleyez/ui/template_selector.py +2 -1
- souleyez/ui/terminal.py +3 -2
- souleyez/ui/timeline_view.py +2 -1
- souleyez/ui/tool_setup.py +92 -31
- souleyez/ui/tutorial.py +7 -4
- souleyez/ui/tutorial_state.py +3 -2
- souleyez/ui/wazuh_vulns_view.py +5 -2
- souleyez/ui/wordlist_browser.py +4 -3
- souleyez/ui.py +13 -7
- souleyez/utils/tool_checker.py +61 -12
- souleyez/utils.py +4 -4
- souleyez/wordlists.py +1 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/METADATA +1 -1
- souleyez-3.0.7.dist-info/RECORD +445 -0
- souleyez-3.0.0.dist-info/RECORD +0 -443
- {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/WHEEL +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/entry_points.txt +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/licenses/LICENSE +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/top_level.txt +0 -0
souleyez/ui/interactive.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import platform
|
|
9
9
|
import shlex
|
|
10
10
|
import shutil
|
|
11
|
+
import subprocess
|
|
11
12
|
import sys
|
|
12
13
|
import tempfile
|
|
13
14
|
import time
|
|
@@ -1947,78 +1948,132 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
|
|
|
1947
1948
|
|
|
1948
1949
|
elif choice == 3:
|
|
1949
1950
|
# Select from credentials in current engagement
|
|
1950
|
-
from souleyez.storage.credentials import
|
|
1951
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
1951
1952
|
from souleyez.storage.engagements import EngagementManager
|
|
1952
1953
|
|
|
1953
1954
|
em = EngagementManager()
|
|
1954
1955
|
current_eng = em.get_current()
|
|
1955
1956
|
|
|
1956
1957
|
if not current_eng:
|
|
1958
|
+
click.echo()
|
|
1957
1959
|
click.echo(click.style("No engagement selected!", fg="red"))
|
|
1958
1960
|
click.echo("Use 'souleyez engagement use <name>' first")
|
|
1961
|
+
click.echo()
|
|
1962
|
+
click.pause()
|
|
1959
1963
|
return None
|
|
1960
1964
|
|
|
1961
|
-
cm =
|
|
1965
|
+
cm = CredentialsManager()
|
|
1962
1966
|
creds = cm.list_credentials(current_eng["id"])
|
|
1963
1967
|
|
|
1964
|
-
# Filter for
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
+
# Filter for actual crackable hashes
|
|
1969
|
+
# Hash is stored in 'password' field, must look like a hash (not plaintext)
|
|
1970
|
+
def is_crackable_hash(cred):
|
|
1971
|
+
cred_type = cred.get("credential_type", "").lower()
|
|
1972
|
+
# Hash value is in 'password' field
|
|
1973
|
+
hash_value = cred.get("password", "") or ""
|
|
1974
|
+
|
|
1975
|
+
if not hash_value or hash_value == "None":
|
|
1976
|
+
return False
|
|
1977
|
+
|
|
1978
|
+
# Must have hash-related type
|
|
1979
|
+
if "hash" not in cred_type:
|
|
1980
|
+
return False
|
|
1981
|
+
|
|
1982
|
+
# Check if value looks like a hash (not plaintext)
|
|
1983
|
+
# Hex hash (32+ chars for MD5, mostly hex characters)
|
|
1984
|
+
hex_chars = sum(c in "0123456789abcdefABCDEF" for c in hash_value)
|
|
1985
|
+
if len(hash_value) >= 32 and hex_chars / len(hash_value) > 0.9:
|
|
1986
|
+
return True
|
|
1987
|
+
# Hash format like $1$salt$hash or $2a$... (bcrypt, etc.)
|
|
1988
|
+
if hash_value.startswith("$") and "$" in hash_value[1:]:
|
|
1989
|
+
return True
|
|
1990
|
+
# Kerberos hashes start with $krb5
|
|
1991
|
+
if hash_value.startswith("$krb5"):
|
|
1992
|
+
return True
|
|
1993
|
+
# NTLM:LM format (hash:hash)
|
|
1994
|
+
if ":" in hash_value and len(hash_value) >= 65:
|
|
1995
|
+
return True
|
|
1996
|
+
|
|
1997
|
+
# If it looks like plaintext (has spaces, common words), reject
|
|
1998
|
+
if " " in hash_value or len(hash_value) < 20:
|
|
1999
|
+
return False
|
|
2000
|
+
|
|
2001
|
+
return False
|
|
2002
|
+
|
|
2003
|
+
hashes = [c for c in creds if is_crackable_hash(c)]
|
|
1968
2004
|
|
|
1969
2005
|
if not hashes:
|
|
2006
|
+
click.echo()
|
|
1970
2007
|
click.echo(
|
|
1971
|
-
click.style(
|
|
2008
|
+
click.style(
|
|
2009
|
+
"No crackable hashes found in current engagement!", fg="yellow"
|
|
2010
|
+
)
|
|
1972
2011
|
)
|
|
2012
|
+
click.echo()
|
|
1973
2013
|
click.echo("Try discovering hashes first with:")
|
|
1974
2014
|
click.echo(" • secretsdump (extract SAM/NTDS)")
|
|
1975
2015
|
click.echo(" • GetNPUsers (AS-REP roasting)")
|
|
2016
|
+
click.echo(" • SQLMap (database password hashes)")
|
|
2017
|
+
click.echo()
|
|
2018
|
+
click.pause()
|
|
1976
2019
|
return None
|
|
1977
2020
|
|
|
1978
|
-
#
|
|
1979
|
-
|
|
1980
|
-
click.echo(
|
|
1981
|
-
click.style(
|
|
1982
|
-
f"Found {len(hashes)} hash(es) in current engagement:",
|
|
1983
|
-
fg="green",
|
|
1984
|
-
bold=True,
|
|
1985
|
-
)
|
|
1986
|
-
)
|
|
1987
|
-
click.echo()
|
|
2021
|
+
# Use interactive selector for hash list
|
|
2022
|
+
from souleyez.ui.interactive_selector import interactive_select
|
|
1988
2023
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2024
|
+
# Prepare hash items with display-friendly fields
|
|
2025
|
+
hash_items = []
|
|
2026
|
+
for idx, cred in enumerate(hashes):
|
|
2027
|
+
hash_value = cred.get("password", "") or ""
|
|
2028
|
+
# Create preview of hash
|
|
2029
|
+
if len(hash_value) > 40:
|
|
2030
|
+
hash_preview = hash_value[:20] + "..." + hash_value[-10:]
|
|
2031
|
+
else:
|
|
2032
|
+
hash_preview = hash_value
|
|
1994
2033
|
|
|
1995
|
-
|
|
1996
|
-
|
|
2034
|
+
hash_items.append(
|
|
2035
|
+
{
|
|
2036
|
+
"idx": idx,
|
|
2037
|
+
"username": cred.get("username", "unknown")[:25],
|
|
2038
|
+
"hash_type": cred.get("credential_type", "unknown"),
|
|
2039
|
+
"hash_preview": hash_preview,
|
|
2040
|
+
"hash_value": hash_value, # Full hash for use
|
|
2041
|
+
"original": cred, # Keep original for later
|
|
2042
|
+
}
|
|
2043
|
+
)
|
|
1997
2044
|
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2045
|
+
columns = [
|
|
2046
|
+
{"name": "#", "width": 5, "key": "idx"},
|
|
2047
|
+
{"name": "Username", "width": 25, "key": "username"},
|
|
2048
|
+
{"name": "Type", "width": 20, "key": "hash_type"},
|
|
2049
|
+
{"name": "Hash Preview", "key": "hash_preview"},
|
|
2050
|
+
]
|
|
2051
|
+
|
|
2052
|
+
selected_ids = set()
|
|
2053
|
+
interactive_select(
|
|
2054
|
+
items=hash_items,
|
|
2055
|
+
columns=columns,
|
|
2056
|
+
selected_ids=selected_ids,
|
|
2057
|
+
get_id=lambda h: h["idx"],
|
|
2058
|
+
title="SELECT HASH TO CRACK",
|
|
2005
2059
|
)
|
|
2006
2060
|
|
|
2007
|
-
if
|
|
2061
|
+
if not selected_ids:
|
|
2008
2062
|
click.echo(click.style("Cancelled", fg="yellow"))
|
|
2009
2063
|
return None
|
|
2010
2064
|
|
|
2011
|
-
#
|
|
2012
|
-
|
|
2013
|
-
|
|
2065
|
+
# Get first selected hash
|
|
2066
|
+
selected_idx = list(selected_ids)[0]
|
|
2067
|
+
selected_item = hash_items[selected_idx]
|
|
2068
|
+
target = selected_item["hash_value"]
|
|
2014
2069
|
|
|
2015
|
-
if not target:
|
|
2070
|
+
if not target or target == "None":
|
|
2016
2071
|
click.echo(click.style("Hash is empty!", fg="red"))
|
|
2017
2072
|
return None
|
|
2018
2073
|
|
|
2019
2074
|
click.echo(
|
|
2020
2075
|
click.style(
|
|
2021
|
-
f"\n✓ Selected: {
|
|
2076
|
+
f"\n✓ Selected: {selected_item['username']} ({selected_item['hash_type']})",
|
|
2022
2077
|
fg="green",
|
|
2023
2078
|
)
|
|
2024
2079
|
)
|
|
@@ -2044,6 +2099,121 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
|
|
|
2044
2099
|
em = EngagementManager()
|
|
2045
2100
|
current_eng = em.get_current()
|
|
2046
2101
|
|
|
2102
|
+
# Valid TLDs for filtering (common ones)
|
|
2103
|
+
valid_tlds = {
|
|
2104
|
+
"com",
|
|
2105
|
+
"org",
|
|
2106
|
+
"net",
|
|
2107
|
+
"edu",
|
|
2108
|
+
"gov",
|
|
2109
|
+
"mil",
|
|
2110
|
+
"int",
|
|
2111
|
+
"io",
|
|
2112
|
+
"co",
|
|
2113
|
+
"us",
|
|
2114
|
+
"uk",
|
|
2115
|
+
"ca",
|
|
2116
|
+
"au",
|
|
2117
|
+
"de",
|
|
2118
|
+
"fr",
|
|
2119
|
+
"jp",
|
|
2120
|
+
"cn",
|
|
2121
|
+
"ru",
|
|
2122
|
+
"br",
|
|
2123
|
+
"in",
|
|
2124
|
+
"mx",
|
|
2125
|
+
"es",
|
|
2126
|
+
"it",
|
|
2127
|
+
"nl",
|
|
2128
|
+
"se",
|
|
2129
|
+
"no",
|
|
2130
|
+
"fi",
|
|
2131
|
+
"dk",
|
|
2132
|
+
"pl",
|
|
2133
|
+
"cz",
|
|
2134
|
+
"at",
|
|
2135
|
+
"ch",
|
|
2136
|
+
"be",
|
|
2137
|
+
"ie",
|
|
2138
|
+
"nz",
|
|
2139
|
+
"za",
|
|
2140
|
+
"sg",
|
|
2141
|
+
"hk",
|
|
2142
|
+
"kr",
|
|
2143
|
+
"tw",
|
|
2144
|
+
"th",
|
|
2145
|
+
"my",
|
|
2146
|
+
"ph",
|
|
2147
|
+
"id",
|
|
2148
|
+
"vn",
|
|
2149
|
+
"ar",
|
|
2150
|
+
"cl",
|
|
2151
|
+
"co",
|
|
2152
|
+
"pe",
|
|
2153
|
+
"ve",
|
|
2154
|
+
"pt",
|
|
2155
|
+
"gr",
|
|
2156
|
+
"tr",
|
|
2157
|
+
"il",
|
|
2158
|
+
"ae",
|
|
2159
|
+
"sa",
|
|
2160
|
+
"eg",
|
|
2161
|
+
"ng",
|
|
2162
|
+
"ke",
|
|
2163
|
+
"info",
|
|
2164
|
+
"biz",
|
|
2165
|
+
"name",
|
|
2166
|
+
"pro",
|
|
2167
|
+
"mobi",
|
|
2168
|
+
"tv",
|
|
2169
|
+
"cc",
|
|
2170
|
+
"ws",
|
|
2171
|
+
"me",
|
|
2172
|
+
"ly",
|
|
2173
|
+
"to",
|
|
2174
|
+
"fm",
|
|
2175
|
+
"am",
|
|
2176
|
+
"gg",
|
|
2177
|
+
"xyz",
|
|
2178
|
+
"app",
|
|
2179
|
+
"dev",
|
|
2180
|
+
"cloud",
|
|
2181
|
+
"tech",
|
|
2182
|
+
"online",
|
|
2183
|
+
"site",
|
|
2184
|
+
"store",
|
|
2185
|
+
"shop",
|
|
2186
|
+
"blog",
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
# Local/invalid TLDs to exclude
|
|
2190
|
+
invalid_tlds = {"lan", "local", "home", "internal", "localdomain", "localhost"}
|
|
2191
|
+
|
|
2192
|
+
def is_valid_domain(domain):
|
|
2193
|
+
"""Check if domain is valid for OSINT tools."""
|
|
2194
|
+
if not domain or not isinstance(domain, str):
|
|
2195
|
+
return False
|
|
2196
|
+
# Must have at least one dot
|
|
2197
|
+
if "." not in domain:
|
|
2198
|
+
return False
|
|
2199
|
+
parts = domain.split(".")
|
|
2200
|
+
if len(parts) < 2:
|
|
2201
|
+
return False
|
|
2202
|
+
# Check TLD
|
|
2203
|
+
tld = parts[-1].lower()
|
|
2204
|
+
if tld in invalid_tlds:
|
|
2205
|
+
return False
|
|
2206
|
+
# Reject if TLD is all digits (partial IP)
|
|
2207
|
+
if tld.isdigit():
|
|
2208
|
+
return False
|
|
2209
|
+
# Reject if domain part is all digits (partial IP)
|
|
2210
|
+
if parts[-2].isdigit():
|
|
2211
|
+
return False
|
|
2212
|
+
# Must have valid TLD or at least 2 chars
|
|
2213
|
+
if tld not in valid_tlds and len(tld) < 2:
|
|
2214
|
+
return False
|
|
2215
|
+
return True
|
|
2216
|
+
|
|
2047
2217
|
discovered_domains = []
|
|
2048
2218
|
if current_eng:
|
|
2049
2219
|
hm = HostManager()
|
|
@@ -2054,11 +2224,10 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
|
|
|
2054
2224
|
for host in all_hosts:
|
|
2055
2225
|
hostname = host.get("hostname")
|
|
2056
2226
|
if hostname and "." in hostname:
|
|
2057
|
-
# Extract domain (everything after first subdomain)
|
|
2058
2227
|
parts = hostname.split(".")
|
|
2059
2228
|
if len(parts) >= 2:
|
|
2060
|
-
domain = ".".join(parts[-2:])
|
|
2061
|
-
if domain not in discovered_domains:
|
|
2229
|
+
domain = ".".join(parts[-2:])
|
|
2230
|
+
if is_valid_domain(domain) and domain not in discovered_domains:
|
|
2062
2231
|
discovered_domains.append(domain)
|
|
2063
2232
|
|
|
2064
2233
|
# Also check OSINT data for hosts/domains (from theHarvester, etc.)
|
|
@@ -2067,77 +2236,109 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
|
|
|
2067
2236
|
for osint_entry in osint_hosts:
|
|
2068
2237
|
hostname = osint_entry.get("value", "")
|
|
2069
2238
|
if hostname and "." in hostname:
|
|
2070
|
-
# Extract domain from hostname
|
|
2071
2239
|
parts = hostname.split(".")
|
|
2072
2240
|
if len(parts) >= 2:
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2241
|
+
domain = ".".join(parts[-2:])
|
|
2242
|
+
if (
|
|
2243
|
+
is_valid_domain(domain)
|
|
2244
|
+
and domain not in discovered_domains
|
|
2245
|
+
):
|
|
2078
2246
|
discovered_domains.append(domain)
|
|
2079
2247
|
except Exception:
|
|
2080
|
-
pass
|
|
2248
|
+
pass
|
|
2081
2249
|
|
|
2082
|
-
|
|
2250
|
+
# Show menu-based selection like Nmap
|
|
2251
|
+
domain_count = len(discovered_domains)
|
|
2252
|
+
if domain_count > 0:
|
|
2083
2253
|
click.echo(
|
|
2084
|
-
click.style(
|
|
2085
|
-
f"🎯 Discovered {len(discovered_domains)} domain(s) in current engagement:",
|
|
2086
|
-
fg="green",
|
|
2087
|
-
bold=True,
|
|
2088
|
-
)
|
|
2254
|
+
f"Found {click.style(str(domain_count), fg='green', bold=True)} valid domain(s) in current engagement."
|
|
2089
2255
|
)
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
click.echo(f" {idx}. {domain}")
|
|
2094
|
-
|
|
2095
|
-
if len(discovered_domains) > 10:
|
|
2096
|
-
click.echo(f" ... and {len(discovered_domains) - 10} more")
|
|
2256
|
+
else:
|
|
2257
|
+
click.echo(click.style("No valid domains discovered yet.", fg="yellow"))
|
|
2258
|
+
click.echo()
|
|
2097
2259
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
click.
|
|
2102
|
-
|
|
2103
|
-
click.style(" 💡 TIP: ", fg="yellow", bold=True)
|
|
2104
|
-
+ "Type "
|
|
2105
|
-
+ click.style("?", fg="cyan", bold=True)
|
|
2106
|
-
+ " for help guide"
|
|
2260
|
+
click.echo(
|
|
2261
|
+
" 1. Select from discovered domains"
|
|
2262
|
+
if domain_count > 0
|
|
2263
|
+
else click.style(
|
|
2264
|
+
" 1. Select from discovered domains (none found)", dim=True
|
|
2107
2265
|
)
|
|
2108
|
-
|
|
2266
|
+
)
|
|
2267
|
+
click.echo(" 2. Enter custom domain")
|
|
2268
|
+
click.echo(" [q] Back")
|
|
2269
|
+
click.echo()
|
|
2270
|
+
click.echo(
|
|
2271
|
+
click.style(" 💡 TIP: ", fg="yellow", bold=True)
|
|
2272
|
+
+ "Type "
|
|
2273
|
+
+ click.style("?", fg="cyan", bold=True)
|
|
2274
|
+
+ " for help guide"
|
|
2275
|
+
)
|
|
2276
|
+
click.echo()
|
|
2109
2277
|
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2278
|
+
choice = click.prompt(
|
|
2279
|
+
click.style("Select option", fg="green", bold=True),
|
|
2280
|
+
type=str,
|
|
2281
|
+
default="2" if domain_count == 0 else "1",
|
|
2282
|
+
show_default=False,
|
|
2283
|
+
).strip()
|
|
2116
2284
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2285
|
+
# Handle help command
|
|
2286
|
+
if choice == "?":
|
|
2287
|
+
show_tool_help(tool_name, help_info)
|
|
2288
|
+
DesignSystem.clear_screen()
|
|
2289
|
+
return {"action": "retry"}
|
|
2122
2290
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2291
|
+
# Handle back/quit
|
|
2292
|
+
if choice.lower() == "q":
|
|
2293
|
+
return {"action": "back"}
|
|
2126
2294
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2295
|
+
target = None
|
|
2296
|
+
if choice == "1" and domain_count > 0:
|
|
2297
|
+
# Use interactive selector for domain list
|
|
2298
|
+
from souleyez.ui.interactive_selector import interactive_select
|
|
2299
|
+
|
|
2300
|
+
# Prepare domain items
|
|
2301
|
+
domain_items = []
|
|
2302
|
+
for idx, domain in enumerate(discovered_domains):
|
|
2303
|
+
# Extract TLD for display
|
|
2304
|
+
parts = domain.split(".")
|
|
2305
|
+
tld = parts[-1].upper() if parts else ""
|
|
2306
|
+
domain_items.append(
|
|
2307
|
+
{
|
|
2308
|
+
"idx": idx,
|
|
2309
|
+
"domain": domain,
|
|
2310
|
+
"tld": f".{tld}",
|
|
2311
|
+
}
|
|
2137
2312
|
)
|
|
2313
|
+
|
|
2314
|
+
columns = [
|
|
2315
|
+
{"name": "#", "width": 5, "key": "idx"},
|
|
2316
|
+
{"name": "Domain", "key": "domain"},
|
|
2317
|
+
{"name": "TLD", "width": 8, "key": "tld"},
|
|
2318
|
+
]
|
|
2319
|
+
|
|
2320
|
+
selected_ids = set()
|
|
2321
|
+
interactive_select(
|
|
2322
|
+
items=domain_items,
|
|
2323
|
+
columns=columns,
|
|
2324
|
+
selected_ids=selected_ids,
|
|
2325
|
+
get_id=lambda d: d["idx"],
|
|
2326
|
+
title="SELECT TARGET DOMAIN",
|
|
2138
2327
|
)
|
|
2328
|
+
|
|
2329
|
+
if not selected_ids:
|
|
2330
|
+
return {"action": "back"}
|
|
2331
|
+
|
|
2332
|
+
# Get first selected domain
|
|
2333
|
+
selected_idx = list(selected_ids)[0]
|
|
2334
|
+
target = domain_items[selected_idx]["domain"]
|
|
2335
|
+
click.echo(click.style(f"\n✓ Selected: {target}", fg="green"))
|
|
2139
2336
|
click.echo()
|
|
2140
|
-
|
|
2337
|
+
elif choice == "2" or (choice == "1" and domain_count == 0):
|
|
2338
|
+
target = None # Will prompt for manual entry below
|
|
2339
|
+
else:
|
|
2340
|
+
click.echo(click.style("Invalid option!", fg="red"))
|
|
2341
|
+
return None
|
|
2141
2342
|
|
|
2142
2343
|
# Manual entry if not selected from list
|
|
2143
2344
|
if not target:
|
|
@@ -6144,10 +6345,12 @@ def view_jobs_menu():
|
|
|
6144
6345
|
"hashes"
|
|
6145
6346
|
):
|
|
6146
6347
|
critical_indicator = "[bold red]💀[/bold red]"
|
|
6147
|
-
# Shell access (evil_winrm, psexec success)
|
|
6148
|
-
elif tool in [
|
|
6149
|
-
"
|
|
6150
|
-
|
|
6348
|
+
# Shell access (evil_winrm, psexec, msf_exploit success)
|
|
6349
|
+
elif tool in [
|
|
6350
|
+
"evil_winrm",
|
|
6351
|
+
"impacket-psexec",
|
|
6352
|
+
"msf_exploit",
|
|
6353
|
+
] and parse_result.get("success"):
|
|
6151
6354
|
critical_indicator = "[bold green]🐚[/bold green]"
|
|
6152
6355
|
# Credentials found (hydra, hashcat cracked, smbpasswd, etc.)
|
|
6153
6356
|
else:
|
|
@@ -6229,8 +6432,8 @@ def view_jobs_menu():
|
|
|
6229
6432
|
click.echo()
|
|
6230
6433
|
|
|
6231
6434
|
# Build menu and legend side by side using Rich
|
|
6232
|
-
from rich.table import Table
|
|
6233
6435
|
from rich import box
|
|
6436
|
+
from rich.table import Table
|
|
6234
6437
|
|
|
6235
6438
|
console = Console()
|
|
6236
6439
|
|
|
@@ -11850,7 +12053,10 @@ def view_job_detail(job_id: int):
|
|
|
11850
12053
|
if web_login_test_handler:
|
|
11851
12054
|
try:
|
|
11852
12055
|
current_status = job.get("status", "")
|
|
11853
|
-
if
|
|
12056
|
+
if (
|
|
12057
|
+
current_status == "done"
|
|
12058
|
+
and web_login_test_handler.has_done_handler
|
|
12059
|
+
):
|
|
11854
12060
|
web_login_test_handler.display_done(
|
|
11855
12061
|
job, log_path, show_all_paths, show_passwords
|
|
11856
12062
|
)
|
|
@@ -14781,7 +14987,11 @@ def view_job_detail(job_id: int):
|
|
|
14781
14987
|
creds = parse_result.get("credentials", [])
|
|
14782
14988
|
# Check for SSH or telnet credentials
|
|
14783
14989
|
for cred in creds if isinstance(creds, list) else []:
|
|
14784
|
-
service =
|
|
14990
|
+
service = (
|
|
14991
|
+
cred.get("service", "").lower()
|
|
14992
|
+
if isinstance(cred, dict)
|
|
14993
|
+
else ""
|
|
14994
|
+
)
|
|
14785
14995
|
if service in ["ssh", "telnet"]:
|
|
14786
14996
|
can_spawn_shell = True
|
|
14787
14997
|
if service == "ssh":
|
|
@@ -14801,7 +15011,10 @@ def view_job_detail(job_id: int):
|
|
|
14801
15011
|
try:
|
|
14802
15012
|
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
14803
15013
|
log_content = f.read()
|
|
14804
|
-
if
|
|
15014
|
+
if (
|
|
15015
|
+
"session" in log_content.lower()
|
|
15016
|
+
and "opened" in log_content.lower()
|
|
15017
|
+
):
|
|
14805
15018
|
can_spawn_shell = True
|
|
14806
15019
|
# Check if SSH or telnet
|
|
14807
15020
|
if "ssh" in log_content.lower():
|
|
@@ -15053,7 +15266,9 @@ def view_job_detail(job_id: int):
|
|
|
15053
15266
|
# Use first credential
|
|
15054
15267
|
first_cred = creds[0]
|
|
15055
15268
|
if isinstance(first_cred, dict):
|
|
15056
|
-
username = first_cred.get("username") or first_cred.get(
|
|
15269
|
+
username = first_cred.get("username") or first_cred.get(
|
|
15270
|
+
"login"
|
|
15271
|
+
)
|
|
15057
15272
|
password = first_cred.get("password")
|
|
15058
15273
|
|
|
15059
15274
|
if not username or (not password and not nt_hash):
|
|
@@ -15120,7 +15335,9 @@ def view_job_detail(job_id: int):
|
|
|
15120
15335
|
click.echo(f" User: {username}")
|
|
15121
15336
|
click.echo(f" Pass: {password}")
|
|
15122
15337
|
click.echo()
|
|
15123
|
-
click.echo(
|
|
15338
|
+
click.echo(
|
|
15339
|
+
" Launching telnet... Enter password when prompted."
|
|
15340
|
+
)
|
|
15124
15341
|
click.echo()
|
|
15125
15342
|
shell_cmd = f"telnet {target}"
|
|
15126
15343
|
else:
|
|
@@ -15155,7 +15372,9 @@ def view_job_detail(job_id: int):
|
|
|
15155
15372
|
|
|
15156
15373
|
elif tool_name == "hydra":
|
|
15157
15374
|
# Hydra found valid credentials - determine service type
|
|
15158
|
-
service_type =
|
|
15375
|
+
service_type = (
|
|
15376
|
+
parse_result.get("service", "").lower() if parse_result else ""
|
|
15377
|
+
)
|
|
15159
15378
|
|
|
15160
15379
|
if service_type == "ssh" or is_ssh_shell:
|
|
15161
15380
|
# SSH - use sshpass
|
|
@@ -15171,7 +15390,9 @@ def view_job_detail(job_id: int):
|
|
|
15171
15390
|
click.echo(f" User: {username}")
|
|
15172
15391
|
click.echo(f" Pass: {password}")
|
|
15173
15392
|
click.echo()
|
|
15174
|
-
click.echo(
|
|
15393
|
+
click.echo(
|
|
15394
|
+
" Launching telnet... Enter password when prompted."
|
|
15395
|
+
)
|
|
15175
15396
|
click.echo()
|
|
15176
15397
|
shell_cmd = f"telnet {target}"
|
|
15177
15398
|
elif service_type == "ftp":
|
|
@@ -15253,9 +15474,7 @@ def view_job_detail(job_id: int):
|
|
|
15253
15474
|
if shell_choice == "1":
|
|
15254
15475
|
# evil-winrm
|
|
15255
15476
|
if nt_hash:
|
|
15256
|
-
shell_cmd =
|
|
15257
|
-
f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
|
|
15258
|
-
)
|
|
15477
|
+
shell_cmd = f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
|
|
15259
15478
|
else:
|
|
15260
15479
|
shell_cmd = f"evil-winrm -i {target} -u '{username}' -p '{password}'"
|
|
15261
15480
|
elif shell_choice == "2":
|
|
@@ -15263,9 +15482,7 @@ def view_job_detail(job_id: int):
|
|
|
15263
15482
|
if nt_hash:
|
|
15264
15483
|
shell_cmd = f"impacket-psexec '{cred_prefix}@{target}' -hashes ':{nt_hash}'"
|
|
15265
15484
|
else:
|
|
15266
|
-
shell_cmd =
|
|
15267
|
-
f"impacket-psexec '{cred_prefix}:{password}@{target}'"
|
|
15268
|
-
)
|
|
15485
|
+
shell_cmd = f"impacket-psexec '{cred_prefix}:{password}@{target}'"
|
|
15269
15486
|
elif shell_choice == "3":
|
|
15270
15487
|
# SSH with sshpass
|
|
15271
15488
|
shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
|
|
@@ -17043,6 +17260,10 @@ def _license_management_menu():
|
|
|
17043
17260
|
_bypass_validation=True, # Already validated
|
|
17044
17261
|
)
|
|
17045
17262
|
if tier_success:
|
|
17263
|
+
# Refresh in-memory user cache so PRO is active immediately
|
|
17264
|
+
from souleyez.auth import get_session_manager
|
|
17265
|
+
|
|
17266
|
+
get_session_manager().set_current_user(None)
|
|
17046
17267
|
click.echo(
|
|
17047
17268
|
click.style(
|
|
17048
17269
|
f" ✓ User '{user.username}' upgraded to PRO tier",
|
|
@@ -17052,8 +17273,8 @@ def _license_management_menu():
|
|
|
17052
17273
|
click.echo()
|
|
17053
17274
|
click.echo(
|
|
17054
17275
|
click.style(
|
|
17055
|
-
"
|
|
17056
|
-
fg="
|
|
17276
|
+
" 💎 PRO features now unlocked!",
|
|
17277
|
+
fg="green",
|
|
17057
17278
|
)
|
|
17058
17279
|
)
|
|
17059
17280
|
else:
|
|
@@ -17188,13 +17409,18 @@ def _license_management_menu():
|
|
|
17188
17409
|
|
|
17189
17410
|
# Reset ALL users with PRO tier to FREE
|
|
17190
17411
|
try:
|
|
17191
|
-
from souleyez.auth import
|
|
17412
|
+
from souleyez.auth import (
|
|
17413
|
+
UserManager,
|
|
17414
|
+
get_session_manager,
|
|
17415
|
+
)
|
|
17192
17416
|
from souleyez.storage.database import get_db
|
|
17193
17417
|
|
|
17194
17418
|
user_mgr = UserManager(get_db().db_path)
|
|
17195
17419
|
count, _ = user_mgr.reset_all_pro_tiers()
|
|
17196
17420
|
if count > 0:
|
|
17197
17421
|
click.echo(f" Reset {count} user(s) to FREE tier.")
|
|
17422
|
+
# Refresh in-memory user cache
|
|
17423
|
+
get_session_manager().set_current_user(None)
|
|
17198
17424
|
except Exception:
|
|
17199
17425
|
pass
|
|
17200
17426
|
else:
|
|
@@ -23629,61 +23855,6 @@ def manage_credentials_menu():
|
|
|
23629
23855
|
break
|
|
23630
23856
|
|
|
23631
23857
|
|
|
23632
|
-
def view_additional_data_menu():
|
|
23633
|
-
"""Additional data viewing menu for OSINT and Web Paths."""
|
|
23634
|
-
em = EngagementManager()
|
|
23635
|
-
current_ws = em.get_current()
|
|
23636
|
-
|
|
23637
|
-
if not current_ws:
|
|
23638
|
-
click.echo(click.style("No engagement selected!", fg="red"))
|
|
23639
|
-
click.pause()
|
|
23640
|
-
return
|
|
23641
|
-
|
|
23642
|
-
engagement_id = current_ws["id"]
|
|
23643
|
-
|
|
23644
|
-
while True:
|
|
23645
|
-
DesignSystem.clear_screen()
|
|
23646
|
-
click.echo("\n" + "=" * 70)
|
|
23647
|
-
click.echo("ADDITIONAL DATA")
|
|
23648
|
-
click.echo("=" * 70 + "\n")
|
|
23649
|
-
|
|
23650
|
-
click.echo(" 1. Web Paths - View and manage discovered web paths")
|
|
23651
|
-
click.echo(" 2. SMB Shares - View and manage enumerated SMB shares")
|
|
23652
|
-
click.echo(
|
|
23653
|
-
" 3. SQLMap Data - View SQL injection discoveries and databases"
|
|
23654
|
-
)
|
|
23655
|
-
click.echo()
|
|
23656
|
-
click.echo(" [q] ← Back")
|
|
23657
|
-
click.echo()
|
|
23658
|
-
|
|
23659
|
-
try:
|
|
23660
|
-
choice_input = click.prompt(
|
|
23661
|
-
"Select data type", type=str, default="q"
|
|
23662
|
-
).strip()
|
|
23663
|
-
|
|
23664
|
-
if choice_input == "q":
|
|
23665
|
-
return
|
|
23666
|
-
try:
|
|
23667
|
-
choice = int(choice_input)
|
|
23668
|
-
except ValueError:
|
|
23669
|
-
click.echo(click.style("Invalid selection!", fg="red"))
|
|
23670
|
-
click.pause()
|
|
23671
|
-
continue
|
|
23672
|
-
|
|
23673
|
-
if choice == 1:
|
|
23674
|
-
view_web_paths(engagement_id)
|
|
23675
|
-
elif choice == 2:
|
|
23676
|
-
view_smb_shares(engagement_id)
|
|
23677
|
-
elif choice == 3:
|
|
23678
|
-
view_sqlmap_data(engagement_id)
|
|
23679
|
-
else:
|
|
23680
|
-
click.echo(click.style("Invalid selection!", fg="red"))
|
|
23681
|
-
click.pause()
|
|
23682
|
-
|
|
23683
|
-
except (KeyboardInterrupt, click.Abort):
|
|
23684
|
-
return
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
23858
|
def view_additional_data_menu():
|
|
23688
23859
|
"""Additional data viewing menu for OSINT and Web Paths."""
|
|
23689
23860
|
em = EngagementManager()
|
|
@@ -27732,7 +27903,13 @@ def view_credentials(engagement_id: int):
|
|
|
27732
27903
|
return
|
|
27733
27904
|
|
|
27734
27905
|
# Active filters
|
|
27735
|
-
filters = {
|
|
27906
|
+
filters = {
|
|
27907
|
+
"service": None,
|
|
27908
|
+
"status": None,
|
|
27909
|
+
"host_id": None,
|
|
27910
|
+
"tool": None,
|
|
27911
|
+
"credential_type": None,
|
|
27912
|
+
}
|
|
27736
27913
|
|
|
27737
27914
|
# Pagination
|
|
27738
27915
|
PAGE_SIZE = 20
|
|
@@ -27780,6 +27957,14 @@ def view_credentials(engagement_id: int):
|
|
|
27780
27957
|
if filters["tool"] and credentials:
|
|
27781
27958
|
credentials = [c for c in credentials if c.get("tool") == filters["tool"]]
|
|
27782
27959
|
|
|
27960
|
+
# Apply credential_type filter
|
|
27961
|
+
if filters["credential_type"] and credentials:
|
|
27962
|
+
credentials = [
|
|
27963
|
+
c
|
|
27964
|
+
for c in credentials
|
|
27965
|
+
if c.get("credential_type") == filters["credential_type"]
|
|
27966
|
+
]
|
|
27967
|
+
|
|
27783
27968
|
# Pagination calculations
|
|
27784
27969
|
total_creds = len(credentials) if credentials else 0
|
|
27785
27970
|
total_pages = (
|
|
@@ -27903,6 +28088,7 @@ def view_credentials(engagement_id: int):
|
|
|
27903
28088
|
click.echo(" [a] Add - Manually add credential")
|
|
27904
28089
|
click.echo(" [f] Filter - Filter by service/status/host")
|
|
27905
28090
|
click.echo(" [o] Tool - Filter by tool")
|
|
28091
|
+
click.echo(" [y] Type - Filter by credential type (password/hash/username)")
|
|
27906
28092
|
click.echo(" [c] Clear filters - Reset all filters")
|
|
27907
28093
|
click.echo(" [q] Back")
|
|
27908
28094
|
click.echo()
|
|
@@ -28004,6 +28190,12 @@ def view_credentials(engagement_id: int):
|
|
|
28004
28190
|
# Filter by tool
|
|
28005
28191
|
filters["tool"] = _filter_credential_by_tool(engagement_id, cm)
|
|
28006
28192
|
current_page = 1
|
|
28193
|
+
elif choice == "y":
|
|
28194
|
+
# Filter by credential type
|
|
28195
|
+
filters["credential_type"] = _filter_credential_by_type(
|
|
28196
|
+
engagement_id, cm
|
|
28197
|
+
)
|
|
28198
|
+
current_page = 1
|
|
28007
28199
|
elif choice == "c":
|
|
28008
28200
|
filters = {k: None for k in filters}
|
|
28009
28201
|
current_page = 1
|
|
@@ -28308,6 +28500,50 @@ def _filter_credential_by_tool(engagement_id: int, cm: "CredentialsManager"):
|
|
|
28308
28500
|
return None
|
|
28309
28501
|
|
|
28310
28502
|
|
|
28503
|
+
def _filter_credential_by_type(engagement_id: int, cm: "CredentialsManager"):
|
|
28504
|
+
"""Prompt for credential type filter."""
|
|
28505
|
+
credentials = cm.list_credentials(engagement_id, decrypt=False)
|
|
28506
|
+
types = sorted(
|
|
28507
|
+
set([c.get("credential_type") for c in credentials if c.get("credential_type")])
|
|
28508
|
+
)
|
|
28509
|
+
|
|
28510
|
+
if not types:
|
|
28511
|
+
click.echo(click.style("\nNo credential types found.", fg="yellow"))
|
|
28512
|
+
click.pause()
|
|
28513
|
+
return None
|
|
28514
|
+
|
|
28515
|
+
# Add friendly names for common types
|
|
28516
|
+
type_labels = {
|
|
28517
|
+
"password": "password (tested credentials)",
|
|
28518
|
+
"hash": "hash (NTLM/other hashes)",
|
|
28519
|
+
"username": "username (enumerated users only)",
|
|
28520
|
+
"kerberos_tgs": "kerberos_tgs (Kerberoast hashes)",
|
|
28521
|
+
"plaintext": "plaintext (GPP/unencrypted)",
|
|
28522
|
+
"database": "database (DB credentials)",
|
|
28523
|
+
"smb": "smb (SMB credentials)",
|
|
28524
|
+
}
|
|
28525
|
+
|
|
28526
|
+
click.echo("\nAvailable credential types:")
|
|
28527
|
+
click.echo(" [q] Clear filter")
|
|
28528
|
+
for idx, ctype in enumerate(types, 1):
|
|
28529
|
+
count = sum(1 for c in credentials if c.get("credential_type") == ctype)
|
|
28530
|
+
label = type_labels.get(ctype, ctype)
|
|
28531
|
+
click.echo(f" [{idx}] {label} ({count})")
|
|
28532
|
+
|
|
28533
|
+
try:
|
|
28534
|
+
choice_input = click.prompt("Select type", type=str, default="q").strip()
|
|
28535
|
+
if choice_input == "q":
|
|
28536
|
+
return None
|
|
28537
|
+
try:
|
|
28538
|
+
choice = int(choice_input)
|
|
28539
|
+
if 1 <= choice <= len(types):
|
|
28540
|
+
return types[choice - 1]
|
|
28541
|
+
except ValueError:
|
|
28542
|
+
return None
|
|
28543
|
+
except (KeyboardInterrupt, click.Abort):
|
|
28544
|
+
return None
|
|
28545
|
+
|
|
28546
|
+
|
|
28311
28547
|
def _filter_credential_by_host(engagement_id: int):
|
|
28312
28548
|
"""Prompt for host filter."""
|
|
28313
28549
|
from souleyez.storage.hosts import HostManager
|
|
@@ -31271,7 +31507,9 @@ def _execute_webpath_quick_action(engagement_id: int, paths: list, hm: "HostMana
|
|
|
31271
31507
|
" ⚙️ Launching Gobuster for test directories...", fg="yellow"
|
|
31272
31508
|
)
|
|
31273
31509
|
)
|
|
31274
|
-
click.echo(
|
|
31510
|
+
click.echo(
|
|
31511
|
+
click.style(" ℹ️ Gobuster integration coming soon!", fg="cyan")
|
|
31512
|
+
)
|
|
31275
31513
|
click.echo(" For now, manually run:")
|
|
31276
31514
|
for path in test_dirs[:3]:
|
|
31277
31515
|
url = path.get("url", "")
|
|
@@ -34820,7 +35058,7 @@ def _view_sqlmap_dumped_data(engagement_id: int):
|
|
|
34820
35058
|
|
|
34821
35059
|
for table in tables_with_dumps:
|
|
34822
35060
|
console.print(
|
|
34823
|
-
f"
|
|
35061
|
+
f" [cyan]{table['table_name']}[/cyan] ({table['row_count']} rows)"
|
|
34824
35062
|
)
|
|
34825
35063
|
|
|
34826
35064
|
# Get dumped data
|
|
@@ -35473,18 +35711,24 @@ def view_sqlmap_data(engagement_id: int):
|
|
|
35473
35711
|
all_columns[key] = set()
|
|
35474
35712
|
all_columns[key].update(columns)
|
|
35475
35713
|
|
|
35476
|
-
# Aggregate dumped data
|
|
35714
|
+
# Aggregate dumped data (track by target URL)
|
|
35715
|
+
job_target = job.get("target", "")
|
|
35477
35716
|
if parsed.get("dumped_data"):
|
|
35478
35717
|
for key, data in parsed["dumped_data"].items():
|
|
35479
|
-
|
|
35480
|
-
|
|
35718
|
+
# Create target-specific key to avoid cross-host bleeding
|
|
35719
|
+
target_key = f"{job_target}::{key}"
|
|
35720
|
+
if target_key not in all_dumped:
|
|
35721
|
+
data["_target"] = job_target
|
|
35722
|
+
all_dumped[target_key] = data
|
|
35481
35723
|
else:
|
|
35482
35724
|
# Merge rows if same table dumped multiple times
|
|
35483
|
-
existing_rows = all_dumped[
|
|
35725
|
+
existing_rows = all_dumped[target_key].get("rows", [])
|
|
35484
35726
|
new_rows = data.get("rows", [])
|
|
35485
|
-
all_dumped[
|
|
35486
|
-
|
|
35487
|
-
|
|
35727
|
+
all_dumped[target_key]["rows"] = (
|
|
35728
|
+
existing_rows + new_rows
|
|
35729
|
+
)
|
|
35730
|
+
all_dumped[target_key]["row_count"] = len(
|
|
35731
|
+
all_dumped[target_key]["rows"]
|
|
35488
35732
|
)
|
|
35489
35733
|
|
|
35490
35734
|
for vuln in parsed.get("vulnerabilities", []):
|
|
@@ -35639,13 +35883,51 @@ def view_sqlmap_data(engagement_id: int):
|
|
|
35639
35883
|
row_num = int(choice)
|
|
35640
35884
|
if 1 <= row_num <= len(filtered_injections):
|
|
35641
35885
|
inj = filtered_injections[row_num - 1]
|
|
35886
|
+
inj_url = inj.get("url", "")
|
|
35887
|
+
|
|
35888
|
+
# Filter databases to only this injection's target
|
|
35889
|
+
filtered_dbs = set()
|
|
35890
|
+
filtered_db_to_dbms = {}
|
|
35891
|
+
for db in all_databases:
|
|
35892
|
+
db_target = db_to_target.get(db, "")
|
|
35893
|
+
if db_target in inj_url or inj_url in db_target:
|
|
35894
|
+
filtered_dbs.add(db)
|
|
35895
|
+
if db in db_to_dbms:
|
|
35896
|
+
filtered_db_to_dbms[db] = db_to_dbms[db]
|
|
35897
|
+
|
|
35898
|
+
# Filter tables to only this injection's target
|
|
35899
|
+
filtered_tables = {}
|
|
35900
|
+
filtered_columns = {}
|
|
35901
|
+
for db, tables in all_tables.items():
|
|
35902
|
+
for tbl in tables:
|
|
35903
|
+
table_key = f"{db}.{tbl}"
|
|
35904
|
+
tbl_target = table_to_target.get(table_key, "")
|
|
35905
|
+
if tbl_target in inj_url or inj_url in tbl_target:
|
|
35906
|
+
if db not in filtered_tables:
|
|
35907
|
+
filtered_tables[db] = set()
|
|
35908
|
+
filtered_tables[db].add(tbl)
|
|
35909
|
+
if table_key in all_columns:
|
|
35910
|
+
filtered_columns[table_key] = all_columns[table_key]
|
|
35911
|
+
|
|
35912
|
+
# Filter dumped data to only this injection's target
|
|
35913
|
+
filtered_dumped = {}
|
|
35914
|
+
for key, data in all_dumped.items():
|
|
35915
|
+
# Key format is "target_url::db.table"
|
|
35916
|
+
if "::" in key:
|
|
35917
|
+
target_part = key.split("::")[0]
|
|
35918
|
+
table_part = key.split("::", 1)[1]
|
|
35919
|
+
if target_part in inj_url or inj_url in target_part:
|
|
35920
|
+
filtered_dumped[table_part] = data
|
|
35921
|
+
elif data.get("_target", "") in inj_url:
|
|
35922
|
+
filtered_dumped[key] = data
|
|
35923
|
+
|
|
35642
35924
|
_view_sqlmap_injection_detail(
|
|
35643
35925
|
inj,
|
|
35644
|
-
|
|
35645
|
-
|
|
35646
|
-
|
|
35647
|
-
|
|
35648
|
-
|
|
35926
|
+
filtered_dbs,
|
|
35927
|
+
filtered_tables,
|
|
35928
|
+
filtered_columns,
|
|
35929
|
+
filtered_db_to_dbms,
|
|
35930
|
+
filtered_dumped,
|
|
35649
35931
|
engagement_id,
|
|
35650
35932
|
)
|
|
35651
35933
|
else:
|
|
@@ -35820,12 +36102,46 @@ def _view_sqlmap_injection_detail(
|
|
|
35820
36102
|
from rich.console import Console
|
|
35821
36103
|
from rich.table import Table
|
|
35822
36104
|
|
|
36105
|
+
from souleyez.ui.interactive_selector import _get_key
|
|
36106
|
+
|
|
35823
36107
|
console = Console()
|
|
35824
36108
|
|
|
35825
36109
|
if all_dumped is None:
|
|
35826
36110
|
all_dumped = {}
|
|
35827
36111
|
|
|
35828
|
-
|
|
36112
|
+
# View states: 'databases', 'tables:<db>', 'data:<table_key>'
|
|
36113
|
+
current_view = "databases"
|
|
36114
|
+
selected_db = None
|
|
36115
|
+
data_page = 1
|
|
36116
|
+
data_view_all = False
|
|
36117
|
+
db_cursor_pos = 0 # Cursor for database list
|
|
36118
|
+
tbl_cursor_pos = 0 # Cursor for table list
|
|
36119
|
+
show_empty_tables = False # Hide empty tables by default
|
|
36120
|
+
|
|
36121
|
+
# Build database list with stats
|
|
36122
|
+
db_list = []
|
|
36123
|
+
for db in sorted(all_databases) if all_databases else sorted(all_tables.keys()):
|
|
36124
|
+
tables = all_tables.get(db, set())
|
|
36125
|
+
table_count = len(tables)
|
|
36126
|
+
# Count extracted rows for this database
|
|
36127
|
+
extracted_count = 0
|
|
36128
|
+
extracted_tables = 0
|
|
36129
|
+
for tbl in tables:
|
|
36130
|
+
dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
|
|
36131
|
+
if dumped_key:
|
|
36132
|
+
extracted_tables += 1
|
|
36133
|
+
extracted_count += all_dumped[dumped_key].get(
|
|
36134
|
+
"row_count", len(all_dumped[dumped_key].get("rows", []))
|
|
36135
|
+
)
|
|
36136
|
+
db_list.append(
|
|
36137
|
+
{
|
|
36138
|
+
"name": db,
|
|
36139
|
+
"table_count": table_count,
|
|
36140
|
+
"extracted_tables": extracted_tables,
|
|
36141
|
+
"extracted_rows": extracted_count,
|
|
36142
|
+
"dbms": db_to_dbms.get(db, "Unknown"),
|
|
36143
|
+
}
|
|
36144
|
+
)
|
|
35829
36145
|
|
|
35830
36146
|
while True:
|
|
35831
36147
|
DesignSystem.clear_screen()
|
|
@@ -35855,412 +36171,729 @@ def _view_sqlmap_injection_detail(
|
|
|
35855
36171
|
)
|
|
35856
36172
|
click.echo()
|
|
35857
36173
|
|
|
35858
|
-
if current_view == "
|
|
35859
|
-
|
|
35860
|
-
|
|
35861
|
-
|
|
35862
|
-
all_tables,
|
|
35863
|
-
all_columns,
|
|
35864
|
-
db_to_dbms,
|
|
36174
|
+
if current_view == "databases":
|
|
36175
|
+
# Database selector view
|
|
36176
|
+
_sqli_database_selector(
|
|
36177
|
+
db_list,
|
|
35865
36178
|
all_dumped,
|
|
35866
36179
|
console,
|
|
36180
|
+
db_cursor_pos,
|
|
35867
36181
|
)
|
|
35868
36182
|
|
|
35869
|
-
#
|
|
35870
|
-
click.echo()
|
|
36183
|
+
# Navigation footer
|
|
36184
|
+
click.echo("─" * width)
|
|
36185
|
+
click.echo(" ↑↓/jk: Navigate | Enter: View tables | q: Back")
|
|
35871
36186
|
click.echo("─" * width)
|
|
35872
|
-
options = []
|
|
35873
|
-
if all_dumped:
|
|
35874
|
-
# List available tables with numbers
|
|
35875
|
-
table_keys = list(all_dumped.keys())
|
|
35876
|
-
click.echo(click.style(" BROWSE EXTRACTED DATA:", bold=True))
|
|
35877
|
-
for i, key in enumerate(table_keys, 1):
|
|
35878
|
-
data = all_dumped[key]
|
|
35879
|
-
row_count = data.get("row_count", len(data.get("rows", [])))
|
|
35880
|
-
# Highlight credential tables
|
|
35881
|
-
if any(
|
|
35882
|
-
x in key.lower()
|
|
35883
|
-
for x in [
|
|
35884
|
-
"user",
|
|
35885
|
-
"account",
|
|
35886
|
-
"login",
|
|
35887
|
-
"credential",
|
|
35888
|
-
"password",
|
|
35889
|
-
"card",
|
|
35890
|
-
"payment",
|
|
35891
|
-
]
|
|
35892
|
-
):
|
|
35893
|
-
click.echo(
|
|
35894
|
-
f" [{i}] {click.style(key, fg='red', bold=True)} ({row_count} rows) 🔐"
|
|
35895
|
-
)
|
|
35896
|
-
else:
|
|
35897
|
-
click.echo(f" [{i}] {key} ({row_count} rows)")
|
|
35898
|
-
options.extend([str(i) for i in range(1, len(table_keys) + 1)])
|
|
35899
|
-
|
|
35900
|
-
click.echo()
|
|
35901
|
-
click.echo(f" [t] View all tables structure")
|
|
35902
|
-
click.echo(f" [q] Back")
|
|
35903
|
-
click.echo()
|
|
35904
36187
|
|
|
36188
|
+
# Handle keyboard input
|
|
35905
36189
|
try:
|
|
35906
|
-
|
|
36190
|
+
key = _get_key()
|
|
35907
36191
|
|
|
35908
|
-
if
|
|
36192
|
+
if key in ("q", "Q", "\x1b"): # q or Escape
|
|
35909
36193
|
return
|
|
35910
|
-
elif
|
|
35911
|
-
|
|
35912
|
-
|
|
35913
|
-
|
|
35914
|
-
|
|
35915
|
-
|
|
35916
|
-
|
|
36194
|
+
elif key in ("\x1b[A", "k"): # Up arrow or k
|
|
36195
|
+
if db_cursor_pos > 0:
|
|
36196
|
+
db_cursor_pos -= 1
|
|
36197
|
+
elif key in ("\x1b[B", "j"): # Down arrow or j
|
|
36198
|
+
if db_cursor_pos < len(db_list) - 1:
|
|
36199
|
+
db_cursor_pos += 1
|
|
36200
|
+
elif key in ("\r", "\n"): # Enter
|
|
36201
|
+
if db_list:
|
|
36202
|
+
selected_db = db_list[db_cursor_pos]["name"]
|
|
36203
|
+
current_view = f"tables:{selected_db}"
|
|
36204
|
+
tbl_cursor_pos = 0
|
|
35917
36205
|
except (KeyboardInterrupt, click.Abort):
|
|
35918
36206
|
return
|
|
35919
36207
|
|
|
35920
|
-
elif current_view
|
|
35921
|
-
|
|
35922
|
-
|
|
36208
|
+
elif current_view.startswith("tables:"):
|
|
36209
|
+
db_name = current_view[7:]
|
|
36210
|
+
tables = all_tables.get(db_name, set())
|
|
36211
|
+
|
|
36212
|
+
# Build full table list for this database
|
|
36213
|
+
full_table_list = []
|
|
36214
|
+
for tbl in sorted(tables):
|
|
36215
|
+
dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
|
|
36216
|
+
# Check if table has data or is a high-value table
|
|
36217
|
+
tbl_lower = tbl.lower()
|
|
36218
|
+
is_high_value = any(
|
|
36219
|
+
x in tbl_lower
|
|
36220
|
+
for x in [
|
|
36221
|
+
"user",
|
|
36222
|
+
"account",
|
|
36223
|
+
"login",
|
|
36224
|
+
"credential",
|
|
36225
|
+
"password",
|
|
36226
|
+
"card",
|
|
36227
|
+
"payment",
|
|
36228
|
+
]
|
|
36229
|
+
)
|
|
36230
|
+
full_table_list.append(
|
|
36231
|
+
{
|
|
36232
|
+
"db": db_name,
|
|
36233
|
+
"table": tbl,
|
|
36234
|
+
"dumped_key": dumped_key,
|
|
36235
|
+
"has_data": dumped_key is not None,
|
|
36236
|
+
"is_high_value": is_high_value,
|
|
36237
|
+
}
|
|
36238
|
+
)
|
|
36239
|
+
|
|
36240
|
+
# Filter table list based on show_empty_tables
|
|
36241
|
+
if show_empty_tables:
|
|
36242
|
+
table_list = full_table_list
|
|
36243
|
+
else:
|
|
36244
|
+
# Show only tables with data or high-value tables
|
|
36245
|
+
table_list = [
|
|
36246
|
+
t for t in full_table_list if t["has_data"] or t["is_high_value"]
|
|
36247
|
+
]
|
|
36248
|
+
# If no tables match filter, show all
|
|
36249
|
+
if not table_list:
|
|
36250
|
+
table_list = full_table_list
|
|
36251
|
+
|
|
36252
|
+
# Ensure cursor is within bounds
|
|
36253
|
+
if tbl_cursor_pos >= len(table_list):
|
|
36254
|
+
tbl_cursor_pos = max(0, len(table_list) - 1)
|
|
36255
|
+
|
|
36256
|
+
# Count hidden tables
|
|
36257
|
+
hidden_count = len(full_table_list) - len(table_list)
|
|
36258
|
+
|
|
36259
|
+
# Show tables for this database
|
|
36260
|
+
_sqli_tables_for_database(
|
|
36261
|
+
db_name,
|
|
36262
|
+
table_list,
|
|
36263
|
+
all_columns,
|
|
36264
|
+
db_to_dbms,
|
|
36265
|
+
all_dumped,
|
|
36266
|
+
console,
|
|
36267
|
+
tbl_cursor_pos,
|
|
36268
|
+
hidden_count=hidden_count,
|
|
36269
|
+
show_all=show_empty_tables,
|
|
35923
36270
|
)
|
|
35924
36271
|
|
|
35925
|
-
|
|
36272
|
+
# Navigation footer
|
|
36273
|
+
click.echo("─" * width)
|
|
36274
|
+
if show_empty_tables:
|
|
36275
|
+
click.echo(
|
|
36276
|
+
" ↑↓/jk: Navigate | Enter: View data | a: Hide empty | q: Back"
|
|
36277
|
+
)
|
|
36278
|
+
else:
|
|
36279
|
+
click.echo(
|
|
36280
|
+
" ↑↓/jk: Navigate | Enter: View data | a: Show all | q: Back"
|
|
36281
|
+
)
|
|
35926
36282
|
click.echo("─" * width)
|
|
35927
|
-
click.echo(f" [b] Back to overview")
|
|
35928
|
-
click.echo()
|
|
35929
36283
|
|
|
36284
|
+
# Handle keyboard input
|
|
35930
36285
|
try:
|
|
35931
|
-
|
|
35932
|
-
|
|
35933
|
-
|
|
36286
|
+
key = _get_key()
|
|
36287
|
+
|
|
36288
|
+
if key in ("q", "Q", "\x1b"): # q or Escape - back to databases
|
|
36289
|
+
current_view = "databases"
|
|
36290
|
+
tbl_cursor_pos = 0
|
|
36291
|
+
elif key in ("\x1b[A", "k"): # Up arrow or k
|
|
36292
|
+
if tbl_cursor_pos > 0:
|
|
36293
|
+
tbl_cursor_pos -= 1
|
|
36294
|
+
elif key in ("\x1b[B", "j"): # Down arrow or j
|
|
36295
|
+
if tbl_cursor_pos < len(table_list) - 1:
|
|
36296
|
+
tbl_cursor_pos += 1
|
|
36297
|
+
elif key in ("a", "A"): # Toggle show all tables
|
|
36298
|
+
show_empty_tables = not show_empty_tables
|
|
36299
|
+
tbl_cursor_pos = 0 # Reset cursor when toggling
|
|
36300
|
+
elif key in ("\r", "\n"): # Enter
|
|
36301
|
+
if table_list and table_list[tbl_cursor_pos]["has_data"]:
|
|
36302
|
+
current_view = (
|
|
36303
|
+
f"data:{table_list[tbl_cursor_pos]['dumped_key']}"
|
|
36304
|
+
)
|
|
36305
|
+
data_page = 1
|
|
36306
|
+
data_view_all = False
|
|
35934
36307
|
except (KeyboardInterrupt, click.Abort):
|
|
35935
36308
|
return
|
|
35936
36309
|
|
|
35937
36310
|
elif current_view.startswith("data:"):
|
|
35938
36311
|
table_key = current_view[5:]
|
|
35939
36312
|
if table_key in all_dumped:
|
|
36313
|
+
table_data = all_dumped[table_key]
|
|
36314
|
+
total_rows = len(table_data.get("rows", []))
|
|
36315
|
+
page_size = 20
|
|
36316
|
+
total_pages = (total_rows + page_size - 1) // page_size
|
|
36317
|
+
|
|
35940
36318
|
_sqli_detail_data_table(
|
|
35941
|
-
table_key,
|
|
36319
|
+
table_key,
|
|
36320
|
+
table_data,
|
|
36321
|
+
console,
|
|
36322
|
+
width,
|
|
36323
|
+
page=data_page,
|
|
36324
|
+
page_size=page_size,
|
|
36325
|
+
view_all=data_view_all,
|
|
35942
36326
|
)
|
|
35943
36327
|
|
|
35944
|
-
click.echo()
|
|
35945
36328
|
click.echo("─" * width)
|
|
35946
|
-
click.echo(
|
|
36329
|
+
click.echo()
|
|
36330
|
+
if total_rows > page_size and not data_view_all:
|
|
36331
|
+
click.echo(" [n] Next - Next page")
|
|
36332
|
+
click.echo(" [p] Prev - Previous page")
|
|
36333
|
+
click.echo(" [a] All - View all records")
|
|
36334
|
+
elif data_view_all:
|
|
36335
|
+
click.echo(" [a] Paginate - Return to paginated view")
|
|
36336
|
+
click.echo(" [q] Back")
|
|
35947
36337
|
click.echo()
|
|
35948
36338
|
|
|
35949
36339
|
try:
|
|
35950
|
-
choice = click.prompt("
|
|
35951
|
-
if choice == "
|
|
35952
|
-
|
|
36340
|
+
choice = click.prompt("Select option", default="q").strip().lower()
|
|
36341
|
+
if choice == "q":
|
|
36342
|
+
# Go back to tables view for the selected database
|
|
36343
|
+
if selected_db:
|
|
36344
|
+
current_view = f"tables:{selected_db}"
|
|
36345
|
+
else:
|
|
36346
|
+
current_view = "databases"
|
|
36347
|
+
data_page = 1
|
|
36348
|
+
data_view_all = False
|
|
36349
|
+
elif choice == "n" and not data_view_all and data_page < total_pages:
|
|
36350
|
+
data_page += 1
|
|
36351
|
+
elif choice == "p" and not data_view_all and data_page > 1:
|
|
36352
|
+
data_page -= 1
|
|
36353
|
+
elif choice == "a":
|
|
36354
|
+
data_view_all = not data_view_all
|
|
36355
|
+
data_page = 1
|
|
35953
36356
|
except (KeyboardInterrupt, click.Abort):
|
|
35954
36357
|
return
|
|
35955
36358
|
|
|
35956
36359
|
|
|
35957
|
-
def
|
|
35958
|
-
|
|
36360
|
+
def _sqli_database_selector(db_list, all_dumped, console, cursor_pos):
|
|
36361
|
+
"""Show database selector with table counts and extracted data."""
|
|
36362
|
+
from rich.table import Table
|
|
36363
|
+
|
|
36364
|
+
# Count total extracted data
|
|
36365
|
+
total_extracted = 0
|
|
36366
|
+
cred_count = 0
|
|
36367
|
+
for key, data in all_dumped.items():
|
|
36368
|
+
row_count = data.get("row_count", len(data.get("rows", [])))
|
|
36369
|
+
total_extracted += row_count
|
|
36370
|
+
key_lower = key.lower()
|
|
36371
|
+
if any(x in key_lower for x in ["user", "account", "login", "credential"]):
|
|
36372
|
+
cred_count += row_count
|
|
36373
|
+
|
|
36374
|
+
# Summary alert if data extracted
|
|
36375
|
+
if total_extracted > 0:
|
|
36376
|
+
alerts = []
|
|
36377
|
+
if cred_count > 0:
|
|
36378
|
+
alerts.append(f"🔑 {cred_count} credentials")
|
|
36379
|
+
if total_extracted > cred_count:
|
|
36380
|
+
alerts.append(f"📊 {total_extracted} total rows")
|
|
36381
|
+
click.echo(
|
|
36382
|
+
click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
|
|
36383
|
+
+ click.style(" | ".join(alerts), fg="red")
|
|
36384
|
+
)
|
|
36385
|
+
click.echo()
|
|
36386
|
+
|
|
36387
|
+
# Summary
|
|
36388
|
+
total_tables = sum(db["table_count"] for db in db_list)
|
|
36389
|
+
extracted_tables = sum(db["extracted_tables"] for db in db_list)
|
|
36390
|
+
click.echo(
|
|
36391
|
+
f" {click.style('Databases:', bold=True)} {len(db_list)} | "
|
|
36392
|
+
f"{click.style('Tables:', bold=True)} {total_tables} | "
|
|
36393
|
+
f"{click.style('Extracted:', bold=True, fg='green')} {extracted_tables}"
|
|
36394
|
+
)
|
|
36395
|
+
click.echo()
|
|
36396
|
+
|
|
36397
|
+
# Database table
|
|
36398
|
+
table = Table(
|
|
36399
|
+
show_header=True,
|
|
36400
|
+
header_style="bold cyan",
|
|
36401
|
+
box=DesignSystem.TABLE_BOX,
|
|
36402
|
+
padding=(0, 1),
|
|
36403
|
+
expand=True,
|
|
36404
|
+
)
|
|
36405
|
+
table.add_column("", width=3, no_wrap=True) # Cursor
|
|
36406
|
+
table.add_column("Database", no_wrap=True)
|
|
36407
|
+
table.add_column("DBMS", width=12)
|
|
36408
|
+
table.add_column("Tables", width=10, justify="right")
|
|
36409
|
+
table.add_column("Extracted", width=12, justify="right")
|
|
36410
|
+
table.add_column("Rows", width=10, justify="right")
|
|
36411
|
+
|
|
36412
|
+
for idx, db in enumerate(db_list):
|
|
36413
|
+
is_selected = idx == cursor_pos
|
|
36414
|
+
cursor = "▶" if is_selected else ""
|
|
36415
|
+
|
|
36416
|
+
name = db["name"]
|
|
36417
|
+
dbms_type = db["dbms"]
|
|
36418
|
+
table_count = db["table_count"]
|
|
36419
|
+
extracted_tables = db["extracted_tables"]
|
|
36420
|
+
extracted_rows = db["extracted_rows"]
|
|
36421
|
+
|
|
36422
|
+
# Highlight rows with extracted data
|
|
36423
|
+
if extracted_tables > 0:
|
|
36424
|
+
if is_selected:
|
|
36425
|
+
table.add_row(
|
|
36426
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36427
|
+
f"[bold yellow]📂 {name}[/bold yellow]",
|
|
36428
|
+
f"[bold yellow]{dbms_type}[/bold yellow]",
|
|
36429
|
+
f"[bold yellow]{table_count}[/bold yellow]",
|
|
36430
|
+
f"[bold yellow]✓ {extracted_tables}[/bold yellow]",
|
|
36431
|
+
f"[bold yellow]{extracted_rows}[/bold yellow]",
|
|
36432
|
+
)
|
|
36433
|
+
else:
|
|
36434
|
+
table.add_row(
|
|
36435
|
+
cursor,
|
|
36436
|
+
f"[green bold]📂 {name}[/green bold]",
|
|
36437
|
+
dbms_type,
|
|
36438
|
+
str(table_count),
|
|
36439
|
+
f"[green]✓ {extracted_tables}[/green]",
|
|
36440
|
+
f"[green]{extracted_rows}[/green]",
|
|
36441
|
+
)
|
|
36442
|
+
else:
|
|
36443
|
+
if is_selected:
|
|
36444
|
+
table.add_row(
|
|
36445
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36446
|
+
f"[bold yellow]📂 {name}[/bold yellow]",
|
|
36447
|
+
f"[bold yellow]{dbms_type}[/bold yellow]",
|
|
36448
|
+
f"[bold yellow]{table_count}[/bold yellow]",
|
|
36449
|
+
"[bold yellow]-[/bold yellow]",
|
|
36450
|
+
"[bold yellow]-[/bold yellow]",
|
|
36451
|
+
)
|
|
36452
|
+
else:
|
|
36453
|
+
table.add_row(
|
|
36454
|
+
cursor,
|
|
36455
|
+
f"[dim]📂 {name}[/dim]",
|
|
36456
|
+
f"[dim]{dbms_type}[/dim]",
|
|
36457
|
+
f"[dim]{table_count}[/dim]",
|
|
36458
|
+
"[dim]-[/dim]",
|
|
36459
|
+
"[dim]-[/dim]",
|
|
36460
|
+
)
|
|
36461
|
+
|
|
36462
|
+
console.print(table)
|
|
36463
|
+
click.echo()
|
|
36464
|
+
|
|
36465
|
+
|
|
36466
|
+
def _sqli_tables_for_database(
|
|
36467
|
+
db_name,
|
|
36468
|
+
table_list,
|
|
36469
|
+
all_columns,
|
|
36470
|
+
db_to_dbms,
|
|
36471
|
+
all_dumped,
|
|
36472
|
+
console,
|
|
36473
|
+
cursor_pos,
|
|
36474
|
+
hidden_count=0,
|
|
36475
|
+
show_all=False,
|
|
35959
36476
|
):
|
|
35960
|
-
"""Show
|
|
36477
|
+
"""Show tables for a specific database with cursor navigation."""
|
|
35961
36478
|
from rich.table import Table
|
|
35962
36479
|
|
|
35963
|
-
|
|
35964
|
-
|
|
36480
|
+
dbms_type = db_to_dbms.get(db_name, "Unknown")
|
|
36481
|
+
|
|
36482
|
+
# Count extracted data
|
|
36483
|
+
extracted_count = sum(1 for t in table_list if t["has_data"])
|
|
35965
36484
|
total_rows = sum(
|
|
35966
|
-
|
|
36485
|
+
all_dumped[t["dumped_key"]].get(
|
|
36486
|
+
"row_count", len(all_dumped[t["dumped_key"]].get("rows", []))
|
|
36487
|
+
)
|
|
36488
|
+
for t in table_list
|
|
36489
|
+
if t["has_data"]
|
|
35967
36490
|
)
|
|
35968
36491
|
|
|
35969
|
-
#
|
|
35970
|
-
|
|
35971
|
-
|
|
35972
|
-
|
|
35973
|
-
|
|
35974
|
-
|
|
35975
|
-
"card" in k.lower() or "payment" in k.lower() or "credit" in k.lower()
|
|
35976
|
-
for k in all_dumped.keys()
|
|
36492
|
+
# Database header
|
|
36493
|
+
click.echo(f" {click.style('📂 ' + db_name, bold=True, fg='cyan')} ({dbms_type})")
|
|
36494
|
+
summary_line = (
|
|
36495
|
+
f" {click.style('Tables:', bold=True)} {len(table_list)} | "
|
|
36496
|
+
f"{click.style('Extracted:', bold=True, fg='green')} {extracted_count} | "
|
|
36497
|
+
f"{click.style('Rows:', bold=True)} {total_rows}"
|
|
35977
36498
|
)
|
|
35978
|
-
|
|
35979
|
-
|
|
35980
|
-
|
|
36499
|
+
if hidden_count > 0 and not show_all:
|
|
36500
|
+
summary_line += click.style(f" | {hidden_count} hidden", dim=True)
|
|
36501
|
+
click.echo(summary_line)
|
|
36502
|
+
click.echo()
|
|
36503
|
+
|
|
36504
|
+
if not table_list:
|
|
36505
|
+
click.echo(" [dim]No tables discovered for this database.[/dim]")
|
|
36506
|
+
return
|
|
36507
|
+
|
|
36508
|
+
# Table list
|
|
36509
|
+
table = Table(
|
|
36510
|
+
show_header=True,
|
|
36511
|
+
header_style="bold cyan",
|
|
36512
|
+
box=DesignSystem.TABLE_BOX,
|
|
36513
|
+
padding=(0, 1),
|
|
36514
|
+
expand=True,
|
|
35981
36515
|
)
|
|
36516
|
+
table.add_column("", width=3, no_wrap=True) # Cursor
|
|
36517
|
+
table.add_column("○", width=3, justify="center", no_wrap=True)
|
|
36518
|
+
table.add_column("Table Name", no_wrap=True)
|
|
36519
|
+
table.add_column("Cols", width=8, justify="right")
|
|
36520
|
+
table.add_column("Rows", width=12, justify="right")
|
|
36521
|
+
table.add_column("Type", width=12)
|
|
36522
|
+
|
|
36523
|
+
for idx, tbl_info in enumerate(table_list):
|
|
36524
|
+
tbl = tbl_info["table"]
|
|
36525
|
+
is_selected = idx == cursor_pos
|
|
36526
|
+
cursor = "▶" if is_selected else ""
|
|
36527
|
+
|
|
36528
|
+
table_key = f"{db_name}.{tbl}"
|
|
36529
|
+
cols = all_columns.get(table_key, set())
|
|
36530
|
+
col_count = len(cols) if cols else 0
|
|
36531
|
+
|
|
36532
|
+
# Determine category
|
|
36533
|
+
tbl_lower = tbl.lower()
|
|
36534
|
+
if any(
|
|
36535
|
+
x in tbl_lower
|
|
36536
|
+
for x in ["user", "account", "login", "credential", "password"]
|
|
36537
|
+
):
|
|
36538
|
+
category = "🔑 Creds"
|
|
36539
|
+
elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
|
|
36540
|
+
category = "💳 Cards"
|
|
36541
|
+
elif any(
|
|
36542
|
+
x in tbl_lower for x in ["address", "personal", "customer", "profile"]
|
|
36543
|
+
):
|
|
36544
|
+
category = "👤 PII"
|
|
36545
|
+
else:
|
|
36546
|
+
category = ""
|
|
35982
36547
|
|
|
35983
|
-
|
|
35984
|
-
|
|
36548
|
+
has_data = tbl_info["has_data"]
|
|
36549
|
+
if has_data:
|
|
36550
|
+
dumped_key = tbl_info["dumped_key"]
|
|
36551
|
+
row_count = all_dumped[dumped_key].get(
|
|
36552
|
+
"row_count", len(all_dumped[dumped_key].get("rows", []))
|
|
36553
|
+
)
|
|
36554
|
+
if is_selected:
|
|
36555
|
+
table.add_row(
|
|
36556
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36557
|
+
"[bold yellow]○[/bold yellow]",
|
|
36558
|
+
f"[bold yellow]{tbl}[/bold yellow]",
|
|
36559
|
+
f"[bold yellow]{col_count}[/bold yellow]",
|
|
36560
|
+
f"[bold yellow]✓ {row_count}[/bold yellow]",
|
|
36561
|
+
f"[bold yellow]{category}[/bold yellow]",
|
|
36562
|
+
)
|
|
36563
|
+
else:
|
|
36564
|
+
table.add_row(
|
|
36565
|
+
cursor,
|
|
36566
|
+
"○",
|
|
36567
|
+
f"[green bold]{tbl}[/green bold]",
|
|
36568
|
+
str(col_count),
|
|
36569
|
+
f"[green]✓ {row_count}[/green]",
|
|
36570
|
+
category,
|
|
36571
|
+
)
|
|
36572
|
+
else:
|
|
36573
|
+
if is_selected:
|
|
36574
|
+
table.add_row(
|
|
36575
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36576
|
+
"[bold yellow]○[/bold yellow]",
|
|
36577
|
+
f"[bold yellow]{tbl}[/bold yellow]",
|
|
36578
|
+
f"[bold yellow]{col_count}[/bold yellow]",
|
|
36579
|
+
"[bold yellow]-[/bold yellow]",
|
|
36580
|
+
f"[bold yellow]{category}[/bold yellow]",
|
|
36581
|
+
)
|
|
36582
|
+
else:
|
|
36583
|
+
table.add_row(
|
|
36584
|
+
cursor,
|
|
36585
|
+
"○",
|
|
36586
|
+
tbl,
|
|
36587
|
+
str(col_count),
|
|
36588
|
+
"[dim]-[/dim]",
|
|
36589
|
+
category,
|
|
36590
|
+
)
|
|
36591
|
+
|
|
36592
|
+
console.print(table)
|
|
35985
36593
|
click.echo()
|
|
35986
36594
|
|
|
35987
|
-
|
|
35988
|
-
|
|
35989
|
-
|
|
35990
|
-
|
|
36595
|
+
|
|
36596
|
+
def _sqli_detail_overview_interactive(
|
|
36597
|
+
inj,
|
|
36598
|
+
all_databases,
|
|
36599
|
+
all_tables,
|
|
36600
|
+
all_columns,
|
|
36601
|
+
db_to_dbms,
|
|
36602
|
+
all_dumped,
|
|
36603
|
+
console,
|
|
36604
|
+
cursor_pos,
|
|
36605
|
+
all_table_list,
|
|
36606
|
+
):
|
|
36607
|
+
"""Show interactive overview with cursor navigation."""
|
|
36608
|
+
from rich.table import Table
|
|
36609
|
+
|
|
36610
|
+
# Count extracted data by category
|
|
36611
|
+
cred_count = 0
|
|
36612
|
+
card_count = 0
|
|
36613
|
+
pii_count = 0
|
|
36614
|
+
|
|
36615
|
+
for key, data in all_dumped.items():
|
|
36616
|
+
row_count = data.get("row_count", len(data.get("rows", [])))
|
|
36617
|
+
key_lower = key.lower()
|
|
36618
|
+
if any(x in key_lower for x in ["user", "account", "login", "credential"]):
|
|
36619
|
+
cred_count += row_count
|
|
36620
|
+
elif any(x in key_lower for x in ["card", "payment", "credit"]):
|
|
36621
|
+
card_count += row_count
|
|
36622
|
+
elif any(
|
|
36623
|
+
x in key_lower for x in ["address", "personal", "customer", "profile"]
|
|
36624
|
+
):
|
|
36625
|
+
pii_count += row_count
|
|
36626
|
+
|
|
36627
|
+
# Summary alert if data extracted
|
|
36628
|
+
if cred_count > 0 or card_count > 0 or pii_count > 0:
|
|
36629
|
+
alerts = []
|
|
36630
|
+
if cred_count > 0:
|
|
36631
|
+
alerts.append(f"🔑 {cred_count} credentials")
|
|
36632
|
+
if card_count > 0:
|
|
36633
|
+
alerts.append(f"💳 {card_count} cards")
|
|
36634
|
+
if pii_count > 0:
|
|
36635
|
+
alerts.append(f"👤 {pii_count} PII records")
|
|
35991
36636
|
click.echo(
|
|
35992
|
-
|
|
36637
|
+
click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
|
|
36638
|
+
+ click.style(" | ".join(alerts), fg="red")
|
|
35993
36639
|
)
|
|
35994
36640
|
click.echo()
|
|
35995
36641
|
|
|
35996
|
-
|
|
35997
|
-
|
|
35998
|
-
|
|
35999
|
-
|
|
36000
|
-
|
|
36001
|
-
)
|
|
36002
|
-
if has_cards:
|
|
36003
|
-
click.echo(
|
|
36004
|
-
f" 💳 {click.style('PAYMENT DATA FOUND', fg='red', bold=True)} - Credit card information"
|
|
36005
|
-
)
|
|
36006
|
-
if has_pii:
|
|
36007
|
-
click.echo(
|
|
36008
|
-
f" 👤 {click.style('PII FOUND', fg='yellow', bold=True)} - Personal addresses/information"
|
|
36009
|
-
)
|
|
36010
|
-
if not (has_credentials or has_cards or has_pii):
|
|
36011
|
-
click.echo(f" 📄 Application data extracted")
|
|
36012
|
-
else:
|
|
36013
|
-
click.echo(f" {click.style('No data extracted yet', fg='yellow')}")
|
|
36014
|
-
click.echo()
|
|
36015
|
-
click.echo(
|
|
36016
|
-
" 💡 Tables discovered but not yet dumped. Select a table to dump data."
|
|
36017
|
-
)
|
|
36642
|
+
# Database structure
|
|
36643
|
+
if not all_databases and not all_tables:
|
|
36644
|
+
click.echo(" No database structure discovered yet.")
|
|
36645
|
+
click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
|
|
36646
|
+
return
|
|
36018
36647
|
|
|
36648
|
+
# Summary line
|
|
36649
|
+
total_tables = len(all_table_list)
|
|
36650
|
+
dumped_count = len(all_dumped)
|
|
36651
|
+
click.echo(
|
|
36652
|
+
f" {click.style('Total:', bold=True)} {total_tables} tables | "
|
|
36653
|
+
f"{click.style('Extracted:', bold=True, fg='green')} {dumped_count}"
|
|
36654
|
+
)
|
|
36019
36655
|
click.echo()
|
|
36020
36656
|
|
|
36021
|
-
# Show
|
|
36022
|
-
if
|
|
36023
|
-
|
|
36657
|
+
# Show database structure with Rich table and cursor
|
|
36658
|
+
for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
|
|
36659
|
+
dbms_type = db_to_dbms.get(db, "SQLite")
|
|
36660
|
+
tables = all_tables.get(db, set())
|
|
36661
|
+
|
|
36662
|
+
click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
|
|
36024
36663
|
click.echo()
|
|
36025
36664
|
|
|
36026
|
-
|
|
36027
|
-
|
|
36028
|
-
|
|
36029
|
-
|
|
36665
|
+
if tables:
|
|
36666
|
+
# Create Rich table with cursor column
|
|
36667
|
+
table = Table(
|
|
36668
|
+
show_header=True,
|
|
36669
|
+
header_style="bold cyan",
|
|
36670
|
+
box=DesignSystem.TABLE_BOX,
|
|
36671
|
+
padding=(0, 1),
|
|
36672
|
+
expand=True,
|
|
36673
|
+
)
|
|
36674
|
+
|
|
36675
|
+
table.add_column("", width=3, no_wrap=True) # Cursor
|
|
36676
|
+
table.add_column("○", width=3, justify="center", no_wrap=True)
|
|
36677
|
+
table.add_column("Table Name", width=28, no_wrap=True)
|
|
36678
|
+
table.add_column("Cols", width=6, justify="right", no_wrap=True)
|
|
36679
|
+
table.add_column("Rows", width=12, justify="right", no_wrap=True)
|
|
36680
|
+
table.add_column("Type", width=12, no_wrap=True)
|
|
36030
36681
|
|
|
36031
|
-
|
|
36032
|
-
|
|
36033
|
-
|
|
36034
|
-
|
|
36035
|
-
|
|
36036
|
-
|
|
36037
|
-
|
|
36038
|
-
for x in ["username", "user", "login", "email"]
|
|
36039
|
-
)
|
|
36040
|
-
),
|
|
36041
|
-
None,
|
|
36042
|
-
)
|
|
36043
|
-
pass_col = next(
|
|
36682
|
+
sorted_tables = sorted(tables)
|
|
36683
|
+
for tbl in sorted_tables:
|
|
36684
|
+
table_key = f"{db}.{tbl}"
|
|
36685
|
+
cols = all_columns.get(table_key, set())
|
|
36686
|
+
|
|
36687
|
+
# Find this table's index in all_table_list
|
|
36688
|
+
tbl_idx = next(
|
|
36044
36689
|
(
|
|
36045
|
-
|
|
36046
|
-
for
|
|
36047
|
-
if
|
|
36048
|
-
x in c.lower()
|
|
36049
|
-
for x in ["password", "passwd", "hash", "pwd"]
|
|
36050
|
-
)
|
|
36690
|
+
i
|
|
36691
|
+
for i, t in enumerate(all_table_list)
|
|
36692
|
+
if t["table"] == tbl and t["db"] == db
|
|
36051
36693
|
),
|
|
36052
|
-
|
|
36694
|
+
-1,
|
|
36053
36695
|
)
|
|
36054
|
-
|
|
36696
|
+
is_selected = tbl_idx == cursor_pos
|
|
36055
36697
|
|
|
36056
|
-
|
|
36057
|
-
|
|
36058
|
-
table = Table(
|
|
36059
|
-
show_header=True,
|
|
36060
|
-
header_style="bold red",
|
|
36061
|
-
box=None,
|
|
36062
|
-
padding=(0, 2),
|
|
36063
|
-
)
|
|
36064
|
-
if user_col:
|
|
36065
|
-
table.add_column("Username", style="yellow")
|
|
36066
|
-
if email_col and email_col != user_col:
|
|
36067
|
-
table.add_column("Email", style="cyan")
|
|
36068
|
-
if pass_col:
|
|
36069
|
-
table.add_column("Password/Hash", style="red")
|
|
36070
|
-
|
|
36071
|
-
for row in rows[:5]:
|
|
36072
|
-
row_data = []
|
|
36073
|
-
if user_col:
|
|
36074
|
-
val = str(row.get(user_col, ""))[:30]
|
|
36075
|
-
row_data.append(
|
|
36076
|
-
val
|
|
36077
|
-
if val and val not in ["<blank>", "None", "NULL"]
|
|
36078
|
-
else "-"
|
|
36079
|
-
)
|
|
36080
|
-
if email_col and email_col != user_col:
|
|
36081
|
-
val = str(row.get(email_col, ""))[:35]
|
|
36082
|
-
row_data.append(val if val else "-")
|
|
36083
|
-
if pass_col:
|
|
36084
|
-
val = str(row.get(pass_col, ""))[:40]
|
|
36085
|
-
row_data.append(val if val else "-")
|
|
36086
|
-
if row_data:
|
|
36087
|
-
table.add_row(*row_data)
|
|
36698
|
+
# Cursor indicator
|
|
36699
|
+
cursor = "▶" if is_selected else ""
|
|
36088
36700
|
|
|
36089
|
-
|
|
36701
|
+
# Check if this table has been dumped
|
|
36702
|
+
dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
|
|
36703
|
+
has_data = dumped_key is not None
|
|
36090
36704
|
|
|
36091
|
-
|
|
36092
|
-
|
|
36093
|
-
|
|
36094
|
-
|
|
36705
|
+
# Determine category
|
|
36706
|
+
tbl_lower = tbl.lower()
|
|
36707
|
+
if any(
|
|
36708
|
+
x in tbl_lower
|
|
36709
|
+
for x in ["user", "account", "login", "credential", "password"]
|
|
36710
|
+
):
|
|
36711
|
+
category = "🔑 Creds"
|
|
36712
|
+
elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
|
|
36713
|
+
category = "💳 Cards"
|
|
36714
|
+
elif any(
|
|
36715
|
+
x in tbl_lower
|
|
36716
|
+
for x in ["address", "personal", "customer", "profile"]
|
|
36717
|
+
):
|
|
36718
|
+
category = "👤 PII"
|
|
36719
|
+
else:
|
|
36720
|
+
category = ""
|
|
36721
|
+
|
|
36722
|
+
col_count = len(cols) if cols else 0
|
|
36723
|
+
|
|
36724
|
+
if has_data:
|
|
36725
|
+
row_count = all_dumped[dumped_key].get(
|
|
36726
|
+
"row_count", len(all_dumped[dumped_key].get("rows", []))
|
|
36727
|
+
)
|
|
36728
|
+
if is_selected:
|
|
36729
|
+
table.add_row(
|
|
36730
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36731
|
+
"[bold yellow]○[/bold yellow]",
|
|
36732
|
+
f"[bold yellow]{tbl}[/bold yellow]",
|
|
36733
|
+
f"[bold yellow]{col_count}[/bold yellow]",
|
|
36734
|
+
f"[bold yellow]✓ {row_count}[/bold yellow]",
|
|
36735
|
+
f"[bold yellow]{category}[/bold yellow]",
|
|
36736
|
+
)
|
|
36737
|
+
else:
|
|
36738
|
+
table.add_row(
|
|
36739
|
+
cursor,
|
|
36740
|
+
"○",
|
|
36741
|
+
f"[green bold]{tbl}[/green bold]",
|
|
36742
|
+
str(col_count),
|
|
36743
|
+
f"[green]✓ {row_count}[/green]",
|
|
36744
|
+
category,
|
|
36745
|
+
)
|
|
36746
|
+
else:
|
|
36747
|
+
if is_selected:
|
|
36748
|
+
table.add_row(
|
|
36749
|
+
f"[bold yellow]{cursor}[/bold yellow]",
|
|
36750
|
+
"[bold yellow]○[/bold yellow]",
|
|
36751
|
+
f"[bold yellow]{tbl}[/bold yellow]",
|
|
36752
|
+
f"[bold yellow]{col_count}[/bold yellow]",
|
|
36753
|
+
"[bold yellow]-[/bold yellow]",
|
|
36754
|
+
f"[bold yellow]{category}[/bold yellow]",
|
|
36755
|
+
)
|
|
36756
|
+
else:
|
|
36757
|
+
table.add_row(
|
|
36758
|
+
cursor,
|
|
36759
|
+
"○",
|
|
36760
|
+
tbl,
|
|
36761
|
+
str(col_count),
|
|
36762
|
+
"[dim]-[/dim]",
|
|
36763
|
+
category,
|
|
36764
|
+
)
|
|
36765
|
+
|
|
36766
|
+
console.print(table)
|
|
36095
36767
|
|
|
36096
|
-
# Show card preview if available
|
|
36097
|
-
if has_cards:
|
|
36098
|
-
click.echo(click.style(" 💳 PAYMENT DATA PREVIEW", bold=True, fg="red"))
|
|
36099
36768
|
click.echo()
|
|
36100
36769
|
|
|
36101
|
-
for table_key, data in all_dumped.items():
|
|
36102
|
-
if any(x in table_key.lower() for x in ["card", "payment", "credit"]):
|
|
36103
|
-
rows = data.get("rows", [])
|
|
36104
|
-
columns = data.get("columns", [])
|
|
36105
36770
|
|
|
36106
|
-
|
|
36107
|
-
|
|
36108
|
-
|
|
36109
|
-
|
|
36110
|
-
|
|
36111
|
-
if any(x in c.lower() for x in ["name", "holder", "fullname"])
|
|
36112
|
-
),
|
|
36113
|
-
None,
|
|
36114
|
-
)
|
|
36115
|
-
card_col = next(
|
|
36116
|
-
(
|
|
36117
|
-
c
|
|
36118
|
-
for c in columns
|
|
36119
|
-
if any(
|
|
36120
|
-
x in c.lower()
|
|
36121
|
-
for x in ["cardnum", "card_number", "ccnumber", "number"]
|
|
36122
|
-
)
|
|
36123
|
-
),
|
|
36124
|
-
None,
|
|
36125
|
-
)
|
|
36126
|
-
exp_col = next(
|
|
36127
|
-
(
|
|
36128
|
-
c
|
|
36129
|
-
for c in columns
|
|
36130
|
-
if any(x in c.lower() for x in ["exp", "expir"])
|
|
36131
|
-
),
|
|
36132
|
-
None,
|
|
36133
|
-
)
|
|
36771
|
+
def _sqli_detail_overview(
|
|
36772
|
+
inj, all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
|
|
36773
|
+
):
|
|
36774
|
+
"""Show overview of SQLi exploitation results with database structure."""
|
|
36775
|
+
from rich.table import Table
|
|
36134
36776
|
|
|
36135
|
-
|
|
36136
|
-
|
|
36137
|
-
|
|
36138
|
-
|
|
36139
|
-
|
|
36140
|
-
|
|
36141
|
-
|
|
36142
|
-
|
|
36143
|
-
|
|
36144
|
-
|
|
36145
|
-
|
|
36146
|
-
|
|
36147
|
-
|
|
36148
|
-
|
|
36149
|
-
|
|
36150
|
-
|
|
36151
|
-
|
|
36152
|
-
|
|
36153
|
-
|
|
36154
|
-
|
|
36777
|
+
# Count extracted data by category
|
|
36778
|
+
cred_count = 0
|
|
36779
|
+
card_count = 0
|
|
36780
|
+
pii_count = 0
|
|
36781
|
+
|
|
36782
|
+
for key, data in all_dumped.items():
|
|
36783
|
+
row_count = data.get("row_count", len(data.get("rows", [])))
|
|
36784
|
+
key_lower = key.lower()
|
|
36785
|
+
if any(x in key_lower for x in ["user", "account", "login", "credential"]):
|
|
36786
|
+
cred_count += row_count
|
|
36787
|
+
elif any(x in key_lower for x in ["card", "payment", "credit"]):
|
|
36788
|
+
card_count += row_count
|
|
36789
|
+
elif any(
|
|
36790
|
+
x in key_lower for x in ["address", "personal", "customer", "profile"]
|
|
36791
|
+
):
|
|
36792
|
+
pii_count += row_count
|
|
36793
|
+
|
|
36794
|
+
# Summary alert if data extracted
|
|
36795
|
+
if cred_count > 0 or card_count > 0 or pii_count > 0:
|
|
36796
|
+
alerts = []
|
|
36797
|
+
if cred_count > 0:
|
|
36798
|
+
alerts.append(f"🔑 {cred_count} credentials")
|
|
36799
|
+
if card_count > 0:
|
|
36800
|
+
alerts.append(f"💳 {card_count} cards")
|
|
36801
|
+
if pii_count > 0:
|
|
36802
|
+
alerts.append(f"👤 {pii_count} PII records")
|
|
36803
|
+
click.echo(
|
|
36804
|
+
click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
|
|
36805
|
+
+ click.style(" | ".join(alerts), fg="red")
|
|
36806
|
+
)
|
|
36807
|
+
click.echo()
|
|
36155
36808
|
|
|
36156
|
-
|
|
36809
|
+
# Database structure
|
|
36810
|
+
if not all_databases and not all_tables:
|
|
36811
|
+
click.echo(" No database structure discovered yet.")
|
|
36812
|
+
click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
|
|
36813
|
+
return
|
|
36157
36814
|
|
|
36158
|
-
|
|
36159
|
-
|
|
36160
|
-
|
|
36161
|
-
|
|
36815
|
+
# Show database structure with Rich table (like Host Management)
|
|
36816
|
+
for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
|
|
36817
|
+
dbms_type = db_to_dbms.get(db, "SQLite")
|
|
36818
|
+
tables = all_tables.get(db, set())
|
|
36162
36819
|
|
|
36163
|
-
|
|
36164
|
-
if has_pii:
|
|
36165
|
-
click.echo(click.style(" 👤 PERSONAL DATA PREVIEW", bold=True, fg="yellow"))
|
|
36820
|
+
click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
|
|
36166
36821
|
click.echo()
|
|
36167
36822
|
|
|
36168
|
-
|
|
36169
|
-
|
|
36170
|
-
|
|
36171
|
-
|
|
36172
|
-
|
|
36173
|
-
|
|
36174
|
-
|
|
36823
|
+
if tables:
|
|
36824
|
+
# Create Rich table like Host Management
|
|
36825
|
+
table = Table(
|
|
36826
|
+
show_header=True,
|
|
36827
|
+
header_style="bold cyan",
|
|
36828
|
+
box=DesignSystem.TABLE_BOX,
|
|
36829
|
+
padding=(0, 1),
|
|
36830
|
+
expand=True,
|
|
36831
|
+
)
|
|
36175
36832
|
|
|
36176
|
-
|
|
36177
|
-
|
|
36178
|
-
|
|
36179
|
-
|
|
36180
|
-
|
|
36181
|
-
if any(
|
|
36182
|
-
x in c.lower() for x in ["name", "fullname", "full_name"]
|
|
36183
|
-
)
|
|
36184
|
-
),
|
|
36185
|
-
None,
|
|
36186
|
-
)
|
|
36187
|
-
city_col = next((c for c in columns if "city" in c.lower()), None)
|
|
36188
|
-
country_col = next((c for c in columns if "country" in c.lower()), None)
|
|
36189
|
-
address_col = next(
|
|
36190
|
-
(
|
|
36191
|
-
c
|
|
36192
|
-
for c in columns
|
|
36193
|
-
if any(x in c.lower() for x in ["address", "street"])
|
|
36194
|
-
),
|
|
36195
|
-
None,
|
|
36196
|
-
)
|
|
36197
|
-
phone_col = next(
|
|
36198
|
-
(
|
|
36199
|
-
c
|
|
36200
|
-
for c in columns
|
|
36201
|
-
if any(x in c.lower() for x in ["phone", "mobile", "tel"])
|
|
36202
|
-
),
|
|
36203
|
-
None,
|
|
36204
|
-
)
|
|
36205
|
-
zip_col = next(
|
|
36206
|
-
(
|
|
36207
|
-
c
|
|
36208
|
-
for c in columns
|
|
36209
|
-
if any(x in c.lower() for x in ["zip", "postal"])
|
|
36210
|
-
),
|
|
36211
|
-
None,
|
|
36212
|
-
)
|
|
36833
|
+
table.add_column("○", width=5, justify="center", no_wrap=True)
|
|
36834
|
+
table.add_column("Table Name", width=28, no_wrap=True)
|
|
36835
|
+
table.add_column("Cols", width=6, justify="right", no_wrap=True)
|
|
36836
|
+
table.add_column("Rows", width=12, justify="right", no_wrap=True)
|
|
36837
|
+
table.add_column("Type", width=12, no_wrap=True)
|
|
36213
36838
|
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36217
|
-
|
|
36218
|
-
padding=(0, 2),
|
|
36219
|
-
)
|
|
36220
|
-
if name_col:
|
|
36221
|
-
table.add_column("Name", style="yellow")
|
|
36222
|
-
if address_col:
|
|
36223
|
-
table.add_column("Address", style="white")
|
|
36224
|
-
if city_col:
|
|
36225
|
-
table.add_column("City", style="cyan")
|
|
36226
|
-
if country_col:
|
|
36227
|
-
table.add_column("Country", style="cyan")
|
|
36228
|
-
if zip_col:
|
|
36229
|
-
table.add_column("ZIP", style="dim")
|
|
36230
|
-
if phone_col:
|
|
36231
|
-
table.add_column("Phone", style="green")
|
|
36232
|
-
|
|
36233
|
-
for row in rows[:5]:
|
|
36234
|
-
row_data = []
|
|
36235
|
-
if name_col:
|
|
36236
|
-
row_data.append(str(row.get(name_col, "-"))[:25])
|
|
36237
|
-
if address_col:
|
|
36238
|
-
row_data.append(str(row.get(address_col, "-"))[:30])
|
|
36239
|
-
if city_col:
|
|
36240
|
-
row_data.append(str(row.get(city_col, "-"))[:15])
|
|
36241
|
-
if country_col:
|
|
36242
|
-
row_data.append(str(row.get(country_col, "-"))[:15])
|
|
36243
|
-
if zip_col:
|
|
36244
|
-
row_data.append(str(row.get(zip_col, "-"))[:10])
|
|
36245
|
-
if phone_col:
|
|
36246
|
-
row_data.append(str(row.get(phone_col, "-"))[:15])
|
|
36247
|
-
if row_data:
|
|
36248
|
-
table.add_row(*row_data)
|
|
36839
|
+
sorted_tables = sorted(tables)
|
|
36840
|
+
for tbl in sorted_tables:
|
|
36841
|
+
table_key = f"{db}.{tbl}"
|
|
36842
|
+
cols = all_columns.get(table_key, set())
|
|
36249
36843
|
|
|
36250
|
-
|
|
36844
|
+
# Check if this table has been dumped
|
|
36845
|
+
dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
|
|
36846
|
+
has_data = dumped_key is not None
|
|
36251
36847
|
|
|
36252
|
-
|
|
36253
|
-
|
|
36254
|
-
|
|
36255
|
-
|
|
36848
|
+
# Determine category
|
|
36849
|
+
tbl_lower = tbl.lower()
|
|
36850
|
+
if any(
|
|
36851
|
+
x in tbl_lower
|
|
36852
|
+
for x in ["user", "account", "login", "credential", "password"]
|
|
36853
|
+
):
|
|
36854
|
+
category = "🔑 Creds"
|
|
36855
|
+
elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
|
|
36856
|
+
category = "💳 Cards"
|
|
36857
|
+
elif any(
|
|
36858
|
+
x in tbl_lower
|
|
36859
|
+
for x in ["address", "personal", "customer", "profile"]
|
|
36860
|
+
):
|
|
36861
|
+
category = "👤 PII"
|
|
36862
|
+
else:
|
|
36863
|
+
category = ""
|
|
36864
|
+
|
|
36865
|
+
col_count = len(cols) if cols else 0
|
|
36866
|
+
|
|
36867
|
+
if has_data:
|
|
36868
|
+
row_count = all_dumped[dumped_key].get(
|
|
36869
|
+
"row_count", len(all_dumped[dumped_key].get("rows", []))
|
|
36870
|
+
)
|
|
36871
|
+
table.add_row(
|
|
36872
|
+
"○",
|
|
36873
|
+
f"[green bold]{tbl}[/green bold]",
|
|
36874
|
+
str(col_count),
|
|
36875
|
+
f"[green]✓ {row_count}[/green]",
|
|
36876
|
+
category,
|
|
36877
|
+
)
|
|
36878
|
+
else:
|
|
36879
|
+
table.add_row(
|
|
36880
|
+
"○",
|
|
36881
|
+
tbl,
|
|
36882
|
+
str(col_count),
|
|
36883
|
+
"[dim]-[/dim]",
|
|
36884
|
+
category,
|
|
36885
|
+
)
|
|
36886
|
+
|
|
36887
|
+
console.print(table)
|
|
36888
|
+
|
|
36889
|
+
click.echo()
|
|
36256
36890
|
|
|
36257
36891
|
|
|
36258
36892
|
def _sqli_detail_tables(
|
|
36259
36893
|
all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
|
|
36260
36894
|
):
|
|
36261
36895
|
"""Show database structure overview."""
|
|
36262
|
-
from rich.
|
|
36263
|
-
from rich.tree import Tree
|
|
36896
|
+
from rich.table import Table
|
|
36264
36897
|
|
|
36265
36898
|
click.echo(click.style(" 📁 DATABASE STRUCTURE", bold=True, fg="cyan"))
|
|
36266
36899
|
click.echo()
|
|
@@ -36270,14 +36903,29 @@ def _sqli_detail_tables(
|
|
|
36270
36903
|
click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
|
|
36271
36904
|
return
|
|
36272
36905
|
|
|
36273
|
-
# Build tree structure
|
|
36274
36906
|
for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
|
|
36275
36907
|
dbms_type = db_to_dbms.get(db, "SQLite")
|
|
36276
36908
|
tables = all_tables.get(db, set())
|
|
36277
36909
|
|
|
36278
36910
|
click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
|
|
36911
|
+
click.echo()
|
|
36279
36912
|
|
|
36280
36913
|
if tables:
|
|
36914
|
+
# Create Rich table like Host Management
|
|
36915
|
+
table = Table(
|
|
36916
|
+
show_header=True,
|
|
36917
|
+
header_style="bold cyan",
|
|
36918
|
+
box=DesignSystem.TABLE_BOX,
|
|
36919
|
+
padding=(0, 1),
|
|
36920
|
+
expand=True,
|
|
36921
|
+
)
|
|
36922
|
+
|
|
36923
|
+
table.add_column("○", width=5, justify="center", no_wrap=True)
|
|
36924
|
+
table.add_column("Table Name", width=28, no_wrap=True)
|
|
36925
|
+
table.add_column("Cols", width=6, justify="right", no_wrap=True)
|
|
36926
|
+
table.add_column("Rows", width=10, justify="right", no_wrap=True)
|
|
36927
|
+
table.add_column("Type", width=12, no_wrap=True)
|
|
36928
|
+
|
|
36281
36929
|
sorted_tables = sorted(tables)
|
|
36282
36930
|
for tbl in sorted_tables:
|
|
36283
36931
|
table_key = f"{db}.{tbl}"
|
|
@@ -36287,55 +36935,74 @@ def _sqli_detail_tables(
|
|
|
36287
36935
|
dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
|
|
36288
36936
|
has_data = dumped_key is not None
|
|
36289
36937
|
|
|
36290
|
-
#
|
|
36291
|
-
|
|
36292
|
-
|
|
36293
|
-
|
|
36294
|
-
|
|
36295
|
-
|
|
36296
|
-
|
|
36297
|
-
|
|
36298
|
-
|
|
36299
|
-
|
|
36300
|
-
|
|
36301
|
-
|
|
36302
|
-
|
|
36303
|
-
|
|
36938
|
+
# Determine category
|
|
36939
|
+
tbl_lower = tbl.lower()
|
|
36940
|
+
if any(
|
|
36941
|
+
x in tbl_lower
|
|
36942
|
+
for x in ["user", "account", "login", "credential", "password"]
|
|
36943
|
+
):
|
|
36944
|
+
category = "🔑 Creds"
|
|
36945
|
+
elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
|
|
36946
|
+
category = "💳 Cards"
|
|
36947
|
+
elif any(
|
|
36948
|
+
x in tbl_lower
|
|
36949
|
+
for x in ["address", "personal", "customer", "profile"]
|
|
36950
|
+
):
|
|
36951
|
+
category = "👤 PII"
|
|
36952
|
+
else:
|
|
36953
|
+
category = ""
|
|
36954
|
+
|
|
36955
|
+
col_count = len(cols) if cols else 0
|
|
36304
36956
|
|
|
36305
36957
|
if has_data:
|
|
36306
36958
|
row_count = all_dumped[dumped_key].get("row_count", 0)
|
|
36307
|
-
|
|
36308
|
-
|
|
36309
|
-
|
|
36310
|
-
|
|
36311
|
-
|
|
36312
|
-
|
|
36313
|
-
f" ├─ {click.style(tbl, fg='red', bold=True)} ({len(cols)} cols) {status}"
|
|
36959
|
+
table.add_row(
|
|
36960
|
+
"○",
|
|
36961
|
+
f"[green bold]{tbl}[/green bold]",
|
|
36962
|
+
str(col_count),
|
|
36963
|
+
f"[green]✓ {row_count}[/green]",
|
|
36964
|
+
category,
|
|
36314
36965
|
)
|
|
36315
36966
|
else:
|
|
36316
|
-
|
|
36967
|
+
table.add_row(
|
|
36968
|
+
"○",
|
|
36969
|
+
tbl,
|
|
36970
|
+
str(col_count),
|
|
36971
|
+
"[dim]-[/dim]",
|
|
36972
|
+
category,
|
|
36973
|
+
)
|
|
36317
36974
|
|
|
36318
|
-
|
|
36319
|
-
if has_data and cols:
|
|
36320
|
-
col_list = sorted(cols)[:8]
|
|
36321
|
-
col_str = ", ".join(col_list)
|
|
36322
|
-
if len(cols) > 8:
|
|
36323
|
-
col_str += f" +{len(cols) - 8} more"
|
|
36324
|
-
click.echo(f" │ └─ {click.style(col_str, dim=True)}")
|
|
36975
|
+
console.print(table)
|
|
36325
36976
|
|
|
36326
36977
|
click.echo()
|
|
36327
36978
|
|
|
36328
36979
|
|
|
36329
|
-
def _sqli_detail_data_table(
|
|
36330
|
-
|
|
36980
|
+
def _sqli_detail_data_table(
|
|
36981
|
+
table_key: str,
|
|
36982
|
+
data: dict,
|
|
36983
|
+
console,
|
|
36984
|
+
width: int,
|
|
36985
|
+
page: int = 1,
|
|
36986
|
+
page_size: int = 20,
|
|
36987
|
+
view_all: bool = False,
|
|
36988
|
+
):
|
|
36989
|
+
"""Show full data for a specific table with pagination."""
|
|
36331
36990
|
from rich.table import Table
|
|
36332
36991
|
|
|
36333
36992
|
rows = data.get("rows", [])
|
|
36334
36993
|
columns = data.get("columns", [])
|
|
36335
36994
|
row_count = data.get("row_count", len(rows))
|
|
36995
|
+
total_pages = (len(rows) + page_size - 1) // page_size if not view_all else 1
|
|
36336
36996
|
|
|
36337
36997
|
click.echo(click.style(f" 📋 TABLE: {table_key}", bold=True, fg="green"))
|
|
36338
|
-
|
|
36998
|
+
if view_all:
|
|
36999
|
+
click.echo(
|
|
37000
|
+
f" {row_count} records | {len(columns)} columns | Showing ALL"
|
|
37001
|
+
)
|
|
37002
|
+
else:
|
|
37003
|
+
click.echo(
|
|
37004
|
+
f" {row_count} records | {len(columns)} columns | Page {page}/{total_pages}"
|
|
37005
|
+
)
|
|
36339
37006
|
click.echo()
|
|
36340
37007
|
|
|
36341
37008
|
if not rows:
|
|
@@ -36398,8 +37065,14 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
|
|
|
36398
37065
|
else:
|
|
36399
37066
|
table.add_column(col, max_width=col_width, overflow="ellipsis")
|
|
36400
37067
|
|
|
36401
|
-
# Add rows
|
|
36402
|
-
|
|
37068
|
+
# Add rows with pagination
|
|
37069
|
+
if view_all:
|
|
37070
|
+
display_rows = rows
|
|
37071
|
+
else:
|
|
37072
|
+
start_idx = (page - 1) * page_size
|
|
37073
|
+
end_idx = start_idx + page_size
|
|
37074
|
+
display_rows = rows[start_idx:end_idx]
|
|
37075
|
+
|
|
36403
37076
|
for row in display_rows:
|
|
36404
37077
|
row_data = []
|
|
36405
37078
|
for col in display_cols:
|
|
@@ -36412,9 +37085,11 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
|
|
|
36412
37085
|
|
|
36413
37086
|
console.print(table)
|
|
36414
37087
|
|
|
36415
|
-
if len(rows) >
|
|
37088
|
+
if not view_all and len(rows) > page_size:
|
|
37089
|
+
start_idx = (page - 1) * page_size
|
|
37090
|
+
end_idx = min(start_idx + page_size, len(rows))
|
|
36416
37091
|
click.echo()
|
|
36417
|
-
click.echo(f"
|
|
37092
|
+
click.echo(f" Showing rows {start_idx + 1}-{end_idx} of {len(rows)}")
|
|
36418
37093
|
|
|
36419
37094
|
# Show hidden columns
|
|
36420
37095
|
if len(columns) > max_cols:
|
|
@@ -40337,6 +41012,30 @@ def _launch_interactive_msfconsole(engagement_id: int):
|
|
|
40337
41012
|
rc_path = rc.name
|
|
40338
41013
|
|
|
40339
41014
|
try:
|
|
41015
|
+
# Auto-copy database.yml to /root/.msf4/ if needed
|
|
41016
|
+
# When msfdb init runs as normal user, config is in ~/.msf4/
|
|
41017
|
+
# But sudo msfconsole looks in /root/.msf4/, so we need to copy it
|
|
41018
|
+
from pathlib import Path
|
|
41019
|
+
|
|
41020
|
+
user_db = Path.home() / ".msf4" / "database.yml"
|
|
41021
|
+
root_db = Path("/root/.msf4/database.yml")
|
|
41022
|
+
if user_db.exists():
|
|
41023
|
+
# Check if root's config exists (use sudo to check)
|
|
41024
|
+
check_result = subprocess.run(
|
|
41025
|
+
["sudo", "test", "-f", str(root_db)], capture_output=True
|
|
41026
|
+
)
|
|
41027
|
+
if check_result.returncode != 0:
|
|
41028
|
+
# Root's config doesn't exist, copy it
|
|
41029
|
+
click.echo(
|
|
41030
|
+
click.style(" Configuring database for sudo access...", fg="cyan")
|
|
41031
|
+
)
|
|
41032
|
+
subprocess.run(
|
|
41033
|
+
["sudo", "mkdir", "-p", "/root/.msf4"], capture_output=True
|
|
41034
|
+
)
|
|
41035
|
+
subprocess.run(
|
|
41036
|
+
["sudo", "cp", str(user_db), str(root_db)], capture_output=True
|
|
41037
|
+
)
|
|
41038
|
+
|
|
40340
41039
|
click.pause("Press Enter to launch msfconsole...")
|
|
40341
41040
|
|
|
40342
41041
|
# Launch msfconsole with our resource script
|
|
@@ -40490,18 +41189,27 @@ def _ensure_msf_database_ready():
|
|
|
40490
41189
|
click.style(" MSF database needs initialization...", fg="yellow")
|
|
40491
41190
|
)
|
|
40492
41191
|
|
|
40493
|
-
# Initialize msfdb
|
|
41192
|
+
# Initialize msfdb - distro-aware (Kali needs sudo, Ubuntu doesn't)
|
|
41193
|
+
from souleyez.utils.tool_checker import detect_distro
|
|
41194
|
+
|
|
41195
|
+
distro = detect_distro()
|
|
41196
|
+
use_sudo = distro in ("kali", "parrot")
|
|
41197
|
+
|
|
40494
41198
|
click.echo(" Running msfdb init (this may take a moment)...")
|
|
41199
|
+
msfdb_cmd = ["sudo", "msfdb", "init"] if use_sudo else ["msfdb", "init"]
|
|
40495
41200
|
result = subprocess.run(
|
|
40496
|
-
|
|
41201
|
+
msfdb_cmd, capture_output=True, text=True, timeout=120
|
|
40497
41202
|
)
|
|
40498
41203
|
if result.returncode == 0:
|
|
40499
41204
|
click.echo(click.style(" ✓ MSF database initialized", fg="green"))
|
|
40500
41205
|
else:
|
|
40501
41206
|
# Try msfdb reinit if init fails
|
|
40502
41207
|
click.echo(click.style(" Init failed, trying reinit...", fg="yellow"))
|
|
41208
|
+
msfdb_reinit_cmd = (
|
|
41209
|
+
["sudo", "msfdb", "reinit"] if use_sudo else ["msfdb", "reinit"]
|
|
41210
|
+
)
|
|
40503
41211
|
result = subprocess.run(
|
|
40504
|
-
|
|
41212
|
+
msfdb_reinit_cmd,
|
|
40505
41213
|
capture_output=True,
|
|
40506
41214
|
text=True,
|
|
40507
41215
|
timeout=120,
|
|
@@ -40740,7 +41448,14 @@ def _guided_msf_setup():
|
|
|
40740
41448
|
click.echo()
|
|
40741
41449
|
click.echo(" Please run these commands manually:")
|
|
40742
41450
|
click.echo(click.style(" sudo systemctl start postgresql", fg="cyan"))
|
|
40743
|
-
|
|
41451
|
+
# Show distro-appropriate msfdb command
|
|
41452
|
+
from souleyez.utils.tool_checker import detect_distro
|
|
41453
|
+
|
|
41454
|
+
distro = detect_distro()
|
|
41455
|
+
if distro in ("kali", "parrot"):
|
|
41456
|
+
click.echo(click.style(" sudo msfdb init", fg="cyan"))
|
|
41457
|
+
else:
|
|
41458
|
+
click.echo(click.style(" msfdb init", fg="cyan"))
|
|
40744
41459
|
click.echo()
|
|
40745
41460
|
if not click.confirm(" Continue anyway?", default=False):
|
|
40746
41461
|
click.pause()
|
|
@@ -43405,15 +44120,22 @@ def _check_msfdb_ready() -> bool:
|
|
|
43405
44120
|
" Without it, you won't be able to store hosts, credentials, or loot."
|
|
43406
44121
|
)
|
|
43407
44122
|
click.echo()
|
|
44123
|
+
|
|
44124
|
+
# Distro-aware msfdb init (Kali needs sudo, Ubuntu doesn't)
|
|
44125
|
+
from souleyez.utils.tool_checker import detect_distro
|
|
44126
|
+
|
|
44127
|
+
distro = detect_distro()
|
|
44128
|
+
use_sudo = distro in ("kali", "parrot")
|
|
44129
|
+
msfdb_cmd_str = "sudo msfdb init" if use_sudo else "msfdb init"
|
|
44130
|
+
msfdb_cmd = ["sudo", "msfdb", "init"] if use_sudo else ["msfdb", "init"]
|
|
44131
|
+
|
|
43408
44132
|
if click.confirm(
|
|
43409
|
-
" Initialize database now? (runs:
|
|
44133
|
+
f" Initialize database now? (runs: {msfdb_cmd_str})", default=True
|
|
43410
44134
|
):
|
|
43411
44135
|
click.echo()
|
|
43412
|
-
click.echo(click.style(" Running
|
|
44136
|
+
click.echo(click.style(f" Running {msfdb_cmd_str}...", fg="cyan"))
|
|
43413
44137
|
try:
|
|
43414
|
-
result = subprocess.run(
|
|
43415
|
-
["sudo", "msfdb", "init"], capture_output=False, text=True
|
|
43416
|
-
)
|
|
44138
|
+
result = subprocess.run(msfdb_cmd, capture_output=False, text=True)
|
|
43417
44139
|
if result.returncode == 0:
|
|
43418
44140
|
click.echo(
|
|
43419
44141
|
click.style(" Database initialized successfully!", fg="green")
|