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-Agent、Referer,模拟浏览器请求,避免被WAF识别为扫描流量。

五、安全合规与避坑指南

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

合规核心要点

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

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

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

六、总结

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

相关推荐
白帽攻防录5 小时前
SRC 挖洞:Apache APISIX 非规范路径授权绕过深度复盘,CVE-2026-94269 怎么绕过网关访问后端
网络安全·apache·src漏洞挖掘
学逆向的15 小时前
win32注入代码
开发语言·网络安全·api·win32
Web Security Loop16 小时前
MAC安装最新Burpsuite pro详细步骤教程
web安全·macos·网络安全·安全性测试
墨云安全1 天前
红队评估和渗透测试的区别是什么?2026 年从目标、方法到交付物的对比
网络·安全·web安全·网络安全·渗透测试
刘婉晴1 天前
【APP安全测试】Frida 基础
网络安全
科技象限1 天前
数据防泄密系统怎么选?看它怎么把几道防线叠成网
运维·安全·网络安全·安企神
sbjdhjd1 天前
云安全 | 从特权容器到旧版 runC:Docker 宿主机访问与运行时缺陷复盘
运维·网络安全·docker·云原生·kubernetes·云计算·云安全
sbjdhjd2 天前
云安全 | Docker 容器逃逸复盘(三):docker.sock 挂载与 Docker-in-Docker 风险
经验分享·网络安全·docker·云原生·容器·kubernetes·云安全
Dachui_11222 天前
内网穿透如何限制访问地区?ZeroNews Geo Location 区域访问控制实践
运维·网络安全·内网穿透·访问控制·远程办公·api安全·ip白名单
HackTwoHub2 天前
DeepSeek Harness 红队破甲插件|适配 SRC 挖掘场景,区分平台拦截与模型原生输出,用于大模型安全边界合规测评研究
安全·web安全·网络安全·系统安全·密码学·网络攻击模型·安全架构