various plugin refactors and fixes

This commit is contained in:
2026-05-09 04:51:50 -05:00
parent f822d6a450
commit 5c6234a317
25 changed files with 2044 additions and 3674 deletions
+94 -75
View File
@@ -1,14 +1,15 @@
"""
This plugin provides a command to perform DNS reconnaissance on a domain.
DNS reconnaissance plugin queries A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR records.
Outputs a formatted code block with emojis and perfectly aligned columns.
"""
import logging
import asyncio
import dns.resolver
import dns.reversename
import simplematrixbotlib as botlib
import re
from plugins.utils import is_public_destination
from plugins.common import is_public_destination, html_escape, collapsible_summary, code_block
RECORD_TYPES = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 'SRV']
@@ -16,113 +17,131 @@ def is_valid_domain(domain):
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):
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):
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':
records.append(f"{rdata.preference} {rdata.exchange}")
elif record_type == 'SOA':
records.append(f"{rdata.mname} {rdata.rname}")
elif record_type == 'SRV':
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
elif record_type == 'TXT':
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:
continue
except dns.resolver.NXDOMAIN:
logging.warning(f"Domain {domain} does not exist")
return None
except dns.resolver.Timeout:
continue
except Exception as e:
logging.error(f"Error querying {record_type} for {domain}: {e}")
continue
return results
loop = asyncio.get_running_loop()
def _resolve():
results = {}
resolver = dns.resolver.Resolver()
resolver.timeout = 5
resolver.lifetime = 5
for record_type in RECORD_TYPES:
try:
answers = resolver.resolve(domain, record_type)
records = []
for rdata in answers:
if record_type == 'MX':
records.append(f"{rdata.preference} {rdata.exchange}")
elif record_type == 'SOA':
records.append(f"{rdata.mname} {rdata.rname}")
elif record_type == 'SRV':
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
elif record_type == 'TXT':
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
except dns.resolver.NoAnswer:
continue
except dns.resolver.NXDOMAIN:
return None
except dns.resolver.Timeout:
continue
except Exception as e:
logging.error(f"Error querying {record_type} for {domain}: {e}")
continue
return results
return await loop.run_in_executor(None, _resolve)
RECORD_META = {
'A': ('🌐', 'A (IPv4)'),
'AAAA': ('🌐', 'AAAA (IPv6)'),
'MX': ('📧', 'MX (Mail)'),
'NS': ('🌐', 'NS (Nameserver)'),
'TXT': ('📄', 'TXT'),
'CNAME': ('🔀', 'CNAME'),
'SOA': ('📋', 'SOA'),
'PTR': ('↩️', 'PTR'),
'SRV': ('🔌', 'SRV'),
}
async def handle_command(room, message, bot, prefix, config):
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")
await bot.api.send_text_message(room.room_id, "Usage: !dns <domain>\nExample: !dns example.com")
return
domain = args[0].lower().strip()
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}")
await bot.api.send_text_message(room.room_id, f"Invalid domain name: {html_escape(domain)}")
return
if not is_public_destination(domain):
await bot.api.send_text_message(room.room_id, "❌ DNS queries for private/internal domains are not allowed.")
return
await bot.api.send_text_message(room.room_id, f"🔍 Performing DNS reconnaissance on {html_escape(domain)}...")
try:
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 {html_escape(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 {html_escape(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.")
await bot.api.send_text_message(room.room_id, "❌ This domain resolves exclusively to private/internal IPs.")
return
output = f"<strong>🔍 DNS Records for {domain}</strong><br><br>"
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>"
for record_type in results:
if record_type not in preferred_order:
output += format_dns_record(record_type, results[record_type])
output += "<br>"
if output.count('<br>') > 15:
output = f"<details><summary><strong>🔍 DNS Records for {domain}</strong></summary>{output}</details>"
rows = []
preferred = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'SRV', 'PTR']
for rtype in preferred:
if rtype in results:
emoji, label = RECORD_META.get(rtype, ('', rtype))
for rec in results[rtype]:
rows.append((emoji, label, rec))
emoji = ""
label = ""
for rtype in results:
if rtype not in preferred:
emoji, label = RECORD_META.get(rtype, ('', rtype))
for rec in results[rtype]:
rows.append((emoji, label, rec))
emoji = ""
label = ""
if not rows:
await bot.api.send_text_message(room.room_id, f"No displayable records for {html_escape(domain)}")
return
sections = [{"title": "", "rows": rows}]
block = code_block(f"🔍 DNS Records for {domain}", sections)
output = collapsible_summary(f"🔍 DNS: {html_escape(domain)}", block)
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.1"
__version__ = "1.1.1"
__author__ = "Funguy Bot"
__description__ = "DNS reconnaissance (SSRFsafe)"
__help__ = """
<details>
<summary><strong>!dns</strong> DNS reconnaissance</summary>
<p><code>!dns &lt;domain&gt;</code> Queries A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR records.</p>
<p><code>!dns &lt;domain&gt;</code> Queries A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR records and displays them in a clean, aligned table.</p>
</details>
"""