mirror of
https://github.com/nmap/nmap.git
synced 2025-12-09 14:11:29 +00:00
Add json.lua, couchdb-databases.nse, and couchdb-stats.nse, all by
Martin Holst Swende.
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
[NOT YET RELEASED]
|
||||
|
||||
o [NSE] Added the scripts couchdb-databases and couchdb-stats by
|
||||
Martin Holst Swende, which list CouchDB databases and show access
|
||||
statistics. These scripts use the new json.lua library, also by
|
||||
Martin.
|
||||
|
||||
o Fixed the parsing of libdnet DLPI interface names that contain more
|
||||
than one string of digits. Joe Dietz reported that an interface with
|
||||
the name e1000g0 was causing the error message
|
||||
|
||||
513
nselib/json.lua
Normal file
513
nselib/json.lua
Normal file
@@ -0,0 +1,513 @@
|
||||
--- Library methods for handling Json data. It handles
|
||||
-- json encoding and decoding
|
||||
--
|
||||
-- There is a test-section at the bottom which shows some example
|
||||
-- parsing. If you want to parse json, you can test it by pasting sample json
|
||||
-- into the TESTS table and run the test() method
|
||||
|
||||
-- More info about Json at http://www.ietf.org/rfc/rfc4627.txt
|
||||
|
||||
-- !NOTE! Due to some differences between javascript and lua, there are some
|
||||
-- conversion problems for null-values. Null-values in javascript are not equal to
|
||||
-- nil values in lua. Nil is more corresponding to javascript 'undefined'.
|
||||
|
||||
-- As an example :
|
||||
-- Executing the following javascript : var a= {b:null}; alert(a.b + " != " + a.c);
|
||||
-- yields the string "null != undefined". Assigning a table a nil value in lua basically
|
||||
-- removes it from the table, without leaving the key in place. I.e,
|
||||
-- >a ={b=nil}
|
||||
-- >print(a.b, a.c)
|
||||
-- nil nil
|
||||
|
||||
-- !!!Therefore, javascript null values are represented by json.NULL.!!!
|
||||
--
|
||||
-- @author Martin Holst Swende
|
||||
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
|
||||
-- TODO: Unescape/escape unicode
|
||||
-- Version 0.4
|
||||
|
||||
-- Created 01/25/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
|
||||
-- Heavily modified 02/22/2010 - v0.3. Rewrote the parser into an OO-form, to not have to handle
|
||||
-- all kinds of state with parameters and return values.
|
||||
-- Modified 02/27/2010 - v0.4 Added unicode handling (written by David Fifield). Renamed toJson
|
||||
-- and fromJson intogenerate() and parse(), implemented more proper numeric parsing and added some more error checking.
|
||||
|
||||
module("json", package.seeall)
|
||||
require("bit")
|
||||
require("nsedebug")
|
||||
|
||||
--Some local shortcuts
|
||||
local function dbg(str,...)
|
||||
stdnse.print_debug("Json:"..str, unpack(arg))
|
||||
end
|
||||
local function d4(str,...)
|
||||
if nmap.debugging() > 3 then dbg(str,unpack(arg)) end
|
||||
end
|
||||
local function d3(str,...)
|
||||
if nmap.debugging() > 2 then dbg(str,unpack(arg)) end
|
||||
end
|
||||
|
||||
--local dbg =stdnse.print_debug
|
||||
local function dbg_err(str,...)
|
||||
stdnse.print_debug("json-ERR:"..str, unpack(arg))
|
||||
end
|
||||
|
||||
-- Javascript null representation, see explanation above
|
||||
NULL = {}
|
||||
|
||||
local ESCAPE_TABLE={
|
||||
['\\"']='"',
|
||||
['\\/']= '/',
|
||||
['\\b']=string.char(0x08),
|
||||
['\\f']=string.char(0x0C),
|
||||
['\\n']=string.char(0x0A),
|
||||
['\\r']=string.char(0x0D),
|
||||
['\\t']=string.char(0x09),
|
||||
}
|
||||
-- Escapes a string
|
||||
--@param str the string
|
||||
--@return a string where the special chars have been escaped
|
||||
local function escape(str)
|
||||
|
||||
--This should be done first, so not in table
|
||||
str = string.gsub(str, '\\\\', '\\')-- Escape \
|
||||
|
||||
for esc,char in pairs(ESCAPE_TABLE) do
|
||||
str =str:gsub(char,esc)
|
||||
end
|
||||
--!TODO : Unicode escape
|
||||
return ('"%s"'):format(str)
|
||||
end
|
||||
|
||||
-- Creates json data from an object
|
||||
--@param object a table containing data
|
||||
--@return a string containing valid json
|
||||
function generate(obj)
|
||||
|
||||
-- NULL-check must be performed before
|
||||
-- checking type == table, since the NULL-object
|
||||
-- is a table
|
||||
if obj == NULL then
|
||||
return "null"
|
||||
elseif obj == false then
|
||||
return "false"
|
||||
elseif obj == true then
|
||||
return "true"
|
||||
elseif type(obj) == "number" then
|
||||
return string.format("%g", obj)
|
||||
elseif type(obj) == "string" then
|
||||
return escape(obj)
|
||||
elseif type(obj) == "table" then
|
||||
local k, v, elems
|
||||
elems = {}
|
||||
if #obj > 0 then
|
||||
-- Array
|
||||
for _, v in ipairs(obj) do
|
||||
elems[#elems + 1] = generate(v)
|
||||
end
|
||||
return "[" .. table.concat(elems, ", ") .. "]"
|
||||
else
|
||||
-- Object
|
||||
for k, v in pairs(obj) do
|
||||
elems[#elems + 1] = escape(k) .. ": " .. generate(v)
|
||||
end
|
||||
return "{" .. table.concat(elems, ", ") .. "}"
|
||||
end
|
||||
else
|
||||
error("Unknown data type in generate")
|
||||
end
|
||||
end
|
||||
|
||||
local esc_chars =
|
||||
{
|
||||
['\\'] = '\\\\',
|
||||
n =string.char(0x0A),
|
||||
f=string.char(0x0C),
|
||||
r=string.char(0x0D),
|
||||
t=string.char(0x09),
|
||||
b=string.char(0x08),
|
||||
['"'] = '\"',
|
||||
}
|
||||
-- This is the parser, implemented in OO-form to deal with state better
|
||||
Json = {}
|
||||
-- Constructor
|
||||
function Json:new(input)
|
||||
local o = {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o.input = input
|
||||
o.pos = 1 -- Pos is where the NEXT letter will be read
|
||||
return o
|
||||
end
|
||||
|
||||
-- Gets next character and ups the position
|
||||
--@return next character
|
||||
function Json:next()
|
||||
self.pos = self.pos+1
|
||||
return self.input:sub(self.pos-1, self.pos-1)
|
||||
end
|
||||
-- Updates the position to next non whitespace position
|
||||
function Json:eatWhiteSpace()
|
||||
--Find next non-white char
|
||||
local a,b = self.input:find("%S",self.pos)
|
||||
if not a then
|
||||
self:syntaxerror("Empty data")
|
||||
return
|
||||
end
|
||||
self.pos = a
|
||||
end
|
||||
|
||||
-- Jumps to a specified position
|
||||
--@param position where to go
|
||||
function Json:jumpTo(position)
|
||||
self.pos = position
|
||||
end
|
||||
|
||||
-- Returns next character, but without upping position
|
||||
--@return next character
|
||||
function Json:peek()
|
||||
return self.input:sub(self.pos, self.pos)
|
||||
end
|
||||
|
||||
--@return true if more input is in store
|
||||
function Json:hasMore()
|
||||
return self.input:len() >= self.pos
|
||||
end
|
||||
|
||||
-- Checks that the following input is equal to a string
|
||||
-- and updates position so next char will be after that string
|
||||
-- If false, triggers a syntax error
|
||||
--@param str the string to test
|
||||
function Json:assertStr(str)
|
||||
local content = self.input:sub(self.pos,self.pos+str:len()-1)
|
||||
if(content == str) then-- All ok
|
||||
-- Jump forward
|
||||
self:jumpTo(self.pos+str:len())
|
||||
return
|
||||
end
|
||||
self:syntaxerror(("Expected '%s' but got '%s'"):format( str, content))
|
||||
end
|
||||
|
||||
-- Trigger a syntax error
|
||||
function Json:syntaxerror(reason)
|
||||
self.error = ("Syntax error near pos %d: %s input: %s"):format( self.pos, reason, self.input)
|
||||
dbg(self.error)
|
||||
end
|
||||
-- Check if any errors has occurred
|
||||
function Json:errors()
|
||||
return self.error ~= nil
|
||||
end
|
||||
-- This is where the parsing begins.
|
||||
--@return the parsed object or puts error messages in self.error
|
||||
function Json:parseStart()
|
||||
return self:parseValue(true)
|
||||
end
|
||||
|
||||
-- Parses a value
|
||||
--@param first if this is the first time the input is read,
|
||||
-- the value must be array or object, otherwise a syntax error will
|
||||
-- be triggered
|
||||
--@return the parsed value
|
||||
function Json:parseValue(first)
|
||||
|
||||
-- If first is set to true, this is the first
|
||||
-- object received. Therefore, this must be
|
||||
-- either an Object or Array ( first chars { or [ )
|
||||
|
||||
self:eatWhiteSpace()
|
||||
local c = self:peek()
|
||||
|
||||
if(first and c ~= '{' and c ~= '[') then
|
||||
self:syntaxerror(("Json must start with object or array (started with %s)"):format(c))
|
||||
return
|
||||
end
|
||||
local value
|
||||
if c == '{' then
|
||||
value = self:parseObject()
|
||||
elseif c == '[' then
|
||||
value = self:parseArray()
|
||||
elseif c == '"' then
|
||||
value = self:parseString()
|
||||
elseif c == 'n' then
|
||||
self:assertStr("null")
|
||||
value = NULL
|
||||
elseif c == 't' then
|
||||
self:assertStr("true")
|
||||
value = true
|
||||
elseif c == 'f' then
|
||||
self:assertStr("false")
|
||||
value = false
|
||||
else -- numeric
|
||||
-- number = [ minus ] int [ frac ] [ exp ]
|
||||
local a,b =self.input:find("-?%d+%.?%d*[eE]?[+-]?%d*", self.pos)
|
||||
if not a or not b then
|
||||
self:syntaxerror("Error 1 parsing numeric value")
|
||||
return
|
||||
end
|
||||
value = tonumber(self.input:sub(a,b))
|
||||
if(value == nil) then
|
||||
self:syntaxerror("Error 2 parsing numeric value")
|
||||
return
|
||||
end
|
||||
self:jumpTo(b+1)
|
||||
end
|
||||
return value
|
||||
end
|
||||
-- Parses a json object {}
|
||||
--@return the object (or triggers a syntax error)
|
||||
function Json:parseObject()
|
||||
local object = {}
|
||||
local _= self:next() -- Eat {
|
||||
|
||||
while(self:hasMore() and not self:errors()) do
|
||||
self:eatWhiteSpace()
|
||||
local c = self:peek()
|
||||
if(c == '}') then -- Empty object, probably
|
||||
self:next() -- Eat it
|
||||
return object
|
||||
end
|
||||
|
||||
if(c ~= '"') then
|
||||
self:syntaxerror(("Expected '\"', got '%s'"):format(c))
|
||||
return
|
||||
end
|
||||
|
||||
local key = self:parseString()
|
||||
if self:errors() then
|
||||
return
|
||||
end
|
||||
self:eatWhiteSpace()
|
||||
c = self:next()
|
||||
if(c ~= ':') then
|
||||
self:syntaxerror("Expected ':' got "..c)
|
||||
return
|
||||
end
|
||||
local value = self:parseValue()
|
||||
|
||||
if self:errors() then
|
||||
return
|
||||
end
|
||||
|
||||
object[key] = value
|
||||
|
||||
self:eatWhiteSpace()
|
||||
c = self:next()
|
||||
-- Valid now is , or }
|
||||
if(c == '}') then
|
||||
return object
|
||||
end
|
||||
if(c ~= ',') then
|
||||
self:syntaxerror("Expected ',' or '}', got "..c)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Parses a json array [] or triggers a syntax error
|
||||
--@return the array object
|
||||
function Json:parseArray()
|
||||
local array = {}
|
||||
self:next()
|
||||
while(self:hasMore() and not self:errors()) do
|
||||
self:eatWhiteSpace()
|
||||
if(self:peek() == ']') then -- Empty array, probably
|
||||
self:next()
|
||||
break
|
||||
end
|
||||
local value = self:parseValue()
|
||||
if self:errors() then
|
||||
return
|
||||
end
|
||||
table.insert(array, value)
|
||||
self:eatWhiteSpace()
|
||||
local c = self:next()
|
||||
-- Valid now is , or ]
|
||||
if(c == ']') then return array end
|
||||
if(c ~= ',') then
|
||||
self:syntaxerror(("Expected ',' but got '%s'"):format(c))
|
||||
return
|
||||
end
|
||||
end
|
||||
return array
|
||||
end
|
||||
|
||||
-- Decode a Unicode escape, assuming that self.pos starts just after the
|
||||
-- initial \u. May consume an additional escape in the case of a UTF-16
|
||||
-- surrogate pair. See RFC 2781 for UTF-16.
|
||||
function Json:parseUnicodeEscape()
|
||||
local n, cp
|
||||
local hex, lowhex
|
||||
local s, e
|
||||
|
||||
s, e, hex = self.input:find("^(....)", self.pos)
|
||||
if not hex then
|
||||
self:syntaxerror(("EOF in Unicode escape \\u%s"):format(self.input:sub(self.pos)))
|
||||
return
|
||||
end
|
||||
n = tonumber(hex, 16)
|
||||
if not n then
|
||||
self:syntaxerror(("Bad unicode escape \\u%s"):format(hex))
|
||||
return
|
||||
end
|
||||
cp = n
|
||||
self.pos = e + 1
|
||||
if n < 0xD800 or n > 0xDFFF then
|
||||
return cp
|
||||
end
|
||||
if n >= 0xDC00 and n <= 0xDFFF then
|
||||
self:syntaxerror(("Not a Unicode character: U+%04X"):format(cp))
|
||||
return
|
||||
end
|
||||
|
||||
-- Beginning of a UTF-16 surrogate.
|
||||
s, e, lowhex = self.input:find("^\\u(....)", self.pos)
|
||||
if not lowhex then
|
||||
self:syntaxerror(("Bad unicode escape \\u%s (missing low surrogate)"):format(hex))
|
||||
return
|
||||
end
|
||||
n = tonumber(lowhex, 16)
|
||||
if not n or not (n >= 0xDC00 and n <= 0xDFFF) then
|
||||
self:syntaxerror(("Bad unicode escape \\u%s\\u%s (bad low surrogate)"):format(hex, lowhex))
|
||||
return
|
||||
end
|
||||
self.pos = e + 1
|
||||
cp = 0x10000 + bit.band(cp, 0x3FF) * 0x400 + bit.band(n, 0x3FF)
|
||||
-- also remove last "
|
||||
return cp
|
||||
end
|
||||
|
||||
-- Encode a Unicode code point to UTF-8. See RFC 3629.
|
||||
-- Does not check that cp is a real charaacter; that is, doesn't exclude the
|
||||
-- surrogate range U+D800 - U+DFFF and a handful of others.
|
||||
local function utf8_enc(cp)
|
||||
local bytes = {}
|
||||
local n, mask
|
||||
|
||||
if cp % 1.0 ~= 0.0 or cp < 0 then
|
||||
-- Only defined for nonnegative integers.
|
||||
return nil
|
||||
elseif cp <= 0x7F then
|
||||
-- Special case of one-byte encoding.
|
||||
return string.char(cp)
|
||||
elseif cp <= 0x7FF then
|
||||
n = 2
|
||||
mask = 0xC0
|
||||
elseif cp <= 0xFFFF then
|
||||
n = 3
|
||||
mask = 0xE0
|
||||
elseif cp <= 0x10FFFF then
|
||||
n = 4
|
||||
mask = 0xF0
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
while n > 1 do
|
||||
bytes[n] = string.char(0x80 + bit.band(cp, 0x3F))
|
||||
cp = bit.rshift(cp, 6)
|
||||
n = n - 1
|
||||
end
|
||||
bytes[1] = string.char(mask + cp)
|
||||
|
||||
return table.concat(bytes)
|
||||
end
|
||||
|
||||
-- Parses a json string
|
||||
-- @return the string or triggers syntax error
|
||||
function Json:parseString()
|
||||
|
||||
local val = ''
|
||||
local c = self:next()
|
||||
assert( c == '"')
|
||||
while(self:hasMore()) do
|
||||
local c = self:next()
|
||||
|
||||
if(c == '"') then -- end of string
|
||||
break
|
||||
elseif(c == '\\') then-- Escaped char
|
||||
local d = self:next()
|
||||
if esc_chars[d] ~= nil then
|
||||
val = val .. esc_chars[d]
|
||||
elseif d == 'u' then -- Unicode chars
|
||||
local codepoint = self:parseUnicodeEscape()
|
||||
if not codepoint then
|
||||
return
|
||||
end
|
||||
val = val .. utf8_enc(codepoint)
|
||||
else
|
||||
self:syntaxerror(("Undefined escape character '%s'"):format(d))
|
||||
return false
|
||||
end
|
||||
else -- Char
|
||||
val = val .. c
|
||||
end
|
||||
end
|
||||
return val
|
||||
end
|
||||
--- Parses json data into an object form
|
||||
-- This is the method you probably want to use if you
|
||||
-- use this library from a script.
|
||||
--@param input : a json string
|
||||
--@return status : true if ok, false if bad
|
||||
--@return an object representing the json, or error message
|
||||
function parse(data)
|
||||
local parser = Json:new(data)
|
||||
local result = parser:parseStart()
|
||||
if(parser.error) then
|
||||
return false, parser.error
|
||||
end
|
||||
return true, result
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------------
|
||||
-- Test-code for debugging purposes below
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
local TESTS = {
|
||||
'{"a":1}',
|
||||
'{"a":true}',
|
||||
'{"a": false}',
|
||||
'{"a": null \r\n, \t "b" \f:"ehlo"}',
|
||||
'{"a\\"a":"a\\"b\\"c\\"d"}',
|
||||
'{"foo":"gaz\\"onk", "pi":3.14159,"hello":{ "wo":"rld"}}',
|
||||
'{"a":1, "b":2}',
|
||||
'{"foo":"gazonk", "pi":3.14159,"hello":{ "wo":"rl\\td"}}',
|
||||
'[1,2,3,4,5,null,false,true,"\195\164\195\165\195\182\195\177","bar"]',
|
||||
'[]',-- This will yield {} in toJson, since in lua there is only one basic datatype - and no difference when empty
|
||||
'{}',
|
||||
|
||||
'', -- error
|
||||
'null', -- error
|
||||
'"abc"', -- error
|
||||
'{a":1}', -- error
|
||||
'{"a" bad :1}', -- error
|
||||
'["a\\\\t"]', -- Should become Lua {"a\t"}
|
||||
'[0.0.0]', -- error
|
||||
'[-1]',
|
||||
'[-1.123e-2]',
|
||||
'[5e3]',
|
||||
'[5e+3]',
|
||||
'[5E-3]',
|
||||
'[5.5e3]',
|
||||
'["a\\\\"]', -- Should become Lua {"a\"}
|
||||
'{"a}": 1}', -- Should become Lua {"a}" = 1}
|
||||
'["key": "value"]', -- error
|
||||
'["\\u0041"]', -- Should become Lua {"A"}
|
||||
'["\\uD800"]', -- error
|
||||
'["\\uD834\\uDD1EX"]', -- Should become Lua {"\240\157\132\158X"}
|
||||
}
|
||||
function test()
|
||||
print("Tests running")
|
||||
local i,v,res,status
|
||||
for i,v in pairs(TESTS) do
|
||||
print("----------------------------")
|
||||
print(v)
|
||||
status,res = parse(v)
|
||||
if not status then print( res) end
|
||||
if(status) then
|
||||
print(generate(res))
|
||||
else
|
||||
print("Error:".. res)
|
||||
end
|
||||
end
|
||||
end
|
||||
test()
|
||||
93
scripts/couchdb-databases.nse
Normal file
93
scripts/couchdb-databases.nse
Normal file
@@ -0,0 +1,93 @@
|
||||
description = [[
|
||||
Gets database tables from a CouchDB database
|
||||
For more info about the CouchDB HTTP Api, see
|
||||
http://wiki.apache.org/couchdb/HTTP_database_API
|
||||
|
||||
]]
|
||||
---
|
||||
-- @usage
|
||||
-- nmap -p 5984 --script "couchdb-databases.nse" <host>
|
||||
-- @output
|
||||
-- PORT STATE SERVICE REASON
|
||||
-- 5984/tcp open unknown syn-ack
|
||||
-- | couchdb-databases:
|
||||
-- | 1 = test_suite_db
|
||||
-- | 2 = test_suite_db_a
|
||||
-- | 3 = test_suite_db/with_slashes
|
||||
-- | 4 = moneyz
|
||||
-- | 5 = creditcards
|
||||
-- | 6 = test_suite_users
|
||||
-- |_ 7 = test_suite_db_b
|
||||
--@version 0.2
|
||||
-- Created 01/12/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
|
||||
|
||||
-- TODO : Authentication not implemented
|
||||
|
||||
author = "Martin Holst Swende"
|
||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "safe"}
|
||||
require "shortport"
|
||||
require "http"
|
||||
require "json"
|
||||
|
||||
portrule = shortport.port_or_service({5984})
|
||||
-- Some lazy shortcuts
|
||||
local function dbg(str,...)
|
||||
stdnse.print_debug("couchdb-get-tables:"..str, unpack(arg))
|
||||
end
|
||||
|
||||
local DISCARD = {}
|
||||
--- Removes uninteresting data from the table
|
||||
-- uses the DISCARD table above to see what
|
||||
-- keys should be omitted from the results
|
||||
-- @param data a table containg data
|
||||
--@return another table containing data, with some keys removed
|
||||
local function queryResultToTable(data)
|
||||
local result = {}
|
||||
for k,v in pairs(data) do
|
||||
dbg("(%s,%s)",k,tostring(v))
|
||||
if DISCARD[k] ~= 1 then
|
||||
if type(v) == 'table' then
|
||||
table.insert(result,k)
|
||||
table.insert(result,queryResultToTable(v))
|
||||
else
|
||||
table.insert(result,(("%s = %s"):format(tostring(k), tostring(v))))
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
action = function(host, port)
|
||||
local data, result, err
|
||||
dbg("Requesting all databases")
|
||||
data = http.get( host, port, '/_all_dbs' )
|
||||
|
||||
-- check that body was received
|
||||
if not data.body or data.body == "" then
|
||||
local msg = ("%s did not respond with any data."):format(host.targetname or host.ip )
|
||||
dbg( msg )
|
||||
return msg
|
||||
end
|
||||
|
||||
-- The html body should look like this :
|
||||
-- ["somedatabase", "anotherdatabase"]
|
||||
|
||||
local status, result = json.parse(data.body)
|
||||
if not status then
|
||||
dbg(result)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Here we know it is a couchdb
|
||||
port.version.name ='httpd'
|
||||
port.version.product='Apache CouchDB'
|
||||
nmap.set_port_version(host,port,'hardmatched')
|
||||
|
||||
-- We have a valid table in result containing the parsed json
|
||||
-- now, get all the interesting bits
|
||||
|
||||
result = queryResultToTable(result)
|
||||
|
||||
return stdnse.format_output(true, result )
|
||||
end
|
||||
166
scripts/couchdb-stats.nse
Normal file
166
scripts/couchdb-stats.nse
Normal file
@@ -0,0 +1,166 @@
|
||||
description = [[
|
||||
Gets database statistics from a CouchDB database
|
||||
For more info about the CouchDB HTTP Api, see
|
||||
http://wiki.apache.org/couchdb/Runtime_Statistics
|
||||
and
|
||||
http://wiki.apache.org/couchdb/HTTP_database_API
|
||||
|
||||
]]
|
||||
---
|
||||
-- @usage
|
||||
-- nmap -p 5984 --script "couchdb-stats.nse" <host>
|
||||
-- @output
|
||||
-- PORT STATE SERVICE REASON
|
||||
-- 5984/tcp open httpd syn-ack
|
||||
-- | couchdb-stats:
|
||||
-- | httpd_request_methods
|
||||
-- | GET (number of HTTP GET requests)
|
||||
-- | current = 5
|
||||
-- | count = 1617
|
||||
-- | couchdb
|
||||
-- | request_time (length of a request inside CouchDB without MochiWeb)
|
||||
-- | current = 1
|
||||
-- | count = 5
|
||||
-- | httpd_status_codes
|
||||
-- | 200 (number of HTTP 200 OK responses)
|
||||
-- | current = 5
|
||||
-- | count = 1617
|
||||
-- | httpd
|
||||
-- | requests (number of HTTP requests)
|
||||
-- | current = 5
|
||||
-- | count = 1617
|
||||
-- |_ Authentication : NOT enabled ('admin party')
|
||||
--@version 0.3
|
||||
--
|
||||
-- Created 01/20/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
|
||||
-- Modified 07/02/2010 - v0.2 - added test if auth is enabled, compacted output a bit (mhs)
|
||||
|
||||
author = "Martin Holst Swende"
|
||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "safe"}
|
||||
require "shortport"
|
||||
require "http"
|
||||
require "json"
|
||||
portrule = shortport.port_or_service({5984})
|
||||
-- Some lazy shortcuts
|
||||
local function dbg(str,...)
|
||||
stdnse.print_debug("couchdb-stats:"..str, unpack(arg))
|
||||
end
|
||||
|
||||
|
||||
local DISCARD = {stddev=1,min=1,max=1, mean=1}
|
||||
--- Removes uninteresting data from the table
|
||||
-- uses the DISCARD table above to see what
|
||||
-- keys should be omitted from the results
|
||||
-- @param data a table containg data
|
||||
--@return another table containing data, with some keys removed
|
||||
local function queryResultToTable(data)
|
||||
local result = {}
|
||||
for k,v in pairs(data) do
|
||||
dbg("(%s,%s)",k,tostring(v))
|
||||
if DISCARD[k] ~= 1 then
|
||||
if type(v) == 'table' then
|
||||
if v["description"] ~= nil then
|
||||
k = string.format("%s (%s)",tostring(k), tostring(v["description"]))
|
||||
v["description"] = nil
|
||||
end
|
||||
table.insert(result,k)
|
||||
table.insert(result,queryResultToTable(v))
|
||||
else
|
||||
table.insert(result,(("%s = %s"):format(tostring(k), tostring(v))))
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
action = function(host, port)
|
||||
local data, result, err
|
||||
|
||||
data = http.get( host, port, '/_stats' )
|
||||
|
||||
-- check that body was received
|
||||
if not data.body or data.body == "" then
|
||||
local msg = ("%s did not respond with any data."):format(hostrgetname or host.ip )
|
||||
dbg( msg )
|
||||
return msg
|
||||
end
|
||||
|
||||
-- The html body should look like this :
|
||||
--
|
||||
--{"httpd_status_codes":{"200":{"current":10,"count":29894,"mean":0.0003345152873486337,"min":0,"max":1,"stddev":0.01828669972606202,"description":"number of HTTP 200 OK responses"},"500":{"current":1,"count":28429,"mean":0.00003517534911534013,"min":0,"max":1,"stddev":0.005930776661631644,"description":"number of HTTP 500 Internal Server Error responses"}},"httpd_request_methods":{"GET":{"current":12,"count":29894,"mean":0.00040141834481835866,"min":0,"max":2,"stddev":0.02163701147572207,"description":"number of HTTP GET requests"}},"httpd":{"requests":{"current":12,"count":29894,"mean":0.00040141834481835866,"min":0,"max":2,"stddev":0.02163701147572207,"description":"number of HTTP requests"}},"couchdb":{"request_time":{"current":23,"count":12,"mean":32.58333333333333,"min":1,"max":287,"stddev":77.76723638882608,"description":"length of a request inside CouchDB without MochiWeb"}}}
|
||||
|
||||
local status, result = json.parse(data.body)
|
||||
if not status then
|
||||
dbg(result)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Here we know it is a couchdb
|
||||
port.version.name ='httpd'
|
||||
port.version.product='Apache CouchDB'
|
||||
nmap.set_port_version(host,port,'hardmatched')
|
||||
|
||||
-- We have a valid table in result containing the parsed json
|
||||
-- now, get all the interesting bits
|
||||
|
||||
result = queryResultToTable(result)
|
||||
|
||||
-- Additionally, we can check if authentication is used :
|
||||
-- The following actions are restricted if auth is used
|
||||
-- create db (PUT /database)
|
||||
-- delete db (DELETE /database)
|
||||
-- Creating a design document (PUT /database/_design/app)
|
||||
-- Updating a design document (PUT /database/_design/app?rev=1-4E2)
|
||||
-- Deleting a design document (DELETE /database/_design/app?rev=1-6A7)
|
||||
-- Triggering compaction (POST /_compact)
|
||||
-- Reading the task status list (GET /_active_tasks)
|
||||
-- Restart the server (POST /_restart)
|
||||
-- Read the active configuration (GET /_config)
|
||||
-- Update the active configuration (PUT /_config)
|
||||
|
||||
data = http.get( host, port, '/_config' )
|
||||
local status, authresult = json.parse(data.body)
|
||||
|
||||
-- If authorization is used, we should get back something like
|
||||
-- {"error":"unauthorized","reason":"You are not a server admin."}
|
||||
-- Otherwise, a *lot* of data, :
|
||||
-- {"httpd_design_handlers":{"_info":"{couch_httpd_db, handle_design_info_req}",
|
||||
-- "_list":"{couch_httpd_show, handle_view_list_req}","_show":"{couch_httpd_show, handle_doc_show_req}",
|
||||
-- "_update":"{couch_httpd_show, handle_doc_update_req}","_view":"{couch_httpd_view, handle_view_req}"},
|
||||
-- "httpd_global_handlers":{"/":"{couch_httpd_misc_handlers, handle_welcome_req, <<\"Welcome\">>}",
|
||||
-- "_active_tasks":"{couch_httpd_misc_handlers, handle_task_status_req}",
|
||||
-- "_all_dbs":"{couch_httpd_misc_handlers, handle_all_dbs_req}",
|
||||
-- "_config":"{couch_httpd_misc_handlers, handle_config_req}",
|
||||
-- "_log":"{couch_httpd_misc_handlers, handle_log_req}","_oauth":"{couch_httpd_oauth, handle_oauth_req}",
|
||||
-- "_replicate":"{couch_httpd_misc_handlers, handle_replicate_req}","_restart":"{couch_httpd_misc_handlers, handle_restart_req}",
|
||||
-- "_session":"{couch_httpd_auth, handle_session_req}","_sleep":"{couch_httpd_misc_handlers, handle_sleep_req}",
|
||||
-- "_stats":"{couch_httpd_stats_handlers, handle_stats_req}","_user":"{couch_httpd_auth, handle_user_req}",
|
||||
-- "_utils":"{couch_httpd_misc_handlers, handle_utils_dir_req, \"/usr/share/couchdb/www\"}",
|
||||
-- "_uuids":"{couch_httpd_misc_handlers, handle_uuids_req}","favicon.ico":"{couch_httpd_misc_handlers, handle_favicon_req, \"/usr/share/couchdb/www\"}"},
|
||||
-- "query_server_config":{"reduce_limit":"true"},"log":{"file":"/var/log/couchdb/0.10.0/couch.log","level":"info"},
|
||||
-- "query_servers":{"javascript":"/usr/bin/couchjs /usr/share/couchdb/server/main.js"},
|
||||
-- "daemons":{"batch_save":"{couch_batch_save_sup, start_link, []}","db_update_notifier":"{couch_db_update_notifier_sup, start_link, []}",
|
||||
-- "external_manager":"{couch_external_manager, start_link, []}","httpd":"{couch_httpd, start_link, []}",
|
||||
-- "query_servers":"{couch_query_servers, start_link, []}","stats_aggregator":"{couch_stats_aggregator, start, []}",
|
||||
-- "stats_collector":"{couch_stats_collector, start, []}","view_manager":"{couch_view, start_link, []}"},
|
||||
-- "httpd":{"WWW-Authenticate":"Basic realm=\"administrator\"","authentication_handlers":"{couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}",
|
||||
-- "bind_address":"127.0.0.1","default_handler":"{couch_httpd_db, handle_request}","port":"5984"},"httpd_db_handlers":{"_changes":"{couch_httpd_db, handle_changes_req}",
|
||||
-- "_compact":"{couch_httpd_db, handle_compact_req}","_design":"{couch_httpd_db, handle_design_req}","_temp_view":"{couch_httpd_view, handle_temp_view_req}",
|
||||
-- "_view":"{couch_httpd_view, handle_db_view_req}","_view_cleanup":"{couch_httpd_db, handle_view_cleanup_req}"},
|
||||
-- "couch_httpd_auth":{"authentication_db":"users","require_valid_user":"false","secret":"replace this with a real secret in your local.ini file"},
|
||||
-- "couchdb":{"batch_save_interval":"1000","batch_save_size":"1000","database_dir":"/var/lib/couchdb/0.10.0","delayed_commits":"true",
|
||||
-- "max_attachment_chunk_size":"4294967296","max_dbs_open":"100","max_document_size":"4294967296",
|
||||
-- "os_process_timeout":"5000","util_driver_dir":"/usr/lib/couchdb/erlang/lib/couch-0.10.0/priv/lib","view_index_dir":"/var/lib/couchdb/0.10.0"}}
|
||||
local auth = "Authentication : %s"
|
||||
local authEnabled = "unknown"
|
||||
|
||||
if(status) then
|
||||
if(authresult["error"] == "unauthorized") then authEnabled = "enabled"
|
||||
elseif (authresult["httpd_design_handlers"] ~= nil) then authEnabled = "NOT enabled ('admin party')"
|
||||
end
|
||||
end
|
||||
table.insert(result, auth:format(authEnabled))
|
||||
return stdnse.format_output(true, result )
|
||||
end
|
||||
Reference in New Issue
Block a user