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]
|
[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
|
o Fixed the parsing of libdnet DLPI interface names that contain more
|
||||||
than one string of digits. Joe Dietz reported that an interface with
|
than one string of digits. Joe Dietz reported that an interface with
|
||||||
the name e1000g0 was causing the error message
|
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