NETGEAR R6700v3: 1day Analysis (CVE-2021-34982) Buffer Overflow RCE Vulnerability
About the vulnerability
This vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of multiple NETGEAR routers. Authentication is not required to exploit this vulnerability. The specific flaw exists within the httpd service, which listens on TCP port 80 by default. When parsing the strings file, the process does not properly validate the length of user-supplied data prior to copying it to a fixed-length stack-based buffer. An attacker can leverage this vulnerability to execute code in the context of root.
After reading the description, I tried using Bindiff, combined the vulnerability description, and I revoked the scope at http_d() function.
Through Bindiff's flowgraph and decompiled code on IDA, I found out that the parameter parsing segment of the String Table Upload has been removed.
I can be sure that the vulnerability is in the String File Table Upload. After a while of searching, I found the vulnerability in the Create_Hashtable() function. The vulnerability occurred when gui_region was copied to the array on the stack without checking the size beforehand.
Recreate the bug
First I will talk briefly about String File Table Upload. String File Table Upload is used to upload configuration files such as language, version, etc. The file content submitted by this function will be handled by the http_d() function.
The http_d() function will receive the request from the socket. Check if the request is for file upload functions. If true, the file upload function will be executed, otherwise the sub_10764() function will be called. The sub_10764() is in charge of parsing HTTP requests, performing authentication, dispatching requests, etc.
Although strtblupload.cgi has been removed in this version, I found that the parameters of the String File Table Upload are executed in the same place with the parameters of other upload file functions, without checking the URL is correct or not.
After looking through the parameter parsing of the http request of the httpd() function, I see that it is possible to take advantage of the upgrade_check.cgi function to get into the parameter parsing of the String File Table Upload.
I will edit some parts of the request so that http_d() jumps into the String File Table Upload function.
After parsing the request parameters, the file content will be separated into header and body. The header contains file_size and checksum.
The body part is copied to the file /tmp/strtbl then renamed to /tmp/strtbl.bz2 and passed through the file checks.
After completing, the file is unzipped and takes 2 main parameters: language and version. The language parameter will be passed to reloadHashTable().
The reloadHashTbl() function will then be called. This function has the main function of parsing the file, creating a hash for the parameters in the file such as language, version, etc, creating a separate memory area and saving the content of those parameters in the corresponding memory area. But the reloadHashTbl() function doesn't check the size when saving the parameter content to their memory, so I can send the payload to the gui_region via the language parameter.
After the reloadHashTbl() function finishes, the stringOut() function will be used to get the content stored in the hash string regions of the language and version then pass the content to the langSaveNvram() function to copy the language's content into the gui_region , and the contents of version into str_tbl_non_en_ver. Now our payload is in gui_region.
Now back to the reloadHashTbl() function. This function will call the Create_Hashtable() function. To get to the vulnerability location, I will have to send another String File Table Upload request to re-enter this function, then our payload will be copied from the gui_region to the stack causing stack based buffer overflow. But before entering the vulnerability position I will have to satisfy the condition “multiLanguageReadLangTblMTD(v2) == -1”.
Because the Create_Hashtable() function calls the multiLanguageReadLangTblMTD() function to check the conditions to determine whether to create the default String File Table, or use the file I sent. To check the condition, the content in mtdn_lang_name will be compared with the language parameter passed to reloadHashTable() to check.
The reloadHashTbl() function before calling Create_Hashtable() will call checkStringTblInMTD() first. This function will read the contents of our file and copy 64 bytes of content in each language, version parameter to mtd9_lang_name and mtd9_lang_version respectively.
Thus, to satisfy the condition that I want, I only need to transmit more than 64 bytes of any character. I have finally determined the location and condition of the error, now let's start exploiting.
Exploitation of this stack overflow is a little bit complicated by a few factors:
+ NX bit of httpd is set.
+ The device ASLR is set to 1, which means that the stack and library addresses are not fixed.
+ The copy which overflows the stack is a string copy. As such, it will stop copying characters if it encounters a NULL character. Thus, the exploit cannot include gadgets with NULL bytes.
First, since the NX bit has been set, I will have to perform other exploit techniques such as ret2libc. But due to the presence of ASLR and no stack address leak, it will be more difficult for us to find the right ROP. The Create_Hashtable() function is a vulnerability function, so I will get the stack address after this function has just finished as the base for calculation. After debugging for a while, I found the address where the request is saved when we send the file.
I proceed to find out if there is a ROP that can help me adjust the sp register to the memory area where the request is stored. Initially during the parsing request, the file content until the end of the request will be copied by fwrite(), so I can pass any byte here.
After the search I will get the address 0x 009C640 as ROP1.
Now the stack has reached the position I want. Next I will find a way to take advantage of the address of the sp register as a parameter to the system. After a while of searching I found the address 0x009FB74 which helps me to pass sp into r0 and call system, this will be ROP2.
I can't pass the language parameter with the payload as usual. If the content is longer than 64 bytes, it will enter the error location, but our String File Table will be replaced with the default file, resulting in gui_region containing the content of the default file. That is, I have to find a way to make the language parameter both contain the payload and satisfy the condition that it is less than 65 bytes.
As I have mentioned earlier that the language parameter passed to the reloadHashTable() function will be parsed after the first unpacking. During the parsing process to find the language parameter, the program keeps reading until the end of the file. So I can take advantage of this to create another language parameter containing 64 bytes of arbitrary characters. Besides, with the language parameter containing the payload, I will remove the character byte ‘ ” ’ so that the stristr() function ignores the language parameter containing the payload.
So when parsing, the language parameter containing 64 bytes of characters will be read and passed to the language variable and I can be sure that the Create_Hashtable() function will use the user file. But there is one more problem, having two language parameters like that results in two memory areas being created for these two parameters. So I need to see the content of which parameter will be returned. After looking through the insertMultiLangugeHashTable() function I found that the function will create a single linked list to contain the contents with the same parameters.
And in the function getMultiLanguageStrByKey() I can easily see that the first parameter of the list will be retrieved.
Therefore, I will put the language parameter with the payload first and the parameter with 64 bytes of characters after. So I've made sure that the file you submitted will be used and the payload is still written to the gui_region.
Since the gui_region will be read first and then the new content written in, the payload will only be saved to the gui_region when transmitting the first request. To trigger the vulnerability I will send a second request, this time when the gui_region is read, it will contain our payload, causing a stack based buffer overflow. Also because I have to send 2 requests, the memory area containing the request will change plus the function that causes the error is strcpy(). So I will put ROP1 in the file when sending request1 and ROP2 will be put at the end of request2.
With request2 in addition to putting ROP2 at the end of the request, I will leave the content of the language parameter larger than 64 bytes to be able to trigger the vulnerability.
With all the steps completed, I will now proceed to write the Python script and run it.