CVE-2025-66176
A stack-based buffer overflow vulnerability exists in the SADP XML parsing functionality of Hangzhou Hikvision Digital Technology Co., Ltd. Ultra Face Recognition Terminal 3.7.60_250613 and Face Recognition Terminal for Turnstyle 3.7.0_240524 (under emulation). A specially crafted network packet can lead to remote code execution. An attacker can send a malicious packet to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Hangzhou Hikvision Digital Technology Co., Ltd. Ultra Face Recognition Terminal 3.7.0_240524 (under emulation)
Hangzhou Hikvision Digital Technology Co., Ltd. Ultra Face Recognition Terminal 3.7.60_250613 (under emulation)
Ultra Face Recognition Terminal – https://www.hikvision.com/en/products/Access-Control-Products/Face-Recognition-Terminals/Ultra-Series/ds-k1t671tmfw/
8.8 – CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-121 – Stack-based Buffer Overflow
The DS-K1T671TMFW and the DS-K5671-3XF/ZU are access control terminals that extend authentication beyond standard badge-based functionality to include facial recognition, mask detection, and body temperature detection for authentication and authorization. They share an ancestry of firmware that is based on embedded Linux and they can connect to a network via ethernet or WiFi.
The core functionality of the device is implemented in a monolithic binary named `hicore`, and the vulnerability this report will discuss occurs due to an implementation flaw within the parsing of XML data encapsulated inside the Hikvision Search Active Device Protocol (SADP), a multicast protocol used to identify Hikvision devices on a network. SADP is an XML based protocol and this vulnerability lies within the seemingly custom XML parser implemented for interpreting SADP payloads. The thread responsible for receiving, parsing and handling inbound SADP packets is titled `multicast_thr_sadp_capture`. The function executed by this thread is located at offset 0x174560 of the hicore binary contained in firmware version 3.7.60_250613 and at offset 0x171d80 of the hicore binary contained in firmware version 3.7.0_240524.
We will showcase the path from receiving a packet off the network to stack overflow, starting with a partial decompilation of `multicast_thr_sadp_capture`. It is included to show the path between `recvfrom` and the function we call `parse_xml` has limited impact on the received payload prior to parsing. It does add one barrier, which is that any malicious payload may contain only one null-byte, and it must be the final byte, otherwise the `strlen(xml_payload)` parameter will truncate the payload prematurely.
“`
int32_t multicast_thr_sadp_capture() { char* xml_payload = nullptr; int ifArrIdx = 0; fprintf(stderr, “%s: n————-n fd1[%d], fd2[%d]n——-n”, “multicast_thr_sadp_capture”, SADP_IFAR[0].sockfd, SADP_IFAR[1].sockfd); if (prctl(0xf, “mu_sadp_cap”) == -1) { printf(“%s:%d, set thread_name: mu_sadp_cap err! errno = %dn”, “multicast_thr_sadp_capture”, 0x144c, *__erno_location()); } while (1) { if (gSADP_DEACTIVATE) { break; } // Eliding readfds setup struct sockaddr s; memset(&s, 0, sizeof(sockaddr)); memset_s(interface->buffer, interface->buffer_sz, 0, interface->buffer_sz); int32_t num_bytes = recvfrom(interface->sockfd, interface->buff, interface->buff_sz, 0, &s, sizeof(sockaddr)); if (num_bytes > 0) { xml_payload = interface->buffer; } else { xml_payload = NULL; } if (xml_payload) { struct XMLTree_t tree; init_xml_tree(&tree); // Any null-byte in the payload will truncate the buffer being passed into parse_xml if (parse_xml(&tree, xml_payload, strlen(xml_payload)) >= 0){ … // Implement SADP functionality based on parsed XML payload … } else { // Error handling … }
“`
As seen in the decompilation, the data is received into the specific interface’s `SADP_IFAR` buffer field. The `SADP_IFAR` global variable is an array initialized in a function titled `sadp_init_sock` located at offset 0x1702d0 or 0x16daf0. The two relevant pieces of information derived from this function are that the sockets are bound to the multicast address 239.255.255.250 on UDP port 37020, and that the value of `interface->buffer_sz` (used as the `len` parameter in the `recvfrom` call) is 0x800 bytes. With an understanding of how data flows into the function and confirmation that the only limitation is that the data can be no longer than 0x800 bytes, we now look at the implementation of `parse_xml`.
“`
int parse_xml(struct XMLTree_t* tree, char* xml_buffer, int buffer_len) { if (xml_buffer == NULL || tree == NULL) { return -1; } int remaining_bytes = buffer_len; char* buffer_tail = &xml_buffer[remaining_bytes – 1]; for (; remaining_bytes >= 0; remaining_bytes–) { if (*buffer_tail == ‘>’) { int n = validate_tag(tree, NULL, xml_buffer, remaining_bytes); } if (tree->root != 0) { return n; } break; } return -1; }
“`
This function is rather straightforward, and adds very few constraints to what a valid SADP payload must conform to for it to be parsed by `validate_tag`. It identifies the location of the trailing-most `>` character (presumed to be the final closing character of the XML payload) and then uses that to derive the total length of the XML payload to be parsed. `validate_tag` takes a pointer to the XML tree structure being populated, an optional pointer to a parent XML tag (NULL in this case, as this is the entry point for parsing the root node), a pointer to the head of the buffer to be parsed, and the calculated length of the payload.
The relevant structure for the `validate_tag` function is one we refer to as ‘XMLTag’, representing a parsed XML element. The below definition incorporates a mix of field names recovered directly from strings in the code and names we derived from context.
“`
struct XMLTag { char magic[4]; // “TAGT” int32_t tag_id; // Incrementing unique value int32_t empty; // Indicates if an element was an XML empty tag, e.g. char* name; // Finalized tag name, after validation char* value; // Finalized tag value, after parsing of content char* attrs; // Finalized attributes string, after validation, or NULL char* start_name; // Pointer to opening ‘<‘ in original payload char* content; // Pointer immediately after opening tags ‘>’ in original payload char* close_name; // Pointer to closing ‘<‘ in original payload int32_t start_len; // Size of opening tag from before ‘<‘ to after ‘>’ int32_t content_len; // Size of content from opening tag’s ‘>’ to closing tag’s ‘<‘ int32_t close_len; // Size of closing tag, from before ‘<‘ to after ‘/>’ int32_t attrs_len; // Size of attributes, from first whitespace to right before ‘>’ XMLTag_t* children; // Pointer to first child tag, or NULL XMLTag_t* siblings; // Pointer to next sibling tag, or NULL XMLTag_t* parent; // Pointer to parent tag, or NULL } XMLTag_t;
“`
Annotated portions of the decompilation of `validate_tag` are provided below to help set the stage for how the vulnerable condition is reached.
“`
int32_t validate_tag(struct XMLTree_t* tree, struct XMLTag_t* parent, char* xml_payload, int32_t tag_len) { int curr_idx = 0; char curr_chr; char next_chr; char prev_chr; while (curr_idx < tag_len) { curr_chr = xml_payload[curr_idx]; next_chr = xml_payload[curr_idx + 1]; prev_chr = (curr_idx > 0) ? xml_payload[curr_idx – 1] : ”; switch (xml_state) { …
“`
The function begins by entering the core parsing loop, where the previous, current, and next character are extracted from the payload for parsing. For each increment of `curr_idx` these characters are retrieved and parsing is then dispatched into the state machine based on the value of `xml_state`. The states that are relevant to this vulnerability are the states that parse start (state #1, which we refer to as `START_TAG`) and close (state #3, a.k.a `CLOSE_TAG`) tags, and the state responsible for validating and finalizing the parsed XML structure (a.k.a FINALIZE).
It is simpler to begin with a review of the `CLOSE_TAG` handler, as it does not need to deal with the same complexities of the `START_TAG` handler and therefore the issue is more readily apparent.
“`
case CLOSE_TAG: // Parse close tag, not always parsed if a tag was self closing (e.g. “”) if (!tag->close_name) { tag->close_name = &xml_payload[curr_idx]; } // [1] For every character parsed by the CLOSE_TAG state, increment the associated length field tag->close_len++; if (curr_chr == ‘>’) { xml_state = FINALIZE_TAG; } break;
“`
For each iteration of the loop where the parser remains in the `CLOSE_TAG` state, the `close_len` field is incremented, without any upper bound. The only way to exit this state is to either consume all characters in the buffer or to reach the final closing ‘ `>`’ character. The implementation of `START_TAG` includes some additional complexities related to parsing specific types of tags (comments, processing instructions, and self-closing), as well as tag attributes. However, the same issue presents for the calculation of the `start_len` field, at `[2]`. Note that it also applies to `attr_len` but that is not relevant to this particular vulnerability. Finally, we note at `[3]` that in the event of a self-closing tag the state machine can skip directly to `FINALIZE_TAG` without entering the `CONTENT` or `CLOSE_TAG` states.
“`
case START_TAG: // Parse start/open tag (and any attributes, since they’re a subset of the start tag) // If we enter case 1 and have not already initialized the start_name pointer, do so now if (tag->start_name == NULL) { char* head = &xml_payload[curr_idx]; if (strncmp(head, “
