Nmap Development mailing list archives

Re: [NSE] NSE HTTP library


From: Sven Klemm <sven () c3d2 de>
Date: Fri, 18 Jan 2008 22:54:55 +0100

Sven Klemm wrote:

Ah I found the bug. I forgot to declare the socket variable as local.
Now everything works fine. I've attached the fixed version.

In the last version the newline fix got lost so this version adds it
back in. I've also attached a patch for HTTPAuth.nse,
showHTMLTitle.nse and robots.nse to make use of the library.

Cheers,
Sven

-- 
Sven Klemm
http://cthulhu.c3d2.de/~sven/

-- See nmaps COPYING for licence
module(...,package.seeall)

require 'stdnse'
require 'url'

--
-- http.get( host, port, path )
-- http.request( host, port, request )
-- http.get_url( url )
--
-- host may either be a string or table
-- port may either be a number or a table
--
-- the format of the return value is a table with the following structure:
-- {status = 200, header = {}, body ="<html>...</html>"}
-- 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
-- in case of an error status is nil


-- fetch relative URL with get request
get = function( host, port, path )
  local hostname = host
  if type(host) == 'table' then
    hostname = ( host.name ~= '' and host.name ) or host.ip
  end

  local data
  data = "GET "..path.." HTTP/1.1\r\n"
  data = data .. "Host: "..hostname.."\r\n"
  data = data .. "User-Agent: Nmap NSE\r\n"
  data = data .. "Connection: close\r\n\r\n"

  return request( host, port, data )
end

-- fetch URL with get request
get_url = function( u )
  local parsed = url.parse( u )
  local port = {}

  port.service = parsed.scheme
  port.number = parsed.port

  if not port.number then
    if parsed.scheme == 'https' then
      port.number = 443
    else
      port.number = 80
    end
  end

  local path = parsed.path or "/"
  if parsed.query then
    path = path .. "?" .. parsed.query
  end

  return get( parsed.host, port, path )
end

-- send http request and return the result as table
-- host may be a table or the hostname
-- port may be a table or the portnumber
request = function( host, port, data )

  if type(host) == 'table' then
    host = ( host.name ~= '' and host.name ) or host.ip
  end

  local protocol = 'tcp'
  if type(port) == 'table' then
    if nmap.have_ssl() and ( port.service == 'https' or ( port.version and port.version.service_tunnel == 'ssl' ) ) then
      protocol = 'ssl'
    end
    port = port.number
  end

  local socket = nmap.new_socket()
  socket:connect( host, port, protocol )
  socket:send( data )

  local buffer = stdnse.make_buffer( socket, "\r?\n" )

  local status, line, result, _
  local header, body = {}, {}
  -- header loop
  while true do
    status, line = buffer()
    if (not status or line == "") then break end
    table.insert(header,line)
  end

  result = {status=nil,header={},body=""}

  -- build nicer table for header
  for key, value in pairs( header ) do
    if key == 1 then
      local code
      _, _, code = string.find( value, "HTTP/%d\.%d (%d+)")
      result.status = tonumber(code)
    else
      _, _, key, value = string.find( value, "(.+): (.*)" )
      if key and value then
        value = value:gsub( '[\r\n]+', '' )
        if result.header[key] then
          result.header[key] = result.header[key] .. ',' .. value
        else
          result.header[key] = value
        end
      end
    end
  end

  -- body loop
  while true do
    status, line = buffer()
    if (not status) then break end
    table.insert(body,line)
  end

  socket:close()
  result.body = table.concat( body, "\n" )

  return result

end

Index: scripts/HTTPAuth.nse
===================================================================
--- scripts/HTTPAuth.nse        (revision 6740)
+++ scripts/HTTPAuth.nse        (working copy)
@@ -14,74 +14,26 @@
 categories = {"intrusive"}
 
 require "shortport"
+require "http"
 
-portrule = shortport.port_or_service({80, 8080}, "http")
+portrule = shortport.port_or_service({80, 443, 8080}, {"http","https"})
 
 action = function(host, port)
-       local socket
-       local catch = function()
-               socket:close()
-       end
 
-       local try = nmap.new_try(catch)
-
-       local get_http_headers = function(dst, dst_port, query_string)
-               socket = nmap.new_socket()
-
-               try(socket:connect(dst, dst_port))
-               try(socket:send(query_string))
-
-               local response = ""
-               local lines
-               local status
-
-               while true do
-                       status, lines = socket:receive_lines(1)
-
-                       if not status then
-                               break
-                       end
-
-                       response = response .. lines
-               end
-
-               try(socket:close())
-
-               local tags = {"(.-)<![Dd][Oo][Cc][Tt][Yy][Pp][Ee]", "(.-)<[Hh][Tt][Mm][Ll]", "(.-)<[Hh][Ee][Aa][Dd]", 
"(.-)<[Bb][Oo][Dd][Yy]"}
-               local hdrs
-
-               for I = 1, #tags do
-                       hdrs = string.match(response, tags[I])
-                       if hdrs ~= nil and hdrs ~= response and hdrs ~= "" then
-                               return hdrs
-                       end
-               end
-
-               return response
-       end
-
-       local auth
-       local value
        local realm
        local scheme
        local result
        local basic = false
+       local query
 
-       local query = "GET / HTTP/1.1\r\n"
-       query = query .. "Accept: */*\r\n"
-       query = query .. "Accept-Language: en\r\n"
-       query = query .. "User-Agent: Nmap NSE\r\n"
-       query = query .. "Connection: close\r\n"
-       query = query .. "Host: " .. host.ip .. ":" .. port.number .. "\r\n\r\n"
+       local answer = http.get( host, port, "/" )
 
-       local headers = get_http_headers(host.ip, port.number, query)
-
        --- check for 401 response code
-       auth = string.match(headers, "HTTP/1.- 401")
-       if auth ~= nil then
+       if answer.status == 401 then
                result = "HTTP Service requires authentication\n"
                -- loop through any WWW-Authenticate: headers to determine valid authentication schemes
-               for value in string.gmatch(headers, "[Aa]uthenticate:(.-)\n") do
+               local value = answer.header['WWW-Authenticate']
+               if value then
                        result = result .. "  Auth type: "
                        scheme, realm = string.match(value, "(%a+).-[Rr]ealm=\"(.-)\"")
                        if scheme == "Basic" then
@@ -104,11 +56,8 @@
                query = query .. "Connection: close\r\n"
                query = query .. "Host: " .. host.ip .. ":" .. port.number .. "\r\n\r\n"
 
-               auth = ""
-               headers = get_http_headers(host.ip, port.number, query)
-
-               auth = string.match(headers, "HTTP/1.- 40[013]")
-               if auth == nil then
+               answer = http.request(host, port, query)
+               if answer.status ~= 401 and answer.status ~= 403 then
                        result = result .. "  HTTP server may accept user=\"admin\" with blank password for Basic 
authentication\n"
                end
 
@@ -120,11 +69,8 @@
                query = query .. "Connection: close\r\n"
                query = query .. "Host: " .. host.ip .. ":" .. port.number .. "\r\n\r\n"
 
-               auth = ""
-               headers = get_http_headers(host.ip, port.number, query)
-
-               auth = string.match(headers, "HTTP/1.- 40[013]")
-               if auth == nil then
+               answer = http.request(host, port, query)
+               if answer.status ~= 401 and answer.status ~= 403 then
                        result = result .. "  HTTP server may accept user=\"admin\" with password=\"admin\" for Basic 
authentication\n"
                end
        end
Index: scripts/showHTMLTitle.nse
===================================================================
--- scripts/showHTMLTitle.nse   (revision 6740)
+++ scripts/showHTMLTitle.nse   (working copy)
@@ -11,7 +11,7 @@
 
 categories = {"demo", "safe"}
 
-require "stdnse"
+require 'http'
 
 portrule = function(host, port)
        if not (port.service == 'http' or port.service == 'https') then
@@ -26,41 +26,20 @@
 end
 
 action = function(host, port)
-       local socket, request, result, status, s, title, protocol
+       local data, result, title, protocol
 
-       socket = nmap.new_socket()
+       data = http.get( host, port, '/' )
+       result = data.body
 
-       if port.service == 'https' or port.version.service_tunnel == 'ssl' then
-               protocol = "ssl"
-       else
-               protocol = "tcp"
-       end
-
-       socket:connect(host.ip, port.number, protocol )
-       request = "GET / HTTP/1.0\r\n\r\n"
-       socket:send(request)
-
-       result = ""
-       while true do
-               status, s = socket:receive_lines(1)
-               if not status then
-                       break
-               end
-
-               result = result .. s
-       end
-       socket:close()
-       
        -- watch out, this doesn't really work for all html tags
-       -- also string.lower consumes the /
-       result = string.gsub(result, "</?(%a+)>", function(c) return "<" .. string.lower(c) .. ">" end)
-       
-       title = string.match(result, "<title>(.+)<title>")
+       result = string.gsub(result, "<(/?%a+)>", function(c) return "<" .. string.lower(c) .. ">" end)
 
+       title = string.match(result, "<title>(.+)</title>")
+
        if title ~= nil then
                result = string.gsub(title , "[\n\r\t]", "")
                if string.len(title) > 50 then
-                       stdnse.print_debug("showHTMLTitle.nse: Title got truncated!");  
+                       stdnse.print_debug("showHTMLTitle.nse: Title got truncated!");
                        result = string.sub(result, 1, 62) .. "..."
                end
        else
Index: scripts/robots.nse
===================================================================
--- scripts/robots.nse  (revision 6740)
+++ scripts/robots.nse  (working copy)
@@ -1,6 +1,7 @@
 require('shortport')
 require('strbuf')
 require('listop')
+require('http')
 
 id = "robots.txt"
 author = "Eddie Bell <ejlbell () gmail com>"
@@ -9,7 +10,7 @@
 categories = {"safe"}
 runlevel = 1.0
 
-portrule = shortport.port_or_service(80, "http")
+portrule = shortport.port_or_service({80,443}, {"http","https"})
 local last_len = 0
 
 -- split the output in 40 character lines 
@@ -32,40 +33,15 @@
 end
 
 action = function(host, port)
-       local soc, lines, status
+       local answer = http.get( host, port, "/robots.txt" )
 
-       local catch = function() soc:close() end
-       local try = nmap.new_try(catch)
-
-       -- connect to webserver 
-       soc = nmap.new_socket()
-       soc:set_timeout(4000)
-       try(soc:connect(host.ip, port.number))
-
-       local query = strbuf.new()
-       query = query .. "GET /robots.txt HTTP/1.1"
-       query = query .. "Accept: */*"
-       query = query .. "Accept-Language: en"
-       query = query .. "User-Agent: Nmap NSE"
-       query = query .. "Host: " .. host.ip .. ":" .. port.number
-       query = query .. "Connection: close"
-       query = query .. '\r\n\r\n';
-       try(soc:send(strbuf.dump(query, '\r\n')))
-
-       local response = strbuf.new()
-       while true do
-               status, lines = soc:receive_lines(1)
-               if not status then break end
-               response = response .. lines
-       end
-
-       if not string.find(strbuf.dump(response), "HTTP/1.1 200 OK") then
+       if answer.status ~= 200 then
                return nil
        end
 
        -- parse all disallowed entries and remove comments
        local output = strbuf.new()
-       for w in string.gmatch(strbuf.dump(response, '\n'), "Disallow:%s*([^\n]*)\n") do
+       for w in string.gmatch(answer.body, "Disallow:%s*([^\n]*)\n") do
                        w = w:gsub("%s*#.*", "")
                        buildOutput(output, w)
        end

Attachment: signature.asc
Description: OpenPGP digital signature


_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: