ID CVE-2021-21955 Type cve Reporter talos-cna@cisco.com Modified 2022-04-28T17:15:00
Description
An authentication bypass vulnerability exists in the get_aes_key_info_by_packetid() function of the home_security binary of Anker Eufy Homebase 2 2.1.6.9h. Generic network sniffing can lead to password recovery. An attacker can sniff network traffic to trigger this vulnerability.
{"talos": [{"lastseen": "2022-01-26T11:40:39", "description": "### Summary\n\nAn authentication bypass vulnerability exists in the get_aes_key_info_by_packetid() function of the home_security binary of Anker Eufy Homebase 2 2.1.6.9h. Generic network sniffing can lead to password recovery. An attacker can sniff network traffic to trigger this vulnerability.\n\n### Tested Versions\n\nAnker Eufy Homebase 2 2.1.6.9h\n\n### Product URLs\n\n<https://us.eufylife.com/products/t88411d1>\n\n### CVSSv3 Score\n\n7.7 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:H\n\n### CWE\n\nCWE-334 - Small Space of Random Values\n\n### Details\n\nThe Eufy Homebase 2 is the video storage and networking gateway that enables the functionality of the Eufy Smarthome ecosystem. All Eufy devices connect back to this device, and this device connects out to the cloud, while also providing assorted services to enhance other Eufy Smarthome devices.\n\nThe Eufy Homebase 2\u2019s `home_security` binary is a central cog in the device, spawning an inordinate amount of pthreads immediately after executing, each with their own little task. For the purposes of this advisory, we care solely about the pthread in charge of a particular cloud connectivity occurring with IP address `18.224.66.194` on UDP port 8006. An example of such traffic is shown below:\n \n \n // device -> cloud\n 0000 58 5a fe b9 0b 00 00 00 59 5e 42 61 01 00 00 00 XZ......Y^Ba....\n 0010 00 00 01 00 54 38 30 31 30 4e 31 32 33 34 35 36 ....T8010N123456\n 0020 37 48 39 3A 00 789A.\n \n\nThis particular packet is the `CMD_DEVICE_HEARTBEAT_CHECK`, and the server\u2019s response is seen below:\n \n \n // cloud -> device response\n 0000 58 5a 32 b2 0b 00 1d 00 59 5e 42 61 01 00 01 00 XZ2.....Y^Ba....\n 0010 00 00 01 00 54 38 30 31 30 4e 31 32 33 34 35 36 ....T8010N123456\n 0020 38 48 39 3a 00 7b 22 64 65 76 69 63 65 5f 69 70 789a.{\"device_ip\n 0030 22 3a 22 37 31 2e 31 36 32 2e 32 33 37 2e 33 34 \":\"71.162.237.34\n 0040 22 7d \"}\n \n\nWhile there is some interesting information already visible, reversing the protocol and viewing with a decoder is much more informative:\n \n \n [>_>] ---Pushpkt---\n Magic : 0x5a58\n CRC : 0x1234\n Opcode : 0x000b (CMD_DEVICE_HEARTBEAT_CHECK)\n Bodylen : 0x0000\n Time (unix) : 1632154786\n msg_ver : 0x0001\n is_resp : 0x00\n idk_lol : 0x00\n idk_lol2 : 0x0000\n non_zero : 0x0001\n Hub SN : T8010N123456789a\\x00\n \n [<_<] response pkt:\n [>_>] ---Pushpkt---\n Magic : 0x5a58\n CRC : 0x5678\n Opcode : 0x000b (CMD_DEVICE_HEARTBEAT_CHECK)\n Bodylen : 0x001d\n Time (unix) : 1632154746\n msg_ver : 0x0001\n is_resp : 0x01\n idk_lol : 0x00\n idk_lol2 : 0x0000\n non_zero : 0x0001\n Hub SN : T8010N123456789a\\x00\n Msgbody : {\"device_ip\":\"71.162.237.34\"}\n \n\nWhile this specific command doesn\u2019t particularly do much, there does exist a decent amount of other opcodes to interact with:\n \n \n opcode_dict = {\n 0xb : \"CMD_DEVICE_HEARTBEAT_CHECK\",\n 0xc : \"CMD_DEVICE_GET_SERVER_LIST_REQUEST\",\n 0xd : \"CMD_DEVICE_GET_RSA_KEY_REQUEST\", // [1]\n 0x22 : \"CMD_SERVER_GET_AES_KEY_INFO\",\n 0x3ea : \"zx_app_unbind_hub_by_server\",\n 0x3eb : \"zx_start_stream\",\n 0x3ec : \"zx_stream_delete\",\n 0x3f1 : \"zx_set_dev_storagetype_by_SN\",\n 0x40a : \"APP_CMD_HUB_REBOOT\",\n 0x410 : \"zx_unbind_dev_by_sn\",\n 0x464 : \"APP_CMD_GET_EXCEPTION_LOG\",\n 0x46d : \"CMD_GET_HUB_UPGRADE\",\n 0xbb8 : \"turn_on_facial_recognition?\",\n 0xfa0 : \"wifi_country_code_update\",\n 0xfa1 : \"wifi_channel_update\",\n 0x1388 : \"CMD_SET_DEFINE_COMMAND_VALUE\",\n 0x1770 : \"CMD_SET_DEFINE_COMMAND_STRING\"\n }\n \n\nThis advisory deals with the fact that some of these opcodes require authentication, whilst others don\u2019t. Any opcodes greater that 0x10 (i.e. below [1]) require authentication. These authenticated commands are rather powerful and some of them can be exploited for further escalation, but this is a digression, for we now discuss exactly how this authentication occurs. To start, we first visit the `zx_push_receiver_msg_process` function, called on boot to initialize the device\u2019s cloud communication server:\n \n \n 005a10a0 int32_t zx_push_receiver_msg_process()\n 005a1114 dzlog(0x79c99c, 0x17, 0x79dcc4, 0x1c, 0x1d7, 0x28, 0x79cf78, getpid(), 0x83bdb0) {\"src/zx_push_interface.c\"} {\"enter PID=%d\"} {\"zx_push_receiver_msg_process\"}\n 005a1120 init_udp_server_domain()\n 005a112c update_udp_push_config_file()\n 005a113c s_aes_key = 0 \n 005a1178 rand_str(&s_aes_key, rand_len: 0x10) // [2]\n \n\nThe only thing worth noting is that the `s_aes_key` static variable is initialized to a random secret on boot by the `rand_str` function:\n \n \n 004ec5a4 void* rand_str(void* arg1, int32_t rand_len)\n 004ec5c4 int32_t var_20 = 0\n 004ec5e4 void var_18\n 004ec5e4 void var_10\n 004ec5e4 gettimeofday(&var_18, &var_10) // [3]\n 004ec5f8 int32_t var_14\n 004ec5f8 int32_t var_1c = var_14\n \n 004ec608 // seeded with usec\n 004ec614 srand(var_1c) // [4]\n 004ec698 for (int32_t ctr = 0; ctr s< rand_len; ctr = ctr + 1) // [5]\n 004ec674 *(arg1 + ctr) = *((rand_r(&var_1c) & 0x3f) + 0x7895a4) {\"0123456789ABCDEFGHIJKLMNOPQRSTUV\u2026\"} // [6]\n 004ec6b0 *(arg1 + rand_len) = 0\n 004ec6c4 return arg1\n \n\nWe can see the `tv.tv_usec` field of `gettimeofday` [3] being used to seed `srand` at [4], which is totally fine. At [5] we start looping and putting random characters into the output at [6]. The full keyspace looks as such:\n \n \n >>> len(\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~_\")\n 64\n \n\nUp until this point, the secret generation seems relatively normal, but let us now examine how this secret is utilized in the `process_msg` function, which handles validation and authentication:\n \n \n 005a28a0 int32_t aes_key_len = strlen(s_aes_key, pkttime)\n 005a28b4 if (aes_key_len != 0x10)\n 005a2d60 $v0_9 = dzlog(0x79c99c, 0x17, 0x79dc6c, 0xb, 0x33b, 0x28, 0x79d3f0) {\"src/zx_push_interface.c\"} {\"process_msg COMM_RANDOM_AES_ENCR\u2026\"} {\"process_msg\"}\n 005a28c0 else\n 005a28c0 int32_t inp_key_str = 0\n 005a2924 if (get_aes_key_info_by_packetid(str: s_aes_key, packetid: devpkt_arg1->time, output: &inp_key_str) != 0) // [7]\n 005a2d0c $v0_9 = dzlog(0x79c99c, 0x17, 0x79dc6c, 0xb, 0x337, 0x28, 0x79d39c) {\"src/zx_push_interface.c\"} {\"process_msg COMM_RANDOM_AES_ENCR\u2026\"} {\"process_msg\"}\n 005a2948 else\n 005a2948 struct aes_key_st* preexpanded\n 005a2948 int_aes_decryptkey(inputkey: &inp_key_str, aeskey: &preexpanded) // [8]\n 005a2978 void some_output_s0x448_l0x401\n 005a2978 memset(&some_output_s0x448_l0x401, 0, 0x401)\n 005a2984 int32_t var_bb8_1 = 0\n 005a29b4 int32_t declen = aes_decrypt(key: &preexpanded, inp_enc: &devpkt_arg1->hub_sn, inplen: 0x10, decbytes: &some_output_s0x448_l0x401) // [9]\n 005a29cc int32_t sn_enc_strncmp\n 005a29cc if (declen == 0x10)\n 005a29f0 sn_enc_strncmp = strncmp(g_hub_sn, &some_output_s0x448_l0x401, 0x10) // [10]\n 005a29cc if (sn_enc_strncmp != 0)\n 005a2a58 dzlog(0x79c99c, 0x17, 0x79dc6c, 0xb, 0x310, 0x28, 0x79d2f0, &some_output_s0x448_l0x401, 0x881c30) {\"src/zx_push_interface.c\"} {\"process_msg COMM_RANDOM_AES_ENCR\u2026\"} {\"process_msg\"}\n 005a2a68 $v0_9 = send_device_packet_by_command_id(opcode: 0xd)\n \n\nAfter getting through some basic validation of the packet length, packet time, etc, we get to the authentication. This process starts at [7], where the previously generated `s_aes_key` is taken, modified and then thrown into the `inp_key_str` stack variable at [7]. We\u2019ll skip the actual implementation of `get_aes_key_info_by_packetid` for a second and continue looking at this function. This new key is utilized as the secret for `AES_set_decrypt_key` inside `int_aes_decryptkey`[8], and then the decryption of our input packet\u2019s `hub_sn` field occurs at [9]. This decrypted string is then compared against the device\u2019s serial number at [10]. If it\u2019s a match, we\u2019ve successfully authenticated. At first glance this might appear secure, but there are two very important facets of this code that we must examine further.\n\nWhile the `hub_sn` eventually is encrypted for authenticated packets, as long as we can sniff the wire, this string flows periodically to the cloud servers unencrypted. Take for example the previously posted example push packet:\n \n \n [>_>] ---Pushpkt---\n Magic : 0x5a58\n CRC : 0x1234\n Opcode : 0x000b (CMD_DEVICE_HEARTBEAT_CHECK)\n Bodylen : 0x0000\n Time (unix) : 1632154786\n msg_ver : 0x0001\n is_resp : 0x00\n idk_lol : 0x00\n idk_lol2 : 0x0000\n non_zero : 0x0001\n Hub SN : T8010N123456789a\\x00 // [11]\n \n\nWe can clearly see this `hub_sn` value [11] over the wire all the time, so this is a known value. Keeping this in mind, the second facet exists within the glossed-over `get_aes_key_info_by_packetid` function:\n \n \n 00551658 int32_t get_aes_key_info_by_packetid(char *inpstr, int32_t packetid, char* output)\n 00551680 void pktid_str\n 00551680 int32_t pktidlen\n 00551680 if (str != 0 && output != 0)\n 005516cc strncpy(output, str, strlen(str)) // [12]\n 005516fc sprintf(&pktid_str, 0x79071c, packetid) // \"%d\" // [13]\n 0055171c pktidlen = strlen(&pktid_str)\n \n\nTo start and reiterate, the input secret is our randomly generated 0x10 length `s_aes_key`, and the `packetid` field is the `devpkt->time` field that we provide in our packet. At [12] the input secret is appropriately `strncpy`\u2018ed to the output, and then at [13] we take the decimal format of the `devpkt->time`. For a quick example of what this would look like:\n \n \n >>> str(time.time()).split(\".\")[0]\n '1632411932'\n \n\nIt\u2019s important to note that the input `devpkt->time` field must be within 60 seconds of the unix time of the Eufy Homebase device, or else it is considered invalid and we never get to the authentication. Thus, since enough time has passed since epoch, the length of this string will always be 10 bytes. Also, to see the Eufy device\u2019s time, it suffices to sniff network traffic and pull it from one of the outbound packets. Continuing on within `get_aes_key_info_by_packetid`:\n \n \n 00551680 if (str != 0 && output != 0)\n 005516cc strncpy(output, str, strlen(str)) \n 005516fc sprintf(&pktid_str, 0x79071c, packetid) // \"%d\" // [14]\n 0055171c pktidlen = strlen(&pktid_str)\n 00551680 int32_t ret\n 00551680 if (str == 0 || (str != 0 && output == 0) || (str != 0 && output != 0 && pktidlen s>= 0x10))\n 0055178c ret = 0xffffffff\n 00551680 if (str != 0 && output != 0 && pktidlen s< 0x10)\n 00551774 memcpy(&output[0x10 - pktidlen], &pktid_str, pktidlen) // [15]\n 00551780 ret = 0\n 00551798 return ret\n \n\nAfter the `pktid_str` is generated at [14], it is then `memcpy`\u2018ed on top of our `s_aes_key` copy at [15]. Since this occurs in an overlapping fashion and not in an appending fashion, the output secret will look something like `aC01_?` \\+ `1632411932`, i.e. the first bytes of our randomly generated `s_aes_key` and then the unix time as a string. While the output secret is the same length as we had before, 0x10, this function reduces entropy of our AES encryption from 0x10 bytes to 0x6 bytes, and our keyspace goes from 64^16 (79228162514264337593543950336 combinations) to 64^6 (68719476736 combinations).\n\nTo summarize all this into a final vulnerability: Since we know what the decrypted authentication string is supposed to be (our device\u2019s `g_hub_sn`, `T8010N123456789a`), and we also know the last 10 bytes of the encryption key that\u2019s used for a given packet (the unix time), then it easily becomes possible to offline bruteforce the first six bytes of the AES secret that\u2019s actually used to encrypt the `g_hub_sn`. Once we know these first 6 bytes, it does not matter what the current unix time is, we can simply append the first six bytes of the bruteforced `s_aes_key` to the current unix time and gain privileges, resulting in an authentication bypass.\n\n### Timeline\n\n2021-09-30 - Vendor Disclosure \n2021-11-22 - Vendor Patched \n \n2021-11-29 - Public Release\n", "cvss3": {"exploitabilityScore": 3.9, "cvssV3": {"baseSeverity": "HIGH", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "NONE", "integrityImpact": "NONE", "baseScore": 7.5, "privilegesRequired": "NONE", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "userInteraction": "NONE", "version": "3.1"}, "impactScore": 3.6}, "published": "2021-11-29T00:00:00", "type": "talos", "title": "Anker Eufy Homebase 2 home_security get_aes_key_info_by_packetid() authentication bypass vulnerability", "bulletinFamily": "info", "cvss2": {"severity": "MEDIUM", "exploitabilityScore": 10.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "PARTIAL", "availabilityImpact": "NONE", "integrityImpact": "NONE", "baseScore": 5.0, "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "version": "2.0", "accessVector": "NETWORK", "authentication": "NONE"}, "acInsufInfo": false, "impactScore": 2.9, "obtainUserPrivilege": false}, "cvelist": ["CVE-2021-21955"], "modified": "2021-11-29T00:00:00", "id": "TALOS-2021-1382", "href": "https://www.talosintelligence.com/vulnerability_reports/TALOS-2021-1382", "cvss": {"score": 5.0, "vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N"}}]}