Skip to content

Commit b431bf3

Browse files
committed
Land rapid7#3052 - Fix nil error in BES
2 parents 045900b + 7839305 commit b431bf3

File tree

6 files changed

+151
-43
lines changed

6 files changed

+151
-43
lines changed

data/js/network/ajax_post.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
function postInfo(path, data) {
1+
function postInfo(path, data, cb) {
22
var xmlHttp = new XMLHttpRequest();
33

44
if (xmlHttp.overrideMimeType) {
55
xmlHttp.overrideMimeType("text/plain; charset=x-user-defined");
66
}
77

8-
xmlHttp.open('POST', path, false);
8+
xmlHttp.open('POST', path, !!cb);
9+
10+
if (cb) {
11+
xmlHttp.onreadystatechange = function() {
12+
if (xmlHttp.readyState == 4) { cb.apply(this, arguments); }
13+
};
14+
}
15+
916
xmlHttp.send(data);
10-
}
17+
return xmlHttp;
18+
}

lib/msf/core/exploit/http/server.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,6 @@ def js_ajax_post
761761
@cache_ajax_post ||= Rex::Exploitation::Js::Network.ajax_post
762762
end
763763

764-
765764
#
766765
# This function takes advantage of MSTIME's CTIMEAnimationBase::put_values function that's
767766
# suitable for a no-spray technique. There should be an allocation that contains an array of

lib/msf/core/exploit/remote/browser_exploit_server.rb

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# -*- coding: binary -*-
22

33
require 'erb'
4+
require 'cgi'
5+
require 'date'
46
require 'rex/exploitation/js'
57

68
###
@@ -16,6 +18,9 @@ module Exploit::Remote::BrowserExploitServer
1618
include Msf::Exploit::Remote::HttpServer::HTML
1719
include Msf::Exploit::RopDb
1820

21+
# this must be static between runs, otherwise the older cookies will be ignored
22+
DEFAULT_COOKIE_NAME = '__ua'
23+
1924
PROXY_REQUEST_HEADER_SET = Set.new(
2025
%w{
2126
CLIENT_IP
@@ -72,10 +77,15 @@ def initialize(info={})
7277
[
7378
OptBool.new('Retries', [false, "Allow the browser to retry the module", true])
7479
], Exploit::Remote::BrowserExploitServer)
80+
81+
register_advanced_options([
82+
OptString.new('CookieName', [false, "The name of the tracking cookie", DEFAULT_COOKIE_NAME]),
83+
OptString.new('CookieExpiration', [false, "Cookie expiration in years (blank=expire on exit)"])
84+
], Exploit::Remote::BrowserExploitServer)
7585
end
7686

7787
#
78-
# Syncs a block of code
88+
# Allows a block of code to access BES resources in a thread-safe fashion
7989
#
8090
# @param block [Proc] Block of code to sync
8191
#
@@ -89,7 +99,7 @@ def sync(&block)
8999
# @return [String] URI to the exploit page
90100
#
91101
def get_module_resource
92-
"#{get_resource.chomp("/")}/#{@exploit_receiver_page}"
102+
"#{get_resource.chomp("/")}/#{@exploit_receiver_page}/"
93103
end
94104

95105
#
@@ -191,7 +201,7 @@ def get_bad_requirements(profile)
191201

192202
#
193203
# Returns the target profile based on the tag. Each profile has the following structure:
194-
# 'cookie' =>
204+
# 'cookie_name' =>
195205
# {
196206
# :os_name => 'Windows',
197207
# :os_flavor => 'something'
@@ -238,13 +248,13 @@ def update_profile(target_profile, key, value)
238248
end
239249

240250
#
241-
# Initializes a profile
251+
# Initializes a profile, if it did not previously exist
242252
#
243253
# @param tag [String] A unique string as a way to ID the profile
244254
#
245255
def init_profile(tag)
246256
sync do
247-
@target_profiles[tag] = {}
257+
@target_profiles[tag] ||= {}
248258
end
249259
end
250260

@@ -256,14 +266,18 @@ def init_profile(tag)
256266
#
257267
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
258268
#
259-
def retrieve_tag(request)
260-
tag = request.headers['Cookie'].to_s
269+
def retrieve_tag(cli, request)
270+
cookie = CGI::Cookie.parse(request.headers['Cookie'].to_s)
271+
tag = cookie.has_key?(cookie_name) && cookie[cookie_name].first
261272

262273
if tag.blank?
263274
# Browser probably doesn't allow cookies, plan B :-/
275+
vprint_status("No cookie received, resorting to headers hash.")
264276
ip = cli.peerhost
265277
os = request.headers['User-Agent']
266278
tag = Rex::Text.md5("#{ip}#{os}")
279+
else
280+
vprint_status("Received cookie '#{tag}'.")
267281
end
268282

269283
tag
@@ -277,31 +291,25 @@ def retrieve_tag(request)
277291
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
278292
#
279293
def process_browser_info(source, cli, request)
280-
tag = retrieve_tag(request)
281-
282-
# Browser doesn't allow cookies. Can't track that, use a different way to track.
283-
init_profile(tag) if request.headers['Cookie'].blank?
294+
tag = retrieve_tag(cli, request)
295+
init_profile(tag)
284296
target_info = get_profile(tag)
297+
update_profile(target_info, :source, source.to_s)
285298

286299
# Gathering target info from the detection stage
287300
case source
288301
when :script
289302
# Gathers target data from a POST request
290-
update_profile(target_info, :source, 'script')
291-
raw = Rex::Text.uri_decode(Rex::Text.decode_base64(request.body))
292-
raw.split('&').each do |item|
293-
k, v = item.scan(/(\w+)=(.*)/).flatten
294-
update_profile(target_info, k.to_sym, v)
295-
end
296-
303+
parsed_body = CGI::parse(Rex::Text.decode_base64(request.body) || '')
304+
vprint_debug("Received sniffed browser data over POST: \n#{parsed_body}.")
305+
parsed_body.each { |k, v| update_profile(target_info, k.to_sym, v.first) }
297306
when :headers
298307
# Gathers target data from headers
299308
# This may be less accurate, and most likely less info.
300309
fp = fingerprint_user_agent(request.headers['User-Agent'])
301310
# Module has all the info it needs, ua_string is kind of pointless.
302311
# Kill this to save space.
303312
fp.delete(:ua_string)
304-
update_profile(target_info, :source, 'headers')
305313
fp.each do |k, v|
306314
update_profile(target_info, k.to_sym, v)
307315
end
@@ -356,6 +364,7 @@ def get_detection_html(user_agent)
356364
return Base64.encode(q.join('&'));
357365
}
358366
367+
359368
window.onload = function() {
360369
var osInfo = window.os_detect.getVersion();
361370
var d = {
@@ -380,8 +389,9 @@ def get_detection_html(user_agent)
380389
<% end %>
381390
382391
var query = objToQuery(d);
383-
postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query);
384-
window.location = "<%=get_resource.chomp("/")%>/<%=@exploit_receiver_page%>/";
392+
postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query, function(){
393+
window.location="<%= get_module_resource %>";
394+
});
385395
}
386396
|).result(binding())
387397

@@ -394,11 +404,26 @@ def get_detection_html(user_agent)
394404
</script>
395405
<noscript>
396406
<img style="visibility:hidden" src="#{get_resource.chomp("/")}/#{@noscript_receiver_page}/">
397-
<meta http-equiv="refresh" content="1; url=#{get_resource.chomp("/")}/#{@exploit_receiver_page}/">
407+
<meta http-equiv="refresh" content="1; url=#{get_module_resource}">
398408
</noscript>
399409
|
400410
end
401411

412+
# @return [String] name of the tracking cookie
413+
def cookie_name
414+
datastore['CookieName'] || DEFAULT_COOKIE_NAME
415+
end
416+
417+
def cookie_header(tag)
418+
cookie = "#{cookie_name}=#{tag};"
419+
if datastore['CookieExpiration'].present?
420+
expires_date = (DateTime.now + 365*datastore['CookieExpiration'].to_i)
421+
expires_str = expires_date.to_time.strftime("%a, %d %b %Y 12:00:00 GMT")
422+
cookie << " Expires=#{expires};"
423+
end
424+
cookie
425+
end
426+
402427
#
403428
# Handles exploit stages.
404429
#
@@ -411,45 +436,51 @@ def on_request_uri(cli, request)
411436
#
412437
# This is the information gathering stage
413438
#
414-
if get_profile(retrieve_tag(request))
415-
send_redirect(cli, "#{get_resource.chomp("/")}/#{@exploit_receiver_page}")
439+
if get_profile(retrieve_tag(cli, request))
440+
send_redirect(cli, get_module_resource)
416441
return
417442
end
418443

419444
print_status("Gathering target information.")
420445
tag = Rex::Text.rand_text_alpha(rand(20) + 5)
421-
ua = request.headers['User-Agent']
446+
ua = request.headers['User-Agent'] || ''
422447
init_profile(tag)
423-
html = get_detection_html(ua)
424-
send_response(cli, html, {'Set-Cookie' => tag})
448+
print_status("Sending response HTML.")
449+
send_response(cli, get_detection_html(ua), {'Set-Cookie' => cookie_header(tag)})
425450

426451
when /#{@info_receiver_page}/
427452
#
428453
# The detection code will hit this if Javascript is enabled
429454
#
430-
process_browser_info(source=:script, cli, request)
431-
send_response(cli, '')
455+
vprint_status "Info receiver page called."
456+
process_browser_info(:script, cli, request)
457+
send_response(cli, '', {'Set-Cookie' => cookie_header(tag)})
432458

433459
when /#{@noscript_receiver_page}/
434460
#
435461
# The detection code will hit this instead of Javascript is disabled
436462
# Should only be triggered by the img src in <noscript>
437463
#
438-
process_browser_info(source=:headers, cli, request)
464+
process_browser_info(:headers, cli, request)
439465
send_not_found(cli)
440466

441467
when /#{@exploit_receiver_page}/
442468
#
443469
# This sends the actual exploit. A module should define its own
444470
# on_request_exploit() to get the target information
445471
#
446-
tag = retrieve_tag(request)
472+
tag = retrieve_tag(cli, request)
473+
vprint_status("Serving exploit to user with tag #{tag}")
447474
profile = get_profile(tag)
448-
if profile[:tried] and datastore['Retries'] == false
475+
if profile.nil?
476+
print_status("Browsing directly to the exploit URL is forbidden.")
477+
send_not_found(cli)
478+
elsif profile[:tried] and datastore['Retries'] == false
449479
print_status("Target with tag \"#{tag}\" wants to retry the module, not allowed.")
450480
send_not_found(cli)
451481
else
452482
update_profile(profile, :tried, true)
483+
vprint_status("Setting target \"#{tag}\" to :tried.")
453484
try_set_target(profile)
454485
bad_reqs = get_bad_requirements(profile)
455486
if bad_reqs.empty?

lib/rex/exploitation/js/network.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def self.ajax_download(opts={})
3737
# @option opts [Boolean] :obfuscate toggles js obfuscation. defaults to true.
3838
# @option opts [Boolean] :inject_xhr_shim automatically stubs XHR to use ActiveXObject when needed.
3939
# defaults to true.
40-
# @return [String] javascript code to perform a synchronous ajax request to the remote with
41-
# the data specified
40+
# @return [String] javascript code to perform a synchronous or asynchronous ajax request to
41+
# the remote with the data specified.
4242
def self.ajax_post(opts={})
4343
should_obfuscate = opts.fetch(:obfuscate, true)
4444
js = ::File.read(::File.join(Msf::Config.data_directory, "js", "network", "ajax_post.js"))
@@ -47,7 +47,7 @@ def self.ajax_post(opts={})
4747
js = ::Rex::Exploitation::ObfuscateJS.new(js,
4848
{
4949
'Symbols' => {
50-
'Variables' => %w{ xmlHttp }
50+
'Variables' => %w{ xmlHttp cb path data }
5151
}
5252
}).obfuscate
5353
end

spec/lib/msf/core/exploit/remote/browser_exploit_server_spec.rb

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
service = double("service")
1616
service.stub(:server_name=)
1717
service.stub(:add_resource)
18-
1918
service
2019
end
2120

@@ -31,6 +30,10 @@
3130
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"
3231
end
3332

33+
let(:exploit_page) do
34+
server.instance_variable_get(:@exploit_receiver_page)
35+
end
36+
3437
let(:expected_profile) do
3538
{
3639
:source=>"script",
@@ -57,9 +60,8 @@
5760

5861
describe ".get_module_resource" do
5962
it "should give me a URI to access the exploit page" do
60-
ivar_exploit_page = server.instance_variable_get(:@exploit_receiver_page)
6163
module_resource = server.get_module_resource
62-
module_resource.should match(ivar_exploit_page)
64+
module_resource.should match(exploit_page)
6365
end
6466
end
6567

@@ -221,4 +223,73 @@
221223
end
222224
end
223225

226+
describe '.on_request_uri' do
227+
let(:cli) { double(:peerhost => '0.0.0.0') }
228+
let(:cookie) { '' }
229+
let(:headers) { {'Cookie' => cookie, 'User-Agent' => ''} }
230+
let(:body) { '' }
231+
let(:cookie_name) { Msf::Exploit::Remote::BrowserExploitServer::DEFAULT_COOKIE_NAME }
232+
let(:request) do
233+
double(:body => body, :headers => headers, :uri => server.get_resource )
234+
end
235+
236+
before do
237+
server.stub(:send_redirect)
238+
server.stub(:send_response)
239+
server.stub(:send_not_found)
240+
end
241+
242+
context 'when a new visitor requests the exploit' do
243+
it 'calls send_response once' do
244+
server.should_receive(:send_response).once
245+
server.on_request_uri(cli, request)
246+
end
247+
248+
it 'serves the os.js detection script' do
249+
server.should_receive(:send_response) do |cli, html, headers|
250+
expect(html).to include('window.os_detect')
251+
end
252+
server.on_request_uri(cli, request)
253+
end
254+
end
255+
256+
context 'when a returning visitor requests the exploit' do
257+
let(:body) { '' }
258+
let(:tag) { 'joe' }
259+
let(:cookie) { "#{cookie_name}=#{tag}" }
260+
261+
before { server.init_profile(tag) }
262+
263+
it 'calls send_redirect once' do
264+
server.should_receive(:send_redirect).once
265+
server.on_request_uri(cli, request)
266+
end
267+
268+
it 'redirects to the exploit URL' do
269+
server.should_receive(:send_redirect) do |cli, url|
270+
expect(url).to end_with("#{exploit_page}/")
271+
end
272+
server.on_request_uri(cli, request)
273+
end
274+
end
275+
276+
context 'when a returning visitor from a previous msf run requests the exploit' do
277+
let(:body) { '' }
278+
let(:tag) { 'joe' }
279+
let(:cookie) { "#{cookie_name}=#{tag}" }
280+
281+
it 'calls send_response once' do
282+
server.should_receive(:send_response).once
283+
server.on_request_uri(cli, request)
284+
end
285+
286+
it 'serves the os.js detection script' do
287+
server.should_receive(:send_response) do |cli, html, headers|
288+
expect(html).to include('window.os_detect')
289+
end
290+
server.on_request_uri(cli, request)
291+
end
292+
end
293+
end
294+
224295
end

0 commit comments

Comments
 (0)