Nmap Scripting Engine (NSE): Using and Writing Custom Nmap Scripts for Vulnerability Scanning

Nmap Scripting Engine (NSE) Using and Writing Custom Nmap Scripts for Vulnerability Scanning

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:

CategoryPurpose
authAuthentication bypass / credential testing
broadcastDiscovers hosts via broadcast queries
bruteBrute-force credential attacks
defaultSafe, commonly useful scripts (runs with -sC)
discoveryDeeper service/network enumeration
dosDenial-of-service testing (use with extreme caution)
exploitActively exploits vulnerabilities
externalSends data to external services (e.g., whois)
fuzzerSends unexpected input to find bugs
intrusiveMay crash services or trigger alerts
malwareChecks for signs of malware/backdoors
safeWon’t crash things or use excessive resources
versionExtends version detection
vulnChecks 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-help output; explain what the script actually does.
  • categories — determines which --script=category invocations will include this script.
  • portrule or hostrule — a function that decides whether this script should run against a given host/port. shortport.http is 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-help before executing anything from the intrusive, exploit, dos, or brute categories.
  • Never run dos category scripts against anything other than an isolated lab target — they are explicitly designed to test denial-of-service conditions.
  • Treat vuln script 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:

LibraryPurpose
shortportCommon port-matching helper functions
httpHTTP request/response handling
smbSMB protocol interaction
ssh2SSH protocol interaction
tlsTLS/SSL handshake and certificate parsing
stdnseGeneral utility functions (formatting, sleep, debug output)
shortport.port_or_serviceMatch 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.

Total
1
Shares

Leave a Reply

Previous Post
Nmap Firewall Evasion Techniques: Fragmenting Packets, Decoys, and Source Port Manipulation

Nmap Firewall Evasion Techniques: Fragmenting Packets, Decoys, and Source Port Manipulation

Next Post
Nmap Host Discovery: Ping Sweeps, ARP Scans, and Finding Live Hosts on a Network

Nmap Host Discovery: Ping Sweeps, ARP Scans, and Finding Live Hosts on a Network

Related Posts