/* CoAP - Constrained Application Protocol https://en.wikipedia.org/wiki/Constrained_Application_Protocol This is a very simple protocol for interacting with IoT devices that have a minimal amount of resources, such as less than a megabyte of RAM. From a scanner point of view, we want to execute the equivalent of: GET /.well-known/core This will return the list of additional items that we can access on the target device. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Ver| T | TKL | Code | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Token (if any, TKL bytes) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |1 1 1 1 1 1 1 1| Payload (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ #include "proto-coap.h" #include "proto-banner1.h" #include "smack.h" #include "unusedparm.h" #include "util-logger.h" #include "masscan-app.h" #include "output.h" #include "stack-tcp-api.h" #include "proto-preprocess.h" #include "proto-ssl.h" #include "proto-udp.h" #include "syn-cookie.h" #include "massip-port.h" #include "util-malloc.h" #include "util-safefunc.h" #include "util-bool.h" #include #include #include struct CoapLink { unsigned link_offset; unsigned link_length; unsigned parms_offset; unsigned parms_length; }; /**************************************************************************** ****************************************************************************/ static const char * response_code(unsigned code) { #define CODE(x,y) (((x)<<5) | (y)) switch (code) { case CODE(2,0): return "Okay"; case CODE(2,1): return "Created"; case CODE(2,2): return "Deleted"; case CODE(2,3): return "Valid"; case CODE(2,4): return "Changed"; case CODE(2,5): return "Content"; case CODE(4,0): return "Bad Request"; case CODE(4,1): return "Unauthorized"; case CODE(4,2): return "Bad Option"; case CODE(4,3): return "Forbidden"; case CODE(4,4): return "Not Found"; case CODE(4,5): return "Method Not Allowed"; case CODE(4,6): return "Not Acceptable"; case CODE(4,12): return "Precondition Failed"; case CODE(4,13): return "Request Too Large"; case CODE(4,15): return "Unsupported Content-Format"; case CODE(5,0): return "Internal Server Error"; case CODE(5,1): return "Not Implemented"; case CODE(5,2): return "Bad Gateway"; case CODE(5,3): return "Service Unavailable"; case CODE(5,4): return "Gateway Timeout"; case CODE(5,5): return "Proxying Not Supported"; } switch (code>>5) { case 2: return "Okay"; case 4: return "Error"; default: return "PARSE_ERR"; } } /**************************************************************************** * RFC5987 * attr-char = ALPHA / DIGIT * / "!" / "#" / "$" / "&" / "+" / "-" / "." * / "^" / "_" / "`" / "|" / "~" * ; token except ( "*" / "'" / "%" ) * We need this in parsing the links, which may have parameters afterwards * whose names are in this format. ****************************************************************************/ static bool is_attr_char(unsigned c) { switch (c) { case '!': case '#': case '$': case '&': case '+': case '-': case '.': case '^': case '_': case '`': case '|': case '~': return true; default: return isalnum(c) != 0; } } /**************************************************************************** ****************************************************************************/ static struct CoapLink * parse_links(const unsigned char *px, unsigned offset, unsigned length, size_t *r_count) { struct CoapLink *l; struct CoapLink *links; unsigned count = 0; enum { LINK_BEGIN=0, LINK_VALUE, LINK_END, PARM_BEGIN, PARM_NAME_BEGIN, PARM_VALUE_BEGIN, PARM_QUOTED, PARM_QUOTED_ESCAPE, PARM_NAME, PARM_VALUE, INVALID } state = LINK_BEGIN; /* For selftesting purposes, we pass in nul-terminated strings, * indicated by a length of (~0) */ if (length == ~0) length = (unsigned)strlen((const char *)px); /* Allocate space for at least one result */ links = CALLOC(1, sizeof(*links)); l = &links[0]; l->parms_offset = offset; l->link_offset = offset; for (; offset < length; offset++) switch (state) { case INVALID: offset = length; break; case LINK_BEGIN: /* Ignore leading whitespace */ if (isspace(px[offset])) continue; /* Links must start with "<" character */ if (px[offset] != '<') { state = INVALID; break; } /* Reserve space for next link */ links = REALLOCARRAY(links, ++count+1, sizeof(*links)); links[count].link_offset = length; /* indicate end-of-list by pointing to end-of-input */ links[count].link_length = 0; links[count].parms_offset = length; links[count].parms_length = 0; /* Grab a pointer to this */ l = &links[count-1]; l->link_offset = offset+1; l->parms_offset = l->link_offset; state = LINK_VALUE; break; case LINK_VALUE: if (px[offset] == '>') { /* End of the link, it may be followed by parameters */ state = LINK_END; } else { l->link_length++; } break; case LINK_END: l->parms_offset = offset+1; l->parms_length = 0; if (isspace(px[offset])) { continue; } else if (px[offset] == ',') { /* next link */ state = LINK_BEGIN; } else if (px[offset] == ';') { state = PARM_NAME_BEGIN; } else { state = INVALID; } break; case PARM_BEGIN: if (isspace(px[offset])) { continue; } else if (px[offset] == ',') { /* next link */ l->parms_length = offset - l->parms_offset; state = LINK_BEGIN; } else if (px[offset] == ';') { state = PARM_NAME_BEGIN; } else { state = INVALID; } break; case PARM_NAME_BEGIN: if (isspace(px[offset])) continue; if (!is_attr_char(px[offset])) state = INVALID; else state = PARM_NAME; break; case PARM_NAME: if (isspace(px[offset])) { continue; } else if (px[offset] == '=') { state = PARM_VALUE_BEGIN; } else if (!is_attr_char(px[offset])) { state = INVALID; } break; case PARM_VALUE_BEGIN: if (isspace(px[offset])) continue; else if (px[offset] == '\"') { state = PARM_QUOTED; } else if (offset == ';') { state = PARM_NAME_BEGIN; } else if (px[offset] == ',') { l->parms_length = offset - l->parms_offset; state = LINK_BEGIN; } else state = PARM_VALUE; break; case PARM_VALUE: if (isspace(px[offset])) continue; else if (px[offset] == ';') state = PARM_NAME_BEGIN; else if (px[offset] == ',') { l->parms_length = offset - l->parms_offset; state = LINK_BEGIN; } else { ; /* do nothing */ } break; case PARM_QUOTED: /* RFC2616: quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) qdtext = > quoted-pair = "\" CHAR */ if (px[offset] == '\\') { state = PARM_QUOTED_ESCAPE; } else if (px[offset] == '\"') { state = PARM_VALUE; } break; case PARM_QUOTED_ESCAPE: state = PARM_QUOTED; break; default: fprintf(stderr, "invalid state\n"); state = INVALID; break; } /* Return an array of links and a count of the number of links */ *r_count = count; return links; } /**************************************************************************** ****************************************************************************/ static bool coap_parse(const unsigned char *px, size_t length, struct BannerOutput *banout, unsigned *request_id) { unsigned version; unsigned type; unsigned code = 0; unsigned token_length = 0; unsigned long long token = 0; unsigned offset; unsigned optnum; unsigned content_format; size_t i; /* All coap responses will be at least 8 bytes */ if (length < 4) { LOG(3, "[-] CoAP: short length\n"); goto not_this_protocol; } /* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Ver| T | TKL | Code | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Token (if any, TKL bytes) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |1 1 1 1 1 1 1 1| Payload (if any) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ version = (px[0]>>6) & 3; type = (px[0]>>4) & 3; token_length = px[0] & 0x0F; code = px[1]; *request_id = px[2]<<8 | px[3]; /* Only version supported is v1 */ if (version != 1) { LOG(3, "[-] CoAP: version=%u\n", version); goto not_this_protocol; } /* Only ACKs suported */ if (type != 2) { LOG(3, "[-] CoAP: type=%u\n", type); goto not_this_protocol; } /* Only token lengths up to 8 bytes are supported. * Token length must fit within the packet */ if (token_length > 8 || 4 + token_length > length) { LOG(3, "[-] CoAP: token-length=%u\n", token_length); goto not_this_protocol; } token = 0; for (i=0; i>5, code&0x1F, response_code(code)); banout_append(banout, PROTO_COAP, buf, AUTO_LEN); //code >>= 5; } /* If there was a token, the print it. */ if (token) { char buf[64]; snprintf(buf, sizeof(buf), " token=0x%llu", token); banout_append(banout, PROTO_COAP, buf, AUTO_LEN); } /* * Now process the options fields 0 1 2 3 4 5 6 7 +---------------+---------------+ | | | | Option Delta | Option Length | 1 byte | | | +---------------+---------------+ \ \ / Option Delta / 0-2 bytes \ (extended) \ +-------------------------------+ \ \ / Option Length / 0-2 bytes \ (extended) \ +-------------------------------+ \ \ / / \ \ / Option Value / 0 or more bytes \ \ / / \ \ +-------------------------------+ */ offset = 4 + token_length; optnum = 0; content_format = 0; while (offset < length) { unsigned delta; unsigned opt; unsigned optlen; /* Get the 'opt' byte */ opt = px[offset++]; if (opt == 0xFF) break; optlen = (opt>>0) & 0x0F; delta = (opt>>4) & 0x0F; /* Decode the delta field */ switch (delta) { default: optnum += delta; break; case 13: if (offset >= length) { banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } else { delta = px[offset++] + 13; optnum += delta; } break; case 14: if (offset + 1 >= length) { banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } else { delta = px[offset+0]<<8 | px[offset+1]; delta += 269; offset += 2; optnum += delta; } break; case 15: if (optlen != 15) banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } /* Decode the optlen field */ switch (optlen) { default: break; case 13: if (offset >= length) { banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } else { optlen = px[offset++] + 13; } break; case 14: if (offset + 1 >= length) { banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } else { optlen = px[offset+0]<<8 | px[offset+1]; optlen += 269; offset += 2; } break; } if (offset + optlen > length) { banout_append(banout, PROTO_COAP, " PARSE_ERR", AUTO_LEN); optnum = 0xFFFFFFFF; } /* Process the option contents */ switch (optnum) { case 0xFFFFFFFF: break; case 1: banout_append(banout, PROTO_COAP, " /If-Match/", AUTO_LEN); break; case 3: banout_append(banout, PROTO_COAP, " /Uri-Host/", AUTO_LEN); break; case 4: banout_append(banout, PROTO_COAP, " /Etag", AUTO_LEN); break; case 5: banout_append(banout, PROTO_COAP, " /If-None-Match/", AUTO_LEN); break; case 7: banout_append(banout, PROTO_COAP, " /Uri-Port/", AUTO_LEN); break; case 8: banout_append(banout, PROTO_COAP, " /Location-Path/", AUTO_LEN); break; case 11: banout_append(banout, PROTO_COAP, " /Uri-Path/", AUTO_LEN); break; case 12: banout_append(banout, PROTO_COAP, " /Content-Format/", AUTO_LEN); content_format = 0; for (i=0; isrc_ip; ipaddress ip_me = parsed->dst_ip; unsigned port_them = parsed->port_src; unsigned port_me = parsed->port_dst; unsigned message_id = 0; unsigned cookie; struct BannerOutput banout[1]; bool is_valid; LOG(1, "[+] COAP\n"); /* Initialize the "banner output" module that we'll use to print * pretty text in place of the raw packet */ banout_init(banout); /* * Do the protocol parsing */ is_valid = coap_parse(px, length, banout, &message_id); /* Validate the "syn-cookie" style information, which should match the "Message ID field*/ cookie = (unsigned)syn_cookie(ip_them, port_them | Templ_UDP, ip_me, port_me, entropy); /*if ((seqno&0xffff) != message_id) goto not_this_protocol;*/ /* See if cookies match. So far, we are allowing responses with the * wrong cookie */ if ((cookie&0xffff) != message_id) banout_append(banout, PROTO_COAP, " IP-MISMATCH", AUTO_LEN); /* Print the banner information, or save to a file, depending */ if (is_valid) { output_report_banner( out, timestamp, ip_them, 17 /*udp*/, parsed->port_src, PROTO_COAP, parsed->ip_ttl, banout_string(banout, PROTO_COAP), banout_string_length(banout, PROTO_COAP)); banout_release(banout); return 0; } else { banout_release(banout); return default_udp_parse(out, timestamp, px, length, parsed, entropy); } } /**************************************************************************** ****************************************************************************/ unsigned coap_udp_set_cookie(unsigned char *px, size_t length, uint64_t seqno) { /* The frame header is 4 bytes long, with bytes 2 and 3 being the Message ID. We can also put up to 8 bytes of a "token" here instead of just using the message ID. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Ver| T | TKL | Code | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Token (if any, TKL bytes) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if (length < 4) return 0; px[2] = (unsigned char)(seqno >> 8); px[3] = (unsigned char)(seqno >> 0); return 0; } /**************************************************************************** * For the selftest code, tests whether the indicated link is within the * given list. ****************************************************************************/ static int test_is_link(const char *name, const unsigned char *vinput, struct CoapLink *links, size_t count, int line_number) { size_t i; size_t name_length = strlen(name); const char *input = (const char *)vinput; for (i=0; i;if=\"se\\\"\\;\\,\\<\\>\\\\nsor\","; links = parse_links(input, 0, (unsigned)(~0), &count); if (!test_is_link("/success", input, links, count, __LINE__)) return 1; } /* test a simple link */ { static const unsigned char *input = (const unsigned char *) ";if=\"sensor\""; links = parse_links(input, 0, (unsigned)(~0), &count); if (!test_is_link("/sensors/temp", input, links, count, __LINE__)) return 1; } /* Test a complex dump */ { static const unsigned char *input = (const unsigned char *) ";if=\"sensor\"," ";if=\"sensor\"," ";ct=40," ";rt=\"temperature-c\";if=\"sensor\"," ";rt=\"light-lux\";if=\"sensor\"," ";rt=\"light-lux\";if=\"sensor\"," ";rt=\"light-lux core.sen-light\";if=\"sensor\"," ";ct=40;title=\"Sensor Index\"," ";rt=\"temperature-c\";if=\"sensor\"," ";rt=\"light-lux\";if=\"sensor\"," ";anchor=\"/sensors/temp\";rel=\"describedby\"," ";anchor=\"/sensors/temp\";rel=\"alternate\"," ";rt=\"firmware\";sz=262144" ; links = parse_links(input, 0, (unsigned)(~0), &count); if (!test_is_link("/firmware/v2.1", input, links, count, __LINE__)) return 1; } /* Now test an entire packet */ { const char input[] = "\x60\x45\x01\xce\xc1\x28\xff\x3c\x2f\x72\x65\x67\x69\x73\x74\x65" "\x72\x3e\x2c\x3c\x2f\x6e\x64\x6d\x2f\x64\x69\x73\x3e\x2c\x3c\x2f" "\x6e\x64\x6d\x2f\x63\x69\x3e\x2c\x3c\x2f\x6d\x69\x72\x72\x6f\x72" "\x3e\x2c\x3c\x2f\x75\x68\x70\x3e\x2c\x3c\x2f\x6e\x64\x6d\x2f\x6c" "\x6f\x67\x6f\x75\x74\x3e\x2c\x3c\x2f\x6e\x64\x6d\x2f\x6c\x6f\x67" "\x69\x6e\x3e\x2c\x3c\x2f\x69\x6e\x66\x6f\x3e"; unsigned request_id = 0; struct BannerOutput banout[1]; bool is_valid; banout_init(banout); /* parse a test packet */ is_valid = coap_parse( (const unsigned char*)input, sizeof(input)-1, banout, &request_id ); //fprintf(stderr, "[+] %.*s\n", (int)banout_string_length(banout, PROTO_COAP), banout_string(banout, PROTO_COAP)); if (!is_valid) return 1; if (request_id != 462) return 1; { const unsigned char *str = banout_string(banout, PROTO_COAP); size_t str_length = banout_string_length(banout, PROTO_COAP); if (str_length <= 16 && memcmp(str, "rsp=2.5(Content)", 16) != 0) return 1; } banout_release(banout); } return 0; }