CVE-2017-15107 An interesting problem has turned up in DNSSEC validation. It turns out that NSEC records expanded from wildcards are allowed, so a domain can include an NSEC record for *.example.org and an actual query reply could expand that to anything in example.org and still have it signed by the signature for the wildcard. So, for example !.example.org NSEC zz.example.org is fine. The problem is that most implementers (your author included, but also the Google public DNS people, powerdns and Unbound) then took that record to prove the nothing exists between !.example.org and zz.example.org, whereas in fact it only provides that proof between *.example.org and zz.example.org. This gives an attacker a way to prove that anything between !.example.org and *.example.org doesn't exists, when it may well do so. Signed-off-by: Kevin Darbyshire-Bryant <ldir@darbyshire-bryant.me.uk>master
parent
9c2ac19b03
commit
a3198061f8
@ -0,0 +1,202 @@ |
||||
From 4fe6744a220eddd3f1749b40cac3dfc510787de6 Mon Sep 17 00:00:00 2001
|
||||
From: Simon Kelley <simon@thekelleys.org.uk>
|
||||
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, *.<domain> 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 (rdlen < 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
|
Loading…
Reference in new issue