使用Perl编写Nmap脚本插件实战指南 从基础语法到高级扫描功能实现与常见错误排查

引言:Nmap脚本引擎与Perl的完美结合
Nmap(Network Mapper)作为网络发现和安全审计的瑞士军刀,其真正的威力在于其强大的脚本引擎(NSE)。虽然Lua是Nmap官方首选的脚本语言,但Perl凭借其强大的文本处理能力和丰富的系统集成经验,在Nmap脚本开发中仍然占有一席之地。本指南将带你从零开始,使用Perl编写高效的Nmap脚本插件,涵盖基础语法、高级扫描功能实现以及常见错误的排查技巧。
第一部分:Perl基础语法回顾与Nmap脚本环境搭建
1.1 Perl基础语法快速入门
在编写Nmap脚本之前,我们需要确保对Perl有基本的了解。Perl是一种解释型、动态编程语言,特别适合文本处理和系统管理任务。
变量与数据类型
#!/usr/bin/perl
use strict;
use warnings;

# 标量变量
my $hostname = “example.com”;
my $port = 80;

# 数组
my @services = (“http”, “https”, “ssh”, “ftp”);

# 哈希表(关联数组)
my %port_service = (
80 => “http”,
443 => “https”,
22 => “ssh”,
21 => “ftp”
);

# 打印输出
print “Hostname: $hostname\n”;
print “Port $port runs: $port_service{$port}\n”;
条件判断与循环
# if-else语句
if ($port > 1024) {
print “High port number\n”;
} elsif ($port == 80) {
print “Standard HTTP port\n”;
} else {
print “Other port\n”;
}

# for循环
foreach my $service (@services) {
print “Service: $service\n”;
}

# while循环
my $counter = 0;
while ($counter < 5) { print "Counter: $counter\n"; $counter++; } 正则表达式 my $text = "The server runs on port 8080"; if ($text =~ /port (\d+)/) { print "Found port: $1\n"; } 1.2 Nmap脚本引擎(NSE)基础 Nmap脚本使用Lua编写,但我们可以创建Perl脚本作为外部程序被Nmap调用。Nmap提供了多种方式来集成外部脚本: 通过 --script 参数调用外部程序 使用 nmap-service-probes 格式 创建Lua包装器调用Perl脚本 1.3 开发环境搭建 安装必要组件 # 确保已安装Nmap sudo apt-get install nmap # 确保已安装Perl sudo apt-get install perl # 安装必要的Perl模块 sudo cpan install Net::DNS sudo cpan install Net::Ping sudo cpan install IO::Socket::INET sudo cpan install LWP::UserAgent 创建第一个Nmap-Perl脚本模板 #!/usr/bin/perl use strict; use warnings; use IO::Socket::INET; # Nmap脚本标准输出格式 # 每行输出以"PORT"或"SERVICE"开头,后跟详细信息 # 获取命令行参数 my ($host, $port) = @ARGV; # 验证参数 unless ($host && $port) { print "Usage: $0 \n”;
exit 1;
}

# 主扫描逻辑
sub scan_port {
my ($target, $port_num) = @_;

my $socket = IO::Socket::INET->new(
PeerAddr => $target,
PeerPort => $port_num,
Proto => ‘tcp’,
Timeout => 5
);

if ($socket) {
close($socket);
return 1; # Port open
} else {
return 0; # Port closed
}
}

# 执行扫描
if (scan_port($host, $port)) {
print “PORT $port open\n”;
} else {
print “PORT $port closed\n”;
}
第二部分:Nmap脚本开发核心概念
2.1 Nmap脚本的生命周期
Nmap脚本遵循特定的执行流程:
规则匹配
:确定脚本何时运行
主机发现
:获取目标主机信息
端口扫描
:确定开放端口
服务版本探测
:识别运行的服务
脚本执行
:运行自定义逻辑
结果输出
:格式化输出结果
2.2 Nmap脚本输出格式规范
Nmap期望脚本输出遵循特定格式,以便正确解析和显示:
PORT STATE SERVICE
80/tcp open http
| my-custom-script:
| Additional information about the service
| More details here
|_ Final conclusion
2.3 创建Lua包装器调用Perl脚本
由于Nmap原生支持Lua,我们通常创建一个Lua包装器来调用Perl脚本:
— my-perl-script.nse
local nmap = require “nmap”
local stdnse = require “stdnse”
local string = require “string”

description = [[
使用Perl编写的自定义Nmap脚本
该脚本通过Lua包装器调用Perl程序进行高级扫描
]]

—
— @usage
— nmap –script my-perl-script
— @output
— PORT STATE SERVICE
— 80/tcp open http
— | my-perl-script:
— | Perl scan results
— |_ Additional info

local function run_perl_script(host, port)
— 构建Perl脚本路径
local perl_script = nmap.get_filename(“scripts/my-perl-scanner.pl”)

— 检查文件是否存在
if not nmap.file_exists(perl_script) then
return nil, “Perl script not found”
end

— 执行Perl脚本
local cmd = string.format(“perl %s %s %d”,
perl_script,
host.ip,
port.number)

local handler = io.popen(cmd)
if not handler then
return nil, “Failed to execute Perl script”
end

local output = handler:read(“*a”)
handler:close()

return output
end

portrule = function(host, port)
— 只在80端口运行
return port.number == 80 and port.state == “open”
end

action = function(host, port)
local status, output = run_perl_script(host, port)

if status then
return output
else
return output
end
end
2.4 直接调用Perl脚本作为外部脚本
Nmap也支持直接调用外部脚本:
# 创建可执行的Perl脚本
chmod +x my-perl-scanner.pl

# 在Nmap命令中直接调用
nmap –script-exec=”perl my-perl-scanner.pl” -p 80
第三部分:基础扫描功能实现
3.1 端口扫描基础
TCP连接扫描
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;

sub tcp_connect_scan {
my ($host, $port) = @_;

my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => 3
);

if ($socket) {
close($socket);
return 1;
}
return 0;
}

# 主程序
my ($host, $port) = @ARGV;
if (tcp_connect_scan($host, $port)) {
print “PORT $port OPEN\n”;
} else {
print “PORT $port CLOSED\n”;
}
SYN扫描(需要root权限)
#!/usr/bin/perl
use strict;
use warnings;
use Net::RawIP;

sub syn_scan {
my ($host, $port) = @_;

# 创建原始IP包
my $packet = Net::RawIP->new({
ip => {
saddr => ‘0.0.0.0’, # 源IP(自动填充)
daddr => $host, # 目标IP
},
tcp => {
source => int(rand(65535)) + 1, # 随机源端口
dest => $port, # 目标端口
syn => 1, # SYN标志
window => 1024, # 窗口大小
}
});

# 发送数据包
$packet->send;

# 接收响应(简化版,实际需要更复杂的处理)
# 这里仅作演示,实际应用需要实现完整的TCP握手模拟
return 1; # 简化返回
}

# 注意:此脚本需要root权限运行
# sudo perl syn_scan.pl 3.2 服务版本探测
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;

sub get_service_banner {
my ($host, $port) = @_;

my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => 5
);

unless ($socket) {
return “Connection failed”;
}

# 发送探测数据
my $probe = “HEAD / HTTP/1.0\r\n\r\n”;
$socket->send($probe);

# 接收响应
my $response = “”;
$socket->recv($response, 1024);
close($socket);

# 提取服务器信息
if ($response =~ /Server:\s*(.+)/i) {
return “Server: $1”;
}

return “No server banner found”;
}

# 使用示例
my ($host, $port) = @ARGV;
my $banner = get_service_banner($host, $port);
print “Banner: $banner\n”;
3.3 操作系统识别
#!/usr/bin/perl
use strict;
use warnings;
use Net::Packet;

sub detect_os {
my $host = shift;

# 发送TTL探测包
my $dp = Net::Packet::IPv4Layer->new(
dst => $host,
protocol => 1, # ICMP
);

# 实际实现需要发送TTL=1的ICMP包并分析响应
# 这里简化处理
return “Linux”; # 简化返回
}

# TTL探测原理:
# 不同操作系统使用不同的初始TTL值
# Windows: 128, Linux: 64, Cisco: 255
3.4 DNS枚举
#!/usr/bin/perl
use strict;
use warnings;
use Net::DNS;

sub dns_enum {
my $domain = shift;

my $resolver = Net::DNS::Resolver->new;

# 查询A记录
my $query = $resolver->query($domain, “A”);
if ($query) {
foreach my $rr ($query->answer) {
next unless $rr->type eq “A”;
print “A record: $domain -> ” . $rr->address . “\n”;
}
}

# 查询MX记录
$query = $resolver->query($domain, “MX”);
if ($query) {
foreach my $rr ($query->answer) {
next unless $rr->type eq “MX”;
print “MX record: ” . $rr->exchange . ” (priority ” . $rr->preference . “)\n”;
}
}

# 子域名爆破(简化版)
my @subdomains = (“www”, “mail”, “ftp”, “blog”, “api”);
foreach my $sub (@subdomains) {
my $full = “$sub.$domain”;
$query = $resolver->query($full, “A”);
if ($query) {
foreach my $rr ($query->answer) {
if ($rr->type eq “A”) {
print “Subdomain found: $full -> ” . $rr->address . “\n”;
}
}
}
}
}

# 使用示例
dns_enum(“example.com”);
第四部分:高级扫描功能实现
4.1 并发扫描与线程管理
#!/usr/bin/perl
use strict;
use warnings;
use threads;
use threads::shared;
use Thread::Queue;
use IO::Socket::INET;

# 共享变量用于统计
my $open_ports :shared = 0;
my $total_ports :shared = 0;

# 扫描单个端口的函数
sub scan_port {
my ($host, $port, $queue) = @_;

my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => 2
);

{
lock($total_ports);
$total_ports++;
}

if ($socket) {
close($socket);
{
lock($open_ports);
$open_ports++;
}
return $port;
}
return undef;
}

# 工作线程函数
sub worker {
my ($host, $queue) = @_;

while (my $port = $queue->dequeue()) {
my $result = scan_port($host, $port, $queue);
if ($result) {
print “PORT $result OPEN\n”;
}
}
}

# 主函数:并发端口扫描
sub concurrent_port_scan {
my ($host, $start_port, $end_port, $threads) = @_;

# 创建队列
my $queue = Thread::Queue->new();

# 填充端口队列
for my $port ($start_port..$end_port) {
$queue->enqueue($port);
}

# 创建工作线程
my @worker_threads;
for (1..$threads) {
push @worker_threads, threads->create(\&worker, $host, $queue);
}

# 等待所有线程完成
$_->join() for @worker_threads;

print “\nScan complete: $open_ports/$total_ports ports open\n”;
}

# 使用示例:扫描1-1000端口,使用10个线程
concurrent_port_scan(“scanme.nmap.org”, 1, 1000, 10);
4.2 HTTP安全扫描器
#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use URI;

sub http_security_scan {
my $url = shift;

my $ua = LWP::UserAgent->new(
timeout => 10,
agent => “MySecurityScanner/1.0”,
max_redirects => 3
);

my $uri = URI->new($url);
my $host = $uri->host;
my $port = $uri->port || 80;

print “Scanning $url\n”;
print “=” x 50 . “\n”;

# 1. 检查HTTPS支持
check_https_support($ua, $host, $port);

# 2. 检查安全头
check_security_headers($ua, $url);

# 3. 检查目录遍历
check_directory_traversal($ua, $url);

# 4. 检查常见漏洞文件
check_vulnerable_files($ua, $url);

# 5. 检查HTTP方法
check_http_methods($ua, $url);
}

sub check_https_support {
my ($ua, $host, $port) = @_;

my $https_url = “https://$host:$port”;
my $response = $ua->get($https_url);

if ($response->is_success) {
print “[+] HTTPS is supported\n”;
} else {
print “[-] HTTPS not available\n”;
}
}

sub check_security_headers {
my ($ua, $url) = @_;

my $response = $ua->get($url);
my @required_headers = (
‘X-Frame-Options’,
‘X-Content-Type-Options’,
‘Strict-Transport-Security’,
‘Content-Security-Policy’
);

print “\nSecurity Headers:\n”;
foreach my $header (@required_headers) {
if ($response->header($header)) {
print “[+] $header: ” . $response->header($header) . “\n”;
} else {
print “[-] $header: MISSING\n”;
}
}
}

sub check_directory_traversal {
my ($ua, $url) = @_;

my $test_paths = (
“../etc/passwd”,
“../windows/win.ini”,
“../../boot.ini”
);

print “\nDirectory Traversal Test:\n”;
foreach my $path (@test_paths) {
my $test_url = $url . “/” . $path;
my $response = $ua->get($test_url);

if ($response->is_success && $response->content =~ /root:/) {
print “[!] VULNERABLE: $test_url\n”;
}
}
}

sub check_vulnerable_files {
my ($ua, $url) = @_;

my @vulnerable_files = (
“phpinfo.php”,
“test.php”,
“info.php”,
“wp-config.php”,
“.env”,
“.git/config”
);

print “\nVulnerable Files Check:\n”;
foreach my $file (@vulnerable_files) {
my $test_url = $url . “/” . $file;
my $response = $ua->head($test_url);

if ($response->is_success) {
print “[!] Found: $test_url\n”;
}
}
}

sub check_http_methods {
my ($ua, $url) = @_;

my @methods = (‘GET’, ‘POST’, ‘PUT’, ‘DELETE’, ‘OPTIONS’, ‘TRACE’);

print “\nHTTP Methods:\n”;
foreach my $method (@methods) {
my $request = HTTP::Request->new($method, $url);
my $response = $ua->request($request);

if ($response->code != 405 && $response->code != 403) {
print “[+] $method: Allowed\n”;
} else {
print “[-] $method: Denied\n”;
}
}
}

# 使用示例
http_security_scan(“http://scanme.nmap.org”);
4.3 SNMP枚举工具
#!/usr/bin/perl
use strict;
use warnings;
use Net::SNMP;

sub snmp_enum {
my ($host, $community) = @_;

# 创建SNMP会话
my ($session, $error) = Net::SNMP->session(
-hostname => $host,
-community => $community,
-version => ‘2c’,
-timeout => 2,
-retries => 1
);

unless ($session) {
print “Error creating SNMP session: $error\n”;
return;
}

# 定义常用OID
my %oids = (
‘System Description’ => ‘1.3.6.1.2.1.1.1.0’,
‘Uptime’ => ‘1.3.6.1.2.1.1.3.0’,
‘Contact’ => ‘1.3.6.1.2.1.1.4.0’,
‘Name’ => ‘1.3.6.1.2.1.1.5.0’,
‘Location’ => ‘1.3.6.1.2.1.1.6.0’,
‘Services’ => ‘1.3.6.1.2.1.1.7.0’,
);

print “SNMP Enumeration on $host\n”;
print “=” x 40 . “\n”;

# 查询基本信息
foreach my $key (keys %oids) {
my $result = $session->get_request($oids{$key});
if ($result) {
print “$key: ” . $result->{$oids{$key}} . “\n”;
}
}

# 获取网络接口
get_network_interfaces($session);

# 获取运行进程(如果支持)
get_processes($session);

$session->close;
}

sub get_network_interfaces {
my $session = shift;

# IF-MIB::ifDescr
my $base_oid = ‘1.3.6.1.2.1.2.2.1.2’;
my $result = $session->get_table($base_oid);

if ($result) {
print “\nNetwork Interfaces:\n”;
foreach my $oid (keys %$result) {
print ” Interface: $result->{$oid}\n”;
}
}
}

sub get_processes {
my $session = shift;

# HOST-RESOURCES-MIB::hrSWRunName
my $base_oid = ‘1.3.6.1.2.1.25.4.2.1.2’;
my $result = $session->get_table($base_oid);

if ($result) {
print “\nRunning Processes:\n”;
my $count = 0;
foreach my $oid (keys %$result) {
print ” Process: $result->{$oid}\n”;
last if ++$count >= 10; # Limit output
}
}
}

# 使用示例
snmp_enum(“192.168.1.1”, “public”);
4.4 SMB/NetBIOS扫描
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;

sub smb_enum {
my $host = shift;

# SMB端口
my @smb_ports = (139, 445);

foreach my $port (@smb_ports) {
my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => 3
);

if ($socket) {
print “SMB port $port is open on $host\n”;

# 发送SMB会话请求(简化版)
# 实际实现需要完整的SMB协议握手
my $negotiate_request = pack(“H*”,
“00000054ff534d4272000000000000000000000000000000000000000000000000000″
);

$socket->send($negotiate_request);

my $response;
$socket->recv($response, 1024);

# 分析响应(简化)
if (length($response) > 0) {
print ” SMB service detected\n”;
# 进一步枚举信息…
}

close($socket);
}
}
}

# 使用示例
smb_enum(“192.168.1.100”);
第五部分:错误处理与调试技巧
5.1 常见错误类型及解决方案
1. 权限相关错误
#!/usr/bin/perl
use strict;
use warnings;

sub check_privileges {
# 检查是否为root用户
if ($< != 0) { warn "WARNING: This script requires root privileges for raw socket operations\n"; warn "Please run with: sudo perl $0\n"; return 0; } return 1; } # 在需要特权操作前检查 unless (check_privileges()) { exit 1; } 2. 网络连接超时处理 #!/usr/bin/perl use strict; use warnings; use IO::Socket::INET; sub robust_connect { my ($host, $port, $timeout) = @_; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm($timeout || 10); my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => $timeout || 10
);

alarm(0);

if ($socket) {
return $socket;
} else {
die “connection failed: $!\n”;
}
};

if ($@) {
if ($@ =~ /timeout/) {
print “Connection to $host:$port timed out\n”;
} else {
print “Connection error: $@”;
}
return undef;
}
}
3. 资源泄漏防护
#!/usr/bin/perl
use strict;
use warnings;

sub safe_socket_operation {
my $socket;

eval {
$socket = IO::Socket::INET->new(
PeerAddr => “example.com”,
PeerPort => 80,
Timeout => 5
);

unless ($socket) {
die “Failed to create socket: $!\n”;
}

# 执行操作
$socket->send(“GET / HTTP/1.0\r\n\r\n”);
my $response;
$socket->recv($response, 1024);

# 处理响应…

return $response;
};

# 确保资源被释放
if ($socket) {
close($socket);
}

if ($@) {
warn “Error during socket operation: $@”;
return undef;
}
}
5.2 调试技巧与日志记录
详细日志系统
#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw(strftime);

# 日志级别
use constant {
LOG_DEBUG => 0,
LOG_INFO => 1,
LOG_WARN => 2,
LOG_ERROR => 3,
};

my $log_level = LOG_INFO;

sub log_message {
my ($level, $message) = @_;

return if $level < $log_level; my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime); my $level_str = ""; given ($level) { when (LOG_DEBUG) { $level_str = "DEBUG"; } when (LOG_INFO) { $level_str = "INFO"; } when (LOG_WARN) { $level_str = "WARN"; } when (LOG_ERROR) { $level_str = "ERROR"; } } print STDERR "[$timestamp] [$level_str] $message\n"; } # 使用示例 sub example_scan { my $host = shift; log_message(LOG_INFO, "Starting scan on $host"); # 模拟连接 log_message(LOG_DEBUG, "Attempting connection to $host:80"); # 模拟错误 log_message(LOG_ERROR, "Connection refused on port 80"); log_message(LOG_INFO, "Scan completed"); } example_scan("example.com"); 调试模式与详细输出 #!/usr/bin/perl use strict; use warnings; use Data::Dumper; # 调试标志 my $DEBUG = $ENV{DEBUG} || 0; sub debug_print { return unless $DEBUG; my $data = shift; if (ref($data)) { print STDERR Dumper($data); } else { print STDERR "DEBUG: $data\n"; } } # 使用示例 sub complex_operation { my ($host, $port) = @_; debug_print("Host: $host, Port: $port"); my $result = { host => $host,
port => $port,
status => “open”,
details => {
banner => “Apache/2.4.41”,
ssl => 1,
vulns => [“CVE-2020-11984”]
}
};

debug_print($result);

return $result;
}

# 运行时设置调试模式:DEBUG=1 perl script.pl
5.3 性能优化技巧
连接池管理
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;

# 简单的连接池
my %connection_pool;

sub get_connection {
my ($host, $port) = @_;
my $key = “$host:$port”;

# 检查池中是否有可用连接
if (exists $connection_pool{$key} && $connection_pool{$key}{socket}) {
my $socket = $connection_pool{$key}{socket};
# 验证连接是否仍然有效
if ($socket->connected) {
return $socket;
} else {
delete $connection_pool{$key};
}
}

# 创建新连接
my $socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => ‘tcp’,
Timeout => 5
);

if ($socket) {
$connection_pool{$key} = {
socket => $socket,
last_used => time(),
};
}

return $socket;
}

sub cleanup_pool {
my $max_age = shift || 300; # 5分钟

my $now = time();
foreach my $key (keys %connection_pool) {
if ($now – $connection_pool{$key}{last_used} > $max_age) {
$connection_pool{$key}{socket}->close();
delete $connection_pool{$key};
}
}
}
内存优化
#!/usr/bin/perl
use strict;
use warnings;

# 避免在循环中创建不必要的变量
sub inefficient_scan {
my @ports = (1..1000);
my @results;

foreach my $port (@ports) {
my $result = { port => $port, status => “open” }; # 每次循环创建新哈希
push @results, $result;
}

return @results;
}

# 优化版本
sub efficient_scan {
my @ports = (1..1000);
my @results;

# 预分配数组
$#results = $#ports;

foreach my $i (0..$#ports) {
# 重用数据结构
$results[$i] = {
port => $ports[$i],
status => “open”
};
}

return @results;
}
5.4 错误排查流程图
脚本执行失败
│
├─→ 检查权限:是否需要root?
│ └─→ 是:使用sudo运行
│
├─→ 检查网络:目标是否可达?
│ └─→ 使用ping/traceroute测试
│
├─→ 检查语法:perl -c script.pl
│ └─→ 修复语法错误
│
├─→ 检查依赖:是否缺少模块?
│ └─→ cpan install Module::Name
│
├─→ 检查防火墙:端口是否被阻止?
│ └─→ 使用iptables/nftables检查
│
└─→ 启用调试:DEBUG=1 perl script.pl
第六部分:实际案例与最佳实践
6.1 完整的Nmap-Perl脚本示例
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
use Getopt::Long;
use Pod::Usage;

# 版本信息
our $VERSION = “1.0”;

# 配置选项
my %opts = (
host => ”,
ports => ‘1-1000’,
threads => 10,
timeout => 3,
verbose => 0,
help => 0,
man => 0,
);

# 解析命令行参数
GetOptions(
‘host=s’ => \$opts{host},
‘ports=s’ => \$opts{ports},
‘threads=i’ => \$opts{threads},
‘timeout=i’ => \$opts{timeout},
‘verbose’ => \$opts{verbose},
‘help’ => \$opts{help},
‘man’ => \$opts{man},
) or pod2usage(2);

pod2usage(1) if $opts{help};
pod2usage(-verbose => 2) if $opts{man};

unless ($opts{host}) {
print “Error: Host is required\n”;
pod2usage(2);
}

# 主扫描类
package PortScanner;

sub new {
my ($class, %args) = @_;
return bless {
host => $args{host},
ports => $args{ports},
threads => $args{threads},
timeout => $args{timeout},
verbose => $args{verbose},
results => [],
}, $class;
}

sub parse_ports {
my $self = shift;
my $port_spec = $self->{ports};

my @ports;

if ($port_spec =~ /,/) {
# 逗号分隔的端口
@ports = split(/,/, $port_spec);
} elsif ($port_spec =~ /(\d+)-(\d+)/) {
# 端口范围
@ports = ($1..$2);
} else {
# 单个端口
@ports = ($port_spec);
}

return @ports;
}

sub scan_port {
my ($self, $port) = @_;

my $start_time = time();

my $socket = IO::Socket::INET->new(
PeerAddr => $self->{host},
PeerPort => $port,
Proto => ‘tcp’,
Timeout => $self->{timeout}
);

my $duration = time() – $start_time;

if ($socket) {
close($socket);

my $result = {
port => $port,
state => ‘open’,
duration => $duration,
};

# 获取服务信息
$self->get_service_info($result);

push @{$self->{results}}, $result;

$self->log(“PORT $port OPEN ($duration s)”) if $self->{verbose};

return 1;
} else {
$self->log(“PORT $port CLOSED ($duration s)”) if $self->{verbose};
return 0;
}
}

sub get_service_info {
my ($self, $result) = @_;

my $socket = IO::Socket::INET->new(
PeerAddr => $self->{host},
PeerPort => $result->{port},
Proto => ‘tcp’,
Timeout => 2
);

return unless $socket;

# 尝试获取banner
eval {
local $SIG{ALRM} = sub { die “timeout\n” };
alarm(2);

my $banner;
$socket->recv($banner, 256);

if ($banner) {
$result->{banner} = $banner;
# 简单的服务识别
if ($banner =~ /HTTP/i) {
$result->{service} = “http”;
} elsif ($banner =~ /SSH/i) {
$result->{service} = “ssh”;
} elsif ($banner =~ /FTP/i) {
$result->{service} = “ftp”;
}
}

alarm(0);
};

close($socket);
}

sub log {
my ($self, $message) = @_;
print STDERR “$message\n”;
}

sub run {
my $self = shift;

$self->log(“Starting scan on $self->{host}”);

my @ports = $self->parse_ports();

# 简单的并发控制(实际项目中应使用线程池)
my $active = 0;
my @pids;

foreach my $port (@ports) {
# 等待子进程完成如果达到限制
while ($active >= $self->{threads}) {
my $pid = wait();
$active– if $pid > 0;
}

my $pid = fork();

if (!defined $pid) {
die “Cannot fork: $!”;
} elsif ($pid == 0) {
# 子进程
$self->scan_port($port);
exit 0;
} else {
# 父进程
$active++;
push @pids, $pid;
}
}

# 等待所有子进程完成
while ($active > 0) {
my $pid = wait();
$active– if $pid > 0;
}

$self->report();
}

sub report {
my $self = shift;

print “\nScan Report:\n”;
print “=” x 60 . “\n”;
printf “%-10s %-10s %-15s %s\n”, “PORT”, “STATE”, “SERVICE”, “BANNER”;
print “-” x 60 . “\n”;

foreach my $result (sort { $a->{port} <=> $b->{port} } @{$self->{results}}) {
my $service = $result->{service} || “unknown”;
my $banner = $result->{banner} || “”;
$banner = substr($banner, 0, 30) . “…” if length($banner) > 30;

printf “%-10d %-10s %-15s %s\n”,
$result->{port},
$result->{state},
$service,
$banner;
}

print “\nTotal open ports: ” . scalar(@{$self->{results}}) . “\n”;
}

# 主程序
package main;

my $scanner = PortScanner->new(
host => $opts{host},
ports => $opts{ports},
threads => $opts{threads},
timeout => $opts{timeout},
verbose => $opts{verbose},
);

$scanner->run();

__END__

=head1 NAME

advanced-port-scanner.pl – Advanced port scanner using Perl

=head1 SYNOPSIS

advanced-port-scanner.pl –host [options]

Options:
–host Target host (required)
–ports Port range (default: 1-1000)
–threads Number of concurrent threads (default: 10)
–timeout Connection timeout in seconds (default: 3)
–verbose Enable verbose output
–help Brief help message
–man Full documentation

=head1 DESCRIPTION

Advanced port scanner with service detection and reporting capabilities.

=cut
6.2 与Nmap集成的Lua包装器
— advanced-perl-scanner.nse
local nmap = require “nmap”
local stdnse = require “stdnse”
local string = require “string”
local table = require “table”

description = [[
Advanced Perl-based scanner for Nmap
Performs comprehensive port scanning and service detection
]]

—
— @usage
— nmap –script advanced-perl-scanner –script-args ports=1-1000
— @output
— PORT STATE SERVICE
— 22/tcp open ssh
— | advanced-perl-scanner:
— | Banner: SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3
— | Protocol: SSH-2
— | Encryption: aes128-ctr,aes192-ctr,aes256-ctr
— |_ MAC: umac-64@openssh.com

local function run_perl_scanner(host, port, args)
local perl_script = nmap.get_filename(“scripts/advanced-port-scanner.pl”)

if not nmap.file_exists(perl_script) then
return nil, “Perl script not found: ” .. perl_script
end

— 构建命令参数
local cmd = string.format(“perl %s –host %s –ports %d”,
perl_script,
host.ip,
port.number)

— 添加可选参数
if args.ports then
cmd = cmd .. ” –ports ” .. args.ports
end

if args.verbose then
cmd = cmd .. ” –verbose”
end

— 执行脚本
local handler = io.popen(cmd)
if not handler then
return nil, “Failed to execute Perl script”
end

local output = handler:read(“*a”)
local status = handler:close()

if not status then
return nil, “Perl script execution failed”
end

return output
end

portrule = function(host, port)
— 只在开放端口运行
return port.state == “open”
end

action = function(host, port)
local args = stdnse.get_script_args(SCRIPT_NAME) or {}

local status, output = run_perl_scanner(host, port, args)

if status then
return output
else
return output
end
end
6.3 最佳实践总结
1. 代码组织
模块化设计
:将功能拆分为独立的子程序
错误处理
:始终检查返回值和错误条件
资源管理
:确保文件句柄和socket正确关闭
文档注释
:使用POD格式编写文档
2. 性能优化
并发控制
:使用线程池或进程池管理并发
连接复用
:避免重复建立连接
内存管理
:及时释放不再需要的数据结构
超时设置
:为所有网络操作设置合理的超时
3. 安全考虑
输入验证
:验证所有用户输入
权限最小化
:只在必要时使用root权限
输出过滤
:防止命令注入和XSS攻击
日志记录
:记录关键操作和错误
4. 与Nmap集成
遵循格式
:输出符合Nmap标准
参数传递
:正确处理脚本参数
错误报告
:向Nmap报告有意义的错误信息
结果解析
:确保Nmap能正确解析输出
第七部分:常见错误排查清单
7.1 脚本执行错误
错误现象
可能原因
解决方案
“Permission denied”
缺少root权限
使用sudo运行脚本
“Module not found”
缺少Perl模块
cpan install Module::Name
“Connection refused”
目标端口关闭/过滤
检查目标服务状态
“Timeout”
网络延迟/防火墙
增加超时时间,检查网络
“Cannot fork”
系统资源不足
减少并发线程数
7.2 Nmap集成错误
错误现象
可能原因
解决方案
脚本不执行
规则不匹配
检查portrule/action函数
输出不显示
格式错误
确保输出符合Nmap格式
脚本崩溃
依赖问题
检查Lua包装器路径
权限错误
Nmap权限不足
以root运行Nmap
7.3 性能问题
问题现象
优化方向
具体措施
扫描速度慢
并发控制
增加线程数,优化算法
内存占用高
资源管理
及时释放数据结构
CPU占用高
算法优化
避免密集计算,使用缓存
网络拥塞
流量控制
限制并发连接数
结论
使用Perl编写Nmap脚本插件是一个强大而灵活的选择,特别适合需要复杂文本处理、系统集成或自定义扫描逻辑的场景。通过本指南,你已经学习了:
基础语法
:Perl的核心概念和Nmap脚本环境
核心开发
:Nmap脚本生命周期和输出格式
基础扫描
:端口扫描、服务识别、OS检测
高级功能
:并发扫描、安全扫描、协议枚举
错误处理
:调试技巧和性能优化
实际案例
:完整的脚本示例和最佳实践
记住,优秀的Nmap脚本应该:
可靠
:正确处理错误和边界情况
高效
:优化性能,合理使用资源
安全
:验证输入,最小化权限
可维护
:代码清晰,文档完善
继续探索和实践,你将能够创建出更加强大和专业的网络扫描工具。无论是用于安全审计、网络监控还是渗透测试,Perl与Nmap的结合都能为你提供强大的自动化能力。