---
-- Common communication functions for network discovery tasks like
-- banner grabbing and data exchange.
--
-- The functions in this module return values appropriate for use with
-- exception handling via nmap.new_try
.
--
-- These functions may be passed a table of options, but it's not required. The
-- keys for the options table are:
-- * bytes
- minimum number of bytes to read.
-- * lines
- minimum number of lines to read.
-- * proto
- string, protocol to use. Default "tcp"
-- * timeout
- socket timeout in milliseconds. Default 8000
-- * connect_timeout
- override timeout for connection
-- * request_timeout
- override timeout for requests
-- * recv_before
- boolean, receive data before sending first payload
--
-- If both "bytes"
and "lines"
are provided,
-- "lines"
takes precedence. If neither are given, the functions
-- read as many bytes as possible.
-- @author Kris Katterjohn 04/2008
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
_ENV = stdnse.module("comm", stdnse.seeall)
-- Makes sure that opts exists and the default proto is there
local initopts = function(opts)
if not opts then
opts = {}
end
if not opts.proto then
opts.proto = "tcp"
end
return opts
end
-- Sets up the socket and connects to host:port
local setup_connect = function(host, port, opts)
local sock = nmap.new_socket()
if opts.timeout then
sock:set_timeout(opts.timeout)
end
local status, err = sock:connect(host, port, opts.proto)
if not status then
return status, err
end
return true, sock
end
local read = function(sock, opts)
local response, status
if opts.lines then
status, response = sock:receive_lines(opts.lines)
return status, response
end
if opts.bytes then
status, response = sock:receive_bytes(opts.bytes)
return status, response
end
status, response = sock:receive()
return status, response
end
--- This function simply connects to the specified port number on the
-- specified host and returns any data received.
--
-- The first return value is true to signal success or false to signal
-- failure. On success the second return value is the response from the
-- remote host. On failure the second return value is an error message.
-- @param host The host to connect to.
-- @param port The port on the host.
-- @param opts The options. See the module description.
-- @return Status (true or false).
-- @return Data (if status is true) or error string (if status is false).
get_banner = function(host, port, opts)
opts = initopts(opts)
opts.recv_before = true
local socket, nothing, correct, banner = tryssl(host, port, "", opts)
if socket then
socket:close()
return true, banner
end
return false, banner
end
--- This function connects to the specified port number on the specified
-- host, sends data, then waits for and returns the response, if any.
--
-- The first return value is true to signal success or false to signal
-- failure. On success the second return value is the response from the
-- remote host. On failure the second return value is an error message.
-- @param host The host to connect to.
-- @param port The port on the host.
-- @param data The data to send initially.
-- @param opts The options. See the module description.
-- @return Status (true or false).
-- @return Data (if status is true) or error string (if status is false).
exchange = function(host, port, data, opts)
opts = initopts(opts)
local status, sock = setup_connect(host, port, opts)
local ret
if not status then
-- sock is an error message in this case
return status, sock
end
status, ret = sock:send(data)
if not status then
sock:close()
return status, ret
end
status, ret = read(sock, opts)
sock:close()
return status, ret
end
--- This function uses shortport.ssl to check if the port is a likely SSL port
-- @see shortport.ssl
--
-- @param port The port table to check
-- @return bool True if port is usually ssl, otherwise false
local function is_ssl(port)
return shortport.ssl(nil, port)
end
--- This function returns best protocol order for trying to open a
-- connection based on port and service information
--
-- The first value is the best option, the second is the worst
-- @param port The port table
-- @return Best option ("tcp" or "ssl")
-- @return Worst option ("tcp" or "ssl")
local function bestoption(port)
if type(port) == 'table' then
if port.version and port.version.service_tunnel and port.version.service_tunnel == "ssl" then return "ssl","tcp" end
if port.version and port.version.name_confidence and port.version.name_confidence > 6 then return "tcp","ssl" end
if is_ssl(port) then return "ssl","tcp" end
elseif type(port) == 'number' then
if is_ssl({number=port, protocol="tcp", state="open", version={}}) then return "ssl","tcp" end
end
return "tcp","ssl"
end
--- This function opens a connection, sends the first data payload and
-- check if a response is correctly received (what means that the
-- protocol used is fine)
--
-- Possible options:
-- timeout: generic timeout value
-- connect_timeout: specific timeout for connection
-- request_timeout: specific timeout for requests
-- recv_before: receive data before sending first payload
--
-- Default timeout is set to 8000.
--
-- @param host The destination host IP
-- @param port The destination host port
-- @param protocol The protocol for the connection
-- @param data The first data payload of the connection
-- @return sd The socket descriptor, nil if no connection is established
-- @return response The response received for the payload
-- @return early_resp If opt recv_before is true, returns the value
-- of the first receive (before sending data)
local function opencon(host, port, protocol, data, opts)
local sd = nmap.new_socket()
-- check for connect_timeout or timeout option
if opts and opts.connect_timeout then
sd:set_timeout(opts.connect_timeout)
elseif opts and opts.timeout then
sd:set_timeout(opts.timeout)
else
sd:set_timeout(8000)
end
local status = sd:connect(host, port, protocol)
if not status then
sd:close()
return nil, nil, nil
end
-- check for request_timeout or timeout option
if opts and opts.request_timeout then
sd:set_timeout(opts.request_timeout)
elseif opts and opts.timeout then
sd:set_timeout(opts.timeout)
else
sd:set_timeout(8000)
end
local response, early_resp;
if opts and opts.recv_before then status, early_resp = read(sd, opts) end
if data and #data > 0 then
sd:send(data)
status, response = sd:receive()
else
if not (opts and opts.recv_before) then
stdnse.print_debug("Using comm.tryssl without either first data payload or opts.recv_before." ..
"\nImpossible to test the connection for the correct protocol!")
end
response = early_resp
end
if not status then
sd:close()
return nil, response, early_resp
end
return sd, response, early_resp
end
--- This function tries to open a connection based on the best
-- option about which is the correct protocol
--
-- If the best option fails, the function tries the other option
--
-- This function allows writing nse scripts in a way that the
-- API will take care of ssl issues, making failure detection
-- transparent to the programmer
--
-- @param host The host table
-- @param port The port table
-- @param data The first data payload of the connection
-- @param opts Options, such as timeout
-- @return sd The socket descriptor
-- @return response The response received for the payload
-- @return correctOpt Correct option for connection guess
-- @return earlyResp If opt recv_before is true, returns the value
-- of the first receive (before sending data)
function tryssl(host, port, data, opts)
local opt1, opt2 = bestoption(port)
local best = opt1
local sd, response, early_resp = opencon(host, port, opt1, data, opts)
if not sd then
sd, response, early_resp = opencon(host, port, opt2, data, opts)
best = opt2
end
if not sd then best = "none" end
return sd, response, best, early_resp
end
local unittest = require "unittest"
if not unittest.testing() then
return _ENV
end
test_suite = unittest.TestSuite:new()
test_suite:add_test(unittest.table_equal({bestoption(443)}, {"ssl", "tcp"}), "bestoption ssl number")
test_suite:add_test(unittest.table_equal({bestoption(80)}, {"tcp", "ssl"}), "bestoption tcp number")
test_suite:add_test(unittest.table_equal({bestoption({number=8443,protocol="tcp",state="open",version={}})}, {"ssl", "tcp"}), "bestoption ssl table")
test_suite:add_test(unittest.table_equal({bestoption({number=1234,protocol="tcp",state="open",version={}})}, {"tcp", "ssl"}), "bestoption tcp table")
return _ENV;