一、问题:为什么我的自定义脚本就是跑不起来
大家用Metasploit的时候,多少都遇到过这种情况。自己辛辛苦苦写了个脚本,或者从网上找了个现成的模块,往modules目录里一丢,然后美滋滋地等着它生效。结果呢?要么是敲命令的时候提示找不到,要么是明明文件就在那儿,程序却跟瞎了一样看不见它,更有甚者,运行了半天才发现执行的是另一个同名但内容完全不同的东西。这种挫败感,玩过渗透的朋友肯定都懂。
我最后一次被这个事儿折磨,是在一个周末的下午。我当时需要针对一个内部系统写一个自定义的辅助模块,用来探测某个特定服务的弱口令。我按照老习惯,把脚本放到了modules/auxiliary/scanner/下面一个自己建的分类文件夹里。为了图方便,我给脚本命名成了ssh_enum.rb。因为这个内部测试网络里确实也有SSH服务,我想着顺便一起测了。
结果,运行use auxiliary/scanner/my_kits/ssh_enum的时候,系统提示找不到。我检查了路径,没问题啊。我又试了试直接运行use auxiliary/scanner/ssh_enum,结果加载出来了一个Metasploit自带的那个老版本SSH枚举脚本。我当时就意识到,这不仅仅是"没生效"的问题,而是我的脚本被"淹没"了,这背后牵扯到的,就是Metasploit那个让人又爱又恨的modules目录结构、路径加载顺序,还有命名冲突的问题。
这篇文章,不聊那些虚头巴脑的理论,就实打实地聊聊我是怎么排查、怎么修复的,以及最后总结出来的一套可以规避大部分类似坑的心法。
二、剖析:Metasploit的modules目录结构到底怎么回事
2.1 基础目录结构
要解决这个问题,首先得知道Metasploit的模块目录是怎么组织的。以我们最常用的Kali Linux为例,你可以把Metasploit的模块目录想象成一个大仓库,里面分了不同的区域:
# 这是Metasploit框架自带的模块目录(通常在Kali系统里是这个路径)
/usr/share/metasploit-framework/modules/
# 这是用户自定义模块的目录(注意:这个目录通常需要你手动创建)
~/.msf4/modules/
系统自带的目录就不多说了,重点说后面那个~/.msf4/modules/。这个目录是留给用户放自定义脚本的。因为Metasploit升级很频繁,如果大家都往系统目录里塞东西,一升级全给你覆盖掉。所以它专门留了这个口子。
这里有一个新手最容易踩的坑:很多人不知道,Metasploit在加载自定义模块的时候,用户目录的优先级是要高于系统目录的。这个优先级,既是福音,也是灾难。说他是福音,因为你可以随意覆盖官方脚本的功能;说他是灾难,是因为如果你目录结构建错了,或者命名有歧义,表面上看着没报错,实际上系统加载的还是旧东西。
我那次问题的根源,就在这个结构上。
三、深入:路径加载顺序是罪魁祸首
3.1 加载顺序的真相
Metasploit启动的时候,会扫描一系列目录来收集可用的模块。它不是只扫描一个目录就完事了,而是按照一个特定的优先级顺序去扫。
大致逻辑是这样的(用伪代码表示理解一下):
加载流程模拟:
1. 先扫描 系统自带模块目录
2. 再扫描 用户自定义模块目录(~/.msf4/modules/)
3. 将两者数据进行合并
4. 如果发现同名同路径的模块,用户目录覆盖系统目录
看到没,问题就出在第4步。如果我发现系统自带的目录里有一个auxiliary/scanner/ssh_enum,而你的用户目录里恰好也有一个auxiliary/scanner/ssh_enum,系统会加载用户目录这个,用来替代系统自带那个。
但是我犯了一个错误:我不小心把目录层级搞错了。我把脚本放到了~/.msf4/modules/auxiliary/scanner/my_kits/下,但我输入命令的时候用的是auxiliary/scanner/ssh_enum,并没有加my_kits这个二级目录。
Metasploit会严格匹配模块的路径与相对路径。它找遍了整个模块库,发现没有auxiliary/scanner/ssh_enum(因为我的在my_kits子目录下),只能去系统自带目录里找,结果找到了官方的老版本脚本。所以我的不生效,是因为路径加载顺序导致的"覆盖"失败,最终执行了官方脚本,而不是我的脚本。
四、严重:命名冲突带来的隐蔽陷阱
4.1 一个典型例子
命名冲突比路径加载顺序更隐蔽。因为我当时那个脚本名字起得"太大众"了——ssh_enum。这个文件在Metasploit框架里已经存在了,且不止一次出现。
我们来模拟一个具体情况,假设我们要写一个扫描Redis未授权访问的辅助模块,但你的命名和官方插件效果类似,而且模块类名(Module Name)也容易撞车。
# 技术栈:Ruby(Metasploit模块开发)
# 文件名:redis_unauth.rb
# 存放路径:~/.msf4/modules/auxiliary/scanner/redis/redis_unauth.rb
# 引入MSF基础库
require 'msf/core'
class MetasploitModule < Msf::Auxiliary
# 注意类名,这里如果不小心和官方模块类名一样,会引发命名冲突!
Rank = ExcellentRanking
# 初始化方法,定义模块基本信息
def initialize(info = {})
super(update_info(info,
'Name' => 'Redis Unauthorized Access Scanner',
'Description' => '扫描内网中配置不当的Redis服务',
'License' => MSF_LICENSE,
'Author' => ['Your Name'],
'References' => [['URL', 'http://example.com']],
'DisclosureDate' => '2024-01-01'
))
# 定义注册表选项,也就是我们要传入的参数
register_options(
[
Opt::RHOSTS, # 目标地址范围
Opt::RPORT(6379, '目标Redis端口'), # 目标端口,默认6379
OptInt.new('TIMEOUT', [true, '连接超时时间(秒)', 10]) # 自定义超时参数
], self.class
)
end
# 运行函数,核心逻辑
def run
# 获取我们之前在选项中定义的主机列表
rhosts = datastore['RHOSTS'] || '127.0.0.1'
rport = datastore['RPORT'] || 6379
timeout = datastore['TIMEOUT'] || 10
# 遍历所有目标主机
Rex::Socket::RangeWalker.new(rhosts).each do |ip|
begin
print_status("正在尝试连接 #{ip}:#{rport} ...")
# 建立TCP连接
sock = Rex::Socket::Tcp.create(
'PeerHost' => ip,
'PeerPort' => rport.to_i,
'Timeout' => timeout
)
# 如果连接成功,发送Redis的PING命令
sock.put("PING\r\n")
resp = sock.get_once
# 如果返回+PONG,说明不需要认证即可访问
if resp && resp.include?('+PONG')
print_good(" [!] 发现未授权访问 -> #{ip}:#{rport} 返回: #{resp.strip}")
# 这里可以加更多利用代码,例如写入crontab等
else
vprint_status(" [-] 目标需要认证或无法访问,返回: #{resp.inspect}")
end
# 关闭连接
sock.close
rescue => e
# 打印错误信息,便于调试
vprint_error(" [!] 连接失败: #{e.class}: #{e.message}")
ensure
# 确保不留残余连接
begin
sock.close if sock && !sock.closed?
rescue NoMethodError
end
end
end
end
end
这个代码看起来没问题吧?类名是MetasploitModule,这在Metasploit Ruby模块里是固定的入口类名,冲突点不在这里,而在于文件名和模块的'Name'字段以及文件位置。
如果你的文件名和自带的redis_unauth一样就算了,问题不大。但如果你的文件名叫redis_scanner.rb,而系统自带一个更具体的redis_scanner,系统在加载时,会根据文件名和路径生成一个内部标识符,这个标识符会被用来在use命令中定位模块。如果文件名一样,但路径层级不同,就会导致你无法通过use命令精确加载你的脚本,或者被系统目录的同名文件拦截。
五、实战复盘:一次完整的排错与修复
5.1 场景设定
我们继续沿着上面的问题走。现在假设我写了一个模块,功能更强大,我不想让它跟Kali默认的冲突,也不想去覆盖系统目录里的东西。我需要做的就是重建自定义目录结构。
我的需求是:新增一个模块,用来检测目标机器是否开启了脆弱的网络打印服务。我把它命名为printer_spooler_detect.rb。
5.2 修复步骤详解
首先,我不再乱放乱命名了。我按照一套严格的规则来。
步骤一:整理目录
# 技术栈:Shell(终端命令)
# 第一步:创建用户自定义模块根目录(如果系统没有的话)
mkdir -p ~/.msf4/modules/
# 第二步:在自定义目录下创建与系统模块顶层目录一致的镜像结构
# auxiliary 代表这是辅助模块,scanner 代表扫描器类型
mkdir -p ~/.msf4/modules/auxiliary/scanner/printer/
# 第三步:把脚本复制进去,注意命名一定要具有极高的辨识度
# 不要用 printer,不要用 detect,这些太容易撞车了
# 建议使用 公司名/项目名缩写加具体功能
cp ~/my_work/printer_spooler_detect.rb ~/.msf4/modules/auxiliary/scanner/printer/printer_spooler_detect.rb
步骤二:修正脚本内部的类名与注册信息
这里需要特别注意,脚本的Name字段虽然不影响加载逻辑,但影响展示,也要改得明确一点。
# 技术栈:Ruby(Metasploit模块开发)
# 文件名:printer_spooler_detect.rb
# 路径:~/.msf4/modules/auxiliary/scanner/printer/printer_spooler_detect.rb
require 'msf/core'
# 注意:这里类名依然是MetasploitModule,这是固定格式,但是文件名决定了use后的路径
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
def initialize(info = {})
super(update_info(info,
# Name字段用来在info里展示,也必须在init里修改成独特名称
'Name' => 'Internal Printer Spooler Remote Detection',
'Description' => '针对内部网络打印机老旧固件进行漏洞探测,规避与常规SSH等模块冲突',
'Author' => ['Security_Team_Internal'],
'License' => MSF_LICENSE
))
register_options(
[
Opt::RHOSTS,
Opt::RPORT(9100, '打印机端口')
], self.class
)
end
def run_host(ip)
begin
connect
# 打印机通常使用9100端口,发送PJL语言指令
sock.put("@PJL INFO ID\r\n")
response = sock.get_once(-1, 5) # 超时5秒
if response && response.include?('PJL')
print_good("#{ip} 存在打印机服务,且响应PJL指令,可以尝试进一步利用。")
# 此处可以泄露内存信息或者执行更多PJL命令
else
vprint_status("#{ip} 没有响应PJL指令,可能是其它服务。")
end
rescue Rex::ConnectionError
vprint_error("#{ip} 连接失败,端口可能未开放。")
ensure
disconnect
end
end
end
步骤三:最关键的一步——清空Metasploit的缓存并重新加载
这一步很容易被忽略。Metasploit有一个模块缓存机制,如果你不是重启msfconsole,它可能无法发现你新加的模块。
# 技术栈:Shell(终端命令)
# 在msfconsole里执行以下命令(不是Linux终端)
# 清除缓存
msf6 > reload_all
# 或者,如果你确定自己的路径没问题,但是还是加载不出来
# 可以执行这个命令重建缓存
msf6 > db_rebuild_cache
这里需要强调一下这两个命令的区别。reload_all是重新扫描所有模块路径,并重新加载没有加载过的模块。但如果你曾经有过同名的错误模块,缓存里可能还有残留数据,此时db_rebuild_cache更彻底。
六、棘手:命名冲突和路径加载顺序的高级避坑技巧
6.1 应用场景
在红队行动或者大规模内网渗透测试项目中,我们经常需要批量部署自研脚本。如果团队有5个人,每个人写一个模块都叫ssh_brute,放在不同的目录层级,那到时候加载谁,完全看运气了。
为了避免这种恶性的命名空间污染,我总结了一个"黄金法则":在自定义模块的路径中,强制加入一个最高级的、专属于你个人或团队的命名空间目录,这个目录是系统绝对没有的。
例如,假设我的团队叫FLASHPOINT,我就这么建目录:
# 技术栈:Shell(终端命令)
# 创建团队专属的一级目录,最外层加一个团队名,彻底隔开
mkdir -p ~/.msf4/modules/auxiliary/scanner/fp_toolkit/
# 然后脚本依然按照对应类型放
# 这样模块加载路径就是(理论上的完整名称)
# use auxiliary/scanner/fp_toolkit/printer_spooler_detect
这样做的最大好处是:永远不会和系统模块发生冲突。因为Metasploit官方模块不可能有一个叫fp_toolkit的目录。就算你的团队成员之间撞车,也只能在fp_toolkit内部撞,故障半径大大缩小。
6.2 技术优缺点分析
这种修复方式的优点:
第一,结构清晰。一眼就能看出哪些是自定义的模块,哪些是官方自带的,方便管理。
第二,升级安全。Metasploit框架升级的时候,只会去覆盖它自己的目录,不会动你的自定义目录,所以你的代码永远不会丢。
第三,加载优先级明确。只要路径匹配,系统会优先生效,这样你可以在不改动官方任何文件的前提下,优雅地修复官方模块的Bug或者给它打补丁。
这种方式的缺点:
第一,需要手动维护目录结构,比较麻烦。如果团队协作,很容易出现有人偷懒直接丢根目录的情况。
第二,如果模块更新频繁,多个目录之间同步会变得繁琐。
七、避坑指南:几个需要注意的细节
7.1 不要忽略权限问题
不要以为放到用户目录就万事大吉了。有时候,因为反病毒软件或者系统安全策略,会对~/.msf4/modules/目录做监控。如果你新加的文件丢失了,或者加载后无法读取,可以去看看是不是被安全软件隔离了。
7.2 注意模块扩展名和编码
在Windows上编辑脚本再传到Linux服务器,经常因为换行符问题导致Ruby脚本语法错误。在Linux下需要转换一下:
# 技术栈:Shell(终端命令)
# 将Windows换行符转成Unix换行符
sed -i 's/\r$//' ~/.msf4/modules/auxiliary/scanner/fp_toolkit/printer_spooler_detect.rb
如果脚本里有中文字符,记得在文件第二行加入# encoding: UTF-8,否则Metasploit的Ruby解释器可能会在注释上直接报错。
7.3 善用 info 命令验证
当你终于用use命令加载成功后,不要急着run。先打一个info看看:
# 技术栈:Shell(终端命令)
# 在msfconsole中,假设你已经执行了
msf6 > use auxiliary/scanner/fp_toolkit/printer_spooler_detect
# 查看模块详情
msf6 auxiliary(scanner/fp_toolkit/printer_spooler_detect) > info
# 观察输出信息里,一定要看到你的路径信息,且没有指向系统目录
# 如果展示的还是系统自带模块的Name,说明又触发覆盖BUG了。
7.4 不要轻易使用 loadpath 命令
很多人遇到自定义模块不生效,会在msfconsole里用loadpath命令去强行加载某个路径。比如:
# 技术栈:Shell(终端命令)
msf6 > loadpath /usr/share/metasploit-framework/modules/
这条命令在旧版本里很有用,但在新版本里,如果用它强行加载了一个与系统目录同级的路径,极大概率会引发重复加载错误,导致报出大量类似于Module already defined的警告,让你误以为自己的脚本出错了。官方推荐的方式永远是重启msfconsole,或者使用reload_all。
八、总结与心得
这次排错过程,给了我很大的启发。Metasploit的模块目录结构,与其说是一个技术缺陷,不如说它是一套约定大于配置的体系。它在加载顺序上给了用户很高的自由度,但自由是需要代价的,代价就是你得深刻理解它的扫描逻辑。
很多新手在遇到"自定义脚本不生效"时,第一反应永远是去改代码逻辑,觉得是不是自己Ruby语法写错了。但实际上,99%的情况下,问题都出在路由和文件摆放上,而不是脚本本身。
我现在复盘,总结了几条核心经验:
第一,永远把自定义模块放在~/.msf4/modules/下,绝对不要去动系统目录。
第二,顶层就必须有自己独立的命名空间文件夹,哪怕麻烦点,但是值得。
第三,脚本的文件名要尽可能具体,杜绝使用类似于scanner、enum、exploit这种太通用通用的名字。
第四,改完脚本或者新加脚本后,如果不想重启msfconsole,务必执行reload_all或者db_rebuild_cache。
第五,模块加载成功后,养成执行info查看信息的习惯,确认确实加载的是自己的东西。
只有这样,你才能在这个强大的框架里,既享受它自带的插件生态,又能把你的定制化需求,无缝地挂在它身上,让它为你所用。武装到牙齿,先从看清目录开始。
评论
围绕“Metasploit的modules目录结构混乱导致自定义脚本不生效,路径加载顺序与命名冲突的修复经验谈”参与讨论