288 lines
9.9 KiB
Lua
288 lines
9.9 KiB
Lua
local nmap = require "nmap"
|
|
local shortport = require "shortport"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local http = require "http"
|
|
|
|
description = [[
|
|
Gathers info from the Metasploit rpc service. It requires a valid login pair.
|
|
After authentication it tries to determine Metasploit version and deduce the OS
|
|
type. Then it creates a new console and executes few commands to get
|
|
additional info.
|
|
|
|
References:
|
|
* http://wiki.msgpack.org/display/MSGPACK/Format+specification
|
|
* https://community.rapid7.com/docs/DOC-1516 Metasploit RPC API Guide
|
|
]]
|
|
|
|
---
|
|
--@usage
|
|
-- nmap <target> --script=metasploit-info --script-args username=root,password=root
|
|
--@output
|
|
-- 55553/tcp open metasploit-msgrpc syn-ack
|
|
-- | metasploit-info:
|
|
-- | Metasploit version: 4.4.0-dev Ruby version: 1.9.3 i386-mingw32 2012-02-16 API version: 1.0
|
|
-- | Additional info:
|
|
-- | Host Name: WIN
|
|
-- | OS Name: Microsoft Windows XP Professional
|
|
-- | OS Version: 5.1.2600 Service Pack 3 Build 2600
|
|
-- | OS Manufacturer: Microsoft Corporation
|
|
-- | OS Configuration: Standalone Workstation
|
|
-- | OS Build Type: Uniprocessor Free
|
|
-- | ..... lots of other info ....
|
|
-- | Domain: WORKGROUP
|
|
-- |_ Logon Server: \\BLABLA
|
|
--
|
|
-- @args metasploit-info.username Valid metasploit rpc username (required)
|
|
-- @args metasploit-info.password Valid metasploit rpc password (required)
|
|
-- @args metasploit-info.command Custom command to run on the server (optional)
|
|
--
|
|
-- @see metasploit-msgrpc-brute.nse
|
|
|
|
|
|
|
|
author = "Aleksandar Nikolic"
|
|
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
|
categories = {"intrusive","safe"}
|
|
|
|
portrule = shortport.port_or_service(55553,"metasploit-msgrpc")
|
|
local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
|
|
local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
|
|
local arg_command = stdnse.get_script_args(SCRIPT_NAME .. ".command")
|
|
local os_type
|
|
|
|
-- returns a "prefix" that msgpack uses for strings
|
|
local get_prefix = function(data)
|
|
if #data <= 31 then
|
|
return string.pack("B", 0xa0 + #data)
|
|
else
|
|
return "\xda" .. string.pack(">I2", #data)
|
|
end
|
|
end
|
|
|
|
-- returns a msgpacked data for console.read
|
|
local encode_console_read = function(method,token, console_id)
|
|
return "\x93" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token .. get_prefix(console_id) .. console_id
|
|
end
|
|
|
|
-- returns a msgpacked data for console.write
|
|
local encode_console_write = function(method, token, console_id, command)
|
|
return "\x94" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token .. get_prefix(console_id) .. console_id .. get_prefix(command) .. command
|
|
end
|
|
|
|
-- returns a msgpacked data for auth.login
|
|
local encode_auth = function(username, password)
|
|
local method = "auth.login"
|
|
return "\x93\xaa" .. method .. get_prefix(username) .. username .. get_prefix(password) .. password
|
|
end
|
|
|
|
-- returns a msgpacked data for any method without extra parameters
|
|
local encode_noparam = function(token,method)
|
|
-- token is always the same length
|
|
return "\x92" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token
|
|
end
|
|
|
|
-- does the actual call with specified, pre-packed data
|
|
-- and returns the response
|
|
local msgrpc_call = function(host, port, msg)
|
|
local data
|
|
local options = {
|
|
header = {
|
|
["Content-Type"] = "binary/message-pack"
|
|
}
|
|
}
|
|
data = http.post(host,port, "/api/",options, nil , msg)
|
|
if data and data.status and tostring( data.status ):match( "200" ) then
|
|
return data.body
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- auth.login wrapper, returns the auth token
|
|
local login = function(username, password,host,port)
|
|
|
|
local data = msgrpc_call(host, port, encode_auth(username,password))
|
|
|
|
if data then
|
|
local start = string.find(data,"success")
|
|
if start > -1 then
|
|
-- get token
|
|
local token = string.sub(string.sub(data,start),17) -- "manually" unpack token
|
|
return true, token
|
|
else
|
|
return false, nil
|
|
end
|
|
end
|
|
stdnse.debug1("something is wrong:" .. data )
|
|
return false, nil
|
|
end
|
|
|
|
-- core.version wrapper, returns version info, and sets the OS type
|
|
-- so we can decide which commands to send later
|
|
local get_version = function(host, port, token)
|
|
local msg = encode_noparam(token,"core.version")
|
|
|
|
local data = msgrpc_call(host, port, msg)
|
|
-- unpack data
|
|
if data then
|
|
-- get version, ruby version, api version
|
|
local start = string.find(data,"version")
|
|
local metasploit_version
|
|
local ruby_version
|
|
local api_version
|
|
if start then
|
|
metasploit_version = string.sub(string.sub(data,start),9)
|
|
start = string.find(metasploit_version,"ruby")
|
|
start = start - 2
|
|
metasploit_version = string.sub(metasploit_version,1,start)
|
|
start = string.find(data,"ruby")
|
|
ruby_version = string.sub(string.sub(data,start),6)
|
|
start = string.find(ruby_version,"api")
|
|
start = start - 2
|
|
ruby_version = string.sub(ruby_version,1,start)
|
|
start = string.find(data,"api")
|
|
api_version = string.sub(string.sub(data,start),5)
|
|
-- put info in a table and parse for OS detection and other info
|
|
port.version.name = "metasploit-msgrpc"
|
|
port.version.product = metasploit_version
|
|
port.version.name_confidence = 10
|
|
nmap.set_port_version(host,port)
|
|
local info = "Metasploit version: " .. metasploit_version .. " Ruby version: " .. ruby_version .. " API version: " .. api_version
|
|
if string.find(ruby_version,"mingw") < 0 then
|
|
os_type = "linux" -- assume linux for now
|
|
else -- mingw compiler means it's a windows build
|
|
os_type = "windows"
|
|
end
|
|
stdnse.debug1("%s", info)
|
|
return info
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- console.create wrapper, returns console_id
|
|
-- which we can use to interact with metasploit further
|
|
local create_console = function(host,port,token)
|
|
local msg = encode_noparam(token,"console.create")
|
|
local data = msgrpc_call(host, port, msg)
|
|
-- unpack data
|
|
if data then
|
|
--get console id
|
|
local start = string.find(data,"id")
|
|
local console_id
|
|
if start then
|
|
console_id = string.sub(string.sub(data,start),4)
|
|
local next_token = string.find(console_id,"prompt")
|
|
console_id = string.sub(console_id,1,next_token-2)
|
|
return console_id
|
|
end
|
|
end
|
|
return nil
|
|
|
|
end
|
|
|
|
-- console.read wrapper
|
|
local read_console = function(host,port,token,console_id)
|
|
local msg = encode_console_read("console.read",token,console_id)
|
|
local data = msgrpc_call(host, port, msg)
|
|
-- unpack data
|
|
if data then
|
|
-- check if busy
|
|
while string.byte(data,string.len(data)) == 0xc3 do
|
|
-- console is busy , let's retry in one second
|
|
stdnse.sleep(1)
|
|
data = msgrpc_call(host, port, msg)
|
|
end
|
|
local start = string.find(data,"data")
|
|
local read_data
|
|
if start then
|
|
read_data = string.sub(string.sub(data,start),8)
|
|
local next_token = string.find(read_data,"prompt")
|
|
read_data = string.sub(read_data,1,next_token-2)
|
|
return read_data
|
|
end
|
|
end
|
|
end
|
|
|
|
-- console.write wrapper
|
|
local write_console = function(host,port,token,console_id,command)
|
|
local msg = encode_console_write("console.write",token,console_id,command .. "\n")
|
|
local data = msgrpc_call(host, port, msg)
|
|
-- unpack data
|
|
if data then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- console.destroy wrapper, just to be nice, we don't want console to hang ...
|
|
local destroy_console = function(host,port,token,console_id)
|
|
local msg = encode_console_read("console.destroy",token,console_id)
|
|
local data = msgrpc_call(host, port, msg)
|
|
end
|
|
|
|
-- write command and read result helper
|
|
local write_read_console = function(host,port,token, console_id,command)
|
|
if write_console(host,port,token,console_id, command) then
|
|
local read_data = read_console(host,port,token,console_id)
|
|
if read_data then
|
|
read_data = string.sub(read_data,string.find(read_data,"\n")+1) -- skip command echo
|
|
return read_data
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
action = function( host, port )
|
|
if not arg_username or not arg_password then
|
|
stdnse.debug1("This script requires username and password supplied as arguments")
|
|
return false
|
|
end
|
|
|
|
-- authenticate
|
|
local status, token = login(arg_username,arg_password,host,port)
|
|
if status then
|
|
-- get version info
|
|
local info = get_version(host,port,token)
|
|
local console_id = create_console(host,port,token)
|
|
if console_id then
|
|
local read_data = read_console(host,port,token,console_id) -- first read the banner/ascii art
|
|
stdnse.debug2("%s", read_data) -- print the nice looking banner if dbg level high enough :)
|
|
if read_data then
|
|
if os_type == "linux" then
|
|
read_data = write_read_console(host,port,token,console_id, "uname -a")
|
|
if read_data then
|
|
info = info .. "\nAdditional info: " .. read_data
|
|
end
|
|
read_data = write_read_console(host,port,token,console_id, "id")
|
|
if read_data then
|
|
info = info .. read_data
|
|
end
|
|
elseif os_type == "windows" then
|
|
read_data = write_read_console(host,port,token,console_id, "systeminfo")
|
|
if read_data then
|
|
stdnse.debug2("%s", read_data) -- print whole info if dbg level high enough
|
|
local stop = string.find(read_data,"Hotfix") -- trim data down , systeminfo return A LOT
|
|
read_data = string.sub(read_data,1,stop-2)
|
|
info = info .. "\nAdditional info: \n" .. read_data
|
|
end
|
|
end
|
|
if arg_command then
|
|
read_data = write_read_console(host,port,token,console_id, arg_command)
|
|
if read_data then
|
|
info = info .. "\nCustom command output: " .. read_data
|
|
end
|
|
end
|
|
if read_data then
|
|
-- let's be nice and close the console
|
|
destroy_console(host,port,token,console_id)
|
|
end
|
|
end
|
|
end
|
|
if info then
|
|
return stdnse.format_output(true,info)
|
|
end
|
|
end
|
|
return false
|
|
end
|