Reverse-Engineering the ZTE ZXDSL 931WIIA Firmware

| tags: asm sec

Quite some time ago (late 2011) I got sufficiently bored to poke and prod the firmware of a ZTE ZXDSL 931WIIA brand VDSL2 device, primarily to find out if it had a usable telnet interface and/or a convenient way to run arbitrary code. I always meant to write up a description of what was hiding under this particular rock, but never got around to. Well, here we (belatedly) are.

Disclaimers: Most things in the following are likely to be — more or less — particular to the exact model and firmware version (ZXDSL931WIIA_ElisaV2.8.2a_Z40_FI) I have. The model is probably discontinued by now, but perhaps the illustrated principles may be helpful for someone. If you still have one, don't try any of this at home if you're very attached to the device. Finally, the approach taken is a very "manual" one (GNU binutils, dd, etc.), and more intelligent tools could well make it simpler.

tl;dr? There is a telnet interface (in this version), with hardcoded ZTE "debugging" username:password pairs, allowing for full root shell access.

Getting Started

Out of the box, my device answered with a HTTP admin interface at port 80, and a telnet login prompt at port 23. However, the user names and passwords for the web admin interface did not work at all for the telnet login, which was a shame. The scant documentation available for the device did not mention the telnet side at all. The most straight-forward way to find out what the telnet login prompt was expecting seemed to be to obtain a copy of the firmware running on the device, and take look around.

It would have been, of course, possible to start poking around looking for potential holes in the HTTP interface "empirically". Clearly, though, having access to a copy of the firmware on a real computer would make things significantly easier. However, the ISP who provided the device (Saunalahti/Elisa) had decided not to provide any firmware upgrade images available for downloading, opting instead to unilaterally push upgrades over a TR-069 remote management session.

After some searching, I located an image of a different firmware version (listed as V1.5.0c_Z31_FI2) from the (now defunct) ztefinland.com website. Based on circumstantial evidence, this seemed to be a version used by a different ISP (Sonera). Under the assumption that many bugs/misfeatures would be common to all versions, starting from this file felt like a reasonable plan.

Looking For a Hole

The firmware upgrade file format is likely to be common to several devices, but it was also mostly self-evident.

extracts of file 'ZXDSL 931WIIV1.5.0c_Z31_FI2'

00000000  36 00 00 00 5a 58 44 53  4c 39 33 31 57 49 49 5a  |6...ZXDSL931WIIZ|
00000010  33 31 00 00 00 00 00 00  76 65 72 2e 20 32 2e 30  |31......ver. 2.0|
...
00000b40  03 60 f8 09 00 00 00 00  3c 04 48 45 34 84 4c 4f  |.`......<.HE4.LO|
00000c10  04 11 01 58 00 00 00 00  3c 04 44 52 34 84 41 4d  |...X....<.DR4.AM|
00000cf0  3c 04 52 41 34 84 4d 58  3c 1b 80 00 27 7b 09 ac  |<.RA4.MX<...'{..|
00000d70  8c 9c 00 18 03 9e e0 20  3c 04 5a 42 34 84 53 53  |....... <.ZB4.SS|
00000e00  3c 04 43 4f 34 84 44 45  3c 1b 80 00 27 7b 09 ac  |<.CO4.DE<...'{..|
00000ea0  00 00 00 00 3c 04 44 41  34 84 54 41 3c 1b 80 00  |....<.DA4.TA<...|
00001090  3c 04 4d 41 34 84 49 4e  3c 1b 80 00 27 7b 09 ac  |<.MA4.IN<...'{..|
...
0000dd20  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
0000dd50  ff c0 ff 3e 00 00 00 00  00 00 00 00 00 00 00 00  |...>............|
0000dd60  00 00 00 00 80 01 5c 20  71 73 68 73 00 00 02 40  |......\ qshs...@|
0000dd70  00 45 4f d4 40 00 66 d4  00 86 05 4a bf fb 04 98  |.EO.@.f....J....|
0000dd80  00 44 78 b2 00 03 00 00  04 54 00 10 c0 01 00 4b  |.Dx......T.....K|

It seemed quite safe to say the file was a combination of some headers, followed by an executable image of some kind, followed by a squashfs-lzma filesystem image (the magic string of which is "qshs"). For the record, squashfs-lzma was a (now discontinued) set of patches to the squashfs file system to make it support the LZMA compression algorithm; more recent versions of squashfs include this feature natively. A short dd if=firmware.img of=squashfs.img bs=56680 skip=1 (quick tip: modern GNU dd has a useful iflag=skip_bytes option) and unsquashfs later, I had an obvious root file system to play around in.

In order to dump the root filesystem of the firmware actually in my unit, any method for running arbitrary shell commands would do. A shell injection vulnerability in the HTTP admin interface seemed like a logical place to start from. Fortunately (or is that unfortunately?), it did not take long to find one.

I've misplaced my notes on the details of this middle part, so the following is an approximate overview of what happened. The (later) analysis for the login prompt of the telnet interface will be carried out in more detail, however.

In general, good candidates for shell injection vulnerabilities are any features involving operations that need root privileges. The HTTP interface has a page for configuring static routing table entries, which sounded like a promising candidate. After disassembling the httpd and following its references to lib/private/libcms_core.so, it became clear that to add a new static route, the box executed something to the tune of system("route add -net N netmask M gw G dev D");. The parameters N, M and G were verified to be IP addresses, but D was passed as-is, presumably because it was selected using a drop-down box in the HTTP interface.

As closely as I can recall, I plugged in a USB stick with a built-for-MIPS BusyBox on it (the version on the device was too stripped down), and ran a suitable route-adding command to get a remote shell open over netcat. After that, it was a simple matter of tar ... | nc to extract the contents of the existing firmware.

Telnet Login

Theoretically speaking, the task (getting access over telnet, running arbitrary code) was in a sense accomplished at this point. However, the shell injection method was both cumbersome and inelegant, especially given that there was a perfectly good telnet interface just waiting for acceptable login credentials. It was clearly time to go look at still more MIPS assembly.

The message given as the telnet login prompt was found only in the file lib/private/libcms_cli.so. As an educated guess, that seemed likely to contain the code for the command-line interface. Further, bin/telnetd linked against the library, and used symbols such as cmsCli_printWelcomeBanner and cmsCli_authenticate from it. Going purely on the name, the last sounded especially relevant.

$ mips-elf-readelf -Da lib/private/libcms_cli.so

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  REGINFO        0x0000d4 0x000000d4 0x000000d4 0x00018 0x00018 R   0x4
  LOAD           0x000000 0x00000000 0x00000000 0x1de44 0x1de44 R E 0x10000
  LOAD           0x01e000 0x0002e000 0x0002e000 0x00c28 0x026a8 RW  0x10000
  DYNAMIC        0x0000ec 0x000000ec 0x000000ec 0x000d0 0x000d0 RWE 0x4
  NULL           0x000000 0x00000000 0x00000000 0x00000 0x00000     0x4

Primary GOT:
 Global entries:
   Address     Access  Initial Sym.Val. Type    Ndx Name
  0002eb78 -32088(gp) 00018770 00018770 FUNC    UND getpass

Symbol table for image:
  Num Buc:    Value  Size   Type   Bind Vis      Ndx Name
   13  82: 00002c68   872 FUNC    GLOBAL DEFAULT   8 cmsCli_authenticate

Based on the above, the code for cmsCli_authenticate can be found by extracting 872 bytes at offset 0x2c68 in the file. That can be done with e.g. dd if=lib/private/libcms_cli.so of=auth.bin bs=1 skip=11368 count=872; mips-elf-objdump --architecture=mips:isa32 -EB -b binary -D auth.bin --adjust-vma=0x2c68. The full disassembly, along with the comments I wrote for myself, can be found in cmsCli_authenticate.txt. To make a long story short, however, it is essentially equivalent to the following C code. (Some minor details have been omitted.)

cmsCli_authenticate

char *currUser;
unsigned char currPerm;
int exitOnIdleTimeout;

int cmsCli_authenticate(int timeout)
{
	char username_in[256], passwd_in[256], *passwd;
	int attempt = 0, auth = 0, err = 0;

	exitOnIdleTimeout = timeout;

	while (!auth)
	{
		username_in[0] = 0; passwd_in[0] = 0;

		printf("Login: "); fflush(stdout);
		err = cli_readString(username_in, 256);
		if (err != 0)
			break;

		passwd = getpass("Password: ");
		if (passwd)
		{
			strcpy(passwd_in, passwd);
			memset(passwd, 0, strlen(passwd));
		}

		attempt++;

		if ((cmsUtl_strcmp(username_in, "root") == 0 &&
		     cmsUtl_strcmp(passwd_in, "public") == 0) ||
		    (cmsUtl_strcmp(username_in, "ztedebug") == 0 &&
		     cmsUtl_strcmp(passwd_in, "ztedebug") == 0))
		{
			auth = 1;
			currPerm = 0x80;
			continue;
		}

		if (attempt >= 3)
		{
			printf("Authorization failed after trying %d times!!!.\n");
			fflush(stdout);
			sleep(3);
			attempt = 0;
			continue;
		}

		puts("Login incorrect. Try again.");
		fflush(stdout);
	}

	if (err == 0)
		log_log(7, "cmsCli_authenticate", 187, "current logged in user %s perm=0x%x", currUser, currPerm);
	return err;
}

In other words, regardless of what user accounts have been configured in the HTTP interface, the telnet login prompt accepts either root:public or ztedebug:ztedebug (and nothing else) as valid credentials.

Further Discussion

The telnet interface is not very polished, but it allows you to drop into a (root) shell with the sh command, which is nice. There is no reasonable way (that I know of) to register code to be executed automatically at startup, which is less nice, as it means all customizations that cannot be performed by setting NVRAM variables are lost on reboot. It is quite possible that some shell injection trick could be used by setting one of the NVRAM variables, but I have not investigated this further.

As far as I have been able to determine, both the telnet and HTTP interfaces are only visible to the physical Ethernet and local wifi interfaces, not the DSL/WAN side. So you do not have to worry about random people from the Internet logging in as ztedebug. On the other hand, if you have untrusted local clients in the network, you do have to worry about them.

There are a number of other curiosities in the firmware (for example, a rather bizarre way of doing DNS for clients behind a NAT), but those fall outside the scope of this text.

I've since heard anecdotally that there exists a newer Elisa-branded firmware version (ZXDSL931WIIA_ElisaV2.8.3_Z40_FI) that possibly disables the telnet interface entirely.