1 #!/usr/bin/python3 2 3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC") 4 # 5 # SPDX-License-Identifier: MPL-2.0 6 7 from collections.abc import AsyncGenerator 8 from dataclasses import dataclass 9 from pathlib import Path 10 11 import json 12 13 from cryptography.hazmat.primitives import serialization 14 15 import dns.dnssec 16 import dns.flags 17 import dns.message 18 import dns.name 19 import dns.rcode 20 import dns.rdata 21 import dns.rdataclass 22 import dns.rdatatype 23 import dns.rrset 24 25 from isctest.asyncserver import ( 26 AsyncDnsServer, 27 DnsResponseSend, 28 QueryContext, 29 ResponseHandler, 30 ) 31 32 TTL = 300 33 PARENT = "p031.test." 34 CHILD = f"c.{PARENT}" 35 GRANDCHILD = f"grand.{CHILD}" 36 GRANDCHILD3 = f"grand3.{CHILD}" 37 ATTACK = f"www-bind.{GRANDCHILD}" 38 ATTACK3 = f"www-bind.{GRANDCHILD3}" 39 FORGED_A = "6.6.6.60" 40 CHILD_DS = "12345 13 2 abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" 41 42 43 @dataclass(frozen=True) 44 class Key: 45 zone: dns.name.Name 46 private_key: object 47 dnskey: dns.rdata.Rdata 48 49 50 def name(text: str) -> dns.name.Name: 51 return dns.name.from_text(text) 52 53 54 def load_key() -> Key: 55 path = Path(__file__).resolve().parent / "keys.json" 56 with path.open(encoding="utf-8") as keys_file: 57 raw_key = json.load(keys_file)[PARENT] 58 59 private_key = serialization.load_pem_private_key( 60 raw_key["private_pem"].encode("ascii"), 61 password=None, 62 ) 63 dnskey = dns.rdata.from_text( 64 dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] 65 ) 66 return Key(name(PARENT), private_key, dnskey) 67 68 69 def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: 70 return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) 71 72 73 def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: 74 return dns.rrset.from_rdata(name(owner), TTL, rdata) 75 76 77 def add_signed( 78 section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key 79 ) -> None: 80 rrsig = dns.dnssec.sign( 81 covered, 82 signer.private_key, 83 signer.zone, 84 signer.dnskey, 85 lifetime=86400, 86 verify=True, 87 ) 88 section.append(covered) 89 section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)) 90 91 92 def soa_rrset() -> dns.rrset.RRset: 93 return rrset( 94 PARENT, 95 dns.rdatatype.SOA, 96 f"ns.{PARENT} hostmaster.{PARENT} 1 3600 600 86400 300", 97 ) 98 99 100 def nsec_rrset(owner: str, next_name: str, *types: str) -> dns.rrset.RRset: 101 return rrset(owner, dns.rdatatype.NSEC, f"{next_name} {' '.join(types)}") 102 103 104 def child_ds_rrset() -> dns.rrset.RRset: 105 return rrset(CHILD, dns.rdatatype.DS, CHILD_DS) 106 107 108 def grandchild_nsec_lie() -> dns.rrset.RRset: 109 return nsec_rrset(GRANDCHILD, f"grandz.{CHILD}", "NS", "RRSIG", "NSEC") 110 111 112 def grandchild3_nsec3_lie() -> dns.rrset.RRset: 113 # An NSEC3 owned by the grandparent zone P that matches the hash of the 114 # grandchild name and shows an (insecure) delegation: NS bit set, DS bit 115 # clear. Same forgery as grandchild_nsec_lie(), but expressed as NSEC3 so 116 # that the resolver reaches is_insecure_referral()'s trynsec3 arm. 117 digest = dns.dnssec.nsec3_hash(name(GRANDCHILD3), None, 0, 1).lower() 118 owner = f"{digest}.{PARENT}" 119 return rrset(owner, dns.rdatatype.NSEC3, f"1 0 0 - {digest} NS") 120 121 122 def add_parent_nodata( 123 response: dns.message.Message, parent_key: Key, nsec: dns.rrset.RRset 124 ) -> None: 125 add_signed(response.authority, soa_rrset(), parent_key) 126 add_signed(response.authority, nsec, parent_key) 127 128 129 def prepare_response(qctx: QueryContext) -> dns.message.Message: 130 qctx.prepare_new_response(with_zone_data=False) 131 qctx.response.flags |= dns.flags.AA 132 qctx.response.set_rcode(dns.rcode.NOERROR) 133 return qctx.response 134 135 136 class GrandparentNsecHandler(ResponseHandler): 137 def __init__(self, parent_key: Key) -> None: 138 self.parent_key = parent_key 139 self.parent = name(PARENT) 140 self.child = name(CHILD) 141 self.grandchild = name(GRANDCHILD) 142 self.grandchild3 = name(GRANDCHILD3) 143 144 def match(self, qctx: QueryContext) -> bool: 145 return qctx.qname.is_subdomain(self.parent) 146 147 async def get_responses( 148 self, qctx: QueryContext 149 ) -> AsyncGenerator[DnsResponseSend, None]: 150 response = prepare_response(qctx) 151 152 if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: 153 # Priming, parent DNSKEY 154 add_signed( 155 response.answer, 156 rrset_from_rdata(PARENT, self.parent_key.dnskey), 157 self.parent_key, 158 ) 159 elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: 160 # Priming, parent SOA 161 add_signed(response.answer, soa_rrset(), self.parent_key) 162 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS: 163 # Priming, child DS 164 add_signed(response.answer, child_ds_rrset(), self.parent_key) 165 elif qctx.qname == self.grandchild and qctx.qtype == dns.rdatatype.DS: 166 # Forge no data for grand child DS (NSEC variant) 167 add_parent_nodata(response, self.parent_key, grandchild_nsec_lie()) 168 elif qctx.qname == self.grandchild3 and qctx.qtype == dns.rdatatype.DS: 169 # Forge no data for grand child DS (NSEC3 variant) 170 add_parent_nodata(response, self.parent_key, grandchild3_nsec3_lie()) 171 elif ( 172 qctx.qname.is_subdomain(self.grandchild) 173 or qctx.qname.is_subdomain(self.grandchild3) 174 ) and qctx.qtype == dns.rdatatype.A: 175 # Attack query 176 response.answer.append( 177 rrset(qctx.qname.to_text(), dns.rdatatype.A, FORGED_A) 178 ) 179 else: 180 response.set_rcode(dns.rcode.NXDOMAIN) 181 182 yield DnsResponseSend(response, authoritative=True) 183 184 185 def main() -> None: 186 server = AsyncDnsServer(default_aa=True) 187 server.install_response_handlers(GrandparentNsecHandler(load_key())) 188 server.run() 189 190 191 if __name__ == "__main__": 192 main() 193