Security fixes

This commit is contained in:
2026-05-07 17:37:20 -05:00
parent e71a145dea
commit f8e8ae9533
9 changed files with 162 additions and 386 deletions
BIN
View File
Binary file not shown.
+24 -120
View File
@@ -8,214 +8,118 @@ import dns.reversename
import simplematrixbotlib as botlib
import re
# Common DNS record types to query
from plugins.utils import is_public_destination
RECORD_TYPES = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 'SRV']
def is_valid_domain(domain):
"""
Validate if the provided string is a valid domain name.
Args:
domain (str): The domain to validate.
Returns:
bool: True if valid, False otherwise.
"""
# Basic domain validation regex
pattern = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
return re.match(pattern, domain) is not None
def format_dns_record(record_type, records):
"""
Format DNS records for display.
Args:
record_type (str): The type of DNS record.
records (list): List of DNS record values.
Returns:
str: Formatted HTML string.
"""
if not records:
return ""
output = f"<strong>{record_type} Records:</strong><br>"
for record in records:
output += f"{record}<br>"
return output
async def query_dns_records(domain):
"""
Query all common DNS record types for a domain.
Args:
domain (str): The domain to query.
Returns:
dict: Dictionary with record types as keys and lists of records as values.
"""
results = {}
resolver = dns.resolver.Resolver()
resolver.timeout = 5
resolver.lifetime = 5
for record_type in RECORD_TYPES:
try:
logging.info(f"Querying {record_type} records for {domain}")
answers = resolver.resolve(domain, record_type)
records = []
for rdata in answers:
if record_type == 'MX':
# MX records have preference and exchange
records.append(f"{rdata.preference} {rdata.exchange}")
elif record_type == 'SOA':
# SOA records have multiple fields
records.append(f"{rdata.mname} {rdata.rname}")
elif record_type == 'SRV':
# SRV records have priority, weight, port, and target
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
elif record_type == 'TXT':
# TXT records can have multiple strings
txt_data = ' '.join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
records.append(txt_data)
else:
records.append(str(rdata))
if records:
results[record_type] = records
logging.info(f"Found {len(records)} {record_type} record(s)")
except dns.resolver.NoAnswer:
logging.debug(f"No {record_type} records found for {domain}")
continue
except dns.resolver.NXDOMAIN:
logging.warning(f"Domain {domain} does not exist")
return None
except dns.resolver.Timeout:
logging.warning(f"Timeout querying {record_type} for {domain}")
continue
except Exception as e:
logging.error(f"Error querying {record_type} for {domain}: {e}")
continue
return results
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !dns command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("dns"):
logging.info("Received !dns command")
args = match.args()
if len(args) != 1:
await bot.api.send_text_message(
room.room_id,
"Usage: !dns <domain>\nExample: !dns example.com"
)
logging.info("Sent usage message for !dns")
await bot.api.send_text_message(room.room_id,
"Usage: !dns <domain>\nExample: !dns example.com")
return
domain = args[0].lower().strip()
# Remove protocol if present
domain = domain.replace('http://', '').replace('https://', '')
# Remove trailing slash if present
domain = domain.rstrip('/')
# Remove www. prefix if present (optional - you can keep it if you want)
# domain = domain.replace('www.', '')
# Validate domain
domain = domain.replace('http://', '').replace('https://', '').rstrip('/')
if not is_valid_domain(domain):
await bot.api.send_text_message(
room.room_id,
f"Invalid domain name: {domain}"
)
logging.warning(f"Invalid domain provided: {domain}")
await bot.api.send_text_message(room.room_id, f"Invalid domain name: {domain}")
return
try:
logging.info(f"Starting DNS reconnaissance for {domain}")
# Send "working on it" message for longer queries
await bot.api.send_text_message(
room.room_id,
f"🔍 Performing DNS reconnaissance on {domain}..."
)
# Query DNS records
await bot.api.send_text_message(room.room_id,
f"🔍 Performing DNS reconnaissance on {domain}...")
results = await query_dns_records(domain)
if results is None:
await bot.api.send_text_message(
room.room_id,
f"Domain {domain} does not exist (NXDOMAIN)"
)
await bot.api.send_text_message(room.room_id,
f"Domain {domain} does not exist (NXDOMAIN)")
return
if not results:
await bot.api.send_text_message(
room.room_id,
f"No DNS records found for {domain}"
)
await bot.api.send_text_message(room.room_id,
f"No DNS records found for {domain}")
return
# SSRF / privacy check: if all A/AAAA records are private, refuse.
a_records = results.get('A', [])
aaaa_records = results.get('AAAA', [])
all_ips = a_records + aaaa_records
if all_ips and not any(is_public_destination(ip) for ip in all_ips):
await bot.api.send_text_message(room.room_id,
"❌ This domain resolves exclusively to private/internal IPs.")
return
# Format the output
output = f"<strong>🔍 DNS Records for {domain}</strong><br><br>"
# Order the records in a logical way
preferred_order = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'SRV', 'PTR']
for record_type in preferred_order:
if record_type in results:
output += format_dns_record(record_type, results[record_type])
output += "<br>"
# Add any remaining record types not in preferred order
for record_type in results:
if record_type not in preferred_order:
output += format_dns_record(record_type, results[record_type])
output += "<br>"
# Wrap in collapsible details if output is large
if output.count('<br>') > 15:
output = f"<details><summary><strong>🔍 DNS Records for {domain}</strong></summary>{output}</details>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent DNS records for {domain}")
except Exception as e:
await bot.api.send_text_message(
room.room_id,
f"An error occurred while performing DNS lookup: {str(e)}"
)
await bot.api.send_text_message(room.room_id,
f"An error occurred while performing DNS lookup: {str(e)}")
logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "DNS reconnaissance"
__description__ = "DNS reconnaissance (SSRFsafe)"
__help__ = """
<details>
<summary><strong>!dns</strong> DNS reconnaissance</summary>
+32 -132
View File
@@ -9,70 +9,37 @@ import simplematrixbotlib as botlib
import socket
import re
from plugins.utils import is_public_destination
async def is_valid_ip(ip):
"""
Check if the provided string is a valid IP address.
Args:
ip (str): The IP address to validate.
Returns:
bool: True if valid IP, False otherwise.
"""
"""Check if the provided string is a valid IP address."""
try:
# Check for IPv4
socket.inet_pton(socket.AF_INET, ip)
return True
except socket.error:
try:
# Check for IPv6
socket.inet_pton(socket.AF_INET6, ip)
return True
except socket.error:
return False
def is_domain(domain):
"""
Check if the provided string is a domain name.
Args:
domain (str): The string to check.
Returns:
bool: True if it's a domain, False otherwise.
"""
"""Check if the provided string is a domain name."""
domain_pattern = re.compile(
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
)
return bool(domain_pattern.match(domain))
async def resolve_domain(domain):
"""
Resolve a domain name to an IP address.
Args:
domain (str): The domain to resolve.
Returns:
str: The resolved IP address or None.
"""
"""Resolve a domain name to an IP address."""
try:
return socket.gethostbyname(domain)
except socket.gaierror:
return None
async def query_ip_api_com(ip):
"""
Query ip-api.com for geolocation information.
Args:
ip (str): The IP address to geolocate.
Returns:
dict: Geolocation data or None if error.
"""
"""Query ip-api.com for geolocation information."""
url = f"http://ip-api.com/json/{ip}"
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
@@ -87,17 +54,8 @@ async def query_ip_api_com(ip):
return None
async def query_ipapi_co(ip):
"""
Query ipapi.co for geolocation information (fallback).
Args:
ip (str): The IP address to geolocate.
Returns:
dict: Geolocation data or None if error.
"""
"""Query ipapi.co for geolocation information (fallback)."""
url = f"https://ipapi.co/{ip}/json/"
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
@@ -112,45 +70,20 @@ async def query_ipapi_co(ip):
return None
async def query_geolocation(ip):
"""
Query geolocation information using primary and fallback APIs.
Args:
ip (str): The IP address to geolocate.
Returns:
dict: Geolocation data or None if error.
"""
# Try primary API first
"""Query geolocation information using primary and fallback APIs."""
data = await query_ip_api_com(ip)
# If primary API fails, try fallback API
if not data or data.get('status') == 'fail':
logging.info("Primary API failed, trying fallback API")
data = await query_ipapi_co(ip)
return data
async def format_geolocation_results(ip, data):
"""
Format geolocation results into a readable message.
Args:
ip (str): The queried IP address
data (dict): Geolocation data
Returns:
str: Formatted message
"""
"""Format geolocation results into a readable message."""
if not data:
return f"🔍 No geolocation data found for {ip}."
# Check if data is from ip-api.com or ipapi.co and format accordingly
if 'status' in data and data.get('status') == 'fail':
return f"🔍 No geolocation data found for {ip}."
# Extract relevant information based on API used
if 'country' in data: # ip-api.com format
if 'country' in data:
country = data.get('country', 'N/A')
country_code = data.get('countryCode', 'N/A')
region = data.get('regionName', data.get('region', 'N/A'))
@@ -162,7 +95,7 @@ async def format_geolocation_results(ip, data):
isp = data.get('isp', 'N/A')
org = data.get('org', 'N/A')
asn = data.get('as', 'N/A')
else: # ipapi.co format
else:
country = data.get('country_name', data.get('country', 'N/A'))
country_code = data.get('country_code', data.get('countryCode', 'N/A'))
region = data.get('region', 'N/A')
@@ -174,8 +107,6 @@ async def format_geolocation_results(ip, data):
isp = data.get('org', 'N/A')
org = data.get('org', 'N/A')
asn = data.get('asn', 'N/A')
# Create collapsible content
content = f"<strong>🔍 IP Geolocation Results for {ip}</strong><br><br>"
content += f"<strong>Country:</strong> {country} ({country_code})<br>"
content += f"<strong>Region:</strong> {region}<br>"
@@ -185,95 +116,64 @@ async def format_geolocation_results(ip, data):
content += f"<strong>Timezone:</strong> {timezone}<br>"
content += f"<strong>ISP/Organization:</strong> {isp}<br>"
content += f"<strong>ASN:</strong> {asn}<br>"
# Wrap in details tag for Matrix compatibility
message = f"<details><summary><strong>🔍 Geolocation: {ip}</strong></summary>{content}</details>"
return message
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !geo command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
"""Handle the !geo command."""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("geo"):
args = match.args()
if len(args) < 1:
await bot.api.send_text_message(
room.room_id,
"Usage: !geo <ip_address/domain>\nExample: !geo 8.8.8.8\nExample: !geo example.com"
)
return
query = args[0].strip()
logging.info(f"Received !geo command for: {query}")
try:
# Determine if input is IP or domain
ip = query
if is_domain(query):
# Resolve domain to IP first
await bot.api.send_text_message(
room.room_id,
f"🔍 Resolving domain {query} to IP address..."
)
ip = await resolve_domain(query)
if not ip:
await bot.api.send_text_message(
room.room_id,
f"Failed to resolve domain {query} to IP address."
)
await bot.api.send_text_message(room.room_id,
f"Failed to resolve domain {query} to IP address.")
return
await bot.api.send_text_message(
room.room_id,
f"Domain {query} resolved to IP {ip}"
)
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id,
"❌ That domain resolves to a private/internal IP, geo not allowed.")
return
await bot.api.send_text_message(room.room_id,
f"Domain {query} resolved to IP {ip}")
elif not await is_valid_ip(query):
await bot.api.send_text_message(
room.room_id,
f"Invalid IP address or domain format: {query}"
)
await bot.api.send_text_message(room.room_id,
f"Invalid IP address or domain format: {query}")
return
# Notify user that we're starting the lookup
await bot.api.send_text_message(
room.room_id,
f"🔍 Looking up geolocation for {ip}..."
)
# Query geolocation data with fallback
else:
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id,
"❌ Geolocation of private IP addresses is not allowed.")
return
await bot.api.send_text_message(room.room_id,
f"🔍 Looking up geolocation for {ip}...")
geo_data = await query_geolocation(ip)
# Format and send results
result_message = await format_geolocation_results(ip, geo_data)
await bot.api.send_markdown_message(room.room_id, result_message)
logging.info(f"Successfully sent geolocation results for {ip}")
except Exception as e:
await bot.api.send_text_message(
room.room_id,
f"An error occurred during geolocation lookup for {query}. Please try again later."
)
await bot.api.send_text_message(room.room_id,
f"An error occurred during geolocation lookup for {query}. Please try again later.")
logging.error(f"Error in geo plugin for {query}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "IP geolocation lookup"
__help__ = """
+11 -1
View File
@@ -9,6 +9,8 @@ from urllib.parse import urlparse
import ssl
import socket
from plugins.utils import is_public_destination
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle !headers command for HTTP security header analysis.
@@ -39,6 +41,14 @@ async def handle_command(room, message, bot, prefix, config):
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
# SSRF protection: refuse internal hosts
parsed = urlparse(url)
host = parsed.hostname
if not is_public_destination(host):
await bot.api.send_text_message(room.room_id,
"❌ Scanning of private/internal addresses is not allowed.")
return
await analyze_headers(room, bot, url)
async def show_usage(room, bot):
@@ -391,7 +401,7 @@ async def format_header_analysis(results):
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "HTTP security header analysis"
__help__ = """
+12 -57
View File
@@ -2,23 +2,15 @@
This plugin provides a command to check if a website or server is up.
"""
# plugins/isup.py
import logging
import aiohttp
import socket
import simplematrixbotlib as botlib
from plugins.utils import is_public_destination
async def check_http(domain):
"""
Check if HTTP service is up for the given domain.
Args:
domain (str): The target domain.
Returns:
bool: True if HTTP service is up, False otherwise.
"""
"""Check if HTTP service is up for the given domain."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://{domain}") as response:
@@ -27,15 +19,7 @@ async def check_http(domain):
return False
async def check_https(domain):
"""
Check if HTTPS service is up for the given domain.
Args:
domain (str): The target domain.
Returns:
bool: True if HTTPS service is up, False otherwise.
"""
"""Check if HTTPS service is up for the given domain."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"https://{domain}") as response:
@@ -44,67 +28,38 @@ async def check_https(domain):
return False
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !isup command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot instance.
prefix (str): The bot command prefix.
config (FunguyConfig): The bot configuration instance.
Returns:
None
"""
# Check if the message matches the command pattern and is not from this bot
"""Handle the !isup command."""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("isup"):
# Log that the !isup command has been received
logging.info("Received !isup command")
args = match.args()
# Check if the command has exactly one argument
if len(args) != 1:
# If the command does not have exactly one argument, send usage message
await bot.api.send_markdown_message(room.room_id, "Usage: !isup <ipv4/ipv6/domain>")
logging.info("Sent usage message to the room")
return
target = args[0]
# Perform DNS resolution
try:
ip_address = socket.gethostbyname(target)
# Log successful DNS resolution
logging.info(f"DNS resolution successful for {target}: {ip_address}")
# Send DNS resolution success message
await bot.api.send_markdown_message(room.room_id, f"✅ DNS resolution successful for **{target}**: **{ip_address}** (A record)")
except socket.gaierror:
# Log DNS resolution failure
logging.info(f"DNS resolution failed for {target}")
# Send DNS resolution failure message
await bot.api.send_markdown_message(room.room_id, f"❌ DNS resolution failed for **{target}**")
return
# Check HTTP/HTTPS services
if not is_public_destination(ip_address):
await bot.api.send_text_message(room.room_id,
"❌ Checking internal/private IPs is not allowed.")
return
await bot.api.send_markdown_message(room.room_id,
f"✅ DNS resolution successful for **{target}**: **{ip_address}** (A record)")
if await check_http(target):
# If HTTP service is up, send HTTP service up message
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTP service is up")
logging.info(f"{target} HTTP service is up")
elif await check_https(target):
# If HTTPS service is up, send HTTPS service up message
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTPS service is up")
logging.info(f"{target} HTTPS service is up")
else:
# If both HTTP and HTTPS services are down, send service down message
await bot.api.send_markdown_message(room.room_id, f"😕 **{target}** HTTP/HTTPS services are down")
logging.info(f"{target} HTTP/HTTPS services are down")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "Check if a site is up"
__help__ = """
+10 -72
View File
@@ -2,8 +2,6 @@
This plugin provides a command to get random SOCKS5 proxies.
"""
# plugins/proxy.py
import os
import logging
import random
@@ -14,10 +12,11 @@ from datetime import datetime, timedelta
import concurrent.futures
import simplematrixbotlib as botlib
import sqlite3
import ipaddress
from plugins.utils import is_public_destination
# Constants
SOCKS5_LIST_URL = 'https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt'
# SOCKS5_LIST_URL = 'https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/socks5/data.txt'
MAX_TRIES = 64
PROXY_LIST_FILENAME = 'socks5.txt'
PROXY_LIST_EXPIRATION = timedelta(hours=8)
@@ -25,19 +24,10 @@ MAX_THREADS = 128
PROXIES_DB_FILE = 'proxies.db'
MAX_PROXIES_IN_DB = 10
# Setup verbose logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def test_proxy(proxy):
"""
Test a SOCKS5 proxy and return the outcome.
Args:
proxy (str): The SOCKS5 proxy address in the format 'ip:port'.
Returns:
tuple: (bool: success, str: proxy, int: latency)
"""
"""Test a SOCKS5 proxy and return the outcome."""
try:
ip, port = proxy.split(':')
logging.info(f"Testing SOCKS5 proxy: {ip}:{port}")
@@ -53,14 +43,7 @@ def test_proxy(proxy):
except Exception as e:
return False, proxy, None
async def download_proxy_list():
"""
Download the SOCKS5 proxy list file if it doesn't already exist or if it's expired.
Returns:
bool: True if the proxy list is downloaded or up-to-date, False otherwise.
"""
try:
if not os.path.exists(PROXY_LIST_FILENAME) or \
datetime.now() - datetime.fromtimestamp(os.path.getctime(PROXY_LIST_FILENAME)) > PROXY_LIST_EXPIRATION:
@@ -77,15 +60,7 @@ async def download_proxy_list():
logging.error(f"Error downloading proxy list: {e}")
return False
def check_db_for_proxy():
"""
Check the proxies database for a working proxy.
If found, test the proxy and remove it from the database if it doesn't work.
Returns:
str or None: The working proxy if found, None otherwise.
"""
try:
with sqlite3.connect(PROXIES_DB_FILE) as conn:
cursor = conn.cursor()
@@ -115,15 +90,7 @@ def check_db_for_proxy():
logging.error(f"Error checking proxies database: {e}")
return None, None
def save_proxy_to_db(proxy, latency):
"""
Save a working proxy to the proxies database.
Args:
proxy (str): The working proxy to be saved.
latency (int): Latency of the proxy.
"""
try:
with sqlite3.connect(PROXIES_DB_FILE) as conn:
cursor = conn.cursor()
@@ -140,44 +107,25 @@ def save_proxy_to_db(proxy, latency):
except Exception as e:
logging.error(f"Error saving proxy to database: {e}")
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !proxy command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("proxy"):
logging.info("Received !proxy command")
# Check database for a working proxy
working_proxy, latency = check_db_for_proxy()
if working_proxy:
await bot.api.send_markdown_message(room.room_id,
f"✅ Using cached working SOCKS5 Proxy: **{working_proxy}** - Latency: **{latency} ms**")
logging.info(f"Using cached working SOCKS5 proxy {working_proxy}")
return
# Download proxy list if needed
else:
if not await download_proxy_list():
await bot.api.send_markdown_message(room.room_id, "Error downloading proxy list")
logging.error("Error downloading proxy list")
return
try:
# Read proxies from file
with open(PROXY_LIST_FILENAME, 'r') as f:
socks5_proxies = [line.replace("socks5://", "") for line in f.read().splitlines()]
# Filter out private/internal proxies before testing
socks5_proxies = [p for p in socks5_proxies if is_public_destination(p.split(':')[0])]
random.shuffle(socks5_proxies)
# Test proxies concurrently
tested_proxies = 0
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
futures = []
@@ -188,36 +136,26 @@ async def handle_command(room, message, bot, prefix, config):
if success:
await bot.api.send_markdown_message(room.room_id,
f"✅ Anonymous SOCKS5 Proxy: **{proxy}** - Latency: **{latency} ms**")
logging.info(f"Sent SOCKS5 proxy {proxy} to the room")
save_proxy_to_db(proxy, latency) # Save working proxy to the database
save_proxy_to_db(proxy, latency)
tested_proxies += 1
if tested_proxies >= MAX_PROXIES_IN_DB:
break # Stop testing proxies once MAX_PROXIES_IN_DB are saved to the database
# Check database for a working proxy after testing
break
working_proxy, latency = check_db_for_proxy()
if working_proxy:
await bot.api.send_markdown_message(room.room_id,
f"✅ Using cached working SOCKS5 Proxy: **{working_proxy}** - Latency: **{latency} ms**")
logging.info(f"Using cached working SOCKS5 proxy {working_proxy}")
else:
# If no working proxy found after testing
await bot.api.send_markdown_message(room.room_id, "❌ No working anonymous SOCKS5 proxy found")
logging.info("No working anonymous SOCKS5 proxy found")
except Exception as e:
logging.error(f"Error handling !proxy command: {e}")
await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "Working SOCKS5 proxy finder"
__description__ = "Working SOCKS5 proxy finder (SSRFsafe)"
__help__ = """
<details>
<summary><strong>!proxy</strong> Random working SOCKS5 proxy</summary>
+14 -4
View File
@@ -11,6 +11,8 @@ import re
import simplematrixbotlib as botlib
from urllib.parse import urlparse
from plugins.utils import is_public_destination
# SSL/TLS configuration - handle missing protocols in modern Python
TLS_VERSIONS = {
'TLSv1.2': ssl.PROTOCOL_TLSv1_2,
@@ -94,6 +96,14 @@ async def handle_command(room, message, bot, prefix, config):
await bot.api.send_text_message(room.room_id, "Invalid port number")
return
# SSRF protection: refuse internal hosts
if not is_public_destination(target):
await bot.api.send_text_message(
room.room_id,
"❌ Scanning of private/internal addresses is not allowed."
)
return
await perform_ssl_scan(room, bot, target, port)
async def show_usage(room, bot):
@@ -334,7 +344,7 @@ async def check_vulnerabilities(scan_results):
"""Check for common SSL/TLS vulnerabilities."""
vulnerabilities = []
# Check for weak protocols (these will be False in modern Python, which is good)
# Check for weak protocols
if scan_results['protocols'].get('SSLv2', False):
vulnerabilities.append({
'name': 'SSLv2 Support',
@@ -400,7 +410,7 @@ async def calculate_security_score(scan_results):
"""Calculate overall security score."""
score = 100
# Protocol penalties (in modern Python, SSLv2/SSLv3 will be False, which is good)
# Protocol penalties
if scan_results['protocols'].get('SSLv2', False):
score -= 30
if scan_results['protocols'].get('SSLv3', False):
@@ -598,9 +608,9 @@ def format_cert_date(date_str):
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "SSL/TLS security scanner"
__description__ = "SSL/TLS security scanner (SSRFsafe)"
__help__ = """
<details>
<summary><strong>!sslscan</strong> SSL/TLS analysis</summary>
+59
View File
@@ -0,0 +1,59 @@
"""
Security utilities for Funguy Bot plugins.
"""
import ipaddress
import socket
import logging
logger = logging.getLogger("security_utils")
# Networks considered unsafe for outbound connections
PRIVATE_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # linklocal
ipaddress.ip_network('0.0.0.0/8'), # "this" network
ipaddress.ip_network('::1/128'), # IPv6 loopback
ipaddress.ip_network('fc00::/7'), # unique local
ipaddress.ip_network('fe80::/10'), # linklocal
ipaddress.ip_network('::/128'), # unspecified
]
def is_public_destination(target: str) -> bool:
"""
Returns True if `target` (hostname or IP) does NOT resolve to any
private, loopback, or linklocal address.
"""
try:
# Try parsing as an IP address first
addr = ipaddress.ip_address(target)
if any(addr in net for net in PRIVATE_RANGES):
return False
return True
except ValueError:
pass
# Resolve hostname to IPs
try:
addrinfo = socket.getaddrinfo(target, None)
for _, _, _, _, sockaddr in addrinfo:
ip = sockaddr[0]
addr = ipaddress.ip_address(ip)
if any(addr in net for net in PRIVATE_RANGES):
return False
return True
except Exception as e:
logger.warning(f"Cannot resolve {target}: {e}")
return False
# ---------------------------------------------------------------------------
# Noop command handler prevents bot crash because funguy.py calls
# handle_command() on every module in the plugins directory.
# ---------------------------------------------------------------------------
async def handle_command(room, message, bot, prefix, config):
"""This module is not a command plugin; ignore all messages."""
pass
BIN
View File
Binary file not shown.