summaryrefslogtreecommitdiffstats
path: root/nselib/ajp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/ajp.lua')
-rw-r--r--nselib/ajp.lua546
1 files changed, 546 insertions, 0 deletions
diff --git a/nselib/ajp.lua b/nselib/ajp.lua
new file mode 100644
index 0000000..b6296d0
--- /dev/null
+++ b/nselib/ajp.lua
@@ -0,0 +1,546 @@
+local base64 = require "base64"
+local http = require "http"
+local match = require "match"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+_ENV = stdnse.module("ajp", stdnse.seeall)
+
+---
+-- 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>
+--
+
+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.
+ local function encstr(str)
+ if ( not(str) or #str == 0 ) then
+ return "\xFF\xFF"
+ end
+ return string.pack(">s2x", 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'] = stdnse.get_hostname(self.host)
+ 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 = {
+ string.pack(">BB", self.code, self.method),
+ encstr(self.version), encstr(p_url.path), encstr(self.raddr),
+ encstr(self.rhost), encstr(self.srv),
+ string.pack(">I2BI2", 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+1] = string.pack(">s2x", header)
+ else
+ data[#data+1] = string.pack(">I2", header)
+ end
+
+ data[#data+1] = encstr(v)
+ end
+
+ -- encode attributes
+ if ( p_url.query ) then
+ data[#data+1] = string.pack("B", AJP.Attribute.QUERY_STRING)
+ data[#data+1] = encstr(p_url.query)
+ end
+
+ -- terminate the attribute list
+ data[#data+1] = string.pack("B", AJP.Attribute.ARE_DONE)
+
+ -- returns the AJP request as a string
+ data = table.concat(data)
+ return string.pack(">I2s2", AJP.Magic, 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
+
+ sh.status, status_msg, pos = string.unpack(">I2s2", data, pos)
+ pos = pos + 1
+ sh.status_line = ("AJP/1.3 %d %s"):format(sh.status, status_msg)
+
+ hdr_count, pos = string.unpack(">I2", 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
+ len, pos = string.unpack(">I2", data, pos)
+
+ if ( len < 0xA000 ) then
+ key, pos = string.unpack("c"..len, data, pos)
+ pos = pos + 1
+ else
+ key = headerById(len)
+ end
+
+ val, pos = string.unpack(">s2", 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
+ -- @name Comm.new
+ -- @param host host table
+ -- @param port port table
+ -- @param options Table of options. Fields:
+ -- * timeout - Timeout in milliseconds. Default: 5000
+ 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
+ -- @name Comm.connect
+ -- @return status true on success, false on failure
+ -- @return err string containing error message on failure
+ connect = function(self, socket)
+ self.socket = socket or 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
+ -- @name Comm.send
+ -- @param req instance of object that can be serialized with tostring
+ -- @return status true on success, false on failure
+ -- @return err string containing error message on failure
+ send = function(self, req)
+ return self.socket:send(tostring(req))
+ end,
+
+ --- AJP response table
+ -- @class table
+ -- @name ajp.response
+ -- @field status status of response (see HTTP status codes)
+ -- @field status_line the complete status line (eg. 200 OK)
+ -- @field body the response body as string
+ -- @field headers table of response headers
+
+ --; Receives an AJP response from the server
+ -- @name Comm.receive
+ -- @return status true on success, false on failure
+ -- @return AJP response table, or error message on failure
+ -- @see ajp.response
+ 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 magic, length, pos = string.unpack(">c2I2", 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 code, pos = string.unpack("B", 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 = string.unpack(">s2", data, pos)
+ elseif( AJP.Code.END_RESPONSE == code ) then
+ break
+ end
+ end
+ return true, response
+ end,
+
+ --; Closes the socket
+ -- @name Comm.close
+ close = function(self)
+ return self.socket:close()
+ end,
+
+}
+
+--- AJP Request options
+-- @name ajp.options
+-- @class table
+-- @field auth table with <code>username</code> and <code>password</code> fields
+-- @field timeout Socket timeout in milliseconds. Default: 5000
+Helper = {
+
+ --- Creates a new AJP Helper instance
+ -- @name Helper.new
+ -- @param host host table
+ -- @param port port table
+ -- @param opt request and comm options
+ -- @see ajp.options
+ -- @return o new Helper instance
+ new = function(self, host, port, opt)
+ local o = { host = host, port = port, opt = opt or {} }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- Connects to the AJP server
+ -- @name Helper.connect
+ -- @return status true on success, false on failure
+ -- @return err string containing error message on failure
+ connect = function(self, socket)
+ self.comm = Comm:new(self.host, self.port, self.opt)
+ return self.comm:connect(socket)
+ end,
+
+ getOption = function(self, options, key)
+
+ -- first check options, then global self.opt
+ if ( options and options[key] ) then
+ return options[key]
+ elseif ( self.opt and self.opt[key] ) then
+ return self.opt[key]
+ end
+
+ end,
+
+ --- Sends an AJP request to the server
+ -- @name Helper.request
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ request = function(self, method, url, headers, attributes, options)
+ local status, lhost, lport, rhost, rport = self.comm.socket:get_info()
+ if ( not(status) ) then
+ return false, "Failed to get socket information"
+ end
+
+ local request = AJP.ForwardRequest:new(self.host, self.port, method, url, headers, attributes, { raddr = rhost })
+ if ( not(self.comm:send(request)) ) then
+ return false, "Failed to send request to server"
+ end
+ local status, result = self.comm:receive()
+
+ -- support Basic authentication
+ if ( status and result.status == 401 and result.headers['www-authenticate'] ) then
+
+ local auth = self:getOption(options, "auth")
+ if not(auth and auth.username and auth.password) then
+ stdnse.debug2("No authentication information")
+ return status, result
+ end
+
+ local challenges = http.parse_www_authenticate(result.headers['www-authenticate'])
+ local scheme
+ for _, challenge in ipairs(challenges or {}) do
+ if ( challenge and challenge.scheme and challenge.scheme:lower() == "basic") then
+ scheme = challenge.scheme:lower()
+ break
+ end
+ end
+
+ if ( not(scheme) ) then
+ stdnse.debug2("Could not find a supported authentication scheme")
+ elseif ( "basic" ~= scheme ) then
+ stdnse.debug2("Unsupported authentication scheme: %s", scheme)
+ else
+ headers = headers or {}
+ headers["Authorization"] = ("Basic %s"):format(base64.enc(auth.username .. ":" .. auth.password))
+ request = AJP.ForwardRequest:new(self.host, self.port, method, url, headers, attributes, { raddr = rhost })
+ if ( not(self.comm:send(request)) ) then
+ return false, "Failed to send request to server"
+ end
+ status, result = self.comm:receive()
+ end
+
+ end
+ return status, result
+ end,
+
+ --- Sends an AJP GET request to the server
+ -- @name Helper.get
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ get = function(self, url, headers, attributes, options)
+ return self:request("GET", url, headers, attributes, options)
+ end,
+
+ --- Sends an AJP HEAD request to the server
+ -- @name Helper.head
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ head = function(self, url, headers, attributes, options)
+ return self:request("HEAD", url, headers, attributes, options)
+ end,
+
+ --- Sends an AJP TRACE request to the server
+ -- @name Helper.trace
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ trace = function(self, url, headers, attributes, options)
+ return self:request("TRACE", url, headers, attributes, options)
+ end,
+
+ --- Sends an AJP PUT request to the server
+ -- @name Helper.put
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ put = function(self, url, headers, attributes, options)
+ return self:request("PUT", url, headers, attributes, options)
+ end,
+
+ --- Sends an AJP DELETE request to the server
+ -- @name Helper.delete
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ delete = function(self, url, headers, attributes, options)
+ return self:request("DELETE", url, headers, attributes, options)
+ end,
+
+ --- Sends an AJP OPTIONS request to the server
+ -- @name Helper.options
+ -- @param url string containing the URL to query
+ -- @param headers table containing optional headers
+ -- @param attributes table containing optional attributes
+ -- @param options table with request specific options
+ -- @see ajp.options
+ -- @return status true on success, false on failure
+ -- @return response table, or error message on failure
+ -- @see ajp.response
+ options = function(self, url, headers, attributes, options)
+ return self:request("OPTIONS", url, headers, attributes, options)
+ end,
+
+ -- should only work against 127.0.0.1
+ shutdownContainer = function(self)
+ self.comm:send("\x12\x34\x00\x01\x07")
+ self.comm:receive()
+ end,
+
+ --- Disconnects from the server
+ -- @name Helper.close
+ close = function(self)
+ return self.comm:close()
+ end,
+
+}
+
+return _ENV;