diff --git a/CHANGELOG b/CHANGELOG index 4f59e6f15..e388dcb5b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script ajp-request, which adds support for creating custom + Apache JServer Protocol requests. [Patrik Karlsson] + +o [NSE] Added the script ajp-brute, which enables password brute force auditing + against the Apache JServ Protocol service. [Patrik Karlsson] + o [NSE] Added the script broadcast-tellstick-discover, which discovers Telldus Technologies TellStickNet devices on the LAN. [Patrik Karlsson] diff --git a/scripts/ajp-brute.nse b/scripts/ajp-brute.nse new file mode 100644 index 000000000..c186f6287 --- /dev/null +++ b/scripts/ajp-brute.nse @@ -0,0 +1,109 @@ +description = [[ +Performs brute force passwords auditing against the Apache JServ protocol. +The Apache JServ Protocol is commonly used by web servers to communicate with +back-end Java application server containers. +]] + +--- +-- @usage +-- nmap -p 8009 --script ajp-brute +-- +-- @output +-- PORT STATE SERVICE +-- 8009/tcp open ajp13 +-- | ajp-brute: +-- | Accounts +-- | root:secret - Valid credentials +-- | Statistics +-- |_ Performed 1946 guesses in 23 seconds, average tps: 82 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "brute"} + +local shortport = require('shortport') +local ajp = require('ajp') +local base64 = require('base64') +local brute = require('brute') +local http = require('http') + +portrule = shortport.port_or_service(8009, 'ajp13', 'tcp') + +local arg_url = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" + +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + +Driver = { + + new = function(self, host, port, options) + local o = { host = host, + port = port, + options = options, + helper = ajp.Helper:new(host, port) + } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + return self.helper:connect() + end, + + disconnect = function(self) + return self.helper:close() + end, + + login = function(self, user, pass) + local headers = { + ["Authorization"] = ("Basic %s"):format(base64.enc(user .. ":" .. pass)) + } + local status, response = self.helper:get(arg_url, headers) + + if ( not(status) ) then + local err = brute.Error:new( data ) + err:setRetry( true ) + return false, err + elseif( response.status ~= 401 ) then + return true, brute.Account:new(user, pass, creds.State.VALID) + end + return false, brute.Error:new( "Incorrect password" ) + 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:get(arg_url) + if ( not(response.headers['www-authenticate']) ) then + return "\n URL does not require authentication" + end + + local challenges = http.parse_www_authenticate(response.headers['www-authenticate']) + local options = { scheme = nil } + for _, challenge in ipairs(challenges or {}) do + if ( challenge and challenge.scheme and challenge.scheme:lower() == "basic") then + options.scheme = challenge.scheme:lower() + break + end + end + + if ( not(options.scheme) ) then + return fail("Could not find a supported authentication scheme") + end + + local engine = brute.Engine:new(Driver, host, port ) + engine.options.script_name = SCRIPT_NAME + + local status, result = engine:start() + if ( status ) then + return result + end +end \ No newline at end of file diff --git a/scripts/ajp-request.nse b/scripts/ajp-request.nse new file mode 100644 index 000000000..1ab1cfd46 --- /dev/null +++ b/scripts/ajp-request.nse @@ -0,0 +1,100 @@ +description = [[ +Request an URI over the Apache JServe Protocol and displays or alternatively +stores the result in a file. Different AJP methods such as; GET, HEAD, TRACE, +PUT or DELETE may be used. + +The Apache JServ Protocol is commonly used by web servers to communicate with +back-end Java application server containers. +]] + +--- +-- @usage +-- nmap -p 8009 --script ajp-request +-- +-- @output +-- PORT STATE SERVICE +-- 8009/tcp open ajp13 +-- | ajp-request: +-- | +-- | +-- | +-- | JSP Test +-- | +-- | +-- | +-- |

Hello, World.

+-- | Fri May 04 02:09:40 UTC 2012 +-- | +-- |_ +-- +-- @args method AJP method to be used when requesting the URI (default: GET) +-- @args path the path part of the URI to request +-- @args filename the name of the file where the results should be stored +-- @args username the username to use to access protected resources +-- @args password the password to use to access protected resources +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +local shortport = require('shortport') +local packet = require('packet') +local ajp = require('ajp') + +portrule = shortport.port_or_service(8009, 'ajp13', 'tcp') + +local arg_method = stdnse.get_script_args(SCRIPT_NAME .. ".method") or "GET" +local arg_path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" +local arg_file = stdnse.get_script_args(SCRIPT_NAME .. ".filename") +local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username") +local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") + +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 valid_methods = { + ["GET"] = true, + ["HEAD"] = true, + ["TRACE"] = true, + ["PUT"] = true, + ["DELETE"] = true, + ["OPTIONS"]= true, + } + + local method = arg_method:upper() + if ( not(valid_methods[method]) ) then + return fail(("Method not supported: %s"):format(arg_method)) + end + + local options = { auth = { username = arg_username, password = arg_password } } + local status, response = helper:request(arg_method, arg_path, nil, nil, options) + if ( not(status) ) then + return fail("Failed to retrieve response for request") + end + helper:close() + + if ( response ) then + local output = response['status-line'] .. "\n" .. + stdnse.strjoin("\n", response.rawheaders) .. + (response.body and "\n\n" .. response.body or "") + if ( arg_file ) then + local f = io.open(arg_file, "w") + if ( not(f) ) then + return fail(("Failed to open file %s for writing"):format(arg_file)) + end + f:write(output) + f:close() + return ("Response was written to file: %s"):format(arg_file) + else + return "\n" .. output + end + end +end + diff --git a/scripts/script.db b/scripts/script.db index 92401ea1a..6e616c4d3 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -6,8 +6,10 @@ Entry { filename = "afp-path-vuln.nse", categories = { "exploit", "intrusive", " Entry { filename = "afp-serverinfo.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "ajp-auth.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "ajp-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "ajp-headers.nse", categories = { "discovery", "safe", } } Entry { filename = "ajp-methods.nse", categories = { "default", "safe", } } +Entry { filename = "ajp-request.nse", categories = { "discovery", "safe", } } Entry { filename = "amqp-info.nse", categories = { "default", "discovery", "safe", "version", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } }