Writing Nmap NSE scripts for vulnerability scanning

Imlementation

Skeleton Code

-- The Head Section --
-- The Rule Section --
portrule = function(host, port)
return port.protocol == "tcp"
and port.number == 80
and port.state == "open"
end

-- The Action Section --
action = function(host, port)
return "Hello world!"
end
  • The Head Section contains meta-data which describes script’s functionality, author, impact, category and other descriptive data. This section will be left blank for now; however, we will populate it later once the sample nse script is more complete.
  • The Rule Section defines necessary conditions for the script to execute. This section must contain at least one function from this list: portrule, hostrule, prerule, postrule. For the purposes of this tutorial (and the majority of scripts), I will concentrate on the portrule which can perform checks on both host and port properties before deciding to run. In the script above, portrule takes advantage of nmap’s API to check for an open TCP port 80.
  • The Action Section defines the script logic. In the tradition of K&R, I will simply output “Hello world!” for any open port 80. It is important to note that script output displayed during nmap’s execution will be based on the string returned by this section.
# nmap --script http-vuln-check thesprawl.org -p 22,80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 02:36 EST
Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.023s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
22/tcp filtered ssh
80/tcp open http
|_http-vuln-check: Hello world!
443/tcp open https

Nmap done: 1 IP address (1 host up) scanned in 1.77 seconds

Using NSE Libraries

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)
return "Hello world!"
end
# nmap --script http-vuln-check thesprawl.org -p 22,80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 02:51 EST
Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.032s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
|_http-vuln-check: Hello world!
443/tcp open https
|_http-vuln-check: Hello world!

Nmap done: 1 IP address (1 host up) scanned in 0.19 seconds

Service Detection

local shortport = require "shortport"
local http = require "http"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"
local response = http.get(host, port, uri)
return response.status

end
# nmap --script http-vuln-check localhost thesprawl.org -p 80,443
Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 03:44 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000040s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
|_http-vuln-check: 200
443/tcp open https
|_http-vuln-check: 200

Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.023s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
80/tcp open http
|_http-vuln-check: 404
443/tcp open https
|_http-vuln-check: 404

Nmap done: 2 IP addresses (2 hosts up) scanned in 8.63 seconds
local shortport = require "shortport"
local http = require "http"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"
local response = http.get(host, port, uri)

if ( response.status == 200 ) then
return response.body
end

end
# nmap --script http-vuln-check localhost thesprawl.org -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 04:03 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000049s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
| http-vuln-check: <html>
| <head>
| <title>ArcticFission 1.0</title>
| </head>
| <body>
| <h1>Welcome to ArcticFission 1.0</h1>
| </body>
|_</html>
443/tcp open https
| http-vuln-check: <html>
| <head>
| <title>ArcticFission 1.0</title>
| </head>
| <body>
| <h1>Welcome to ArcticFission 1.0</h1>
| </body>
|_</html>

Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.023s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
80/tcp open http
443/tcp open https

Nmap done: 2 IP addresses (2 hosts up) scanned in 8.64 seconds

Vulnerability Detection

local shortport = require "shortport"
local http = require "http"
local string = require "string"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"
local response = http.get(host, port, uri)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")
return title
end

end
# nmap --script http-vuln-check localhost -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 04:17 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000031s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
|_http-vuln-check: 1.0
443/tcp open https
|_http-vuln-check: 1.0
local shortport = require "shortport"
local http = require "http"
local string = require "string"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"
local response = http.get(host, port, uri)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")

if ( title == "1.0" ) then
return "Vulnerable"
else
return "Not Vulnerable"
end
end
end
# nmap --script http-vuln-check localhost -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-09 04:24 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000034s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
|_http-vuln-check: Vulnerable
443/tcp open https
|_http-vuln-check: Vulnerable

Nmap done: 1 IP address (1 host up) scanned in 8.08 seconds
local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
local openssl = require "openssl"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"
local response = http.get(host, port, uri)

if ( response.status == 200 ) then
local vulnsha1 = "984c6f159d5b5baba8fe23dfa5372d047ed1de2e"
local sha1 = string.lower(stdnse.tohex(openssl.sha1(response.body)))

if ( sha1 == vulnsha1 ) then
return "Vulnerable"
else
return "Not Vulnerable"
end
end
end

Adding a bit of stealth

# nmap --script http-vuln-check localhost -p 80,443 --script-trace
Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-11 01:42 EST
NSOCK (0.0490s) nsi_new (IOD #1)
NSOCK (0.0790s) TCP connection requested to 127.0.0.1:80 (IOD #1) EID 8
NSOCK (0.0790s) nsi_new (IOD #2)
NSOCK (0.0790s) SSL connection requested to 127.0.0.1:443/tcp (IOD #2) EID 17
NSOCK (0.0800s) Callback: CONNECT SUCCESS for EID 8 [127.0.0.1:80]
NSE: TCP 127.0.0.1:56968 > 127.0.0.1:80 | CONNECT
NSOCK (0.0800s) Callback: SSL-CONNECT SUCCESS for EID 17 [127.0.0.1:443]
NSE: TCP 127.0.0.1:55825 > 127.0.0.1:443 | CONNECT
NSE: TCP 127.0.0.1:56968 > 127.0.0.1:80 | 00000000: 47 45 54 20 2f 61 72 63 74 69 63 66 69 73 73 69 GET /arcticfissi
00000010: 6f 6e 2e 68 74 6d 6c 20 48 54 54 50 2f 31 2e 31 on.html HTTP/1.1
00000020: 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 4d 6f User-Agent: Mo
00000030: 7a 69 6c 6c 61 2f 35 2e 30 20 28 63 6f 6d 70 61 zilla/5.0 (compa
00000040: 74 69 62 6c 65 3b 20 4e 6d 61 70 20 53 63 72 69 tible; Nmap Scri
00000050: 70 74 69 6e 67 20 45 6e 67 69 6e 65 3b 20 68 74 pting Engine; ht
00000060: 74 70 3a 2f 2f 6e 6d 61 70 2e 6f 72 67 2f 62 6f tp://nmap.org/bo
00000070: 6f 6b 2f 6e 73 65 2e 68 74 6d 6c 29 0d 0a 43 6f ok/nse.html) Co
00000080: 6e 6e 65 63 74 69 6f 6e 3a 20 63 6c 6f 73 65 0d nnection: close
00000090: 0a 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 Host: localhost
000000a0: 0d 0a 0d 0a
...
# nmap --script http-vuln-check localhost -p 80,443 --script-args="http.useragent='Mozilla/5.0 (compatible; ArcticFission)'"
local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"

local options = {header={}}
options['header']['User-Agent'] = "Mozilla/5.0 (compatible; ArcticFission)"

local response = http.get(host, port, uri, options)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")

if ( title == "1.0" ) then
return "Vulnerable"
else
return "Not Vulnerable"
end
end
end

Packaging the Script

-- The Head Section --
description = [[Sample script to detect a fictional vulnerability
in a fictional ArcticFission 1.0 web server]]
author = "iphelix"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "safe"}

local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"

local options = {header={}}
options['header']['User-Agent'] = "Mozilla/5.0 (compatible; ArcticFission)"

local response = http.get(host, port, uri, options)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")

if ( title == "1.0" ) then
return "Vulnerable"
else
return "Not Vulnerable"
end
end
end
-- The Head Section --
description = [[Sample script to detect a fictional vulnerability
in a fictional ArcticFission 1.0 web server]]
author = "iphelix"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "safe"}

local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

local uri = "/arcticfission.html"

local options = {header={}}
options['header']['User-Agent'] = "Mozilla/5.0 (compatible; ArcticFission)"

local response = http.get(host, port, uri, options)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")

if ( title == "1.0" ) then
return "Vulnerable"
else
return "Not Vulnerable"
end
end
end

Parsing the output

#!/usr/bin/env python
# nmap-xml-parse by iphelix
import sys
from xml.dom.minidom import parse

if len(sys.argv) != 2:
print "Usage: %s nmap_output.xml"
sys.exit(1)

nmap = parse(sys.argv[1])

for host in nmap.getElementsByTagName("host"):
addresses = [addr.getAttribute("addr") for addr in host.getElementsByTagName("address")]

for port in host.getElementsByTagName("port"):
portid = port.getAttribute("portid")

for script in port.getElementsByTagName("script"):
if script.getAttribute("id") == "http-vuln-check":
output = script.getAttribute("output")

for address in addresses:
print "%s,%s,%s" % (address, portid, output)
# nmap --script http-vuln-check localhost -p 80,443 -oA http-vuln

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-11 02:47 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000066s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
|_http-vuln-check: Vulnerable
443/tcp open https
|_http-vuln-check: Vulnerable

Nmap done: 1 IP address (1 host up) scanned in 8.08 seconds
# ./nmap-xml-parse.py http-vuln.xml
127.0.0.1,80,Vulnerable
127.0.0.1,443,Vulnerable

Vulnerability Management

NSE Vulnerability Library

-- The Head Section --
description = [[Sample script to detect a fictional vulnerability
in a fictional ArcticFission 1.0 web server]]

---
-- @usage
-- nmap --script http-vuln-check <target>
-- @output
-- PORT STATE SERVICE
-- 80/tcp open http
-- | http-vuln-check:
-- | VULNERABLE:
-- | ArcticFission 1.0 Vulnerability
-- | State: VULNERABLE
-- | IDs: CVE:CVE-XXXX-XX
-- | References:
-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX


author = "iphelix"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "safe"}

local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"
local vulns = require "vulns"

-- The Rule Section --
portrule = shortport.http

-- The Action Section --
action = function(host, port)

-- The Vuln Definition Section --
local vuln = {
title = "ArcticFission 1.0 Vulnerability",
state = vulns.STATE.NOT_VULN, --default
IDS = { CVE = 'CVE-XXXX-XX' }
}
local report = vulns.Report:new(SCRIPT_NAME, host, port)

local uri = "/arcticfission.html"

local options = {header={}}
options['header']['User-Agent'] = "Mozilla/5.0 (compatible; ArcticFission)"

local response = http.get(host, port, uri, options)

if ( response.status == 200 ) then
local title = string.match(response.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>ArcticFission ([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")

if ( title == "1.0" ) then
vuln.state = vulns.STATE.VULN
else
vuln.state = vulns.STATE.NOT_VULN
end
end

return report:make_output(vuln)
end
-- The Vuln Definition Section --
local vuln = {
title = "ArcticFission 1.0 Vulnerability",
state = vulns.STATE.NOT_VULN, --default
IDS = { CVE = 'CVE-XXXX-XX' }
}
if ( title == "1.0" ) then
vuln.state = vulns.STATE.VULN
else
vuln.state = vulns.STATE.NOT_VULN
end
# nmap --script http-vuln-check localhost -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-11 02:52 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000038s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX
443/tcp open https
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX

Aggregating output

local stdnse = require "stdnse"
local vulns = require "vulns"

local FID -- my script FILTER ID

prerule = function()
FID = vulns.save_reports()
if FID then
return true
end
return false
end

postrule = function()
if nmap.registry[SCRIPT_NAME] then
FID = nmap.registry[SCRIPT_NAME].FID
if vulns.get_ids(FID) then
return true
end
end
return false
end

prerule_action = function()
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
nmap.registry[SCRIPT_NAME].FID = FID
return nil
end

postrule_action = function()
return vulns.make_output(FID) -- show all the vulnerabilities
end

local tactions = {
prerule = prerule_action,
postrule = postrule_action,
}

action = function(...) return tactions[SCRIPT_TYPE](...) end
# nmap --script http-vuln-check,vulns-post-process localhost thesprawl.org -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-11 03:12 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000051s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX
443/tcp open https
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX

Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.012s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
80/tcp open http
443/tcp open https

Post-scan script results:
| vulns-post-process:
| Vulnerability report for 108.59.3.64: NOT VULNERABLE
|
| Vulnerability report for 127.0.0.1: VULNERABLE
| Target: localhost (127.0.0.1) Port: 80/http
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
| http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX
| Reported by scripts: http-vuln-check
|
| Target: localhost (127.0.0.1) Port: 443/https
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
| http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX
|_ Reported by scripts: http-vuln-check
Nmap done: 2 IP addresses (2 hosts up) scanned in 0.42 seconds
local stdnse = require "stdnse"
local vulns = require "vulns"

local FID -- my script FILTER ID

prerule = function()
FID = vulns.save_reports()
if FID then
return true
end
return false
end

postrule = function()
if nmap.registry[SCRIPT_NAME] then
FID = nmap.registry[SCRIPT_NAME].FID
if vulns.get_ids(FID) then
return true
end
end
return false
end

prerule_action = function()
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
nmap.registry[SCRIPT_NAME].FID = FID
return nil
end

postrule_action = function()
local filter = {state = vulns.STATE.VULN}
local list = vulns.find(FID, filter)
if list then
local out = {}
for _, vuln_table in ipairs(list) do
local ip = vuln_table.host.ip
local port = vuln_table.port.number
local state = vulns.STATE_MSG[vuln_table.state]
local title = vuln_table.title
table.insert(out, string.format("%s:%d - %s - %s", ip, port, title, state))

end
return stdnse.format_output(true, out)
end
end

local tactions = {
prerule = prerule_action,
postrule = postrule_action,
}

action = function(...) return tactions[SCRIPT_TYPE](...) end
# nmap --script http-vuln-check,vulns-post-process localhost thesprawl.org -p 80,443

Starting Nmap 6.25 ( http://nmap.org ) at 2013-01-11 04:15 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000067s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT STATE SERVICE
80/tcp open http
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX
443/tcp open https
| http-vuln-check:
| VULNERABLE:
| ArcticFission 1.0 Vulnerability
| State: VULNERABLE
| IDs: CVE:CVE-XXXX-XX
| References:
|_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-XXXX-XX

Nmap scan report for thesprawl.org (108.59.3.64)
Host is up (0.012s latency).
rDNS record for 108.59.3.64: web219.webfaction.com
PORT STATE SERVICE
80/tcp open http
443/tcp open https

Post-scan script results:
| vulns-post-process:
| 127.0.0.1:80 - ArcticFission 1.0 Vulnerability - VULNERABLE
|_ 127.0.0.1:443 - ArcticFission 1.0 Vulnerability - VULNERABLE
Nmap done: 2 IP addresses (2 hosts up) scanned in 13.44 seconds
postrule_action = function()
local filter = {state = vulns.STATE.VULN}
local list = vulns.find(FID, filter)
if list then
local out = {}
for _, vuln_table in ipairs(list) do
local ip = vuln_table.host.ip
local port = vuln_table.port.number
local state = vulns.STATE_MSG[vuln_table.state]
local title = vuln_table.title
table.insert(out, string.format("%s:%d - %s - %s", ip, port, title, state))

end
return stdnse.format_output(true, out)
end
end

Where to go from here

External Links and References

--

--

--

Blockchain Security, Malware Analysis, Incident Response, Pentesting, BlockThreat.net

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Peter Kacherginsky

Peter Kacherginsky

Blockchain Security, Malware Analysis, Incident Response, Pentesting, BlockThreat.net

More from Medium

VulnHub — VulnCMS: 1 (Drupalgeddon Path)

Top 10 Links to harden your Linux machine

An Exploration of SQL Injection