Dumping the RMI Registry with NMAP
A while ago, I wrote a NSE script to a Java RMI Registry and dump out information about the objects in the registry. This is a blog-post to shed some light on NSE-development in general and that script in particular.
Nmap nowadays comes with a scripting engine, (Nmap Scripting Engine : NSE). When a particular service is encountered (say, “rmi”) in the service-scan, or a particular port is found open in the port-scan, a script which is “interested” in communicating with that particular port or service can be executed. NSE scripts are written in Lua, which I find very nice to work with. Nmap provides basic functionality for common tasks, such as socket communication, binary conversion, output control etc.
RMI is Java’s remote method invocation, i.e. Java’s native API for distributing application across different hosts. An object which implements the Remote interface can be called by anoher JVM. For example, say there is an object- let’s call it monitor – which resides inside your application and measures statistics for the application, or contains the runtime-configuration. Another application which monitors a cluster of servers may want to connect to each of these and provide real-time monitoring and administration of the services. In order for the cluster-monitor to locate the different monitors, the monitors would first have to make their presence known. They would typically do that by connecting to the RMI Registry.
The RMI Registry is typically found on port 1098 and 1099, often in pairs where one is primary and one is fall-over. It simply keeps tabs on where objects are located. So, the Registry is the natural starting point for anyone trying to gain information about the application which is running.
Over the wire, RMI is mostly java serialization protocol with a few additions (method call invocation, for example). Creating an RMI library for Lua may sound like an odd idea, but it has a few benefits:
- By reimplementing RMI instead of using java native RMI to perform these tests, I did not have to worry about problems arising when java tries to instantiate stubs of remote objects which belong to a class that is not on the current classpath.
- It can be used by Nmap!
Also: by using OpenJDK as a reference, I really just had to translate Java into Lua (and try to avoid the messy parts).
The basics
Nmap provides only the basics of socket programming, and the sockets do not perform any buffering on reads and writes. Therefore, I start off with creating my Lua-version of buffered reader and writer. I’ll just show the reader here:
---
-- 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.
--@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
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,
---
--@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 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
--@return the unpacked value (NOT the index)
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
--@return the unpacked value (NOT the index)
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, self: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,
}
A couple of comments: NSE does not provide a very good exception-handling system, so (almost) all functions return multiple return values, where the first is usually the status: true if all is well, false if something went wrong. The function doh(..) is just a wrapper for that :
local function doh(str,...)
stdnse.print_debug("RMI-ERR:"..tostring(str), unpack(arg))
return false, str
end
Once buffering sockets are up and running, I need something which resembles the DataWriter and DataReader from java. NSE provides a bin-library to do binary conversion between formats, and my library uses them. I'll just show the Lua-version of java DataInputStream here:
---
-- 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 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 doh("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')
-- Check that we have data
if not self.bReader:canRead(len) then
return doh("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')
return true, val
end,
skip = function(self, len)
return self.bReader:skip(len)
end,
canRead = function(self, len)
return self.bReader:canRead(len)
end,
}
It actually only defines readUTF, not the other types. In order to separate a bit between actual *code* and *format*, I define a set of translations between Java format for basic types and the format used by the bin-library:
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},
}
It basically specifies that a java short corresponds to the format string ">s" in the bin-library. Once that is done, I monkey-patch the javaDOS with the format definitions:
-- Generate writer-functions on the JavaDOS/JavaDIS classes on the fly
-- 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
The call to _generateWriterFunc() creates a new function named ("write"+"Short") in the library. The only reason I had to implement UTF a bit 'special' is that the length of the UTF is not known in advance, so the reader cannot know if it can read the entire string until it has read the string length.
RMI
I won't go into the details about RMI implementation details of the library. But to give a brief overview, in order to perform a remote method invocation, a few things are needed.
- Host and port, which Nmap provides.
- Object id: a unique identifier for the object with which you are trying to communicate. The Registry-object has id 0
- Hash code for the remote class of the object you are trying to invoke a method on. The hash for the Registry, (sun.rmi.registry.RegistryImpl_Stub) is 0x44154dc9d4e63bdf
- Operation id: a numeric identifier of what method you are trying to invoke. The Registry has the following methods which will be used: list()=0, lookup(String)=2
- Optional: any arguments which are needed by the method
These values can be located inside the class-files for any Remote class. The RMIRegistry is implemented in OO-fashion inside my RMI library, and looks like this:
---
-- 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
The actual NSE-script uses the library like this:
local registry= rmi.Registry:new( host.ip, port.number)
local status, j_array = registry:list()
If all goes well, the variable j_array will contain a list of strings, where each string is the name of an object in the registry. It then loops over the names and queries additional info about each of them:
-- 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
Testing
To write NSE scripts yourself, just place the scripts (or symlinks to them) into your nmap home scripts-dir, any libraries in the nselib/-dir, and run:
nmap --script "rmi-dumpregistry.nse" -p 1098
In order to perform some real live testing, you can use Nmap's built in functionality to randomly scan internet hosts with the -iR switch. The following command will randomly scan ten thousand addresses on port 1098 and port 1099, and if they are found to be open, the rmi-registry dumper will be executed:
nmap --script rmi-dumpregistry.nse -p 1098,1099 -iR 10000
As I was testing this on the internet, I noticed two things. The first being that the probably most common usage for RMI is JMX. JMX is Java Management eXtensions, basically it is what I mentioned above: a java object which can be used to monitor and modify a java application. These objects are typically named "jmxconsole", and if you locate such an object it is possible to perform bruteforce password guessing against it. This is not implemented in Lua, but is very simple to write in native Java. Anyone with a valid username/password can then use jconsole to connect to a jmx service.
The other thing I noticed was that ColdFusion/Flex disclosed the entire classpath of the application that I had encountered:
-- 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
-- [.. the list goes on ...]
What is displayed above is, for each object in the registry:
- The name: cfassembler/default
- The location of the remote object. In this case, the object resides on an internal address, 192.168.0.3. In other cases, it may reveal other publically accessible hosts which were not part of the original scan
- The class hierarchy. In this case "extends RemoteStub which extends RemoteObject". In other cases, it may reveal information about what this application is used for and what the objects represent. It may also reveal the name of a class which is open source - if so, it is possible to download the class definitions and communicate directly with the object using java native RMI - and invoke methods on the objects.
- Any so called 'Custom data', which is data that is encoded in a way which is privy to the class: Java RMI cannot parse custom data without first instantiating an object of said class first. It can contain data of any format that the programmer choose to put in the field. In this case, I noticed that it was a classpath and made a modification to parse that out and prettify for the end user.
While this is far from a 0day, it is definitely not good to send out your internal driveletters, paths and source code details on the tubes.
Conclusion
Writing Nmap scripts is easy to do, if you think the steps above sounds complicated, blame Java (and me). The script itself was committed a while ago, and will be available in the next release (unless you update your nmap from subversion, in which case you already have it).
I can also recommend viewing the presentation from BlackHat 2010 (http://nmap.org/presentations/BHDC10/) given by Fyodor and David Fifield with the title "Mastering the Nmap Scripting Engine". It is an awesome presentation where Fyodor hacks Microsoft and David shows his skills by creating an nmap script from scratch (in front of a live audience - now THAT's hardcore!).
Also, the presentation briefly mentions me by name (blink and you'll miss it!) in the section about upcoming scripts, which is the closest I have come to the stage at BlackHat so far
All source code is available either from nmap subversion repository or my swanky mercurial repositories, the repository nsescripts contains this script and some other NSE-related things I have been working on.
Disclaimer
This tool is provided as a means for a system administrator or security consultant, such as myself, to be able to discover potentially vulnerable services in complex or large environments without having to be an expert at Java RMI. As with all tools of this type, it may just as well be used by attackers - but an attacker with basic knowledge of Java RMI could easily perform all of the above (and more) with a very simple Java program.