Add enhanced urlf4ck3r

This commit is contained in:
Manuel Vergara 2024-09-01 19:18:39 +02:00
parent dc9e81f06e
commit 21c7e1e6c6
7 changed files with 613 additions and 9 deletions

View File

@ -0,0 +1,7 @@
README.md
docker-compose.yaml
docker-compose.yml
Dockerfile
.git
.gitignore
.vscode

View File

@ -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"]

View File

@ -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 <URL> -o output.txt
```
De lo contrario, desde el directorio donde se encuentra ubicado el script:
```
./urlf4ck3r.py -u <URL> -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.

View File

@ -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

View File

@ -0,0 +1,3 @@
beautifulsoup4==4.12.3
pwntools==4.13.0
Requests==2.32.3

View File

@ -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()

View File

@ -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 |