NetExec/nxc/modules/scan-network.py

382 lines
12 KiB
Python
Raw Normal View History

2022-09-07 14:49:55 +00:00
# Credit to https://twitter.com/snovvcrash/status/1550518555438891009
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
2023-05-07 22:51:01 +00:00
from os.path import expanduser
2022-09-07 14:49:55 +00:00
import codecs
2023-04-07 17:12:56 +00:00
import socket
from builtins import str
from datetime import datetime
from struct import unpack
2022-09-07 14:49:55 +00:00
import dns.name
2023-04-07 17:12:56 +00:00
import dns.resolver
from impacket.structure import Structure
from ldap3 import LEVEL
2022-09-07 14:49:55 +00:00
def get_dns_zones(connection, root, debug=False):
2023-05-08 18:39:36 +00:00
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
2022-09-07 14:49:55 +00:00
zones = []
for entry in connection.response:
2023-05-02 15:17:59 +00:00
if entry["type"] != "searchResEntry":
2022-09-07 14:49:55 +00:00
continue
2023-05-02 15:17:59 +00:00
zones.append(entry["attributes"]["dc"])
2022-09-07 14:49:55 +00:00
return zones
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
def get_dns_resolver(server, context):
# Create a resolver object
dnsresolver = dns.resolver.Resolver()
# Is our host an IP? In that case make sure the server IP is used
# if not assume lookups are working already
try:
2023-05-02 15:17:59 +00:00
if server.startswith("ldap://"):
2022-09-07 14:49:55 +00:00
server = server[7:]
2023-05-02 15:17:59 +00:00
if server.startswith("ldaps://"):
2022-09-07 14:49:55 +00:00
server = server[8:]
socket.inet_aton(server)
dnsresolver.nameservers = [server]
except socket.error:
2023-05-08 18:39:36 +00:00
context.info("Using System DNS to resolve unknown entries. Make sure resolving your" " target domain works here or specify an IP as target host to use that" " server for queries")
2022-09-07 14:49:55 +00:00
return dnsresolver
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
def ldap2domain(ldap):
2023-05-02 15:17:59 +00:00
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc=") :], flags=re.I)[3:]
2022-09-07 14:49:55 +00:00
def new_record(rtype, serial):
nr = DNS_RECORD()
2023-05-02 15:17:59 +00:00
nr["Type"] = rtype
nr["Serial"] = serial
nr["TtlSeconds"] = 180
2022-09-07 14:49:55 +00:00
# From authoritive zone
2023-05-02 15:17:59 +00:00
nr["Rank"] = 240
2022-09-07 14:49:55 +00:00
return nr
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
# From: https://docs.microsoft.com/en-us/windows/win32/dns/dns-constants
RECORD_TYPE_MAPPING = {
2023-05-02 15:17:59 +00:00
0: "ZERO",
1: "A",
2: "NS",
5: "CNAME",
6: "SOA",
12: "PTR",
# 15: 'MX',
# 16: 'TXT',
28: "AAAA",
33: "SRV",
}
2022-09-07 14:49:55 +00:00
def searchResEntry_to_dict(results):
data = {}
2023-05-02 15:17:59 +00:00
for attr in results["attributes"]:
key = str(attr["type"])
value = str(attr["vals"][0])
2022-09-07 14:49:55 +00:00
data[key] = value
return data
class NXCModule:
2023-05-02 15:17:59 +00:00
name = "get-network"
2022-09-07 14:49:55 +00:00
description = ""
2023-05-02 15:17:59 +00:00
supported_protocols = ["ldap"]
2022-09-07 14:49:55 +00:00
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
2023-05-02 15:17:59 +00:00
ALL Get DNS and IP (default: false)
ONLY_HOSTS Get DNS only (no ip) (default: false)
2022-09-07 14:49:55 +00:00
"""
self.showall = False
self.showhosts = False
self.showip = True
2023-05-02 15:17:59 +00:00
if module_options and "ALL" in module_options:
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
2022-09-07 14:49:55 +00:00
self.showall = True
else:
print("Could not parse ALL option.")
2023-05-02 15:17:59 +00:00
if module_options and "IP" in module_options:
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
2022-09-07 14:49:55 +00:00
self.showip = True
else:
print("Could not parse ONLY_HOSTS option.")
2023-05-02 15:17:59 +00:00
if module_options and "ONLY_HOSTS" in module_options:
2023-05-08 18:39:36 +00:00
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
2022-09-07 14:49:55 +00:00
self.showhosts = True
else:
print("Could not parse ONLY_HOSTS option.")
def on_login(self, context, connection):
zone = ldap2domain(connection.baseDN)
2023-05-02 15:17:59 +00:00
dnsroot = "CN=MicrosoftDNS,DC=DomainDnsZones,%s" % connection.baseDN
searchtarget = "DC=%s,%s" % (zone, dnsroot)
context.log.display("Querying zone for records")
sfilter = "(DC=*)"
2022-09-07 14:49:55 +00:00
try:
list_sites = connection.ldapConnection.search(
searchBase=searchtarget,
searchFilter=sfilter,
2023-05-02 15:17:59 +00:00
attributes=["dnsRecord", "dNSTombstoned", "name"],
sizeLimit=100000,
2022-09-07 14:49:55 +00:00
)
except ldap.LDAPSearchError as e:
2023-05-02 15:17:59 +00:00
if e.getErrorString().find("sizeLimitExceeded") >= 0:
2023-05-08 18:39:36 +00:00
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the" " data received")
2022-09-07 14:49:55 +00:00
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
list_sites = e.getAnswers()
pass
else:
raise
targetentry = None
dnsresolver = get_dns_resolver(connection.host, context.log)
outdata = []
for item in list_sites:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
site = searchResEntry_to_dict(item)
2023-05-02 15:17:59 +00:00
recordname = site["name"]
2022-09-07 14:49:55 +00:00
if "dnsRecord" in site:
2023-05-02 15:17:59 +00:00
record = bytes(site["dnsRecord"].encode("latin1"))
2022-09-07 14:49:55 +00:00
dr = DNS_RECORD(record)
2023-05-02 15:17:59 +00:00
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
if dr["Type"] == 1:
address = DNS_RPC_RECORD_A(dr["Data"])
2023-05-08 18:39:36 +00:00
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
2023-05-02 15:17:59 +00:00
outdata.append(
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address.formatCanonical(),
}
)
2023-05-08 18:39:36 +00:00
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
2023-05-02 15:17:59 +00:00
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
2023-05-08 18:39:36 +00:00
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
2023-05-02 15:17:59 +00:00
outdata.append(
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address[list(address.fields)[0]].toFqdn(),
}
)
elif dr["Type"] == 28:
address = DNS_RPC_RECORD_AAAA(dr["Data"])
2023-05-08 18:39:36 +00:00
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
2023-05-02 15:17:59 +00:00
outdata.append(
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address.formatCanonical(),
}
)
context.log.highlight("Found %d records" % len(outdata))
path = expanduser("~/.nxc/logs/{}_network_{}.log".format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
2023-05-02 15:17:59 +00:00
with codecs.open(path, "w", "utf-8") as outfile:
2022-09-07 14:49:55 +00:00
for row in outdata:
if self.showhosts:
2023-05-02 15:17:59 +00:00
outfile.write("{}\n".format(row["name"] + "." + connection.domain))
2022-09-07 14:49:55 +00:00
elif self.showall:
2023-05-08 18:39:36 +00:00
outfile.write("{} \t {}\n".format(row["name"] + "." + connection.domain, row["value"]))
2022-09-07 14:49:55 +00:00
else:
2023-05-02 15:17:59 +00:00
outfile.write("{}\n".format(row["value"]))
context.log.success("Dumped {} records to {}".format(len(outdata), path))
2022-09-07 14:49:55 +00:00
if not self.showall and not self.showhosts:
2023-05-08 18:39:36 +00:00
context.log.display("To extract CIDR from the {} ip, run the following command: cat" " your_file | mapcidr -aa -silent | mapcidr -a -silent".format(len(outdata)))
2022-09-07 14:49:55 +00:00
class DNS_RECORD(Structure):
"""
dnsRecord - used in LDAP
[MS-DNSP] section 2.3.2.2
"""
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
structure = (
2023-05-02 15:17:59 +00:00
("DataLength", "<H-Data"),
("Type", "<H"),
("Version", "B=5"),
("Rank", "B"),
("Flags", "<H=0"),
("Serial", "<L"),
("TtlSeconds", ">L"),
("Reserved", "<L=0"),
("TimeStamp", "<L=0"),
("Data", ":"),
2022-09-07 14:49:55 +00:00
)
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
# Note that depending on whether we use RPC or LDAP all the DNS_RPC_XXXX
# structures use DNS_RPC_NAME when communication is over RPC,
# but DNS_COUNT_NAME is the way they are stored in LDAP.
#
# Since LDAP is the primary goal of this script we use that, but for use
# over RPC the DNS_COUNT_NAME in the structures must be replaced with DNS_RPC_NAME,
# which is also consistent with how MS-DNSP describes it.
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_NAME(Structure):
"""
DNS_RPC_NAME
Used for FQDNs in RPC communication.
MUST be converted to DNS_COUNT_NAME for LDAP
[MS-DNSP] section 2.2.2.2.1
"""
2023-05-02 15:17:59 +00:00
structure = (("cchNameLength", "B-dnsName"), ("dnsName", ":"))
2022-09-07 14:49:55 +00:00
class DNS_COUNT_NAME(Structure):
"""
DNS_COUNT_NAME
Used for FQDNs in LDAP communication
MUST be converted to DNS_RPC_NAME for RPC communication
[MS-DNSP] section 2.2.2.2.2
"""
2023-05-02 15:17:59 +00:00
structure = (("Length", "B-RawName"), ("LabelCount", "B"), ("RawName", ":"))
2022-09-07 14:49:55 +00:00
def toFqdn(self):
ind = 0
labels = []
2023-05-02 15:17:59 +00:00
for i in range(self["LabelCount"]):
nextlen = unpack("B", self["RawName"][ind : ind + 1])[0]
labels.append(self["RawName"][ind + 1 : ind + 1 + nextlen].decode("utf-8"))
2022-09-07 14:49:55 +00:00
ind += nextlen + 1
# For the final dot
2023-05-02 15:17:59 +00:00
labels.append("")
return ".".join(labels)
2022-09-07 14:49:55 +00:00
class DNS_RPC_NODE(Structure):
"""
DNS_RPC_NODE
[MS-DNSP] section 2.2.2.2.3
"""
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
structure = (
2023-05-02 15:17:59 +00:00
("wLength", ">H"),
("wRecordCount", ">H"),
("dwFlags", ">L"),
("dwChildCount", ">L"),
("dnsNodeName", ":"),
2022-09-07 14:49:55 +00:00
)
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_A(Structure):
"""
DNS_RPC_RECORD_A
[MS-DNSP] section 2.2.2.2.4.1
"""
2023-05-02 15:17:59 +00:00
structure = (("address", ":"),)
2022-09-07 14:49:55 +00:00
def formatCanonical(self):
2023-05-02 15:17:59 +00:00
return socket.inet_ntoa(self["address"])
2022-09-07 14:49:55 +00:00
def fromCanonical(self, canonical):
2023-05-02 15:17:59 +00:00
self["address"] = socket.inet_aton(canonical)
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_NODE_NAME(Structure):
"""
DNS_RPC_RECORD_NODE_NAME
[MS-DNSP] section 2.2.2.2.4.2
"""
2023-05-02 15:17:59 +00:00
structure = (("nameNode", ":", DNS_COUNT_NAME),)
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_SOA(Structure):
"""
DNS_RPC_RECORD_SOA
[MS-DNSP] section 2.2.2.2.4.3
"""
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
structure = (
2023-05-02 15:17:59 +00:00
("dwSerialNo", ">L"),
("dwRefresh", ">L"),
("dwRetry", ">L"),
("dwExpire", ">L"),
("dwMinimumTtl", ">L"),
("namePrimaryServer", ":", DNS_COUNT_NAME),
("zoneAdminEmail", ":", DNS_COUNT_NAME),
2022-09-07 14:49:55 +00:00
)
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_NULL(Structure):
"""
DNS_RPC_RECORD_NULL
[MS-DNSP] section 2.2.2.2.4.4
"""
2023-05-02 15:17:59 +00:00
structure = (("bData", ":"),)
2022-09-07 14:49:55 +00:00
# Some missing structures here that I skipped
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_NAME_PREFERENCE(Structure):
"""
DNS_RPC_RECORD_NAME_PREFERENCE
[MS-DNSP] section 2.2.2.2.4.8
"""
2023-05-02 15:17:59 +00:00
structure = (("wPreference", ">H"), ("nameExchange", ":", DNS_COUNT_NAME))
2022-09-07 14:49:55 +00:00
# Some missing structures here that I skipped
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_AAAA(Structure):
"""
DNS_RPC_RECORD_AAAA
[MS-DNSP] section 2.2.2.2.4.17
"""
2023-05-02 15:17:59 +00:00
structure = (("ipv6Address", "16s"),)
2022-09-07 14:49:55 +00:00
def formatCanonical(self):
2023-05-02 15:17:59 +00:00
return socket.inet_ntop(socket.AF_INET6, self["ipv6Address"])
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_SRV(Structure):
"""
DNS_RPC_RECORD_SRV
[MS-DNSP] section 2.2.2.2.4.18
"""
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
structure = (
2023-05-02 15:17:59 +00:00
("wPriority", ">H"),
("wWeight", ">H"),
("wPort", ">H"),
("nameTarget", ":", DNS_COUNT_NAME),
2022-09-07 14:49:55 +00:00
)
2023-05-02 15:17:59 +00:00
2022-09-07 14:49:55 +00:00
class DNS_RPC_RECORD_TS(Structure):
"""
DNS_RPC_RECORD_TS
[MS-DNSP] section 2.2.2.2.4.23
"""
2023-05-02 15:17:59 +00:00
structure = (("entombedTime", "<Q"),)
2022-09-07 14:49:55 +00:00
def toDatetime(self):
2023-05-02 15:17:59 +00:00
microseconds = int(self["entombedTime"] / 10)
2022-09-07 14:49:55 +00:00
try:
2023-05-08 18:39:36 +00:00
return datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=microseconds)
2022-09-07 14:49:55 +00:00
except OverflowError:
2022-09-18 18:49:03 +00:00
return None