def search_soa(self, zone: Zone, result: DNSSECScannerResult, resolver: dns.resolver.Resolver) -> SoaState: response = utils.dns_query(self.domain, zone.ip, dns.rdatatype.SOA) rrsets = response.answer + response.authority # RCODE 3 (NXDomain) means the domain name does not exist # Source: https://tools.ietf.org/html/rfc6895#section-2.3 if response.rcode() == 3: # Domain name does not exist. Validate with NSEC the integrity of the none-existence. result.note = "Domain name does not exist" zone.RR = rrsets nsec.proof_none_existence(zone, result, False) return SoaState.FOUND elif utils.get_rrs_by_type(rrsets, dns.rdatatype.CNAME): # We have found a CNAME RR set so we have to start from the top again zone.RR = rrsets validate_rrset(zone, result) # validate CNAME entry self.domain = str( utils.get_rr_by_type(rrsets, dns.rdatatype.CNAME).items[0].target) result.domain = self.domain return SoaState.FOUND_CNAME elif utils.get_rr_by_type(rrsets, dns.rdatatype.SOA): # We are in the zone for the domain name rr_types = self.find_records(zone) zone.RR = self.get_records(zone, rr_types) validate_rrset(zone, result, True) return SoaState.FOUND return SoaState.NOT_FOUND
def get_dnskey(self, zone: Zone): response = utils.dns_query(zone.name, zone.ip, dns.rdatatype.DNSKEY) zone.DNSKEY = utils.get_rr_by_type(response.answer, dns.rdatatype.DNSKEY) zone.DNSKEY_RRSIG = utils.get_rr_by_type(response.answer, dns.rdatatype.RRSIG)
def get_ns(self, zone: Zone, resolver: dns.resolver.Resolver) -> Zone: response = utils.dns_query(self.domain, zone.ip, dns.rdatatype.NS) response = utils.get_rr_by_type(response.authority, dns.rdatatype.NS) next_zone_name = str(response.name) zone.child_name = next_zone_name next_zone_domain = response.items[0].to_text() next_zone_ip = resolver.query(next_zone_domain, "A").rrset.items[0].address return Zone(next_zone_name, next_zone_ip, next_zone_domain, zone)
def find_records_from_nsec(self, zone: Zone) -> Set[int]: response = utils.dns_query(self.domain, zone.ip, dns.rdatatype.NSEC) rrsets = response.answer + response.authority nsec = utils.get_rr_by_type(rrsets, dns.rdatatype.NSEC) if nsec: return nsec_utils.nsec_window_to_array(nsec.items[0]) nsec3 = utils.get_rr_by_type(rrsets, dns.rdatatype.NSEC3) if nsec3: return nsec_utils.nsec_window_to_array(nsec3.items[0]) return set()
def get_records(self, zone: Zone, rrs: Set[int]) -> List[dns.rrset.RRset]: output = [] for rr in rrs: response = utils.dns_query(self.domain, zone.ip, rr) # check if RR exists rrsets = utils.get_rrs_by_type(response.answer, rr) if rrsets: output.extend(response.answer) for name, rrset in rrsets: # only for pretty printing for entry in rrset.to_text().split("\n"): log.info(f"Found DNS entry: {entry}") return output
def get_ds(self, zone: Zone, next_zone: Zone, result: DNSSECScannerResult) -> bool: response = utils.dns_query(next_zone.name, zone.ip, dns.rdatatype.DS) zone.RR = response.answer if not utils.get_rr_by_type(zone.RR, dns.rdatatype.DS): zone.RR = response.authority nsec.proof_none_existence(zone, result, True) msg = Message(zone.name, zone.child_name, dns.rdatatype.DS) msg.set_not_found(Msg.NOT_FOUND) result.errors.append(str(msg)) result.change_state(False) return False return True
def find_records(self, zone: Zone) -> Set[int]: # define a default list of records in case ANY does not return anything rr_types = [ dns.rdatatype.SOA, dns.rdatatype.NS, dns.rdatatype.A, dns.rdatatype.AAAA, dns.rdatatype.MX, dns.rdatatype.CNAME, dns.rdatatype.PTR, dns.rdatatype.WKS, dns.rdatatype.HINFO, dns.rdatatype.MINFO, dns.rdatatype.TXT, ] if self.requested_type: rr_types.append(self.requested_type) # ask with ANY for all existing records response = utils.dns_query(self.domain, zone.ip, dns.rdatatype.ANY) for rr in response.answer: if rr.rdtype != dns.rdatatype.DNSKEY and rr.rdtype != dns.rdatatype.RRSIG: rr_types.append(rr.rdtype) # Try to query a NSEC or NSEC3 record. This would tell # us all available record types. rr_types.extend(self.find_records_from_nsec(zone)) # remove duplicates rr_types = set(rr_types) # remove NSEC and NSEC3 record types if dns.rdatatype.NSEC in rr_types: rr_types.remove(dns.rdatatype.NSEC) if dns.rdatatype.NSEC3 in rr_types: rr_types.remove(dns.rdatatype.NSEC3) return rr_types
def nsec3_proof_of_none_existence( nsec3s: List[dns.rdtypes.ANY.NSEC3], zone: Zone, result: DNSSECScannerResult, check_ds: Optional[bool] = False, ) -> bool: """ This method does the closest encloser proof after: https://tools.ietf.org/html/rfc7129#section-5.5 :param nsec3s: :param zone: :param result: :param check_ds: :return: """ success = True qname = dns.name.from_text(result.domain) # query NSEC3 paramter from zone and validate them response = utils.dns_query(zone.name, zone.ip, dns.rdatatype.NSEC3PARAM) zone.RR = utils.remove_duplicates(response.answer) success &= validate_rrset(zone, result) nsec3param = utils.get_rr_by_type(zone.RR, dns.rdatatype.NSEC3PARAM) if check_ds: status = check_nsec_bitmap(nsec3s, nsec3param.items[0], zone.child_name) # If we want to show that there is no DS record for the QNAME # and we have found an NSEC3 record for the QNAME without the # DS record in its bitmap field we are already done. if status: return status # search for closest enclosure status, closest_encloser, next_closer_name = find_closest_encloser( nsec3s, nsec3param.items[0], qname) if status: msg = f"{zone.name} zone: Found closest encloser {closest_encloser}" result.logs.append(msg) else: msg = f"{zone.name} zone: Could not find closest encloser for {qname.to_text()}" result.errors.append(msg) success &= status # check if the next closer name is covered by an NSEC3 record status = check_name_cover(nsec3s, nsec3param.items[0], next_closer_name) if status: msg = f"{zone.name} zone: Found NSEC3 that covers the next closer name {next_closer_name}" result.logs.append(msg) else: msg = f"{zone.name} zone: Could not find a NSEC3 record that covers the next closer name {next_closer_name}" result.errors.append(msg) success &= status if check_ds: # If we want to show an insecure delegation at this point, # every NSEC3 record must have the Opt-Out flag set. success &= check_opt_out(nsec3s) else: # Proof "Three to Tango" to show that the domain name does not exist. # The only missing part is to show that no wildcard expansion could be used. # Source: https://tools.ietf.org/html/rfc7129#section-5.6 success = check_name_cover(nsec3s, nsec3param.items[0], f"*.{closest_encloser}") if success: msg = f"{zone.name} zone: Found NSEC3 that covers the wildcard *.{closest_encloser}" result.logs.append(msg) else: msg = f"{zone.name} zone: Could not find a NSEC3 record that covers the wildcard *.{closest_encloser}" result.errors.append(msg) return success