Nmap Security Scanner
*Ref Guide
Security Lists
Security Tools
Site News
Advertising
About/Contact
Credits
Sponsors





Intro Reference Guide Book Install Guide
Download Changelog Zenmap GUI Docs
Bug Reports OS Detection Propaganda Related Projects
In the Movies In the News
Script Writing Tutorial
Prev Chapter 9. Nmap Scripting Engine Next

Script Writing Tutorial

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

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

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).

The Mechanism

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


Prev Up Next
Nmap API Home Version Detection using NSE
[ Nmap | Sec Tools | Mailing Lists | Site News | About/Contact | Advertising | Privacy ]