Suppose that you are convinced of the power of NSE. How do you
go about writing your own script? Let's say
that you want to extract information from an identification
server. Nmap used to have this functionality but it was removed
because of inconsistencies in the code base. Fortunately, the
protocol identd uses is pretty simple. Unfortunately, it is too
complicated to be expressible in Nmap's version detection
language. Let's look at how the identification protocol
works. First you connect to the identification server. Next you
send a query of the form port-on-server
,
port-on-client
terminated with a new line
character. The server should then respond with a string of the
form port-on-server
, port-on-client
:response-type
:address-information
. In case of an error the address
information is omitted. This description is sufficient for our
purposes, for more details refer to RFC 1413 The protocol cannot be modeled in Nmap's version
detection language for two reasons. The first is that you need
to know both the local and the remote port of a
connection. Version detection does not provide this data. The
second, more severe obstacle, is that you need two open
connections to the target—one to the identification server and
one to the port you want to query. Both obstacles are easily
overcome with NSE.
The anatomy of a script is described in the section called “Script Format”
In this section we will show how the described structure is utilized.
The head of the script is essentially its meta
information. This includes the fields
id
, description
,
author
, license
and
categories
. We are not going to change the
run level for now. The id
of a script
should uniquely identify it. If it is absent, the path to the
script will be used as an id. We recommend to choose an id
which concisely identifies the purpose of the script, since
the ID is printed before the script's results in Nmap output.
id = "Service Owner"
The description field should contain a sentence or two describing what the script does. If anything about the script results might confuse or mislead users, and you can't eliminate the issue by improving the script or results text, it should be documented in the description
string.
description = "Opens a connection to the scanned port, opens a connection to \
port 113, queries the owner of the service on the scanned port and prints it."
Users must tell the Lua interpreter that the string
continues on the following line by ending the line with a
backslash (‘\
’). They must also decide what
categories the script belongs to. This script is a good
example of a script which cannot be categorized clearly. It is
safe
because we are not using the service
for anything it was not intended for. On the other hand, it
is intrusive
because we connect to a
service on the target and therefore potentially give out
information about us. To solve this dilemma we will place our
script in two categories:
categories = {"safe", "intrusive"}
The rule section is a Lua method which decides when the
script's action should be performed and when it should be
skipped. Usually this decision is based on the host and port
information passed to the rule function. In the case of the
identification script it is slightly more complicated than
that. To decide whether to run the identification script on a
given port we need to know if there is an identification
server running on the target machine. Or more formally: the
script should be run if (and only if) the currently scanned TCP port is open and
TCP port 113 is also open. For now we will rely on the fact that
identification servers listen on TCP port 113. Unfortunately NSE
only gives us information about the currently scanned port.
To find out if port 113 is open we are going to use the
nmap.get_port_state()
method. If the identd
port was not scanned, the get_port_state
function returns nil
. So we need to make
sure that the table is not nil
. We also
check if both ports are in the open
state.
If this is the case, the action is executed, otherwise we skip
the action.
portrule = function(host, port)
local identd, decision
local ident_port = { number=113, protocol="tcp" }
identd = nmap.get_port_state(host, ident_port)
if
identd ~= nil and identd.state == "open" and port.state == "open"
then
decision = true
else
decision = false
end
return decision
end
This rule is almost correct, but still
slightly buggy. Can you find the bug? It is a pretty subtle
one. The problem is that this script fires on any kind of open
port, TCP or UDP. The connect()
method on
the other hand assumes a TCP protocol unless it is explicitly
told to use another protocol. Since the identification service
is only defined for TCP connections, we need to narrow down
the range of ports which fire our script. Our new rule only
runs the script if the port is open, we are looking at a TCP
port, and TCP port 113 is open. Writing the new and
improved port rule is left as an exercise to the reader (or
peek at the script in the latest Nmap distribution).
At last we implement the actual functionality. The script will
first connect to the port on which we expect to find the
identification server, then it will connect to the port we
want information about. Afterward we construct a query string
and parse the response. If we received a satisfactory
response, we return the retrieved information.
First we need to create two socket objects. These objects
represent the sockets we are going to use. By using object methods
like
PrivoxyWindowOpen()
,
close()
,
send()
or
receive()
we can operate on the network
socket. To avoid excessive error checking code we use NSE's
exception handling mechanism. We create a function which will
be executed if an error occurs and call this function
catch
. Using this function we generate
a try
function. The try
function will call the catch
function
whenever there is an error condition in the tried block. Note
that we could have ignored the last two return values
of client_service:get_info()
like this:
local localip, localport = client_service:get_info()
This would have sufficed because we know that the remote port is
stored in port.number
.
In this example we
prefer not to tell the user if the query resulted in an
error. To inform users of failed
identification queries, simply uncomment the corresponding
line. It is necessary that we assign the variable owner
a nil
value because returning nil
is the only way to tell the script engine to suppress script output.
action = function(host, port)
local owner = ""
local client_ident = nmap.new_socket()
local client_service = nmap.new_socket()
local catch = function()
client_ident:close()
client_service:close()
end
local try = nmap.newtry(catch)
try(client_ident:connect(host.ip, 113))
try(client_service:connect(host.ip, port.number))
local localip, localport, remoteip,
remoteport = client_service:get_info()
local request = port.number .. ", " .. localport .. "\n"
try(client_ident:send(request))
owner = try(client_ident:receive_lines(1))
if string.match(owner, "ERROR") then
owner = nil
-- owner = "Service owner could not be determined: " .. owner
else
owner = string.match(owner, "USERID : .+ : (.+)\n", 1)
end
try(client_ident:close())
try(client_service:close())
return owner
end