Skip to content

Commit

Permalink
Merge pull request #47 from lawndoc/sniff-queries
Browse files Browse the repository at this point in the history
Sniff for hosts vulnerable to Responder
  • Loading branch information
xmjp authored Jul 17, 2024
2 parents 49de908 + 1ecbb67 commit 01b9379
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Project specific
config.json
config/

# Redis
dump.rdp
Expand Down
134 changes: 109 additions & 25 deletions respotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import datetime, timedelta
from ipaddress import ip_network
import json
from multiprocessing import Process
from scapy.all import *
from scapy.layers.dns import DNS, DNSQR
from scapy.layers.inet import IP, UDP
Expand Down Expand Up @@ -37,39 +38,36 @@ def __init__(self,
slack_webhook="",
teams_webhook="",
syslog_address="",
):

):
self.log = logging.getLogger('respotter')
formatter = logging.Formatter('')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
self.log.addHandler(handler)
self.log.setLevel((5 - verbosity) * 10)

if syslog_address :
if syslog_address:
handler = logging.handlers.SysLogHandler(address=(syslog_address, 514))
formatter = logging.Formatter('Respotter {processName}[{process}]: {message}', style='{')
handler.setFormatter(formatter)
self.log.addHandler(handler)

conf.checkIPaddr = False # multicast/broadcast responses won't come from dst IP
self.delay = delay
self.excluded_protocols = excluded_protocols
self.hostname = hostname
self.is_daemon = False
self.timeout = timeout
self.verbosity = verbosity
self.alerts = {}
self.responder_alerts = {}
self.vulnerable_alerts = {}
if subnet:
try:
network = ip_network(subnet)
except:
self.log.error(f"[!] ERROR: could not parse subnet CIDR. Netbios protocol will be disabled.")
self.broadcast_ip = str(network.broadcast_address)
else:
elif "nbns" not in self.excluded_protocols:
self.log.error(f"[!] ERROR: subnet CIDR not configured. Netbios protocol will be disabled.")
self.excluded_protocols.append("nbns")

self.webhooks = {}
for service in ["teams", "slack", "discord"]:
webhook = eval(f"{service}_webhook")
Expand All @@ -78,17 +76,37 @@ def __init__(self,
else:
self.log.warning(f"[-] WARNING: {service} webhook URL not set")

def webhook_alert(self, responder_ip):
if responder_ip in self.alerts:
if self.alerts[responder_ip] > datetime.now() - timedelta(hours=1):
def webhook_responder_alert(self, responder_ip):
if responder_ip in self.responder_alerts:
if self.responder_alerts[responder_ip] > datetime.now() - timedelta(hours=1):
return
title = "Responder instance found"
details = f"Responder instance found at {responder_ip}"
if "teams" in self.webhooks:
send_teams_message(self.webhooks["teams"], responder_ip)
send_teams_message(self.webhooks["teams"], title=title, details=details)
self.log.info(f"[+] Alert sent to Teams for {responder_ip}")
if "discord" in self.webhooks:
send_discord_message(self.webhooks["discord"], responder_ip)
self.log.info(f"[+] Alert sent to Discord for {responder_ip}")
self.alerts[responder_ip] = datetime.now()
send_discord_message(self.webhooks["discord"], title=title, details=details)
self.log.info(f"[+] Alert sent to Discord for {responder_ip}")
self.responder_alerts[responder_ip] = datetime.now()

def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname):
if requester_ip in self.vulnerable_alerts:
if protocol in self.vulnerable_alerts[requester_ip]:
if self.vulnerable_alerts[requester_ip][protocol] > datetime.now() - timedelta(days=1):
return
title = f"{protocol.upper()} query detected"
details = f"{protocol.upper()} query for '{requested_hostname}' from {requester_ip} - potentially vulnerable to Responder"
if "teams" in self.webhooks:
send_teams_message(self.webhooks["teams"], title=title, details=details)
self.log.info(f"[+] Alert sent to Teams for {requester_ip}")
if "discord" in self.webhooks:
send_discord_message(self.webhooks["discord"], title=title, details=details)
self.log.info(f"[+] Alert sent to Discord for {requester_ip}")
if requester_ip in self.vulnerable_alerts:
self.vulnerable_alerts[requester_ip][protocol] = datetime.now()
else:
self.vulnerable_alerts[requester_ip] = {protocol: datetime.now()}


def send_llmnr_request(self):
Expand All @@ -107,7 +125,7 @@ def send_llmnr_request(self):
if answer.type == 1: # Type 1 is A record, which contains the IP address
self.log.critical(f"[!] [LLMNR] Responder detected at: {answer.rdata} - responded to name '{self.hostname}'")
if self.is_daemon:
self.webhook_alert(answer.rdata)
self.webhook_responder_alert(answer.rdata)

def send_mdns_request(self):
# mDNS uses the multicast IP 224.0.0.251 and UDP port 5353
Expand All @@ -125,7 +143,7 @@ def send_mdns_request(self):
if answer.type == 1:
self.log.critical(f"[!] [MDNS] Responder detected at: {answer.rdata} - responded to name '{self.hostname}'")
if self.is_daemon:
self.webhook_alert(answer.rdata)
self.webhook_responder_alert(answer.rdata)

def send_nbns_request(self):
try:
Expand All @@ -135,6 +153,7 @@ def send_nbns_request(self):
return
# WORKAROUND: Scapy not matching long req to resp (secdev/scapy PR #4446)
hostname = self.hostname[:15]
# Netbios uses the broadcast IP and UDP port 137
packet = IP(dst=self.broadcast_ip)/UDP(sport=137, dport=137)/NBNSHeader(OPCODE=0x0, NM_FLAGS=0x11, QDCOUNT=1)/NBNSQueryRequest(SUFFIX="file server service", QUESTION_NAME=hostname, QUESTION_TYPE="NB")
response = sr1(packet, timeout=self.timeout, verbose=0)
if not response:
Expand All @@ -148,11 +167,19 @@ def send_nbns_request(self):
for answer in sniffed_packet[NBNSQueryResponse].ADDR_ENTRY:
self.log.critical(f"[!] [NBT-NS] Responder detected at: {answer.NB_ADDRESS} - responded to name '{hostname}'")
if self.is_daemon:
self.webhook_alert(answer.NB_ADDRESS)

self.webhook_responder_alert(answer.NB_ADDRESS)

def daemon(self):
self.is_daemon = True
scanner_process = Process(target=self.responder_scan)
scanner_process.start()
sniffer_process = Process(target=self.vuln_sniff)
sniffer_process.start()
scanner_process.join()
sniffer_process.join()

def responder_scan(self):
self.log.info("[*] Responder scans started")
while True:
if "llmnr" not in self.excluded_protocols:
self.send_llmnr_request()
Expand All @@ -161,13 +188,71 @@ def daemon(self):
if "nbns" not in self.excluded_protocols:
self.send_nbns_request()
sleep(self.delay)

def vuln_sniff(self):
"""
This sniffer will NOT poison responses; it will only listen for queries.
Poisoning responses isn't opsec-safe for the honeypot, and may cause issues with
the client. Use Responder to identify accounts that are vulnerable to poisoning
once a vulnerable host has been discovered by Respotter.
"""
llmnr_sniffer = AsyncSniffer(
filter="udp port 5355",
lfilter=lambda pkt: pkt.haslayer(LLMNRQuery), # TODO: should this be DNSQR?
started_callback=self.sniffer_startup,
prn=self.llmnr_found,
store=0
)
mdns_sniffer = AsyncSniffer(
filter="udp port 5353",
lfilter=lambda pkt: pkt.haslayer(DNS), # TODO: should this be DNSQR?
started_callback=self.sniffer_startup,
prn=self.mdns_found,
store=0
)
nbns_sniffer = AsyncSniffer(
filter="udp port 137",
lfilter=lambda pkt: pkt.haslayer(NBNSQueryRequest),
started_callback=self.sniffer_startup,
prn=self.nbns_found,
store=0
)
llmnr_sniffer.start()
mdns_sniffer.start()
nbns_sniffer.start()
while True:
sleep(1)

def sniffer_startup(self):
self.log.info("[*] Sniffer started")

def llmnr_found(self, packet):
for dns_packet in packet[LLMNRQuery].qd:
requested_hostname = dns_packet.qname.decode()
if requested_hostname == self.hostname + ".":
return
self.log.critical(f"[!] [LLMNR] LLMNR query for '{requested_hostname}' from {packet[IP].src} - potentially vulnerable to Responder")
if self.is_daemon:
self.webhook_sniffer_alert("LLMNR", packet[IP].src, requested_hostname)

def mdns_found(self, packet):
for dns_packet in packet[DNS].qd:
requested_hostname = dns_packet.qname.decode()
if requested_hostname == self.hostname + ".":
return
self.log.critical(f"[!] [MDNS] mDNS query for '{requested_hostname}' from {packet[IP].src} - potentially vulnerable to Responder")
if self.is_daemon:
self.webhook_sniffer_alert("mDNS", packet[IP].src, requested_hostname)

def nbns_found(self, packet):
requested_hostname = packet[NBNSQueryRequest].QUESTION_NAME.decode()
if requested_hostname == self.hostname[:15]:
return
self.log.critical(f"[!] [NBT-NS] NBT-NS query for '{requested_hostname}' from {packet[IP].src} - potentially vulnerable to Responder")
if self.is_daemon:
self.webhook_sniffer_alert("Netbios", packet[IP].src, requested_hostname)

def parse_options():
# Do argv default this way, as doing it in the functional
# declaration sets it at compile time.
# if argv is None:
# argv = sys.argv

# add_help=False so it doesn't parse -h yet
config_parser = argparse.ArgumentParser(add_help=False)
config_parser.add_argument("-c", "--config", help="Specify config file", metavar="FILE")
Expand Down Expand Up @@ -214,7 +299,6 @@ def parse_options():
print("\nScanning for Responder...\n")

options = parse_options()

excluded_protocols = options.exclude.split(",")
if excluded_protocols == [""]:
excluded_protocols = []
Expand Down
5 changes: 2 additions & 3 deletions utils/discord.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import json
from discord_webhook import DiscordWebhook, DiscordEmbed

def send_discord_message(webhook_url, responder_ip):
def send_discord_message(webhook_url, title, details):
webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True)
embed = DiscordEmbed(title='Responder instance found', description=f"Responder instance found at {responder_ip}", color=242424)
embed = DiscordEmbed(title=title, description=details, color=242424)
embed.set_author(name='Respotter')
embed.set_thumbnail(url='https://raw.githubusercontent.com/lawndoc/Respotter/main/assets/respotter_logo.png')
webhook.add_embed(embed)
Expand Down
11 changes: 3 additions & 8 deletions utils/teams.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import json
import requests

class TeamsException(Exception):
pass

# You will need to edit the teams.conf file with your own webhook URL

# Sending a message to Microsoft Teams:
def send_teams_message(webhook_url, responder_ip):
def send_teams_message(webhook_url, title, details):
headers = {'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0',
}
Expand All @@ -28,7 +22,8 @@ def send_teams_message(webhook_url, responder_ip):
},
{
"type": "TextBlock",
"text": f"Responder instance found at {responder_ip}\n"
"wrap": True,
"text": details + "\n"
}
]
}
Expand Down

0 comments on commit 01b9379

Please sign in to comment.