Lesson 6 of 10
The NSE library toolbox
The libraries you'll use in nearly every script: stdnse, shortport, nmap sockets, comm, http and vulns. Plus where to find the rest.
Real scripts are short because the nselib libraries do the heavy lifting: sockets, protocols, parsing, reporting. Learn the handful below and you can read most of Nmap’s bundled scripts.
stdnse: the everyday helpers
local stdnse = require "stdnse"
-- script arguments: --script-args 'my-script.path=/admin,my-script.timeout=10'
local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
local timeout = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 5
-- debug output: only visible with -d (level 1) and up
stdnse.debug1("checking %s:%d", host.ip, port.number)
stdnse.debug2("raw response: %q", data)
-- ordered output table
local out = stdnse.output_table()
-- a timeout scaled to the user's -T timing template
local ms = stdnse.get_timeout(host, 5000)
-- sleeping (cooperatively: other scripts keep running)
stdnse.sleep(0.5)
Script arguments always arrive as strings. Convert with tonumber and validate. get_script_args also accepts several names and returns the first that is set, which lets users pass either my-script.path or a shared short name.
shortport: choosing what to run against
Covered in lesson 5. shortport.http, shortport.port_or_service, shortport.service and shortport.ssl cover nearly every rule you’ll need.
nmap: sockets and the registry
The nmap library gives you raw TCP/UDP sockets that cooperate with NSE’s scheduler.
local nmap = require "nmap"
local socket = nmap.new_socket() -- TCP by default
socket:set_timeout(5000) -- milliseconds
-- nmap.new_try builds a helper that runs your cleanup and aborts on failure
local try = nmap.new_try(function() socket:close() end)
try(socket:connect(host, port)) -- host and port tables work directly
try(socket:send("HELLO\r\n"))
local line = try(socket:receive_lines(1)) -- first line
socket:close()
Socket methods return status, data_or_error. Wrapping them in try(...) unwraps the data on success and, on failure, calls your cleanup function and ends the script quietly. That’s usually what you want for a network hiccup.
Other socket calls: receive_bytes(n), receive_buf(delimiter, keeppattern) for reading until a delimiter, and nmap.new_socket("udp").
The registry is a table shared by every script in the scan:
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
nmap.registry[SCRIPT_NAME].seen = (nmap.registry[SCRIPT_NAME].seen or 0) + 1
Use it to pass data between scripts or between calls, and namespace your keys.
comm: one-call network exchanges
For simple “connect, send something, read the reply” work, comm wraps all the socket code, including trying SSL when appropriate.
local comm = require "comm"
-- just read whatever the server says first (banner grab)
local status, banner = comm.get_banner(host, port, {lines = 1, timeout = 5000})
-- send a request and read the reply
local status, reply = comm.exchange(host, port, "VERSION\r\n", {lines = 1, timeout = 5000})
Both return true, data on success, and false, error_message on failure. Check the status before using the data.
http: talking to web servers
local http = require "http"
local resp = http.get(host, port, "/status")
if resp and resp.status == 200 then
print(resp.body)
print(resp.header["server"]) -- header names are lower-cased
end
http.head(host, port, "/")
http.post(host, port, "/login", nil, {user = "a", pass = "b"})
-- useful options
http.get(host, port, "/", {
timeout = 5000,
redirect_ok = false, -- don't follow redirects
header = {["Accept"] = "text/html"},
max_body_size = 64 * 1024, -- stop reading after 64 KiB
})
The response is a table with status (a number), header (lower-case keys), body, and cookies. On a network failure status is nil, so always test it before comparing. The library also caches responses and handles redirects, SSL, and the user agent (--script-args http.useragent=...).
vulns: reporting findings in the standard format
Vulnerability scripts share one output format so results look the same everywhere:
local vulns = require "vulns"
local vuln = {
title = "Exposed .git directory",
state = vulns.STATE.NOT_VULN, -- start pessimistic, then update
risk_factor = "Medium",
description = [[The repository metadata is downloadable, which can expose source code and secrets.]],
}
local report = vulns.Report:new(SCRIPT_NAME, host, port)
-- ... run the check ...
vuln.state = vulns.STATE.VULN
return report:make_output(vuln)
make_output produces the familiar State: VULNERABLE block. States include VULN, NOT_VULN, LIKELY_VULN and EXPLOIT. By default only vulnerable results are shown.
A few more you’ll meet
| Library | What it’s for |
|---|---|
stringaux | strsplit, strjoin and other string helpers. |
tableaux | Table helpers (copy, merge, membership). |
json, base64, url | Encoding and parsing. |
sslcert, tls | TLS handshakes and certificate details. |
creds, unpwdb, brute | Credential storage, username/password lists, brute-force framework. |
target | Adding newly discovered hosts to the scan. |
nsedebug | Pretty-printing tables for debugging (nsedebug.tostr(t)). |
Lab
- Use
shortport.httpas the rule. - Use
http.getand checkresp and resp.status. - Read a
web-summary.pathscript argument with a default of/. - Return a
stdnse.output_table()containingpath,status,server(only if present) andbytes. - Log the request with
stdnse.debug1, and confirm with-dthat you can see it.
Solution
local http = require "http"
local shortport = require "shortport"
local stdnse = require "stdnse"
description = [[Summarises the response for a path on a web server.]]
author = "Your Name"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.http
action = function(host, port)
local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
stdnse.debug1("GET %s on %s:%d", path, host.ip, port.number)
local resp = http.get(host, port, path)
if not (resp and resp.status) then return nil end
local out = stdnse.output_table()
out.path = path
out.status = resp.status
out.server = resp.header["server"] -- nil is fine: the field is simply omitted
out.bytes = #resp.body
return out
end
Run it: nmap -p 8000 --script ./web-summary.nse --script-args web-summary.path=/index.html 127.0.0.1
Checkpoint
A comm.get_banner call returns false, "TIMEOUT". What should your script do?
Log it with stdnse.debug1 and return nil. A timeout is a normal network outcome, not a script failure, and the scan shouldn’t print an error for it.
Why check resp.status rather than just resp?
http.get returns a table even when the request fails. On failure status is nil, so the table alone doesn’t prove you got a response.