Home | History | Annotate | Line # | Download | only in ans4
      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 datetime import datetime, timedelta, timezone
     10 from pathlib import Path
     11 
     12 import base64
     13 
     14 from cryptography.hazmat.primitives import serialization
     15 from dns.rdtypes.dnskeybase import Flag
     16 
     17 import dns.dnssec
     18 import dns.flags
     19 import dns.message
     20 import dns.name
     21 import dns.rcode
     22 import dns.rdata
     23 import dns.rdataclass
     24 import dns.rdatatype
     25 import dns.rrset
     26 
     27 from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext
     28 
     29 TTL = 300
     30 ZONE = "f217.test."
     31 PEM_PATH = Path(f"{ZONE}.pem")
     32 CHILD = f"evil.{ZONE}"
     33 ATTACK = f"www.{CHILD}"
     34 NSEC_OWNER = f"00000000.{CHILD}"
     35 NSEC_NEXT = f"zzz.{CHILD}"
     36 FORGED_A = "192.0.2.217"
     37 
     38 
     39 @dataclass(frozen=True)
     40 class Key:
     41     zone: dns.name.Name
     42     private_key: object
     43     dnskey: dns.rdata.Rdata
     44 
     45 
     46 def name(text: str) -> dns.name.Name:
     47     return dns.name.from_text(text)
     48 
     49 
     50 def load_key() -> Key:
     51     private_key = serialization.load_pem_private_key(
     52         PEM_PATH.read_bytes(), password=None
     53     )
     54 
     55     dnskey = dns.dnssec.make_dnskey(
     56         private_key.public_key(),
     57         dns.dnssec.Algorithm.ECDSAP256SHA256,
     58         flags=Flag.ZONE | Flag.SEP,
     59     )
     60 
     61     return Key(name(ZONE), private_key, dnskey)
     62 
     63 
     64 def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset:
     65     return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas)
     66 
     67 
     68 def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset:
     69     return dns.rrset.from_rdata(name(owner), TTL, rdata)
     70 
     71 
     72 def add_signed(
     73     section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key
     74 ) -> None:
     75     rrsig = dns.dnssec.sign(
     76         covered,
     77         signer.private_key,
     78         signer.zone,
     79         signer.dnskey,
     80         lifetime=86400,
     81         verify=True,
     82     )
     83     section.append(covered)
     84     section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig))
     85 
     86 
     87 def soa_rrset(zone: str) -> dns.rrset.RRset:
     88     return rrset(
     89         zone,
     90         dns.rdatatype.SOA,
     91         f"ns.{ZONE} hostmaster.{ZONE} 1 7200 3600 1209600 300",
     92     )
     93 
     94 
     95 def garbage_rrsig(
     96     owner: str, covered: dns.rdatatype.RdataType, labels: int, signer: str
     97 ) -> dns.rrset.RRset:
     98     now = datetime.now(timezone.utc)
     99     inception = (now - timedelta(hours=1)).strftime("%Y%m%d%H%M%S")
    100     expiration = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%S")
    101     signature = base64.b64encode(bytes(64)).decode("ascii")
    102     text = (
    103         f"{dns.rdatatype.to_text(covered)} 13 {labels} {TTL} "
    104         f"{expiration} {inception} 12345 {signer} {signature}"
    105     )
    106     rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, text)
    107     return dns.rrset.from_rdata(name(owner), TTL, rdata)
    108 
    109 
    110 def add_ds_denial(response: dns.message.Message, key: Key) -> None:
    111     add_signed(response.authority, soa_rrset(ZONE), key)
    112     nsec = rrset(CHILD, dns.rdatatype.NSEC, f"ns.{ZONE} NS RRSIG NSEC")
    113     add_signed(response.authority, nsec, key)
    114 
    115 
    116 def add_attack_answer(response: dns.message.Message) -> None:
    117     """
    118     Crafted authoritative response to <q>.evil.f217.hack./A
    119 
    120         ;; ANSWER
    121         <q>.evil.f217.hack.        300 IN A     192.0.2.217
    122         <q>.evil.f217.hack.        300 IN RRSIG A 13 1 300 <exp> <inc> 12345 evil.f217.hack. <base64 of 640x00>
    123                                                     ^^^ Labels = 1, qname has 4 labels, wildcard heuristic fires
    124 
    125         ;; AUTHORITY (single owner, three rdatasets in this wire order)
    126         00000000.evil.f217.hack.   300 IN NSEC  zzz.evil.f217.hack. A RRSIG NSEC
    127         00000000.evil.f217.hack.   300 IN RRSIG NSEC 13 4 300 <exp> <inc> 12345 evil.f217.hack. <base64 of 640x00>
    128         00000000.evil.f217.hack.   300 IN NSEC3 1 0 0 - VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV A RRSIG
    129     """
    130     # A + RRSIG
    131     response.answer.append(rrset(ATTACK, dns.rdatatype.A, FORGED_A))
    132     response.answer.append(garbage_rrsig(ATTACK, dns.rdatatype.A, 1, CHILD))
    133     # NSEC
    134     nsec = rrset(
    135         NSEC_OWNER,
    136         dns.rdatatype.NSEC,
    137         f"{NSEC_NEXT} A RRSIG NSEC",
    138     )
    139     response.authority.append(nsec)
    140     # RRSIG(NSEC)
    141     response.authority.append(
    142         garbage_rrsig(
    143             NSEC_OWNER,
    144             dns.rdatatype.NSEC,
    145             len(name(NSEC_OWNER).labels) - 1,
    146             CHILD,
    147         )
    148     )
    149     # NSEC3
    150     nsec3 = rrset(
    151         NSEC_OWNER,
    152         dns.rdatatype.NSEC3,
    153         "1 0 0 - VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV A RRSIG",
    154     )
    155     response.authority.append(nsec3)
    156 
    157 
    158 class RuntimeCheckHandler(DomainHandler):
    159     """Serve attacker.rrsig-labels-signer. with crafted wildcard RRSIG."""
    160 
    161     domains = [ZONE]
    162 
    163     def __init__(self) -> None:
    164         super().__init__()
    165         self.key = load_key()
    166         self.zone = name(ZONE)
    167         self.child = name(CHILD)
    168         self.attack = name(ATTACK)
    169 
    170     def match(self, qctx: QueryContext) -> bool:
    171         return qctx.qname.is_subdomain(self.zone)
    172 
    173     async def get_responses(
    174         self, qctx: QueryContext
    175     ) -> AsyncGenerator[DnsResponseSend, None]:
    176         qctx.prepare_new_response(with_zone_data=False)
    177         qctx.response.flags |= dns.flags.AA
    178         qctx.response.set_rcode(dns.rcode.NOERROR)
    179 
    180         if qctx.qname == self.zone and qctx.qtype == dns.rdatatype.DNSKEY:
    181             add_signed(
    182                 qctx.response.answer,
    183                 rrset_from_rdata(ZONE, self.key.dnskey),
    184                 self.key,
    185             )
    186         elif qctx.qname == self.zone and qctx.qtype == dns.rdatatype.SOA:
    187             add_signed(qctx.response.answer, soa_rrset(ZONE), self.key)
    188         elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS:
    189             add_ds_denial(qctx.response, self.key)
    190         elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DNSKEY:
    191             qctx.response.authority.append(soa_rrset(CHILD))
    192         elif qctx.qname == self.attack and qctx.qtype == dns.rdatatype.A:
    193             add_attack_answer(qctx.response)
    194         else:
    195             add_signed(qctx.response.authority, soa_rrset(ZONE), self.key)
    196 
    197         yield DnsResponseSend(qctx.response, authoritative=True)
    198