DNSSEC fix for wildcard NSEC records (CVE-2017-15107)

Signed-off-by: Petr Menšík <pemensik@redhat.com>
This commit is contained in:
Petr Menšík 2018-01-22 15:38:20 +01:00
parent 1447e0aebc
commit 8cd7421e9d
2 changed files with 218 additions and 1 deletions

View File

@ -0,0 +1,212 @@
From 7dcf32a822eeaf134526660f8c2826e18f0a8c83 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.
(cherry picked from commit 4fe6744a220eddd3f1749b40cac3dfc510787de6)
Fix DNSSEC validation errors introduced in 4fe6744a220eddd3f1749b40cac3dfc510787de6
(cherry picked from commit cd7df612b14ec1bf831a966ccaf076be0dae7404)
---
src/dnssec.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 103 insertions(+), 14 deletions(-)
diff --git a/src/dnssec.c b/src/dnssec.c
index a74d01a..704f03f 100644
--- 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 */
@@ -1405,8 +1407,8 @@ static int hostname_cmp(const char *a, const char *b)
}
}
-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;
@@ -1419,6 +1421,9 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
/* 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;
@@ -1427,7 +1432,27 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
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)
@@ -1825,24 +1850,26 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
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 */
@@ -1859,7 +1886,69 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
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))
@@ -1867,7 +1956,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
}
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
--
2.14.3

View File

@ -13,7 +13,7 @@
Name: dnsmasq
Version: 2.78
Release: 1%{?extraversion:.%{extraversion}}%{?dist}
Release: 2%{?extraversion:.%{extraversion}}%{?dist}
Summary: A lightweight DHCP/caching DNS server
Group: System Environment/Daemons
@ -24,6 +24,7 @@ Source1: %{name}.service
# https://bugzilla.redhat.com/show_bug.cgi?id=1495409
Patch1: dnsmasq-2.77-underflow.patch
Patch2: dnsmasq-2.78-CVE-2017-15107.patch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
@ -59,6 +60,7 @@ query/remove a DHCP server's leases.
%prep
%setup -q -n %{name}-%{version}%{?extraversion}
%patch1 -p1 -b .underflow
%patch2 -p1 -b .CVE-2017-15107
# use /var/lib/dnsmasq instead of /var/lib/misc
for file in dnsmasq.conf.example man/dnsmasq.8 man/es/dnsmasq.8 src/config.h; do
@ -145,6 +147,9 @@ rm -rf $RPM_BUILD_ROOT
%{_mandir}/man1/dhcp_*
%changelog
* Mon Jan 22 2018 Petr Menšík <pemensik@redhat.com> - 2.78-2
- DNSSEC fix for wildcard NSEC records (CVE-2017-15107)
* Tue Oct 03 2017 Petr Menšík <pemensik@redhat.com> - 2.78-1
- Rebase to 2.78