1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-15 20:29:03 +00:00

Make the http NSE module better handle different delimiters in the headers of

messages (and in the body when chunked encoding is used). The patch is from
jah.
This commit is contained in:
david
2008-10-03 01:00:55 +00:00
parent 168e38357e
commit ebaf939f5f
3 changed files with 104 additions and 52 deletions

View File

@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*- # Nmap Changelog ($Id$); -*-text-*-
o The http NSE module tries to deal with non-standards-compliant HTTP
traffic, particularly responses in which the header fields are
separated by plain LF rather than CRLF. [Jah]
o [Zenmap] The help function now properly converts the pathname of the o [Zenmap] The help function now properly converts the pathname of the
local help file to a URL, for better compatibility with different local help file to a URL, for better compatibility with different
web browsers. [David] web browsers. [David]

View File

@@ -1587,16 +1587,19 @@ if(s) code_to_be_done_on_match end
The <literal>http</literal> module provides functions for dealing with the client side of the http protocol. The <literal>http</literal> module provides functions for dealing with the client side of the http protocol.
The functions reside inside the <literal>http</literal> namespace. The functions reside inside the <literal>http</literal> namespace.
The return value of each function in this module is a table with the following keys: The return value of each function in this module is a table with the following keys:
<literal>status</literal>, <literal>header</literal> and <literal>body</literal>. <literal>status</literal>, <literal>status-line</literal>, <literal>header</literal>
and <literal>body</literal>.
<literal>status</literal> is a number representing the HTTP <literal>status</literal> is a number representing the HTTP
status code returned in response to the HTTP request. In case status code returned in response to the HTTP request. In case
of an unhandled error, <literal>status</literal> of an unhandled error, <literal>status</literal>
is <literal>nil</literal>. The <literal>header</literal> value is <literal>nil</literal>. <literal>status-line</literal> is
is a table containing key-value pairs of HTTP headers received the entire status message which includes the HTTP version,
in response to the request. The header names are in lower-case status code and reason phrase. The <literal>header</literal>
and are the keys to their corresponding header values value is a table containing key-value pairs of HTTP headers
(e.g. <literal>header.location = received in response to the request. The header names are in
lower-case and are the keys to their corresponding header
values (e.g. <literal>header.location =
"http://nmap.org/"</literal>). Multiple headers of the same "http://nmap.org/"</literal>). Multiple headers of the same
name are concatenated and separated by name are concatenated and separated by
commas. The <literal>body</literal> value is a string commas. The <literal>body</literal> value is a string

View File

@@ -1,12 +1,14 @@
--- The http module provides functions for dealing with the client side --- The http module provides functions for dealing with the client side
-- of the http protocol. The functions reside inside the http namespace. -- of the http protocol. The functions reside inside the http namespace.
-- The return value of each function in this module is a table with the -- The return value of each function in this module is a table with the
-- following keys: status, header and body. status is a number representing -- following keys: status, status-line, header and body. status is a number
-- the HTTP status code returned in response to the HTTP request. In case -- representing the HTTP status code returned in response to the HTTP
-- of an unhandled error, status is nil. The header value is a table -- request. In case of an unhandled error, status is nil. status-line is
-- containing key-value pairs of HTTP headers received in response to the -- the entire status message which includes the HTTP version, status code
-- request. The header names are in lower-case and are the keys to their -- and reason phrase. The header value is a table containing key-value
-- corresponding header values (e.g. header.location = "http://nmap.org/"). -- pairs of HTTP headers received in response to the request. The header
-- names are in lower-case and are the keys to their corresponding header
-- values (e.g. header.location = "http://nmap.org/").
-- Multiple headers of the same name are concatenated and separated by -- Multiple headers of the same name are concatenated and separated by
-- commas. The body value is a string containing the body of the HTTP -- commas. The body value is a string containing the body of the HTTP
-- response. -- response.
@@ -14,8 +16,8 @@
module(... or "http",package.seeall) module(... or "http",package.seeall)
require 'stdnse' local url = require 'url'
require 'url' local stdnse = require 'stdnse'
-- --
-- http.get( host, port, path, options ) -- http.get( host, port, path, options )
@@ -26,7 +28,7 @@ require 'url'
-- port may either be a number or a table -- port may either be a number or a table
-- --
-- the format of the return value is a table with the following structure: -- the format of the return value is a table with the following structure:
-- {status = 200, header = {}, body ="<html>...</html>"} -- {status = 200, status-line = "HTTP/1.1 200 OK", header = {}, body ="<html>...</html>"}
-- the header table has an entry for each received header with the header name being the key -- the header table has an entry for each received header with the header name being the key
-- the table also has an entry named "status" which contains the http status code of the request -- the table also has an entry named "status" which contains the http status code of the request
-- in case of an error status is nil -- in case of an error status is nil
@@ -43,7 +45,7 @@ require 'url'
-- @param host The host to query. -- @param host The host to query.
-- @param port The port for the host. -- @param port The port for the host.
-- @param path The path of the resource. -- @param path The path of the resource.
-- @param options A table of optoins. See function description. -- @param options A table of options. See function description.
-- @return table -- @return table
get = function( host, port, path, options ) get = function( host, port, path, options )
options = options or {} options = options or {}
@@ -113,7 +115,7 @@ request = function( host, port, data, options )
options = options or {} options = options or {}
if type(host) == 'table' then if type(host) == 'table' then
host = host.ip host = host.targetname or host.ip
end end
local protocol = 'tcp' local protocol = 'tcp'
@@ -124,7 +126,7 @@ request = function( host, port, data, options )
port = port.number port = port.number
end end
local result = {status=nil,header={},body=""} local result = {status=nil,["status-line"]=nil,header={},body=""}
local socket = nmap.new_socket() local socket = nmap.new_socket()
local default_timeout = {} local default_timeout = {}
if options.timeout then if options.timeout then
@@ -133,36 +135,76 @@ request = function( host, port, data, options )
default_timeout = get_default_timeout( nmap.timing_level() ) default_timeout = get_default_timeout( nmap.timing_level() )
socket:set_timeout( default_timeout.connect ) socket:set_timeout( default_timeout.connect )
end end
if not socket:connect( host, port, protocol ) then if not socket:connect( host, port, protocol ) then
return result return result
end end
if not options.timeout then if not options.timeout then
socket:set_timeout( default_timeout.request ) socket:set_timeout( default_timeout.request )
end end
if not socket:send( data ) then if not socket:send( data ) then
return result return result
end end
local buffer = stdnse.make_buffer( socket, "\r\n" ) -- no buffer - we want everything now!
local response = {}
while true do
local status, part = socket:receive()
if not status then
break
else
response[#response+1] = part
end
end
socket:close()
response = table.concat( response )
-- try and separate the head from the body
local header, body, h1, h2, b1, b2
if response:match( "\r\n\r\n" ) and response:match( "\n\n" ) then
h1, b1 = response:match( "^(.-)\r\n\r\n(.*)$" )
h2, b2 = response:match( "^(.-)\n\n(.*)$" )
if h1 and h2 and h1:len() <= h2:len() then
header, body = h1, b1
else
header, body = h2, b2
end
elseif response:match( "\r\n\r\n" ) then
header, body = response:match( "^(.-)\r\n\r\n(.*)$" )
elseif response:match( "\n\r\n" ) then
header, body = response:match( "^(.-)\n\r\n(.*)$" )
elseif response:match( "\n\n" ) then
header, body = response:match( "^(.-)\n\n(.*)$" )
else
body = response
end
local head_delim, body_delim
if type( header ) == "string" then
head_delim = ( header:match( "\r\n" ) and "\r\n" ) or
( header:match( "\n" ) and "\n" ) or nil
header = ( head_delim and stdnse.strsplit( head_delim, header ) ) or { header }
end
if type( body ) == "string" then
body_delim = ( body:match( "\r\n" ) and "\r\n" ) or
( body:match( "\n" ) and "\n" ) or nil
end
local line, _ local line, _
local header, body = {}, {}
-- header loop
while true do
line = buffer()
if (not line or line == "") then break end
table.insert(header,line)
end
-- build nicer table for header -- build nicer table for header
local last_header, match local last_header, match
for number, line in ipairs( header ) do for number, line in ipairs( header or {} ) do
if number == 1 then if number == 1 then
local code local code
_, _, code = string.find( line, "HTTP/%d\.%d (%d+)") _, _, code = string.find( line, "HTTP/%d\.%d (%d+)")
result.status = tonumber(code) result.status = tonumber(code)
if not result.status then table.insert(body,line) end if code then result["status-line"] = line end
else else
match, _, key, value = string.find( line, "(.+): (.*)" ) match, _, key, value = string.find( line, "(.+): (.*)" )
if match and key and value then if match and key and value then
@@ -177,39 +219,42 @@ request = function( host, port, data, options )
match, _, value = string.find( line, " +(.*)" ) match, _, value = string.find( line, " +(.*)" )
if match and value and last_header then if match and value and last_header then
result.header[last_header] = result.header[last_header] .. ',' .. value result.header[last_header] = result.header[last_header] .. ',' .. value
elseif match and value then
table.insert(body,line)
end end
end end
end end
end end
-- handle body -- handle chunked encoding
if result.header['transfer-encoding'] == 'chunked' then if type( result.header ) == "table" and result.header['transfer-encoding'] == 'chunked' and type( body_delim ) == "string" then
-- if the server used chunked encoding we have to 'dechunk' the answer body = body_delim .. body
local counter, chunk_size local b = {}
counter = 0; chunk_size = 0 local start, ptr = 1, 1
while true do local chunk_len
if counter >= chunk_size then local pattern = ("%s([^%s]+)%s"):format( body_delim, body_delim, body_delim )
counter = 0 while ( ptr < ( type( body ) == "string" and body:len() ) or 1 ) do
chunk_size = tonumber( buffer(), 16 ) local hex = body:match( pattern, ptr )
if chunk_size == 0 or not chunk_size then break end if not hex then break end
chunk_len = tonumber( hex or 0, 16 ) or nil
if chunk_len then
start = ptr + hex:len() + 2*body_delim:len()
ptr = start + chunk_len
b[#b+1] = body:sub( start, ptr-1 )
end end
line = buffer()
if not line then break end
counter = counter + #line + 2
table.insert(body,line)
end end
else body = table.concat( b )
while true do end
line = buffer()
if not line then break end -- special case for conjoined header and body
table.insert(body,line) if type( result.status ) ~= "number" and type( body ) == "string" then
local code, remainder = body:match( "HTTP/%d\.%d (%d+)(.*)") -- The Reason-Phrase will be prepended to the body :(
if code then
stdnse.print_debug( "Interesting variation on the HTTP standard. Please submit a --script-trace output for this host (%s) to nmap-dev[at]insecure.org.", host )
result.status = tonumber(code)
body = remainder or body
end end
end end
socket:close() result.body = body
result.body = table.concat( body, "\r\n" )
return result return result