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
2023-09-17 20:20:40 +00:00
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 ) )
2023-09-14 21:07:15 +00:00
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