Security fixes
This commit is contained in:
+24
-120
@@ -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 (SSRF‑safe)"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!dns</strong> – DNS reconnaissance</summary>
|
||||
|
||||
+32
-132
@@ -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
@@ -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
@@ -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
@@ -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 (SSRF‑safe)"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!proxy</strong> – Random working SOCKS5 proxy</summary>
|
||||
|
||||
+14
-4
@@ -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 (SSRF‑safe)"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!sslscan</strong> – SSL/TLS analysis</summary>
|
||||
|
||||
@@ -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'), # link‑local
|
||||
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'), # link‑local
|
||||
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 link‑local 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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# No‑op 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
|
||||
Reference in New Issue
Block a user