Nmap自定义NSE脚本实战:从入门到落地

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系统
  1. 部署脚本:复制至默认目录并更新缓存: 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

  2. 调用脚本: nmap --script http-openresty-version-leak -p 80,443 192.168.1.100

Windows系统(以D:\tool\nmap为例)
  1. 部署脚本:复制脚本到D:\tool\nmap\scripts\,管理员CMD执行更新缓存: D:\tool\nmap\nmap.exe --script-updatedb

  2. 调用脚本: 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系统
  1. 部署脚本:同前序实例,复制至默认目录并更新缓存;

  2. 调用脚本: nmap --script http-openresty-error-version -p 80,443 192.168.1.100

Windows系统(以D:\tool\nmap为例)
  1. 部署脚本:复制脚本到D:\tool\nmap\scripts\,更新缓存;

  2. 调用脚本: 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-AgentReferer,模拟浏览器请求,避免被WAF识别为扫描流量。

五、安全合规与避坑指南

自定义NSE脚本能力强大,但需坚守合规底线,避免触碰法律红线,同时避开常见坑点。

合规核心要点

  • 必须获得目标所有者「明确书面授权」,未经授权的扫描、爆破属于违法违规行为,需承担法律责任;

  • 爆破类、漏洞利用类脚本仅用于授权测试,禁止用于非法攻击;

  • 留存扫描日志,以备后续核查,避免纠纷。

六、总结

Nmap自定义NSE脚本的核心价值,在于「精准适配场景」------当内置脚本无法满足业务需求(如OpenResty专属探测、自定义认证爆破)时,通过简单的Lua语法和NSE API,即可打造专属扫描工具。

相关推荐
heze092 小时前
sqli-labs-Less-26a
数据库·mysql·网络安全
内心如初15 小时前
17_等保系列之密评、关基安全检测评估与等级测评区别(无广)
网络安全·等保测评·等保测评从0-1·等保测评笔记
niaiheni1 天前
[深度技术] 绝境求生利用 Pdo\Sqlite 绕过 PHP disable_functions 限制
网络安全
啥都想学点1 天前
kali 基础介绍(Privilege Escalation、Defense Evasion)
安全·网络安全
浩浩测试一下1 天前
应急响应 > > > DDoS HTTP 应用层攻击研判溯源手法详解
安全·web安全·网络安全·系统安全·ddos·安全架构
BEGCCYD1 天前
kali最新版不显示鼠标
linux·网络安全
Whoami!1 天前
⓫⁄₆ ⟦ OSCP ⬖ 研记 ⟧ Windows权限提升 ➱ 自动化枚举
windows·网络安全·信息安全·自动化枚举·winpeas
中科固源1 天前
中科数测卫星网络及系统安全检测智能体亮相北京国际航天展
网络安全·卫星·商业航天·卫星安全