TP-Link TL-WR940N: Analysis of a 1day (CVE-2022-24355) Buffer Overflow RCE Vulnerability

About the bug

CVE-2022-24355 allows network-adjacent attackers to execute arbitrary code on affected installations of TP-Link TL-WR940N routers. Authentication is not required to exploit this vulnerability. The specific flaw exists within the parsing of file name extensions. The issue results from the lack of proper validation of 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. (www.zerodayinitiative.com)

After reading the description, I think the bug is like common buffer-overflow which is trigger by send a request for a file with very long file extension (e.g., index.aaaa..........aaaa). However, this case is different.

Re-create the bug

From the init function named httpd(), to make the program call httpRpmFs() function, the request url must contain “/fs/” or “/loginFs/”.

Moreover, referer ip in the request must equal to host ip to pass the security check in httpDispatcher() function.

Now, about the vulnerable function, httpRpmFs()is used to read and response content of the requested file if it exists in /tmp/ or /web/ folder.

At first, I think the flaw is strcat(v2, v3) but, both v2 and v3 is alloc-ed and used on heap only. So, I tried to compare this httpRpmFs() with the fixed one. They are mostly the same, except there is a new security check if you want to read file in /tmp/ folder.

From that clue, I made several requests to read every file in /tmp/ and finally, it crashed when I tried to read a file named passwd. Look at the debugger, a part of request was written in stack and cause overflow.

Let’s go back with the code and find out how can this happen. At htttpRpmFs() function, after successful read file, the program will make a response contain the content of that file.

Function sub_5299E0() is called to detect the response’s Content-Type that match with the file extension. This is where the overflow begin

Function sub_5299E0() find the file name extension by create a pointer that point to the end of file path. After that, the point will be moved backward until it meet the ‘.’ character. Finally, sub_5299E0() will copy all the character after the ‘.’ character one-by-one until it meet the null byte. The copied character will be changed to upper key and saved into stack. In our case, passwd has no file extension, the pointer was moved backward, pass the file path, reach the saved request header area and only stop when it met the last ‘.’ in the ip address of Referer section. As a result, everything after it was copied to stack and overwrite the saved registers values. After sub_5299E0() finished, these values were pop-ed back to the register and cause the program crash.

Writing an Exploit

I don’t have the actual router, so I use firmware-analysis-toolkit to emulate the firmware. After a little research, I found that the router’s ASLR was disable by default so I also disable ASLR on my emulator. Since the program have no protection, I will use the overflow bug to store the shellcode into stack and run it.

From the debug above, I’ve created a pay load stored in cookie section of the request. The payload contains 16 bytes buffer, a stack pointer to shellcode and the shellcode.

Everything look perfect and I thought this will be easy, but the request only make the program crash without any shell or connect back. I went back to debug and found out the problem was the toupper() function. The shellcode contains some bytes in range of 0x61 to 0x7a (‘a’ to ‘z’ in ascii). These bytes will be changed when go through toupper() function (minus 0x20) which cause the shellcode not working.

So, I found a solution to bypass toupper() that is encode the shellcode before send to router. Luckily, the pwntool lib has a XORencode function for this case. However, after encoded, the shellcode still have one byte 0x70.

I’ve tried to disassemble 0x01c07027 and receive an instruction “nor $t6, $t6, $zero”. To terminate the bad byte, I’ve changed the encoded shellcode a little. Since $t1 register has the same purpose with $t6 and it was not used in both shellcode and encode, I’ve replaced $t6 register with $t1 and the bad byte is disappeared.

Moreover, I’ve learned that a MIPS router has cache coherency to increase the speed of memory access by conducting reads and writes to main memory asynchronously. After thestack buffer overflow overwrote the stored return address with shellcode address, the processor directed execution to the correct location because the return address was data. However, it executed the old instructions that still occupied the instruction cache, rather than the ones that recently written to the data cache. In order to make shellcode work, I have to flush the data and instruction cache by calling a sleep() function. I borrowed a ROPchain from old exploit code (www.exploit-db.com/exploits/46678) and modify it a little to work with my case.

The request also needs to edit to match with the new payload.

Finally, the exploit is complete and ready to run.

Tác giả: Thanhnc41