1553 lines
48 KiB
Lua
1553 lines
48 KiB
Lua
|
--- 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 invocation. 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 https://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.2
|
||
|
|
||
|
-- Created 09/06/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
|
||
|
-- Fixed more documentation - v0.2 Martin Holst Swende <martin@swende.se>
|
||
|
|
||
|
local nmap = require "nmap"
|
||
|
local stdnse = require "stdnse"
|
||
|
local string = require "string"
|
||
|
local table = require "table"
|
||
|
_ENV = stdnse.module("rmi", stdnse.seeall)
|
||
|
-- Some lazy shortcuts
|
||
|
|
||
|
local function dbg(str,...)
|
||
|
local arg={...}
|
||
|
stdnse.debug3("RMI:"..str, table.unpack(arg))
|
||
|
end
|
||
|
-- Convenience function to both print an error message and return <false, msg>
|
||
|
-- Example usage :
|
||
|
-- if foo ~= "gazonk" then
|
||
|
-- return doh("Foo should be gazonk but was %s", foo)
|
||
|
-- end
|
||
|
local function doh(str,...)
|
||
|
local arg={...}
|
||
|
stdnse.debug1("RMI-ERR:"..tostring(str), table.unpack(arg))
|
||
|
return false, str
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- BufferedWriter
|
||
|
-- This buffering writer provide functionality much like javas BufferedWriter.
|
||
|
--
|
||
|
-- BufferedWriter wraps string.pack, 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.
|
||
|
--@usage:
|
||
|
-- local bWriter = BufferedWriter:new(socket)
|
||
|
-- local breader= BufferedReader:new(socket)
|
||
|
--
|
||
|
-- bWriter.pack('>i4', integer)
|
||
|
-- bWriter.flush() -- sends the data
|
||
|
--
|
||
|
-- if bsocket:canRead(4) then -- Waits until four bytes can be read
|
||
|
-- local packetLength = bsocket:unpack('>i4') -- 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 string.pack
|
||
|
pack = function(self, fmt, ... )
|
||
|
local arg={...}
|
||
|
self.writeBuffer = self.writeBuffer .. string.pack( fmt, table.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 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 string.unpack 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.
|
||
|
--@usage:
|
||
|
-- local bWriter = BufferedWriter:new(socket)
|
||
|
-- local breader= BufferedReader:new(socket)
|
||
|
--
|
||
|
-- bWriter.pack('>i4', integer)
|
||
|
-- bWriter.flush() -- sends the data
|
||
|
--
|
||
|
-- if bsocket:canRead(4) then -- Waits until four bytes can be read
|
||
|
-- local packetLength = bsocket:unpack('>i4') -- Read the four bytess
|
||
|
-- if bsocket:canRead(packetLength) then
|
||
|
-- -- ...continue reading packet values
|
||
|
|
||
|
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 available 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,
|
||
|
---
|
||
|
--@return Returns the number of bytes already available for reading
|
||
|
bufferSize = function(self)
|
||
|
return #self.readBuffer +1 -self.pos
|
||
|
end,
|
||
|
---
|
||
|
-- This function works just like string.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
|
||
|
--@return the unpacked value (NOT the index)
|
||
|
unpack = function(self,format)
|
||
|
local ret = {string.unpack(format, self.readBuffer, self.pos)}
|
||
|
self.pos = ret[#ret]
|
||
|
ret[#ret] = nil
|
||
|
return table.unpack(ret)
|
||
|
end,
|
||
|
---
|
||
|
-- This function works just like string.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
|
||
|
--@return the unpacked value (NOT the index)
|
||
|
peekUnpack = function(self,format)
|
||
|
return string.unpack(format, self.readBuffer, self.pos)
|
||
|
end,
|
||
|
---
|
||
|
-- Tries to read a byte, without consuming it.
|
||
|
--@return status
|
||
|
--@return bytevalue
|
||
|
peekByte = function(self)
|
||
|
if self:canRead(1) then
|
||
|
return true, self:peekUnpack('B')
|
||
|
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 classes are generated when this file is loaded, by the definitions in the JavaTypes
|
||
|
-- table. That table contains mappings between the format used by string.pack and the types
|
||
|
-- available in java, as well 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 = '>i4', len= 4},
|
||
|
{name = 'UnsignedInt', expr = '>I4', len= 4},
|
||
|
{name = 'Short', expr = '>i2', len= 2},
|
||
|
{name = 'UnsignedShort', expr = '>I2', len= 2},
|
||
|
{name = 'Long', expr = '>i8', len= 8},
|
||
|
{name = 'UnsignedLong', expr = '>I8', len= 8},
|
||
|
{name = 'Byte', expr = '>B', len= 1},
|
||
|
}
|
||
|
|
||
|
---
|
||
|
-- The JavaDOS classes
|
||
|
-- The JavaDOS class is an approximation of a java DataOutputStream. It provides convenience functions
|
||
|
-- for writing java types to an underlying 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()
|
||
|
|
||
|
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('>s2', text)
|
||
|
end,
|
||
|
pack = function(self, ...)
|
||
|
local arg={...}
|
||
|
return self.bWriter:pack(table.unpack(arg))
|
||
|
end,
|
||
|
write = function(self, data)
|
||
|
return self.bWriter:send(data)
|
||
|
end,
|
||
|
flush = function(self)
|
||
|
return self.bWriter:flush()
|
||
|
end,
|
||
|
}
|
||
|
|
||
|
---
|
||
|
-- The JavaDIS class
|
||
|
-- JavaDIS is close to java DataInputStream. It provides convenience functions
|
||
|
-- for reading java types from an underlying BufferedReader
|
||
|
--
|
||
|
-- 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()
|
||
|
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 nonstandard 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. Therefore
|
||
|
-- 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 string.unpack can reuse the data to unpack with '>s2'
|
||
|
local len = self.bReader:peekUnpack('>I2')
|
||
|
--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('>s2')
|
||
|
--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, stdnse.tohex(self.bReader:unpack('c8'))
|
||
|
|
||
|
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
|
||
|
---
|
||
|
-- This class represents a java class and is what is returned by the library
|
||
|
-- when invoking a remote function. Therefore, this can also represent a java
|
||
|
-- object instance.
|
||
|
JavaClass = {
|
||
|
new = function(self)
|
||
|
local o = {}
|
||
|
setmetatable(o, self)
|
||
|
self.__index = self
|
||
|
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 (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[#data+1] = ("%s "):format(self.name)
|
||
|
else
|
||
|
data[#data+1] = "???"
|
||
|
end
|
||
|
if self.superClass~=nil then
|
||
|
data[#data+1] = " extends ".. tostring( self.superClass)
|
||
|
end
|
||
|
if self.ifaces ~= nil then
|
||
|
data[#data+1] = " implements " .. self.ifaces
|
||
|
end
|
||
|
if self.fields ~=nil then
|
||
|
for i=1, #self.fields do
|
||
|
if i == 1 then
|
||
|
data[#data+1] = "["
|
||
|
end
|
||
|
data[#data+1] = tostring(self.fields[i])
|
||
|
if ( i < #self.fields ) then
|
||
|
data[#data+1] = ";"
|
||
|
else
|
||
|
data[#data+1] = "]"
|
||
|
end
|
||
|
|
||
|
end
|
||
|
end
|
||
|
return table.concat(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,
|
||
|
|
||
|
}
|
||
|
--- Represents a field in an object, i.e an object member
|
||
|
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 )
|
||
|
if self.value ~= nil then
|
||
|
return string.format("%s %s = %s", self.type, self.name, self.value)
|
||
|
else
|
||
|
return string.format("%s %s", self.type, self.name)
|
||
|
end
|
||
|
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
|
||
|
table.insert(data, self.value)
|
||
|
end
|
||
|
end
|
||
|
return data
|
||
|
end,
|
||
|
|
||
|
}
|
||
|
---
|
||
|
-- Represents a java array. Internally, this is a lua list of JavaClass-instances
|
||
|
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+1] = self.values[i]..","
|
||
|
end
|
||
|
data[#data+1] = "}"
|
||
|
return table.concat(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",
|
||
|
},
|
||
|
|
||
|
}
|
||
|
|
||
|
local Version= 0x02
|
||
|
local Proto= {Stream=0x4b, SingleOp=0x4c, Multiplex=0x4d}
|
||
|
|
||
|
---
|
||
|
-- 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
|
||
|
--
|
||
|
|
||
|
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,
|
||
|
}
|
||
|
-- 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
|
||
|
|
||
|
----
|
||
|
-- Connects to a remote service. The connection process creates a
|
||
|
-- socket and does some handshaking. If this is successful,
|
||
|
-- 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: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 connection 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:writeUnsignedShort(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:write(stdnse.fromhex(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
|
||
|
--@return status
|
||
|
--@return a JavaClass instance
|
||
|
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:readUnsignedShort() -- 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('c'..tostring(dis.bReader:bufferSize()))
|
||
|
dbg("Buffer content: %s", stdnse.tohex(content))
|
||
|
end
|
||
|
return status, x
|
||
|
end
|
||
|
---
|
||
|
-- Deserializes a serialized java object
|
||
|
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
|
||
|
|
||
|
-- 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(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
|
||
|
|
||
|
----
|
||
|
-- ExternalClassParsers : 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 sts_host, host = dis:readUTF()
|
||
|
if not sts_host then
|
||
|
return doh("Parsing external data, could not read host (UTF)")
|
||
|
end
|
||
|
local sts_port, port = dis:readUnsignedInt()
|
||
|
if not sts_port 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,
|
||
|
---
|
||
|
--@see sun.rmi.transport.tcp.TCPEndpoint
|
||
|
--@see sun.rmi.server.UnicastRef
|
||
|
--@see sun.rmi.server.UnicastRef2
|
||
|
UnicastRef2 = function(dis)
|
||
|
local sts_form, form = dis:readByte()
|
||
|
if not sts_form then
|
||
|
return doh("Parsing external data, could not read byte")
|
||
|
end
|
||
|
if not (form == 0 or form == 1) then-- FORMAT_HOST_PORT or FORMAT_HOST_PORT_FACTORY
|
||
|
return doh("Invalid endpoint format")
|
||
|
end
|
||
|
local sts_host, host = dis:readUTF()
|
||
|
if not sts_host then
|
||
|
return doh("Parsing external data, could not read host (UTF)")
|
||
|
end
|
||
|
local sts_port, port = dis:readUnsignedInt()
|
||
|
if not sts_port 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 sts_object, object = readObject0(dis)
|
||
|
return true, ("@%s:%d"):format(host,port)
|
||
|
--return true, {host = host, port = port, factory = object}
|
||
|
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)
|
||
|
return data:gsub("[\x00-\x1f\x7f-\xff]", function (x)
|
||
|
return ("\\x%02x"):format(x:byte())
|
||
|
end)
|
||
|
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("error reading".. RMIUtils.tcString(fieldType))
|
||
|
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 == '[' 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,
|
||
|
}
|
||
|
|
||
|
---
|
||
|
-- Registry
|
||
|
-- 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
|
||
|
--@return status
|
||
|
--@return JavaClass-object
|
||
|
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 method invoked
|
||
|
-- remotely. It is mean to contain functionality to add java primitive datatypes,
|
||
|
-- such as pushInt, pushString, pushLong etc. All of these are not implemented
|
||
|
-- currently
|
||
|
--@usage: When invoking a remote method
|
||
|
-- use this class in this manner:
|
||
|
-- Arguments a = Arguments:new()
|
||
|
-- a:addString("foo")
|
||
|
-- 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,
|
||
|
addRaw = function(self, str)
|
||
|
self.dos:write(str)
|
||
|
end,
|
||
|
getData = function(self)
|
||
|
local _, res = self.dos:flush()
|
||
|
return res
|
||
|
end
|
||
|
}
|
||
|
|
||
|
|
||
|
---
|
||
|
-- RMIUtils class provides some 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 ( (flags & RMIUtils.SC_WRITE_METHOD) ~= 0) then
|
||
|
retval = retval .. " WRITE_METHOD"
|
||
|
end
|
||
|
if ( (flags & RMIUtils.SC_BLOCK_DATA) ~= 0) then
|
||
|
retval = retval .. " BLOCK_DATA"
|
||
|
end
|
||
|
if ( (flags & RMIUtils.SC_EXTERNALIZABLE) ~= 0) then
|
||
|
retval = retval .. " EXTERNALIZABLE"
|
||
|
end
|
||
|
if ( (flags & RMIUtils.SC_SERIALIZABLE) ~= 0) then
|
||
|
retval = retval .. " SC_SERIALIZABLE"
|
||
|
end
|
||
|
if ( (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
|
||
|
|
||
|
return _ENV;
|