Backstory
During a red teaming exercise conducted for one of our customers, we abused weak passwords to obtain an administrative access to some Zyxel ZyWALL Unified Security Gateway (USG) appliances that were used as both firewalls and VPN concentrators in their branch offices. These appliances are targeted at small and medium businesses and are somewhat popular, at least according to Shodan.
Thanks to our administrative access, we were able to dump the configurations and we noticed that a series of passwords stored on these devices were encrypted in some way. When we did a Google search we could not find any public information on the Internet about how these passwords were stored. Based on our observations, they had to be encrypted with a reversible algorithm because they were passwords that the device itself used, such as the PSKs of VPNs.
Since we had some spare budget, we decided to buy a similar device on eBay and spend some time auditing it on our own. Our next articles will cover the results of our analysis carried out on the physical device. In the meantime, let’s focus on the work we did while we were waiting for the device to arrive.
In this first article of our Zyxel audit series we will cover firmware extraction and password decryption against Zyxel ZyWALL Unified Security Gateway (USG) appliances.
Firmware extraction
First of all, we downloaded from the official website the same firmware images (version 4.10 and 4.70) for the USG310 device that were deployed by our customer.
We started working on version 4.10 of the firmware: the file 410AAPJ2C0.bin is the firmware image in ZIP format. Unfortunately, the ZIP archive was password protected. Searching online we were unable to find any information on the passwords used by Zyxel.
In the PDF documents distributed with the firmware, however, a recovery method is described in the event that the upgrade procedure is unsuccessful.
A lower level procedure is also available in case the previously described method does not work:
https://kb.zyxel.com/KB/searchArticle!gwsViewDetail.action?articleOid=006845&lang=EN
Based on the information in the low level recovery guide we can assume that the “.ri” file is directly executed by the system to install the fundamental components in order to then be able to flash the firmware from the “.bin” file.
We extracted “410AAPJ2C0.ri” with binwalk:
inode@stormbringer:w$ binwalk -e 410AAPJ2C0.ri DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 512 0x200 uImage header, header size: 64 bytes, header CRC: 0xF9EC0106, created: 2014-12-06 03:27:53, image size: 4948010 bytes, Data Address: 0x5000000, Entry Point: 0x80101400, data CRC: 0xD43F46E, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "Linux Kernel Image" 576 0x240 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 16814648 bytes inode@stormbringer:w$
And the files inside it:
inode@stormbringer:w/_410AAPJ2C0.ri.extracted$ binwalk -e 240 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 ELF, 64-bit MSB MIPS32 rel2 executable, MIPS, version 1 (SYSV) 5095576 0x4DC098 Linux kernel version 2.6.32 11385704 0xADBB68 gzip compressed data, maximum compression, from Unix, last modified: 2014-12-06 01:57:21 11487616 0xAF4980 DES SP2, big endian 11488128 0xAF4B80 DES SP1, big endian 11511376 0xAFA650 CRC32 polynomial table, little endian 11910064 0xB5BBB0 Unix path: /usr/bin/magic-seed 11919441 0xB5E051 Unix path: /sys/module/perf_counters/parameters/counter{0,1} 11919495 0xB5E087 Unix path: /sys/module/perf_counters/parameters/l2counter{0-3} 11948723 0xB652B3 PARity archive data - file number 16717 12987688 0xC62D28 Unix path: /home/sdd1/source/410/formal/p2/usg310/src/kernel/arch/mips/include/asm/bugs.h 13224040 0xC9C868 Neighborly text, "NeighborSolicits" 13224064 0xC9C880 Neighborly text, "NeighborAdvertisementsorts" 13227391 0xC9D57F Neighborly text, "neighbor %.2x%.2x.%.2x:%.2x:%.2x:%.2x:%.2x:%.2x lost on port %d(%s)(%s)" 13231704 0xC9E658 Flattened device tree, size: 10827 bytes, version: 17 13242536 0xCA10A8 Flattened device tree, size: 11728 bytes, version: 17 13622896 0xCFDE70 Unix path: /usr/local/zld_udev/sbin/uevent_helper.sh 14045184 0xD65000 gzip compressed data, maximum compression, from Unix, last modified: 2014-12-06 15042374 0xE58746 Zip archive data, at least v2.0 to extract, compressed size: 226984, uncompressed size: 229081, name: etc_writable/zyxel/secuextender/SecuExtender_x64.cab 15269496 0xE8FE78 Zip archive data, at least v2.0 to extract, compressed size: 610493, uncompressed size: 610989, name: etc_writable/zyxel/secuextender/ssltun.jar 15880177 0xF24FF1 Zip archive data, at least v2.0 to extract, compressed size: 493928, uncompressed size: 516542, name: etc_writable/zyxel/secuextender/sslapp.jar inode@stormbringer:w/_410AAPJ2C0.ri.extracted$
And again:
inode@stormbringer:w/_410AAPJ2C0.ri.extracted/_240.extracted$ binwalk -e D65000 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 ASCII cpio archive (SVR4 with no CRC), file name: ".", file name length: "0x00000002", file size: "0x00000000" 112 0x70 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit", file name length: "0x00000007", file size: "0x00000000" 232 0xE8 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zyinit", file name length: "0x0000000E", file size: "0x00040DEC" 266064 0x40F50 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/etc_inittab", file name length: "0x00000013", file size: "0x00000BB8" 269196 0x41B8C ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/e2fsck", file name length: "0x0000000E", file size: "0x0006B64C" 709204 0xAD254 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_mrd.ko", file name length: "0x00000012", file size: "0x00001788" 715356 0xAEA5C ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/rw.zip", file name length: "0x0000000E", file size: "0x002106C8" 2879904 0x2BF1A0 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/mke2fs", file name length: "0x0000000E", file size: "0x0004239C" 3151288 0x3015B8 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/switchdev.ko", file name length: "0x00000014", file size: "0x0000AF10" 3196236 0x30C54C ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_fsextract", file name length: "0x00000015", file size: "0x00017098" 3290728 0x323668 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/sw_cn60xx.ko", file name length: "0x00000014", file size: "0x00005418" 3312388 0x328B04 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_udev", file name length: "0x00000010", file size: "0x0001347C" 3391488 0x33C000 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/db.zip", file name length: "0x0000000E", file size: "0x00001428" 3396772 0x33D4A4 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zyinit_gpl", file name length: "0x00000012", file size: "0x00055128" 3745356 0x39264C ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/fwversion", file name length: "0x00000011", file size: "0x0000014A" 3745816 0x392818 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/switchdev_char.ko", file name length: "0x00000019", file size: "0x00005800" 3768480 0x3980A0 ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/unzip", file name length: "0x0000000D", file size: "0x0002B8B0" 3946956 0x3C39CC ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/lkm.lst", file name length: "0x0000000F", file size: "0x00000050" 3947164 0x3C3A9C ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/platform_support.ko", file name length: "0x0000001B", file size: "0x00004CC8" 3966960 0x3C87F0 ASCII cpio archive (SVR4 with no CRC), file name: "init", file name length: "0x00000005", file size: "0x0000000D" 3967092 0x3C8874 ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000" inode@stormbringer:w/_410AAPJ2C0.ri.extracted/_240.extracted$
Looking at the extracted files, it is possible to identify the “zyinit” binary, which contains some firmware related strings.
By analyzing it, it is possible to see that it launches other external commands, in particular the “zld_fsextract” command:
Searching online for these executable binaries, only an interesting URL was identified (https://www.dslreports.com/forum/remark,26961186), which gave us some information about the ZIP password:
Searching in the “zld_fsextract” binary for the password, it is possible to identify some good starting points for the analysis, like this one:
These options are used by the “unzip” binary to unzip a file with a specific password which is defined in the parameter “-P”. Based on information found online and on a quick analysis, it seems that the binary calculates the unzip password in some way based on the binary name or the binary content.
The fastest way to extract the files is to emulate the MIPS processor and execute the binary.
Launching a Linux/MIPS virtual machine:
> wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-5kc-malta > wget https://people.debian.org/~aurel32/qemu/mips/debian_wheezy_mips_standard.qcow2 > qemu-system-mips64.exe -M malta -kernel vmlinux-3.2.0-4-5kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic -net user,hostfwd=tcp:127.0.0.1:2222-:22
Getting firmware image information using the “zld_fsextract” binary:
root@debian-mips:~# ./zld_fsextract 410AAPJ2C0.bin -s list name :kernel scope :-f kernelusg310.bin -f kernelchecksum -D / nc_scope :-f kernelusg310.bin version :2.6.32 build_date :2014-12-06 11:27:35 checksum :6e4e1ad212be0a8a3ce89484f3c5dc1e core_checksum :a028057f7c742ea52bd3d0408f38a673 name :code scope :-f bmusg310.bin -f bmchecksum -f kernelusg310.bin -f kernelchecksum -d wtp_image -d db -i -D /rw scope :-d db/etc/zyxel/ftp/conf -D / nc_scope :-f fwversion -f filechecksum -f wtpinfo version :4.10(AAPJ.2) build_date :2014-12-09 08:51:18 checksum :899be95dac4a5bdcfd2f694035f16746 core_checksum :e25ab639d4ff432bb7ffdc8c1bd39be3 name :WTP_wtp_image/400AAS4C0.bin scope :-f wtp_image/400AAS4C0.bin -D /db nc_scope : version :4.00(###.4) build_date :2013-08-31 20:36:43 checksum :07773b3deb39a1dd9c2036201097529c core_checksum :07773b3deb39a1dd9c2036201097529c name :WTP_wtp_image/400AADG4C0.bin scope :-f wtp_image/400AADG4C0.bin -D /db nc_scope : version :V4.00(###.4) build_date :2013-08-31 20:35:40 checksum :aabb0606887cec28a07b8598e699f027 core_checksum :aabb0606887cec28a07b8598e699f027 root@debian-mips:~#
And using the zld_fsextract binary to extract the firmware without the need to specify a password:
root@debian-mips:~# ./zld_fsextract 410AAPJ2C0.bin ./unzip -s extract -e code ... root@debian-mips:~# root@debian-mips:~# ls -al /rw/ total 57192 drwxr-xr-x 3 root root 4096 Nov 23 12:56 . drwxr-xr-x 24 root root 4096 Nov 23 12:56 .. -r--r--r-- 1 root root 58511360 Dec 6 2014 compress.img drwxr-xr-x 5 root root 4096 Dec 9 2014 etc_writable -rw-r--r-- 1 root root 139 Dec 9 2014 filechecksum -rw-r--r-- 1 root root 21415 Dec 9 2014 filelist -rw-r--r-- 1 root root 326 Dec 9 2014 fwversion -rw-r--r-- 1 root root 1671 Dec 9 2014 wtpinfo root@debian-mips:~#
Then uncompressing the new image:
inode@stormbringer:w$ binwalk -e compress.img DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Squashfs filesystem, little endian, version 4.0, compression:gzip, size: 58509745 bytes, 4958 inodes, blocksize: 131072 bytes, created: 2014-12-06 03:26:20 inode@stormbringer:w$
After seeing that the process worked correctly with a fully emulated environment, we decided to try the single binary emulation provided by qemu. By using “strace” we can quickly see how the “unzip” binary was launched to find the ZIP password.
strace -f -s 199 qemu-mipsn32-static ./zld_fsextract 410AAPJ2C0.bin ./unzip -s extract -e code
Here is the output.
The binary that calculates the ZIP password is statically compiled, therefore it is not so easy to understand how that password is generated, and above all we are lazy…
Password encryption algorithm
Now that we have access to the filesystem, let’s start looking for information on the algorithm used to store passwords in the configuration files. In the configurations available to us, all passwords used the $4$ format and were preceded by the following keywords:
- encrypted-key
- encrypted-keystring
- encrypted-password
- encrypted-presharekey
- password-encrypted
- wpa-psk-encrypted
First step, identify which executables deal with these types of keywords:
inode@stormbringer:w$ grep "encrypted-" _compress.img.extracted/squashfs-root/bin/* Binary file _compress.img.extracted/squashfs-root/bin/zysh matches Binary file _compress.img.extracted/squashfs-root/bin/zyshd matches inode@stormbringer:w$
Looking in the code of “zyshd” for the string “$4$” which identifies the encrypted password:
Looking inside the schrodinger routine. This part generates the salt:
Encryption is performed by the following code:
And then the BASE64 encoding at the end:
Looking inside the “RIJ_cbc_encrypt” function, it is possible to identify the routine “RIJ_decrypt” which leads us to the AES algorithm. This is also confirmed by the name of the function (RIJ = Rijndael) and by the 0xC0 used on the set key, which is 192. AES is one of the few encryption algorithms that support 192-bit keys.
Looking inside the encryption routine, it is possible to identify AES parameters like Td0 memory; it is also possible to use the FindCrypt Ghidra plugin that helps identify the encryption routines in use.
We can assume that the encryption/decryption function works as follows:
RIJ_cbc_encrypt(destination, source, len, unknown, IV, encryption/decryption byte)
At this point we only need to identify the parameters passed to the function.
The IV is:
The encryption key is:
So:
aes_key = "001200054A1F23FB1F060A14CD0D018F5AC0001306F0121C" aes_iv = "0006001C01F01FC0FFFFFFFFFFFFFFFF"
The same process can be followed on the firmware version 4.70 to identify the $5$ encryption scheme.
To summarize, in the case of a password hash starting with $4$ the algorithm is as follows:
iv = static_iv key = static_key salt = generate_8_bytes_from_random() to_encrypt = salt + password (the password is repeated to reach the 80 bytes) AES_SET_KEY(aes,key,192) AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv) final_encrypted = base64(cyphertext) final_string = "$4$" + salt + "$" + final_encrypted "$"
In the case of a password hash starting with $5$ the algorithm is as follows:
iv = static_iv key = static_key salt = generate_8_bytes_from_random() to_encrypt = salt + password (the password is repeated to reach the 80 bytes) AES_SET_KEY(aes,key,192) AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv) salt = generate_8_bytes_from_random() to_encrypt = salt + password (the password is repeated to reach the 80 bytes) AES_SET_KEY(aes,key,192) AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv) step1 = base64(cyphertext) salt1 = generate_8_bytes_from_random() to_encrypt = salt1 + step1 AES_SET_KEY(aes,key,192) AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv) final_encrypted = base64(cyphertext) final_string = "$5$" + salt1 + "$" + salt2 + "$" + final_encrypted "$"
We developed a decryption tool that can be downloaded from: https://github.com/inode-/zyxel_password_decrypter
Another possible way to reverse these algorithms is to use dynamic analysis, but it seems QEMU does not fully support the SoC used by Zyxel and generates some illegal instruction fault on most executables. There is a potentially working QEMU patch at https://github.com/amir-mehmood/QEMU-Octeon-MIPS64, but we did not test it.
Finally, when we received the physical device we found out that the /usr/sbin/zencrypt binary present in the filesystem can be used to decrypt these passwords as well:
root@USG20-VPN:~# zencrypt -k 'E8AzAwOyYBwVHJJjtUTDqdAxLDPIr/ncDNV5HCfZFRJlqhYkSqbGvX9BP06YyBg5fGp2HQrwtHsp9mQLNAtBeEIPejrUv5jw/ZRmUx20nuwCNK0UZdeklaFWc945oR3zWVupR2i/KakLhb9d46q72geNfFnXggbJdsl7ObjGKUNLfRF6tMnvxGtPO54PJKpL8E9Af4LAqStAAEPu3S70og8h9PzUER9/Qq246kyTfjONyyVF49EX1l+vg6rD0y+5UnHamBedqvzr/e7az469oViXUIxfMnr/CpMgR5hgzKn3efqldNVSEqmS07UN6CkjHiWaQgnDTxhc4tjbZQjaT3CGVpghF7W3GzmYtMqQlYQ$' pluto
Stay tuned for our upcoming articles in this series, along with some juicy vulnerability disclosures!