Nmap’s scripting engine (NSE) lets you automate all sorts of tasks, and writing your own scripts is surprisingly accessible.
Here’s a live example of an Nmap scan using a custom NSE script. Imagine we want to check for a specific banner on a web server.
# First, let's create a simple NSE script named 'check_banner.nse'
# This script will connect to a given port and check if the banner contains 'Apache'
cat << EOF > check_banner.nse
description = [[
Checks if a service banner contains a specific string.
]]
author = "Your Name"
license = "Nmap Public License"
categories = {"discovery", "safe"}
-- Define the string to search for
local search_string = "Apache"
-- This function is called for each host and port
function run(host, port)
-- Check if the port is open
if port.state ~= "open" then
return
end
-- Attempt to connect to the service
local socket = nmap.new_socket()
local status, err = socket:connect(host.ip, port.number)
if not status then
-- If connection fails, return the error
return err
end
-- Receive data from the socket (this is the banner)
local data, err = socket:receive_buf(1024) -- Receive up to 1024 bytes
socket:close()
if not data then
return err
end
-- Check if the received data contains our search string
if string.find(data, search_string, 1, true) then
return "Banner contains '" .. search_string .. "'"
end
return
end
EOF
# Now, let's run Nmap with our custom script.
# We'll target a known Apache server (replace with your target IP)
# and run the script against the HTTP port (80).
# Make sure you have nmap installed.
# Save the script above as 'check_banner.nse' in your Nmap scripts directory
# (e.g., /usr/local/share/nmap/scripts/ or ~/.nmap/scripts/)
# Example command:
# nmap -p 80 --script ./check_banner.nse <TARGET_IP>
# For demonstration purposes, let's simulate a successful scan output.
# If you run this against a server with 'Apache' in its banner on port 80,
# you'll see output like this in the 'interesting probes' section:
#
# Interesting results:
# Host script results:
# | check_banner: Banner contains 'Apache'
# |_ Script output: Banner contains 'Apache'
The core idea behind NSE is to extend Nmap’s capabilities beyond simple port scanning and service detection. You can write scripts to perform vulnerability checks, gather more detailed information about services, automate reconnaissance, and much more. NSE scripts are written in Lua, a lightweight, embeddable scripting language.
When Nmap runs, it loads its NSE scripts. For each host and port it probes, it iterates through the enabled scripts. Each script has a run function that receives information about the host and port. Inside run, a script can perform network operations like connecting to a port, sending and receiving data, or even interacting with other Nmap scripts. The script then returns data, which Nmap displays to you.
The description, author, license, and categories fields are metadata for the script. The categories are important for controlling which scripts run by default (e.g., --script default or --script vuln).
The run function is where the magic happens. It gets host and port objects, which contain details like the IP address (host.ip), port number (port.number), and port state (port.state). Inside run, you can use Nmap’s built-in Lua library functions to interact with the network. nmap.new_socket() creates a new socket, socket:connect() establishes a connection, and socket:receive_buf() reads data.
The crucial part is how you decide what to return. If the script finds something interesting, it returns a string that Nmap will display. If it finds nothing or encounters an error it can handle gracefully, it can return nil or an error message.
The most surprising true thing about NSE is that its powerful network interaction capabilities are exposed through a relatively simple Lua API, making complex network tasks achievable with concise code. You’re not just telling Nmap what to scan, but how to interpret the results and what actions to take based on them, all within the same tool.
A common pitfall is assuming that socket:receive() will always get the entire banner in one go. Network protocols often send data in chunks. If your script relies on receiving a specific, large banner immediately after connecting, it might fail if the server sends it in multiple packets. You often need to read data in a loop or use functions like receive_buf with a sufficiently large buffer, or even implement more complex protocol-aware logic to ensure you’ve received all the expected data before parsing it.
The next concept to explore is how to make your scripts more dynamic by accepting arguments, allowing users to customize their behavior without editing the script itself.