From 4fe6744a220eddd3f1749b40cac3dfc510787de6 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 19 Jan 2018 12:26:08 +0000 Subject: [PATCH] DNSSEC fix for wildcard NSEC records. CVE-2017-15107 applies. It's OK for NSEC records to be expanded from wildcards, but in that case, the proof of non-existence is only valid starting at the wildcard name, *. NOT the name expanded from the wildcard. Without this check it's possible for an attacker to craft an NSEC which wrongly proves non-existence in a domain which includes a wildcard for NSEC. --- src/dnssec.c | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 114 insertions(+), 15 deletions(-) --- a/src/dnssec.c +++ b/src/dnssec.c @@ -424,15 +424,17 @@ static void from_wire(char *name) static int count_labels(char *name) { int i; - + char *p; + if (*name == 0) return 0; - for (i = 0; *name; name++) - if (*name == '.') + for (p = name, i = 0; *p; p++) + if (*p == '.') i++; - return i+1; + /* Don't count empty first label. */ + return *name == '.' ? i : i+1; } /* Implement RFC1982 wrapped compare for 32-bit numbers */ @@ -1412,8 +1414,8 @@ static int hostname_cmp(const char *a, c } } -static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, - char *workspace1, char *workspace2, char *name, int type, int *nons) +static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, unsigned char **labels, int nsec_count, + char *workspace1_in, char *workspace2, char *name, int type, int *nons) { int i, rc, rdlen; unsigned char *p, *psave; @@ -1426,6 +1428,9 @@ static int prove_non_existence_nsec(stru /* Find NSEC record that proves name doesn't exist */ for (i = 0; i < nsec_count; i++) { + char *workspace1 = workspace1_in; + int sig_labels, name_labels; + p = nsecs[i]; if (!extract_name(header, plen, &p, workspace1, 1, 10)) return 0; @@ -1434,7 +1439,27 @@ static int prove_non_existence_nsec(stru psave = p; if (!extract_name(header, plen, &p, workspace2, 1, 10)) return 0; - + + /* If NSEC comes from wildcard expansion, use original wildcard + as name for computation. */ + sig_labels = *labels[i]; + name_labels = count_labels(workspace1); + + if (sig_labels < name_labels) + { + int k; + for (k = name_labels - sig_labels; k != 0; k--) + { + while (*workspace1 != '.' && *workspace1 != 0) + workspace1++; + if (k != 1 && *workspace1 == '.') + workspace1++; + } + + workspace1--; + *workspace1 = '*'; + } + rc = hostname_cmp(workspace1, name); if (rc == 0) @@ -1832,24 +1857,26 @@ static int prove_non_existence_nsec3(str static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass, char *wildname, int *nons) { - static unsigned char **nsecset = NULL; - static int nsecset_sz = 0; + static unsigned char **nsecset = NULL, **rrsig_labels = NULL; + static int nsecset_sz = 0, rrsig_labels_sz = 0; int type_found = 0; - unsigned char *p = skip_questions(header, plen); + unsigned char *auth_start, *p = skip_questions(header, plen); int type, class, rdlen, i, nsecs_found; /* Move to NS section */ if (!p || !(p = skip_section(p, ntohs(header->ancount), header, plen))) return 0; + + auth_start = p; for (nsecs_found = 0, i = ntohs(header->nscount); i != 0; i--) { unsigned char *pstart = p; - if (!(p = skip_name(p, header, plen, 10))) + if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10)) return 0; - + GETSHORT(type, p); GETSHORT(class, p); p += 4; /* TTL */ @@ -1866,7 +1893,69 @@ static int prove_non_existence(struct dn if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found)) return 0; - nsecset[nsecs_found++] = pstart; + if (type == T_NSEC) + { + /* If we're looking for NSECs, find the corresponding SIGs, to + extract the labels value, which we need in case the NSECs + are the result of wildcard expansion. + Note that the NSEC may not have been validated yet + so if there are multiple SIGs, make sure the label value + is the same in all, to avoid be duped by a rogue one. + If there are no SIGs, that's an error */ + unsigned char *p1 = auth_start; + int res, j, rdlen1, type1, class1; + + if (!expand_workspace(&rrsig_labels, &rrsig_labels_sz, nsecs_found)) + return 0; + + rrsig_labels[nsecs_found] = NULL; + + for (j = ntohs(header->nscount); j != 0; j--) + { + if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10))) + return 0; + + GETSHORT(type1, p1); + GETSHORT(class1, p1); + p1 += 4; /* TTL */ + GETSHORT(rdlen1, p1); + + if (!CHECK_LEN(header, p1, plen, rdlen1)) + return 0; + + if (res == 1 && class1 == qclass && type1 == T_RRSIG) + { + int type_covered; + unsigned char *psav = p1; + + if (rdlen1 < 18) + return 0; /* bad packet */ + + GETSHORT(type_covered, p1); + + if (type_covered == T_NSEC) + { + p1++; /* algo */ + + /* labels field must be the same in every SIG we find. */ + if (!rrsig_labels[nsecs_found]) + rrsig_labels[nsecs_found] = p1; + else if (*rrsig_labels[nsecs_found] != *p1) /* algo */ + return 0; + } + p1 = psav; + } + + if (!ADD_RDLEN(header, p1, plen, rdlen1)) + return 0; + } + + /* Must have found at least one sig. */ + if (!rrsig_labels[nsecs_found]) + return 0; + } + + nsecset[nsecs_found++] = pstart; } if (!ADD_RDLEN(header, p, plen, rdlen)) @@ -1874,7 +1963,7 @@ static int prove_non_existence(struct dn } if (type_found == T_NSEC) - return prove_non_existence_nsec(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, nons); + return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons); else if (type_found == T_NSEC3) return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons); else