diff --git a/catch-all/08_urlf4ck3r/.dockerignore b/catch-all/08_urlf4ck3r/.dockerignore new file mode 100644 index 0000000..f04aebd --- /dev/null +++ b/catch-all/08_urlf4ck3r/.dockerignore @@ -0,0 +1,7 @@ +README.md +docker-compose.yaml +docker-compose.yml +Dockerfile +.git +.gitignore +.vscode diff --git a/catch-all/08_urlf4ck3r/Dockerfile b/catch-all/08_urlf4ck3r/Dockerfile new file mode 100644 index 0000000..6c38ff2 --- /dev/null +++ b/catch-all/08_urlf4ck3r/Dockerfile @@ -0,0 +1,24 @@ +# Usa una imagen base de Python oficial +FROM python:3.9-slim +ARG maintaner="manuelver" + +# Establece el directorio de trabajo dentro del contenedor +WORKDIR /app + +# Copia los archivos necesarios al contenedor +COPY requirements.txt /app + +# Instala las dependencias del script +RUN pip install --no-cache-dir -r requirements.txt + +# Copia el script al contenedor +COPY urlf4ck3r.py /app + +# Da permisos de ejecución al script +RUN chmod +x urlf4ck3r.py + +# Define el comando predeterminado para ejecutar el script con argumentos +ENTRYPOINT ["./urlf4ck3r.py"] + +# Especifica el comando por defecto, que puede ser sobreescrito al correr el contenedor +CMD ["-h"] diff --git a/catch-all/08_urlf4ck3r/README.md b/catch-all/08_urlf4ck3r/README.md new file mode 100644 index 0000000..c1ad749 --- /dev/null +++ b/catch-all/08_urlf4ck3r/README.md @@ -0,0 +1,116 @@ +# 🕸️ URLf4ck3r 🕵️‍♂️ + +> Repositorio original: [URLf4ck3r](https://github.com/n0m3l4c000nt35/urlf4ck3r) + +URLf4ck3r es una herramienta de reconocimiento diseñada para escanear y extraer URLs del código fuente de sitios web. + +📝 **Tabla de contenidos** +- [🕸️ URLf4ck3r 🕵️‍♂️](#️-urlf4ck3r-️️) + - [🚀 Características principales](#-características-principales) + - [📋 Requisitos](#-requisitos) + - [🛠️ Instalación](#️-instalación) + - [💻 Uso](#-uso) + - [Con docker](#con-docker) + - [Construir la imagen de Docker](#construir-la-imagen-de-docker) + - [Ejecutar el contenedor](#ejecutar-el-contenedor) + - [Ejecutar con Docker Compose:](#ejecutar-con-docker-compose) + + +## 🚀 Características principales + +- 🔍 Escaneo recursivo de URLs +- 🌐 Detección de subdominios +- ✍️ Detección de palabras sensibles en los comentarios +- 🔗 Clasificación de URLs absolutas y relativas +- 💠 Detección de archivos JavaScript +- 🎨 Salida colorida para una fácil lectura +- ⏱️ Interrumpible en cualquier momento + + +## 📋 Requisitos + +- Python 3.x +- Bibliotecas: `requests`, `beautifulsoup4`, `pwntools` (Ver en el fichero [requirements.txt](requirements.txt)) + + +## 🛠️ Instalación + +1. Descarga esta carpeta +2. Instalá las dependencias: + +``` +pip install -r requirements.txt +``` + +3. Haz el script ejecutable: + +``` +chmod +x urlf4ck3r.py +``` + +4. Para ejecutar el script desde cualquier ubicación: + +- Mueve el script a un directorio que esté en el PATH, por ejemplo: + ``` + sudo mv urlf4ck3r.py /usr/bin/urlf4ck3r + ``` +- O añade el directorio del script al PATH editando el archivo `.bashrc` o `.zshrc`: + ``` + echo 'export PATH=$PATH:/ruta/al/directorio/de/urlf4ck3r' >> ~/.bashrc + source ~/.bashrc + ``` + + +## 💻 Uso + +Si seguiste el paso 4 de la instalación, puedes ejecutar el script desde cualquier ubicación simplemente con: + +``` +urlf4ck3r -u -o output.txt +``` + +De lo contrario, desde el directorio donde se encuentra ubicado el script: + +``` +./urlf4ck3r.py -u -o output +``` + +Ejemplo: + +``` +urlf4ck3r -u https://ejemplo.com -o output.txt +``` + +## Con docker + +### Construir la imagen de Docker + +Para construir la imagen desde el Dockerfile, navega al directorio donde se encuentra tu Dockerfile y ejecuta el siguiente comando: + +```sh +docker build -t urlf4ck3r . +``` + +### Ejecutar el contenedor + +Después de construir la imagen, puedes ejecutar tu script dentro de un contenedor de la siguiente manera: + +```sh +docker run --rm urlf4ck3r -u https://ejemplo.com -o output.txt +``` + +El flag --rm asegura que el contenedor se elimina automáticamente después de que se complete su ejecución. + + +### Ejecutar con Docker Compose: + +En la línea de comandos, navega hasta el directorio donde guardaste estos archivos. + +Ejecuta el siguiente comando para construir la imagen y ejecutar el contenedor usando Docker Compose: + +```sh +docker-compose up --build +``` + +Esto ejecutará urlf4ck3r y generará el archivo output.txt en el directorio ./output de tu máquina local. + diff --git a/catch-all/08_urlf4ck3r/docker-compose.yaml b/catch-all/08_urlf4ck3r/docker-compose.yaml new file mode 100644 index 0000000..8dd055d --- /dev/null +++ b/catch-all/08_urlf4ck3r/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + urlf4ck3r: + build: + context: . + dockerfile: Dockerfile + command: ["-u", "https://vergaracarmona.es", "-o", "output.txt"] + volumes: + - ./output:/app/output + container_name: urlf4ck3r \ No newline at end of file diff --git a/catch-all/08_urlf4ck3r/requirements.txt b/catch-all/08_urlf4ck3r/requirements.txt new file mode 100644 index 0000000..0931024 --- /dev/null +++ b/catch-all/08_urlf4ck3r/requirements.txt @@ -0,0 +1,3 @@ +beautifulsoup4==4.12.3 +pwntools==4.13.0 +Requests==2.32.3 diff --git a/catch-all/08_urlf4ck3r/urlf4ck3r.py b/catch-all/08_urlf4ck3r/urlf4ck3r.py new file mode 100644 index 0000000..7b6a268 --- /dev/null +++ b/catch-all/08_urlf4ck3r/urlf4ck3r.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 + +import argparse +import os +import requests +import signal +import sys + +from bs4 import BeautifulSoup, Comment +from collections import defaultdict +from urllib.parse import urljoin, urlparse +from typing import Optional, Tuple, Dict, List, Set + + +class URLf4ck3r: + """ + URLf4ck3r es una herramienta que extrae las URL's del código fuente de una + página web. Además, puede extraer comentarios sensibles del código fuente + y guardar las URL's en archivos de texto. + """ + + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + GRAY = "\033[90m" + PURPLE = "\033[95m" + END_COLOR = "\033[0m" + + SENSITIVE_KEYWORDS = [ + # Palabras clave originales + "password", "user", "username", "clave", "secret", "key", "token", + "private", "admin", "credential", "login", "auth", "api_key", "apikey", + "administrator", + + # # Criptografía y Seguridad + # "encryption", "decrypt", "cipher", "security", "hash", "salt", "ssl", + # "tls", "secure", "firewall", "integrity", + + # # Gestión de Usuarios y Autenticación + # "auth_token", "session_id", "access_token", "oauth", "id_token", + # "refresh_token", "csrf", "sso", "two_factor", "2fa", + + # # Información Personal Identificable (PII) + # "social_security", "ssn", "address", "phone_number", "email", "dob", + # "credit_card", "card_number", "ccv", "passport", "tax_id", "personal_info", + + # # Configuración de Sistemas + # "config", "database", "db_password", "db_user", "connection_string", + # "server", "host", "port", + + # # Archivos y Rutas + # "filepath", "filename", "root_path", "home_dir", "backup", "logfile", + + # # Llaves y Tokens de API + # "aws_secret", "aws_key", "api_secret", "secret_key", "private_key", + # "public_key", "ssh_key", + + # # Finanzas y Pagos + # "payment", "transaction", "account_number", "iban", "bic", "swift", + # "bank", "routing_number", "billing", "invoice", + + # # Cuentas y Roles de Administrador + # "superuser", "root", "sudo", "admin_password", "admin_user", + + # # Otros + # "jwt", "cookie", "session", "bypass", "debug", "exploit" + ] + + + def __init__(self): + """ + Inicializa las variables de instancia. + """ + + self.all_urls: Dict[str, Set[str]] = defaultdict(set) + self.comments_data: Dict[str, List[str]] = defaultdict(list) + self.base_url: Optional[str] = None + self.urls_to_scan: List[str] = [] + self.flag = self.Killer() + self.output: Optional[str] = None + + + def banner(self) -> None: + """ + Muestra el banner de la herramienta. + """ + + print(""" + + █ ██ ██▀███ ██▓ █████▒▄████▄ ██ ▄█▀ ██▀███ + ██ ▓██▒▓██ ▒ ██▒▓██▒ ▓██ ▒▒██▀ ▀█ ██▄█▒ ▓██ ▒ ██▒ +▓██ ▒██░▓██ ░▄█ ▒▒██░ ▒████ ░▒▓█ ▄ ▓███▄░ ▓██ ░▄█ ▒ +▓▓█ ░██░▒██▀▀█▄ ▒██░ ░▓█▒ ░▒▓▓▄ ▄██▒▓██ █▄ ▒██▀▀█▄ +▒▒█████▓ ░██▓ ▒██▒░██████▒░▒█░ ▒ ▓███▀ ░▒██▒ █▄░██▓ ▒██▒ +░▒▓▒ ▒ ▒ ░ ▒▓ ░▒▓░░ ▒░▓ ░ ▒ ░ ░ ░▒ ▒ ░▒ ▒▒ ▓▒░ ▒▓ ░▒▓░ +░░▒░ ░ ░ ░▒ ░ ▒░░ ░ ▒ ░ ░ ░ ▒ ░ ░▒ ▒░ ░▒ ░ ▒░ + ░░░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ░░ ░ + ░ ░ ░ ░ ░ ░ ░ ░ ░ + ░ +""") + + + def run(self) -> None: + """ + Ejecuta la herramienta. + """ + + self.banner() + + args, parser = self.get_arguments() + + if not args.url: + parser.print_help() + sys.exit(1) + + if args.output: + self.output = args.output + + self.base_url = args.url + self.all_urls["scanned_urls"] = set() + self.urls_to_scan = [self.base_url] + + _, domain, _ = self.parse_url(self.base_url) + + print(f"\n[{self.GREEN}DOMAIN{self.END_COLOR}] {domain}\n") + + while self.urls_to_scan and not self.flag.exit(): + url = self.urls_to_scan.pop(0) + self.scan_url(url) + + print() + self.show_lists() + self.save_files() + + print(f"\n[{self.GREEN}URLS TO SCAN{self.END_COLOR}]:") + + if self.flag.exit(): + print( + f"[{self.RED}!{self.END_COLOR}] Quedaron por escanear {self.RED}{len(self.urls_to_scan)}{self.END_COLOR} URLs" + ) + + elif not self.urls_to_scan: + print( + f"[{self.GREEN}+{self.END_COLOR}] Se escanearon todas las URLs posibles" + ) + else: + print( + f"[{self.RED}!{self.END_COLOR}] Quedaron por escanear {self.RED}{len(self.urls_to_scan)}{self.END_COLOR} URLs" + ) + + + def get_arguments(self) -> Tuple[argparse.Namespace, argparse.ArgumentParser]: + """ + Obtiene los argumentos proporcionados por el usuario. + """ + + parser = argparse.ArgumentParser( + prog="urlf4ck3r", + description="Extraer las URL's del código fuente de una web", + epilog="Creado por https://github.com/n0m3l4c000nt35 y modificado por gitea.vergaracarmona.es/manuelver" + ) + parser.add_argument("-u", "--url", type=str, dest="url", + help="URL a escanear", required=True) + parser.add_argument("-o", "--output", type=str, + dest="output", help="Nombre del archivo de salida") + + return parser.parse_args(), parser + + def scan_url(self, url: str) -> None: + """ + Escanea una URL en busca de URLs, comentarios sensibles y archivos JS. + """ + + if self.flag.exit(): + return + + if url in self.all_urls["scanned_urls"]: + return + + self.all_urls["scanned_urls"].add(url) + print(f"[{self.GREEN}SCANNING{self.END_COLOR}] {url}") + + try: + res = requests.get(url, timeout=5) + soup = BeautifulSoup(res.content, 'html.parser') + + self.extract_js_files(soup, url) + self.extract_comments(soup, url) + self.extract_hrefs(soup, url, res) + + except requests.Timeout: + print(f"[{self.RED}REQUEST TIMEOUT{self.END_COLOR}] {url}") + self.all_urls['request_error'].add(url) + + except requests.exceptions.RequestException: + print(f"{self.RED}[REQUEST ERROR]{self.END_COLOR} {url}") + self.all_urls['request_error'].add(url) + + except Exception as e: + print( + f"[{self.RED}UNEXPECTED ERROR{self.END_COLOR}] {url}: {str(e)}" + ) + + + def extract_hrefs(self, soup: BeautifulSoup, url: str, res: requests.Response) -> None: + """ + Extrae las URL's del código fuente de una página web. + """ + + for link in soup.find_all("a", href=True): + href = link.get("href") + scheme, domain, path = self.parse_url(href) + schemes = ["http", "https"] + + if href: + full_url = urljoin(url, path) if not scheme else href + + if full_url not in self.all_urls["all_urls"]: + self.all_urls["all_urls"].add(full_url) + + if not scheme: + self.all_urls["relative_urls"].add(full_url) + + else: + self.all_urls["absolute_urls"].add(full_url) + + if self.is_jsfile(url, res): + self.all_urls["javascript_files"].add(url) + + if (self.is_internal_url(self.base_url, full_url) or + self.is_subdomain(self.base_url, full_url)): + + if full_url not in self.all_urls["scanned_urls"] and full_url not in self.urls_to_scan: + self.urls_to_scan.append(full_url) + + + def extract_js_files(self, soup: BeautifulSoup, base_url: str) -> None: + """ + Extrae los archivos JS del código fuente de una página web. + """ + + js_files = set() + + for script in soup.find_all('script', src=True): + + js_url = script['src'] + + if not urlparse(js_url).netloc: + + js_url = urljoin(base_url, js_url) + + js_files.add(js_url) + + self.all_urls["javascript_files"].update(js_files) + + + def is_jsfile(self, url: str, res: requests.Response) -> bool: + """ + Verifica si un archivo es un archivo JS. + """ + + return url.lower().endswith(('.js', '.mjs')) or 'javascript' in res.headers.get('Content-Type', '').lower() + + + def extract_subdomain(self, url: str) -> str: + """ + Extrae el subdominio de una URL. + """ + + netloc = urlparse(url).netloc.split(".") + + return ".".join(netloc[1:] if netloc[0] == "www" else netloc) + + + def is_subdomain(self, base_url: str, url: str) -> bool: + """ + Verifica si una URL es un subdominio del dominio base. + """ + + base_domain = self.extract_subdomain(base_url) + sub = self.extract_subdomain(url) + + return sub.endswith(base_domain) and sub != base_domain + + + def is_internal_url(self, base_url: str, url: str) -> bool: + """ + Verifica si una URL es interna (pertenece al mismo dominio). + """ + + return urlparse(base_url).netloc == urlparse(url).netloc + + + def extract_comments(self, soup: BeautifulSoup, url: str) -> None: + """ + Extrae los comentarios del código fuente de una página web. + """ + + comments = soup.find_all(string=lambda text: isinstance(text, Comment)) + + for comment in comments: + + comment_str = comment.strip() + + if any(keyword in comment_str.lower() for keyword in self.SENSITIVE_KEYWORDS): + self.comments_data[url].append(comment_str) + print( + f"{self.YELLOW}[SENSITIVE COMMENT FOUND]{self.END_COLOR} {comment_str}" + ) + + + def parse_url(self, url: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: + """ + Parsea una URL y devuelve el esquema, dominio y path. + """ + + parsed_url = urlparse(url) + + return parsed_url.scheme, parsed_url.netloc, parsed_url.path + + + + def ensure_directory_exists(self, directory: str) -> None: + """ + Asegura que el directorio existe, y lo crea si no es así. + """ + + if not os.path.exists(directory): + os.makedirs(directory) + + + def save_file(self, data: List[str], filename: str) -> None: + """ + Guarda los datos en un archivo. + """ + + try: + # Asegurarse de que el directorio 'output' existe + self.ensure_directory_exists("output") + + if self.output: + filename = f"{self.output}_{filename}" + + filepath = os.path.join("output", filename) + + with open(filepath, "w") as f: + f.write("\n".join(data)) + + print(f"[{self.GREEN}+{self.END_COLOR}] Guardado en {filepath}") + + except IOError as e: + print( + f"{self.RED}[FILE WRITE ERROR]{self.END_COLOR} No se pudo guardar el archivo {filename}: {str(e)}" + ) + + + def save_files(self) -> None: + """ + Guarda las URLs y los comentarios extraídos en archivos. + """ + self.save_file( + sorted(self.all_urls["all_urls"]), + "all_urls.txt" + ) + + self.save_file( + sorted(self.all_urls["absolute_urls"]), + "absolute_urls.txt" + ) + + self.save_file( + sorted(self.all_urls["relative_urls"]), + "relative_urls.txt" + ) + + self.save_file( + sorted(self.all_urls["javascript_files"]), + "javascript_files.txt" + ) + + if self.comments_data: + sensitive_comments = [] + + for url, comments in self.comments_data.items(): + sensitive_comments.append(f"\n[ {url} ]\n") + sensitive_comments.extend(comments) + + self.save_file(sensitive_comments, "sensitive_comments.txt") + + + def show_lists(self) -> None: + """ + Muestra el resumen de las URLs extraídas. + """ + + print( + f"\n[{self.GREEN}ALL URLS{self.END_COLOR}]: {len(self.all_urls['all_urls'])}" + ) + print( + f"[{self.GREEN}ABSOLUTE URLS{self.END_COLOR}]: {len(self.all_urls['absolute_urls'])}" + ) + print( + f"[{self.GREEN}RELATIVE URLS{self.END_COLOR}]: {len(self.all_urls['relative_urls'])}" + ) + print( + f"[{self.GREEN}JAVASCRIPT FILES{self.END_COLOR}]: {len(self.all_urls['javascript_files'])}" + ) + print( + f"[{self.GREEN}SENSITIVE COMMENTS{self.END_COLOR}]: {len(self.comments_data)}" + ) + + class Killer: + """ + Clase utilizada para manejar la interrupción del script con Ctrl+C. + """ + + kill_now = False + + + def __init__(self): + + signal.signal(signal.SIGINT, self.exit_gracefully) + + + def exit_gracefully(self, signum, frame) -> None: + """ + Método llamado cuando se recibe la señal de interrupción. + """ + + self.kill_now = True + + def exit(self) -> bool: + """ + Retorna True si el script debe terminar. + """ + + return self.kill_now + + +if __name__ == "__main__": + + tool = URLf4ck3r() + tool.run() diff --git a/catch-all/README.md b/catch-all/README.md index 29bf992..a60948a 100644 --- a/catch-all/README.md +++ b/catch-all/README.md @@ -6,12 +6,13 @@ Aquí iré dejando scripts y ejercicios que se me ocurran, con lo que no hay un ## Índice de ejercicios -| Nombre | Descripción | Nivel | -| -------------------------------------------------------: | :------------------------------------------------------- | :--------: | -| [Words Linux](./01_scripts_words_linux/README.md) | Script con el fichero: `/usr/share/dict/words` | fácil | -| [Descifrador wargame](./02_scripts_descifrador_wargame/) | Script descifrador de código al estilo wargame | fácil | -| [Clima España](./03_clima/) | Script conectado a API AEMET | intermedio | -| [acortador de enlaces](./04_acortador_url/) | Script para acortar enlaces y redirigirlos con app Flask | fácil | -| [Pruebas de infraestructuras](./05_infra_test/README.md) | Redis, RabbitMQ, Kafka, Prometheus, etc | intermedio | -| [Bots Telegram](./06_bots_telegram/README.md) | Bots de Telegram con Python | avanzado | -| [Diagram as code](./07_diagrams_as_code/README.md) | Diagramas de infraestructuras con Python | fácil | +| Nombre | Descripción | Nivel | +| -------------------------------------------------------: | :--------------------------------------------------------------- | :--------: | +| [Words Linux](./01_scripts_words_linux/README.md) | Script con el fichero: `/usr/share/dict/words` | fácil | +| [Descifrador wargame](./02_scripts_descifrador_wargame/) | Script descifrador de código al estilo wargame | fácil | +| [Clima España](./03_clima/) | Script conectado a API AEMET | intermedio | +| [acortador de enlaces](./04_acortador_url/) | Script para acortar enlaces y redirigirlos con app Flask | fácil | +| [Pruebas de infraestructuras](./05_infra_test/README.md) | Redis, RabbitMQ, Kafka, Prometheus, etc | intermedio | +| [Bots Telegram](./06_bots_telegram/README.md) | Bots de Telegram con Python | avanzado | +| [Diagram as code](./07_diagrams_as_code/README.md) | Diagramas de infraestructuras con Python | fácil | +| [urlf4ck3r](./08_urlf4ck3r/README.md) | Script para buscar enlaces en una web y guardarlos en un fichero | intermedio |