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 to determine the owner of the process listening on a TCP port. This is not really the purpose of identd (it is meant for querying the owner of outgoing connections, not listening daemons), but many identd servers allow it anyway. Nmap used to have this functionality (called ident scan), but it was removed while transitioning to a new scan engine architecture. The protocol identd uses is pretty simple, but still too complicated to handle with Nmap's version detection language. First, you connect to the identification server and send a query of the form <port-on-server>, <port-on-client> and terminated with a newline character. The server should then respond with a string containing the server port, client port, response type, and address information. The address information is omitted if there is an error. More details are available in RFC 1413, but this description is sufficient for our purposes. 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 listening port you wish 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: description, categories, dependencies, author, and license as well as initial NSEDoc information such as usage, args, and output tags (see the section called “Writing Script Documentation (NSEDoc)”).

The description field should contain a paragraph or more 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. If there are multiple paragraphs, the first is used as a short summary where necessary. Make sure that first paragraph can serve as a stand alone abstract. This description is short because it is such a simple script:

description = [[
Attempts to find the owner of an open TCP port by querying an auth
(identd - port 113) daemon which must also be open on the target system.
]]

Next comes NSEDoc information. This script is missing the common @usage and @args tags since it is so simple, but it does have an NSEDoc @output tag:

---
--@output
-- 21/tcp   open     ftp       ProFTPD 1.3.1
-- |_ auth-owners: nobody
-- 22/tcp   open     ssh       OpenSSH 4.3p2 Debian 9etch2 (protocol 2.0)
-- |_ auth-owners: root
-- 25/tcp   open     smtp      Postfix smtpd
-- |_ auth-owners: postfix
-- 80/tcp   open     http      Apache httpd 2.0.61 ((Unix) PHP/4.4.7 ...)
-- |_ auth-owners: dhapache
-- 113/tcp  open     auth?
-- |_ auth-owners: nobody
-- 587/tcp  open     submission Postfix smtpd
-- |_ auth-owners: postfix
-- 5666/tcp open     unknown
-- |_ auth-owners: root

Next come the author, license, and categories tags. This script belongs to the safe because we are not using the service for anything it was not intended for. Because this script is one that should run by default it is also in the default category. Here are the variables in context:

author = "Diman Todorov"

license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"default", "safe"}

The Rule

The rule section is a Lua method which decides whether to skip or execute the script's action. This decision is usually based on the type of the rule and the host and port information passed to it. A prerule or a postrule will always evaluate to true. In the case of the identification script, it is slightly more complicated than that. To decide whether to run the identification script against a given port we need to know if there is an auth server running on the target machine. In other words, the script should be run 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 use the nmap.get_port_state function. If the auth port was not scanned, the get_port_state function returns nil. So we check that the table is not nil. We also check that 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 auth_port = { number=113, protocol="tcp" }
	local identd = nmap.get_port_state(host, auth_port)

	return identd ~= nil
		and identd.state == "open"
		and port.protocol == "tcp"
		and port.state == "open"
end

The Action

At last we implement the actual functionality! The script first connects to the port on which we expect to find the identification server, then it will connect to the port we want information about. Doing so involves first creating two socket options by calling nmap.new_socket. Next we define an error-handling catch function which closes those sockets if failure is detected. At this point we can safely use object methods such as open, close, send and receive to operate on the network socket. In this case we call connect to make the connections. NSE's exception handling mechanism is used to avoid excessive error-handling code. We simply wrap the networking calls in a try call which will in turn call our catch function if anything goes wrong.

If the two connections succeed, we construct a query string and parse the response. If we received a satisfactory response, we return the retrieved information.

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.new_try(catch)

        try(client_ident:connect(host.ip, 113))
        try(client_service:connect(host.ip, port.number))

        local localip, localport, remoteip, remoteport =
                try(client_service:get_info())

        local request = port.number .. ", " .. localport .. "\r\n"

        try(client_ident:send(request))

        owner = try(client_ident:receive_lines(1))

        if string.match(owner, "ERROR") then 
                owner = nil
        else
                owner = string.match(owner,
                        "%d+%s*,%s*%d+%s*:%s*USERID%s*:%s*.+%s*:%s*(.+)\r?\n")
        end

        try(client_ident:close())
        try(client_service:close())

        return owner
end

Note that because we know that the remote port is stored in port.number, we could have ignored the last two return values of client_service:get_info() like this:

local localip, localport = try(client_service:get_info())

In this example we exit quietly if the service responds with an error. This is done by assigning nil to the owner variable which will be returned. NSE scripts generally only return messages when they succeed, so they don't flood the user with pointless alerts.