diff --git a/README.md b/README.md index a2d66ef..1a54837 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,59 @@ Perform comprehensive WHOIS lookups for domains and IP addresses. - Provides clear error messages for failed lookups - Handles rate limiting and WHOIS server unavailability -## ExploitDB Plugin +### 🔍 Subdomain Enumeration + +**🔍 !subdomains [domain]** +Enumerate subdomains using SSL certificate transparency logs with the CertSpotter API. + +**Features:** +- Discovers subdomains through SSL certificate transparency logs +- Uses the free CertSpotter API for enumeration +- No rate limiting or API key required +- Identifies subdomains through certificate SAN (Subject Alternative Name) enumeration +- No configuration required + +**Examples:** +```bash +!subdomains example.com +!subdomains google.com +!subdomains github.com +``` + +**Output includes:** +- List of discovered subdomains from certificate transparency logs +- Formatted list with up to 20 subdomains shown +- Total count of discovered subdomains + +### 🌐 IP Geolocation + +**📍 !geo [ip/domain]** +Perform IP geolocation lookups with detailed geographic information. + +**Features:** +- Uses ip-api.com as primary geolocation service with ipapi.co fallback +- Automatic domain to IP resolution +- Comprehensive geographic information +- No API key required for basic usage + +**Examples:** +```bash +!geo 8.8.8.8 +!geo example.com +!geo google.com +``` + +**Information provided:** +- Country and country code +- Region/State +- City +- Postal code +- Latitude/Longitude coordinates +- Timezone +- ISP/Organization +- Autonomous System Number (ASN) + +### ExploitDB Plugin A security plugin that searches Exploit-DB for vulnerabilities and exploits directly from Matrix. diff --git a/plugins/geo.py b/plugins/geo.py new file mode 100644 index 0000000..171ca6c --- /dev/null +++ b/plugins/geo.py @@ -0,0 +1,269 @@ +""" +This plugin provides IP geolocation functionality using free APIs. +It uses ip-api.com as the primary API with a fallback to ipapi.co. +""" + +import logging +import aiohttp +import simplematrixbotlib as botlib +import socket +import re + +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. + """ + 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. + """ + 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. + """ + 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. + """ + url = f"http://ip-api.com/json/{ip}" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + return data + else: + logging.error(f"ip-api.com returned status {response.status}") + return None + except Exception as e: + logging.error(f"Error querying ip-api.com: {e}") + 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. + """ + url = f"https://ipapi.co/{ip}/json/" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + return data + else: + logging.error(f"ipapi.co returned status {response.status}") + return None + except Exception as e: + logging.error(f"Error querying ipapi.co: {e}") + 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 + 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 + """ + 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 + country = data.get('country', 'N/A') + country_code = data.get('countryCode', 'N/A') + region = data.get('regionName', data.get('region', 'N/A')) + city = data.get('city', 'N/A') + postal = data.get('zip', 'N/A') + latitude = data.get('lat', 'N/A') + longitude = data.get('lon', 'N/A') + timezone = data.get('timezone', 'N/A') + isp = data.get('isp', 'N/A') + org = data.get('org', 'N/A') + asn = data.get('as', 'N/A') + else: # ipapi.co format + 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') + city = data.get('city', 'N/A') + postal = data.get('postal', 'N/A') + latitude = data.get('latitude', 'N/A') + longitude = data.get('longitude', 'N/A') + timezone = data.get('timezone', 'N/A') + isp = data.get('org', 'N/A') + org = data.get('org', 'N/A') + asn = data.get('asn', 'N/A') + + # Create collapsible content + content = f"🔍 IP Geolocation Results for {ip}

" + content += f"Country: {country} ({country_code})
" + content += f"Region: {region}
" + content += f"City: {city}
" + content += f"Postal Code: {postal}
" + content += f"Coordinates: {latitude}, {longitude}
" + content += f"Timezone: {timezone}
" + content += f"ISP/Organization: {isp}
" + content += f"ASN: {asn}
" + + # Wrap in details tag for Matrix compatibility + message = f"
🔍 Geolocation: {ip}{content}
" + + 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 + """ + 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 \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." + ) + 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}" + ) + 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 + 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." + ) + logging.error(f"Error in geo plugin for {query}: {e}", exc_info=True) \ No newline at end of file diff --git a/plugins/help.py b/plugins/help.py index db5cccd..7965ad2 100644 --- a/plugins/help.py +++ b/plugins/help.py @@ -94,6 +94,64 @@ async def handle_command(room, message, bot, prefix, config):

Output includes: Domain/IP information, registrar, WHOIS server, creation/expiration dates, name servers, and contact details.

+
🔍 !subdomains +

Enumerate subdomains using SSL certificate transparency logs. Discovers associated subdomains by querying the CertSpotter API for SSL certificates issued for a domain.

+

Usage:

+ +

Features:

+ +

Examples:

+ +

Essential for reconnaissance and subdomain enumeration in penetration testing

+
+ +
📍 !geo [ip/domain] +

Perform IP geolocation lookups with detailed geographic information. Resolves domains to IP addresses and provides location data including country, region, city, coordinates, and ISP information.

+

Usage:

+ +

Features:

+ +

Examples:

+ +

Information provided:

+ +

Essential for network reconnaissance and IP investigation

+
+
🔍 !shodan [command] [query]

Shodan.io integration for security reconnaissance and threat intelligence.

Commands:

diff --git a/plugins/subdomains.py b/plugins/subdomains.py new file mode 100644 index 0000000..36c625e --- /dev/null +++ b/plugins/subdomains.py @@ -0,0 +1,127 @@ +""" +This plugin provides subdomain enumeration functionality using the CertSpotter API. +It queries SSL certificates issued for domains to discover associated subdomains. +""" + +import logging +import aiohttp +import simplematrixbotlib as botlib +import json +import asyncio + +async def query_certspotter(domain): + """ + Query CertSpotter API for subdomain enumeration. + + Args: + domain (str): The domain to enumerate subdomains for + + Returns: + list: List of discovered subdomains + """ + url = f"https://api.certspotter.com/v1/issuances?domain={domain}&include_subdomains=true&expand=dns_names" + subdomains = set() + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + for cert in data: + if 'dns_names' in cert: + for dns_name in cert['dns_names']: + if domain in dns_name: + subdomains.add(dns_name) + else: + logging.error(f"CertSpotter API returned status {response.status}") + return [] + except Exception as e: + logging.error(f"Error querying CertSpotter API: {e}") + return [] + + return list(subdomains) + +async def format_subdomain_results(domain, subdomains): + """ + Format subdomain results into a readable message. + + Args: + domain (str): The queried domain + subdomains (list): List of discovered subdomains + + Returns: + str: Formatted message + """ + if not subdomains: + return f"🔍 No subdomains found for {domain} using CertSpotter API." + + # Sort and remove duplicates + subdomains = sorted(list(set(subdomains))) + count = len(subdomains) + + # Create the subdomain list with proper HTML list formatting + subdomain_list = "" + + # Create collapsible content + content = f"🔍 Subdomain Enumeration Results for {domain}

" + content += f"Total subdomains found: {count}

" + content += f"Discovered subdomains:
{subdomain_list}" + + # Wrap in details tag for Matrix compatibility + message = f"
🔍 Subdomain Enumeration: {domain} ({count} found){content}
" + + return message + +async def handle_command(room, message, bot, prefix, config): + """ + Function to handle the !subdomains 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("subdomains"): + args = match.args() + + if len(args) < 1: + await bot.api.send_text_message( + room.room_id, + "Usage: !subdomains \nExample: !subdomains example.com" + ) + return + + domain = args[0].strip() + logging.info(f"Received !subdomains command for: {domain}") + + try: + # Notify user that we're starting the lookup + await bot.api.send_text_message( + room.room_id, + f"🔍 Enumerating subdomains for {domain} using CertSpotter API..." + ) + + # Query CertSpotter API + subdomains = await query_certspotter(domain) + + # Format and send results + result_message = await format_subdomain_results(domain, subdomains) + await bot.api.send_markdown_message(room.room_id, result_message) + logging.info(f"Successfully sent subdomain enumeration results for {domain}") + + except Exception as e: + await bot.api.send_text_message( + room.room_id, + f"An error occurred during subdomain enumeration for {domain}. Please try again later." + ) + logging.error(f"Error in subdomains plugin for {domain}: {e}", exc_info=True) \ No newline at end of file