The Nmap Scripting Engine is, honestly, the feature that turned Nmap from “a port scanner” into “a platform” for me. Once I understood NSE, I stopped thinking of Nmap as something that just tells you what’s open and started thinking of it as something that can actively investigate what’s behind those open ports — brute-forcing weak credentials, detecting specific CVEs, grabbing HTTP titles, enumerating SMB shares, and hundreds of other tasks, all through a single consistent scripting interface written in Lua.
This article covers how NSE works, how to use the scripts that ship with Nmap, and how to write your own from scratch.
What NSE Actually Is
NSE scripts are Lua programs that hook into Nmap’s scan lifecycle at specific points — before scanning, during host discovery, after a port is found open, or after the entire scan finishes. Nmap ships with over 600 scripts organized into categories, and you can write your own using the same NSE API.
flowchart TD
A[Nmap Scan Starts] --> B[prerule scripts]
B --> C[Host Discovery]
C --> D[Port Scanning]
D --> E{Port Open?}
E -->|Yes| F[hostrule / portrule scripts]
E -->|No| G[Skip]
F --> H[postrule scripts]
G --> H
H --> I[Scan Complete]
Script Categories
Every NSE script belongs to one or more categories, which lets you run broad groups without naming individual scripts:
| Category | Purpose |
|---|---|
auth | Authentication bypass / credential testing |
broadcast | Discovers hosts via broadcast queries |
brute | Brute-force credential attacks |
default | Safe, commonly useful scripts (runs with -sC) |
discovery | Deeper service/network enumeration |
dos | Denial-of-service testing (use with extreme caution) |
exploit | Actively exploits vulnerabilities |
external | Sends data to external services (e.g., whois) |
fuzzer | Sends unexpected input to find bugs |
intrusive | May crash services or trigger alerts |
malware | Checks for signs of malware/backdoors |
safe | Won’t crash things or use excessive resources |
version | Extends version detection |
vuln | Checks for specific known vulnerabilities |
Running the Default Script Set
nmap -sC 192.168.1.10
This runs every script tagged default — a curated, generally safe set that includes things like http-title, ssh-hostkey, and ftp-anon. It’s what -A includes automatically.
Running Scripts by Category
nmap --script=vuln 192.168.1.10
nmap --script=safe 192.168.1.10
nmap --script=discovery 192.168.1.10
I use --script=vuln constantly during authorized assessments — it runs every script tagged as a known-vulnerability check in one pass.
Running Individual Scripts
nmap --script=http-title 192.168.1.10
nmap --script=ssh-hostkey 192.168.1.10
nmap --script=smb-os-discovery 192.168.1.10
nmap --script=ftp-anon 192.168.1.10
Running Multiple Specific Scripts
nmap --script=http-title,http-headers,http-methods 192.168.1.10
Combining Categories and Wildcards
nmap --script="http-*" 192.168.1.10 # all scripts starting with http-
nmap --script="not intrusive" 192.168.1.10 # everything except intrusive scripts
nmap --script="vuln and safe" 192.168.1.10 # scripts matching both categories
I use the wildcard pattern constantly when I know a target is running a web service and want every HTTP-related script Nmap has without typing them all individually:
nmap -p80,443 --script="http-*" 192.168.1.10
Passing Arguments to Scripts
Many scripts accept configuration arguments via --script-args:
nmap --script=http-brute --script-args userdb=users.txt,passdb=pass.txt 192.168.1.10
nmap --script=whois-ip --script-args whois.whodb=nofollow 192.168.1.10
Getting Help on a Specific Script
nmap --script-help=http-title
This shows the script’s description, categories, and any arguments it accepts — I check this before running anything unfamiliar, especially in the intrusive or exploit categories.
Updating the Script Database
sudo nmap --script-updatedb
Run this after installing new scripts manually, or periodically to make sure Nmap’s internal script index is current.
Vulnerability Scanning with NSE
nmap --script=vuln -p 80,443 192.168.1.10
Sample output against a deliberately vulnerable test target:
80/tcp open http
| http-vuln-cve2017-5638:
| VULNERABLE:
| Apache Struts2 remote code execution
| State: VULNERABLE
| IDs: CVE:CVE-2017-5638
This is where NSE genuinely earns the “vulnerability scanner” label — it’s not comprehensive like a dedicated tool such as Nessus or OpenVAS, but for a targeted, script-based check against known CVE patterns, it’s fast and requires no additional software.
Writing a Custom NSE Script
Here’s where NSE really shines for me — when I need something Nmap doesn’t ship with. NSE scripts are written in Lua and follow a fairly consistent structure.
Basic Script Skeleton
-- my-custom-script.nse
local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
description = [[
Checks for a custom HTTP header that indicates a specific internal
application is running, and reports its presence.
]]
author = "Ahmad"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.http
action = function(host, port)
local response = http.get(host, port, "/")
if response and response.header then
local custom_header = response.header["x-internal-app"]
if custom_header then
return "Internal app detected: " .. custom_header
end
end
return nil
end
Breaking Down the Structure
description— shown in--script-helpoutput; explain what the script actually does.categories— determines which--script=categoryinvocations will include this script.portruleorhostrule— a function that decides whether this script should run against a given host/port.shortport.httpis a built-in helper that matches common HTTP ports.action— the actual logic that runs when the rule matches.
Running Your Custom Script
nmap --script=./my-custom-script.nse -p80 192.168.1.10
Or install it into Nmap’s script directory for it to be discoverable by name:
sudo cp my-custom-script.nse /usr/share/nmap/scripts/
sudo nmap --script-updatedb
nmap --script=my-custom-script -p80 192.168.1.10
A Slightly More Advanced Example: Banner Grabber with Argument Support
-- banner-grab-custom.nse
local shortport = require "shortport"
local comm = require "comm"
local stdnse = require "stdnse"
description = [[
Connects to a TCP port and grabs the first line of any banner returned,
useful for quick fingerprinting of non-standard services.
]]
author = "Ahmad"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.port_or_service({21, 22, 25, 110, 143}, {"ftp", "ssh", "smtp", "pop3", "imap"})
action = function(host, port)
local status, banner = comm.get_banner(host, port)
if status then
return stdnse.format_output(true, banner)
end
return nil
end
This kind of script is exactly the level of customization I reach for when I need to check something very specific across a batch of hosts that no built-in script quite covers.
Practical Example: A Full Vulnerability-Focused Workflow
# Step 1: identify open ports and services
sudo nmap -sV -p- 192.168.1.10 -oN services.txt
# Step 2: run default safe scripts for general enumeration
nmap -sC -p 22,80,443 192.168.1.10 -oN default_scripts.txt
# Step 3: run vulnerability-specific scripts
nmap --script=vuln -p 22,80,443 192.168.1.10 -oN vuln_scripts.txt
# Step 4: targeted follow-up on anything flagged
nmap --script=http-vuln-cve2017-5638 --script-args http-vuln-cve2017-5638.uri=/app -p80 192.168.1.10
Python Integration
import nmap
scanner = nmap.PortScanner()
scanner.scan('192.168.1.10', arguments='--script=vuln -p 80,443')
host = '192.168.1.10'
for proto in scanner[host].all_protocols():
for port in scanner[host][proto]:
port_info = scanner[host][proto][port]
if 'script' in port_info:
for script_name, output in port_info['script'].items():
if 'VULNERABLE' in output:
print(f"[!] Port {port}: {script_name} flagged a vulnerability")
print(output)
Troubleshooting
“NSE: failed to initialize the script engine” — usually a syntax error in a custom script. Run lua -c my-script.nse for a syntax check before loading it into Nmap.
Script runs but produces no output — check that your action function actually returns something; NSE scripts that return nil produce no visible output by design (used when there’s nothing to report).
--script-updatedb doesn’t pick up a new script — confirm the script is actually in /usr/share/nmap/scripts/ and has the .nse extension.
Scripts in the intrusive category cause target instability — that’s expected; those scripts are explicitly flagged as risky. Never run intrusive or exploit category scripts against production systems without a clear go-ahead and rollback plan.
Limitations
NSE’s vuln category checks for known, signature-based vulnerability patterns — it is not a substitute for a dedicated vulnerability management platform, and it will miss anything outside its script database, including zero-days or misconfigurations that don’t match a specific check. False positives happen, particularly with version-based vulnerability detection where a patched system still reports a vulnerable-looking version string.
Security Best Practices
- Always read a script’s source or run
--script-helpbefore executing anything from theintrusive,exploit,dos, orbrutecategories. - Never run
doscategory scripts against anything other than an isolated lab target — they are explicitly designed to test denial-of-service conditions. - Treat
vulnscript hits as leads to manually verify, not confirmed findings, in any report you deliver. - Keep the script database updated (
--script-updatedb) regularly, since new CVE-detection scripts get added over time.
Frequently Asked Questions
Is NSE the same as a full vulnerability scanner like Nessus? No — NSE’s vulnerability scripts check for specific known patterns and are much narrower in scope than a dedicated vulnerability management platform, though they’re a genuinely useful lightweight first pass.
What language are NSE scripts written in? Lua, a lightweight embeddable scripting language. Nmap bundles its own Lua interpreter, so you don’t need Lua installed separately.
Can NSE scripts modify or damage a target system? Scripts in the intrusive, exploit, and dos categories genuinely can, which is exactly why they’re segregated from the safe and default categories.
How many scripts ship with Nmap by default? Over 600 as of recent releases, spanning categories from simple banner grabbing to specific CVE detection.
Debugging Scripts With Trace Output
When a script isn’t behaving the way I expect, --script-trace shows the raw data sent and received during script execution:
nmap --script=http-title --script-trace -p80 192.168.1.10
This dumps every byte exchanged during the script’s execution, which is invaluable when a service returns a response format the script’s author didn’t anticipate — I’ve used this exact flag to figure out why a custom internal web service was tripping up http-title (it turned out to be sending a non-standard Content-Type header that confused the script’s HTML parsing).
The NSE Library Ecosystem
Custom scripts get most of their power from Nmap’s bundled Lua libraries, which handle the tedious parts of protocol interaction so script authors don’t have to reimplement them from scratch:
| Library | Purpose |
|---|---|
shortport | Common port-matching helper functions |
http | HTTP request/response handling |
smb | SMB protocol interaction |
ssh2 | SSH protocol interaction |
tls | TLS/SSL handshake and certificate parsing |
stdnse | General utility functions (formatting, sleep, debug output) |
shortport.port_or_service | Match specific ports or service names |
Reading through these libraries directly (they live in /usr/share/nmap/nselib/) taught me more about protocol-level programming than most tutorials I’ve read — they’re clean, well-commented reference implementations of exactly the kind of network interaction NSE scripts need.
A Real Example: Detecting an Internal API Version Header
Here’s a script closer to something I’ve actually built for a real internal need — checking whether internal API gateways are exposing a version header that shouldn’t be visible externally:
-- api-version-leak.nse
local shortport = require "shortport"
local http = require "http"
local stdnse = require "stdnse"
description = [[
Checks whether an internal API gateway is leaking an X-API-Version header
that should be stripped before reaching external-facing endpoints.
]]
author = "Ahmad"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.http
action = function(host, port)
local paths = {"/", "/api", "/api/v1/health"}
local findings = {}
for _, path in ipairs(paths) do
local response = http.get(host, port, path)
if response and response.header and response.header["x-api-version"] then
table.insert(findings, path .. " -> " .. response.header["x-api-version"])
end
end
if #findings > 0 then
return stdnse.format_output(true, findings)
end
return nil
end
This kind of small, targeted script is exactly where NSE earns its keep for me — a five-minute script that checks something specific to my own infrastructure, running as a natural part of a scan I’m already doing anyway.
Wrapping Up
NSE is what makes Nmap genuinely extensible rather than a fixed-function tool. Learning to read and write these scripts changed how I approach reconnaissance entirely — instead of running a generic scan and manually researching every open port afterward, I can encode exactly the checks I care about into a script and run them automatically across every target in scope. If you only take one thing from this article, let it be this: spend an afternoon reading through /usr/share/nmap/scripts/ on your own machine. You’ll find capabilities in there you didn’t know Nmap had.