Nmap作为网络安全领域的"瑞士军刀",内置脚本库虽能覆盖大部分通用场景,但面对个性化需求(如专属业务系统探测、自定义漏洞验证)时,内置脚本难免捉襟见肘。而Nmap脚本引擎(NSE)提供的自定义能力,正是解锁其终极潜力的关键------基于Lua语言编写专属脚本,可精准适配业务场景,实现"按需扫描"。本文从基础规范入手,结合2个实战实例,从零掌握NSE脚本编写与落地,同时规避系统适配类常见错误。
一、NSE脚本核心基础:先搞懂这些规范
NSE脚本基于Lua语言开发,遵循固定框架和API,无需深入Lua语法,掌握核心规范即可上手。核心要点如下,是编写脚本的"必修课"。
1. 脚本文件与存放路径(跨系统适配)
-
文件后缀:必须为
.nse(如http-openresty-check.nse),Nmap仅识别该后缀脚本,且脚本名需严格区分大小写; -
默认路径(系统差异): - Linux/Mac:
/usr/share/nmap/scripts/,自定义脚本放入此目录后,需执行sudo nmap --script-updatedb更新缓存; - Windows:以路径D:\tool\nmap\为例,默认脚本目录为D:\tool\nmap\scripts\,放入脚本后执行D:\tool\nmap\nmap.exe --script-updatedb更新缓存; -
自定义路径(无系统差异,替代`--script-path`): 若不想污染默认目录,直接在调用命令中指定脚本完整路径,无需更新缓存,格式如下: - Linux/Mac:
nmap --script /自定义路径/http-openresty-lualib.nse -p 80 目标IP; - Windows:D:\tool\nmap\nmap.exe --script D:\自定义路径\http-openresty-lualib.nse -p 80 目标IP; 注意:Windows路径用反斜杠`\`,Linux/Mac用正斜杠`/`,路径含空格需加英文引号。
2. 脚本核心结构(三要素)
任何NSE脚本都包含「元数据定义」「触发规则」「核心逻辑」三部分,缺一不可,结构如下:
-- 1. 元数据定义(必填,用于Nmap识别脚本信息) description = [[脚本功能描述,说明脚本用途、探测逻辑]] author = "作者名称" license = "许可协议(通常与Nmap一致,填写Same as Nmap)" categories = {"分类标签"} -- 如discovery(信息收集)、vuln(漏洞探测)、safe(安全无侵入) -- 2. 触发规则(portrule/hostrule,二选一) -- portrule:基于端口触发(最常用,如仅对80/443端口生效) portrule = function(host, port) -- 逻辑判断,返回true则触发脚本,false则跳过 return port.number == 80 and port.service == "http" end -- hostrule:基于主机触发(如仅对特定IP段生效,较少用) -- 3. 核心逻辑(action函数,脚本执行的核心代码) action = function(host, port) -- 业务逻辑代码(如发送HTTP请求、解析响应、输出结果) return "扫描结果输出" end
3. 常用NSE API(实战高频)
Nmap封装了大量内置API,无需手动实现底层逻辑,以下是Web探测场景高频API,覆盖大部分实战需求:
|-------------------------------------------|------------------------|----------------|
| API名称 | 功能描述 | 使用场景 |
| http.get(host, port, path, opts) | 发送HTTP GET请求,返回响应对象 | 探测Web目录、文件是否存在 |
| http.post(host, port, path, opts, data) | 发送HTTP POST请求,支持提交表单数据 | 弱口令爆破、表单验证 |
| stdnse.printf(fmt, ...) | 格式化输出结果,支持变量拼接 | 自定义扫描结果展示格式 |
| stdnse.log_debug(msg) | 输出调试信息,需开启-d调试模式 | 脚本调试、排查逻辑错误 |
二、实战实例1:OpenResty响应头与默认页面版本泄露探测脚本
OpenResty默认配置下,可能通过HTTP响应头(Server字段)、默认欢迎页泄露版本信息。此脚本精准探测这两类场景,适配未关闭server_tokens的常见配置漏洞,属于无侵入式信息收集。
1. 需求与逻辑梳理
-
触发条件:仅对80/443端口生效,服务为HTTP/HTTPS;
-
探测逻辑:① 读取响应头Server字段,匹配OpenResty/Nginx版本格式;② 访问默认页面(/、/index.html),提取页面中隐藏的版本信息;
-
结果输出:区分"明确泄露""疑似泄露""无泄露"三种状态,标注风险等级。
2. 完整脚本代码(含注释)
创建脚本文件http-openresty-version-leak.nse,代码如下:
description = [[
Detect OpenResty version leakage via HTTP response headers and default pages.
Optimized for Nmap compatibility with clear risk indicators.
]]
author = "Security Engineer"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
-- 纯文本风险标识(Nmap兼容)
local HIGH_RISK = "!!! HIGH RISK !!!"
local MEDIUM_RISK = "!! MEDIUM RISK !!"
local LOW_RISK = "LOW RISK"
local SAFE = "SAFE"
local INFO = "INFO"
local WARNING = "WARNING"
-- 创建视觉分隔线
local function separator(length)
return string.rep("=", length or 50)
end
-- 格式化输出为易读的文本
local function format_risk(risk_level, message)
return string.format("%-20s %s", "[" .. risk_level .. "]", message)
end
portrule = function(host, port)
return true
end
action = function(host, port)
local result = {}
local target_port = port.number
-- 获取脚本参数
local port_arg = stdnse.get_script_args("port")
local ignore_service = stdnse.get_script_args("ignore_service") == "true"
-- 默认扫描80和443端口
local default_ports = {[80]=true, [443]=true}
local custom_ports = {}
if port_arg then
for p_str in port_arg:gmatch("[^,]+") do
local p = tonumber(p_str:match("%d+"))
if p then
custom_ports[p] = true
end
end
end
-- 决定是否扫描此端口
local should_scan = false
if port_arg then
should_scan = custom_ports[target_port] and (target_port == 80 or target_port == 443 or ignore_service)
else
should_scan = default_ports[target_port] or ignore_service
end
if not should_scan then
return nil
end
-- 添加标题
table.insert(result, separator(60))
table.insert(result, string.format("OpenResty Version Detection - Port %d", target_port))
table.insert(result, separator(60))
-- 收集结果的数据结构
local findings = {
headers = {}, -- header中发现的版本
bodies = {}, -- body中发现的版本
paths_tested = 0
}
-- 测试的路径
local test_paths = {
{path = "/", name = "Root"},
{path = "/index.html", name = "Index"},
{path = "/index.php", name = "PHP Index"},
{path = "/nginx-status", name = "Nginx Status"},
{path = "/status", name = "Status"},
{path = "/server-status", name = "Server Status"}
}
-- 执行请求
for _, path_info in ipairs(test_paths) do
local response = http.get(host, port, path_info.path, {timeout=3000})
if response and response.status then
findings.paths_tested = findings.paths_tested + 1
-- 检查响应头
local server_header = response.header["server"] or response.header["Server"] or ""
if server_header ~= "" then
local openresty_ver = server_header:match("openresty/([%d%.]+)")
local nginx_ver = server_header:match("nginx/([%d%.]+)")
if openresty_ver then
findings.headers[openresty_ver] = findings.headers[openresty_ver] or {count=0, paths={}}
findings.headers[openresty_ver].count = findings.headers[openresty_ver].count + 1
table.insert(findings.headers[openresty_ver].paths, path_info.name)
elseif nginx_ver then
local key = "nginx:" .. nginx_ver
findings.headers[key] = findings.headers[key] or {count=0, paths={}}
findings.headers[key].count = findings.headers[key].count + 1
table.insert(findings.headers[key].paths, path_info.name)
end
end
-- 检查响应体
if response.body and #response.body > 0 then
local body_openresty = response.body:match("openresty/([%d%.]+)")
local body_nginx = response.body:match("nginx/([%d%.]+)")
if body_openresty then
findings.bodies[body_openresty] = findings.bodies[body_openresty] or {count=0, paths={}}
findings.bodies[body_openresty].count = findings.bodies[body_openresty].count + 1
table.insert(findings.bodies[body_openresty].paths, path_info.name)
elseif body_nginx then
local key = "nginx:" .. body_nginx
findings.bodies[key] = findings.bodies[key] or {count=0, paths={}}
findings.bodies[key].count = findings.bodies[key].count + 1
table.insert(findings.bodies[key].paths, path_info.name)
end
end
end
end
-- 显示统计信息
table.insert(result, format_risk(INFO, string.format("Tested %d paths on port %d", findings.paths_tested, target_port)))
table.insert(result, "")
if findings.paths_tested == 0 then
table.insert(result, format_risk(WARNING, "No responses received"))
table.insert(result, separator(60))
return stdnse.format_output(true, result)
end
-- 显示header中的发现
if not next(findings.headers) then
table.insert(result, format_risk(SAFE, "No version information in response headers"))
else
table.insert(result, "Response Headers Analysis:")
for version, data in pairs(findings.headers) do
if version:match("^%d") then -- OpenResty版本
table.insert(result, format_risk(HIGH_RISK,
string.format("OpenResty v%s found in %d paths", version, data.count)))
if #data.paths > 0 then
table.insert(result, string.format(" Paths: %s", table.concat(data.paths, ", ")))
end
else -- Nginx版本
local nginx_ver = version:match("nginx:(.+)")
table.insert(result, format_risk(MEDIUM_RISK,
string.format("Nginx v%s found in %d paths", nginx_ver, data.count)))
end
end
end
table.insert(result, "")
-- 显示body中的发现
if not next(findings.bodies) then
table.insert(result, format_risk(SAFE, "No version information in response bodies"))
else
table.insert(result, "Response Bodies Analysis:")
for version, data in pairs(findings.bodies) do
if version:match("^%d") then -- OpenResty版本
table.insert(result, format_risk(HIGH_RISK,
string.format("OpenResty v%s found in %d paths", version, data.count)))
if #data.paths > 0 then
table.insert(result, string.format(" Paths: %s", table.concat(data.paths, ", ")))
end
else -- Nginx版本
local nginx_ver = version:match("nginx:(.+)")
table.insert(result, format_risk(MEDIUM_RISK,
string.format("Nginx v%s found in %d paths", nginx_ver, data.count)))
end
end
end
table.insert(result, "")
-- 版本不一致警告
local version_count = 0
for _ in pairs(findings.headers) do version_count = version_count + 1 end
for _ in pairs(findings.bodies) do version_count = version_count + 1 end
if version_count > 1 then
table.insert(result, format_risk(WARNING,
string.format("Multiple different versions detected (%d total)", version_count)))
end
-- 漏洞统计
local vulnerability_count = 0
for version, _ in pairs(findings.headers) do
if version:match("^%d") then vulnerability_count = vulnerability_count + 1 end
end
for version, _ in pairs(findings.bodies) do
if version:match("^%d") then vulnerability_count = vulnerability_count + 1 end
end
table.insert(result, "")
table.insert(result, separator(40))
if vulnerability_count > 0 then
table.insert(result, string.format("VULNERABILITIES FOUND: %d", vulnerability_count))
table.insert(result, "")
table.insert(result, "Recommended Actions:")
table.insert(result, " 1. Hide Server header in nginx.conf:")
table.insert(result, " Add: server_tokens off;")
table.insert(result, " 2. Remove version info from default pages")
table.insert(result, " 3. Upgrade to latest OpenResty version")
if target_port ~= 80 and target_port ~= 443 then
table.insert(result, " 4. Consider using standard ports (80/443)")
end
else
table.insert(result, "NO VULNERABILITIES FOUND")
table.insert(result, "")
table.insert(result, "Status: Secure - No version information leaked")
end
table.insert(result, separator(60))
return stdnse.format_output(true, result)
end
3. 脚本部署与调用(跨系统适配)
Linux/Mac系统
-
部署脚本:复制至默认目录并更新缓存:
sudo cp http-openresty-version-leak.nse /usr/share/nmap/scripts/ ``sudo chmod 644 /usr/share/nmap/scripts/http-openresty-version-leak.nse ``sudo nmap --script-updatedb -
调用脚本:
nmap --script http-openresty-version-leak -p 80,443 192.168.1.100
Windows系统(以D:\tool\nmap为例)
-
部署脚本:复制脚本到
D:\tool\nmap\scripts\,管理员CMD执行更新缓存:D:\tool\nmap\nmap.exe --script-updatedb -
调用脚本:
nmap -p 8888,443 --script "D:\\tool\\nmap\\sc\\http-openresty-version-leak.nse" --script-args ignore_service=true 192.168.1.100
4. 扫描结果解析
三、实战实例2:OpenResty错误页面版本泄露探测脚本
部分OpenResty服务器虽隐藏了正常请求的版本信息,但在错误页面(如404、500页面)仍会泄露详细版本。此脚本通过构造无效路径触发错误,探测这类隐蔽泄露场景。
1. 需求与逻辑梳理
-
触发条件:80/443端口且服务为HTTP/HTTPS;
-
探测逻辑:构造随机无效路径(避免缓存干扰),触发404错误,解析错误页面HTML源码,匹配OpenResty/Nginx版本格式;
-
结果输出:明确错误页面是否泄露版本,同时给出防护建议(对应
server_tokens off配置)。
2. 完整脚本代码(含注释)
创建脚本文件http-openresty-error-version.nse,代码如下:
description = [[
Detect OpenResty error page information leakage via various methods.
Modified to scan all TCP ports with HTTP-based testing.
]]
author = "Security Engineer"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe", "vuln"}
local http = require "http"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local shortport = require "shortport"
local nmap = require "nmap"
-- 纯文本风险标识(Nmap兼容)
local CRITICAL_RISK = "!!! CRITICAL !!!"
local HIGH_RISK = "!!! HIGH !!!"
local MEDIUM_RISK = "!! MEDIUM !!"
local LOW_RISK = "[LOW RISK]"
local INFO = "[INFO]"
local WARNING = "[WARNING]"
-- 创建视觉分隔线
local function separator(length, char)
return string.rep(char or "=", length or 60)
end
-- 格式化输出为易读的文本
local function format_risk(risk_level, message)
return string.format("%-15s %s", risk_level, message)
end
-- 检查响应中是否包含敏感信息
local function check_sensitive_info(response, pattern_name, pattern, risk_level)
if response.body and response.body:match(pattern) then
return true, string.format("Found %s in response body", pattern_name)
end
return false, nil
end
-- 提取匹配到的敏感信息片段
local function extract_snippet(text, pattern, context_length)
local start_pos = text:find(pattern)
if not start_pos then return nil end
context_length = context_length or 50
local snippet_start = math.max(1, start_pos - context_length)
local snippet_end = math.min(#text, start_pos + context_length + #(text:match(pattern) or ""))
local snippet = text:sub(snippet_start, snippet_end)
snippet = snippet:gsub("[\n\r\t]", " ")
if snippet_start > 1 then
snippet = "..." .. snippet
end
if snippet_end < #text then
snippet = snippet .. "..."
end
return snippet
end
-- 修改portrule以检查所有TCP端口,但支持参数过滤
portrule = function(host, port)
-- 获取脚本参数
local port_arg = stdnse.get_script_args("port")
local force_scan = stdnse.get_script_args("force") == "true"
-- 如果指定了端口参数,只扫描指定端口
if port_arg then
local ports = {}
for p_str in port_arg:gmatch("[^,]+") do
local p = tonumber(p_str:match("%d+"))
if p and p == port.number then
return true
end
end
end
-- 强制扫描所有开放的TCP端口
if force_scan then
return port.state == "open" and port.protocol == "tcp"
end
-- 默认扫描常见的HTTP端口
local http_ports = {
[80] = true, [443] = true, [8080] = true, [8888] = true,
[8443] = true, [9443] = true, [8081] = true, [8000] = true,
[8008] = true, [8088] = true, [8880] = true, [9080] = true
}
-- 如果端口是常见HTTP端口,则扫描
if http_ports[port.number] then
return true
end
-- 如果端口被识别为HTTP服务,也扫描
local service = port.service
if service and (service == "http" or service == "https" or service == "http-proxy") then
return true
end
return false
end
action = function(host, port)
local result = {}
local target_port = port.number
local critical_findings = 0
local high_findings = 0
local medium_findings = 0
-- 检查端口是否应该被扫描
local skip_scan = stdnse.get_script_args("skip_port_" .. target_port)
if skip_scan then
return nil
end
-- 添加标题
table.insert(result, separator(70, "#"))
table.insert(result, format_risk("SCAN START",
string.format("OpenResty Error Page Detection - Port %d", target_port)))
table.insert(result, separator(70, "#"))
table.insert(result, "")
-- 记录初始状态
table.insert(result, format_risk(INFO,
string.format("Port %d - Service: %s, State: %s",
target_port, port.service or "unknown", port.state or "unknown")))
table.insert(result, "")
-- 首先发送一个简单的HTTP请求来测试端口是否响应HTTP
table.insert(result, separator(40, "-"))
table.insert(result, format_risk(INFO, "Initial HTTP connection test"))
local test_response = http.get(host, port, "/", {timeout=3000})
if not test_response then
table.insert(result, format_risk(WARNING, "No HTTP response received"))
table.insert(result, "")
table.insert(result, "Trying advanced detection methods...")
table.insert(result, "")
else
table.insert(result, format_risk(INFO,
string.format("HTTP response: %d %s",
test_response.status, test_response.status_line or "")))
-- 检查初始响应中的服务器头
local server_header = test_response.header["server"] or test_response.header["Server"] or ""
if server_header ~= "" then
table.insert(result, format_risk(INFO,
string.format("Server header: %s", server_header)))
end
end
-- 测试路径和触发方法
local test_cases = {
{
name = "Invalid Lua Script",
path = "/?test=<?php echo 'test'; ?>",
method = "GET",
check_patterns = {
{pattern = "attempt to call global", name = "Lua stack trace", risk = CRITICAL_RISK},
{pattern = "no file", name = "File not found error", risk = HIGH_RISK},
{pattern = "content_by_lua", name = "Lua handler info", risk = HIGH_RISK},
}
},
{
name = "Lua Code Injection",
path = "/?test={{2*2}}",
method = "GET",
check_patterns = {
{pattern = "eval:", name = "Lua eval trace", risk = CRITICAL_RISK},
{pattern = "loadstring", name = "Lua loadstring", risk = CRITICAL_RISK},
{pattern = "syntax error", name = "Lua syntax error", risk = HIGH_RISK},
}
},
{
name = "Directory Traversal",
path = "/../../../../etc/passwd",
method = "GET",
check_patterns = {
{pattern = "root:", name = "System file content", risk = CRITICAL_RISK},
{pattern = "/etc/", name = "Internal path disclosure", risk = HIGH_RISK},
{pattern = "no such file", name = "File system error", risk = MEDIUM_RISK},
}
},
{
name = "Malformed Request",
path = "/",
method = "INVALIDMETHOD",
check_patterns = {
{pattern = "405 Not Allowed", name = "Method not allowed", risk = LOW_RISK},
{pattern = "openresty", name = "OpenResty in error", risk = HIGH_RISK},
{pattern = "nginx", name = "Nginx in error", risk = MEDIUM_RISK},
}
},
{
name = "Large Header Attack",
path = "/",
method = "GET",
headers = {["X-Large-Header"] = string.rep("A", 4096)},
check_patterns = {
{pattern = "request headers too large", name = "Buffer overflow error", risk = MEDIUM_RISK},
{pattern = "400 Bad Request", name = "Bad request error", risk = LOW_RISK},
}
},
{
name = "Lua Module Path",
path = "/test.lua",
method = "GET",
check_patterns = {
{pattern = "lua/", name = "Lua module path", risk = HIGH_RISK},
{pattern = "package%.path", name = "Lua package path", risk = CRITICAL_RISK},
{pattern = "module '.*' not found", name = "Module not found", risk = HIGH_RISK},
}
}
}
local total_tests = #test_cases
local tests_completed = 0
local tests_with_errors = 0
local tests_with_http_response = 0
-- 执行测试用例
for i, test in ipairs(test_cases) do
table.insert(result, separator(50, "-"))
table.insert(result, format_risk(INFO,
string.format("Test %d/%d: %s (%s %s)",
i, total_tests, test.name, test.method, test.path)))
local response
if test.method == "POST" and test.body then
response = http.post(host, port, test.path, {timeout=3000}, nil, test.body, test.headers)
else
response = http.generic_request(host, port, test.method, test.path, {timeout=3000, header=test.headers})
end
if response and response.status then
tests_completed = tests_completed + 1
tests_with_http_response = tests_with_http_response + 1
-- 检查是否是错误响应
local is_error_response = (response.status >= 400 and response.status < 600)
if is_error_response then
tests_with_errors = tests_with_errors + 1
-- 检查响应头
local server_header = response.header["server"] or response.header["Server"] or ""
if server_header ~= "" then
local openresty_ver = server_header:match("openresty/([%d%.]+)")
if openresty_ver then
table.insert(result, format_risk(CRITICAL_RISK,
string.format("Version leaked in header: OpenResty/%s", openresty_ver)))
critical_findings = critical_findings + 1
end
end
-- 检查响应体
if response.body and #response.body > 0 then
-- 检查每个模式
for _, pattern_info in ipairs(test.check_patterns) do
local match = response.body:match(pattern_info.pattern)
if match then
local snippet = extract_snippet(response.body, pattern_info.pattern)
local risk = pattern_info.risk
table.insert(result, format_risk(risk,
string.format("%s detected", pattern_info.name)))
if snippet then
table.insert(result, string.format(" Excerpt: %s", snippet))
end
-- 统计风险等级
if risk == CRITICAL_RISK then
critical_findings = critical_findings + 1
elseif risk == HIGH_RISK then
high_findings = high_findings + 1
elseif risk == MEDIUM_RISK then
medium_findings = medium_findings + 1
end
end
end
end
else
table.insert(result, format_risk(INFO,
string.format("Normal response: %d", response.status)))
end
else
table.insert(result, format_risk(WARNING, "No response received"))
end
end
-- 统计信息
table.insert(result, "")
table.insert(result, separator(60, "="))
table.insert(result, format_risk("SUMMARY", "Port " .. target_port .. " Results"))
table.insert(result, separator(60, "="))
table.insert(result, string.format("Total tests attempted: %d", total_tests))
table.insert(result, string.format("Tests with HTTP response: %d", tests_with_http_response))
table.insert(result, string.format("Error responses received: %d", tests_with_errors))
table.insert(result, "")
-- 只显示有发现的端口
if critical_findings > 0 or high_findings > 0 or medium_findings > 0 then
table.insert(result, "Security Findings:")
table.insert(result, string.format(" Critical findings: %d", critical_findings))
table.insert(result, string.format(" High findings: %d", high_findings))
table.insert(result, string.format(" Medium findings: %d", medium_findings))
table.insert(result, "")
if critical_findings > 0 then
table.insert(result, format_risk(CRITICAL_RISK,
"CRITICAL: Error pages leaking sensitive information!"))
end
elseif tests_with_http_response == 0 then
table.insert(result, format_risk(WARNING,
"No HTTP responses received. Port may not be an HTTP service."))
else
table.insert(result, format_risk(INFO,
"No version information leakage detected"))
end
table.insert(result, "")
table.insert(result, separator(70, "#"))
table.insert(result, format_risk("SCAN END",
string.format("OpenResty Error Page Analysis - Port %d", target_port)))
table.insert(result, separator(70, "#"))
-- 只在有发现或强制显示时返回结果
if critical_findings > 0 or high_findings > 0 or medium_findings > 0 or
stdnse.get_script_args("verbose") == "true" then
return stdnse.format_output(true, result)
end
return nil
end
3. 脚本部署与调用(跨系统适配)
Linux/Mac系统
-
部署脚本:同前序实例,复制至默认目录并更新缓存;
-
调用脚本:
nmap --script http-openresty-error-version -p 80,443 192.168.1.100
Windows系统(以D:\tool\nmap为例)
-
部署脚本:复制脚本到
D:\tool\nmap\scripts\,更新缓存; -
调用脚本:
nmap -p 8888,443 --script "D:\\tool\\nmap\\sc\\http-openresty-error-version.nse" --script-args port=8888,443 192.168.1.100
4. 扫描结果解析
四、NSE脚本调试与优化技巧
编写脚本难免出现逻辑错误、请求异常等问题,掌握以下调试技巧,可大幅提升排错效率,同时规避前文"选项不识别""路径错误"等问题。
1. 开启调试模式(跨系统通用)
执行脚本时加-d(基础调试)或-dd(深度调试),查看脚本执行过程、API调用细节,快速定位"选项不识别""路径错误"等问题:
# Linux/Mac nmap --script http-openresty-lualib -p 80 192.168.1.100 -d # Windows D:\tool\nmap\nmap.exe --script http-openresty-lualib -p 80 192.168.1.100 -d
调试信息会展示请求参数、响应内容、变量值,若提示"脚本未找到",优先检查脚本路径是否正确、脚本名是否拼写一致。
2. 性能与隐蔽性优化
-
控制请求频率:避免高频请求触发防火墙拦截,可通过
stdnse.sleep(0.5)设置0.5秒间隔; -
复用连接:使用
http.pipeline批量发送请求,减少TCP连接建立次数; -
伪装请求头:添加
User-Agent、Referer,模拟浏览器请求,避免被WAF识别为扫描流量。
五、安全合规与避坑指南
自定义NSE脚本能力强大,但需坚守合规底线,避免触碰法律红线,同时避开常见坑点。
合规核心要点
-
必须获得目标所有者「明确书面授权」,未经授权的扫描、爆破属于违法违规行为,需承担法律责任;
-
爆破类、漏洞利用类脚本仅用于授权测试,禁止用于非法攻击;
-
留存扫描日志,以备后续核查,避免纠纷。
六、总结
Nmap自定义NSE脚本的核心价值,在于「精准适配场景」------当内置脚本无法满足业务需求(如OpenResty专属探测、自定义认证爆破)时,通过简单的Lua语法和NSE API,即可打造专属扫描工具。