Taking Back What Is Already Yours: Router Wars Episode III

In the previous part, we gathered the firmware and caught the password for Root user. Note that the password is for the web Interface.

First, I want to log in to the web interface to see if the Root user has extra control than usual admin user. Also it is easier to try than reversing the firmware. I had no problem with logging in and, indeed, the Root user is more privileged than admin user.

The first thing I did is to change Root’s password. Then I started discovering the interface. Have you seen CWMP menu option on the left? CWMP stands for CPE WAN Management Protocol and that menu is where we can set our ACS URL, inform period, etc.

So, I think if we set up our ACS and set its URL from the web interface, we can access and change more of settings. I decided to install genieacs which is an open-source ACS software. The installation process was easy and clean with the instructions in genieacs docs. Geniacs has three binaries:

  • genieacs-cwmp: It is the main ACS binary. By default in runs on port 7547.
  • genieacs-nbi: This is the server we’ll send our queries to. Basically, it is an administrator interface to communicate with genieacs-cwmp.
  • geniacs-fs: This is the fileserver to serve firmware, config files, etc.

Since I don’t want to serve anything for now, I only started geniacs-cwmp and genieacs-nbi. From the web interface, I set http://psaux.io:7547 as ACS URL without a username or password. I also updated the inform period to 5 seconds, so that I don’t have to wait 86400 seconds (24 hours) to get a connection. With the help of this API reference, I started to poll for new connections but no luck.

1
2
3
4
$ curl localhost:7557/devices
[

]

There may be a limitation on the router. Maybe it only connects to acs.superonline.net. Once again, I took my laptop and two ethernet converters, sat beside my router, started sniffing communication hoping that I see something related. And yes, I could see that the router was sending DNS queries for psaux.io but never connected to it.

So it’s an ip restriction. With high hopes I installed genieacs on my laptop which is connected to LAN, changed CWMP settings accordingly but received no connection again. It’s life. You don’t get lucky all the time. So now it’s time to take the harder path: reversing the firmware. Let’s run binwalk on the firmware:

1
2
3
4
5
$ binwalk HG253sV100R001C01B039_upgrade_main.bin

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
131200 0x20080 JFFS2 filesystem, big endian

It seems like there is a JFFS2 filesystem embedded in the binary. Let’s extract the image with:

1
2
3
4
5
6
7
8
$ binwalk -e HG253sV100R001C01B039_upgrade_main.bin

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
131200 0x20080 JFFS2 filesystem, big endian

$ ls
20080.jffs2 HG253sV100R001C01B039_upgrade_main.bin

I’ll use jffs2dump script written by project-magpie to dump the image. First convert the endianness:

1
2
3
4
5
6
$ jffs2dump -v -b -r -edum.bin 20080.jffs2
Wrong bitmask at 0x00c00000, 0xbd61
Wrong bitmask at 0x00c00004, 0x0000
Wrong bitmask at 0x00c00008, 0x0000
Wrong bitmask at 0x00c0000c, 0x0000
Wrong bitmask at 0x00c00010, 0x0000

Note that jffs2dump here isn’t the python script, it’s a tool that belongs to mtd-utils. We got “Wrong bitmask” warnings, so let’s fix them with sumtool:

1
$ sumtool -l -i dum.bin -o dum.sum -v

Now we can run the python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ python jffs2-dump.py dum.sum
.
.
.
root/etc/passwd
root/etc/printers.ini
root/etc/profile
root/etc/radius.conf
root/etc/resolv.conf
root/etc/root.crt
Traceback (most recent call last):
File "jffs2-dump.py", line 285, in <module>
unpack_main(sys.argv[1], 'root')
File "jffs2-dump.py", line 277, in unpack_main
for din in inodes[n.ino]:
KeyError: 157

Damn, it exited with an error and left a lot of missing files. After spending some time, I added try/except block around the line we got the error to ignore KeyErrors.

1
2
3
4
5
6
7
try:
for din in inodes[n.ino]:
if din.dsize > 0:
cur_file.seek(din.offset)
cur_file.write(din.get_data())
except:
pass

Now let’s run it again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ python jffs2-dump.py dum.sum 
root/cferam.000
root/linuxrc
root/status
root/vmlinux.lz
root/bin/acs_cli
root/bin/acsd
root/bin/adduser
root/bin/ash
root/bin/at
.
.
.
root/html/lang/zh/wlaninfo.res
root/html/lang/zh/wlbridge.res
root/html/lang/zh/wlmacflt.res
root/lib/kernel/crypto/ecb.ko
root/lib/kernel/crypto/pcbc.ko
root/lib/kernel/drivers/scsi/scsi_wait_scan.ko
root/lib/kernel/drivers/watchdog/bcmdog.ko
root/lib/kernel/net/netfilter/xt_tcpmss_j.ko
root/lib/kernel/drivers/usb/serial/option.ko

No errors. The image extracted to a folder named root.

1
2
3
4
$ cd root/
$ ls
bin config etc lib mnt sbin tmp var voip
cferam.000 dev html linuxrc proc status usr vmlinux.lz

It looks like a usual linux system. After looking into folders, I found some interesting files. I won’t go through them here but I want to mention just one of them:

1
2
3
4
5
6
7
8
9
10
$ cat etc/ssh/authorized_keys 
ssh-dss AAAAB3NzaC1kc3MAAACBAJPULv45yonyV4BbBLVxj3iYxjMZvPL7cHR45iPsxMpUehc7
BuzdQyylNMzIZHhN8NrnNB1nJ1IJ5JOTLlPIuj89R4qmNOm+flKEQkWpcKALck1327VaJVZFP0F6
IsEePxqTbXa/5RRyvqwy0fWL8/iJilWeZiUHDfoall0Sw4/zAAAAFQDViCwqGmgXRMuLrq/+V0aW
jzbBwQAAAIBgJa4nVDo4XgR4Tm/FANieoCV2bXlkvPS+wKZ1X68FVh12DQVAaqLq4vTqdlQ0dkkG
luTIpgk8cVo0lRoU3a53h9uq2TPqEeNIh8c5qOXWPiW4XfxenRFiEkjz3uaIHv7tUh5cwq/RZ4lS
q7fmkU/FzfTMPS+cjrUflbeOhQ/OJAAAAIAkf85UnqPpfsfirD1xg2WVxd+Av4HFplX+c+J1mf3Z
3K551eO9wzEMCOiFTSbK/YlqU+J9C+NitLTDuvJyNADpMF3hu9wbR39ejlhr0JM2SCTWems/3oe7
HX3ijTR7YJBOG0tMQMLpXOcLpCac3CANnH14CpVA5ODP8Ml1S4K9Vw== z00163152@HUAWEI-62
7FB9A3

Maybe an engineer from Huawei (I assume z00163152@HUAWEI-627FB9A3) who owns a specific DSS key, can connect all HG253s routers without needing a password, who knows?

Alright, enough of corporation backdoors, I want to look at ATP Cli from the first part because I think it’s very promising. It must be accepting some commands which may provide us a wider perspective. To find which binary handles ATP Cli, I simply ran:

1
2
$ grep -nr "Welcome to ATP Cli"
Binary file bin/cli matches

The string “Welcome to ATP Cli” is hardcoded in bin/cli. Let’s inspect bin/cli with strings command and find out what other strings are hardcoded:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ strings bin/cli
.
.
.
console
Login:
HG253s56jiioHJ5867nihNJI255KOIg7
The console is prohibited!
Login incorrect!
Password:
/var/telnetIpaddr
open telnetIpaddr fail
get telnetIpaddr fail
rm -rf /var/telnetIpaddr
telnet unlocked and login from %s successfully.
telnet login from %s successfully.
console unlocked and login successfully.
console login successfully.
telnet login failed %d times: locked.
console login failed %d times: locked.
Authorization failed after trying %d times.Please wait one minute.
telnet login failed:Authorization failed.
console login failed:Authorization failed.
Login incorrect. Try again.
CLI login return %x
console logout:timeout.
telnet logout:timeout.
ATP>
exit
logout
quit
console logout by command.
telnet logout by command.
ssh logout by command.
shell
debug display cwmp
debug display sysuptime
debug display atpversion
save
help
cpuinfo
meminfo
arptable
ifconfig
iptables
nattable
ping
traceroute
netstat
Command failed.
Byebye! Have a nice day!
.
.
.
ing %s > %s 2>/dev/null
/var/PingInfo
ping [ip|domain]
ping result:%s
%s > %s
cat proc/loadavg
/var/CPUInfo
cat proc/meminfo
/var/MemInfo
cat proc/net/nf_conntrack
/var/NatTableCurSize
nattable currentsize
cat proc/sys/net/nf_conntrack_max
/var/NatTableTotalSize
nattable totalsize
/var/NatTableInfo
nattable info
ifconfig %s > %s 2>&1
/var/IfconfigInfo
ifconfig > %s 2>&1
iptables %s > %s 2>&1
/var/IptablesInfo
iptables > %s 2>&1
.
.
.

It seems like it’s indeed accepting some commands. To confirm I connected to ATP Cli and ran:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-------------------------------
-----Welcome to ATP Cli------
-------------------------------
ATP>ping psaux.io
ping psaux.io
ping result:
PING psaux.io (104.248.35.218): 56 data bytes
64 bytes from 104.248.35.218: seq=0 ttl=53 time=50.722 ms
64 bytes from 104.248.35.218: seq=1 ttl=53 time=47.797 ms
64 bytes from 104.248.35.218: seq=2 ttl=53 time=47.537 ms

--- psaux.io ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 47.537/48.685/50.722 ms

That’s quite a response! Since we’ve found out that there is an IP restriction on ACS URL before, iptables command took my attention:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
ATP>iptables -L
iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
INPUT_URLFLT all -- anywhere anywhere
INPUT_SERVICE_ACL all -- anywhere anywhere
INPUT_SERVICE all -- anywhere anywhere
INPUT_FIREWALL all -- anywhere anywhere

Chain FORWARD (policy ACCEPT)
target prot opt source destination
ACC_FLT all -- anywhere anywhere
FWD_IPFLT all -- anywhere anywhere
FWD_APPFLT all -- anywhere anywhere
FWD_URLFLT all -- anywhere anywhere
FWD_SERVICE all -- anywhere anywhere
FWD_FIREWALL all -- anywhere anywhere
DROP all -- anywhere anywhere

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
OUT_SERVICE all -- anywhere anywhere
DROP tcp -- anywhere anywhere tcp dpt:7547

Chain ACC_FLT (1 references)
target prot opt source destination

Chain FWD_APPFLT (1 references)
target prot opt source destination

Chain FWD_FIREWALL (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere
ACCEPT all -- 192.168.239.0/24 anywhere
LOG tcp -- anywhere anywhere tcp flags:SYN,RST,ACK/SYN limit: avg 6/hour burst 5 LOG level alert prefix `Intrusion -> '

Chain FWD_IPFLT (1 references)
target prot opt source destination

Chain FWD_SERVICE (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED

Chain FWD_URLFLT (1 references)
target prot opt source destination

Chain INPUT_FIREWALL (1 references)
target prot opt source destination
LOG tcp -- anywhere anywhere tcp flags:SYN,RST,ACK/SYN limit: avg 6/hour burst 5 LOG level alert prefix `Intrusion -> '
DROP all -- anywhere anywhere

Chain INPUT_SERVICE (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED
ACCEPT udp -- anywhere anywhere udp dpt:500
ACCEPT udp -- anywhere anywhere udp dpt:4500
ACCEPT esp -- anywhere anywhere
ACCEPT udp -- anywhere anywhere udp dpts:bootps:bootpc
ACCEPT udp -- anywhere anywhere udp dpt:6050
ACCEPT udp -- anywhere anywhere udp dpt:6050
ACCEPT udp -- anywhere anywhere udp dpts:50000:50020

Chain INPUT_SERVICE_ACL (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED
ACCEPT all -- anywhere anywhere
ACCEPT tcp -- anywhere anywhere tcp dpt:www
ACCEPT icmp -- anywhere anywhere
ACCEPT tcp -- anywhere anywhere tcp dpt:2323
ACCEPT tcp -- anywhere anywhere tcp dpt:ftp
ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
ACCEPT tcp -- anywhere anywhere tcp dpt:https
ACCEPT tcp -- anywhere anywhere source IP range 85.29.0.1-85.29.63.254 tcp dpt:3050
ACCEPT icmp -- anywhere anywhere source IP range 85.29.0.1-85.29.63.254
ACCEPT icmp -- anywhere anywhere source IP range 10.31.0.1-10.31.255.254
ACCEPT tcp -- anywhere anywhere source IP range 85.29.0.1-85.29.63.254 tcp dpt:https
ACCEPT tcp -- anywhere anywhere source IP range 85.29.0.1-85.29.63.254 tcp dpt:www
DROP tcp -- anywhere anywhere multiport dports ftp,www,ssh,2323,3050,https
DROP icmp -- anywhere anywhere

Chain INPUT_URLFLT (1 references)
target prot opt source destination

Chain OUT_SERVICE (1 references)
target prot opt source destination
ACCEPT tcp -- anywhere anywhere destination IP range 85.29.0.1-85.29.63.254 tcp dpt:7547

The important parts of this output are:

1
2
3
4
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
OUT_SERVICE all -- anywhere anywhere
DROP tcp -- anywhere anywhere tcp dpt:7547

and

1
2
3
Chain OUT_SERVICE (1 references)
target prot opt source destination
ACCEPT tcp -- anywhere anywhere destination IP range 85.29.0.1-85.29.63.254 tcp dpt:7547

at lines 20-23 and 84-86 respectively. We can see that our ACS port 7547 is added as a rule in iptables but it only accepts connections to the IPs in range 85.29.0.1-85.29.63.254. To remove the restriction let’s flush OUTPUT chain:

1
2
ATP>iptables -F OUTPUT
iptables -F OUTPUT

Now we can check our ACS to see if there is a connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ curl localhost:7557/devices
[
{"_id":"00E0FC-HG253s-XXXXXXXXXXXXXXXX","InternetGatewayDevice":{"DeviceInfo
":{"HardwareVersion":{"_object":false,"_timestamp":"2020-03-23T05:17:07.480Z
","_type":"xsd:string","_value":"VER.A"},"ProvisioningCode":{"_object":false
,"_timestamp":"2020-03-23T05:17:07.480Z","_type":"xsd:string","_value":""},"
SoftwareVersion":{"_object":false,"_timestamp":"2020-03-23T05:17:07.480Z","_
type":"xsd:string","_value":"HG253sC01B039"},"SpecVersion":{"_object":false,
"_timestamp":"2020-03-23T05:17:07.480Z","_type":"xsd:string","_value":"1.0"}
,"_object":true},"DeviceSummary":{"_object":false,"_timestamp":"2020-03-23T0
5:17:07.480Z","_type":"xsd:string","_value":"InternetGatewayDevice:1.1[](Bas
eline:1, EthernetLAN:1, USBLAN:1, WiFiLAN:1, EthernetWAN:1, Bridging:1, Time
:1, IPPing:1, DeviceAssociation:1, UDPConnReq:1)"},"ManagementServer":{"Conn
ectionRequestURL":{"_object":false,"_timestamp":"2020-03-23T05:17:07.480Z","
_type":"xsd:string","_value":"http://10.26.13.246:3050/eeflacbsvmtvm"},"Para
meterKey":{"_object":false,"_timestamp":"2020-03-23T05:17:07.480Z","_type":"
xsd:string","_value":""},"_object":true},"WANDevice":{"1":{"WANConnectionDev
ice":{"1":{"WANPPPConnection":{"1":{"ExternalIPAddress":{"_object":false,"_t
imestamp":"2020-03-23T05:17:07.480Z","_type":"xsd:string","_value":"10.26.13
.246"},"_object":true},"_object":true},"_object":true},"_object":true},"_obj
ect":true},"_object":true},"_object":true},"_deviceId":{"_Manufacturer":"Hua
wei","_OUI":"00E0FC","_ProductClass":"HG253s","_SerialNumber":"XXXXXXXXXXXXX
XXX"},"_lastBootstrap":"2020-03-23T05:17:07.479Z","_lastInform":"2020-03-23T
05:17:07.479Z","_registered":"2020-03-23T05:17:07.479Z"}

Nice! Now let’s ask for the full variable list:

1
2
$ curl -i 'localhost:7557/devices/00E0FC-HG253s-XXXXXXXXXXXXXXXX/tasks' \ 
-d '{"name":"getParameterValues", "parameterNames":["InternetGatewayDevice."]}'

After a few seconds, the router informed about its 1352 parameters. Here is the full list for the ones interested. In all these parameters, these took my attention:

  • InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetEnable
  • InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetUserName
  • InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetPassword
  • InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetPort

Not that “ServiceManage” is not a typo I made. Also I searched for ssh credential parameters but there was none. I retrieved the values of above parameters with:

1
2
3
$ curl localhost:7557/devices/00E0FC-HG253s-XXXXXXXXXXXXXXXX/tasks  \ 
-d '{"name":"getParameterValues", "parameterNames":["InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetUserName",
"InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetPassword", "InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetPort", "InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetEnable"]}'

and the response came in quickly:

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
{
"TelnetEnable": {
"_object": false,
"_writable": true,
"_timestamp": "2020-03-23T06:06:01.200Z",
"_type": "xsd:boolean",
"_value": false
},
"TelnetPassword": {
"_object": false,
"_writable": true,
"_timestamp": "2020-03-23T06:06:01.200Z",
"_type": "xsd:string",
"_value": ""
},
"TelnetPort": {
"_object": false,
"_writable": true,
"_timestamp": "2020-03-23T06:06:01.200Z",
"_type": "xsd:int",
"_value": 2323
},
"TelnetUserName": {
"_object": false,
"_timestamp": "2020-03-23T06:06:01.200Z",
"_type": "xsd:string",
"_value": "Root",
"_writable": true
}
}

Let’s enable telnet:

1
2
3
$ curl localhost:7557/devices/00E0FC-HG253s-XXXXXXXXXXXXXXXX/tasks \ 
-d '{"name":"setParameterValues", "parameterValues":[["InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetEnable", true, "xsd:bool"],
["InternetGatewayDevice.DeviceInfo.X_ATP_ServiceManage.TelnetPassword", "somesecurepassword", "xsd:string"]]}'

And then connect to telnet:

1
2
3
4
5
6
7
8
9
10
11
$ telnet 192.168.1.1 2323
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
-------------------------------
-----Welcome to ATP Cli------
-------------------------------

Login: Root
Password:
ATP>

Our old friend ATP Cli again… Wait, if I could log in to ATP Cli with the new password, maybe that password will also work for ssh. I tried afterwards and yes it’s also changed.

So we managed to change passwords for both ssh and telnet, gain access to Root user for the web interface, changed that password too. We changed ACS URL to ours and remove the IP restrictions. To put it simply, we cleaned up our router from our ISP. Good for our privacy.

Still there is an authorized ssh key left in the firmware but for now it’s enough that we’re keeping the ISP out. Maybe in the future, we can repack the firmware with our configuration and keys and install it on the router. For now, take care!

Note: The main discussion about these post series is here.