diff --git a/CHANGELOG b/CHANGELOG index 1743cbdd3..46b48bd9e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the rmi-dumpregistry script, which shows the contents of + Java RMI registry. [Martin Holst Swende] + o [NSE] Added the ssh2-enum-algos script which reports the number of algorithms the target SSH2 server supports, by type. If verbosity is set, then the offered algorithms are listed. Output is reduced diff --git a/nselib/rmi.lua b/nselib/rmi.lua new file mode 100644 index 000000000..fae4a6f1f --- /dev/null +++ b/nselib/rmi.lua @@ -0,0 +1,1510 @@ +--- Library method for communicating over RMI (JRMP + java serialization) +-- +-- This is a not complete RMI implementation for LUA, which is meant to be able +-- to invoke methods and parse returnvalues which are simple, basically the java primitives. +-- This can be used to e.g dump out the registry, and perform authentication against +-- e.g JMX-services. + +-- This library also contains some classes which works pretty much like the +-- java classes BufferedReader, BufferedWriter, DataOutputStream and DataInputStream. + +-- Most of the methods in the RMIDataStream class is based on the OpenJDK RMI Implementation, +-- and I have kept the methodnames as they are in java, so it should not be too hard to find +-- the corresponding functionality in the jdk codebase to see how things 'should' be done, in case +-- there are bugs or someone wants to make additions. I have only implemented the +-- things that were needed to get things working, but it should be pretty simple to add more +-- functionality by lifting over more stuff from the jdk. + +-- The interesting classes in OpenJDK are: +-- java.io.ObjectStreamConstants +-- java.io.ObjectStreamClass +-- java.io.ObjectInputStream +-- sun.rmi.transport.StreamRemoteCall +-- and a few more. + +-- If you want to add calls to classes you know of, you can use e.g Jode to decompile the +-- stub-class or skeleton class and find out the details that are needed to perform an +-- RMI method invokation. Those are +-- Class hashcode +-- Method number (each method gets a number) +-- Arguments f +-- You also need the object id (so the remote server knows what instance you are talking to). That can be +-- fetched from the registry (afaik) but not currently implemented. Some object ids are static : the registry is always 0 + +-- @author Martin Holst Swende +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @see java 1.4 RMI-spec: http://java.sun.com/j2se/1.4.2/docs/guide/rmi/ +-- @see java 5 RMI-spec: http://java.sun.com/j2se/1.5.0/docs/guide/rmi/spec/rmiTOC.html +-- @see java 6 RMI-spec : http://java.sun.com/javase/6/docs/technotes/guides/rmi/index.html +-- @see The protocol for Java object serializtion : http://java.sun.com/javase/6/docs/platform/serialization/spec/protocol.html +-- Version 0.1 + +-- Created 09/06/2010 - v0.1 - created by Martin Holst Swende +module("rmi", package.seeall) +require("bin") +require("bit") +-- Some lazy shortcuts + +local function dbg(str,...) + stdnse.print_debug("RMI:"..str, unpack(arg)) +end +-- Convenience function to both print an error message and return +-- Example usage : +-- if foo ~= "gazonk" then +-- return doh("Foo should be gazonk but was %s", foo) +-- end +local function doh(str,...) + stdnse.print_debug("RMI-ERR:"..tostring(str), unpack(arg)) +end + +---------------------------------------------------------------------------------------------------------- +-- The Buffering classes +---------------------------------------------------------------------------------------------------------- +-- These buffering clases provide functionality much like javas BufferedReader +-- and BufferedWriter. + +-- BufferedWriter wraps the pack-functionality from bin, and buffers data internally +-- until flush is called. When flush is called, it either sends the data to the socket OR +-- returns the data, if no socket has been set. + +-- BufferedReader reads data from the supplied socket and contains functionality +-- to read all that is available and store all that is not currently needed, so the caller +-- gets an exact number of bytes (which is not the case with the basic nmap socket implementation) +-- If not enough data is available, it blocks until data is received, thereby handling the case +-- if data is spread over several tcp packets (which is a pitfall for many scripts) + +-- It wraps unpack from bin for the reading. +-- OBS! You need to check before invoking skip or unpack that there is enough +-- data to read. Since this class does not parse arguments to unpack, it does not +-- know how much data to read ahead on those calls. +-- Example usage: +-- local bWriter = BufferedWriter:new(socket) +-- local breader= BufferedReader:new(socket) +-- +-- bWriter.pack('>i', integer) +-- bWrier.flush() -- sends the data +-- +-- if bsocket:canRead(4) then -- Waits until four bytes can be read +-- local packetLength = bsocket:unpack('i') -- Read the four bytess +-- if bsocket:canRead(packetLength) then +-- -- ...continue reading packet values + +BufferedWriter = { + new = function(self, socket) + local o = {} + setmetatable(o, self) + self.__index = self + o.writeBuffer = '' + o.pos = 1 + o.socket = socket + return o + end, + + --- Sends data over the socket + -- (Actually, just buffers until flushed) + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + self.writeBuffer = self.writeBuffer .. data + end, + -- Convenience function, wraps bin + pack = function(self, fmt, ... ) + self.writeBuffer = self.writeBuffer .. bin.pack( fmt, unpack(arg)) + end, + + -- This function flushes the buffer contents, thereby emptying + -- the buffer. If a socket has been supplied, that's where it will be sent + -- otherwise the buffer contents are returned + --@return status + --@return content of buffer, in case no socket was used + flush = function(self) + + local content = self.writeBuffer + self.writeBuffer = '' + + if not self.socket then + return true, content + end + return self.socket:send(content) + end, + +} +BufferedReader = { + new = function(self, socket, readBuffer) + local o = {} + setmetatable(o, self) + self.__index = self + o.readBuffer = readBuffer -- May be nil + o.pos = 1 + o.socket = socket -- May also be nil + return o + end, + + -- This method blocks until the specified number of bytes + -- have been read from the socket and are avaiable for + -- the caller to read, e.g via the unpack function + canRead= function(self,count) + local status, data + self.readBuffer = self.readBuffer or "" + local missing = self.pos + count - #self.readBuffer -1 + if ( missing > 0) then + if self.socket == nil then + return doh("Not enough data in static buffer") + end + + status, data = self.socket:receive_bytes( missing ) + if ( not(status) ) then + return false, data + end + self.readBuffer = self.readBuffer .. data + end + -- Now and then, we flush the buffer + if ( self.pos > 1024) then + self.readBuffer = self.readBuffer:sub( self.pos ) + self.pos = 1 + end + return true + end, + + --Returns the number of bytes already available for reading + bufferSize = function(self) + return #self.readBuffer +1 -self.pos + end, + + -- This function works just like bin.unpack (in fact, it is + -- merely a wrapper around it. However, it uses the data + -- already read into the buffer, and the internal position + --@param format - see bin + unpack = function(self,format) + local ret = {bin.unpack(format, self.readBuffer, self.pos)} + self.pos = ret[1] + return unpack(ret,2) + end, + -- This function works just like bin.unpack (in fact, it is + -- merely a wrapper around it. However, it uses the data + -- already read into the buffer, and the internal position. + -- This method does not update the current position, and the + -- data can be read again + --@param format - see bin + peekUnpack = function(self,format) + local ret = {bin.unpack(format, self.readBuffer, self.pos)} + return unpack(ret,2) + end, + -- Tries to read a byte, without consuming it. + --@return status + --@return bytevalue + peekByte = function(self) + if self:canRead(1) then + return true, peekUnpack('C') + end + return false + end, + + -- Skips a number of bytes + --@param len the number of bytes to skip + skip = function(self, len) + if(#self.readBuffer < len + self.pos) then + return doh("ERROR: reading too far ahead") + end + local skipped = self.readBuffer:sub(self.pos, self.pos+len-1) + self.pos = self.pos + len + return true, skipped + end, + +} + +---------------------------------------------------------------------------------------------------------- +-- The JavaDOS / JavaDIS classes +---------------------------------------------------------------------------------------------------------- +-- The JavaDOS class is an approximation of a java DataOutputStream, and +-- JavaDIS close to java DataOutputStream. They provide convenience functions +-- for reading and writing java types to/from an underlying BufferedReader/BufferedWriter + +-- When used in conjunction with the BufferedX- classes, they handle the availability- +-- checks transparently, i.e the caller does not have to check if enough data is available + +-- Usage : +-- local dos = JavaDOS:new(BufferedWriter:new(socket)) +-- local dos = JavaDIS:new(BufferedReader:new(socket)) +-- dos:writeUTF("Hello world") +-- dos:writeInt(3) +-- dos:writeLong(3) +-- dos:flush() -- send data +-- local answer = dis:readUTF() +-- local int = dis:readInt() +-- local long = dis:readLong() + +-- +-- The classes are generated when this file is loaded, by the definitions in the JavaTypes +-- table. That table contains mappings between the format used by bin and the types +-- available in java, aswell as the lengths (used for availability-checks) and the name which +-- is prefixed by read* or write* when monkey-patching the classes and adding functions. +-- For example: {name = 'Int', expr = '>i', len= 4}, will generate the functions +-- writeInt(self, value) and readInt() respectively + +local JavaTypes = { + {name = 'Int', expr = '>i', len= 4}, + {name = 'UnsignedInt', expr = '>I', len= 4}, + {name = 'Short', expr = '>s', len= 2}, + {name = 'UnsignedShort', expr = '>S', len= 2}, + {name = 'Long', expr = '>l', len= 8}, + {name = 'UnsignedLong', expr = '>L', len= 8}, + {name = 'Byte', expr = '>C', len= 1}, +} +JavaDOS = { + new = function (self,bWriter) + local o = {} -- create new object if user does not provide one + setmetatable(o, self) + self.__index = self -- DIY inheritance + o.bWriter = bWriter + return o + end, + -- This closure method generates all writer methods on the fly + -- according to the definitions in JavaTypes + _generateWriterFunc = function(self, javatype) + local functionName = 'write'..javatype.name + local newFunc = function(_self, value) + --dbg(functionName .."(%s) called" ,tostring(value)) + return _self:pack(javatype.expr, value) + end + self[functionName] = newFunc + end, + + writeUTF = function(self, text) + -- TODO: Make utf-8 of it + return self:pack('>P', text) + end, + pack = function(self, ...) + return self.bWriter:pack(unpack(arg)) + end, + write = function(self, data) + return self.bWriter:send(data) + end, + flush = function(self) + return self.bWriter:flush() + end, +} + +JavaDIS = { + new = function (self,bReader) + local o = {} -- create new object if user does not provide one + setmetatable(o, self) + self.__index = self -- DIY inheritance + o.bReader = bReader + return o + end, + + -- This closure method generates all reader methods (except unstandard ones) on the fly + -- according to the definitions in JavaTypes. + _generateReaderFunc = function(self, javatype) + local functionName = 'read'..javatype.name + local newFunc = function(_self) + --dbg(functionName .."() called" ) + if not _self.bReader:canRead(javatype.len) then + local err = ("Not enough data in buffer (%d required by %s)"):format(javatype.len, functionName) + return doh(err) + end + return true, _self.bReader:unpack(javatype.expr) + end + self[functionName] = newFunc + end, + -- This is a bit special, since we do not know beforehand how many bytes must be read. Therfore + -- this cannot be generated on the fly like the others. + readUTF = function(self, text) + -- First, we need to read the length, 2 bytes + if not self.bReader:canRead(2) then-- Length of the string is two bytes + return false, "Not enough data in buffer [0]" + end + -- We do it as a 'peek', so bin can reuse the data to unpack with 'P' + local len = self.bReader:peekUnpack('>S') + --dbg("Reading utf, len %d" , len) + -- Check that we have data + if not self.bReader:canRead(len) then + return false, "Not enough data in buffer [1]" + end + -- For some reason, the 'P' switch does not work for me. + -- Probably some idiot thing. This is a hack: + local val = self.bReader.readBuffer:sub(self.bReader.pos+2, self.bReader.pos+len+2-1) + self.bReader.pos = self.bReader.pos+len+2 + -- Someone smarter than me can maybe get this working instead: + --local val = self.bReader:unpack('P') + --dbg("Read UTF: %s", val) + return true, val + end, + readLongAsHexString = function(self) + if not self.bReader:canRead(8) then-- Length of the string is two bytes + return false, "Not enough data in buffer [3]" + end + return true, self.bReader:unpack('H8') + + end, + skip = function(self, len) + return self.bReader:skip(len) + end, + canRead = function(self, len) + return self.bReader:canRead(len) + end, +} + +-- Generate writer-functions on the JavaDOS/JavaDIS classes on the fly +for _,x in ipairs(JavaTypes) do + JavaDOS._generateWriterFunc(JavaDOS, x) + JavaDIS._generateReaderFunc(JavaDIS, x) +end + +JavaClass = { + + + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + --o.fields = {} + --o.isclass = true + return o + end, + + customDataFormatter = nil, + + setName = function( self, name ) + dbg("Setting class name to %s", name) + self.name = name + end, + setSerialID = function( self, serial ) self.serial = serial end, + setFlags = function( self, flags ) + self.flags = RMIUtils.flagsToString(flags) + self._binaryflags = flags + end, + + isExternalizable = function(self) + if self._binaryFlags == nil then return false end + + return bit.band(self._binaryflags, RMIUtils.SC_EXTERNALIZABLE) + end, + + addField = function( self, field ) + if self.fields == nil then self.fields = {} end + table.insert( self.fields, field ) + --self[field.name] = field + end, + setSuperClass = function(self,super) self.superClass = super end, + + setCustomData = function(self, data) self.customData = data end, + getCustomData = function(self) return self.customData end, + + setInterfaces = function(self,ifaces) self.ifaces = ifaces end, + getName = function( self ) return self.name end, + getSuperClass = function(self) return self.superClass end, + getSerialID = function( self ) return self.serial end, + getFlags = function( self ) return self.flags end, + getFields = function( self ) return self.fields end, + getFieldByName = function( self, name ) + if self.fields == nil then return end + for i=1, #self.fields do + if ( self.fields[i].name == name ) then + return self.fields[i] + end + end + end, + + __tostring = function( self ) + local data + if self.name ~=nil then + data = ("%s "):format(self.name) + else + data = "???" + end + if self.superClass~=nil then + data = data .. " extends ".. tostring( self.superClass) + end + if self.ifaces ~= nil then + data = data .. " implements " .. self.ifaces + end + if self.fields ~=nil then + for i=1, #self.fields do + if i == 1 then + data = data .. "[" + end + data = data .. tostring(self.fields[i]) + if ( i < #self.fields ) then + data = data .. ";" + else + data = data .. "]" + end + + end + end + return data + end, + toTable = function(self, customDataFormatter) + local data = {self.name} + + if self.externalData ~=nil then + table.insert(data, tostring(self.externalData)) + end + + --if self.name ~=nil then + -- data.class = self.name + --end + if self.ifaces ~= nil then + table.insert(data, " implements " .. self.ifaces) + end + + if self.superClass~=nil then + local extends = self.superClass:toTable() + table.insert(data ,"extends") + table.insert(data, extends) + --data.extends = self.superClass:toTable() + end + if self.fields ~=nil then + table.insert(data, "fields") + local f = {} + for i=1, #self.fields do + table.insert(f, self.fields[i]:toTable()) + end + table.insert(data, f) + end + + if self.customData ~=nil then + local formatter = JavaClass['customDataFormatter'] + if formatter ~= nil then + local title, cdata = formatter(self.name, self.customData) + table.insert(data, title) + table.insert(data, cdata) + else + table.insert(data, "Custom data") + table.insert(data, self.customData) + end + end + + return data + + end, + +} + +JavaField = { + + new = function(self, name, typ ) + local o = {} + setmetatable(o, self) + self.__index = self + o.name = name + o.type = typ + return o + end, + + setType = function( self, typ ) self.type = typ end, + setSignature = function( self, sig ) self.signature = sig end, + setName = function( self, name ) self.name = name end, + setObjectType = function( self, ot ) self.object_type = ot end, + setReference = function( self, ref ) self.ref = ref end, + setValue = function (self, val) + dbg("Setting field value to %s", tostring(val)) + self.value = val + + end, + + getType = function( self ) return self.type end, + getSignature = function( self ) return self.signature end, + getName = function( self ) return self.name end, + getObjectType = function( self ) return self.object_type end, + getReference = function( self ) return self.ref end, + getValue = function( self ) return self.value end, + + __tostring = function( self ) + local data = tostring(self.type) .. " " .. tostring(self.name) + if self.value ~= nil then + data = data .." = " .. tostring(self.value) + end + + return data + end, + toTable = function(self) + local data = {tostring(self.type) .. " " .. tostring(self.name)} + print("FIELD VALUE:", self.value) + if self.value ~= nil then + if type(self.value) == 'table' then + if self.value.toTable ~=nil then + table.insert(data, self.value:toTable()) + else + table.insert(data, self.value) + end + else + data = data .." = " .. tostring(self.value) + end + end + return data + end, + +} + +JavaArray = { + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + o.values = {} + return o + end, + setClass = function( self, class ) self.class = class end, + setLength = function( self, length ) self.length = length end, + setValue = function(self, index, object) self.values[index] = object end, + __tostring=function(self) + local data = ("Array: %s [%d] = {"):format(tostring(self.class), self.length) + + for i=1, #self.values do + data = data .. self.values[i].."," + end + data = data .."}" + return data + end, + toTable = function(self) + local title = ("Array: %s [%d] = {"):format(tostring(self.class), self.length) + local t = {title = self.values} + return t + end, + + getValues = function(self) return self.values end +} + + + + + +TC = { + TC_NULL = 0x70, + TC_REFERENCE = 0x71, + TC_CLASSDESC = 0x72, + TC_OBJECT = 0x73, + TC_STRING = 0x74, + TC_ARRAY = 0x75, + TC_CLASS = 0x76, + TC_BLOCKDATA = 0x77, + TC_ENDBLOCKDATA = 0x78, + TC_RESET = 0x79, + TC_BLOCKDATALONG = 0x7A, + TC_EXCEPTION = 0x7B, + TC_LONGSTRING = 0x7C, + TC_PROXYCLASSDESC = 0x7D, + TC_ENUM = 0x7E, + + Integer = 0x49, + Object = 0x4c, + + Strings = { + [0x49] = "Integer", + [0x4c] = "Object", + [0x71] = "TC_REFERENCE", + [0x70] = "TC_NULL", + [0x71] = "TC_REFERENCE", + [0x72] = "TC_CLASSDESC", + [0x73] = "TC_OBJECT", + [0x74] = "TC_STRING", + [0x75] = "TC_ARRAY", + [0x76] = "TC_CLASS", + [0x77] = "TC_BLOCKDATA", + [0x78] = "TC_ENDBLOCKDATA", + [0x79] = "TC_RESET", + [0x7A] = "TC_BLOCKDATALONG", + [0x7B] = "TC_EXCEPTION", + [0x7C] = "TC_LONGSTRING", + [0x7D] = "TC_PROXYCLASSDESC", + [0x7E] = "TC_ENUM", + }, + +} + +---------------------------------------------------------------------------------------------------------- +-- RmiDataStream class +---------------------------------------------------------------------------------------------------------- +-- This class can handle reading and writing JRMP, i.e RMI wire protocol and +-- can do some very limited java deserialization. This implementation has +-- borrowed from OpenJDK RMI implementation, but only implements an +-- absolute minimum of what is required in order to perform some basic calls +-- +-- An output stream in RMI consists of transport Header information followed by a sequence of Messages. +-- Out: +-- Header Messages +-- HttpMessage +-- Header: +-- 0x4a 0x52 0x4d 0x49 Version Protocol +-- (4a 52 4d 49 === JRMI) +-- Version: +-- 0x00 0x01 +-- Protocol: +-- StreamProtocol +-- SingleOpProtocol +-- MultiplexProtocol +-- StreamProtocol: +-- 0x4b +-- SingleOpProtocol: +-- 0x4c +-- MultiplexProtocol: +-- 0x4d +-- Messages: +-- Message +-- Messages Message + +local Version= 0x02 +local Proto= {Stream=0x4b, SingleOp=0x4c, Multiplex=0x4d} +RmiDataStream = { + new = function (self,o) + o = o or {} -- create object if user does not provide one + setmetatable(o, self) + self.__index = self -- DIY inheritance + return o + end, +} +---- +-- Connects to a remote service. The connection process creates a +-- socket and does some handshaking. If this is successfull, +-- we are definitely talking to an RMI service. +function RmiDataStream:connect(host, port) + local status, err + + local socket = nmap.new_socket() + socket:set_timeout(5000) + +-- local bsocket = BufferedSocket:new() + socket:connect(host,port, "tcp") + + -- Output and input + local dos = JavaDOS:new(BufferedWriter:new(socket)) + local dis = JavaDIS:new(BufferedReader:new(socket)) + + -- Start sending a message -- + -- Add Header, Version and Protocol + + --dos:write('JRMI' .. bin.pack('H', Version .. Proto.Stream)) + dos:writeInt(1246907721) -- == JRMI + dos:writeShort(Version) + dos:writeByte(Proto.Stream) + status = dos:flush() + if not status then + return doh(err) + end + + -- For the StreamProtocol and the MultiplexProtocol, the server must respond with a a byte 0x4e + -- acknowledging support for the protocol, and an EndpointIdentifier that contains the host name + -- and port number that the server can see is being used by the client. + -- The client can use this information to determine its host name if it is otherwise unable to do that for security reasons. + + -- Read ack + status, err = self:readAck(dis) + if not status then + return doh("No ack received from server:" .. tostring(err)) + end + + -- The client must then respond with another EndpointIdentifier that contains the clients + -- default endpoint for accepting connections. This can be used by a server in the MultiplexProtocol case to identify the client. + + dos:writeUTF("127.0.0.1") -- TODO, write our own ip instead (perhaps not necessary, since we are not using MultiplexProtocol + dos:writeInt(0) -- Port ( 0 works fine) + dos:flush() + self.dos = dos + self.dis =dis + return true +end + +-- Reads a DgcAck message, which is sent during conection handshake +--@param dis - a JavaDIS to read from +--@return status +--@return error message +function RmiDataStream:readAck(dis) + local status, ack = dis:readByte() + + if not status then return doh( "Could not read data") end + + if ack ~= 78 then + return doh("No ack received: ".. tostring(ack)) + end + local status, host = dis:readUTF() + if not status then return false, "Could not read data" end + local status, port = dis:readUnsignedInt() + if not status then return false, "Could not read data" end + + dbg("RMI-Ack received (host %s, port: %d) " , host, port) + return true +end + +-- Sends an RMI method call +--@param out - a JavaDos outputstream +--@param objNum -object id (target of call) +--@param hash - the hashcode for the class that is invoked +--@param op - the operation number (method) invoked +--@param arguments - optional, if arguments are needed to this method. Should be an Arguments table +-- or something else which has a getData() function to get binary data +function RmiDataStream:writeMethodCall(out,objNum, hash, op, arguments) + dbg("Invoking object %s, hash %s, opNum %s, args %s", tostring(objNum), tostring(hash), tostring(op), tostring(arguments)) + local dos = self.dos + local dis = self.dis + + -- Send Call: + dos:writeByte(0x50) + -- Send Magic 0xaced + dos:writeShort(0xACED) + -- Send version 0x0005 + dos:writeShort(0x0005) + -- Send TC_BLOKDATA + dos:writeByte(0x77) + + -- send length (byte) + dos:writeByte(0x22) + + -- From sun.rmi.transport.StreamRemoteCall : + -- // write out remote call header info... + -- // call header, part 1 (read by Transport) + -- conn.getOutputStream().write(TransportConstants.Call); + -- getOutputStream(); // creates a MarshalOutputStream + -- id.write(out); // object id (target of call) + -- // call header, part 2 (read by Dispatcher) + -- out.writeInt(op); // method number (operation index) + -- out.writeLong(hash); // stub/skeleton hash + -- Send rest of the call + + local unique, time, count =0,0,0 + + dos:writeLong(objNum);-- id objNum + dos:writeInt(unique); -- space + dos:writeLong(time); + dos:writeShort(count); + dos:writeInt(op) + dos:pack('H',hash) + + -- And now, the arguments + if arguments ~= nil then + dos:write(arguments:getData()) + end + + + dos:flush() + +end +-- Invokes a method over RMI +--@param methodData, a table which should contain the following +--@param objNum -object id (target of call) +--@param hash - the hashcode for the class that is invoked +--@param op - the operation number (method) invoked +--@param arguments - optional, if arguments are needed to this method. Should be an Arguments table +-- or something else which has a getData() function to get binary data +function RmiDataStream:invoke(objNum, hash, op, arguments) + local status, data + local out = self.out + local dis = self.dis + self:writeMethodCall(out,objNum,hash, op, arguments) + local status, retByte = dis:readByte() + if not status then return false, "No return data received from server" end + + if 0x51 ~= retByte then -- 0x51 : Returndata + return false, "No return data received from server" + end + + status, data = self:readReturnData(dis) + return status, data +end + + +-- Reads an RMI ReturnData packet +--@param dis a JavaDIS inputstream +function RmiDataStream:readReturnData(dis) + + --[[ + From -http://turtle.ee.ncku.edu.tw/docs/java/jdk1.2.2/guide/rmi/spec/rmi-protocol.doc3.html : + A ReturnValue of an RMI call consists of a return code to indicate either a normal or + exceptional return, a UniqueIdentifier to tag the return value (used to send a DGCAck if necessary) + followed by the return result: either the Value returned or the Exception thrown. + + + CallData: ObjectIdentifier Operation Hash (Arguments) + ReturnValue: + 0x01 UniqueIdentifier (Value) + 0x02 UniqueIdentifier Exception + + ObjectIdentifier: ObjectNumber UniqueIdentifier + UniqueIdentifier: Number Time Count + Arguments: Value Arguments Value + Value: Object Primitive + + Example: [ac ed][00 05][77][0f][01][25 14 95 21][00 00 01 2b 16 9a 62 5a 80 0b] + [magc][ver ][BL][L ][Ok][ --------------- not interesting atm ----------------------] + + --]] + + -- We need to be able to read at least 7 bytes + -- If that is doable, we can ignore the status on the following readbyte operations + if not dis:canRead(7) then + return doh("Not enough data received") + end + + local status, magic = dis:readShort() -- read magic + local status, version = dis:readShort() -- read version + + + local status, typ = dis:readByte() + if typ ~= TC.TC_BLOCKDATA then + return doh("Expected block data when reading return data") + end + local status, len = dis:readByte() -- packet length + --dis:setReadLimit(len) + local status, ex = dis:readByte() -- 1=ok, 2=exception thrown + if ex ~= 1 then + return doh("Remote call threw exception") + end + + -- We can skip the rest of this block + dis:skip(len -1) + + -- Now, the return value object: + local status, x = readObject0(dis) + dbg("Read object, got %d left in buffer", dis.bReader:bufferSize()) + + + if(dis.bReader:bufferSize() > 0) then + local content = dis.bReader:unpack('H'..tostring(dis.bReader:bufferSize())) + dbg("Buffer content: %s" ,content) + end + return status, x +end +function readObject0(dis) + + local finished = false + local data, status, responseType + + status, responseType = dis:readByte() + if not status then + return doh("Not enough data received") + end + + dbg("Reading object of type : %s" , RMIUtils.tcString(responseType)) + local decoder = TypeDecoders[responseType] + if decoder ~= nil then + status, data = decoder(dis) + if not status then return doh("readObject0: Could not read data %s", tostring(data)) end + dbg("Read: %s", tostring(data)) + return true, data + else + return doh("No decoder found for responsetype: %s" , RMIUtils.tcString(responseType)) + end +end +function readString(dis) + return dis:readUTF() +end +-- Reads return type array +function readArray(dis) + local array = JavaArray:new() + dbg("Reading array class description") + local status, classDesc = readClassDesc(dis) + array:setClass(classDesc) + dbg("Reading array length") + local status, len = dis:readInt() + + if not status then + return doh("Could not read data") + end + + array:setLength(len) + dbg("Reading array of length is %X", len) + for i =1, len, 1 do + local status, object = readObject0(dis) + array:setValue(i,object) + end + return true, array +end + +function readClassDesc(dis) + local status, p = dis:readByte() + if not status then return doh( "Could not read data" ) end + + dbg("reading classdesc: %s" , RMIUtils.tcString(p)) + + local val + + if p == TC.TC_CLASSDESC then + dbg("Reading TC_CLASSDESC") + status, val = readNonProxyDesc(dis) + elseif p == TC.TC_NULL then + dbg("Reading TC_NULL") + status, val = true, nil + elseif p == TC.TC_PROXYCLASSDESC then + dbg("Reading TC_PROXYCLASSDESC") + status, val = readProxyDesc(dis) + else + return doh("TC_classdesc is other %d", p) + end + + if not status then + return doh("Error reading class description") + end + return status, val + + +end +function readOrdinaryObject(dis) + local status, desc = readClassDesc(dis) + if not status then + return doh("Error reading ordinary object") + end + + + if desc:isExternalizable() then + dbg("External content") + local status, extdata = readExternalData(dis) + if status then + desc["externalData"] = extdata + end + else + dbg("Serial content") + local status, serdata = readExternalData(dis) + if status then + desc["externalData"] = serdata + local status, data =parseExternalData(desc) + if status then + desc['externalData'] = data + end + end + end + return status, desc + +end +--[[ + +00000060: 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65 53 74 .server.RemoteSt +00000070: 75 62 e9 fe dc c9 8b e1 65 1a 02 00 00 70 78 72 ub e pxr +00000080: 00 1c 6a 61 76 61 2e 72 6d 69 2e 73 65 72 76 65 java.rmi.serve +00000090: 72 2e 52 65 6d 6f 74 65 4f 62 6a 65 63 74 d3 61 r.RemoteObject a +000000a0: b4 91 0c 61 33 1e 03 00 00 70 78 70 77 32 00 0a a3 pxpw2 +000000b0: 55 6e 69 63 61 73 74 52 65 66 00 09 31 32 37 2e UnicastRef 127. +000000c0: 30 2e 31 2e 31 00 00 94 5c 66 06 14 9c 2c 38 ba 0.1.1 \f ,8 +000000d0: c7 25 14 95 21 00 00 01 2b 16 9a 62 5a 80 01 01 % ! + bZ +000000e0: 78 + +--]] + +-- Attempts to read some object-data, at least remove the block +-- header. This method returns the external data in 'raw' form, +-- since it is up to each class to define an readExternal method +function readExternalData(dis) + local data = {} + while dis.bReader:bufferSize() > 0 do + local status, tc= dis:readByte() + if not status then + return doh("Could not read external data") + end + dbg("readExternalData: %s", RMIUtils.tcString(tc)) + local status, len, content + if tc == TC.TC_BLOCKDATA then + status, len = dis:readByte() + status, content = dis.bReader:skip(len) + print(bin.unpack("H"..tostring(#content),content)) + print(makeStringReadable(content)) + dbg("Read external data (%d bytes): %s " ,len, content) + --local object = ExternalClassParsers['java.rmi.server.RemoteObject'](dis) + --print(object) + return status, content + elseif tc == TC.TC_BLOCKDATALONG then + status, len = dis:readUnsignedInt() + status, content = dis.bReader:skip(len) + return status, content + elseif tc == TC.TC_ENDBLOCKDATA then + --noop + else + return doh("Got unexpected field in readExternalData: %s ", RMIUtils.tcString(tc)) + end + end +end + +---------------------------------------------------------------------------------------------------------- +-- External Java Classes +---------------------------------------------------------------------------------------------------------- +-- This 'class' contains information about certain specific java classes, +-- such as UnicastRef, UnicastRef2. After such an object has been read by +-- the object serialization protocol, it will contain a lump of data which is +-- in 'external' form, and needs to be read in a way which is specific for the class +-- itself. This class contains the implementations for reading out the +-- 'goodies' of e.g UnicastRef, which contain important information about +-- where another RMI-socket is listening and waiting for someone to connect. +ExternalClassParsers = { + --@see sun.rmi.transport.tcp.TCPEndpoint + --@see sun.rmi.server.UnicastRef + --@see sun.rmi.server.UnicastRef2 + UnicastRef = function(dis) + local stat, host = dis:readUTF(); + if not stat then return doh("Parsing external data, could not read host (UTF)") end + local status, port = dis:readUnsignedInt(); + if not stat then return doh("Parsing external data, could not read port (int)") end + + dbg("a host: %s, port %d", host, port) + return true, ("@%s:%d"):format(host,port) + end, + UnicastRef2 = function(dis) + local stat, form = dis:readByte(); + if not stat then return doh("Parsing external data, could not read byte") end + if form == 0 or form == 1 then-- FORMAT_HOST_PORT or FORMAT_HOST_PORT_FACTORY + local stat, host = dis:readUTF(); + if not stat then return doh("Parsing external data, could not read host (UTF)") end + local status, port = dis:readUnsignedInt(); + if not stat then return doh("Parsing external data, could not read port (int)") end + dbg("b host: %s, port %d", host, port) + if form ==0 then + return true, ("@%s:%d"):format(host,port) + end + -- for FORMAT_HOST_PORT_FACTORY, there's an object left to read + local status, object = readObject0(dis) + return true, ("@%s:%d"):format(host,port) + --return true, {host = host, port = port, factory = object} + else + return doh("Invalid endpoint format") + end + end +} +--@see java.rmi.server.RemoteObject:readObject() +ExternalClassParsers['java.rmi.server.RemoteObject'] = function(dis) + local status, refClassName = dis:readUTF() + if not status then return doh("Parsing external data, could not read classname (UTF)") end + if #refClassName == 0 then + local status, ref = readObject0(dis) + return status, ref + end + dbg("Ref class name: %s ", refClassName) + local parser = ExternalClassParsers[refClassName] + + if parser == nil then + return doh("No external class reader for %s" , refClassName) + end + + local status, object = parser(dis) + return status, object +end + +-- Attempts to parse the externalized data of an object. +--@return status, the object data +function parseExternalData(j_object) + + if j_object == nil then + return doh("parseExternalData got nil object") + end + + local className = j_object:getName() + + -- Find parser for the object, move up the hierarchy + local obj = j_object + local parser = nil + while(className ~= nil) do + parser = ExternalClassParsers[className] + if parser ~= nil then break end + + obj = obj:getSuperClass() + if obj== nil then break end-- No more super classes + className = obj:getName() + end + + if parser == nil then + return doh("External reader for class %s is not implemented", tostring(className)) + end + -- Read the actual object, start by creating a new dis based on the data-string + local dis = JavaDIS:new(BufferedReader:new(nil,j_object.externalData)) + local status, object = parser(dis) + if not status then + return doh("Could not parse external data") + end + return true, object +end + +-- Helper function to display data +-- returns the string with all non-printable chars +-- coded as hex +function makeStringReadable(data) + local r = "" + for i=1,#data,1 do + local x = data:byte(i) + if x > 31 and x <127 then + r = r .. data:sub(i,i) + else + r = r .. ("\x%x"):format(x) + end + end + return r +end + +function readNonProxyDesc(dis) + dbg("-- entering readNonProxyDesc--") + local j_class = JavaClass:new() + local status, classname = dis:readUTF() + if not status then return doh( "Could not read data" ) end + j_class:setName(classname) + + local status, serialID = dis:readLongAsHexString() + if not status then return doh("Could not read data") end + j_class:setSerialID(serialID) + + dbg("Set serial ID to %s", tostring(serialID)) + + local status, flags = dis:readByte() + if not status then return doh("Could not read data") end + j_class:setFlags(flags) + + + local status, fieldCount = dis:readShort() + if not status then return doh( "Could not read data") end + + dbg("Fieldcount %d", fieldCount) + + local fields = {} + for i =0, fieldCount-1,1 do + local status, fieldDesc = readFieldDesc(dis) + j_class:addField(fieldDesc) + -- Need to store in list, the field values need to be read + -- after we have finished reading the class description + -- hierarchy + table.insert(fields,fieldDesc) + end + local status, customStrings = skipCustomData(dis) + if status and customStrings ~= nil and #customStrings > 0 then + j_class:setCustomData(customStrings) + end + + local _,superDescriptor = readClassDesc(dis) + + j_class:setSuperClass(superDescriptor) + dbg("Superclass read, now reading %i field values", #fields) + --Read field values + for i=1, #fields, 1 do + local status, fieldType = dis:readByte() + local value = nil + if ( TypeDecoders[fieldType] ) then + status, value= TypeDecoders[fieldType](dis) + else + dbg("ellol reading".. RMIUtils.tcString(p)) + return + end + dbg("Read fieldvalue ".. tostring(value) .. " for field ".. tostring(fields[i])) + fields[i]:setValue(value) + end + dbg("-- leaving readNonProxyDesc--") + return true, j_class + + +end + +function readProxyDesc(dis) + dbg("-- in readProxyDesc--") + local interfaces = '' + local superclass = nil + local status, ifaceNum= dis:readInt() + if not status then return doh("Could not read data") end + --dbg("# interfaces: %d" , ifaceNum) + while ifaceNum > 0 do + local status, iface = dis:readUTF() + if not status then return doh( "Could not read data") end + --table.insert(interfaces, iface) + interfaces = interfaces .. iface ..', ' + dbg("Interface: %s " ,iface) + ifaceNum = ifaceNum-1 + end + + local j_class = JavaClass:new() + + local status, customStrings = skipCustomData(dis) + if status and customStrings ~= nil and #customStrings > 0 then + j_class:setCustomData(customStrings) + end + + local _,superDescriptor = readClassDesc(dis) + + + print ("superdescriptor", superDescriptor) + j_class:setSuperClass(superDescriptor) + j_class:setInterfaces(interfaces) + + dbg("-- leaving readProxyDesc--") + return true, j_class + +end +-- +-- Skips over all block data and objects until TC_ENDBLOCKDATA is +-- encountered. +-- @see java.io.ObjectInputStream.skipCustomData() +--@return status +--@return any strings found while searching +function skipCustomData(dis) + -- If we come across something interesting, just put it into + -- the returnData list + local returnData = {} + while true do + local status, p = dis:readByte() + if not status then + return doh("Could not read data") + end + + if not status then return doh("Could not read data") end + dbg("skipCustomData read %s", RMIUtils.tcString(p)) + + if p == TC.TC_BLOCKDATA or p == TC.TC_BLOCKDATALONG then + dbg("continuing") + --return + elseif p == TC.TC_ENDBLOCKDATA then + return true, returnData + else + -- In the java impl, this is a function called readObject0. We just + -- use the read null, otherwise error + if p == TC.TC_NULL then + -- No op, already read the byte, continue reading + elseif p == TC.TC_STRING then + --dbg("A string is coming!") + local status, str = dis:readUTF() + if not status then + return doh("Could not read data") + end + dbg("Got a string, but don't know what to do with it! : %s",str) + -- Object serialization is a bit messy. I have seen the + -- classpath being sent over a customdata-field, so it is + -- definitely interesting. Quick fix to get it showing + -- is to just stick it onto the object we are currently at. + -- So, just put the string into the returnData and continue + table.insert(returnData, str) + else + return doh("Not implemented in skipcustomData:: %s", RMIUtils.tcString(p)) + end + end + end +end + +function readFieldDesc(dis) + -- fieldDesc: + -- primitiveDesc + -- objectDesc + -- primitiveDesc: + -- prim_typecode fieldName + -- objectDesc: + -- obj_typecode fieldName className1 + -- prim_typecode: + -- `B' // byte + -- `C' // char + -- `D' // double + -- `F' // float + -- `I' // integer + -- `J' // long + -- `S' // short + -- `Z' // boolean + -- obj_typecode: + -- `[` // array + -- `L' // object + local j_field = JavaField:new() + + local status, c = dis:readByte() + if not status then return doh("Could not read data") end + + local char = string.char(c) + + local status, name = dis:readUTF() + if not status then return doh("Could not read data") end + + local fieldType = ('primitive type: (%s) '):format(char) + dbg("Fieldtype, char = %s, %s", tostring(fieldType), tostring(char)) + if char == 'L' or char == '[' then + -- These also have classname which tells the type + -- on the field + local status, fieldclassname = readTypeString(dis) + if not status then return doh("Could not read data") end + if char == '[s' then + fieldType = fieldclassname .. " []" + else + fieldType = fieldclassname + end + end + + if not status then + return false, fieldType + end + + dbg("Field description: name: %s, type: %s", tostring(name), tostring(fieldType)) + + j_field:setType(fieldType) + j_field:setName(name) +-- setType = function( self, typ ) self.type = typ end, +-- setSignature = function( self, sig ) self.signature = sig end, +-- setName = function( self, name ) self.name = name end, +-- setObjectType = function( self, ot ) self.object_type = ot end, +-- setReference = function( self, ref ) self.ref = ref end, + + dbg("Created java field:".. tostring(j_field)) + + return true, j_field + +end + +function readTypeString(dis) + local status, tc = dis:readByte() + if not status then return doh("Could not read data") end + if tc == TC.TC_NULL then + return true, nil + elseif tc== TC.TC_REFERENCE then + return doh("Not implemented, readTypeString(TC_REFERENCE)"); + elseif tc == TC.TC_STRING then + return dis:readUTF() + elseif tc == TC.TC_LONGSTRING then + --TODO, add this (will throw error as is) + return dis:readLongUTF() + end +end + +TypeDecoders = +{ + [TC.TC_ARRAY] = readArray, + [TC.TC_CLASSDESC] = readClassDesc, + [TC.TC_STRING] = readString, + [TC.TC_OBJECT] = readOrdinaryObject, +} + + +-- Class to represent the RMI Registry. +--- Usage : +-- registry = rmi.Registry:new() +-- status, data = registry:list() +Registry ={ + new = function (self,host, port) + local o ={} -- create object + setmetatable(o, self) + self.__index = self -- DIY inheritance + -- Hash code for sun.rmi.registry.RegistryImpl_Stub, which we are invoking : + -- hex: 0x44154dc9d4e63bdf , dec: 4905912898345647071 + self.hash = '44154dc9d4e63bdf' + -- RmiRegistry object id is 0 + self.objId = 0 + o.host = host + o.port = port + return o + end +} +-- Connect to the remote registry. +--@return status +--@return error message +function Registry:_handshake() + local out = RmiDataStream:new() + local status, err = out:connect(self.host,self.port) + + if not status then + return doh("Registry connection failed: %s", tostring(err)) + end + dbg("Registry connection OK "..tostring(out.bsocket) ) + self.out = out + return true +end +-- List the named objects in the remote RMI registry +--@return status +--@return a table of strings , or error message +function Registry:list() + if not self:_handshake() then + return doh("Handshake failed") + end + -- Method list() is op number 1 + return self.out:invoke(self.objId, self.hash,1) +end +-- Perform a lookup on an object in the Registry, +-- takes the name which is bound in the registry +-- as argument +function Registry:lookup(name) + self:_handshake() + -- Method lookup() is op number 2 + -- Takes a string as arguments + local a = Arguments:new() + a:addString(name) + return self.out:invoke(self.objId, self.hash,2, a) +end +---------------------------------------------------------------------------------------------------------- +-- Arguments class +---------------------------------------------------------------------------------------------------------- +-- This class is meant to handle arguments which is sent to a mehtod invoked +-- remotely. It is mean to contain functionality to add java primitive datatypes, +-- such as pushInt, pushString, pushLong etc. When invoking a remote method +-- use this class in this manner: +-- Arguments a = Arguments:new() +-- a:pushString("Argument one") +-- a:pushInt(1) +-- datastream:invoke{objNum=oid, hash=hash, opNum = opid, arguments=a} +-- ... +-- +Arguments = { + + new = function (self,o) + o = o or {} -- create object if user does not provide one + setmetatable(o, self) + self.__index = self -- DIY inheritance + -- We use a buffered socket just to be able to use a javaDOS for writing + self.dos = JavaDOS:new(BufferedWriter:new()) + return o + end, + addString = function(self, str) + self.dos:writeByte(TC.TC_STRING) + self.dos:writeUTF(str) + end, + getData = function(self) + local _, res = self.dos:flush() + return res + end +} + + + +---------------------------------------------------------------------------------------------------------- +-- Below is some codes and definitions from Java +---------------------------------------------------------------------------------------------------------- +-- There are three types of output messages: Call, Ping and DgcAck. +-- A Call encodes a method invocation. A Ping is a transport-level message +-- for testing liveness of a remote virtual machine. +-- A DGCAck is an acknowledgment directed to a +-- server's distributed garbage collector that indicates that remote objects +-- in a return value from a server have been received by the client. + +RMIUtils = { + + -- Indicates a Serializable class defines its own writeObject method. + SC_WRITE_METHOD = 0x01, + -- Indicates Externalizable data written in Block Data mode. + SC_BLOCK_DATA = 0x08, + -- Bit mask for ObjectStreamClass flag. Indicates class is Serializable. + SC_SERIALIZABLE = 0x02, + --Bit mask for ObjectStreamClass flag. Indicates class is Externalizable. + SC_EXTERNALIZABLE = 0x04, + --Bit mask for ObjectStreamClass flag. Indicates class is an enum type. + SC_ENUM = 0x10, + + flagsToString = function(flags) + local retval = '' + if ( bit.band(flags, RMIUtils.SC_WRITE_METHOD) ~= 0) then + retval = retval .. " WRITE_METHOD" + end + if ( bit.band(flags, RMIUtils.SC_BLOCK_DATA) ~= 0) then + retval = retval .. " BLOCK_DATA" + end + if ( bit.band(flags, RMIUtils.SC_EXTERNALIZABLE) ~= 0) then + retval = retval .. " EXTERNALIZABLE" + end + if ( bit.band(flags, RMIUtils.SC_SERIALIZABLE) ~= 0) then + retval = retval .. " SC_SERIALIZABLE" + end + if ( bit.band(flags, RMIUtils.SC_ENUM) ~= 0) then + retval = retval .. " SC_ENUM" + end + return retval + end, + tcString = function (constant) + local x = TC.Strings[constant] or "Unknown code" + return ("%s (0x%x)"):format(x,tostring(constant)) + + end, + +} + +local RMIMessage = { + Call = 0x50, + Ping = 0x52, + DgcAck= 0x54, +} +STREAM_MAGIC = 0xaced +STREAM_VERSION = 5 + +baseWireHandle = 0x7E0000 + diff --git a/scripts/rmi-dumpregistry.nse b/scripts/rmi-dumpregistry.nse new file mode 100644 index 000000000..1ebec747e --- /dev/null +++ b/scripts/rmi-dumpregistry.nse @@ -0,0 +1,225 @@ +description = [[ +This script connects to ta remote RMI registry, and attempts to dump all objects from an RMI Registry. First it tries to +determine the names of all objects bound in the registry, and then it tries to determine information about the objects, +such as the the class names of the superclasses and interfaces. This may, depending on what the registry is used for, give +valuable information about the service. E.g, if the app uses JMX (Java Management eXtensions, you should see an object +called "jmxconnector" on it. + +It also gives information about where the objects are located, (marked with @:port in the output). + +Some apps give away the classpath, which this scripts catches in so called "Custom data". +]] + +-- @usage nmap --script "rmi-dumpregistry.nse" -p 1098 +-- @output +-- PORT STATE SERVICE REASON +-- 1099/tcp open rmi syn-ack +-- | rmi-dumpregistry: +-- | jmxrmi +-- | javax.management.remote.rmi.RMIServerImpl_Stub +-- | @127.0.1.1:40353 +-- | extends +-- | java.rmi.server.RemoteStub +-- | extends +-- |_ java.rmi.server.RemoteObject + +-- @output +-- PORT STATE SERVICE REASON +-- 1099/tcp open rmi syn-ack +-- | rmi-dumpregistry: +-- | cfassembler/default +-- | coldfusion.flex.rmi.DataServicesCFProxyServer_Stub +-- | @192.168.0.3:1271 +-- | extends +-- | java.rmi.server.RemoteStub +-- | extends +-- | java.rmi.server.RemoteObject +-- | Custom data +-- | Classpath +-- | file:/C:/CFusionMX7/runtime/../lib/ant-launcher.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ant.jar +-- | file:/C:/CFusionMX7/runtime/../lib/axis.jar +-- | file:/C:/CFusionMX7/runtime/../lib/backport-util-concurrent.jar +-- | file:/C:/CFusionMX7/runtime/../lib/bcel.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cdo.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cdohost.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cf4was.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cf4was_ae.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cfmx-ssl.jar +-- | file:/C:/CFusionMX7/runtime/../lib/cfusion.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-beanutils-1.5.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-collections-2.1.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-digester-1.3.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-digester-1.7.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-discovery-0.2.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-discovery.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-logging-1.0.2.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-logging-api-1.0.2.jar +-- | file:/C:/CFusionMX7/runtime/../lib/commons-net-1.2.2.jar +-- | file:/C:/CFusionMX7/runtime/../lib/crystal.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flashgateway.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flashremoting_update.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flex-assemblerservice.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-common.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-opt.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-req.jar +-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging.jar +-- | file:/C:/CFusionMX7/runtime/../lib/httpclient.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ib61patch.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ib6addonpatch.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ib6core.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ib6swing.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ib6util.jar +-- | file:/C:/CFusionMX7/runtime/../lib/im.jar +-- | file:/C:/CFusionMX7/runtime/../lib/iText.jar +-- | file:/C:/CFusionMX7/runtime/../lib/iTextAsian.jar +-- | file:/C:/CFusionMX7/runtime/../lib/izmado.jar +-- | file:/C:/CFusionMX7/runtime/../lib/jakarta-oro-2.0.6.jar +-- | file:/C:/CFusionMX7/runtime/../lib/java2wsdl.jar +-- | file:/C:/CFusionMX7/runtime/../lib/jaxrpc.jar +-- | file:/C:/CFusionMX7/runtime/../lib/jdom.jar +-- | file:/C:/CFusionMX7/runtime/../lib/jeb.jar +-- | file:/C:/CFusionMX7/runtime/../lib/jintegra.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ldap.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ldapbp.jar +-- | file:/C:/CFusionMX7/runtime/../lib/log4j.jar +-- | file:/C:/CFusionMX7/runtime/../lib/macromedia_drivers.jar +-- | file:/C:/CFusionMX7/runtime/../lib/mail.jar +-- | file:/C:/CFusionMX7/runtime/../lib/msapps.jar +-- | file:/C:/CFusionMX7/runtime/../lib/pbclient42RE.jar +-- | file:/C:/CFusionMX7/runtime/../lib/pbembedded42RE.jar +-- | file:/C:/CFusionMX7/runtime/../lib/pbserver42RE.jar +-- | file:/C:/CFusionMX7/runtime/../lib/pbtools42RE.jar +-- | file:/C:/CFusionMX7/runtime/../lib/poi-2.5.1-final-20040804.jar +-- | file:/C:/CFusionMX7/runtime/../lib/poi-contrib-2.5.1-final-20040804.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ri_generic.jar +-- | file:/C:/CFusionMX7/runtime/../lib/saaj.jar +-- | file:/C:/CFusionMX7/runtime/../lib/smack.jar +-- | file:/C:/CFusionMX7/runtime/../lib/smpp.jar +-- | file:/C:/CFusionMX7/runtime/../lib/STComm.jar +-- | file:/C:/CFusionMX7/runtime/../lib/tools.jar +-- | file:/C:/CFusionMX7/runtime/../lib/tt-bytecode.jar +-- | file:/C:/CFusionMX7/runtime/../lib/vadmin.jar +-- | file:/C:/CFusionMX7/runtime/../lib/verity.jar +-- | file:/C:/CFusionMX7/runtime/../lib/vparametric.jar +-- | file:/C:/CFusionMX7/runtime/../lib/vsearch.jar +-- | file:/C:/CFusionMX7/runtime/../lib/wc50.jar +-- | file:/C:/CFusionMX7/runtime/../lib/webchartsJava2D.jar +-- | file:/C:/CFusionMX7/runtime/../lib/wsdl2java.jar +-- | file:/C:/CFusionMX7/runtime/../lib/wsdl4j-1.5.1.jar +-- | file:/C:/CFusionMX7/runtime/../lib/wsdl4j.jar +-- | file:/C:/CFusionMX7/runtime/../lib/xalan.jar +-- | file:/C:/CFusionMX7/runtime/../lib/xercesImpl.jar +-- | file:/C:/CFusionMX7/runtime/../lib/xml-apis.jar +-- | file:/C:/CFusionMX7/runtime/../lib/ +-- | file:/C:/CFusionMX7/runtime/../gateway/lib/examples.jar +-- | file:/C:/CFusionMX7/runtime/../gateway/lib/ +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-awt-util.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-css.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-ext.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-transcoder.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-util.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/commons-discovery.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/commons-logging.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/concurrent.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/flex.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jakarta-oro-2.0.7.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jcert.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jnet.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jsse.jar +-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/oscache.jar +-- |_ file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/ + + +--@version 0.5 + +author = "Martin Holst Swende" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require "shortport" +require "rmi" +portrule = shortport.port_or_service({1098, 1099, 1090, 8901, 8902, 8903}, {"rmi"}) + +-- Some lazy shortcuts + +local function dbg(str,...) + stdnse.print_debug("RMI-DUMPREG:"..str, unpack(arg)) +end + +local function dbg_err(str, ... ) + stdnse.print_debug("RMI-DUMPREG-ERR:"..str, unpack(arg)) +end + +-- Function to split a string +local function split(str, sep) + local sep, fields = sep or "; ", {} + local pattern = string.format("([^%s]+)", sep) + str:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields + end + + +--This is a customData formatter. In some cases, the RMI library finds 'custom data' which belongs to an object. +-- This data is not handled correctly, instead, the data is dumped in the objects customData field (which is a table with strings) +-- The RMI library does not do anything more than that - however, here in the land of rmi-dumpregistry land, we may have +-- more knowledge about how to interpret that data. +-- In the wild, coldfusion.flex.rmi.DataServicesCFProxyServer_Stub e.g discloses the classpath in this variable. This method looks at +-- the contents of the custom data. if it looks like a class path, we display it as such. This method is passed to the toTable() method +-- of the returned RMI object. +-- @return title, data +function customDataFormatter(className, customData) + if customData == nil then return nil end + if #customData ==0 then return nil end + + local retData = {} + for k,v in ipairs(customData) do + if v:find("file:/") == 1 then + -- This is a classpath + cp = split(v, "; ") -- Splits into table + table.insert(retData, "Classpath") + table.insert(retData, cp) + else + table.insert(retData[v]) + end + end + + return "Custom data", retData +end + + +function action(host,port, args) + + local registry= rmi.Registry:new( host.ip, port.number) + + + local status, j_array = registry:list() + local output = {} + if not status then + return false, ("Registry listing failed (%s)"):format(tostring(j_array)) + end + -- It's definitely RMI! + port.version.name ='rmi' + port.version.product='Java RMI Registry' + nmap.set_port_version(host,port,'hardmatched') + + -- Monkey patch the java-class in rmi, to set our own custom data formatter + -- for classpaths + rmi.JavaClass.customDataFormatter = customDataFormatter + + -- We expect an array of strings to be the return data + local data = j_array:getValues() + for i,name in ipairs( data ) do + --print(data) + table.insert(output, name) + dbg("Querying object %s", name) + local status, j_object= registry:lookup(name) + + if status then + table.insert(output, j_object:toTable()) + end + + + end + return stdnse.format_output(true, output) +end diff --git a/scripts/script.db b/scripts/script.db index ff6a51b33..3221583a1 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -105,6 +105,7 @@ Entry { filename = "pptp-version.nse", categories = { "version", } } Entry { filename = "qscan.nse", categories = { "discovery", "safe", } } Entry { filename = "realvnc-auth-bypass.nse", categories = { "default", "safe", "vuln", } } Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } } +Entry { filename = "rmi-dumpregistry.nse", categories = { "discovery", "safe", } } Entry { filename = "robots.txt.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "rpcinfo.nse", categories = { "discovery", "safe", } } Entry { filename = "skypev2-version.nse", categories = { "version", } }