1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 12:41:29 +00:00

o [NSE] Added the Apache JServer Protocol (AJP) library and the scripts

ajp-methods, ajp-headers and ajp-auth. [Patrik Karlsson]
This commit is contained in:
patrik
2012-05-07 18:49:22 +00:00
parent cec2dd7816
commit d02dafb630
6 changed files with 634 additions and 0 deletions

View File

@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*- # Nmap Changelog ($Id$); -*-text-*-
o [NSE] Added the Apache JServer Protocol (AJP) library and the scripts
ajp-methods, ajp-headers and ajp-auth. [Patrik Karlsson]
o In XML output, <osclass> elements are now child elements of the o In XML output, <osclass> elements are now child elements of the
<osmatch> they belong to. Old output was thus: <osmatch> they belong to. Old output was thus:
<os><osclass/><osclass/>...<osmatch/><osmatch/>...</os> <os><osclass/><osclass/>...<osmatch/><osmatch/>...</os>

430
nselib/ajp.lua Normal file
View File

@@ -0,0 +1,430 @@
---
-- A basic AJP 1.3 implementation based on documentation available from Apache
-- mod_proxy_ajp; http://httpd.apache.org/docs/2.2/mod/mod_proxy_ajp.html
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
--
module(... or "ajp",package.seeall)
local bin = require('bin')
local match = require('match')
local url = require('url')
AJP = {
-- The magic prefix that has to be present in all requests
Magic = 0x1234,
-- Methods encoded as numeric values
Method = {
['OPTIONS'] = 1,
['GET'] = 2,
['HEAD'] = 3,
['POST'] = 4,
['PUT'] = 5,
['DELETE'] = 6,
['TRACE'] = 7,
['PROPFIND'] = 8,
['PROPPATCH'] = 9,
['MKCOL'] = 10,
['COPY'] = 11,
['MOVE'] = 12,
['LOCK'] = 13,
['UNLOCK'] = 14,
['ACL'] = 15,
['REPORT'] = 16,
['VERSION-CONTROL'] = 17,
['CHECKIN'] = 18,
['CHECKOUT'] = 19,
['UNCHECKOUT'] = 20,
['SEARCH'] = 21,
['MKWORKSPACE'] = 22,
['UPDATE'] = 23,
['LABEL'] = 24,
['MERGE'] = 25,
['BASELINE_CONTROL'] = 26,
['MKACTIVITY'] = 27,
},
-- Request codes
Code = {
FORWARD_REQUEST = 2,
SEND_BODY = 3,
SEND_HEADERS = 4,
END_RESPONSE = 5,
SHUTDOWN = 7,
PING = 8,
CPING = 10,
},
-- Request attributes
Attribute = {
CONTEXT = 0x01,
SERVLET_PATH = 0x02,
REMOTE_USER = 0x03,
AUTH_TYPE = 0x04,
QUERY_STRING = 0x05,
JVM_ROUTE = 0x06,
SSL_CERT = 0x07,
SSL_CIPHER = 0x08,
SSL_SESSION = 0x09,
REQ_ATTRIBUTE= 0x0A,
SSL_KEY_SIZE = 0x0B,
ARE_DONE = 0xFF,
},
ForwardRequest = {
-- Common headers encoded as numeric values
Header = {
['accept'] = 0xA001,
['accept-charset'] = 0xA002,
['accept-encoding'] = 0xA003,
['accept-language'] = 0xA004,
['authorization'] = 0xA005,
['connection'] = 0xA006,
['content-type'] = 0xA007,
['content-length'] = 0xA008,
['cookie'] = 0xA009,
['cookie2'] = 0xA00A,
['host'] = 0xA00B,
['pragma'] = 0xA00C,
['referer'] = 0xA00D,
['user-agent'] = 0xA00E,
},
new = function(self, host, port, method, uri, headers, attributes, options)
local o = {
host = host,
magic = 0x1234,
length = 0,
code = AJP.Code.FORWARD_REQUEST,
method = AJP.Method[method],
version = "HTTP/1.1",
uri = uri,
raddr = options.raddr or "127.0.0.1",
rhost = options.rhost or "",
srv = host.ip,
port = port.number,
is_ssl = (port.service == "https"),
headers = headers or {},
attributes = attributes or {},
}
setmetatable(o, self)
self.__index = self
return o
end,
__tostring = function(self)
-- encodes a string, prefixing it with a 2-byte length
-- and suffixing it with a zero. P-encoding can't be used
-- as the zero terminator should not be counted in the length
local function encstr(str)
if ( not(str) or #str == 0 ) then
return bin.pack(">S", 0xFFFF)
end
return bin.pack(">Sz", #str, str)
end
-- count the number of headers
local function headerCount()
local i = 0
for _, _ in pairs(self.headers) do i = i + 1 end
return i
end
-- add host header if it's missing
if ( not(self.headers['host']) ) then
self.headers['host'] = self.host.ip
end
-- add keep-alive connection header if missing
if ( not(self.headers['connection']) ) then
self.headers['connection'] = "keep-alive"
end
local p_url = url.parse(self.uri)
-- save the magic and data for last
local data = bin.pack(">CCAAAAASCS", self.code, self.method,
encstr(self.version), encstr(p_url.path), encstr(self.raddr),
encstr(self.rhost), encstr(self.srv),
self.port, (self.is_ssl and 1 or 0),
headerCount())
-- encode headers
for k, v in pairs(self.headers) do
local header = AJP.ForwardRequest.Header[k:lower()] or k
if ( "string" == type(header) ) then
data = data .. bin.pack(">Sz", #header, header)
else
data = data .. bin.pack(">S", header)
end
data = data .. encstr(v)
end
-- encode attributes
if ( p_url.query ) then
data = data .. bin.pack("C", AJP.Attribute.QUERY_STRING)
data = data .. encstr(p_url.query)
end
-- terminate the attribute list
data = data .. bin.pack("C", AJP.Attribute.ARE_DONE)
-- returns the AJP request as a string
return bin.pack(">SSA", AJP.Magic, #data, data)
end,
},
Response = {
Header = {
['Content-Type'] = 0xA001,
['Content-Language'] = 0xA002,
['Content-Length'] = 0xA003,
['Date'] = 0xA004,
['Last-Modified'] = 0xA005,
['Location'] = 0xA006,
['Set-Cookie'] = 0xA007,
['Set-Cookie2'] = 0xA008,
['Servlet-Engine'] = 0xA009,
['Status'] = 0xA00A,
['WWW-Authenticate'] = 0xA00B,
},
SendHeaders = {
new = function(self)
local o = { headers = {}, rawheaders = {} }
setmetatable(o, self)
self.__index = self
return o
end,
parse = function(data)
local sh = AJP.Response.SendHeaders:new()
local pos = 6
local status_msg, hdr_count
pos, sh.status = bin.unpack(">S", data, pos)
pos, status_msg = bin.unpack(">P", data, pos)
pos = pos + 1
sh['status-line'] = ("AJP1.3 %d %s"):format(sh.status, status_msg)
pos, hdr_count = bin.unpack(">S", data, pos)
local function headerById(id)
for k, v in pairs(AJP.Response.Header) do
if ( v == id ) then return k end
end
end
for i=1, hdr_count do
local key, val, len
pos, len = bin.unpack(">S", data, pos)
if ( len < 0xA000 ) then
pos, key = bin.unpack("A"..len, data, pos)
pos = pos + 1
else
key = headerById(len)
end
pos, val = bin.unpack(">P", data, pos)
pos = pos + 1
sh.headers[key:lower()] = val
-- to keep the order, in which the headers were received,
-- add them to the rawheader table as well. This is based
-- on the same principle as the http library, however the
-- difference being that we have to "construct" the "raw"
-- format of the header, as we're receiving kvp's.
table.insert(sh.rawheaders, ("%s: %s"):format(key,val))
end
return sh
end,
},
},
}
-- The Comm class handles sending and receiving AJP requests/responses
Comm = {
-- Creates a new Comm instance
new = function(self, host, port, options)
local o = { host = host, port = port, options = options or {}}
setmetatable(o, self)
self.__index = self
return o
end,
-- Connects to the AJP server
--
-- @return status true on success, false on failure
-- @return err string containing error message on failure
connect = function(self)
self.socket = nmap.new_socket()
self.socket:set_timeout(self.options.timeout or 5000)
return self.socket:connect(self.host, self.port)
end,
-- Sends a request to the server
--
-- @param req instance of object that can be serialized with tostring
-- @return status true on succes, false on failure
-- @return err string containing error message on failure
send = function(self, req)
return self.socket:send(tostring(req))
end,
-- Receives an AJP response from the server
--
-- @return status true on succes, false on failure
-- @return response table containing the following fields, or string
-- containing error message on failure
-- <code>status</code> - status of response (see HTTP status codes)
-- <code>status-line</code> - the complete status line (eg. 200 OK)
-- <code>body</code> - the response body as string
-- <code>headers</code> - table of response headers
--
receive = function(self)
local response = {}
while(true) do
local status, buf = self.socket:receive_buf(match.numbytes(4), true)
if ( not(status) ) then
return false, "Failed to receive response from server"
end
local pos, magic, length = bin.unpack(">A2S", buf)
if ( magic ~= "AB" ) then
return false, ("Invalid magic received from server (%s)"):format(magic)
end
local status, data = self.socket:receive_buf(match.numbytes(length), true)
if ( not(status) ) then
return false, "Failed to receive response from server"
end
local pos, code = bin.unpack("C", data)
if ( AJP.Code.SEND_HEADERS == code ) then
local sh = AJP.Response.SendHeaders.parse(buf .. data)
response = sh
elseif( AJP.Code.SEND_BODY == code ) then
response.body = select(2, bin.unpack(">P", data, pos))
elseif( AJP.Code.END_RESPONSE == code ) then
break
end
end
return true, response
end,
-- Closes the socket
close = function(self)
return self.socket:close()
end,
}
Helper = {
--- Creates a new AJP Helper instance
--
-- @param host table
-- @param port table
-- @param opt
-- @return o new Helper instance
new = function(self, host, port, opt)
local o = { host = host, port = port, opt = opt }
setmetatable(o, self)
self.__index = self
return o
end,
--- Connects to the AJP server
--
-- @return status true on success, false on failure
-- @return err string containing error message on failure
connect = function(self)
self.comm = Comm:new(self.host, self.port, self.opt)
return self.comm:connect()
end,
--- Sends an AJP request to the server
--
-- @param url string containing the URL to query
-- @param headers table containing optional headers
-- @param attributes table containing optional attributes
-- @return status true on succes, false on failure
-- @return response table (@see Comm.receive), or string containing error
-- message on failure
request = function(self, method, url, headers, attributes)
local status, lhost, lport, rhost, rport = self.comm.socket:get_info()
local options = {}
if ( status ) then
options.raddr = rhost
end
local request = AJP.ForwardRequest:new(self.host, self.port, method, url, headers, attributes, options)
if ( not(self.comm:send(request)) ) then
return false, "Failed to send request to server"
end
return self.comm:receive()
end,
--- Sends an AJP GET request to the server
--
-- @param url string containing the URL to query
-- @param headers table containing optional headers
-- @param attributes table containing optional attributes
-- @return status true on succes, false on failure
-- @return response table (@see Comm.receive), or string containing error
-- message on failure
get = function(self, url, headers, attributes)
return self:request("GET", url, headers, attributes)
end,
--- Sends an AJP HEAD request to the server
--
-- @param url string containing the URL to query
-- @param headers table containing optional headers
-- @param attributes table containing optional attributes
-- @return status true on succes, false on failure
-- @return response table (@see Comm.receive), or string containing error
-- message on failure
head = function(self, url, headers, attributes)
return self:request("HEAD", url, headers, attributes)
end,
--- Sends an AJP OPTIONS request to the server
--
-- @param url string containing the URL to query
-- @param headers table containing optional headers
-- @param attributes table containing optional attributes
-- @return status true on succes, false on failure
-- @return response table (@see Comm.receive), or string containing error
-- message on failure
options = function(self, url, headers, attributes)
return self:request("OPTIONS", url, headers, attributes)
end,
-- should only work against 127.0.0.1
shutdownContainer = function(self)
self.comm:send(bin.pack("H", "1234000107"))
self.comm:receive()
end,
--- Disconnects from the server
close = function(self)
return self.comm:close()
end,
}

72
scripts/ajp-auth.nse Normal file
View File

@@ -0,0 +1,72 @@
description = [[
Retrieves the authentication scheme and realm of an AJP service that requires
authentication.
]]
---
-- @usage
-- nmap -p 8009 <ip> --script ajp-auth [--script-args ajp-auth.path=/login]
--
-- @output
-- PORT STATE SERVICE
-- 8009/tcp open ajp13
-- | ajp-auth:
-- |_ Digest opaque=GPui3SvCGBoHrRMMzSsgaYBV qop=auth nonce=1336063830612:935b5b389696b0f67b9193e19f47e037 realm=example.org
--
-- @args ajp-auth.path Define the request path
--
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "auth", "safe"}
local ajp = require('ajp')
local http = require('http')
local shortport = require('shortport')
portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
local arg_path = stdnse.get_script_args(SCRIPT_NAME .. ".path")
local function fail(err) return ("\n ERROR: %s"):format(err or "") end
action = function(host, port)
local helper = ajp.Helper:new(host, port)
if ( not(helper:connect()) ) then
return fail("Failed to connect to AJP server")
end
local status, answer = helper:get(arg_path or "/")
--- check for 401 response code
if ( not(status) or answer.status ~= 401 ) then
return
end
local result = { name = answer["status-line"]:match("^(.*)\r?\n$") }
local www_authenticate = answer.headers["www-authenticate"]
if not www_authenticate then
table.insert( result, ("Server returned status %d but no WWW-Authenticate header."):format(answer.status) )
return stdnse.format_output(true, result)
end
local challenges = http.parse_www_authenticate(www_authenticate)
if ( not(challenges) ) then
table.insert( result, ("Server returned status %d but the WWW-Authenticate header could not be parsed."):format(answer.status) )
table.insert( result, ("WWW-Authenticate: %s"):format(www_authenticate) )
return stdnse.format_output(true, result)
end
for _, challenge in ipairs(challenges) do
local line = challenge.scheme
if ( challenge.params ) then
for name, value in pairs(challenge.params) do
line = line .. (" %s=%s"):format(name, value)
end
end
table.insert(result, line)
end
return stdnse.format_output(true, result)
end

46
scripts/ajp-headers.nse Normal file
View File

@@ -0,0 +1,46 @@
description = [[
Performs a HEAD or GET request against either the root directory or any
optional directory and returns the server response headers.
]]
---
-- @usage
-- nmap -p 8009 <ip> --script ajp-headers
--
-- @output
-- PORT STATE SERVICE
-- 8009/tcp open ajp13
-- | ajp-headers:
-- | X-Powered-By: JSP/2.2
-- | Set-Cookie: JSESSIONID=goTHax+8ktEcZsBldANHBAuf.undefined; Path=/helloworld
-- | Content-Type: text/html;charset=ISO-8859-1
-- |_ Content-Length: 149
--
-- @args ajp-headers.path The path to request, such as <code>/index.php</code>. Default <code>/</code>.
local ajp = require('ajp')
local shortport = require('shortport')
portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
local arg_path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/"
local function fail(err) return ("\n ERROR: %s"):format(err or "") end
action = function(host, port)
local method
local helper = ajp.Helper:new(host, port)
helper:connect()
local status, response = helper:get(arg_path)
helper:close()
if ( not(status) ) then
return fail("Failed to retrieve server headers")
end
return stdnse.format_output(true, response.rawheaders)
end

80
scripts/ajp-methods.nse Normal file
View File

@@ -0,0 +1,80 @@
description = [[
Finds out what options are supported by the AJP server by sending an OPTIONS
request and lists potentially risky methods.
In this script, "potentially risky" methods are anything except GET,
HEAD, POST, and OPTIONS. If the script reports potentially risky
methods, they may not all be security risks, but you should check to
make sure. This page lists the dangers of some common methods:
http://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
]]
---
-- @usage
-- nmap -p 8009 <ip> --script ajp-methods
--
-- @output
-- PORT STATE SERVICE
-- 8009/tcp open ajp13
-- | ajp-methods:
-- | Supported methods: GET HEAD POST PUT DELETE TRACE OPTIONS
-- | Potentially risky methods: PUT DELETE TRACE
-- |_ See http://nmap.org/nsedoc/scripts/ajp-methods.html
--
-- @args ajp-methods.path the path to check or <code>/<code> if none was given
--
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "safe"}
local ajp = require('ajp')
local shortport = require('shortport')
portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
local arg_url = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
local UNINTERESTING_METHODS = { "GET", "HEAD", "POST", "OPTIONS" }
local function contains(t, elem)
for _, e in ipairs(t) do if e == elem then return true end end
return false
end
local function filter_out(t, filter)
local result = {}
for _, e in ipairs(t) do
if ( not(contains(filter, e)) ) then
result[#result + 1] = e
end
end
return result
end
action = function(host, port)
local helper = ajp.Helper:new(host, port)
if ( not(helper:connect()) ) then
return fail("Failed to connect to server")
end
local status, response = helper:options(arg_url)
helper:close()
if ( not(status) or response.status ~= 200 or
not(response.headers) or not(response.headers['allow']) ) then
return "Failed to get a valid response for the OPTION request"
end
local methods = stdnse.strsplit(",%s", response.headers['allow'])
local output = {}
table.insert(output, ("Supported methods: %s"):format(stdnse.strjoin(" ", methods)))
local interesting = filter_out(methods, UNINTERESTING_METHODS)
if ( #interesting > 0 ) then
table.insert(output, "Potentially risky methods: " .. stdnse.strjoin(" ", interesting))
table.insert(output, "See http://nmap.org/nsedoc/scripts/ajp-methods.html")
end
return stdnse.format_output(true, output)
end

View File

@@ -5,6 +5,9 @@ Entry { filename = "afp-ls.nse", categories = { "discovery", "safe", } }
Entry { filename = "afp-path-vuln.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "afp-path-vuln.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "afp-serverinfo.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "afp-serverinfo.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } }
Entry { filename = "ajp-auth.nse", categories = { "auth", "default", "safe", } }
Entry { filename = "ajp-headers.nse", categories = { "discovery", "safe", } }
Entry { filename = "ajp-methods.nse", categories = { "default", "safe", } }
Entry { filename = "amqp-info.nse", categories = { "default", "discovery", "safe", "version", } } Entry { filename = "amqp-info.nse", categories = { "default", "discovery", "safe", "version", } }
Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } }
Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } }