一、问题:为什么我的自定义脚本就是跑不起来

大家用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/下,绝对不要去动系统目录。

第二,顶层就必须有自己独立的命名空间文件夹,哪怕麻烦点,但是值得。

第三,脚本的文件名要尽可能具体,杜绝使用类似于scannerenumexploit这种太通用通用的名字。

第四,改完脚本或者新加脚本后,如果不想重启msfconsole,务必执行reload_all或者db_rebuild_cache

第五,模块加载成功后,养成执行info查看信息的习惯,确认确实加载的是自己的东西。

只有这样,你才能在这个强大的框架里,既享受它自带的插件生态,又能把你的定制化需求,无缝地挂在它身上,让它为你所用。武装到牙齿,先从看清目录开始。